diff --git a/Marlin/Configuration.h b/Marlin/Configuration.h
index d3221f80de..3b3087e46f 100644
--- a/Marlin/Configuration.h
+++ b/Marlin/Configuration.h
@@ -35,7 +35,7 @@
*
* Advanced settings can be found in Configuration_adv.h
*/
-#define CONFIGURATION_H_VERSION 02000900
+#define CONFIGURATION_H_VERSION 02000901
//===========================================================================
//============================= Getting Started =============================
@@ -1700,15 +1700,13 @@
//#define MANUAL_J_HOME_POS 0
//#define MANUAL_K_HOME_POS 0
-// Use "Z Safe Homing" to avoid homing with a Z probe outside the bed area.
-//
-// With this feature enabled:
-//
-// - Allow Z homing only after X and Y homing AND stepper drivers still enabled.
-// - If stepper drivers time out, it will need X and Y homing again before Z homing.
-// - Move the Z probe (or nozzle) to a defined XY point before Z Homing.
-// - Prevent Z homing when the Z probe is outside bed area.
-//
+/**
+ * Use "Z Safe Homing" to avoid homing with a Z probe outside the bed area.
+ *
+ * - Moves the Z probe (or nozzle) to a defined XY point before Z homing.
+ * - Allows Z homing only when XY positions are known and trusted.
+ * - If stepper drivers sleep, XY homing may be required again before Z homing.
+ */
//#define Z_SAFE_HOMING
#if ENABLED(Z_SAFE_HOMING)
diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h
index a13df514b0..818edecea2 100644
--- a/Marlin/Configuration_adv.h
+++ b/Marlin/Configuration_adv.h
@@ -30,7 +30,7 @@
*
* Basic settings can be found in Configuration.h
*/
-#define CONFIGURATION_ADV_H_VERSION 02000900
+#define CONFIGURATION_ADV_H_VERSION 02000901
//===========================================================================
//============================= Thermal Settings ============================
diff --git a/Marlin/src/MarlinCore.cpp b/Marlin/src/MarlinCore.cpp
index f77f3b2f0b..254283da5b 100644
--- a/Marlin/src/MarlinCore.cpp
+++ b/Marlin/src/MarlinCore.cpp
@@ -617,7 +617,7 @@ inline void manage_inactivity(const bool no_stepper_sleep=false) {
TERN_(USE_CONTROLLER_FAN, controllerFan.update()); // Check if fan should be turned on to cool stepper drivers down
- TERN_(AUTO_POWER_CONTROL, powerManager.check());
+ TERN_(AUTO_POWER_CONTROL, powerManager.check(!ui.on_status_screen() || printJobOngoing() || printingIsPaused()));
TERN_(HOTEND_IDLE_TIMEOUT, hotend_idle.check());
diff --git a/Marlin/src/MarlinCore.h b/Marlin/src/MarlinCore.h
index 01a1be4d59..243811d7fb 100644
--- a/Marlin/src/MarlinCore.h
+++ b/Marlin/src/MarlinCore.h
@@ -91,7 +91,11 @@ extern bool wait_for_heatup;
#define PSU_OFF_SOON() powerManager.power_off_soon()
#else
#define PSU_ON() PSU_PIN_ON()
- #define PSU_OFF() PSU_PIN_OFF()
+ #if ENABLED(PS_OFF_SOUND)
+ #define PSU_OFF() do{ BUZZ(1000, 659); PSU_PIN_OFF(); }while(0)
+ #else
+ #define PSU_OFF() PSU_PIN_OFF()
+ #endif
#define PSU_OFF_SOON PSU_OFF
#endif
#endif
diff --git a/Marlin/src/feature/power.cpp b/Marlin/src/feature/power.cpp
index 519b2308d0..9b173d6ee7 100644
--- a/Marlin/src/feature/power.cpp
+++ b/Marlin/src/feature/power.cpp
@@ -50,6 +50,9 @@ Power powerManager;
millis_t Power::lastPowerOn;
bool Power::is_power_needed() {
+
+ if (printJobOngoing() || printingIsPaused()) return true;
+
#if ENABLED(AUTO_POWER_FANS)
FANS_LOOP(i) if (thermalManager.fan_speed[i]) return true;
#endif
@@ -110,9 +113,17 @@ bool Power::is_power_needed() {
#define POWER_TIMEOUT 0
#endif
-void Power::check() {
+void Power::check(const bool pause) {
+ static bool _pause = false;
static millis_t nextPowerCheck = 0;
- millis_t now = millis();
+ const millis_t now = millis();
+ #if POWER_TIMEOUT > 0
+ if (pause != _pause) {
+ lastPowerOn = now + !now;
+ _pause = pause;
+ }
+ if (pause) return;
+ #endif
if (ELAPSED(now, nextPowerCheck)) {
nextPowerCheck = now + 2500UL;
if (is_power_needed())
@@ -123,7 +134,8 @@ void Power::check() {
}
void Power::power_on() {
- lastPowerOn = millis();
+ const millis_t now = millis();
+ lastPowerOn = now + !now;
if (!powersupply_on) {
PSU_PIN_ON();
safe_delay(PSU_POWERUP_DELAY);
@@ -152,6 +164,7 @@ void Power::power_off() {
void Power::power_off_soon() {
#if POWER_OFF_DELAY
lastPowerOn = millis() - SEC_TO_MS(POWER_TIMEOUT) + SEC_TO_MS(POWER_OFF_DELAY);
+ //if (!lastPowerOn) ++lastPowerOn;
#else
power_off();
#endif
diff --git a/Marlin/src/feature/power.h b/Marlin/src/feature/power.h
index 2462b9231b..bca5432946 100644
--- a/Marlin/src/feature/power.h
+++ b/Marlin/src/feature/power.h
@@ -29,7 +29,7 @@
class Power {
public:
- static void check();
+ static void check(const bool pause);
static void power_on();
static void power_off();
static void power_off_soon();
diff --git a/Marlin/src/inc/SanityCheck.h b/Marlin/src/inc/SanityCheck.h
index 224729d35f..36ccbee9b9 100644
--- a/Marlin/src/inc/SanityCheck.h
+++ b/Marlin/src/inc/SanityCheck.h
@@ -593,24 +593,29 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L
#endif
#ifdef PTC_SAMPLE_START
- constexpr int _ptc_sample_start = PTC_SAMPLE_START;
- static_assert(_test_ptc_sample_start != PTC_SAMPLE_START, "PTC_SAMPLE_START must be a whole number.");
+ constexpr auto _ptc_sample_start = PTC_SAMPLE_START;
+ constexpr decltype(_ptc_sample_start) _test_ptc_sample_start = 12.3f;
+ static_assert(_test_ptc_sample_start != 12.3f, "PTC_SAMPLE_START must be a whole number.");
#endif
#ifdef PTC_SAMPLE_RES
- constexpr int _ptc_sample_res = PTC_SAMPLE_END;
- static_assert(_test_ptc_sample_res != PTC_SAMPLE_END, "PTC_SAMPLE_RES must be a whole number.");
+ constexpr auto _ptc_sample_res = PTC_SAMPLE_RES;
+ constexpr decltype(_ptc_sample_res) _test_ptc_sample_res = 12.3f;
+ static_assert(_test_ptc_sample_res != 12.3f, "PTC_SAMPLE_RES must be a whole number.");
#endif
#ifdef BTC_SAMPLE_START
- constexpr int _btc_sample_start = BTC_SAMPLE_START;
- static_assert(_test_btc_sample_start != BTC_SAMPLE_START, "BTC_SAMPLE_START must be a whole number.");
+ constexpr auto _btc_sample_start = BTC_SAMPLE_START;
+ constexpr decltype(_btc_sample_start) _test_btc_sample_start = 12.3f;
+ static_assert(_test_btc_sample_start != 12.3f, "BTC_SAMPLE_START must be a whole number.");
#endif
#ifdef BTC_SAMPLE_RES
- constexpr int _btc_sample_res = BTC_SAMPLE_END;
- static_assert(_test_btc_sample_res != BTC_SAMPLE_END, "BTC_SAMPLE_RES must be a whole number.");
+ constexpr _btc_sample_res = BTC_SAMPLE_RES;
+ constexpr decltype(_btc_sample_res) _test_btc_sample_res = 12.3f;
+ static_assert(_test_btc_sample_res != 12.3f, "BTC_SAMPLE_RES must be a whole number.");
#endif
#ifdef BTC_PROBE_TEMP
- constexpr int _btc_probe_temp = BTC_PROBE_TEMP;
- static_assert(_test_btc_probe_temp != BTC_PROBE_TEMP, "BTC_PROBE_TEMP must be a whole number.");
+ constexpr auto _btc_probe_temp = BTC_PROBE_TEMP;
+ constexpr decltype(_btc_probe_temp) _test_btc_probe_temp = 12.3f;
+ static_assert(_test_btc_probe_temp != 12.3f, "BTC_PROBE_TEMP must be a whole number.");
#endif
#endif
@@ -3583,6 +3588,13 @@ static_assert( _ARR_TEST(3,0) && _ARR_TEST(3,1) && _ARR_TEST(3,2)
#error "Either enable MEATPACK_ON_SERIAL_PORT_* or BINARY_FILE_TRANSFER, not both."
#endif
+/**
+ * Sanity Check for Slim LCD Menus and Probe Offset Wizard
+ */
+#if BOTH(SLIM_LCD_MENUS, PROBE_OFFSET_WIZARD)
+ #error "SLIM_LCD_MENUS disables \"Advanced Settings > Probe Offsets > PROBE_OFFSET_WIZARD.\""
+#endif
+
/**
* Sanity check for unique start and stop values in NOZZLE_CLEAN_FEATURE
*/
diff --git a/Marlin/src/inc/Version.h b/Marlin/src/inc/Version.h
index 77330b82e2..5fdcc7c728 100644
--- a/Marlin/src/inc/Version.h
+++ b/Marlin/src/inc/Version.h
@@ -42,7 +42,7 @@
* version was tagged.
*/
#ifndef STRING_DISTRIBUTION_DATE
- #define STRING_DISTRIBUTION_DATE "2021-06-28"
+ #define STRING_DISTRIBUTION_DATE "2021-07-05"
#endif
/**
@@ -52,7 +52,7 @@
* to alert users to major changes.
*/
-#define MARLIN_HEX_VERSION 02000900
+#define MARLIN_HEX_VERSION 02000901
#ifndef REQUIRED_CONFIGURATION_H_VERSION
#define REQUIRED_CONFIGURATION_H_VERSION MARLIN_HEX_VERSION
#endif
diff --git a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/archim2-flash/flash_storage.cpp b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/archim2-flash/flash_storage.cpp
index a64c237fa1..1d4711c0e2 100644
--- a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/archim2-flash/flash_storage.cpp
+++ b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/archim2-flash/flash_storage.cpp
@@ -20,7 +20,7 @@
* location: . *
****************************************************************************/
-#include "../compat.h"
+#include "../config.h"
#if ENABLED(TOUCH_UI_FTDI_EVE)
diff --git a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/archim2-flash/media_file_reader.cpp b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/archim2-flash/media_file_reader.cpp
index ea8d403753..b4165a742a 100644
--- a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/archim2-flash/media_file_reader.cpp
+++ b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/archim2-flash/media_file_reader.cpp
@@ -20,7 +20,7 @@
* location: . *
****************************************************************************/
-#include "../compat.h"
+#include "../config.h"
#if ENABLED(TOUCH_UI_FTDI_EVE)
#include "media_file_reader.h"
diff --git a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/config.h b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/config.h
index 76b231536b..05e19b20e4 100644
--- a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/config.h
+++ b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/config.h
@@ -21,6 +21,10 @@
#pragma once
+// Configure this display with options in Configuration_adv.h
+#include "../../../inc/MarlinConfigPre.h"
+#if ENABLED(TOUCH_UI_FTDI_EVE)
+
#include "compat.h"
-// Configure this display with options in Configuration_adv.h
+#endif
diff --git a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/screens.h b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/screens.h
index f118303d21..4fe4cb938a 100644
--- a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/screens.h
+++ b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/screens.h
@@ -22,7 +22,7 @@
#pragma once
-#include "compat.h"
+#include "config.h"
#if ENABLED(TOUCH_UI_FTDI_EVE)
diff --git a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/theme/sounds.cpp b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/theme/sounds.cpp
index afbed0cab1..1ed712ded1 100644
--- a/Marlin/src/lcd/extui/ftdi_eve_touch_ui/theme/sounds.cpp
+++ b/Marlin/src/lcd/extui/ftdi_eve_touch_ui/theme/sounds.cpp
@@ -20,7 +20,7 @@
* location: . *
****************************************************************************/
-#include "../compat.h"
+#include "../config.h"
#if ENABLED(TOUCH_UI_FTDI_EVE)
diff --git a/Marlin/src/lcd/marlinui.cpp b/Marlin/src/lcd/marlinui.cpp
index 67e5adc0f0..6741de3e40 100644
--- a/Marlin/src/lcd/marlinui.cpp
+++ b/Marlin/src/lcd/marlinui.cpp
@@ -120,1169 +120,1169 @@ constexpr uint8_t epps = ENCODER_PULSES_PER_STEP;
}
#endif
-#if HAS_WIRED_LCD
-
-#if HAS_MARLINUI_U8GLIB
- #include "dogm/marlinui_DOGM.h"
-#endif
-
-#include "lcdprint.h"
-
-#include "../sd/cardreader.h"
-
-#include "../module/temperature.h"
-#include "../module/planner.h"
-#include "../module/motion.h"
-
-#if HAS_LCD_MENU
- #include "../module/settings.h"
-#endif
-
-#if ENABLED(AUTO_BED_LEVELING_UBL)
- #include "../feature/bedlevel/bedlevel.h"
-#endif
-
-#if HAS_TRINAMIC_CONFIG
- #include "../feature/tmc_util.h"
-#endif
-
-#if HAS_ADC_BUTTONS
- #include "../module/thermistor/thermistors.h"
-#endif
-
-#if HAS_POWER_MONITOR
- #include "../feature/power_monitor.h"
-#endif
-
-#if HAS_ENCODER_ACTION
- volatile uint8_t MarlinUI::buttons;
- #if HAS_SLOW_BUTTONS
- volatile uint8_t MarlinUI::slow_buttons;
- #endif
- #if HAS_TOUCH_BUTTONS
- #include "touch/touch_buttons.h"
- bool MarlinUI::on_edit_screen = false;
- #endif
-#endif
-
-#if SCREENS_CAN_TIME_OUT
- bool MarlinUI::defer_return_to_status;
- millis_t MarlinUI::return_to_status_ms = 0;
-#endif
-
-uint8_t MarlinUI::lcd_status_update_delay = 1; // First update one loop delayed
-
-#if BOTH(FILAMENT_LCD_DISPLAY, SDSUPPORT)
- millis_t MarlinUI::next_filament_display; // = 0
-#endif
-
-millis_t MarlinUI::next_button_update_ms; // = 0
-
-#if HAS_MARLINUI_U8GLIB
- bool MarlinUI::drawing_screen, MarlinUI::first_page; // = false
-#endif
-
-// Encoder Handling
-#if HAS_ENCODER_ACTION
- uint32_t MarlinUI::encoderPosition;
- volatile int8_t encoderDiff; // Updated in update_buttons, added to encoderPosition every LCD update
-#endif
-
-#if ENABLED(SDSUPPORT)
-
- #include "../sd/cardreader.h"
-
- #if MARLINUI_SCROLL_NAME
- uint8_t MarlinUI::filename_scroll_pos, MarlinUI::filename_scroll_max;
- #endif
-
- const char * MarlinUI::scrolled_filename(CardReader &theCard, const uint8_t maxlen, uint8_t hash, const bool doScroll) {
- const char *outstr = theCard.longest_filename();
- if (theCard.longFilename[0]) {
- #if MARLINUI_SCROLL_NAME
- if (doScroll) {
- for (uint8_t l = FILENAME_LENGTH; l--;)
- hash = ((hash << 1) | (hash >> 7)) ^ theCard.filename[l]; // rotate, xor
- static uint8_t filename_scroll_hash;
- if (filename_scroll_hash != hash) { // If the hash changed...
- filename_scroll_hash = hash; // Save the new hash
- filename_scroll_max = _MAX(0, utf8_strlen(theCard.longFilename) - maxlen); // Update the scroll limit
- filename_scroll_pos = 0; // Reset scroll to the start
- lcd_status_update_delay = 8; // Don't scroll right away
- }
- // Advance byte position corresponding to filename_scroll_pos char position
- outstr += TERN(UTF_FILENAME_SUPPORT, utf8_byte_pos_by_char_num(outstr, filename_scroll_pos), filename_scroll_pos);
- }
- #else
- theCard.longFilename[
- TERN(UTF_FILENAME_SUPPORT, utf8_byte_pos_by_char_num(theCard.longFilename, maxlen), maxlen)
- ] = '\0'; // cutoff at screen edge
- #endif
- }
- return outstr;
- }
-
-#endif
-
#if EITHER(HAS_LCD_MENU, EXTENSIBLE_UI)
bool MarlinUI::lcd_clicked;
#endif
-#if HAS_LCD_MENU
- #include "menu/menu.h"
+#if HAS_WIRED_LCD
- screenFunc_t MarlinUI::currentScreen; // Initialized in CTOR
- bool MarlinUI::screen_changed;
-
- #if ENABLED(ENCODER_RATE_MULTIPLIER)
- bool MarlinUI::encoderRateMultiplierEnabled;
- millis_t MarlinUI::lastEncoderMovementMillis = 0;
- void MarlinUI::enable_encoder_multiplier(const bool onoff) {
- encoderRateMultiplierEnabled = onoff;
- lastEncoderMovementMillis = 0;
- }
+ #if HAS_MARLINUI_U8GLIB
+ #include "dogm/marlinui_DOGM.h"
#endif
- #if EITHER(REVERSE_MENU_DIRECTION, REVERSE_SELECT_DIRECTION)
- int8_t MarlinUI::encoderDirection = ENCODERBASE;
+ #include "lcdprint.h"
+
+ #include "../sd/cardreader.h"
+
+ #include "../module/temperature.h"
+ #include "../module/planner.h"
+ #include "../module/motion.h"
+
+ #if HAS_LCD_MENU
+ #include "../module/settings.h"
#endif
- #if HAS_TOUCH_BUTTONS
- uint8_t MarlinUI::touch_buttons;
- uint8_t MarlinUI::repeat_delay;
+ #if ENABLED(AUTO_BED_LEVELING_UBL)
+ #include "../feature/bedlevel/bedlevel.h"
#endif
- #if EITHER(AUTO_BED_LEVELING_UBL, G26_MESH_VALIDATION)
-
- bool MarlinUI::external_control; // = false
-
- void MarlinUI::wait_for_release() {
- while (button_pressed()) safe_delay(50);
- safe_delay(50);
- }
-
+ #if HAS_TRINAMIC_CONFIG
+ #include "../feature/tmc_util.h"
#endif
- #if !HAS_GRAPHICAL_TFT
+ #if HAS_ADC_BUTTONS
+ #include "../module/thermistor/thermistors.h"
+ #endif
- void _wrap_string(uint8_t &col, uint8_t &row, const char * const string, read_byte_cb_t cb_read_byte, bool wordwrap/*=false*/) {
- SETCURSOR(col, row);
- if (!string) return;
+ #if HAS_POWER_MONITOR
+ #include "../feature/power_monitor.h"
+ #endif
- auto _newline = [&col, &row]{
- col = 0; row++; // Move col to string len (plus space)
- SETCURSOR(0, row); // Simulate carriage return
- };
+ #if HAS_ENCODER_ACTION
+ volatile uint8_t MarlinUI::buttons;
+ #if HAS_SLOW_BUTTONS
+ volatile uint8_t MarlinUI::slow_buttons;
+ #endif
+ #if HAS_TOUCH_BUTTONS
+ #include "touch/touch_buttons.h"
+ bool MarlinUI::on_edit_screen = false;
+ #endif
+ #endif
- uint8_t *p = (uint8_t*)string;
- wchar_t ch;
- if (wordwrap) {
- uint8_t *wrd = nullptr, c = 0;
- // find the end of the part
- for (;;) {
- if (!wrd) wrd = p; // Get word start /before/ advancing
- p = get_utf8_value_cb(p, cb_read_byte, &ch);
- const bool eol = !ch; // zero ends the string
- // End or a break between phrases?
- if (eol || ch == ' ' || ch == '-' || ch == '+' || ch == '.') {
- if (!c && ch == ' ') { if (wrd) wrd++; continue; } // collapse extra spaces
- // Past the right and the word is not too long?
- if (col + c > LCD_WIDTH && col >= (LCD_WIDTH) / 4) _newline(); // should it wrap?
- c += !eol; // +1 so the space will be printed
- col += c; // advance col to new position
- while (c) { // character countdown
- --c; // count down to zero
- wrd = get_utf8_value_cb(wrd, cb_read_byte, &ch); // get characters again
- lcd_put_wchar(ch); // character to the LCD
+ #if SCREENS_CAN_TIME_OUT
+ bool MarlinUI::defer_return_to_status;
+ millis_t MarlinUI::return_to_status_ms = 0;
+ #endif
+
+ uint8_t MarlinUI::lcd_status_update_delay = 1; // First update one loop delayed
+
+ #if BOTH(FILAMENT_LCD_DISPLAY, SDSUPPORT)
+ millis_t MarlinUI::next_filament_display; // = 0
+ #endif
+
+ millis_t MarlinUI::next_button_update_ms; // = 0
+
+ #if HAS_MARLINUI_U8GLIB
+ bool MarlinUI::drawing_screen, MarlinUI::first_page; // = false
+ #endif
+
+ // Encoder Handling
+ #if HAS_ENCODER_ACTION
+ uint32_t MarlinUI::encoderPosition;
+ volatile int8_t encoderDiff; // Updated in update_buttons, added to encoderPosition every LCD update
+ #endif
+
+ #if ENABLED(SDSUPPORT)
+
+ #include "../sd/cardreader.h"
+
+ #if MARLINUI_SCROLL_NAME
+ uint8_t MarlinUI::filename_scroll_pos, MarlinUI::filename_scroll_max;
+ #endif
+
+ const char * MarlinUI::scrolled_filename(CardReader &theCard, const uint8_t maxlen, uint8_t hash, const bool doScroll) {
+ const char *outstr = theCard.longest_filename();
+ if (theCard.longFilename[0]) {
+ #if MARLINUI_SCROLL_NAME
+ if (doScroll) {
+ for (uint8_t l = FILENAME_LENGTH; l--;)
+ hash = ((hash << 1) | (hash >> 7)) ^ theCard.filename[l]; // rotate, xor
+ static uint8_t filename_scroll_hash;
+ if (filename_scroll_hash != hash) { // If the hash changed...
+ filename_scroll_hash = hash; // Save the new hash
+ filename_scroll_max = _MAX(0, utf8_strlen(theCard.longFilename) - maxlen); // Update the scroll limit
+ filename_scroll_pos = 0; // Reset scroll to the start
+ lcd_status_update_delay = 8; // Don't scroll right away
}
- if (eol) break; // all done!
- wrd = nullptr; // set up for next word
- }
- else c++; // count word characters
- }
- }
- else {
- for (;;) {
- p = get_utf8_value_cb(p, cb_read_byte, &ch);
- if (!ch) break;
- lcd_put_wchar(ch);
- col++;
- if (col >= LCD_WIDTH) _newline();
- }
- }
- }
-
- void MarlinUI::draw_select_screen_prompt(PGM_P const pref, const char * const string/*=nullptr*/, PGM_P const suff/*=nullptr*/) {
- const uint8_t plen = utf8_strlen_P(pref), slen = suff ? utf8_strlen_P(suff) : 0;
- uint8_t col = 0, row = 0;
- if (!string && plen + slen <= LCD_WIDTH) {
- col = (LCD_WIDTH - plen - slen) / 2;
- row = LCD_HEIGHT > 3 ? 1 : 0;
- }
- wrap_string_P(col, row, pref, true);
- if (string) {
- if (col) { col = 0; row++; } // Move to the start of the next line
- wrap_string(col, row, string);
- }
- if (suff) wrap_string_P(col, row, suff);
- }
-
- #endif // !HAS_GRAPHICAL_TFT
-
-#endif // HAS_LCD_MENU
-
-void MarlinUI::init() {
-
- init_lcd();
-
- #if HAS_DIGITAL_BUTTONS
- #if BUTTON_EXISTS(EN1)
- SET_INPUT_PULLUP(BTN_EN1);
- #endif
- #if BUTTON_EXISTS(EN2)
- SET_INPUT_PULLUP(BTN_EN2);
- #endif
- #if BUTTON_EXISTS(ENC)
- SET_INPUT_PULLUP(BTN_ENC);
- #endif
- #if BUTTON_EXISTS(ENC_EN)
- SET_INPUT_PULLUP(BTN_ENC_EN);
- #endif
- #if BUTTON_EXISTS(BACK)
- SET_INPUT_PULLUP(BTN_BACK);
- #endif
- #if BUTTON_EXISTS(UP)
- SET_INPUT(BTN_UP);
- #endif
- #if BUTTON_EXISTS(DWN)
- SET_INPUT(BTN_DWN);
- #endif
- #if BUTTON_EXISTS(LFT)
- SET_INPUT(BTN_LFT);
- #endif
- #if BUTTON_EXISTS(RT)
- SET_INPUT(BTN_RT);
- #endif
- #endif
-
- #if HAS_SHIFT_ENCODER
-
- #if ENABLED(SR_LCD_2W_NL) // Non latching 2 wire shift register
-
- SET_OUTPUT(SR_DATA_PIN);
- SET_OUTPUT(SR_CLK_PIN);
-
- #elif PIN_EXISTS(SHIFT_CLK)
-
- SET_OUTPUT(SHIFT_CLK_PIN);
- OUT_WRITE(SHIFT_LD_PIN, HIGH);
- #if PIN_EXISTS(SHIFT_EN)
- OUT_WRITE(SHIFT_EN_PIN, LOW);
- #endif
- SET_INPUT_PULLUP(SHIFT_OUT_PIN);
-
- #endif
-
- #endif // HAS_SHIFT_ENCODER
-
- #if BOTH(HAS_ENCODER_ACTION, HAS_SLOW_BUTTONS)
- slow_buttons = 0;
- #endif
-
- update_buttons();
-
- TERN_(HAS_ENCODER_ACTION, encoderDiff = 0);
-}
-
-bool MarlinUI::get_blink() {
- static uint8_t blink = 0;
- static millis_t next_blink_ms = 0;
- millis_t ms = millis();
- if (ELAPSED(ms, next_blink_ms)) {
- blink ^= 0xFF;
- next_blink_ms = ms + 1000 - (LCD_UPDATE_INTERVAL) / 2;
- }
- return blink != 0;
-}
-
-////////////////////////////////////////////
-///////////// Keypad Handling //////////////
-////////////////////////////////////////////
-
-#if IS_RRW_KEYPAD && HAS_ENCODER_ACTION
-
- volatile uint8_t MarlinUI::keypad_buttons;
-
- #if HAS_LCD_MENU && !HAS_ADC_BUTTONS
-
- void lcd_move_x();
- void lcd_move_y();
- void lcd_move_z();
-
- void _reprapworld_keypad_move(const AxisEnum axis, const int16_t dir) {
- ui.manual_move.menu_scale = REPRAPWORLD_KEYPAD_MOVE_STEP;
- ui.encoderPosition = dir;
- switch (axis) {
- case X_AXIS: lcd_move_x(); break;
- case Y_AXIS: lcd_move_y(); break;
- case Z_AXIS: lcd_move_z();
- default: break;
- }
- }
-
- #endif
-
- bool MarlinUI::handle_keypad() {
-
- #if HAS_ADC_BUTTONS
-
- #define ADC_MIN_KEY_DELAY 100
- if (keypad_buttons) {
- #if HAS_ENCODER_ACTION
- refresh(LCDVIEW_REDRAW_NOW);
- #if HAS_LCD_MENU
- if (encoderDirection == -(ENCODERBASE)) { // HAS_ADC_BUTTONS forces REVERSE_MENU_DIRECTION, so this indicates menu navigation
- if (RRK(EN_KEYPAD_UP)) encoderPosition += ENCODER_STEPS_PER_MENU_ITEM;
- else if (RRK(EN_KEYPAD_DOWN)) encoderPosition -= ENCODER_STEPS_PER_MENU_ITEM;
- else if (RRK(EN_KEYPAD_LEFT)) { MenuItem_back::action(); quick_feedback(); }
- else if (RRK(EN_KEYPAD_RIGHT)) { return_to_status(); quick_feedback(); }
- }
- else
- #endif
- {
- #if HAS_LCD_MENU
- if (RRK(EN_KEYPAD_UP)) encoderPosition -= epps;
- else if (RRK(EN_KEYPAD_DOWN)) encoderPosition += epps;
- else if (RRK(EN_KEYPAD_LEFT)) { MenuItem_back::action(); quick_feedback(); }
- else if (RRK(EN_KEYPAD_RIGHT)) encoderPosition = 0;
- #else
- if (RRK(EN_KEYPAD_UP) || RRK(EN_KEYPAD_LEFT)) encoderPosition -= epps;
- else if (RRK(EN_KEYPAD_DOWN) || RRK(EN_KEYPAD_RIGHT)) encoderPosition += epps;
- #endif
+ // Advance byte position corresponding to filename_scroll_pos char position
+ outstr += TERN(UTF_FILENAME_SUPPORT, utf8_byte_pos_by_char_num(outstr, filename_scroll_pos), filename_scroll_pos);
}
+ #else
+ theCard.longFilename[
+ TERN(UTF_FILENAME_SUPPORT, utf8_byte_pos_by_char_num(theCard.longFilename, maxlen), maxlen)
+ ] = '\0'; // cutoff at screen edge
#endif
- next_button_update_ms = millis() + ADC_MIN_KEY_DELAY;
- return true;
}
+ return outstr;
+ }
- #else // !HAS_ADC_BUTTONS
-
- static uint8_t keypad_debounce = 0;
-
- if (!RRK( EN_KEYPAD_F1 | EN_KEYPAD_F2
- | EN_KEYPAD_F3 | EN_KEYPAD_DOWN
- | EN_KEYPAD_RIGHT | EN_KEYPAD_MIDDLE
- | EN_KEYPAD_UP | EN_KEYPAD_LEFT )
- ) {
- if (keypad_debounce > 0) keypad_debounce--;
- }
- else if (!keypad_debounce) {
- keypad_debounce = 2;
-
- const bool homed = all_axes_homed();
-
- #if HAS_LCD_MENU
-
- if (RRK(EN_KEYPAD_MIDDLE)) goto_screen(menu_move);
-
- #if NONE(DELTA, Z_HOME_TO_MAX)
- if (RRK(EN_KEYPAD_F2)) _reprapworld_keypad_move(Z_AXIS, 1);
- #endif
-
- if (homed) {
- #if EITHER(DELTA, Z_HOME_TO_MAX)
- if (RRK(EN_KEYPAD_F2)) _reprapworld_keypad_move(Z_AXIS, 1);
- #endif
- if (RRK(EN_KEYPAD_F3)) _reprapworld_keypad_move(Z_AXIS, -1);
- if (RRK(EN_KEYPAD_LEFT)) _reprapworld_keypad_move(X_AXIS, -1);
- if (RRK(EN_KEYPAD_RIGHT)) _reprapworld_keypad_move(X_AXIS, 1);
- if (RRK(EN_KEYPAD_DOWN)) _reprapworld_keypad_move(Y_AXIS, 1);
- if (RRK(EN_KEYPAD_UP)) _reprapworld_keypad_move(Y_AXIS, -1);
- }
-
- #endif // HAS_LCD_MENU
-
- if (!homed && RRK(EN_KEYPAD_F1)) queue.inject_P(G28_STR);
- return true;
- }
-
- #endif // !HAS_ADC_BUTTONS
-
- return false;
- }
-
-#endif // IS_RRW_KEYPAD && HAS_ENCODER_ACTION
-
-/**
- * Status Screen
- *
- * This is very display-dependent, so the lcd implementation draws this.
- */
-
-#if BASIC_PROGRESS_BAR
- millis_t MarlinUI::progress_bar_ms; // = 0
- #if PROGRESS_MSG_EXPIRE > 0
- millis_t MarlinUI::expire_status_ms; // = 0
#endif
-#endif
-void MarlinUI::status_screen() {
+ #if HAS_LCD_MENU
+ #include "menu/menu.h"
- TERN_(HAS_LCD_MENU, ENCODER_RATE_MULTIPLY(false));
+ screenFunc_t MarlinUI::currentScreen; // Initialized in CTOR
+ bool MarlinUI::screen_changed;
- #if BASIC_PROGRESS_BAR
-
- //
- // HD44780 implements the following message blinking and
- // message expiration because Status Line and Progress Bar
- // share the same line on the display.
- //
-
- #if DISABLED(PROGRESS_MSG_ONCE) || (PROGRESS_MSG_EXPIRE > 0)
- #define GOT_MS
- const millis_t ms = millis();
+ #if ENABLED(ENCODER_RATE_MULTIPLIER)
+ bool MarlinUI::encoderRateMultiplierEnabled;
+ millis_t MarlinUI::lastEncoderMovementMillis = 0;
+ void MarlinUI::enable_encoder_multiplier(const bool onoff) {
+ encoderRateMultiplierEnabled = onoff;
+ lastEncoderMovementMillis = 0;
+ }
#endif
- // If the message will blink rather than expire...
- #if DISABLED(PROGRESS_MSG_ONCE)
- if (ELAPSED(ms, progress_bar_ms + PROGRESS_BAR_MSG_TIME + PROGRESS_BAR_BAR_TIME))
- progress_bar_ms = ms;
+ #if EITHER(REVERSE_MENU_DIRECTION, REVERSE_SELECT_DIRECTION)
+ int8_t MarlinUI::encoderDirection = ENCODERBASE;
#endif
- #if PROGRESS_MSG_EXPIRE > 0
+ #if HAS_TOUCH_BUTTONS
+ uint8_t MarlinUI::touch_buttons;
+ uint8_t MarlinUI::repeat_delay;
+ #endif
- // Handle message expire
- if (expire_status_ms) {
+ #if EITHER(AUTO_BED_LEVELING_UBL, G26_MESH_VALIDATION)
- // Expire the message if a job is active and the bar has ticks
- if (get_progress_percent() > 2 && !print_job_timer.isPaused()) {
- if (ELAPSED(ms, expire_status_ms)) {
- status_message[0] = '\0';
- expire_status_ms = 0;
+ bool MarlinUI::external_control; // = false
+
+ void MarlinUI::wait_for_release() {
+ while (button_pressed()) safe_delay(50);
+ safe_delay(50);
+ }
+
+ #endif
+
+ #if !HAS_GRAPHICAL_TFT
+
+ void _wrap_string(uint8_t &col, uint8_t &row, const char * const string, read_byte_cb_t cb_read_byte, bool wordwrap/*=false*/) {
+ SETCURSOR(col, row);
+ if (!string) return;
+
+ auto _newline = [&col, &row]{
+ col = 0; row++; // Move col to string len (plus space)
+ SETCURSOR(0, row); // Simulate carriage return
+ };
+
+ uint8_t *p = (uint8_t*)string;
+ wchar_t ch;
+ if (wordwrap) {
+ uint8_t *wrd = nullptr, c = 0;
+ // find the end of the part
+ for (;;) {
+ if (!wrd) wrd = p; // Get word start /before/ advancing
+ p = get_utf8_value_cb(p, cb_read_byte, &ch);
+ const bool eol = !ch; // zero ends the string
+ // End or a break between phrases?
+ if (eol || ch == ' ' || ch == '-' || ch == '+' || ch == '.') {
+ if (!c && ch == ' ') { if (wrd) wrd++; continue; } // collapse extra spaces
+ // Past the right and the word is not too long?
+ if (col + c > LCD_WIDTH && col >= (LCD_WIDTH) / 4) _newline(); // should it wrap?
+ c += !eol; // +1 so the space will be printed
+ col += c; // advance col to new position
+ while (c) { // character countdown
+ --c; // count down to zero
+ wrd = get_utf8_value_cb(wrd, cb_read_byte, &ch); // get characters again
+ lcd_put_wchar(ch); // character to the LCD
+ }
+ if (eol) break; // all done!
+ wrd = nullptr; // set up for next word
+ }
+ else c++; // count word characters
}
}
else {
- // Defer message expiration before bar appears
- // and during any pause (not just SD)
- expire_status_ms += LCD_UPDATE_INTERVAL;
- }
- }
-
- #endif // PROGRESS_MSG_EXPIRE
-
- #endif // BASIC_PROGRESS_BAR
-
- #if HAS_LCD_MENU
- if (use_click()) {
- #if BOTH(FILAMENT_LCD_DISPLAY, SDSUPPORT)
- next_filament_display = millis() + 5000UL; // Show status message for 5s
- #endif
- goto_screen(menu_main);
- #if DISABLED(NO_LCD_REINIT)
- init_lcd(); // May revive the LCD if static electricity killed it
- #endif
- return;
- }
-
- #endif
-
- #if ENABLED(ULTIPANEL_FEEDMULTIPLY)
-
- const int16_t old_frm = feedrate_percentage;
- int16_t new_frm = old_frm + int16_t(encoderPosition);
-
- // Dead zone at 100% feedrate
- if (old_frm == 100) {
- if (int16_t(encoderPosition) > ENCODER_FEEDRATE_DEADZONE)
- new_frm -= ENCODER_FEEDRATE_DEADZONE;
- else if (int16_t(encoderPosition) < -(ENCODER_FEEDRATE_DEADZONE))
- new_frm += ENCODER_FEEDRATE_DEADZONE;
- else
- new_frm = old_frm;
- }
- else if ((old_frm < 100 && new_frm > 100) || (old_frm > 100 && new_frm < 100))
- new_frm = 100;
-
- LIMIT(new_frm, 10, 999);
-
- if (old_frm != new_frm) {
- feedrate_percentage = new_frm;
- encoderPosition = 0;
- #if BOTH(HAS_BUZZER, BEEP_ON_FEEDRATE_CHANGE)
- static millis_t next_beep;
- #ifndef GOT_MS
- const millis_t ms = millis();
- #endif
- if (ELAPSED(ms, next_beep)) {
- buzz(FEEDRATE_CHANGE_BEEP_DURATION, FEEDRATE_CHANGE_BEEP_FREQUENCY);
- next_beep = ms + 500UL;
- }
- #endif
- }
-
- #endif // ULTIPANEL_FEEDMULTIPLY
-
- draw_status_screen();
-}
-
-void MarlinUI::kill_screen(PGM_P lcd_error, PGM_P lcd_component) {
- init();
- status_printf_P(1, PSTR(S_FMT ": " S_FMT), lcd_error, lcd_component);
- TERN_(HAS_LCD_MENU, return_to_status());
-
- // RED ALERT. RED ALERT.
- #ifdef LED_BACKLIGHT_TIMEOUT
- leds.set_color(LEDColorRed());
- #ifdef NEOPIXEL_BKGD_INDEX_FIRST
- neo.set_background_color(255, 0, 0, 0);
- neo.show();
- #endif
- #endif
-
- draw_kill_screen();
-}
-
-void MarlinUI::quick_feedback(const bool clear_buttons/*=true*/) {
-
- TERN_(HAS_LCD_MENU, refresh());
-
- #if HAS_ENCODER_ACTION
- if (clear_buttons) buttons = 0;
- next_button_update_ms = millis() + 500;
- #else
- UNUSED(clear_buttons);
- #endif
-
- #if HAS_CHIRP
- chirp(); // Buzz and wait. Is the delay needed for buttons to settle?
- #if BOTH(HAS_LCD_MENU, USE_BEEPER)
- for (int8_t i = 5; i--;) { buzzer.tick(); delay(2); }
- #elif HAS_LCD_MENU
- delay(10);
- #endif
- #endif
-}
-
-////////////////////////////////////////////
-/////////////// Manual Move ////////////////
-////////////////////////////////////////////
-
-#if HAS_LCD_MENU
-
- ManualMove MarlinUI::manual_move{};
-
- millis_t ManualMove::start_time = 0;
- float ManualMove::menu_scale = 1;
- #if IS_KINEMATIC
- float ManualMove::offset = 0;
- xyze_pos_t ManualMove::all_axes_destination = { 0 };
- bool ManualMove::processing = false;
- #endif
- #if MULTI_E_MANUAL
- int8_t ManualMove::e_index = 0;
- #endif
- AxisEnum ManualMove::axis = NO_AXIS_ENUM;
-
- /**
- * If a manual move has been posted and its time has arrived, and if the planner
- * has a space for it, then add a linear move to current_position the planner.
- *
- * If any manual move needs to be interrupted, make sure to force a manual move
- * by setting manual_move.start_time to millis() after updating current_position.
- *
- * To post a manual move:
- * - Update current_position to the new place you want to go.
- * - Set manual_move.axis to an axis like X_AXIS. Use ALL_AXES_ENUM for diagonal moves.
- * - Set manual_move.start_time to a point in the future (in ms) when the move should be done.
- *
- * For kinematic machines:
- * - Set manual_move.offset to modify one axis and post the move.
- * This is used to achieve more rapid stepping on kinematic machines.
- *
- * Currently used by the _lcd_move_xyz function in menu_motion.cpp
- * and the ubl_map_move_to_xy funtion in menu_ubl.cpp.
- */
- void ManualMove::task() {
-
- if (processing) return; // Prevent re-entry from idle() calls
-
- // Add a manual move to the queue?
- if (axis != NO_AXIS_ENUM && ELAPSED(millis(), start_time) && !planner.is_full()) {
-
- const feedRate_t fr_mm_s = (axis <= LOGICAL_AXES) ? manual_feedrate_mm_s[axis] : XY_PROBE_FEEDRATE_MM_S;
-
- #if IS_KINEMATIC
-
- #if HAS_MULTI_EXTRUDER
- REMEMBER(ae, active_extruder);
- #if MULTI_E_MANUAL
- if (axis == E_AXIS) active_extruder = e_index;
- #endif
- #endif
-
- // Apply a linear offset to a single axis
- if (axis == ALL_AXES_ENUM)
- destination = all_axes_destination;
- else if (axis <= XYZE) {
- destination = current_position;
- destination[axis] += offset;
- }
-
- // Reset for the next move
- offset = 0;
- axis = NO_AXIS_ENUM;
-
- // DELTA and SCARA machines use segmented moves, which could fill the planner during the call to
- // move_to_destination. This will cause idle() to be called, which can then call this function while the
- // previous invocation is being blocked. Modifications to offset shouldn't be made while
- // processing is true or the planner will get out of sync.
- processing = true;
- prepare_internal_move_to_destination(fr_mm_s); // will set current_position from destination
- processing = false;
-
- #else
-
- // For Cartesian / Core motion simply move to the current_position
- planner.buffer_line(current_position, fr_mm_s,
- TERN_(MULTI_E_MANUAL, axis == E_AXIS ? e_index :) active_extruder
- );
-
- //SERIAL_ECHOLNPAIR("Add planner.move with Axis ", AS_CHAR(axis_codes[axis]), " at FR ", fr_mm_s);
-
- axis = NO_AXIS_ENUM;
-
- #endif
- }
- }
-
- //
- // Tell ui.update() to start a move to current_position after a short delay.
- //
- void ManualMove::soon(const AxisEnum move_axis
- OPTARG(MULTI_E_MANUAL, const int8_t eindex/*=active_extruder*/)
- ) {
- TERN_(MULTI_E_MANUAL, if (move_axis == E_AXIS) e_index = eindex);
- start_time = millis() + (menu_scale < 0.99f ? 0UL : 250UL); // delay for bigger moves
- axis = move_axis;
- //SERIAL_ECHOLNPAIR("Post Move with Axis ", AS_CHAR(axis_codes[axis]), " soon.");
- }
-
- #if ENABLED(AUTO_BED_LEVELING_UBL)
-
- void MarlinUI::external_encoder() {
- if (external_control && encoderDiff) {
- ubl.encoder_diff += encoderDiff; // Encoder for UBL G29 mesh editing
- encoderDiff = 0; // Hide encoder events from the screen handler
- refresh(LCDVIEW_REDRAW_NOW); // ...but keep the refresh.
- }
- }
-
- #endif
-
-#endif // HAS_LCD_MENU
-
-/**
- * Update the LCD, read encoder buttons, etc.
- * - Read button states
- * - Check the SD Card slot state
- * - Act on RepRap World keypad input
- * - Update the encoder position
- * - Apply acceleration to the encoder position
- * - Do refresh(LCDVIEW_CALL_REDRAW_NOW) on controller events
- * - Reset the Info Screen timeout if there's any input
- * - Update status indicators, if any
- *
- * Run the current LCD menu handler callback function:
- * - Call the handler only if lcdDrawUpdate != LCDVIEW_NONE
- * - Before calling the handler, LCDVIEW_CALL_NO_REDRAW => LCDVIEW_NONE
- * - Call the menu handler. Menu handlers should do the following:
- * - If a value changes, set lcdDrawUpdate to LCDVIEW_REDRAW_NOW and draw the value
- * (Encoder events automatically set lcdDrawUpdate for you.)
- * - if (should_draw()) { redraw }
- * - Before exiting the handler set lcdDrawUpdate to:
- * - LCDVIEW_CLEAR_CALL_REDRAW to clear screen and set LCDVIEW_CALL_REDRAW_NEXT.
- * - LCDVIEW_REDRAW_NOW to draw now (including remaining stripes).
- * - LCDVIEW_CALL_REDRAW_NEXT to draw now and get LCDVIEW_REDRAW_NOW on the next loop.
- * - LCDVIEW_CALL_NO_REDRAW to draw now and get LCDVIEW_NONE on the next loop.
- * - NOTE: For graphical displays menu handlers may be called 2 or more times per loop,
- * so don't change lcdDrawUpdate without considering this.
- *
- * After the menu handler callback runs (or not):
- * - Clear the LCD if lcdDrawUpdate == LCDVIEW_CLEAR_CALL_REDRAW
- * - Update lcdDrawUpdate for the next loop (i.e., move one state down, usually)
- *
- * This function is only called from the main thread.
- */
-
-LCDViewAction MarlinUI::lcdDrawUpdate = LCDVIEW_CLEAR_CALL_REDRAW;
-millis_t next_lcd_update_ms;
-
-inline bool can_encode() {
- return !BUTTON_PRESSED(ENC_EN); // Update encoder only when ENC_EN is not LOW (pressed)
-}
-
-void MarlinUI::update() {
-
- static uint16_t max_display_update_time = 0;
- millis_t ms = millis();
-
- #ifdef LED_BACKLIGHT_TIMEOUT
- leds.update_timeout(powersupply_on);
- #endif
-
- #if HAS_LCD_MENU
-
- // Handle any queued Move Axis motion
- manual_move.task();
-
- // Update button states for button_pressed(), etc.
- // If the state changes the next update may be delayed 300-500ms.
- update_buttons();
-
- // If the action button is pressed...
- static bool wait_for_unclick; // = false
-
- auto do_click = [&]{
- wait_for_unclick = true; // - Set debounce flag to ignore continuous clicks
- lcd_clicked = !wait_for_user; // - Keep the click if not waiting for a user-click
- wait_for_user = false; // - Any click clears wait for user
- quick_feedback(); // - Always make a click sound
- };
-
- #if HAS_TOUCH_BUTTONS
- if (touch_buttons) {
- reset_status_timeout(ms);
- if (touch_buttons & (EN_A | EN_B)) { // Menu arrows, in priority
- if (ELAPSED(ms, next_button_update_ms)) {
- encoderDiff = (ENCODER_STEPS_PER_MENU_ITEM) * epps * encoderDirection;
- if (touch_buttons & EN_A) encoderDiff *= -1;
- TERN_(AUTO_BED_LEVELING_UBL, external_encoder());
- next_button_update_ms = ms + repeat_delay; // Assume the repeat delay
- if (!wait_for_unclick) {
- next_button_update_ms += 250; // Longer delay on first press
- wait_for_unclick = true; // Avoid Back/Select click while repeating
- chirp();
- }
+ for (;;) {
+ p = get_utf8_value_cb(p, cb_read_byte, &ch);
+ if (!ch) break;
+ lcd_put_wchar(ch);
+ col++;
+ if (col >= LCD_WIDTH) _newline();
}
}
- else if (!wait_for_unclick && (buttons & EN_C)) // OK button, if not waiting for a debounce release:
- do_click();
}
- // keep wait_for_unclick value
- #endif
- if (!touch_buttons) {
- // Integrated LCD click handling via button_pressed
- if (!external_control && button_pressed()) {
- if (!wait_for_unclick) do_click(); // Handle the click
+ void MarlinUI::draw_select_screen_prompt(PGM_P const pref, const char * const string/*=nullptr*/, PGM_P const suff/*=nullptr*/) {
+ const uint8_t plen = utf8_strlen_P(pref), slen = suff ? utf8_strlen_P(suff) : 0;
+ uint8_t col = 0, row = 0;
+ if (!string && plen + slen <= LCD_WIDTH) {
+ col = (LCD_WIDTH - plen - slen) / 2;
+ row = LCD_HEIGHT > 3 ? 1 : 0;
+ }
+ wrap_string_P(col, row, pref, true);
+ if (string) {
+ if (col) { col = 0; row++; } // Move to the start of the next line
+ wrap_string(col, row, string);
+ }
+ if (suff) wrap_string_P(col, row, suff);
}
- else
- wait_for_unclick = false;
- }
- if (LCD_BACK_CLICKED()) {
- quick_feedback();
- goto_previous_screen();
- }
+ #endif // !HAS_GRAPHICAL_TFT
#endif // HAS_LCD_MENU
- if (ELAPSED(ms, next_lcd_update_ms) || TERN0(HAS_MARLINUI_U8GLIB, drawing_screen)) {
+ void MarlinUI::init() {
- next_lcd_update_ms = ms + LCD_UPDATE_INTERVAL;
+ init_lcd();
- #if HAS_TOUCH_BUTTONS
+ #if HAS_DIGITAL_BUTTONS
+ #if BUTTON_EXISTS(EN1)
+ SET_INPUT_PULLUP(BTN_EN1);
+ #endif
+ #if BUTTON_EXISTS(EN2)
+ SET_INPUT_PULLUP(BTN_EN2);
+ #endif
+ #if BUTTON_EXISTS(ENC)
+ SET_INPUT_PULLUP(BTN_ENC);
+ #endif
+ #if BUTTON_EXISTS(ENC_EN)
+ SET_INPUT_PULLUP(BTN_ENC_EN);
+ #endif
+ #if BUTTON_EXISTS(BACK)
+ SET_INPUT_PULLUP(BTN_BACK);
+ #endif
+ #if BUTTON_EXISTS(UP)
+ SET_INPUT(BTN_UP);
+ #endif
+ #if BUTTON_EXISTS(DWN)
+ SET_INPUT(BTN_DWN);
+ #endif
+ #if BUTTON_EXISTS(LFT)
+ SET_INPUT(BTN_LFT);
+ #endif
+ #if BUTTON_EXISTS(RT)
+ SET_INPUT(BTN_RT);
+ #endif
+ #endif
- if (on_status_screen()) next_lcd_update_ms += (LCD_UPDATE_INTERVAL) * 2;
+ #if HAS_SHIFT_ENCODER
- TERN_(HAS_ENCODER_ACTION, touch_buttons = touch.read_buttons());
+ #if ENABLED(SR_LCD_2W_NL) // Non latching 2 wire shift register
+
+ SET_OUTPUT(SR_DATA_PIN);
+ SET_OUTPUT(SR_CLK_PIN);
+
+ #elif PIN_EXISTS(SHIFT_CLK)
+
+ SET_OUTPUT(SHIFT_CLK_PIN);
+ OUT_WRITE(SHIFT_LD_PIN, HIGH);
+ #if PIN_EXISTS(SHIFT_EN)
+ OUT_WRITE(SHIFT_EN_PIN, LOW);
+ #endif
+ SET_INPUT_PULLUP(SHIFT_OUT_PIN);
+
+ #endif
+
+ #endif // HAS_SHIFT_ENCODER
+
+ #if BOTH(HAS_ENCODER_ACTION, HAS_SLOW_BUTTONS)
+ slow_buttons = 0;
+ #endif
+
+ update_buttons();
+
+ TERN_(HAS_ENCODER_ACTION, encoderDiff = 0);
+ }
+
+ bool MarlinUI::get_blink() {
+ static uint8_t blink = 0;
+ static millis_t next_blink_ms = 0;
+ millis_t ms = millis();
+ if (ELAPSED(ms, next_blink_ms)) {
+ blink ^= 0xFF;
+ next_blink_ms = ms + 1000 - (LCD_UPDATE_INTERVAL) / 2;
+ }
+ return blink != 0;
+ }
+
+ ////////////////////////////////////////////
+ ///////////// Keypad Handling //////////////
+ ////////////////////////////////////////////
+
+ #if IS_RRW_KEYPAD && HAS_ENCODER_ACTION
+
+ volatile uint8_t MarlinUI::keypad_buttons;
+
+ #if HAS_LCD_MENU && !HAS_ADC_BUTTONS
+
+ void lcd_move_x();
+ void lcd_move_y();
+ void lcd_move_z();
+
+ void _reprapworld_keypad_move(const AxisEnum axis, const int16_t dir) {
+ ui.manual_move.menu_scale = REPRAPWORLD_KEYPAD_MOVE_STEP;
+ ui.encoderPosition = dir;
+ switch (axis) {
+ case X_AXIS: lcd_move_x(); break;
+ case Y_AXIS: lcd_move_y(); break;
+ case Z_AXIS: lcd_move_z();
+ default: break;
+ }
+ }
#endif
- TERN_(LCD_HAS_STATUS_INDICATORS, update_indicators());
+ bool MarlinUI::handle_keypad() {
- #if HAS_ENCODER_ACTION
+ #if HAS_ADC_BUTTONS
- TERN_(HAS_SLOW_BUTTONS, slow_buttons = read_slow_buttons()); // Buttons that take too long to read in interrupt context
-
- if (TERN0(IS_RRW_KEYPAD, handle_keypad()))
- reset_status_timeout(ms);
-
- uint8_t abs_diff = ABS(encoderDiff);
-
- #if ENCODER_PULSES_PER_STEP > 1
- // When reversing the encoder direction, a movement step can be missed because
- // encoderDiff has a non-zero residual value, making the controller unresponsive.
- // The fix clears the residual value when the encoder is idle.
- // Also check if past half the threshold to compensate for missed single steps.
- static int8_t lastEncoderDiff;
-
- // Timeout? No decoder change since last check. 10 or 20 times per second.
- if (encoderDiff == lastEncoderDiff && abs_diff <= epps / 2) // Same direction & size but not over a half-step?
- encoderDiff = 0; // Clear residual pulses.
- else if (WITHIN(abs_diff, epps / 2 + 1, epps - 1)) { // Past half of threshold?
- abs_diff = epps; // Treat as a full step size
- encoderDiff = (encoderDiff < 0 ? -1 : 1) * abs_diff; // ...in the spin direction.
+ #define ADC_MIN_KEY_DELAY 100
+ if (keypad_buttons) {
+ #if HAS_ENCODER_ACTION
+ refresh(LCDVIEW_REDRAW_NOW);
+ #if HAS_LCD_MENU
+ if (encoderDirection == -(ENCODERBASE)) { // HAS_ADC_BUTTONS forces REVERSE_MENU_DIRECTION, so this indicates menu navigation
+ if (RRK(EN_KEYPAD_UP)) encoderPosition += ENCODER_STEPS_PER_MENU_ITEM;
+ else if (RRK(EN_KEYPAD_DOWN)) encoderPosition -= ENCODER_STEPS_PER_MENU_ITEM;
+ else if (RRK(EN_KEYPAD_LEFT)) { MenuItem_back::action(); quick_feedback(); }
+ else if (RRK(EN_KEYPAD_RIGHT)) { return_to_status(); quick_feedback(); }
+ }
+ else
+ #endif
+ {
+ #if HAS_LCD_MENU
+ if (RRK(EN_KEYPAD_UP)) encoderPosition -= epps;
+ else if (RRK(EN_KEYPAD_DOWN)) encoderPosition += epps;
+ else if (RRK(EN_KEYPAD_LEFT)) { MenuItem_back::action(); quick_feedback(); }
+ else if (RRK(EN_KEYPAD_RIGHT)) encoderPosition = 0;
+ #else
+ if (RRK(EN_KEYPAD_UP) || RRK(EN_KEYPAD_LEFT)) encoderPosition -= epps;
+ else if (RRK(EN_KEYPAD_DOWN) || RRK(EN_KEYPAD_RIGHT)) encoderPosition += epps;
+ #endif
+ }
+ #endif
+ next_button_update_ms = millis() + ADC_MIN_KEY_DELAY;
+ return true;
}
- lastEncoderDiff = encoderDiff;
+
+ #else // !HAS_ADC_BUTTONS
+
+ static uint8_t keypad_debounce = 0;
+
+ if (!RRK( EN_KEYPAD_F1 | EN_KEYPAD_F2
+ | EN_KEYPAD_F3 | EN_KEYPAD_DOWN
+ | EN_KEYPAD_RIGHT | EN_KEYPAD_MIDDLE
+ | EN_KEYPAD_UP | EN_KEYPAD_LEFT )
+ ) {
+ if (keypad_debounce > 0) keypad_debounce--;
+ }
+ else if (!keypad_debounce) {
+ keypad_debounce = 2;
+
+ const bool homed = all_axes_homed();
+
+ #if HAS_LCD_MENU
+
+ if (RRK(EN_KEYPAD_MIDDLE)) goto_screen(menu_move);
+
+ #if NONE(DELTA, Z_HOME_TO_MAX)
+ if (RRK(EN_KEYPAD_F2)) _reprapworld_keypad_move(Z_AXIS, 1);
+ #endif
+
+ if (homed) {
+ #if EITHER(DELTA, Z_HOME_TO_MAX)
+ if (RRK(EN_KEYPAD_F2)) _reprapworld_keypad_move(Z_AXIS, 1);
+ #endif
+ if (RRK(EN_KEYPAD_F3)) _reprapworld_keypad_move(Z_AXIS, -1);
+ if (RRK(EN_KEYPAD_LEFT)) _reprapworld_keypad_move(X_AXIS, -1);
+ if (RRK(EN_KEYPAD_RIGHT)) _reprapworld_keypad_move(X_AXIS, 1);
+ if (RRK(EN_KEYPAD_DOWN)) _reprapworld_keypad_move(Y_AXIS, 1);
+ if (RRK(EN_KEYPAD_UP)) _reprapworld_keypad_move(Y_AXIS, -1);
+ }
+
+ #endif // HAS_LCD_MENU
+
+ if (!homed && RRK(EN_KEYPAD_F1)) queue.inject_P(G28_STR);
+ return true;
+ }
+
+ #endif // !HAS_ADC_BUTTONS
+
+ return false;
+ }
+
+ #endif // IS_RRW_KEYPAD && HAS_ENCODER_ACTION
+
+ /**
+ * Status Screen
+ *
+ * This is very display-dependent, so the lcd implementation draws this.
+ */
+
+ #if BASIC_PROGRESS_BAR
+ millis_t MarlinUI::progress_bar_ms; // = 0
+ #if PROGRESS_MSG_EXPIRE > 0
+ millis_t MarlinUI::expire_status_ms; // = 0
+ #endif
+ #endif
+
+ void MarlinUI::status_screen() {
+
+ TERN_(HAS_LCD_MENU, ENCODER_RATE_MULTIPLY(false));
+
+ #if BASIC_PROGRESS_BAR
+
+ //
+ // HD44780 implements the following message blinking and
+ // message expiration because Status Line and Progress Bar
+ // share the same line on the display.
+ //
+
+ #if DISABLED(PROGRESS_MSG_ONCE) || (PROGRESS_MSG_EXPIRE > 0)
+ #define GOT_MS
+ const millis_t ms = millis();
#endif
- const bool encoderPastThreshold = (abs_diff >= epps);
- if (encoderPastThreshold || lcd_clicked) {
- if (encoderPastThreshold && TERN1(IS_TFTGLCD_PANEL, !external_control)) {
+ // If the message will blink rather than expire...
+ #if DISABLED(PROGRESS_MSG_ONCE)
+ if (ELAPSED(ms, progress_bar_ms + PROGRESS_BAR_MSG_TIME + PROGRESS_BAR_BAR_TIME))
+ progress_bar_ms = ms;
+ #endif
- #if BOTH(HAS_LCD_MENU, ENCODER_RATE_MULTIPLIER)
+ #if PROGRESS_MSG_EXPIRE > 0
- int32_t encoderMultiplier = 1;
+ // Handle message expire
+ if (expire_status_ms) {
- if (encoderRateMultiplierEnabled) {
- const float encoderMovementSteps = float(abs_diff) / epps;
-
- if (lastEncoderMovementMillis) {
- // Note that the rate is always calculated between two passes through the
- // loop and that the abs of the encoderDiff value is tracked.
- const float encoderStepRate = encoderMovementSteps / float(ms - lastEncoderMovementMillis) * 1000;
-
- if (encoderStepRate >= ENCODER_100X_STEPS_PER_SEC) encoderMultiplier = 100;
- else if (encoderStepRate >= ENCODER_10X_STEPS_PER_SEC) encoderMultiplier = 10;
-
- // Enable to output the encoder steps per second value
- //#define ENCODER_RATE_MULTIPLIER_DEBUG
- #if ENABLED(ENCODER_RATE_MULTIPLIER_DEBUG)
- SERIAL_ECHO_START();
- SERIAL_ECHOPAIR("Enc Step Rate: ", encoderStepRate);
- SERIAL_ECHOPAIR(" Multiplier: ", encoderMultiplier);
- SERIAL_ECHOPAIR(" ENCODER_10X_STEPS_PER_SEC: ", ENCODER_10X_STEPS_PER_SEC);
- SERIAL_ECHOPAIR(" ENCODER_100X_STEPS_PER_SEC: ", ENCODER_100X_STEPS_PER_SEC);
- SERIAL_EOL();
- #endif
- }
-
- lastEncoderMovementMillis = ms;
- } // encoderRateMultiplierEnabled
-
- #else
-
- constexpr int32_t encoderMultiplier = 1;
-
- #endif // ENCODER_RATE_MULTIPLIER
-
- if (can_encode()) encoderPosition += (encoderDiff * encoderMultiplier) / epps;
-
- encoderDiff = 0;
+ // Expire the message if a job is active and the bar has ticks
+ if (get_progress_percent() > 2 && !print_job_timer.isPaused()) {
+ if (ELAPSED(ms, expire_status_ms)) {
+ status_message[0] = '\0';
+ expire_status_ms = 0;
+ }
+ }
+ else {
+ // Defer message expiration before bar appears
+ // and during any pause (not just SD)
+ expire_status_ms += LCD_UPDATE_INTERVAL;
+ }
}
- reset_status_timeout(ms);
+ #endif // PROGRESS_MSG_EXPIRE
- refresh(LCDVIEW_REDRAW_NOW);
+ #endif // BASIC_PROGRESS_BAR
- #ifdef LED_BACKLIGHT_TIMEOUT
- if (!powersupply_on) leds.reset_timeout(ms);
+ #if HAS_LCD_MENU
+ if (use_click()) {
+ #if BOTH(FILAMENT_LCD_DISPLAY, SDSUPPORT)
+ next_filament_display = millis() + 5000UL; // Show status message for 5s
+ #endif
+ goto_screen(menu_main);
+ #if DISABLED(NO_LCD_REINIT)
+ init_lcd(); // May revive the LCD if static electricity killed it
+ #endif
+ return;
+ }
+
+ #endif
+
+ #if ENABLED(ULTIPANEL_FEEDMULTIPLY)
+
+ const int16_t old_frm = feedrate_percentage;
+ int16_t new_frm = old_frm + int16_t(encoderPosition);
+
+ // Dead zone at 100% feedrate
+ if (old_frm == 100) {
+ if (int16_t(encoderPosition) > ENCODER_FEEDRATE_DEADZONE)
+ new_frm -= ENCODER_FEEDRATE_DEADZONE;
+ else if (int16_t(encoderPosition) < -(ENCODER_FEEDRATE_DEADZONE))
+ new_frm += ENCODER_FEEDRATE_DEADZONE;
+ else
+ new_frm = old_frm;
+ }
+ else if ((old_frm < 100 && new_frm > 100) || (old_frm > 100 && new_frm < 100))
+ new_frm = 100;
+
+ LIMIT(new_frm, 10, 999);
+
+ if (old_frm != new_frm) {
+ feedrate_percentage = new_frm;
+ encoderPosition = 0;
+ #if BOTH(HAS_BUZZER, BEEP_ON_FEEDRATE_CHANGE)
+ static millis_t next_beep;
+ #ifndef GOT_MS
+ const millis_t ms = millis();
+ #endif
+ if (ELAPSED(ms, next_beep)) {
+ buzz(FEEDRATE_CHANGE_BEEP_DURATION, FEEDRATE_CHANGE_BEEP_FREQUENCY);
+ next_beep = ms + 500UL;
+ }
#endif
}
+ #endif // ULTIPANEL_FEEDMULTIPLY
+
+ draw_status_screen();
+ }
+
+ void MarlinUI::kill_screen(PGM_P lcd_error, PGM_P lcd_component) {
+ init();
+ status_printf_P(1, PSTR(S_FMT ": " S_FMT), lcd_error, lcd_component);
+ TERN_(HAS_LCD_MENU, return_to_status());
+
+ // RED ALERT. RED ALERT.
+ #ifdef LED_BACKLIGHT_TIMEOUT
+ leds.set_color(LEDColorRed());
+ #ifdef NEOPIXEL_BKGD_INDEX_FIRST
+ neo.set_background_color(255, 0, 0, 0);
+ neo.show();
+ #endif
#endif
- // This runs every ~100ms when idling often enough.
- // Instead of tracking changes just redraw the Status Screen once per second.
- if (on_status_screen() && !lcd_status_update_delay--) {
- lcd_status_update_delay = TERN(HAS_MARLINUI_U8GLIB, 12, 9);
- if (max_display_update_time) max_display_update_time--; // Be sure never go to a very big number
- refresh(LCDVIEW_REDRAW_NOW);
+ draw_kill_screen();
+ }
+
+ void MarlinUI::quick_feedback(const bool clear_buttons/*=true*/) {
+
+ TERN_(HAS_LCD_MENU, refresh());
+
+ #if HAS_ENCODER_ACTION
+ if (clear_buttons) buttons = 0;
+ next_button_update_ms = millis() + 500;
+ #else
+ UNUSED(clear_buttons);
+ #endif
+
+ #if HAS_CHIRP
+ chirp(); // Buzz and wait. Is the delay needed for buttons to settle?
+ #if BOTH(HAS_LCD_MENU, USE_BEEPER)
+ for (int8_t i = 5; i--;) { buzzer.tick(); delay(2); }
+ #elif HAS_LCD_MENU
+ delay(10);
+ #endif
+ #endif
+ }
+
+ ////////////////////////////////////////////
+ /////////////// Manual Move ////////////////
+ ////////////////////////////////////////////
+
+ #if HAS_LCD_MENU
+
+ ManualMove MarlinUI::manual_move{};
+
+ millis_t ManualMove::start_time = 0;
+ float ManualMove::menu_scale = 1;
+ #if IS_KINEMATIC
+ float ManualMove::offset = 0;
+ xyze_pos_t ManualMove::all_axes_destination = { 0 };
+ bool ManualMove::processing = false;
+ #endif
+ #if MULTI_E_MANUAL
+ int8_t ManualMove::e_index = 0;
+ #endif
+ AxisEnum ManualMove::axis = NO_AXIS_ENUM;
+
+ /**
+ * If a manual move has been posted and its time has arrived, and if the planner
+ * has a space for it, then add a linear move to current_position the planner.
+ *
+ * If any manual move needs to be interrupted, make sure to force a manual move
+ * by setting manual_move.start_time to millis() after updating current_position.
+ *
+ * To post a manual move:
+ * - Update current_position to the new place you want to go.
+ * - Set manual_move.axis to an axis like X_AXIS. Use ALL_AXES_ENUM for diagonal moves.
+ * - Set manual_move.start_time to a point in the future (in ms) when the move should be done.
+ *
+ * For kinematic machines:
+ * - Set manual_move.offset to modify one axis and post the move.
+ * This is used to achieve more rapid stepping on kinematic machines.
+ *
+ * Currently used by the _lcd_move_xyz function in menu_motion.cpp
+ * and the ubl_map_move_to_xy funtion in menu_ubl.cpp.
+ */
+ void ManualMove::task() {
+
+ if (processing) return; // Prevent re-entry from idle() calls
+
+ // Add a manual move to the queue?
+ if (axis != NO_AXIS_ENUM && ELAPSED(millis(), start_time) && !planner.is_full()) {
+
+ const feedRate_t fr_mm_s = (axis <= LOGICAL_AXES) ? manual_feedrate_mm_s[axis] : XY_PROBE_FEEDRATE_MM_S;
+
+ #if IS_KINEMATIC
+
+ #if HAS_MULTI_EXTRUDER
+ REMEMBER(ae, active_extruder);
+ #if MULTI_E_MANUAL
+ if (axis == E_AXIS) active_extruder = e_index;
+ #endif
+ #endif
+
+ // Apply a linear offset to a single axis
+ if (axis == ALL_AXES_ENUM)
+ destination = all_axes_destination;
+ else if (axis <= XYZE) {
+ destination = current_position;
+ destination[axis] += offset;
+ }
+
+ // Reset for the next move
+ offset = 0;
+ axis = NO_AXIS_ENUM;
+
+ // DELTA and SCARA machines use segmented moves, which could fill the planner during the call to
+ // move_to_destination. This will cause idle() to be called, which can then call this function while the
+ // previous invocation is being blocked. Modifications to offset shouldn't be made while
+ // processing is true or the planner will get out of sync.
+ processing = true;
+ prepare_internal_move_to_destination(fr_mm_s); // will set current_position from destination
+ processing = false;
+
+ #else
+
+ // For Cartesian / Core motion simply move to the current_position
+ planner.buffer_line(current_position, fr_mm_s,
+ TERN_(MULTI_E_MANUAL, axis == E_AXIS ? e_index :) active_extruder
+ );
+
+ //SERIAL_ECHOLNPAIR("Add planner.move with Axis ", AS_CHAR(axis_codes[axis]), " at FR ", fr_mm_s);
+
+ axis = NO_AXIS_ENUM;
+
+ #endif
+ }
}
- #if BOTH(HAS_LCD_MENU, SCROLL_LONG_FILENAMES)
- // If scrolling of long file names is enabled and we are in the sd card menu,
- // cause a refresh to occur until all the text has scrolled into view.
- if (currentScreen == menu_media && !lcd_status_update_delay--) {
- lcd_status_update_delay = ++filename_scroll_pos >= filename_scroll_max ? 12 : 4; // Long delay at end and start
- if (filename_scroll_pos > filename_scroll_max) filename_scroll_pos = 0;
- refresh(LCDVIEW_REDRAW_NOW);
- reset_status_timeout(ms);
+ //
+ // Tell ui.update() to start a move to current_position after a short delay.
+ //
+ void ManualMove::soon(const AxisEnum move_axis
+ OPTARG(MULTI_E_MANUAL, const int8_t eindex/*=active_extruder*/)
+ ) {
+ TERN_(MULTI_E_MANUAL, if (move_axis == E_AXIS) e_index = eindex);
+ start_time = millis() + (menu_scale < 0.99f ? 0UL : 250UL); // delay for bigger moves
+ axis = move_axis;
+ //SERIAL_ECHOLNPAIR("Post Move with Axis ", AS_CHAR(axis_codes[axis]), " soon.");
+ }
+
+ #if ENABLED(AUTO_BED_LEVELING_UBL)
+
+ void MarlinUI::external_encoder() {
+ if (external_control && encoderDiff) {
+ ubl.encoder_diff += encoderDiff; // Encoder for UBL G29 mesh editing
+ encoderDiff = 0; // Hide encoder events from the screen handler
+ refresh(LCDVIEW_REDRAW_NOW); // ...but keep the refresh.
+ }
}
+
#endif
- // Then we want to use only 50% of the time
- const uint16_t bbr2 = planner.block_buffer_runtime() >> 1;
+ #endif // HAS_LCD_MENU
- if ((should_draw() || drawing_screen) && (!bbr2 || bbr2 > max_display_update_time)) {
+ /**
+ * Update the LCD, read encoder buttons, etc.
+ * - Read button states
+ * - Check the SD Card slot state
+ * - Act on RepRap World keypad input
+ * - Update the encoder position
+ * - Apply acceleration to the encoder position
+ * - Do refresh(LCDVIEW_CALL_REDRAW_NOW) on controller events
+ * - Reset the Info Screen timeout if there's any input
+ * - Update status indicators, if any
+ *
+ * Run the current LCD menu handler callback function:
+ * - Call the handler only if lcdDrawUpdate != LCDVIEW_NONE
+ * - Before calling the handler, LCDVIEW_CALL_NO_REDRAW => LCDVIEW_NONE
+ * - Call the menu handler. Menu handlers should do the following:
+ * - If a value changes, set lcdDrawUpdate to LCDVIEW_REDRAW_NOW and draw the value
+ * (Encoder events automatically set lcdDrawUpdate for you.)
+ * - if (should_draw()) { redraw }
+ * - Before exiting the handler set lcdDrawUpdate to:
+ * - LCDVIEW_CLEAR_CALL_REDRAW to clear screen and set LCDVIEW_CALL_REDRAW_NEXT.
+ * - LCDVIEW_REDRAW_NOW to draw now (including remaining stripes).
+ * - LCDVIEW_CALL_REDRAW_NEXT to draw now and get LCDVIEW_REDRAW_NOW on the next loop.
+ * - LCDVIEW_CALL_NO_REDRAW to draw now and get LCDVIEW_NONE on the next loop.
+ * - NOTE: For graphical displays menu handlers may be called 2 or more times per loop,
+ * so don't change lcdDrawUpdate without considering this.
+ *
+ * After the menu handler callback runs (or not):
+ * - Clear the LCD if lcdDrawUpdate == LCDVIEW_CLEAR_CALL_REDRAW
+ * - Update lcdDrawUpdate for the next loop (i.e., move one state down, usually)
+ *
+ * This function is only called from the main thread.
+ */
+
+ LCDViewAction MarlinUI::lcdDrawUpdate = LCDVIEW_CLEAR_CALL_REDRAW;
+ millis_t next_lcd_update_ms;
+
+ inline bool can_encode() {
+ return !BUTTON_PRESSED(ENC_EN); // Update encoder only when ENC_EN is not LOW (pressed)
+ }
+
+ void MarlinUI::update() {
+
+ static uint16_t max_display_update_time = 0;
+ millis_t ms = millis();
+
+ #ifdef LED_BACKLIGHT_TIMEOUT
+ leds.update_timeout(powersupply_on);
+ #endif
+
+ #if HAS_LCD_MENU
+
+ // Handle any queued Move Axis motion
+ manual_move.task();
+
+ // Update button states for button_pressed(), etc.
+ // If the state changes the next update may be delayed 300-500ms.
+ update_buttons();
+
+ // If the action button is pressed...
+ static bool wait_for_unclick; // = false
+
+ auto do_click = [&]{
+ wait_for_unclick = true; // - Set debounce flag to ignore continuous clicks
+ lcd_clicked = !wait_for_user; // - Keep the click if not waiting for a user-click
+ wait_for_user = false; // - Any click clears wait for user
+ quick_feedback(); // - Always make a click sound
+ };
+
+ #if HAS_TOUCH_BUTTONS
+ if (touch_buttons) {
+ reset_status_timeout(ms);
+ if (touch_buttons & (EN_A | EN_B)) { // Menu arrows, in priority
+ if (ELAPSED(ms, next_button_update_ms)) {
+ encoderDiff = (ENCODER_STEPS_PER_MENU_ITEM) * epps * encoderDirection;
+ if (touch_buttons & EN_A) encoderDiff *= -1;
+ TERN_(AUTO_BED_LEVELING_UBL, external_encoder());
+ next_button_update_ms = ms + repeat_delay; // Assume the repeat delay
+ if (!wait_for_unclick) {
+ next_button_update_ms += 250; // Longer delay on first press
+ wait_for_unclick = true; // Avoid Back/Select click while repeating
+ chirp();
+ }
+ }
+ }
+ else if (!wait_for_unclick && (buttons & EN_C)) // OK button, if not waiting for a debounce release:
+ do_click();
+ }
+ // keep wait_for_unclick value
+ #endif
+
+ if (!touch_buttons) {
+ // Integrated LCD click handling via button_pressed
+ if (!external_control && button_pressed()) {
+ if (!wait_for_unclick) do_click(); // Handle the click
+ }
+ else
+ wait_for_unclick = false;
+ }
+
+ if (LCD_BACK_CLICKED()) {
+ quick_feedback();
+ goto_previous_screen();
+ }
+
+ #endif // HAS_LCD_MENU
+
+ if (ELAPSED(ms, next_lcd_update_ms) || TERN0(HAS_MARLINUI_U8GLIB, drawing_screen)) {
+
+ next_lcd_update_ms = ms + LCD_UPDATE_INTERVAL;
+
+ #if HAS_TOUCH_BUTTONS
+
+ if (on_status_screen()) next_lcd_update_ms += (LCD_UPDATE_INTERVAL) * 2;
+
+ TERN_(HAS_ENCODER_ACTION, touch_buttons = touch.read_buttons());
+
+ #endif
+
+ TERN_(LCD_HAS_STATUS_INDICATORS, update_indicators());
+
+ #if HAS_ENCODER_ACTION
+
+ TERN_(HAS_SLOW_BUTTONS, slow_buttons = read_slow_buttons()); // Buttons that take too long to read in interrupt context
+
+ if (TERN0(IS_RRW_KEYPAD, handle_keypad()))
+ reset_status_timeout(ms);
+
+ uint8_t abs_diff = ABS(encoderDiff);
+
+ #if ENCODER_PULSES_PER_STEP > 1
+ // When reversing the encoder direction, a movement step can be missed because
+ // encoderDiff has a non-zero residual value, making the controller unresponsive.
+ // The fix clears the residual value when the encoder is idle.
+ // Also check if past half the threshold to compensate for missed single steps.
+ static int8_t lastEncoderDiff;
+
+ // Timeout? No decoder change since last check. 10 or 20 times per second.
+ if (encoderDiff == lastEncoderDiff && abs_diff <= epps / 2) // Same direction & size but not over a half-step?
+ encoderDiff = 0; // Clear residual pulses.
+ else if (WITHIN(abs_diff, epps / 2 + 1, epps - 1)) { // Past half of threshold?
+ abs_diff = epps; // Treat as a full step size
+ encoderDiff = (encoderDiff < 0 ? -1 : 1) * abs_diff; // ...in the spin direction.
+ }
+ lastEncoderDiff = encoderDiff;
+ #endif
+
+ const bool encoderPastThreshold = (abs_diff >= epps);
+ if (encoderPastThreshold || lcd_clicked) {
+ if (encoderPastThreshold && TERN1(IS_TFTGLCD_PANEL, !external_control)) {
+
+ #if BOTH(HAS_LCD_MENU, ENCODER_RATE_MULTIPLIER)
+
+ int32_t encoderMultiplier = 1;
+
+ if (encoderRateMultiplierEnabled) {
+ const float encoderMovementSteps = float(abs_diff) / epps;
+
+ if (lastEncoderMovementMillis) {
+ // Note that the rate is always calculated between two passes through the
+ // loop and that the abs of the encoderDiff value is tracked.
+ const float encoderStepRate = encoderMovementSteps / float(ms - lastEncoderMovementMillis) * 1000;
+
+ if (encoderStepRate >= ENCODER_100X_STEPS_PER_SEC) encoderMultiplier = 100;
+ else if (encoderStepRate >= ENCODER_10X_STEPS_PER_SEC) encoderMultiplier = 10;
+
+ // Enable to output the encoder steps per second value
+ //#define ENCODER_RATE_MULTIPLIER_DEBUG
+ #if ENABLED(ENCODER_RATE_MULTIPLIER_DEBUG)
+ SERIAL_ECHO_START();
+ SERIAL_ECHOPAIR("Enc Step Rate: ", encoderStepRate);
+ SERIAL_ECHOPAIR(" Multiplier: ", encoderMultiplier);
+ SERIAL_ECHOPAIR(" ENCODER_10X_STEPS_PER_SEC: ", ENCODER_10X_STEPS_PER_SEC);
+ SERIAL_ECHOPAIR(" ENCODER_100X_STEPS_PER_SEC: ", ENCODER_100X_STEPS_PER_SEC);
+ SERIAL_EOL();
+ #endif
+ }
+
+ lastEncoderMovementMillis = ms;
+ } // encoderRateMultiplierEnabled
+
+ #else
+
+ constexpr int32_t encoderMultiplier = 1;
+
+ #endif // ENCODER_RATE_MULTIPLIER
+
+ if (can_encode()) encoderPosition += (encoderDiff * encoderMultiplier) / epps;
+
+ encoderDiff = 0;
+ }
+
+ reset_status_timeout(ms);
+
+ refresh(LCDVIEW_REDRAW_NOW);
+
+ #ifdef LED_BACKLIGHT_TIMEOUT
+ if (!powersupply_on) leds.reset_timeout(ms);
+ #endif
+ }
+
+ #endif
+
+ // This runs every ~100ms when idling often enough.
+ // Instead of tracking changes just redraw the Status Screen once per second.
+ if (on_status_screen() && !lcd_status_update_delay--) {
+ lcd_status_update_delay = TERN(HAS_MARLINUI_U8GLIB, 12, 9);
+ if (max_display_update_time) max_display_update_time--; // Be sure never go to a very big number
+ refresh(LCDVIEW_REDRAW_NOW);
+ }
+
+ #if BOTH(HAS_LCD_MENU, SCROLL_LONG_FILENAMES)
+ // If scrolling of long file names is enabled and we are in the sd card menu,
+ // cause a refresh to occur until all the text has scrolled into view.
+ if (currentScreen == menu_media && !lcd_status_update_delay--) {
+ lcd_status_update_delay = ++filename_scroll_pos >= filename_scroll_max ? 12 : 4; // Long delay at end and start
+ if (filename_scroll_pos > filename_scroll_max) filename_scroll_pos = 0;
+ refresh(LCDVIEW_REDRAW_NOW);
+ reset_status_timeout(ms);
+ }
+ #endif
+
+ // Then we want to use only 50% of the time
+ const uint16_t bbr2 = planner.block_buffer_runtime() >> 1;
+
+ if ((should_draw() || drawing_screen) && (!bbr2 || bbr2 > max_display_update_time)) {
+
+ // Change state of drawing flag between screen updates
+ if (!drawing_screen) switch (lcdDrawUpdate) {
+ case LCDVIEW_CALL_NO_REDRAW:
+ refresh(LCDVIEW_NONE);
+ break;
+ case LCDVIEW_CLEAR_CALL_REDRAW:
+ case LCDVIEW_CALL_REDRAW_NEXT:
+ refresh(LCDVIEW_REDRAW_NOW);
+ case LCDVIEW_REDRAW_NOW: // set above, or by a handler through LCDVIEW_CALL_REDRAW_NEXT
+ case LCDVIEW_NONE:
+ break;
+ } // switch
+
+ TERN_(HAS_ADC_BUTTONS, keypad_buttons = 0);
+
+ #if HAS_MARLINUI_U8GLIB
+
+ #if ENABLED(LIGHTWEIGHT_UI)
+ const bool in_status = on_status_screen(),
+ do_u8g_loop = !in_status;
+ lcd_in_status(in_status);
+ if (in_status) status_screen();
+ #else
+ constexpr bool do_u8g_loop = true;
+ #endif
+
+ if (do_u8g_loop) {
+ if (!drawing_screen) { // If not already drawing pages
+ u8g.firstPage(); // Start the first page
+ drawing_screen = first_page = true; // Flag as drawing pages
+ }
+ set_font(FONT_MENU); // Setup font for every page draw
+ u8g.setColorIndex(1); // And reset the color
+ run_current_screen(); // Draw and process the current screen
+ first_page = false;
+
+ // The screen handler can clear drawing_screen for an action that changes the screen.
+ // If still drawing and there's another page, update max-time and return now.
+ // The nextPage will already be set up on the next call.
+ if (drawing_screen && (drawing_screen = u8g.nextPage())) {
+ if (on_status_screen())
+ NOLESS(max_display_update_time, millis() - ms);
+ return;
+ }
+ }
+
+ #else
+
+ run_current_screen();
+
+ #endif
+
+ TERN_(HAS_LCD_MENU, lcd_clicked = false);
+
+ // Keeping track of the longest time for an individual LCD update.
+ // Used to do screen throttling when the planner starts to fill up.
+ if (on_status_screen())
+ NOLESS(max_display_update_time, millis() - ms);
+ }
+
+ #if SCREENS_CAN_TIME_OUT
+ // Return to Status Screen after a timeout
+ if (on_status_screen() || defer_return_to_status)
+ reset_status_timeout(ms);
+ else if (ELAPSED(ms, return_to_status_ms))
+ return_to_status();
+ #endif
// Change state of drawing flag between screen updates
if (!drawing_screen) switch (lcdDrawUpdate) {
- case LCDVIEW_CALL_NO_REDRAW:
- refresh(LCDVIEW_NONE);
- break;
case LCDVIEW_CLEAR_CALL_REDRAW:
- case LCDVIEW_CALL_REDRAW_NEXT:
- refresh(LCDVIEW_REDRAW_NOW);
- case LCDVIEW_REDRAW_NOW: // set above, or by a handler through LCDVIEW_CALL_REDRAW_NEXT
+ clear_lcd(); break;
+ case LCDVIEW_REDRAW_NOW:
+ refresh(LCDVIEW_NONE);
case LCDVIEW_NONE:
- break;
+ case LCDVIEW_CALL_REDRAW_NEXT:
+ case LCDVIEW_CALL_NO_REDRAW:
+ default: break;
} // switch
- TERN_(HAS_ADC_BUTTONS, keypad_buttons = 0);
+ } // ELAPSED(ms, next_lcd_update_ms)
- #if HAS_MARLINUI_U8GLIB
+ TERN_(HAS_GRAPHICAL_TFT, tft_idle());
+ }
- #if ENABLED(LIGHTWEIGHT_UI)
- const bool in_status = on_status_screen(),
- do_u8g_loop = !in_status;
- lcd_in_status(in_status);
- if (in_status) status_screen();
- #else
- constexpr bool do_u8g_loop = true;
- #endif
+ #if HAS_ADC_BUTTONS
- if (do_u8g_loop) {
- if (!drawing_screen) { // If not already drawing pages
- u8g.firstPage(); // Start the first page
- drawing_screen = first_page = true; // Flag as drawing pages
- }
- set_font(FONT_MENU); // Setup font for every page draw
- u8g.setColorIndex(1); // And reset the color
- run_current_screen(); // Draw and process the current screen
- first_page = false;
+ typedef struct {
+ uint16_t ADCKeyValueMin, ADCKeyValueMax;
+ uint8_t ADCKeyNo;
+ } _stADCKeypadTable_;
- // The screen handler can clear drawing_screen for an action that changes the screen.
- // If still drawing and there's another page, update max-time and return now.
- // The nextPage will already be set up on the next call.
- if (drawing_screen && (drawing_screen = u8g.nextPage())) {
- if (on_status_screen())
- NOLESS(max_display_update_time, millis() - ms);
- return;
- }
- }
-
- #else
-
- run_current_screen();
-
- #endif
-
- TERN_(HAS_LCD_MENU, lcd_clicked = false);
-
- // Keeping track of the longest time for an individual LCD update.
- // Used to do screen throttling when the planner starts to fill up.
- if (on_status_screen())
- NOLESS(max_display_update_time, millis() - ms);
- }
-
- #if SCREENS_CAN_TIME_OUT
- // Return to Status Screen after a timeout
- if (on_status_screen() || defer_return_to_status)
- reset_status_timeout(ms);
- else if (ELAPSED(ms, return_to_status_ms))
- return_to_status();
+ #ifndef ADC_BUTTONS_VALUE_SCALE
+ #define ADC_BUTTONS_VALUE_SCALE 1.0 // for the power voltage equal to the reference voltage
+ #endif
+ #ifndef ADC_BUTTONS_R_PULLUP
+ #define ADC_BUTTONS_R_PULLUP 4.7 // common pull-up resistor in the voltage divider
+ #endif
+ #ifndef ADC_BUTTONS_LEFT_R_PULLDOWN
+ #define ADC_BUTTONS_LEFT_R_PULLDOWN 0.47 // pull-down resistor for LEFT button voltage divider
+ #endif
+ #ifndef ADC_BUTTONS_RIGHT_R_PULLDOWN
+ #define ADC_BUTTONS_RIGHT_R_PULLDOWN 4.7 // pull-down resistor for RIGHT button voltage divider
+ #endif
+ #ifndef ADC_BUTTONS_UP_R_PULLDOWN
+ #define ADC_BUTTONS_UP_R_PULLDOWN 1.0 // pull-down resistor for UP button voltage divider
+ #endif
+ #ifndef ADC_BUTTONS_DOWN_R_PULLDOWN
+ #define ADC_BUTTONS_DOWN_R_PULLDOWN 10.0 // pull-down resistor for DOWN button voltage divider
+ #endif
+ #ifndef ADC_BUTTONS_MIDDLE_R_PULLDOWN
+ #define ADC_BUTTONS_MIDDLE_R_PULLDOWN 2.2 // pull-down resistor for MIDDLE button voltage divider
#endif
- // Change state of drawing flag between screen updates
- if (!drawing_screen) switch (lcdDrawUpdate) {
- case LCDVIEW_CLEAR_CALL_REDRAW:
- clear_lcd(); break;
- case LCDVIEW_REDRAW_NOW:
- refresh(LCDVIEW_NONE);
- case LCDVIEW_NONE:
- case LCDVIEW_CALL_REDRAW_NEXT:
- case LCDVIEW_CALL_NO_REDRAW:
- default: break;
- } // switch
+ // Calculate the ADC value for the voltage divider with specified pull-down resistor value
+ #define ADC_BUTTON_VALUE(r) int(HAL_ADC_RANGE * (ADC_BUTTONS_VALUE_SCALE) * r / (r + ADC_BUTTONS_R_PULLUP))
- } // ELAPSED(ms, next_lcd_update_ms)
+ static constexpr uint16_t adc_button_tolerance = HAL_ADC_RANGE * 25 / 1024,
+ adc_other_button = HAL_ADC_RANGE * 1000 / 1024;
+ static const _stADCKeypadTable_ stADCKeyTable[] PROGMEM = {
+ // VALUE_MIN, VALUE_MAX, KEY
+ { adc_other_button, HAL_ADC_RANGE, 1 + BLEN_KEYPAD_F1 }, // F1
+ { adc_other_button, HAL_ADC_RANGE, 1 + BLEN_KEYPAD_F2 }, // F2
+ { adc_other_button, HAL_ADC_RANGE, 1 + BLEN_KEYPAD_F3 }, // F3
+ { ADC_BUTTON_VALUE(ADC_BUTTONS_LEFT_R_PULLDOWN) - adc_button_tolerance,
+ ADC_BUTTON_VALUE(ADC_BUTTONS_LEFT_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_LEFT }, // LEFT ( 272 ... 472)
+ { ADC_BUTTON_VALUE(ADC_BUTTONS_RIGHT_R_PULLDOWN) - adc_button_tolerance,
+ ADC_BUTTON_VALUE(ADC_BUTTONS_RIGHT_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_RIGHT }, // RIGHT (1948 ... 2148)
+ { ADC_BUTTON_VALUE(ADC_BUTTONS_UP_R_PULLDOWN) - adc_button_tolerance,
+ ADC_BUTTON_VALUE(ADC_BUTTONS_UP_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_UP }, // UP ( 618 ... 818)
+ { ADC_BUTTON_VALUE(ADC_BUTTONS_DOWN_R_PULLDOWN) - adc_button_tolerance,
+ ADC_BUTTON_VALUE(ADC_BUTTONS_DOWN_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_DOWN }, // DOWN (2686 ... 2886)
+ { ADC_BUTTON_VALUE(ADC_BUTTONS_MIDDLE_R_PULLDOWN) - adc_button_tolerance,
+ ADC_BUTTON_VALUE(ADC_BUTTONS_MIDDLE_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_MIDDLE }, // ENTER (1205 ... 1405)
+ };
- TERN_(HAS_GRAPHICAL_TFT, tft_idle());
-}
-
-#if HAS_ADC_BUTTONS
-
- typedef struct {
- uint16_t ADCKeyValueMin, ADCKeyValueMax;
- uint8_t ADCKeyNo;
- } _stADCKeypadTable_;
-
- #ifndef ADC_BUTTONS_VALUE_SCALE
- #define ADC_BUTTONS_VALUE_SCALE 1.0 // for the power voltage equal to the reference voltage
- #endif
- #ifndef ADC_BUTTONS_R_PULLUP
- #define ADC_BUTTONS_R_PULLUP 4.7 // common pull-up resistor in the voltage divider
- #endif
- #ifndef ADC_BUTTONS_LEFT_R_PULLDOWN
- #define ADC_BUTTONS_LEFT_R_PULLDOWN 0.47 // pull-down resistor for LEFT button voltage divider
- #endif
- #ifndef ADC_BUTTONS_RIGHT_R_PULLDOWN
- #define ADC_BUTTONS_RIGHT_R_PULLDOWN 4.7 // pull-down resistor for RIGHT button voltage divider
- #endif
- #ifndef ADC_BUTTONS_UP_R_PULLDOWN
- #define ADC_BUTTONS_UP_R_PULLDOWN 1.0 // pull-down resistor for UP button voltage divider
- #endif
- #ifndef ADC_BUTTONS_DOWN_R_PULLDOWN
- #define ADC_BUTTONS_DOWN_R_PULLDOWN 10.0 // pull-down resistor for DOWN button voltage divider
- #endif
- #ifndef ADC_BUTTONS_MIDDLE_R_PULLDOWN
- #define ADC_BUTTONS_MIDDLE_R_PULLDOWN 2.2 // pull-down resistor for MIDDLE button voltage divider
- #endif
-
- // Calculate the ADC value for the voltage divider with specified pull-down resistor value
- #define ADC_BUTTON_VALUE(r) int(HAL_ADC_RANGE * (ADC_BUTTONS_VALUE_SCALE) * r / (r + ADC_BUTTONS_R_PULLUP))
-
- static constexpr uint16_t adc_button_tolerance = HAL_ADC_RANGE * 25 / 1024,
- adc_other_button = HAL_ADC_RANGE * 1000 / 1024;
- static const _stADCKeypadTable_ stADCKeyTable[] PROGMEM = {
- // VALUE_MIN, VALUE_MAX, KEY
- { adc_other_button, HAL_ADC_RANGE, 1 + BLEN_KEYPAD_F1 }, // F1
- { adc_other_button, HAL_ADC_RANGE, 1 + BLEN_KEYPAD_F2 }, // F2
- { adc_other_button, HAL_ADC_RANGE, 1 + BLEN_KEYPAD_F3 }, // F3
- { ADC_BUTTON_VALUE(ADC_BUTTONS_LEFT_R_PULLDOWN) - adc_button_tolerance,
- ADC_BUTTON_VALUE(ADC_BUTTONS_LEFT_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_LEFT }, // LEFT ( 272 ... 472)
- { ADC_BUTTON_VALUE(ADC_BUTTONS_RIGHT_R_PULLDOWN) - adc_button_tolerance,
- ADC_BUTTON_VALUE(ADC_BUTTONS_RIGHT_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_RIGHT }, // RIGHT (1948 ... 2148)
- { ADC_BUTTON_VALUE(ADC_BUTTONS_UP_R_PULLDOWN) - adc_button_tolerance,
- ADC_BUTTON_VALUE(ADC_BUTTONS_UP_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_UP }, // UP ( 618 ... 818)
- { ADC_BUTTON_VALUE(ADC_BUTTONS_DOWN_R_PULLDOWN) - adc_button_tolerance,
- ADC_BUTTON_VALUE(ADC_BUTTONS_DOWN_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_DOWN }, // DOWN (2686 ... 2886)
- { ADC_BUTTON_VALUE(ADC_BUTTONS_MIDDLE_R_PULLDOWN) - adc_button_tolerance,
- ADC_BUTTON_VALUE(ADC_BUTTONS_MIDDLE_R_PULLDOWN) + adc_button_tolerance, 1 + BLEN_KEYPAD_MIDDLE }, // ENTER (1205 ... 1405)
- };
-
- uint8_t get_ADC_keyValue() {
- if (thermalManager.ADCKey_count >= 16) {
- const uint16_t currentkpADCValue = thermalManager.current_ADCKey_raw;
- thermalManager.current_ADCKey_raw = HAL_ADC_RANGE;
- thermalManager.ADCKey_count = 0;
- if (currentkpADCValue < adc_other_button)
- LOOP_L_N(i, ADC_KEY_NUM) {
- const uint16_t lo = pgm_read_word(&stADCKeyTable[i].ADCKeyValueMin),
- hi = pgm_read_word(&stADCKeyTable[i].ADCKeyValueMax);
- if (WITHIN(currentkpADCValue, lo, hi)) return pgm_read_byte(&stADCKeyTable[i].ADCKeyNo);
- }
- }
- return 0;
- }
-
-#endif // HAS_ADC_BUTTONS
-
-#if HAS_ENCODER_ACTION
-
- /**
- * Read encoder buttons from the hardware registers
- * Warning: This function is called from interrupt context!
- */
- void MarlinUI::update_buttons() {
- const millis_t now = millis();
- if (ELAPSED(now, next_button_update_ms)) {
-
- #if HAS_DIGITAL_BUTTONS
-
- #if ANY_BUTTON(EN1, EN2, ENC, BACK)
-
- uint8_t newbutton = 0;
- if (BUTTON_PRESSED(EN1)) newbutton |= EN_A;
- if (BUTTON_PRESSED(EN2)) newbutton |= EN_B;
- if (can_encode() && BUTTON_PRESSED(ENC)) newbutton |= EN_C;
- if (BUTTON_PRESSED(BACK)) newbutton |= EN_D;
-
- #else
-
- constexpr uint8_t newbutton = 0;
-
- #endif
-
- //
- // Directional buttons
- //
- #if ANY_BUTTON(UP, DWN, LFT, RT)
-
- const int8_t pulses = epps * encoderDirection;
-
- if (BUTTON_PRESSED(UP)) {
- encoderDiff = (ENCODER_STEPS_PER_MENU_ITEM) * pulses;
- next_button_update_ms = now + 300;
+ uint8_t get_ADC_keyValue() {
+ if (thermalManager.ADCKey_count >= 16) {
+ const uint16_t currentkpADCValue = thermalManager.current_ADCKey_raw;
+ thermalManager.current_ADCKey_raw = HAL_ADC_RANGE;
+ thermalManager.ADCKey_count = 0;
+ if (currentkpADCValue < adc_other_button)
+ LOOP_L_N(i, ADC_KEY_NUM) {
+ const uint16_t lo = pgm_read_word(&stADCKeyTable[i].ADCKeyValueMin),
+ hi = pgm_read_word(&stADCKeyTable[i].ADCKeyValueMax);
+ if (WITHIN(currentkpADCValue, lo, hi)) return pgm_read_byte(&stADCKeyTable[i].ADCKeyNo);
}
- else if (BUTTON_PRESSED(DWN)) {
- encoderDiff = -(ENCODER_STEPS_PER_MENU_ITEM) * pulses;
- next_button_update_ms = now + 300;
- }
- else if (BUTTON_PRESSED(LFT)) {
- encoderDiff = -pulses;
- next_button_update_ms = now + 300;
- }
- else if (BUTTON_PRESSED(RT)) {
- encoderDiff = pulses;
- next_button_update_ms = now + 300;
- }
-
- #endif // UP || DWN || LFT || RT
-
- buttons = (newbutton | TERN0(HAS_SLOW_BUTTONS, slow_buttons)
- #if BOTH(HAS_TOUCH_BUTTONS, HAS_ENCODER_ACTION)
- | (touch_buttons & TERN(HAS_ENCODER_WHEEL, ~(EN_A | EN_B), 0xFF))
- #endif
- );
-
- #elif HAS_ADC_BUTTONS
-
- buttons = 0;
-
- #endif
-
- #if HAS_ADC_BUTTONS
- if (keypad_buttons == 0) {
- const uint8_t b = get_ADC_keyValue();
- if (WITHIN(b, 1, 8)) keypad_buttons = _BV(b - 1);
- }
- #endif
-
- #if HAS_SHIFT_ENCODER
- /**
- * Set up Rotary Encoder bit values (for two pin encoders to indicate movement).
- * These values are independent of which pins are used for EN_A / EN_B indications.
- * The rotary encoder part is also independent of the LCD chipset.
- */
- uint8_t val = 0;
- WRITE(SHIFT_LD_PIN, LOW);
- WRITE(SHIFT_LD_PIN, HIGH);
- LOOP_L_N(i, 8) {
- val >>= 1;
- if (READ(SHIFT_OUT_PIN)) SBI(val, 7);
- WRITE(SHIFT_CLK_PIN, HIGH);
- WRITE(SHIFT_CLK_PIN, LOW);
- }
- TERN(REPRAPWORLD_KEYPAD, keypad_buttons, buttons) = ~val;
- #endif
-
- #if IS_TFTGLCD_PANEL
- next_button_update_ms = now + (LCD_UPDATE_INTERVAL / 2);
- buttons = slow_buttons;
- TERN_(AUTO_BED_LEVELING_UBL, external_encoder());
- #endif
-
- } // next_button_update_ms
-
- #if HAS_ENCODER_WHEEL
- static uint8_t lastEncoderBits;
-
- // Manage encoder rotation
- #define ENCODER_SPIN(_E1, _E2) switch (lastEncoderBits) { case _E1: encoderDiff += encoderDirection; break; case _E2: encoderDiff -= encoderDirection; }
-
- uint8_t enc = 0;
- if (buttons & EN_A) enc |= B01;
- if (buttons & EN_B) enc |= B10;
- if (enc != lastEncoderBits) {
- switch (enc) {
- case ENCODER_PHASE_0: ENCODER_SPIN(ENCODER_PHASE_3, ENCODER_PHASE_1); break;
- case ENCODER_PHASE_1: ENCODER_SPIN(ENCODER_PHASE_0, ENCODER_PHASE_2); break;
- case ENCODER_PHASE_2: ENCODER_SPIN(ENCODER_PHASE_1, ENCODER_PHASE_3); break;
- case ENCODER_PHASE_3: ENCODER_SPIN(ENCODER_PHASE_2, ENCODER_PHASE_0); break;
- }
- #if BOTH(HAS_LCD_MENU, AUTO_BED_LEVELING_UBL)
- external_encoder();
- #endif
- lastEncoderBits = enc;
}
+ return 0;
+ }
- #endif // HAS_ENCODER_WHEEL
- }
+ #endif // HAS_ADC_BUTTONS
-#endif // HAS_ENCODER_ACTION
+ #if HAS_ENCODER_ACTION
+
+ /**
+ * Read encoder buttons from the hardware registers
+ * Warning: This function is called from interrupt context!
+ */
+ void MarlinUI::update_buttons() {
+ const millis_t now = millis();
+ if (ELAPSED(now, next_button_update_ms)) {
+
+ #if HAS_DIGITAL_BUTTONS
+
+ #if ANY_BUTTON(EN1, EN2, ENC, BACK)
+
+ uint8_t newbutton = 0;
+ if (BUTTON_PRESSED(EN1)) newbutton |= EN_A;
+ if (BUTTON_PRESSED(EN2)) newbutton |= EN_B;
+ if (can_encode() && BUTTON_PRESSED(ENC)) newbutton |= EN_C;
+ if (BUTTON_PRESSED(BACK)) newbutton |= EN_D;
+
+ #else
+
+ constexpr uint8_t newbutton = 0;
+
+ #endif
+
+ //
+ // Directional buttons
+ //
+ #if ANY_BUTTON(UP, DWN, LFT, RT)
+
+ const int8_t pulses = epps * encoderDirection;
+
+ if (BUTTON_PRESSED(UP)) {
+ encoderDiff = (ENCODER_STEPS_PER_MENU_ITEM) * pulses;
+ next_button_update_ms = now + 300;
+ }
+ else if (BUTTON_PRESSED(DWN)) {
+ encoderDiff = -(ENCODER_STEPS_PER_MENU_ITEM) * pulses;
+ next_button_update_ms = now + 300;
+ }
+ else if (BUTTON_PRESSED(LFT)) {
+ encoderDiff = -pulses;
+ next_button_update_ms = now + 300;
+ }
+ else if (BUTTON_PRESSED(RT)) {
+ encoderDiff = pulses;
+ next_button_update_ms = now + 300;
+ }
+
+ #endif // UP || DWN || LFT || RT
+
+ buttons = (newbutton | TERN0(HAS_SLOW_BUTTONS, slow_buttons)
+ #if BOTH(HAS_TOUCH_BUTTONS, HAS_ENCODER_ACTION)
+ | (touch_buttons & TERN(HAS_ENCODER_WHEEL, ~(EN_A | EN_B), 0xFF))
+ #endif
+ );
+
+ #elif HAS_ADC_BUTTONS
+
+ buttons = 0;
+
+ #endif
+
+ #if HAS_ADC_BUTTONS
+ if (keypad_buttons == 0) {
+ const uint8_t b = get_ADC_keyValue();
+ if (WITHIN(b, 1, 8)) keypad_buttons = _BV(b - 1);
+ }
+ #endif
+
+ #if HAS_SHIFT_ENCODER
+ /**
+ * Set up Rotary Encoder bit values (for two pin encoders to indicate movement).
+ * These values are independent of which pins are used for EN_A / EN_B indications.
+ * The rotary encoder part is also independent of the LCD chipset.
+ */
+ uint8_t val = 0;
+ WRITE(SHIFT_LD_PIN, LOW);
+ WRITE(SHIFT_LD_PIN, HIGH);
+ LOOP_L_N(i, 8) {
+ val >>= 1;
+ if (READ(SHIFT_OUT_PIN)) SBI(val, 7);
+ WRITE(SHIFT_CLK_PIN, HIGH);
+ WRITE(SHIFT_CLK_PIN, LOW);
+ }
+ TERN(REPRAPWORLD_KEYPAD, keypad_buttons, buttons) = ~val;
+ #endif
+
+ #if IS_TFTGLCD_PANEL
+ next_button_update_ms = now + (LCD_UPDATE_INTERVAL / 2);
+ buttons = slow_buttons;
+ TERN_(AUTO_BED_LEVELING_UBL, external_encoder());
+ #endif
+
+ } // next_button_update_ms
+
+ #if HAS_ENCODER_WHEEL
+ static uint8_t lastEncoderBits;
+
+ // Manage encoder rotation
+ #define ENCODER_SPIN(_E1, _E2) switch (lastEncoderBits) { case _E1: encoderDiff += encoderDirection; break; case _E2: encoderDiff -= encoderDirection; }
+
+ uint8_t enc = 0;
+ if (buttons & EN_A) enc |= B01;
+ if (buttons & EN_B) enc |= B10;
+ if (enc != lastEncoderBits) {
+ switch (enc) {
+ case ENCODER_PHASE_0: ENCODER_SPIN(ENCODER_PHASE_3, ENCODER_PHASE_1); break;
+ case ENCODER_PHASE_1: ENCODER_SPIN(ENCODER_PHASE_0, ENCODER_PHASE_2); break;
+ case ENCODER_PHASE_2: ENCODER_SPIN(ENCODER_PHASE_1, ENCODER_PHASE_3); break;
+ case ENCODER_PHASE_3: ENCODER_SPIN(ENCODER_PHASE_2, ENCODER_PHASE_0); break;
+ }
+ #if BOTH(HAS_LCD_MENU, AUTO_BED_LEVELING_UBL)
+ external_encoder();
+ #endif
+ lastEncoderBits = enc;
+ }
+
+ #endif // HAS_ENCODER_WHEEL
+ }
+
+ #endif // HAS_ENCODER_ACTION
#endif // HAS_WIRED_LCD
diff --git a/Marlin/src/lcd/marlinui.h b/Marlin/src/lcd/marlinui.h
index 87ecc48366..7531fae674 100644
--- a/Marlin/src/lcd/marlinui.h
+++ b/Marlin/src/lcd/marlinui.h
@@ -529,10 +529,13 @@ public:
static void draw_select_screen_prompt(PGM_P const pref, const char * const string=nullptr, PGM_P const suff=nullptr);
- #elif HAS_WIRED_LCD
+ #else
static constexpr bool on_status_screen() { return true; }
- FORCE_INLINE static void run_current_screen() { status_screen(); }
+
+ #if HAS_WIRED_LCD
+ FORCE_INLINE static void run_current_screen() { status_screen(); }
+ #endif
#endif
diff --git a/Marlin/src/lcd/menu/menu_filament.cpp b/Marlin/src/lcd/menu/menu_filament.cpp
index c6b8568085..d70ed98aa8 100644
--- a/Marlin/src/lcd/menu/menu_filament.cpp
+++ b/Marlin/src/lcd/menu/menu_filament.cpp
@@ -106,10 +106,11 @@ void _menu_temp_filament_op(const PauseMode mode, const int8_t extruder) {
* "Change Filament" submenu
*/
#if E_STEPPERS > 1 || ENABLED(FILAMENT_LOAD_UNLOAD_GCODES)
-
bool printingIsPaused();
+#endif
- void menu_change_filament() {
+void menu_change_filament() {
+ #if E_STEPPERS > 1 || ENABLED(FILAMENT_LOAD_UNLOAD_GCODES)
// Say "filament change" when no print is active
editable.int8 = printingIsPaused() ? PAUSE_MODE_PAUSE_PRINT : PAUSE_MODE_CHANGE_FILAMENT;
@@ -204,8 +205,16 @@ void _menu_temp_filament_op(const PauseMode mode, const int8_t extruder) {
#endif
END_MENU();
- }
-#endif
+
+ #else
+
+ if (thermalManager.targetHotEnoughToExtrude(active_extruder))
+ queue.inject_P(PSTR("M600B0"));
+ else
+ _menu_temp_filament_op(PAUSE_MODE_CHANGE_FILAMENT, 0);
+
+ #endif
+}
static uint8_t hotend_status_extruder = 0;
diff --git a/Marlin/src/lcd/menu/menu_main.cpp b/Marlin/src/lcd/menu/menu_main.cpp
index 1e79edb864..4e3310f238 100644
--- a/Marlin/src/lcd/menu/menu_main.cpp
+++ b/Marlin/src/lcd/menu/menu_main.cpp
@@ -77,7 +77,6 @@ void menu_configuration();
#endif
#if ENABLED(ADVANCED_PAUSE_FEATURE)
- void _menu_temp_filament_op(const PauseMode, const int8_t);
void menu_change_filament();
#endif
@@ -365,10 +364,11 @@ void menu_main() {
#if ENABLED(ADVANCED_PAUSE_FEATURE)
#if E_STEPPERS == 1 && DISABLED(FILAMENT_LOAD_UNLOAD_GCODES)
- if (thermalManager.targetHotEnoughToExtrude(active_extruder))
- GCODES_ITEM(MSG_FILAMENTCHANGE, PSTR("M600 B0"));
- else
- SUBMENU(MSG_FILAMENTCHANGE, []{ _menu_temp_filament_op(PAUSE_MODE_CHANGE_FILAMENT, 0); });
+ CONFIRM_ITEM(MSG_FILAMENTCHANGE,
+ MSG_YES, MSG_NO,
+ menu_change_filament, ui.goto_previous_screen,
+ GET_TEXT(MSG_FILAMENTCHANGE), (const char *)nullptr, PSTR("?")
+ );
#else
SUBMENU(MSG_FILAMENTCHANGE, menu_change_filament);
#endif
diff --git a/Marlin/src/pins/lpc1769/pins_BTT_SKR_E3_TURBO.h b/Marlin/src/pins/lpc1769/pins_BTT_SKR_E3_TURBO.h
index 1e7b3f02a2..4719dd8111 100644
--- a/Marlin/src/pins/lpc1769/pins_BTT_SKR_E3_TURBO.h
+++ b/Marlin/src/pins/lpc1769/pins_BTT_SKR_E3_TURBO.h
@@ -201,7 +201,15 @@
#define EXP1_09_PIN P0_16
#define EXP1_10_PIN P2_08
-#if HAS_WIRED_LCD
+#if ENABLED(DWIN_CREALITY_LCD)
+ #error "DWIN_CREALITY_LCD requires a custom cable with TX = P0_15, RX = P0_16, and LCD_SERIAL_PORT 1. Comment out this line to continue."
+
+ #define BEEPER_PIN EXP1_10_PIN
+ #define BTN_EN1 EXP1_03_PIN
+ #define BTN_EN2 EXP1_04_PIN
+ #define BTN_ENC EXP1_06_PIN
+
+#elif HAS_WIRED_LCD
#if ENABLED(CR10_STOCKDISPLAY)
diff --git a/buildroot/share/PlatformIO/boards/marlin_blackSTM32F407VET6.json b/buildroot/share/PlatformIO/boards/marlin_blackSTM32F407VET6.json
index 1765634086..a3f130c6b1 100644
--- a/buildroot/share/PlatformIO/boards/marlin_blackSTM32F407VET6.json
+++ b/buildroot/share/PlatformIO/boards/marlin_blackSTM32F407VET6.json
@@ -48,7 +48,7 @@
"upload": {
"disable_flushing": false,
"maximum_ram_size": 131072,
- "maximum_size": 514288,
+ "maximum_size": 524288,
"protocol": "stlink",
"protocols": [
"stlink",
diff --git a/buildroot/share/PlatformIO/scripts/custom_board.py b/buildroot/share/PlatformIO/scripts/custom_board.py
index 5d3ca3c652..e462738190 100644
--- a/buildroot/share/PlatformIO/scripts/custom_board.py
+++ b/buildroot/share/PlatformIO/scripts/custom_board.py
@@ -1,6 +1,9 @@
#
# buildroot/share/PlatformIO/scripts/custom_board.py
#
+# - For build.address replace VECT_TAB_ADDR to relocate the firmware
+# - For build.ldscript use one of the linker scripts in buildroot/share/PlatformIO/ldscripts
+#
import marlin
board = marlin.env.BoardConfig()
diff --git a/buildroot/share/PlatformIO/scripts/marlin.py b/buildroot/share/PlatformIO/scripts/marlin.py
index 23c1b95742..3949037904 100644
--- a/buildroot/share/PlatformIO/scripts/marlin.py
+++ b/buildroot/share/PlatformIO/scripts/marlin.py
@@ -67,11 +67,3 @@ def encrypt_mks(source, target, env, new_name):
def add_post_action(action):
env.AddPostAction(join("$BUILD_DIR", "${PROGNAME}.bin"), action);
-
-# Apply customizations for a MKS Robin
-def prepare_robin(address, ldname, fwname):
- def encrypt(source, target, env):
- encrypt_mks(source, target, env, fwname)
- relocate_firmware(address)
- custom_ld_script(ldname)
- add_post_action(encrypt);
diff --git a/buildroot/share/PlatformIO/scripts/mks_robin.py b/buildroot/share/PlatformIO/scripts/mks_robin.py
index 8c5e4ae276..2dea7c615f 100644
--- a/buildroot/share/PlatformIO/scripts/mks_robin.py
+++ b/buildroot/share/PlatformIO/scripts/mks_robin.py
@@ -1,5 +1,5 @@
#
# buildroot/share/PlatformIO/scripts/mks_robin.py
#
-import marlin
-marlin.prepare_robin("0x08007000", "mks_robin.ld", "Robin.bin")
+import robin
+robin.prepare("0x08007000", "mks_robin.ld", "Robin.bin")
diff --git a/buildroot/share/PlatformIO/scripts/mks_robin_e3.py b/buildroot/share/PlatformIO/scripts/mks_robin_e3.py
index 7f6f538d6a..6ddeccbf80 100644
--- a/buildroot/share/PlatformIO/scripts/mks_robin_e3.py
+++ b/buildroot/share/PlatformIO/scripts/mks_robin_e3.py
@@ -1,5 +1,5 @@
#
# buildroot/share/PlatformIO/scripts/mks_robin_e3.py
#
-import marlin
-marlin.prepare_robin("0x08005000", "mks_robin_e3.ld", "Robin_e3.bin")
+import robin
+robin.prepare("0x08005000", "mks_robin_e3.ld", "Robin_e3.bin")
diff --git a/buildroot/share/PlatformIO/scripts/mks_robin_e3p.py b/buildroot/share/PlatformIO/scripts/mks_robin_e3p.py
index 1f3cacf873..5eeb93c096 100644
--- a/buildroot/share/PlatformIO/scripts/mks_robin_e3p.py
+++ b/buildroot/share/PlatformIO/scripts/mks_robin_e3p.py
@@ -1,5 +1,5 @@
#
# buildroot/share/PlatformIO/scripts/mks_robin_e3p.py
#
-import marlin
-marlin.prepare_robin("0x08007000", "mks_robin_e3p.ld", "Robin_e3p.bin")
+import robin
+robin.prepare("0x08007000", "mks_robin_e3p.ld", "Robin_e3p.bin")
diff --git a/buildroot/share/PlatformIO/scripts/mks_robin_lite.py b/buildroot/share/PlatformIO/scripts/mks_robin_lite.py
index b8c039ada8..c2018336fd 100644
--- a/buildroot/share/PlatformIO/scripts/mks_robin_lite.py
+++ b/buildroot/share/PlatformIO/scripts/mks_robin_lite.py
@@ -1,5 +1,5 @@
#
# buildroot/share/PlatformIO/scripts/mks_robin_lite.py
#
-import marlin
-marlin.prepare_robin("0x08005000", "mks_robin_lite.ld", "mksLite.bin")
+import robin
+robin.prepare("0x08005000", "mks_robin_lite.ld", "mksLite.bin")
diff --git a/buildroot/share/PlatformIO/scripts/mks_robin_lite3.py b/buildroot/share/PlatformIO/scripts/mks_robin_lite3.py
index bea8b80ace..42c8fb18b6 100644
--- a/buildroot/share/PlatformIO/scripts/mks_robin_lite3.py
+++ b/buildroot/share/PlatformIO/scripts/mks_robin_lite3.py
@@ -1,5 +1,5 @@
#
# buildroot/share/PlatformIO/scripts/mks_robin_lite3.py
#
-import marlin
-marlin.prepare_robin("0x08005000", "mks_robin_lite.ld", "mksLite3.bin")
+import robin
+robin.prepare("0x08005000", "mks_robin_lite.ld", "mksLite3.bin")
diff --git a/buildroot/share/PlatformIO/scripts/mks_robin_mini.py b/buildroot/share/PlatformIO/scripts/mks_robin_mini.py
index 3ff9ccf4a6..b0d8388653 100644
--- a/buildroot/share/PlatformIO/scripts/mks_robin_mini.py
+++ b/buildroot/share/PlatformIO/scripts/mks_robin_mini.py
@@ -1,5 +1,5 @@
#
# buildroot/share/PlatformIO/scripts/mks_robin_mini.py
#
-import marlin
-marlin.prepare_robin("0x08007000", "mks_robin_mini.ld", "Robin_mini.bin")
+import robin
+robin.prepare("0x08007000", "mks_robin_mini.ld", "Robin_mini.bin")
diff --git a/buildroot/share/PlatformIO/scripts/mks_robin_nano.py b/buildroot/share/PlatformIO/scripts/mks_robin_nano.py
index 319b4d4982..35e99830c4 100644
--- a/buildroot/share/PlatformIO/scripts/mks_robin_nano.py
+++ b/buildroot/share/PlatformIO/scripts/mks_robin_nano.py
@@ -1,5 +1,5 @@
#
# buildroot/share/PlatformIO/scripts/mks_robin_nano.py
#
-import marlin
-marlin.prepare_robin("0x08007000", "mks_robin_nano.ld", "Robin_nano.bin")
+import robin
+robin.prepare("0x08007000", "mks_robin_nano.ld", "Robin_nano.bin")
diff --git a/buildroot/share/PlatformIO/scripts/mks_robin_nano35.py b/buildroot/share/PlatformIO/scripts/mks_robin_nano35.py
index 310c3d6606..4a5726ad5b 100644
--- a/buildroot/share/PlatformIO/scripts/mks_robin_nano35.py
+++ b/buildroot/share/PlatformIO/scripts/mks_robin_nano35.py
@@ -1,5 +1,5 @@
#
# buildroot/share/PlatformIO/scripts/mks_robin_nano35.py
#
-import marlin
-marlin.prepare_robin("0x08007000", "mks_robin_nano.ld", "Robin_nano35.bin")
+import robin
+robin.prepare("0x08007000", "mks_robin_nano.ld", "Robin_nano35.bin")
diff --git a/buildroot/share/PlatformIO/scripts/mks_robin_pro.py b/buildroot/share/PlatformIO/scripts/mks_robin_pro.py
index c624663a33..60e2482bb0 100644
--- a/buildroot/share/PlatformIO/scripts/mks_robin_pro.py
+++ b/buildroot/share/PlatformIO/scripts/mks_robin_pro.py
@@ -1,5 +1,5 @@
#
# buildroot/share/PlatformIO/scripts/mks_robin_pro.py
#
-import marlin
-marlin.prepare_robin("0x08007000", "mks_robin_pro.ld", "Robin_pro.bin")
+import robin
+robin.prepare("0x08007000", "mks_robin_pro.ld", "Robin_pro.bin")
diff --git a/buildroot/share/PlatformIO/scripts/robin.py b/buildroot/share/PlatformIO/scripts/robin.py
new file mode 100644
index 0000000000..50d0d92d2f
--- /dev/null
+++ b/buildroot/share/PlatformIO/scripts/robin.py
@@ -0,0 +1,12 @@
+#
+# buildroot/share/PlatformIO/scripts/robin.py
+#
+import marlin
+
+# Apply customizations for a MKS Robin
+def prepare(address, ldname, fwname):
+ def encrypt(source, target, env):
+ marlin.encrypt_mks(source, target, env, fwname)
+ marlin.relocate_firmware(address)
+ marlin.custom_ld_script(ldname)
+ marlin.add_post_action(encrypt);
diff --git a/buildroot/share/PlatformIO/scripts/stm32_bootloader.py b/buildroot/share/PlatformIO/scripts/stm32_bootloader.py
index eb28b901d2..f3b1b273a2 100644
--- a/buildroot/share/PlatformIO/scripts/stm32_bootloader.py
+++ b/buildroot/share/PlatformIO/scripts/stm32_bootloader.py
@@ -1,6 +1,13 @@
#
# stm32_bootloader.py
#
+# - If 'build.offset' is provided, either by JSON or by the environment...
+# - Set linker flag LD_FLASH_OFFSET and relocate the VTAB based on 'build.offset'.
+# - Set linker flag LD_MAX_DATA_SIZE based on 'build.maximum_ram_size'.
+# - Define STM32_FLASH_SIZE from 'upload.maximum_size' for use by Flash-based EEPROM emulation.
+#
+# - For 'board_build.rename' add a post-action to rename the firmware file.
+#
import os,sys,marlin
Import("env")
diff --git a/ini/stm32f1.ini b/ini/stm32f1.ini
index 3f09a45c7b..0b394d1730 100644
--- a/ini/stm32f1.ini
+++ b/ini/stm32f1.ini
@@ -41,7 +41,6 @@ board = genericSTM32F103RC
monitor_speed = 115200
board_build.core = stm32
board_build.variant = MARLIN_F103Rx
-board_build.ldscript = ldscript.ld
extra_scripts = ${common_stm32.extra_scripts}
pre:buildroot/share/PlatformIO/scripts/generic_create_variant.py
buildroot/share/PlatformIO/scripts/stm32_bootloader.py
@@ -108,7 +107,6 @@ extends = common_stm32
board = genericSTM32F103ZE
board_build.core = stm32
board_build.variant = MARLIN_F103Zx
-board_build.ldscript = ldscript.ld
board_build.offset = 0x7000
board_build.encrypt = Robin.bin
build_flags = ${common_stm32.build_flags}
@@ -150,7 +148,6 @@ monitor_speed = 115200
board_build.core = stm32
board_build.variant = MARLIN_F103Rx
board_build.offset = 0x7000
-board_build.ldscript = ldscript.ld
board_upload.offset_address = 0x08007000
build_unflags = ${common_stm32.build_unflags} -DUSBCON -DUSBD_USE_CDC
extra_scripts = ${common.extra_scripts}
@@ -175,7 +172,6 @@ monitor_speed = 115200
board_build.core = stm32
board_build.variant = MARLIN_F103Rx
board_build.offset = 0x7000
-board_build.ldscript = ldscript.ld
board_upload.offset_address = 0x08007000
build_unflags = ${common_stm32.build_unflags}
extra_scripts = ${common.extra_scripts}
@@ -207,7 +203,6 @@ build_flags = ${common_stm32.build_flags} -DMCU_STM32F103VE -DSS_TIMER=
board = genericSTM32F103VE
board_build.core = stm32
board_build.variant = MARLIN_F103Vx
-board_build.ldscript = ldscript.ld
board_build.offset = 0x7000
board_build.encrypt = Robin_mini.bin
board_upload.offset_address = 0x08007000
@@ -227,7 +222,6 @@ build_flags = ${common_stm32.build_flags} -DMCU_STM32F103VE -DSS_TIMER=
board = genericSTM32F103VE
board_build.core = stm32
board_build.variant = MARLIN_F103Vx
-board_build.ldscript = ldscript.ld
board_build.offset = 0x7000
board_build.encrypt = Robin_nano35.bin
board_upload.offset_address = 0x08007000
@@ -248,7 +242,6 @@ extends = common_stm32
board = genericSTM32F103ZE
board_build.core = stm32
board_build.variant = MARLIN_F103Zx
-board_build.ldscript = ldscript.ld
board_build.offset = 0x10000
build_flags = ${common_stm32.build_flags} -DENABLE_HWSERIAL3 -DTIMER_SERIAL=TIM5
build_unflags = ${common_stm32.build_unflags} -DUSBCON -DUSBD_USE_CDC
diff --git a/ini/stm32f4.ini b/ini/stm32f4.ini
index ee88135eca..e7695dcc7a 100644
--- a/ini/stm32f4.ini
+++ b/ini/stm32f4.ini
@@ -112,7 +112,6 @@ build_flags = ${common_stm32.build_flags} -DHAL_SD_MODULE_ENABLED -DHAL
board = marlin_STM32F407VGT6_CCM
board_build.core = stm32
board_build.variant = MARLIN_F4x7Vx
-board_build.ldscript = ldscript.ld
board_build.encrypt = firmware.srec
# Just openblt.py (not stm32_bootloader.py) generates the file
board_build.offset = 0x10000
@@ -222,7 +221,6 @@ extends = common_stm32
board = marlin_STM32F407VGT6_CCM
board_build.core = stm32
board_build.variant = MARLIN_F4x7Vx
-board_build.ldscript = ldscript.ld
board_build.offset = 0x8000
board_upload.offset_address = 0x08008000
extra_scripts = ${common.extra_scripts}
@@ -344,7 +342,6 @@ upload_protocol = dfu
monitor_speed = 500000
board_build.core = stm32
board_build.variant = MARLIN_F446VE
-board_build.ldscript = ldscript.ld
board_build.offset = 0x0000
extra_scripts = ${common.extra_scripts}
pre:buildroot/share/PlatformIO/scripts/generic_create_variant.py
@@ -361,7 +358,6 @@ build_flags = ${stm_flash_drive.build_flags}
board = genericSTM32F407VET6
board_build.core = stm32
board_build.variant = MARLIN_F4x7Vx
-board_build.ldscript = ldscript.ld
board_build.offset = 0x0000
board_upload.offset_address = 0x08000000
build_unflags = ${common_stm32.build_unflags} -DUSBCON -DUSBD_USE_CDC
@@ -387,7 +383,6 @@ build_flags = ${common_stm32.build_flags} ${stm32f4_I2C1.build_flags} -
board = marlin_STM32F407VGT6_CCM
board_build.core = stm32
board_build.variant = MARLIN_F4x7Vx
-board_build.ldscript = ldscript.ld
board_build.rename = Robin_nano_v3.bin
board_build.offset = 0xC000
board_upload.offset_address = 0x0800C000