diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h
index 9090eb5a61..278853ac60 100644
--- a/Marlin/Configuration_adv.h
+++ b/Marlin/Configuration_adv.h
@@ -1675,6 +1675,7 @@
//#define NO_SD_AUTOSTART // Remove auto#.g file support completely to save some Flash, SRAM
//#define MENU_ADDAUTOSTART // Add a menu option to run auto#.g files
+ //#define ONE_CLICK_PRINT // Prompt to print the newest file on inserted media
//#define BROWSE_MEDIA_ON_INSERT // Open the file browser when media is inserted
//#define MEDIA_MENU_AT_TOP // Force the media menu to be listed on the top of the main menu
diff --git a/Marlin/src/inc/SanityCheck.h b/Marlin/src/inc/SanityCheck.h
index 53333d8727..2a9fdde2dc 100644
--- a/Marlin/src/inc/SanityCheck.h
+++ b/Marlin/src/inc/SanityCheck.h
@@ -4044,6 +4044,19 @@ static_assert(_PLUS_TEST(3), "DEFAULT_MAX_ACCELERATION values must be positive."
// Multi-Stepping Limit
static_assert(WITHIN(MULTISTEPPING_LIMIT, 1, 128) && IS_POWER_OF_2(MULTISTEPPING_LIMIT), "MULTISTEPPING_LIMIT must be 1, 2, 4, 8, 16, 32, 64, or 128.");
+// One Click Print
+#if ENABLED(ONE_CLICK_PRINT)
+ #if !HAS_MEDIA
+ #error "SD Card or Flash Drive is required for ONE_CLICK_PRINT."
+ #elif ENABLED(BROWSE_MEDIA_ON_INSERT)
+ #error "ONE_CLICK_PRINT is incompatible with BROWSE_MEDIA_ON_INSERT."
+ #elif DISABLED(NO_SD_AUTOSTART)
+ #error "NO_SD_AUTOSTART must be enabled for ONE_CLICK_PRINT."
+ #elif !defined(HAS_MARLINUI_MENU)
+ #error "ONE_CLICK_PRINT needs a display that has Marlin UI menus."
+ #endif
+#endif
+
// Misc. Cleanup
#undef _TEST_PWM
#undef _NUM_AXES_STR
diff --git a/Marlin/src/lcd/marlinui.cpp b/Marlin/src/lcd/marlinui.cpp
index 638a133184..e8956ab9e1 100644
--- a/Marlin/src/lcd/marlinui.cpp
+++ b/Marlin/src/lcd/marlinui.cpp
@@ -424,7 +424,7 @@ void MarlinUI::init() {
#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*/) {
+ void _wrap_string(uint8_t &col, uint8_t &row, const char * const string, read_byte_cb_t cb_read_byte, const bool wordwrap/*=false*/) {
SETCURSOR(col, row);
if (!string) return;
diff --git a/Marlin/src/lcd/menu/menu.h b/Marlin/src/lcd/menu/menu.h
index 5a70ddd3a2..1face4774b 100644
--- a/Marlin/src/lcd/menu/menu.h
+++ b/Marlin/src/lcd/menu/menu.h
@@ -259,6 +259,10 @@ void _lcd_draw_homing();
void touch_screen_calibration();
#endif
+#if ENABLED(ONE_CLICK_PRINT)
+ void one_click_print();
+#endif
+
extern uint8_t screen_history_depth;
inline void clear_menu_history() { screen_history_depth = 0; }
diff --git a/Marlin/src/lcd/menu/menu_media.cpp b/Marlin/src/lcd/menu/menu_media.cpp
index e32f41a9a6..795ac2052b 100644
--- a/Marlin/src/lcd/menu/menu_media.cpp
+++ b/Marlin/src/lcd/menu/menu_media.cpp
@@ -73,14 +73,11 @@ class MenuItem_sdfile : public MenuItem_sdbase {
#endif
#if ENABLED(SD_MENU_CONFIRM_START)
MenuItem_submenu::action(fstr, []{
- char * const longest = card.longest_filename();
- char buffer[strlen(longest) + 2];
- buffer[0] = ' ';
- strcpy(buffer + 1, longest);
+ char * const filename = card.longest_filename();
MenuItem_confirm::select_screen(
GET_TEXT_F(MSG_BUTTON_PRINT), GET_TEXT_F(MSG_BUTTON_CANCEL),
sdcard_start_selected_file, nullptr,
- GET_TEXT_F(MSG_START_PRINT), buffer, F("?")
+ GET_TEXT_F(MSG_START_PRINT), filename, F("?")
);
});
#else
diff --git a/Marlin/src/lcd/menu/menu_one_click_print.cpp b/Marlin/src/lcd/menu/menu_one_click_print.cpp
new file mode 100644
index 0000000000..f1ed92d0d5
--- /dev/null
+++ b/Marlin/src/lcd/menu/menu_one_click_print.cpp
@@ -0,0 +1,44 @@
+/**
+ * Marlin 3D Printer Firmware
+ * Copyright (c) 2023 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
+ *
+ * Based on Sprinter and grbl.
+ * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include "../../inc/MarlinConfigPre.h"
+
+#if ENABLED(ONE_CLICK_PRINT)
+
+#include "menu.h"
+
+void one_click_print() {
+ ui.goto_screen([]{
+ char * const filename = card.longest_filename();
+ MenuItem_confirm::select_screen(
+ GET_TEXT_F(MSG_BUTTON_PRINT), GET_TEXT_F(MSG_BUTTON_CANCEL),
+ []{
+ card.openAndPrintFile(card.filename);
+ ui.return_to_status();
+ ui.reset_status();
+ }, nullptr,
+ GET_TEXT_F(MSG_START_PRINT), filename, F("?")
+ );
+ });
+}
+
+#endif // ONE_CLICK_PRINT
diff --git a/Marlin/src/sd/cardreader.cpp b/Marlin/src/sd/cardreader.cpp
index 4e864e5672..cd3d1d2cd6 100644
--- a/Marlin/src/sd/cardreader.cpp
+++ b/Marlin/src/sd/cardreader.cpp
@@ -56,6 +56,10 @@
#include "../feature/pause.h"
#endif
+#if ENABLED(ONE_CLICK_PRINT)
+ #include "../../src/lcd/menu/menu.h"
+#endif
+
#define DEBUG_OUT EITHER(DEBUG_CARDREADER, MARLIN_DEV_MODE)
#include "../core/debug_out.h"
#include "../libs/hex_print.h"
@@ -290,7 +294,7 @@ void CardReader::printListing(MediaFile parent, const char * const prepend, cons
while (parent.readDir(&p, longFilename) > 0) {
if (DIR_IS_SUBDIR(&p)) {
- size_t lenPrepend = prepend ? strlen(prepend) + 1 : 0;
+ const size_t lenPrepend = prepend ? strlen(prepend) + 1 : 0;
// Allocate enough stack space for the full path including / separator
char path[lenPrepend + FILENAME_LENGTH];
if (prepend) { strcpy(path, prepend); path[lenPrepend - 1] = '/'; }
@@ -545,20 +549,28 @@ void CardReader::manage_media() {
if (!stat) return; // Exit if no media is present
- if (old_stat != 2) return; // First mount?
-
- DEBUG_ECHOLNPGM("First mount.");
-
- // Load settings the first time media is inserted (not just during init)
- TERN_(SDCARD_EEPROM_EMULATION, settings.first_load());
-
bool do_auto = true; UNUSED(do_auto);
- // Check for PLR file.
- TERN_(POWER_LOSS_RECOVERY, if (recovery.check()) do_auto = false);
+ // First mount on boot? Load emulated EEPROM and look for PLR file.
+ if (old_stat == 2) {
+ DEBUG_ECHOLNPGM("First mount.");
- // Look for auto0.g on the next idle()
- IF_DISABLED(NO_SD_AUTOSTART, if (do_auto) autofile_begin());
+ // Load settings the first time media is inserted (not just during init)
+ TERN_(SDCARD_EEPROM_EMULATION, settings.first_load());
+
+ // Check for PLR file. Skip One-Click and auto#.g if found
+ TERN_(POWER_LOSS_RECOVERY, if (recovery.check()) do_auto = false);
+ }
+
+ // Find the newest file and prompt to print it.
+ TERN_(ONE_CLICK_PRINT, if (do_auto && one_click_check()) do_auto = false);
+
+ // Also for the first mount run auto#.g for machine init.
+ // (Skip if PLR or One-Click Print was invoked.)
+ if (old_stat == 2) {
+ // Look for auto0.g on the next idle()
+ IF_DISABLED(NO_SD_AUTOSTART, if (do_auto) autofile_begin());
+ }
}
/**
@@ -887,6 +899,81 @@ void CardReader::write_command(char * const buf) {
}
#endif
+#if ENABLED(ONE_CLICK_PRINT)
+
+ /**
+ * Select the newest file and ask the user if they want to print it.
+ */
+ bool CardReader::one_click_check() {
+ const bool found = selectNewestFile();
+ if (found) {
+ //SERIAL_ECHO_MSG(" OCP File: ", longest_filename(), "\n");
+ //ui.init();
+ one_click_print();
+ }
+ return found;
+ }
+
+ /**
+ * Recurse the entire directory to find the newest file.
+ * This may take a very long time so watch out for watchdog reset.
+ * It may be best to only look at root for reasonable boot and mount times.
+ */
+ void CardReader::diveToNewestFile(MediaFile parent, uint32_t &compareDateTime, MediaFile &outdir, char * const outname) {
+ // Iterate the given parent dir
+ parent.rewind();
+ for (dir_t p; parent.readDir(&p, longFilename) > 0;) {
+
+ // If the item is a dir, recurse into it
+ if (DIR_IS_SUBDIR(&p)) {
+ // Get the name of the dir for opening
+ char dirname[FILENAME_LENGTH];
+ createFilename(dirname, p);
+
+ // Open the item in a new MediaFile
+ MediaFile child; // child.close() in destructor
+ if (child.open(&parent, dirname, O_READ))
+ diveToNewestFile(child, compareDateTime, outdir, outname);
+ }
+ else if (is_visible_entity(p)) {
+ // Get the newer of the modified/created date and time
+ const uint32_t modDateTime = uint32_t(p.lastWriteDate) << 16 | p.lastWriteTime,
+ createDateTime = uint32_t(p.creationDate) << 16 | p.creationTime,
+ newerDateTime = _MAX(modDateTime, createDateTime);
+ // If a newer item is found overwrite the outdir and outname
+ if (newerDateTime > compareDateTime) {
+ compareDateTime = newerDateTime;
+ outdir = parent;
+ createFilename(outname, p);
+ }
+ }
+ }
+ }
+
+ /**
+ * Recurse the entire directory to find the newest file.
+ * Make the found file the current selection.
+ */
+ bool CardReader::selectNewestFile() {
+ uint32_t dateTimeStorage = 0;
+ MediaFile foundDir;
+ char foundName[FILENAME_LENGTH];
+ foundName[0] = '\0';
+
+ diveToNewestFile(root, dateTimeStorage, foundDir, foundName);
+
+ if (foundName[0]) {
+ workDir = foundDir;
+ workDir.rewind();
+ selectByName(workDir, foundName);
+ //workDir.close(); // Not needed?
+ return true;
+ }
+ return false;
+ }
+
+#endif // ONE_CLICK_PRINT
+
void CardReader::closefile(const bool store_location/*=false*/) {
file.sync();
file.close();
diff --git a/Marlin/src/sd/cardreader.h b/Marlin/src/sd/cardreader.h
index 3b7b7debcf..4baaa73e1a 100644
--- a/Marlin/src/sd/cardreader.h
+++ b/Marlin/src/sd/cardreader.h
@@ -128,6 +128,12 @@ public:
static void autofile_cancel() { autofile_index = 0; }
#endif
+ #if ENABLED(ONE_CLICK_PRINT)
+ static bool one_click_check(); // Check for the newest file and prompt to run it.
+ static void diveToNewestFile(MediaFile parent, uint32_t &compareDateTime, MediaFile &outdir, char * const outname);
+ static bool selectNewestFile();
+ #endif
+
// Basic file ops
static void openFileRead(const char * const path, const uint8_t subcall=0);
static void openFileWrite(const char * const path);
diff --git a/buildroot/share/PlatformIO/scripts/common-dependencies.h b/buildroot/share/PlatformIO/scripts/common-dependencies.h
index 4438b4efe1..c75d9a3d67 100644
--- a/buildroot/share/PlatformIO/scripts/common-dependencies.h
+++ b/buildroot/share/PlatformIO/scripts/common-dependencies.h
@@ -104,6 +104,9 @@
#if ENABLED(AUTO_BED_LEVELING_UBL)
#define HAS_MENU_UBL
#endif
+ #if ENABLED(ONE_CLICK_PRINT)
+ #define HAS_MENU_ONE_CLICK_PRINT
+ #endif
#endif
#if HAS_GRAPHICAL_TFT
diff --git a/buildroot/tests/mks_tinybee b/buildroot/tests/mks_tinybee
index 9dcc33ede7..0351946d0a 100755
--- a/buildroot/tests/mks_tinybee
+++ b/buildroot/tests/mks_tinybee
@@ -25,8 +25,8 @@ opt_set MOTHERBOARD BOARD_MKS_TINYBEE \
LCD_INFO_SCREEN_STYLE 0 \
DISPLAY_CHARSET_HD44780 WESTERN \
NEOPIXEL_TYPE NEO_RGB
-opt_enable FYSETC_MINI_12864_2_1 SDSUPPORT
-opt_enable LED_CONTROL_MENU LED_USER_PRESET_STARTUP LED_COLOR_PRESETS NEOPIXEL_LED
+opt_enable FYSETC_MINI_12864_2_1 SDSUPPORT ONE_CLICK_PRINT NO_SD_AUTOSTART \
+ NEOPIXEL_LED LED_CONTROL_MENU LED_USER_PRESET_STARTUP LED_COLOR_PRESETS
exec_test $1 $2 "MKS TinyBee with NeoPixel LCD, SD and Speaker" "$3"
# cleanup
diff --git a/ini/features.ini b/ini/features.ini
index 17d2ece04a..86e7d2e396 100644
--- a/ini/features.ini
+++ b/ini/features.ini
@@ -81,6 +81,7 @@ HAS_MENU_LED = build_src_filter=+
HAS_MENU_MIXER = build_src_filter=+
HAS_MENU_MMU2 = build_src_filter=+
+HAS_MENU_ONE_CLICK_PRINT = build_src_filter=+
HAS_MENU_PASSWORD = build_src_filter=+
HAS_MENU_POWER_MONITOR = build_src_filter=+
HAS_MENU_CUTTER = build_src_filter=+