diff --git a/.travis.yml b/.travis.yml index d869c01f7..4516857c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -95,7 +95,7 @@ script: - opt_enable REPRAP_DISCOUNT_SMART_CONTROLLER SDSUPPORT EEPROM_SETTINGS - opt_enable BLINKM PCA9632 RGB_LED NEOPIXEL_LED AUTO_POWER_CONTROL NOZZLE_PARK_FEATURE FILAMENT_RUNOUT_SENSOR - opt_enable AUTO_BED_LEVELING_LINEAR Z_MIN_PROBE_REPEATABILITY_TEST DEBUG_LEVELING_FEATURE SKEW_CORRECTION SKEW_CORRECTION_FOR_Z SKEW_CORRECTION_GCODE - - opt_enable_adv ARC_P_CIRCLES ADVANCED_PAUSE_FEATURE CNC_WORKSPACE_PLANES CNC_COORDINATE_SYSTEMS + - opt_enable_adv ARC_P_CIRCLES ADVANCED_PAUSE_FEATURE CNC_WORKSPACE_PLANES CNC_COORDINATE_SYSTEMS POWER_LOSS_RECOVERY - opt_enable_adv FWRETRACT MAX7219_DEBUG LED_CONTROL_MENU CASE_LIGHT_ENABLE CASE_LIGHT_USE_NEOPIXEL CODEPENDENT_XY_HOMING - opt_set GRID_MAX_POINTS_X 16 - opt_set_adv FANMUX0_PIN 53 diff --git a/Marlin/Conditionals_post.h b/Marlin/Conditionals_post.h index ad6032250..c4e291c43 100644 --- a/Marlin/Conditionals_post.h +++ b/Marlin/Conditionals_post.h @@ -1225,7 +1225,7 @@ #define GRID_MAX_POINTS ((GRID_MAX_POINTS_X) * (GRID_MAX_POINTS_Y)) // Add commands that need sub-codes to this list - #define USE_GCODE_SUBCODES ENABLED(G38_PROBE_TARGET) || ENABLED(CNC_COORDINATE_SYSTEMS) + #define USE_GCODE_SUBCODES ENABLED(G38_PROBE_TARGET) || ENABLED(CNC_COORDINATE_SYSTEMS) || ENABLED(POWER_LOSS_RECOVERY) // Parking Extruder #if ENABLED(PARKING_EXTRUDER) diff --git a/Marlin/Configuration_adv.h b/Marlin/Configuration_adv.h index f005fe263..6fc096c17 100644 --- a/Marlin/Configuration_adv.h +++ b/Marlin/Configuration_adv.h @@ -555,6 +555,16 @@ // Add an option in the menu to run all auto#.g files //#define MENU_ADDAUTOSTART + /** + * Continue after Power-Loss (Creality3D) + * + * Store the current state to the SD Card at the start of each layer + * during SD printing. If the recovery file is found at boot time, present + * an option on the LCD screen to continue the print from the last-known + * point in the file. + */ + //#define POWER_LOSS_RECOVERY + /** * Sort SD file listings in alphabetical order. * diff --git a/Marlin/M100_Free_Mem_Chk.cpp b/Marlin/M100_Free_Mem_Chk.cpp index 9a455aba9..9680908dc 100644 --- a/Marlin/M100_Free_Mem_Chk.cpp +++ b/Marlin/M100_Free_Mem_Chk.cpp @@ -59,8 +59,6 @@ #define TEST_BYTE ((char) 0xE5) -extern char command_queue[BUFSIZE][MAX_CMD_SIZE]; - extern char* __brkval; extern size_t __heap_start, __heap_end, __flp; extern char __bss_end; diff --git a/Marlin/Marlin.h b/Marlin/Marlin.h index 90f703f08..aadbbdaf2 100644 --- a/Marlin/Marlin.h +++ b/Marlin/Marlin.h @@ -197,6 +197,10 @@ bool enqueue_and_echo_command(const char* cmd, bool say_ok=false); // Add a sing void enqueue_and_echo_commands_P(const char * const cmd); // Set one or more commands to be prioritized over the next Serial/SD command. void clear_command_queue(); +#if ENABLED(M100_FREE_MEMORY_WATCHER) || ENABLED(POWER_LOSS_RECOVERY) + extern char command_queue[BUFSIZE][MAX_CMD_SIZE]; +#endif + #define HAS_LCD_QUEUE_NOW (ENABLED(MALYAN_LCD) || (ENABLED(ULTIPANEL) && (ENABLED(AUTO_BED_LEVELING_UBL) || ENABLED(PID_AUTOTUNE_MENU) || ENABLED(ADVANCED_PAUSE_FEATURE)))) #define HAS_QUEUE_NOW (ENABLED(SDSUPPORT) || HAS_LCD_QUEUE_NOW) #if HAS_QUEUE_NOW diff --git a/Marlin/Marlin_main.cpp b/Marlin/Marlin_main.cpp index 8b8a0d97a..804a59b7d 100644 --- a/Marlin/Marlin_main.cpp +++ b/Marlin/Marlin_main.cpp @@ -288,6 +288,10 @@ #include "fwretract.h" #endif +#if ENABLED(POWER_LOSS_RECOVERY) + #include "power_loss_recovery.h" +#endif + #if ENABLED(FILAMENT_RUNOUT_SENSOR) #include "runout.h" #endif @@ -415,14 +419,11 @@ static long gcode_N, gcode_LastN, Stopped_gcode_LastN = 0; * the main loop. The process_next_command function parses the next * command and hands off execution to individual handler functions. */ -uint8_t commands_in_queue = 0; // Count of commands in the queue -static uint8_t cmd_queue_index_r = 0, // Ring buffer read position - cmd_queue_index_w = 0; // Ring buffer write position -#if ENABLED(M100_FREE_MEMORY_WATCHER) - char command_queue[BUFSIZE][MAX_CMD_SIZE]; // Necessary so M100 Free Memory Dumper can show us the commands and any corruption -#else // This can be collapsed back to the way it was soon. -static char command_queue[BUFSIZE][MAX_CMD_SIZE]; -#endif +uint8_t commands_in_queue = 0, // Count of commands in the queue + cmd_queue_index_r = 0, // Ring buffer read (out) position + cmd_queue_index_w = 0; // Ring buffer write (in) position + +char command_queue[BUFSIZE][MAX_CMD_SIZE]; /** * Next Injected Command pointer. NULL if no commands are being injected. @@ -1231,21 +1232,43 @@ inline void get_serial_commands() { } } + #if ENABLED(POWER_LOSS_RECOVERY) + + inline bool drain_job_recovery_commands() { + static uint8_t job_recovery_commands_index = 0; // Resets on reboot + if (job_recovery_commands_count) { + if (_enqueuecommand(job_recovery_commands[job_recovery_commands_index])) { + ++job_recovery_commands_index; + if (!--job_recovery_commands_count) job_recovery_phase = JOB_RECOVERY_IDLE; + } + return true; + } + return false; + } + + #endif + #endif // SDSUPPORT /** * Add to the circular command queue the next command from: * - The command-injection queue (injected_commands_P) * - The active serial input (usually USB) + * - Commands left in the queue after power-loss * - The SD card file being actively printed */ void get_available_commands() { - // if any immediate commands remain, don't get other commands yet + // Immediate commands block the other queues if (drain_injected_commands_P()) return; get_serial_commands(); + #if ENABLED(POWER_LOSS_RECOVERY) + // Commands for power-loss recovery take precedence + if (job_recovery_phase == JOB_RECOVERY_YES && drain_job_recovery_commands()) return; + #endif + #if ENABLED(SDSUPPORT) get_sdcard_commands(); #endif @@ -7017,6 +7040,10 @@ inline void gcode_M17() { * M24: Start or Resume SD Print */ inline void gcode_M24() { + #if ENABLED(POWER_LOSS_RECOVERY) + card.removeJobRecoveryFile(); + #endif + #if ENABLED(PARK_HEAD_ON_PAUSE) resume_print(); #endif @@ -14274,6 +14301,10 @@ void setup() { #endif #endif + #if ENABLED(POWER_LOSS_RECOVERY) + do_print_job_recovery(); + #endif + #if ENABLED(USE_WATCHDOG) watchdog_init(); #endif @@ -14351,8 +14382,12 @@ void loop() { ok_to_send(); } } - else + else { process_next_command(); + #if ENABLED(POWER_LOSS_RECOVERY) + if (card.cardOK && card.sdprinting) save_job_recovery_info(); + #endif + } #else diff --git a/Marlin/SanityCheck.h b/Marlin/SanityCheck.h index 2393f725f..a1b35ad17 100644 --- a/Marlin/SanityCheck.h +++ b/Marlin/SanityCheck.h @@ -435,7 +435,7 @@ static_assert(X_MAX_LENGTH >= X_BED_SIZE && Y_MAX_LENGTH >= Y_BED_SIZE, #elif ENABLED(BABYSTEP_ZPROBE_OFFSET) && !HAS_BED_PROBE #error "BABYSTEP_ZPROBE_OFFSET requires a probe." #elif ENABLED(BABYSTEP_ZPROBE_GFX_OVERLAY) && !ENABLED(DOGLCD) - #error "BABYSTEP_ZPROBE_GFX_OVERLAY requires a DOGLCD." + #error "BABYSTEP_ZPROBE_GFX_OVERLAY requires a Graphical LCD." #elif ENABLED(BABYSTEP_ZPROBE_GFX_OVERLAY) && !ENABLED(BABYSTEP_ZPROBE_OFFSET) #error "BABYSTEP_ZPROBE_GFX_OVERLAY requires a BABYSTEP_ZPROBE_OFFSET." #endif @@ -1743,4 +1743,8 @@ static_assert(COUNT(sanity_arr_3) <= XYZE_N, "DEFAULT_MAX_ACCELERATION has too m #endif #endif +#if ENABLED(POWER_LOSS_RECOVERY) && !ENABLED(ULTIPANEL) + #error "POWER_LOSS_RECOVERY currently requires an LCD Controller." +#endif + #endif // _SANITYCHECK_H_ diff --git a/Marlin/cardreader.cpp b/Marlin/cardreader.cpp index 0b5b8e044..581e0ec5a 100644 --- a/Marlin/cardreader.cpp +++ b/Marlin/cardreader.cpp @@ -31,6 +31,10 @@ #include "language.h" #include "printcounter.h" +#if ENABLED(POWER_LOSS_RECOVERY) + #include "power_loss_recovery.h" +#endif + #define LONGEST_FILENAME (longFilename[0] ? longFilename : filename) CardReader::CardReader() { @@ -420,8 +424,7 @@ void CardReader::openFile(char* name, const bool read, const bool subcall/*=fals strncpy(subdirname, dirname_start, dirname_end - dirname_start); subdirname[dirname_end - dirname_start] = '\0'; if (!myDir.open(curDir, subdirname, O_READ)) { - SERIAL_PROTOCOLPGM(MSG_SD_OPEN_FILE_FAIL); - SERIAL_PROTOCOL(subdirname); + SERIAL_PROTOCOLPAIR(MSG_SD_OPEN_FILE_FAIL, subdirname); SERIAL_PROTOCOLCHAR('.'); return; } @@ -928,6 +931,15 @@ void CardReader::printingHasFinished() { } else { sdprinting = false; + + #if ENABLED(POWER_LOSS_RECOVERY) + openJobRecoveryFile(false); + job_recovery_info.valid_head = job_recovery_info.valid_foot = 0; + (void)saveJobRecoveryInfo(); + closeJobRecoveryFile(); + job_recovery_commands_count = 0; + #endif + #if ENABLED(SD_FINISHED_STEPPERRELEASE) && defined(SD_FINISHED_RELEASECOMMAND) stepper.cleaning_buffer_counter = 1; // The command will fire from the Stepper ISR #endif @@ -959,4 +971,46 @@ void CardReader::printingHasFinished() { } #endif // AUTO_REPORT_SD_STATUS +#if ENABLED(POWER_LOSS_RECOVERY) + + char job_recovery_file_name[4] = "bin"; + + void CardReader::openJobRecoveryFile(const bool read) { + if (!cardOK) return; + if (jobRecoveryFile.isOpen()) return; + if (!jobRecoveryFile.open(&root, job_recovery_file_name, read ? O_READ : O_CREAT | O_WRITE | O_TRUNC | O_SYNC)) { + SERIAL_PROTOCOLPAIR(MSG_SD_OPEN_FILE_FAIL, job_recovery_file_name); + SERIAL_PROTOCOLCHAR('.'); + SERIAL_EOL(); + } + else + SERIAL_PROTOCOLLNPAIR(MSG_SD_WRITE_TO_FILE, job_recovery_file_name); + } + + void CardReader::closeJobRecoveryFile() { jobRecoveryFile.close(); } + + bool CardReader::jobRecoverFileExists() { + return jobRecoveryFile.open(&root, job_recovery_file_name, O_READ); + } + + int16_t CardReader::saveJobRecoveryInfo() { + jobRecoveryFile.seekSet(0); + const int16_t ret = jobRecoveryFile.write(&job_recovery_info, sizeof(job_recovery_info)); + if (ret == -1) SERIAL_PROTOCOLLNPGM("Power-loss file write failed."); + return ret; + } + + int16_t CardReader::loadJobRecoveryInfo() { + return jobRecoveryFile.read(&job_recovery_info, sizeof(job_recovery_info)); + } + + void CardReader::removeJobRecoveryFile() { + if (jobRecoveryFile.remove(&root, job_recovery_file_name)) + SERIAL_PROTOCOLLNPGM("Power-loss file deleted."); + else + SERIAL_PROTOCOLLNPGM("Power-loss file delete failed."); + } + +#endif // POWER_LOSS_RECOVERY + #endif // SDSUPPORT diff --git a/Marlin/cardreader.h b/Marlin/cardreader.h index d9c068e4d..a7bb1cd46 100644 --- a/Marlin/cardreader.h +++ b/Marlin/cardreader.h @@ -89,11 +89,21 @@ public: #endif #endif + #if ENABLED(POWER_LOSS_RECOVERY) + void openJobRecoveryFile(const bool read); + void closeJobRecoveryFile(); + bool jobRecoverFileExists(); + int16_t saveJobRecoveryInfo(); + int16_t loadJobRecoveryInfo(); + void removeJobRecoveryFile(); + #endif + FORCE_INLINE void pauseSDPrint() { sdprinting = false; } FORCE_INLINE bool isFileOpen() { return file.isOpen(); } FORCE_INLINE bool eof() { return sdpos >= filesize; } FORCE_INLINE int16_t get() { sdpos = file.curPosition(); return (int16_t)file.read(); } - FORCE_INLINE void setIndex(long index) { sdpos = index; file.seekSet(index); } + FORCE_INLINE void setIndex(const uint32_t index) { sdpos = index; file.seekSet(index); } + FORCE_INLINE uint32_t getIndex() { return sdpos; } FORCE_INLINE uint8_t percentDone() { return (isFileOpen() && filesize) ? sdpos / ((filesize + 99) / 100) : 0; } FORCE_INLINE char* getWorkDirName() { workDir.getFilename(filename); return filename; } @@ -168,6 +178,10 @@ private: SdVolume volume; SdFile file; + #if ENABLED(POWER_LOSS_RECOVERY) + SdFile jobRecoveryFile; + #endif + #define SD_PROCEDURE_DEPTH 1 #define MAXPATHNAMELENGTH (FILENAME_LENGTH*MAX_DIR_DEPTH + MAX_DIR_DEPTH + 1) uint8_t file_subcall_ctr; diff --git a/Marlin/power_loss_recovery.cpp b/Marlin/power_loss_recovery.cpp new file mode 100644 index 000000000..fdee4274c --- /dev/null +++ b/Marlin/power_loss_recovery.cpp @@ -0,0 +1,237 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (C) 2016 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 . + * + */ + +/** + * power_loss_recovery.cpp - Resume an SD print after power-loss + */ + +#include "MarlinConfig.h" + +#if ENABLED(POWER_LOSS_RECOVERY) + +#include "power_loss_recovery.h" + +#include "cardreader.h" +#include "planner.h" +#include "printcounter.h" +#include "serial.h" +#include "temperature.h" +#include "ultralcd.h" + +// Recovery data +job_recovery_info_t job_recovery_info; +JobRecoveryPhase job_recovery_phase = JOB_RECOVERY_IDLE; +uint8_t job_recovery_commands_count; //=0 +char job_recovery_commands[BUFSIZE + APPEND_CMD_COUNT][MAX_CMD_SIZE]; + +// Extern +extern uint8_t commands_in_queue, cmd_queue_index_r; + +// Private +static char sd_filename[MAXPATHNAMELENGTH]; + +#if ENABLED(DEBUG_POWER_LOSS_RECOVERY) + void debug_print_job_recovery(const bool recovery) { + SERIAL_PROTOCOLPAIR("valid_head:", (int)job_recovery_info.valid_head); + SERIAL_PROTOCOLLNPAIR(" valid_foot:", (int)job_recovery_info.valid_foot); + if (job_recovery_info.valid_head) { + if (job_recovery_info.valid_head == job_recovery_info.valid_foot) { + SERIAL_PROTOCOLPGM("current_position"); + LOOP_XYZE(i) SERIAL_PROTOCOLPAIR(": ", job_recovery_info.current_position[i]); + SERIAL_EOL(); + SERIAL_PROTOCOLLNPAIR("feedrate: ", job_recovery_info.feedrate); + SERIAL_PROTOCOLPGM("target_temperature"); + HOTEND_LOOP() SERIAL_PROTOCOLPAIR(": ", job_recovery_info.target_temperature[e]); + SERIAL_EOL(); + SERIAL_PROTOCOLPGM("fanSpeeds"); + for(uint8_t i = 0; i < FAN_COUNT; i++) SERIAL_PROTOCOLPAIR(": ", job_recovery_info.fanSpeeds[i]); + SERIAL_EOL(); + #if HAS_LEVELING + SERIAL_PROTOCOLPAIR("leveling: ", int(job_recovery_info.leveling)); + SERIAL_PROTOCOLLNPAIR(" fade: ", int(job_recovery_info.fade)); + #endif + SERIAL_PROTOCOLLNPAIR("target_temperature_bed: ", job_recovery_info.target_temperature_bed); + SERIAL_PROTOCOLLNPAIR("cmd_queue_index_r: ", job_recovery_info.cmd_queue_index_r); + SERIAL_PROTOCOLLNPAIR("commands_in_queue: ", job_recovery_info.commands_in_queue); + if (recovery) + for (uint8_t i = 0; i < job_recovery_commands_count; i++) SERIAL_PROTOCOLLNPAIR("> ", job_recovery_commands[i]); + else + for (uint8_t i = 0; i < job_recovery_info.commands_in_queue; i++) SERIAL_PROTOCOLLNPAIR("> ", job_recovery_info.command_queue[i]); + SERIAL_PROTOCOLLNPAIR("sd_filename: ", sd_filename); + SERIAL_PROTOCOLLNPAIR("sdpos: ", job_recovery_info.sdpos); + SERIAL_PROTOCOLLNPAIR("print_job_elapsed: ", job_recovery_info.print_job_elapsed); + } + else + SERIAL_PROTOCOLLNPGM("INVALID DATA"); + } + } +#endif // DEBUG_POWER_LOSS_RECOVERY + +/** + * Check for Print Job Recovery + * If the file has a saved state, populate the job_recovery_commands queue + */ +void do_print_job_recovery() { + //if (job_recovery_commands_count > 0) return; + memset(&job_recovery_info, 0, sizeof(job_recovery_info)); + ZERO(job_recovery_commands); + + if (!card.cardOK) card.initsd(); + + if (card.cardOK) { + + #if ENABLED(DEBUG_POWER_LOSS_RECOVERY) + SERIAL_PROTOCOLLNPAIR("Init job recovery info. Size: ", (int)sizeof(job_recovery_info)); + #endif + + if (card.jobRecoverFileExists()) { + card.openJobRecoveryFile(true); + card.loadJobRecoveryInfo(); + card.closeJobRecoveryFile(); + //card.removeJobRecoveryFile(); + + if (job_recovery_info.valid_head && job_recovery_info.valid_head == job_recovery_info.valid_foot) { + + uint8_t ind = 0; + + #if HAS_LEVELING + strcpy_P(job_recovery_commands[ind++], PSTR("M420 S0 Z0")); // Leveling off before G92 or G28 + #endif + + strcpy_P(job_recovery_commands[ind++], PSTR("G92.0 Z0")); // Ensure Z is equal to 0 + strcpy_P(job_recovery_commands[ind++], PSTR("G1 Z2")); // Raise Z by 2mm (we hope!) + strcpy_P(job_recovery_commands[ind++], PSTR("G28 R0" + #if !IS_KINEMATIC + " X Y" // Home X and Y for Cartesian + #endif + )); + + #if HAS_LEVELING + // Restore leveling state before G92 sets Z + // This ensures the steppers correspond to the native Z + sprintf_P(job_recovery_commands[ind++], PSTR("M420 S%i Z%s"), int(job_recovery_info.leveling), job_recovery_info.fade); + #endif + + char str_1[16], str_2[16]; + dtostrf(job_recovery_info.current_position[Z_AXIS] + 2, 1, 3, str_1); + dtostrf(job_recovery_info.current_position[E_AXIS] + #if ENABLED(SAVE_EACH_CMD_MODE) + - 5 + #endif + , 1, 3, str_2 + ); + sprintf_P(job_recovery_commands[ind++], PSTR("G92.0 Z%s E%s"), str_1, str_2); // Current Z + 2 and E + + strcpy_P(job_recovery_commands[ind++], PSTR("M117 Continuing...")); + + uint8_t r = job_recovery_info.cmd_queue_index_r; + while (job_recovery_info.commands_in_queue) { + strcpy(job_recovery_commands[ind++], job_recovery_info.command_queue[r]); + job_recovery_info.commands_in_queue--; + r = (r + 1) % BUFSIZE; + } + + job_recovery_commands_count = ind; + + #if ENABLED(DEBUG_POWER_LOSS_RECOVERY) + debug_print_job_recovery(true); + #endif + + card.openFile(sd_filename, true); + card.setIndex(job_recovery_info.sdpos); + } + else { + if (job_recovery_info.valid_head != job_recovery_info.valid_foot) + LCD_ALERTMESSAGEPGM("INVALID DATA"); + memset(&job_recovery_info, 0, sizeof(job_recovery_info)); + } + } + } +} + +/** + * Save the current machine state to the "bin" file + */ +void save_job_recovery_info() { + #if SAVE_INFO_INTERVAL_MS > 0 + static millis_t next_save_ms; // = 0; // Init on reset + millis_t ms = millis(); + #endif + if ( + #if SAVE_INFO_INTERVAL_MS > 0 + ELAPSED(ms, next_save_ms) || + #endif + #if ENABLED(SAVE_EACH_CMD_MODE) + true + #else + (current_position[Z_AXIS] > 0 && current_position[Z_AXIS] > job_recovery_info.current_position[Z_AXIS]) + #endif + ) { + #if SAVE_INFO_INTERVAL_MS > 0 + next_save_ms = ms + SAVE_INFO_INTERVAL_MS; + #endif + + // Head and foot will match if valid data was saved + if (!++job_recovery_info.valid_head) ++job_recovery_info.valid_head; // non-zero in sequence + job_recovery_info.valid_foot = job_recovery_info.valid_head; + + // Machine state + COPY(job_recovery_info.current_position, current_position); + job_recovery_info.feedrate = feedrate_mm_s; + COPY(job_recovery_info.target_temperature, thermalManager.target_temperature); + job_recovery_info.target_temperature_bed = thermalManager.target_temperature_bed; + COPY(job_recovery_info.fanSpeeds, fanSpeeds); + + #if HAS_LEVELING + job_recovery_info.leveling = planner.leveling_active; + job_recovery_info.fade = ( + #if ENABLED(ENABLE_LEVELING_FADE_HEIGHT) + planner.z_fade_height + #else + 0 + #endif + ); + #endif + + // Commands in the queue + job_recovery_info.cmd_queue_index_r = cmd_queue_index_r; + job_recovery_info.commands_in_queue = commands_in_queue; + COPY(job_recovery_info.command_queue, command_queue); + + // Elapsed print job time + job_recovery_info.print_job_elapsed = print_job_timer.duration() * 1000UL; + + // SD file position + card.getAbsFilename(sd_filename); + job_recovery_info.sdpos = card.getIndex(); + + #if ENABLED(DEBUG_POWER_LOSS_RECOVERY) + SERIAL_PROTOCOLLNPGM("Saving job_recovery_info"); + debug_print_job_recovery(false); + #endif + + card.openJobRecoveryFile(false); + (void)card.saveJobRecoveryInfo(); + } +} + +#endif // POWER_LOSS_RECOVERY diff --git a/Marlin/power_loss_recovery.h b/Marlin/power_loss_recovery.h new file mode 100644 index 000000000..202f9654a --- /dev/null +++ b/Marlin/power_loss_recovery.h @@ -0,0 +1,86 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (C) 2016 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 . + * + */ + +/** + * power_loss_recovery.h - Resume an SD print after power-loss + */ + +#ifndef _POWER_LOSS_RECOVERY_H_ +#define _POWER_LOSS_RECOVERY_H_ + +#include "cardreader.h" +#include "types.h" +#include "MarlinConfig.h" + +#define SAVE_INFO_INTERVAL_MS 0 +//#define SAVE_EACH_CMD_MODE +//#define DEBUG_POWER_LOSS_RECOVERY + +typedef struct { + uint8_t valid_head; + + // Machine state + float current_position[NUM_AXIS], feedrate; + int16_t target_temperature[HOTENDS], + target_temperature_bed, + fanSpeeds[FAN_COUNT]; + + #if HAS_LEVELING + bool leveling; + float fade; + #endif + + // Command queue + uint8_t cmd_queue_index_r, commands_in_queue; + char command_queue[BUFSIZE][MAX_CMD_SIZE]; + + // SD File position + uint32_t sdpos; + + // Job elapsed time + millis_t print_job_elapsed; + + uint8_t valid_foot; +} job_recovery_info_t; + +extern job_recovery_info_t job_recovery_info; + +enum JobRecoveryPhase : unsigned char { + JOB_RECOVERY_IDLE, + JOB_RECOVERY_MAYBE, + JOB_RECOVERY_YES +}; +extern JobRecoveryPhase job_recovery_phase; + +#if HAS_LEVELING + #define APPEND_CMD_COUNT 7 +#else + #define APPEND_CMD_COUNT 5 +#endif + +extern char job_recovery_commands[BUFSIZE + APPEND_CMD_COUNT][MAX_CMD_SIZE]; +extern uint8_t job_recovery_commands_count; + +void do_print_job_recovery(); +void save_job_recovery_info(); + +#endif // _POWER_LOSS_RECOVERY_H_ diff --git a/Marlin/ultralcd.cpp b/Marlin/ultralcd.cpp index a91f881ff..be2d06290 100644 --- a/Marlin/ultralcd.cpp +++ b/Marlin/ultralcd.cpp @@ -65,6 +65,10 @@ bool lcd_external_control; // = false #endif +#if ENABLED(POWER_LOSS_RECOVERY) + #include "power_loss_recovery.h" +#endif + // Initialized by settings.load() int16_t lcd_preheat_hotend_temp[2], lcd_preheat_bed_temp[2], lcd_preheat_fan_speed[2]; @@ -831,10 +835,70 @@ void kill_screen(const char* lcd_msg) { abort_sd_printing = true; lcd_setstatusPGM(PSTR(MSG_PRINT_ABORTED), -1); lcd_return_to_status(); + + #if ENABLED(POWER_LOSS_RECOVERY) + card.openJobRecoveryFile(false); + job_recovery_info.valid_head = job_recovery_info.valid_foot = 0; + (void)card.saveJobRecoveryInfo(); + card.closeJobRecoveryFile(); + job_recovery_commands_count = 0; + #endif } #endif // SDSUPPORT + #if ENABLED(POWER_LOSS_RECOVERY) + + static void lcd_sdcard_recover_job() { + char cmd[20]; + + // Return to status now + lcd_return_to_status(); + + // Turn leveling off and home + enqueue_and_echo_commands_P(PSTR("M420 S0\nG28" + #if !IS_KINEMATIC + " X Y" + #endif + )); + + // Restore the bed temperature + sprintf_P(cmd, PSTR("M190 S%i"), job_recovery_info.target_temperature_bed); + enqueue_and_echo_command(cmd); + + // Restore all hotend temperatures + HOTEND_LOOP() { + sprintf_P(cmd, PSTR("M109 S%i"), job_recovery_info.target_temperature[e]); + enqueue_and_echo_command(cmd); + } + + // Restore print cooling fan speeds + for (uint8_t i = 0; i < FAN_COUNT; i++) { + sprintf_P(cmd, PSTR("M106 P%i S%i"), i, job_recovery_info.fanSpeeds[i]); + enqueue_and_echo_command(cmd); + } + + // Start draining the job recovery command queue + job_recovery_phase = JOB_RECOVERY_YES; + + // Resume the print job timer + if (job_recovery_info.print_job_elapsed) + print_job_timer.resume(job_recovery_info.print_job_elapsed); + + // Start getting commands from SD + card.startFileprint(); + } + + static void lcd_job_recovery_menu() { + defer_return_to_status = true; + START_MENU(); + MENU_ITEM(function, MSG_RESUME_PRINT, lcd_sdcard_recover_job); + MENU_ITEM(function, MSG_STOP_PRINT, lcd_sdcard_stop); + END_MENU(); + } + + #endif + #if ENABLED(MENU_ITEM_CASE_LIGHT) extern uint8_t case_light_brightness; @@ -5025,7 +5089,7 @@ void lcd_update() { } #endif - #endif + #endif // ULTIPANEL #if ENABLED(SDSUPPORT) && PIN_EXISTS(SD_DETECT) @@ -5055,6 +5119,13 @@ void lcd_update() { #endif // SDSUPPORT && SD_DETECT_PIN + #if ENABLED(POWER_LOSS_RECOVERY) + if (job_recovery_commands_count && job_recovery_phase == JOB_RECOVERY_IDLE) { + lcd_goto_screen(lcd_job_recovery_menu); + job_recovery_phase = JOB_RECOVERY_MAYBE; // Waiting for a response + } + #endif + const millis_t ms = millis(); if (ELAPSED(ms, next_lcd_update_ms) #if ENABLED(DOGLCD)