Browse Source

Fix some Power Loss Recovery behaviors (#18558)

Jason Smith 3 years ago
parent
commit
76b67d55e2
No account linked to committer's email address

+ 8
- 1
Marlin/src/MarlinCore.cpp View File

@@ -395,6 +395,13 @@ void disable_all_steppers() {
395 395
 #endif
396 396
 
397 397
 /**
398
+ * A Print Job exists when the timer is running or SD printing
399
+ */
400
+bool printJobOngoing() {
401
+  return print_job_timer.isRunning() || IS_SD_PRINTING();
402
+}
403
+
404
+/**
398 405
  * Printing is active when the print job timer is running
399 406
  */
400 407
 bool printingIsActive() {
@@ -690,7 +697,7 @@ void idle(TERN_(ADVANCED_PAUSE_FEATURE, bool no_stepper_sleep/*=false*/)) {
690 697
 
691 698
   // Handle Power-Loss Recovery
692 699
   #if ENABLED(POWER_LOSS_RECOVERY) && PIN_EXISTS(POWER_LOSS)
693
-    recovery.outage();
700
+    if (printJobOngoing()) recovery.outage();
694 701
   #endif
695 702
 
696 703
   // Run StallGuard endstop checks

+ 116
- 69
Marlin/src/feature/powerloss.cpp View File

@@ -66,12 +66,16 @@ PrintJobRecovery recovery;
66 66
 #ifndef POWER_LOSS_PURGE_LEN
67 67
   #define POWER_LOSS_PURGE_LEN 0
68 68
 #endif
69
+#ifndef POWER_LOSS_ZRAISE
70
+  #define POWER_LOSS_ZRAISE 2     // Move on loss with backup power, or on resume without it
71
+#endif
72
+
73
+#if DISABLED(BACKUP_POWER_SUPPLY)
74
+  #undef POWER_LOSS_RETRACT_LEN   // No retract at outage without backup power
75
+#endif
69 76
 #ifndef POWER_LOSS_RETRACT_LEN
70 77
   #define POWER_LOSS_RETRACT_LEN 0
71 78
 #endif
72
-#ifndef POWER_LOSS_ZRAISE
73
-  #define POWER_LOSS_ZRAISE 2
74
-#endif
75 79
 
76 80
 /**
77 81
  * Clear the recovery info
@@ -144,7 +148,7 @@ void PrintJobRecovery::prepare() {
144 148
 /**
145 149
  * Save the current machine state to the power-loss recovery file
146 150
  */
147
-void PrintJobRecovery::save(const bool force/*=false*/) {
151
+void PrintJobRecovery::save(const bool force/*=false*/, const float zraise/*=0*/) {
148 152
 
149 153
   #if SAVE_INFO_INTERVAL_MS > 0
150 154
     static millis_t next_save_ms; // = 0
@@ -177,6 +181,7 @@ void PrintJobRecovery::save(const bool force/*=false*/) {
177 181
 
178 182
     // Machine state
179 183
     info.current_position = current_position;
184
+    info.zraise = zraise;
180 185
     TERN_(HAS_HOME_OFFSET, info.home_offset = home_offset);
181 186
     TERN_(HAS_POSITION_SHIFT, info.position_shift = position_shift);
182 187
     info.feedrate = uint16_t(feedrate_mm_s * 60.0f);
@@ -228,30 +233,73 @@ void PrintJobRecovery::save(const bool force/*=false*/) {
228 233
 
229 234
 #if PIN_EXISTS(POWER_LOSS)
230 235
 
236
+  #if ENABLED(BACKUP_POWER_SUPPLY)
237
+
238
+    void PrintJobRecovery::retract_and_lift(const float &zraise) {
239
+      #if POWER_LOSS_RETRACT_LEN || POWER_LOSS_ZRAISE
240
+
241
+        gcode.set_relative_mode(true);  // Use relative coordinates
242
+
243
+        #if POWER_LOSS_RETRACT_LEN
244
+          // Retract filament now
245
+          gcode.process_subcommands_now_P(PSTR("G1 F3000 E-" STRINGIFY(POWER_LOSS_RETRACT_LEN)));
246
+        #endif
247
+
248
+        #if POWER_LOSS_ZRAISE
249
+          // Raise the Z axis now
250
+          if (zraise) {
251
+            char cmd[20], str_1[16];
252
+            sprintf_P(cmd, PSTR("G0 Z%s"), dtostrf(zraise, 1, 3, str_1));
253
+            gcode.process_subcommands_now(cmd);
254
+          }
255
+        #else
256
+          UNUSED(zraise);
257
+        #endif
258
+
259
+        //gcode.axis_relative = info.axis_relative;
260
+        planner.synchronize();
261
+      #endif
262
+    }
263
+
264
+  #endif
265
+
266
+  /**
267
+   * An outage was detected by a sensor pin.
268
+   *  - If not SD printing, let the machine turn off on its own with no "KILL" screen
269
+   *  - Disable all heaters first to save energy
270
+   *  - Save the recovery data for the current instant
271
+   *  - If backup power is available Retract E and Raise Z
272
+   *  - Go to the KILL screen
273
+   */
231 274
   void PrintJobRecovery::_outage() {
232 275
     #if ENABLED(BACKUP_POWER_SUPPLY)
233 276
       static bool lock = false;
234
-      if (lock) return; // No re-entrance from idle() during raise_z()
277
+      if (lock) return; // No re-entrance from idle() during retract_and_lift()
235 278
       lock = true;
236 279
     #endif
237
-    if (IS_SD_PRINTING()) save(true);
238
-    TERN_(BACKUP_POWER_SUPPLY, raise_z());
239 280
 
240
-    kill(GET_TEXT(MSG_OUTAGE_RECOVERY));
241
-  }
281
+    #if POWER_LOSS_ZRAISE
282
+      // Get the limited Z-raise to do now or on resume
283
+      const float zraise = _MAX(0, _MIN(current_position.z + POWER_LOSS_ZRAISE, Z_MAX_POS - 1) - current_position.z);
284
+    #else
285
+      constexpr float zraise = 0;
286
+    #endif
242 287
 
243
-  #if ENABLED(BACKUP_POWER_SUPPLY)
288
+    // Save, including the limited Z raise
289
+    if (IS_SD_PRINTING()) save(true, zraise);
290
+
291
+    // Disable all heaters to reduce power loss
292
+    thermalManager.disable_all_heaters();
244 293
 
245
-    void PrintJobRecovery::raise_z() {
246
-      // Disable all heaters to reduce power loss
247
-      thermalManager.disable_all_heaters();
294
+    #if ENABLED(BACKUP_POWER_SUPPLY)
295
+      // Do a hard-stop of the steppers (with possibly a loud thud)
248 296
       quickstop_stepper();
249
-      // Raise Z axis
250
-      gcode.process_subcommands_now_P(PSTR("G91\nG0 Z" STRINGIFY(POWER_LOSS_ZRAISE)));
251
-      planner.synchronize();
252
-    }
297
+      // With backup power a retract and raise can be done now
298
+      retract_and_lift(zraise);
299
+    #endif
253 300
 
254
-  #endif
301
+    kill(GET_TEXT(MSG_OUTAGE_RECOVERY));
302
+  }
255 303
 
256 304
 #endif
257 305
 
@@ -274,6 +322,8 @@ void PrintJobRecovery::write() {
274 322
  */
275 323
 void PrintJobRecovery::resume() {
276 324
 
325
+  char cmd[MAX_CMD_SIZE+16], str_1[16], str_2[16];
326
+
277 327
   const uint32_t resume_sdpos = info.sdpos; // Get here before the stepper ISR overwrites it
278 328
 
279 329
   #if HAS_LEVELING
@@ -282,52 +332,46 @@ void PrintJobRecovery::resume() {
282 332
   #endif
283 333
 
284 334
   // Reset E, raise Z, home XY...
285
-  gcode.process_subcommands_now_P(PSTR("G92.9 E0"
286
-    #if Z_HOME_DIR > 0
335
+  #if Z_HOME_DIR > 0
287 336
 
288
-      // If Z homing goes to max, just reset E and home all
289
-      "\n"
290
-      "G28R0"
291
-      TERN_(MARLIN_DEV_MODE, "S")
337
+    // If Z homing goes to max, just reset E and home all
338
+    gcode.process_subcommands_now_P(PSTR(
339
+      "G92.9 E0\n"
340
+      "G28R0" TERN_(MARLIN_DEV_MODE, "S")
341
+    ));
292 342
 
293
-    #else // "G92.9 E0 ..."
343
+  #else // "G92.9 E0 ..."
294 344
 
295
-      // Set Z to 0, raise Z by RECOVERY_ZRAISE, and Home (XY only for Cartesian)
296
-      // with no raise. (Only do simulated homing in Marlin Dev Mode.)
297
-      #if ENABLED(BACKUP_POWER_SUPPLY)
298
-        "Z" STRINGIFY(POWER_LOSS_ZRAISE)    // Z-axis was already raised at outage
299
-      #else
300
-        "Z0\n"                              // Set Z=0
301
-        "G1Z" STRINGIFY(POWER_LOSS_ZRAISE)  // Raise Z
302
-      #endif
303
-      "\n"
345
+    // Set Z to 0, raise Z by info.zraise, and Home (XY only for Cartesian)
346
+    // with no raise. (Only do simulated homing in Marlin Dev Mode.)
304 347
 
305
-      "G28R0"
306
-      #if ENABLED(MARLIN_DEV_MODE)
307
-        "S"
308
-      #elif !IS_KINEMATIC
309
-        "XY"
310
-      #endif
311
-    #endif
312
-  ));
313
-
314
-  // Pretend that all axes are homed
315
-  axis_homed = axis_known_position = xyz_bits;
348
+    sprintf_P(cmd, PSTR("G92.9 E0 "
349
+        #if ENABLED(BACKUP_POWER_SUPPLY)
350
+          "Z%s"                             // Z was already raised at outage
351
+        #else
352
+          "Z0\nG1Z%s"                       // Set Z=0 and Raise Z now
353
+        #endif
354
+      ),
355
+      dtostrf(info.zraise, 1, 3, str_1)
356
+    );
357
+    gcode.process_subcommands_now(cmd);
316 358
 
317
-  char cmd[MAX_CMD_SIZE+16], str_1[16], str_2[16];
359
+    gcode.process_subcommands_now_P(PSTR(
360
+      "G28R0"                               // No raise during G28
361
+      TERN_(MARLIN_DEV_MODE, "S")           // Simulated Homing
362
+      TERN_(IS_CARTESIAN, "XY")             // Don't home Z on Cartesian
363
+    ));
318 364
 
319
-  // Select the previously active tool (with no_move)
320
-  #if EXTRUDERS > 1
321
-    sprintf_P(cmd, PSTR("T%i S"), info.active_extruder);
322
-    gcode.process_subcommands_now(cmd);
323 365
   #endif
324 366
 
367
+  // Pretend that all axes are homed
368
+  axis_homed = axis_known_position = xyz_bits;
369
+
325 370
   // Recover volumetric extrusion state
326 371
   #if DISABLED(NO_VOLUMETRICS)
327 372
     #if EXTRUDERS > 1
328 373
       for (int8_t e = 0; e < EXTRUDERS; e++) {
329
-        dtostrf(info.filament_size[e], 1, 3, str_1);
330
-        sprintf_P(cmd, PSTR("M200 T%i D%s"), e, str_1);
374
+        sprintf_P(cmd, PSTR("M200 T%i D%s"), e, dtostrf(info.filament_size[e], 1, 3, str_1));
331 375
         gcode.process_subcommands_now(cmd);
332 376
       }
333 377
       if (!info.volumetric_enabled) {
@@ -336,8 +380,7 @@ void PrintJobRecovery::resume() {
336 380
       }
337 381
     #else
338 382
       if (info.volumetric_enabled) {
339
-        dtostrf(info.filament_size[0], 1, 3, str_1);
340
-        sprintf_P(cmd, PSTR("M200 D%s"), str_1);
383
+        sprintf_P(cmd, PSTR("M200 D%s"), dtostrf(info.filament_size[0], 1, 3, str_1));
341 384
         gcode.process_subcommands_now(cmd);
342 385
       }
343 386
     #endif
@@ -358,7 +401,7 @@ void PrintJobRecovery::resume() {
358 401
       const int16_t et = info.target_temperature[e];
359 402
       if (et) {
360 403
         #if HAS_MULTI_HOTEND
361
-          sprintf_P(cmd, PSTR("T%i"), e);
404
+          sprintf_P(cmd, PSTR("T%i S"), e);
362 405
           gcode.process_subcommands_now(cmd);
363 406
         #endif
364 407
         sprintf_P(cmd, PSTR("M109 S%i"), et);
@@ -367,6 +410,12 @@ void PrintJobRecovery::resume() {
367 410
     }
368 411
   #endif
369 412
 
413
+  // Select the previously active tool (with no_move)
414
+  #if EXTRUDERS > 1
415
+    sprintf_P(cmd, PSTR("T%i S"), info.active_extruder);
416
+    gcode.process_subcommands_now(cmd);
417
+  #endif
418
+
370 419
   // Restore print cooling fan speeds
371 420
   FANS_LOOP(i) {
372 421
     uint8_t f = info.fan_speed[i];
@@ -400,18 +449,21 @@ void PrintJobRecovery::resume() {
400 449
     memcpy(&mixer.gradient, &info.gradient, sizeof(info.gradient));
401 450
   #endif
402 451
 
403
-  // Extrude and retract to clean the nozzle
404
-  #if POWER_LOSS_PURGE_LEN
405
-    //sprintf_P(cmd, PSTR("G1 E%d F200"), POWER_LOSS_PURGE_LEN);
406
-    //gcode.process_subcommands_now(cmd);
407
-    gcode.process_subcommands_now_P(PSTR("G1 E" STRINGIFY(POWER_LOSS_PURGE_LEN) " F200"));
452
+  // Un-retract if there was a retract at outage
453
+  #if POWER_LOSS_RETRACT_LEN
454
+    gcode.process_subcommands_now_P(PSTR("G1 E" STRINGIFY(POWER_LOSS_RETRACT_LEN) " F3000"));
408 455
   #endif
409 456
 
410
-  #if POWER_LOSS_RETRACT_LEN
411
-    sprintf_P(cmd, PSTR("G1 E%d F3000"), POWER_LOSS_PURGE_LEN - (POWER_LOSS_RETRACT_LEN));
457
+  // Additional purge if configured
458
+  #if POWER_LOSS_PURGE_LEN
459
+    sprintf_P(cmd, PSTR("G1 E%d F200"), (POWER_LOSS_PURGE_LEN) + (POWER_LOSS_RETRACT_LEN));
412 460
     gcode.process_subcommands_now(cmd);
413 461
   #endif
414 462
 
463
+  #if ENABLED(NOZZLE_CLEAN_FEATURE)
464
+    gcode.process_subcommands_now_P(PSTR("G12"));
465
+  #endif
466
+
415 467
   // Move back to the saved XY
416 468
   sprintf_P(cmd, PSTR("G1 X%s Y%s F3000"),
417 469
     dtostrf(info.current_position.x, 1, 3, str_1),
@@ -429,13 +481,6 @@ void PrintJobRecovery::resume() {
429 481
   #endif
430 482
   gcode.process_subcommands_now(cmd);
431 483
 
432
-  // Un-retract
433
-  #if POWER_LOSS_PURGE_LEN
434
-    //sprintf_P(cmd, PSTR("G1 E%d F3000"), POWER_LOSS_PURGE_LEN);
435
-    //gcode.process_subcommands_now(cmd);
436
-    gcode.process_subcommands_now_P(PSTR("G1 E" STRINGIFY(POWER_LOSS_PURGE_LEN) " F3000"));
437
-  #endif
438
-
439 484
   // Restore the feedrate
440 485
   sprintf_P(cmd, PSTR("G1 F%d"), info.feedrate);
441 486
   gcode.process_subcommands_now(cmd);
@@ -476,6 +521,8 @@ void PrintJobRecovery::resume() {
476 521
         }
477 522
         DEBUG_EOL();
478 523
 
524
+        DEBUG_ECHOLNPAIR("zraise: ", info.zraise);
525
+
479 526
         #if HAS_HOME_OFFSET
480 527
           DEBUG_ECHOPGM("home_offset: ");
481 528
           LOOP_XYZ(i) {

+ 23
- 19
Marlin/src/feature/powerloss.h View File

@@ -26,6 +26,8 @@
26 26
  */
27 27
 
28 28
 #include "../sd/cardreader.h"
29
+#include "../gcode/gcode.h"
30
+
29 31
 #include "../inc/MarlinConfig.h"
30 32
 
31 33
 #if ENABLED(MIXING_EXTRUDER)
@@ -45,6 +47,7 @@ typedef struct {
45 47
 
46 48
   // Machine state
47 49
   xyze_pos_t current_position;
50
+  float zraise;
48 51
 
49 52
   #if HAS_HOME_OFFSET
50 53
     xyz_pos_t home_offset;
@@ -161,33 +164,34 @@ class PrintJobRecovery {
161 164
     static inline void cancel() { purge(); card.autostart_index = 0; }
162 165
 
163 166
     static void load();
164
-    static void save(const bool force=ENABLED(SAVE_EACH_CMD_MODE));
167
+    static void save(const bool force=ENABLED(SAVE_EACH_CMD_MODE), const float zraise=0);
165 168
 
166
-  #if PIN_EXISTS(POWER_LOSS)
167
-    static inline void outage() {
168
-      if (enabled && READ(POWER_LOSS_PIN) == POWER_LOSS_STATE)
169
-        _outage();
170
-    }
171
-  #endif
169
+    #if PIN_EXISTS(POWER_LOSS)
170
+      static inline void outage() {
171
+        if (enabled && READ(POWER_LOSS_PIN) == POWER_LOSS_STATE)
172
+          _outage();
173
+      }
174
+    #endif
172 175
 
173
-  static inline bool valid() { return info.valid(); }
176
+    static inline bool valid() { return info.valid(); }
174 177
 
175
-  #if ENABLED(DEBUG_POWER_LOSS_RECOVERY)
176
-    static void debug(PGM_P const prefix);
177
-  #else
178
-    static inline void debug(PGM_P const) {}
179
-  #endif
178
+    #if ENABLED(DEBUG_POWER_LOSS_RECOVERY)
179
+      static void debug(PGM_P const prefix);
180
+    #else
181
+      static inline void debug(PGM_P const) {}
182
+    #endif
180 183
 
181 184
   private:
182 185
     static void write();
183 186
 
184
-  #if ENABLED(BACKUP_POWER_SUPPLY)
185
-    static void raise_z();
186
-  #endif
187
+    #if ENABLED(BACKUP_POWER_SUPPLY)
188
+      static void retract_and_lift(const float &zraise);
189
+    #endif
187 190
 
188
-  #if PIN_EXISTS(POWER_LOSS)
189
-    static void _outage();
190
-  #endif
191
+    #if PIN_EXISTS(POWER_LOSS)
192
+      friend class GcodeSuite;
193
+      static void _outage();
194
+    #endif
191 195
 };
192 196
 
193 197
 extern PrintJobRecovery recovery;

+ 3
- 0
Marlin/src/gcode/feature/powerloss/M413.cpp View File

@@ -50,6 +50,9 @@ void GcodeSuite::M413() {
50 50
     if (parser.seen("RL")) recovery.load();
51 51
     if (parser.seen('W')) recovery.save(true);
52 52
     if (parser.seen('P')) recovery.purge();
53
+    #if PIN_EXISTS(POWER_LOSS)
54
+      if (parser.seen('O')) recovery._outage();
55
+    #endif
53 56
     if (parser.seen('E')) serialprintPGM(recovery.exists() ? PSTR("PLR Exists\n") : PSTR("No PLR\n"));
54 57
     if (parser.seen('V')) serialprintPGM(recovery.valid() ? PSTR("Valid\n") : PSTR("Invalid\n"));
55 58
   #endif

+ 1
- 1
Marlin/src/lcd/language/language_en.h View File

@@ -363,7 +363,7 @@ namespace Language_en {
363 363
   PROGMEM Language_Str MSG_PRINTING_OBJECT                 = _UxGT("Printing Object");
364 364
   PROGMEM Language_Str MSG_CANCEL_OBJECT                   = _UxGT("Cancel Object");
365 365
   PROGMEM Language_Str MSG_CANCEL_OBJECT_N                 = _UxGT("Cancel Object =");
366
-  PROGMEM Language_Str MSG_OUTAGE_RECOVERY                 = _UxGT("Outage Recovery");
366
+  PROGMEM Language_Str MSG_OUTAGE_RECOVERY                 = _UxGT("Power Outage");
367 367
   PROGMEM Language_Str MSG_MEDIA_MENU                      = _UxGT("Print from Media");
368 368
   PROGMEM Language_Str MSG_NO_MEDIA                        = _UxGT("No Media");
369 369
   PROGMEM Language_Str MSG_DWELL                           = _UxGT("Sleep...");

+ 9
- 13
Marlin/src/module/temperature.cpp View File

@@ -2026,33 +2026,29 @@ void Temperature::disable_all_heaters() {
2026 2026
 
2027 2027
   TERN_(AUTOTEMP, planner.autotemp_enabled = false);
2028 2028
 
2029
-  #if HAS_HOTEND
2030
-    HOTEND_LOOP() setTargetHotend(0, e);
2031
-  #endif
2032
-  TERN_(HAS_HEATED_BED, setTargetBed(0));
2033
-  TERN_(HAS_HEATED_CHAMBER, setTargetChamber(0));
2034
-
2035 2029
   // Unpause and reset everything
2036 2030
   TERN_(PROBING_HEATERS_OFF, pause(false));
2037 2031
 
2038
-  #define DISABLE_HEATER(N) {           \
2039
-    setTargetHotend(0, N);              \
2040
-    temp_hotend[N].soft_pwm_amount = 0; \
2041
-    WRITE_HEATER_##N(LOW);              \
2042
-  }
2032
+  #if HAS_HOTEND
2033
+    HOTEND_LOOP() {
2034
+      setTargetHotend(0, e);
2035
+      temp_hotend[e].soft_pwm_amount = 0;
2036
+    }
2037
+  #endif
2043 2038
 
2044 2039
   #if HAS_TEMP_HOTEND
2040
+    #define DISABLE_HEATER(N) WRITE_HEATER_##N(LOW)
2045 2041
     REPEAT(HOTENDS, DISABLE_HEATER);
2046 2042
   #endif
2047 2043
 
2048 2044
   #if HAS_HEATED_BED
2049
-    temp_bed.target = 0;
2045
+    setTargetBed(0);
2050 2046
     temp_bed.soft_pwm_amount = 0;
2051 2047
     WRITE_HEATER_BED(LOW);
2052 2048
   #endif
2053 2049
 
2054 2050
   #if HAS_HEATED_CHAMBER
2055
-    temp_chamber.target = 0;
2051
+    setTargetChamber(0);
2056 2052
     temp_chamber.soft_pwm_amount = 0;
2057 2053
     WRITE_HEATER_CHAMBER(LOW);
2058 2054
   #endif

+ 2
- 2
Marlin/src/module/temperature.h View File

@@ -576,7 +576,7 @@ class Temperature {
576 576
           else if (temp_hotend[ee].target == 0)
577 577
             start_preheat_time(ee);
578 578
         #endif
579
-        TERN_(AUTO_POWER_CONTROL, powerManager.power_on());
579
+        TERN_(AUTO_POWER_CONTROL, if (celsius) powerManager.power_on());
580 580
         temp_hotend[ee].target = _MIN(celsius, temp_range[ee].maxtemp - HOTEND_OVERSHOOT);
581 581
         start_watching_hotend(ee);
582 582
       }
@@ -624,7 +624,7 @@ class Temperature {
624 624
       #endif
625 625
 
626 626
       static void setTargetBed(const int16_t celsius) {
627
-        TERN_(AUTO_POWER_CONTROL, powerManager.power_on());
627
+        TERN_(AUTO_POWER_CONTROL, if (celsius) powerManager.power_on());
628 628
         temp_bed.target =
629 629
           #ifdef BED_MAX_TARGET
630 630
             _MIN(celsius, BED_MAX_TARGET)

+ 1
- 0
buildroot/tests/rambo-tests View File

@@ -40,6 +40,7 @@ opt_enable REPRAP_DISCOUNT_SMART_CONTROLLER LCD_PROGRESS_BAR LCD_PROGRESS_BAR_TE
40 40
            PSU_CONTROL AUTO_POWER_CONTROL POWER_LOSS_RECOVERY POWER_LOSS_PIN POWER_LOSS_STATE \
41 41
            SLOW_PWM_HEATERS THERMAL_PROTECTION_CHAMBER LIN_ADVANCE EXTRA_LIN_ADVANCE_K \
42 42
            HOST_ACTION_COMMANDS HOST_PROMPT_SUPPORT PINS_DEBUGGING MAX7219_DEBUG M114_DETAIL
43
+opt_add DEBUG_POWER_LOSS_RECOVERY
43 44
 exec_test $1 $2 "RAMBO | EXTRUDERS 2 | CHAR LCD + SD | FIX Probe | ABL-Linear | Advanced Pause | PLR | LEDs ..."
44 45
 
45 46
 #

Loading…
Cancel
Save