diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 74774f1bf2..79d50e844c 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -19225,6 +19225,12 @@ msgstr "" msgid "Could not connect to Duet" msgstr "" +msgid "Connection to Ultimaker is working correctly." +msgstr "" + +msgid "Could not connect to Ultimaker" +msgstr "" + msgid "Unknown error occurred" msgstr "" diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index cca4d9b9a7..7636bf2484 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -250,6 +250,7 @@ src/slic3r/GUI/TroubleshootDialog.cpp src/slic3r/Utils/3DPrinterOS.cpp src/slic3r/Utils/AstroBox.cpp src/slic3r/Utils/Duet.cpp +src/slic3r/Utils/UltiMaker.cpp src/slic3r/Utils/FlashAir.cpp src/slic3r/Utils/MKS.cpp src/slic3r/Utils/Moonraker.cpp diff --git a/resources/profiles/UltiMaker.json b/resources/profiles/UltiMaker.json index 52daf03cec..d437ce7191 100644 --- a/resources/profiles/UltiMaker.json +++ b/resources/profiles/UltiMaker.json @@ -1,13 +1,17 @@ { "name": "UltiMaker", "url": "", - "version": "02.04.00.02", + "version": "02.04.00.03", "force_update": "0", "description": "UltiMaker configurations", "machine_model_list": [ { "name": "UltiMaker 2", "sub_path": "machine/UltiMaker 2.json" + }, + { + "name": "UltiMaker S5", + "sub_path": "machine/UltiMaker S5.json" } ], "process_list": [ @@ -23,9 +27,17 @@ "name": "0.18mm Standard @UltiMaker 2", "sub_path": "process/0.18mm Standard @UltiMaker 2.json" }, + { + "name": "0.18mm Standard @UltiMaker S5", + "sub_path": "process/0.18mm Standard @UltiMaker S5.json" + }, { "name": "0.25mm Draft @UltiMaker 2", "sub_path": "process/0.25mm Darft @UltiMaker 2.json" + }, + { + "name": "0.2mm Fast @UltiMaker S5", + "sub_path": "process/0.2mm Fast @UltiMaker S5.json" } ], "filament_list": [], @@ -37,6 +49,14 @@ { "name": "UltiMaker 2 0.4 nozzle", "sub_path": "machine/UltiMaker 2 0.4 nozzle.json" + }, + { + "name": "UltiMaker S5 0.4 nozzle", + "sub_path": "machine/UltiMaker S5 0.4 nozzle.json" + }, + { + "name": "UltiMaker S5 0.4 nozzle - Single Extruder", + "sub_path": "machine/UltiMaker S5 0.4 nozzle Single Extruder.json" } ] } diff --git a/resources/profiles/UltiMaker/UltiMaker S5_cover.png b/resources/profiles/UltiMaker/UltiMaker S5_cover.png new file mode 100644 index 0000000000..f123d6c81c Binary files /dev/null and b/resources/profiles/UltiMaker/UltiMaker S5_cover.png differ diff --git a/resources/profiles/UltiMaker/machine/UltiMaker S5 0.4 nozzle Single Extruder.json b/resources/profiles/UltiMaker/machine/UltiMaker S5 0.4 nozzle Single Extruder.json new file mode 100644 index 0000000000..2bd40746c9 --- /dev/null +++ b/resources/profiles/UltiMaker/machine/UltiMaker S5 0.4 nozzle Single Extruder.json @@ -0,0 +1,38 @@ +{ + "type": "machine", + "name": "UltiMaker S5 0.4 nozzle - Single Extruder", + "inherits": "UltiMaker S5 0.4 nozzle", + "from": "system", + "setting_id": "7BKZlwffTrzDsUgj", + "instantiation": "true", + "printer_model": "UltiMaker S5", + "default_print_profile": "0.18mm Standard @UltiMaker S5", + "default_nozzle_volume_type": [ + "Standard" + ], + "extruder_colour": [ + "#FCE94F" + ], + "extruder_offset": [ + "0x0" + ], + "extruder_type": [ + "Direct Drive" + ], + "machine_start_gcode": "; NOTE: Do NOT remove the following header. It is necessary for the UM printer to not crash.\n;START_OF_HEADER\n;HEADER_VERSION:0.1\n;FLAVOR:Griffin\n;GENERATOR.NAME:OrcaSlicer\n;GENERATOR.VERSION:5.11.0\n;GENERATOR.BUILD_DATE:2016-06-23\n;TARGET_MACHINE.NAME:[printer_model]\n;EXTRUDER_TRAIN.0.INITIAL_TEMPERATURE:{first_layer_temperature[0]}\n;EXTRUDER_TRAIN.0.MATERIAL.VOLUME_USED:{extruded_volume[0]}\n;EXTRUDER_TRAIN.0.MATERIAL.GUID:506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9\n;EXTRUDER_TRAIN.0.NOZZLE.DIAMETER:{nozzle_diameter[0]}\n;EXTRUDER_TRAIN.0.NOZZLE.NAME:AA {nozzle_diameter[0]}\n;BUILD_PLATE.INITIAL_TEMPERATURE:[bed_temperature_initial_layer_single]\n;BUILD_VOLUME.TEMPERATURE:28\n;PRINT.TIME:2\n;PRINT.SIZE.MIN.X:{first_layer_print_min[0]}\n;PRINT.SIZE.MIN.Y:{first_layer_print_min[1]}\n;PRINT.SIZE.MIN.Z:{first_layer_height}\n;PRINT.SIZE.MAX.X:{first_layer_print_max[0]}\n;PRINT.SIZE.MAX.Y:{first_layer_print_max[1]}\n;PRINT.SIZE.MAX.Z:{int(total_layer_count * first_layer_height)}\n;END_OF_HEADER\nT[initial_extruder]\n\nM82 ;absolute extrusion mode\nG92 E0\nM190 S[bed_temperature_initial_layer_single] ; Wait for bed temp\nM104 T[initial_extruder] S[first_layer_temperature[initial_extruder]] ; Heat extruder\nM109 T[initial_extruder] S[first_layer_temperature[initial_extruder]] ; Wait for extruder temp\n\n; Set the settings we removed in the post-processing (orcaslicer generated header).\nM73 P0 R0 ;TODO change the R value into the total minutes of print\nM201 X[machine_max_acceleration_x] Y[machine_max_acceleration_y] Z[machine_max_acceleration_z] E[machine_max_acceleration_e] ; accel\nM203 X[machine_max_speed_x] Y[machine_max_speed_y] Z[machine_max_speed_z] E[machine_max_speed_e] ; max speed\nM204 S[machine_max_acceleration_extruding] ; starting accel\nM204 P[machine_max_acceleration_extruding] R[machine_max_acceleration_retracting] T[machine_max_acceleration_travel]\nM205 X[machine_max_jerk_x] Y[machine_max_jerk_y] Z[machine_max_jerk_z] E[machine_max_jerk_e] ; jerk limits\n\nM107; start with the fan off\n\n; Prime blob for left extruder\nG0 F9000 X4 Y6 Z2\nG280 ; g280 is UM-specific\nG1 F2700 E-6.5 ;retract\n\nM82; set extruder to absolute mode\n\n; End of the start gcode sequence.\n;LAYER_COUNT:[total_layer_count]", + "max_layer_height": [ + "0.4" + ], + "min_layer_height": [ + "0.06" + ], + "nozzle_diameter": [ + "0.4" + ], + "printer_extruder_id": [ + "1" + ], + "printer_extruder_variant": [ + "Direct Drive Standard" + ] +} diff --git a/resources/profiles/UltiMaker/machine/UltiMaker S5 0.4 nozzle.json b/resources/profiles/UltiMaker/machine/UltiMaker S5 0.4 nozzle.json new file mode 100644 index 0000000000..5c212070b7 --- /dev/null +++ b/resources/profiles/UltiMaker/machine/UltiMaker S5 0.4 nozzle.json @@ -0,0 +1,139 @@ +{ + "type": "machine", + "name": "UltiMaker S5 0.4 nozzle", + "inherits": "fdm_machine_common", + "from": "system", + "setting_id": "hmkJtw7J1KXbiYfj", + "instantiation": "true", + "printer_model": "UltiMaker S5", + "default_print_profile": "0.18mm Standard @UltiMaker S5", + "nozzle_diameter": [ + "0.4", + "0.4" + ], + "printable_area": [ + "0x0", + "330x0", + "330x240", + "0x240" + ], + "printable_height": "300", + "extruder_clearance_height_to_rod": "60", + "extruder_clearance_radius": "61", + "nozzle_type": "brass", + "auxiliary_fan": "0", + "use_relative_e_distances": "0", + "machine_max_acceleration_extruding": [ + "3500", + "3500" + ], + "machine_max_acceleration_retracting": [ + "3500", + "3500" + ], + "machine_max_acceleration_travel": [ + "3500", + "3500" + ], + "machine_max_acceleration_x": [ + "3500", + "3500" + ], + "machine_max_acceleration_y": [ + "3500", + "3500" + ], + "machine_max_acceleration_z": [ + "500", + "500" + ], + "machine_max_speed_e": [ + "120", + "120" + ], + "machine_max_speed_x": [ + "500", + "500" + ], + "machine_max_speed_y": [ + "500", + "500" + ], + "machine_max_speed_z": [ + "12", + "12" + ], + "machine_max_jerk_e": [ + "2.5", + "2.5" + ], + "machine_max_jerk_x": [ + "20", + "20" + ], + "machine_max_jerk_y": [ + "20", + "20" + ], + "machine_max_jerk_z": [ + "0.4", + "0.4" + ], + "max_layer_height": [ + "0.4", + "0.4" + ], + "min_layer_height": [ + "0.06", + "0.06" + ], + "printer_settings_id": "Qidi", + "retraction_minimum_travel": [ + "5" + ], + "retract_before_wipe": [ + "70%" + ], + "retraction_length": [ + "4.5" + ], + "retract_length_toolchange": [ + "11" + ], + "retraction_speed": [ + "45" + ], + "deretraction_speed": [ + "0" + ], + "wipe_distance": [ + "0.2" + ], + "single_extruder_multi_material": "0", + "change_filament_gcode": "; Set temps to avoid oozing\nM104 T{previous_extruder} S{old_filament_temp - 5}\nM104 T{next_extruder} S{new_filament_temp}\n; Change to next extruder\nT{next_extruder}", + "machine_pause_gcode": "M0", + "default_filament_profile": [ + "Generic PLA @System" + ], + "z_hop": [ + "1" + ], + "z_hop_types": [ + "Normal Lift" + ], + "extruder_type": [ + "Direct Drive", + "Direct Drive" + ], + "extruder_offset": [ + "0x0", + "0x22" + ], + "extra_loading_move": "0", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n", + "fan_speedup_time": "0.5", + "machine_start_gcode": "; NOTE: Do NOT remove the following header. It is necessary for the UM printer to not crash.\n;START_OF_HEADER\n;HEADER_VERSION:0.1\n;FLAVOR:Griffin\n;GENERATOR.NAME:OrcaSlicer\n;GENERATOR.VERSION:5.11.0\n;GENERATOR.BUILD_DATE:2016-06-23\n;TARGET_MACHINE.NAME:[printer_model]\n{if is_extruder_used[0]};EXTRUDER_TRAIN.0.INITIAL_TEMPERATURE:{first_layer_temperature[0]}\n;EXTRUDER_TRAIN.0.MATERIAL.VOLUME_USED:{extruded_volume[0]}\n;EXTRUDER_TRAIN.0.MATERIAL.GUID:506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9\n;EXTRUDER_TRAIN.0.NOZZLE.DIAMETER:{nozzle_diameter[0]}\n;EXTRUDER_TRAIN.0.NOZZLE.NAME:AA {nozzle_diameter[0]}{endif}\n{if is_extruder_used[1]};EXTRUDER_TRAIN.1.INITIAL_TEMPERATURE:{first_layer_temperature[1]}\n;EXTRUDER_TRAIN.1.MATERIAL.VOLUME_USED:{extruded_volume[1]}\n;EXTRUDER_TRAIN.1.MATERIAL.GUID:506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9\n;EXTRUDER_TRAIN.1.NOZZLE.DIAMETER:{nozzle_diameter[1]}\n;EXTRUDER_TRAIN.1.NOZZLE.NAME:AA {nozzle_diameter[1]}{endif}\n;BUILD_PLATE.INITIAL_TEMPERATURE:[bed_temperature_initial_layer_single]\n;BUILD_VOLUME.TEMPERATURE:28\n;PRINT.TIME:2\n;PRINT.SIZE.MIN.X:{first_layer_print_min[0]}\n;PRINT.SIZE.MIN.Y:{first_layer_print_min[1]}\n;PRINT.SIZE.MIN.Z:{first_layer_height}\n;PRINT.SIZE.MAX.X:{first_layer_print_max[0]}\n;PRINT.SIZE.MAX.Y:{first_layer_print_max[1]}\n;PRINT.SIZE.MAX.Z:{int(total_layer_count * first_layer_height)}\n;END_OF_HEADER\nT[initial_extruder]\n\nM82 ;absolute extrusion mode\nG92 E0\nM190 S[bed_temperature_initial_layer_single] ; Wait for bed temp\nM104 T[initial_extruder] S[first_layer_temperature[initial_extruder]] ; Heat extruder\nM109 T[initial_extruder] S[first_layer_temperature[initial_extruder]] ; Wait for extruder temp\n\n\n; Set the settings we removed in the post-processing (orcaslicer generated header).\nM73 P0 R0 ;TODO change the R value into the total minutes of print\nM201 X[machine_max_acceleration_x] Y[machine_max_acceleration_y] Z[machine_max_acceleration_z] E[machine_max_acceleration_e] ; accel\nM203 X[machine_max_speed_x] Y[machine_max_speed_y] Z[machine_max_speed_z] E[machine_max_speed_e] ; max speed\nM204 S[machine_max_acceleration_extruding] ; starting accel\nM204 P[machine_max_acceleration_extruding] R[machine_max_acceleration_retracting] T[machine_max_acceleration_travel]\nM205 X[machine_max_jerk_x] Y[machine_max_jerk_y] Z[machine_max_jerk_z] E[machine_max_jerk_e] ; jerk limits\n\nM107; start with the fan off\n\n{if is_extruder_used[0]}T0\n; Prime blob for left extruder\nG0 F9000 X4 Y6 Z2\nG280 ; g280 is UM-specific\nG1 F2700 E-6.5 ;retract{endif}\n\n{if is_extruder_used[1]}T1\n; Prime blob for right extruder\nG0 F9000 X-4 Y6 Z2\nG280 ; g280 is UM-specific\nG1 F2700 E-6.5 ;retract\n{if initial_extruder == 0}T0{endif}{endif}\n\n\n\nM82; set extruder to absolute mode\n\n; End of the start gcode sequence.\n;LAYER_COUNT:[total_layer_count]", + "machine_end_gcode": "; Move toolhead out of the way\nG28 X Y\n\nM204 S3000; reset accel\nM107; fan off\n\n; heaters off\nM104 S0\nM104 T0 S0\nM104 T1 S0\nM140 S0\n\n; disable motors\nM84\n\n\nM82 ;absolute extrusion mode\n\n;End of Gcode\n", + "machine_tool_change_time": "1.5", + "scan_first_layer": "0" +} diff --git a/resources/profiles/UltiMaker/machine/UltiMaker S5.json b/resources/profiles/UltiMaker/machine/UltiMaker S5.json new file mode 100644 index 0000000000..2a9b858938 --- /dev/null +++ b/resources/profiles/UltiMaker/machine/UltiMaker S5.json @@ -0,0 +1,12 @@ +{ + "type": "machine_model", + "name": "UltiMaker S5", + "model_id": "UltiMaker-S5", + "nozzle_diameter": "0.4", + "machine_tech": "FFF", + "family": "UltiMaker", + "bed_model": "ultimaker_s5_buildplate_model.stl", + "bed_texture": "ultimaker_2_buildplate_texture.png", + "hotend_model": "ultimaker_hotend.stl", + "default_materials": "Generic ABS @System;Generic PETG @System;Generic PLA @System" +} diff --git a/resources/profiles/UltiMaker/machine/fdm_machine_common.json b/resources/profiles/UltiMaker/machine/fdm_machine_common.json index 035efc68ba..2f5795627a 100644 --- a/resources/profiles/UltiMaker/machine/fdm_machine_common.json +++ b/resources/profiles/UltiMaker/machine/fdm_machine_common.json @@ -112,5 +112,6 @@ "default_print_profile": "", "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n", "machine_start_gcode": "", - "machine_end_gcode": "" + "machine_end_gcode": "", + "thumbnails": "" } diff --git a/resources/profiles/UltiMaker/process/0.18mm Standard @UltiMaker S5.json b/resources/profiles/UltiMaker/process/0.18mm Standard @UltiMaker S5.json new file mode 100644 index 0000000000..7920fed2e6 --- /dev/null +++ b/resources/profiles/UltiMaker/process/0.18mm Standard @UltiMaker S5.json @@ -0,0 +1,111 @@ +{ + "type": "process", + "name": "0.18mm Standard @UltiMaker S5", + "inherits": "fdm_process_common", + "from": "system", + "setting_id": "UAhqUBDi1CzMB2Vg", + "instantiation": "true", + "ooze_prevention": "1", + "preheat_time": "20", + "reduce_crossing_wall": "0", + "layer_height": "0.18", + "max_travel_detour_distance": "0", + "bottom_surface_pattern": "monotonic", + "bottom_shell_layers": "4", + "bottom_shell_thickness": "0.8", + "bridge_flow": "1", + "bridge_speed": "60", + "brim_width": "0", + "brim_object_gap": "0", + "compatible_printers_condition": "", + "print_sequence": "by layer", + "default_acceleration": "0", + "top_surface_acceleration": "0", + "bridge_no_support": "0", + "draft_shield": "disabled", + "elefant_foot_compensation": "0", + "enable_arc_fitting": "0", + "outer_wall_line_width": "0.4", + "wall_infill_order": "inner wall/outer wall/infill", + "line_width": "0.45", + "infill_direction": "45", + "sparse_infill_density": "15%", + "sparse_infill_pattern": "crosshatch", + "initial_layer_acceleration": "500", + "travel_acceleration": "0", + "inner_wall_acceleration": "0", + "initial_layer_line_width": "0.45", + "initial_layer_print_height": "0.25", + "infill_combination": "0", + "sparse_infill_line_width": "0.5", + "infill_wall_overlap": "35%", + "interface_shells": "0", + "ironing_flow": "15%", + "ironing_spacing": "0.1", + "ironing_speed": "15", + "ironing_type": "no ironing", + "reduce_infill_retraction": "1", + "filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode", + "detect_overhang_wall": "1", + "overhang_1_4_speed": "0", + "overhang_2_4_speed": "20", + "overhang_3_4_speed": "15", + "overhang_4_4_speed": "10", + "inner_wall_line_width": "0.45", + "wall_loops": "3", + "print_settings_id": "", + "raft_layers": "0", + "seam_position": "aligned", + "skirt_distance": "3", + "skirt_height": "1", + "skirt_loops": "2", + "minimum_sparse_infill_area": "10", + "internal_solid_infill_line_width": "0.45", + "spiral_mode": "0", + "standby_temperature_delta": "-10", + "enable_support": "0", + "resolution": "0.012", + "support_type": "normal(auto)", + "support_style": "grid", + "support_on_build_plate_only": "0", + "support_top_z_distance": "0.2", + "support_filament": "0", + "support_line_width": "0.45", + "support_interface_loop_pattern": "0", + "support_interface_filament": "0", + "support_interface_top_layers": "3", + "support_interface_bottom_layers": "-1", + "support_interface_spacing": "0.2", + "support_interface_speed": "100%", + "support_base_pattern": "rectilinear", + "support_base_pattern_spacing": "0.2", + "support_speed": "45", + "support_threshold_angle": "30", + "support_object_xy_distance": "50%", + "tree_support_branch_angle": "40", + "tree_support_wall_count": "0", + "detect_thin_wall": "1", + "top_surface_pattern": "monotonicline", + "top_surface_line_width": "0.4", + "top_shell_layers": "5", + "top_shell_thickness": "0.8", + "initial_layer_speed": "15", + "initial_layer_infill_speed": "30", + "initial_layer_travel_speed": "60", + "outer_wall_speed": "30", + "inner_wall_speed": "40", + "internal_solid_infill_speed": "40", + "top_surface_speed": "15", + "gap_infill_speed": "20", + "sparse_infill_speed": "40", + "travel_speed": "120", + "enable_prime_tower": "0", + "wipe_tower_no_sparse_layers": "0", + "prime_tower_width": "60", + "xy_hole_compensation": "0", + "xy_contour_compensation": "0", + "compatible_printers": [ + "UltiMaker S5 0.4 nozzle", + "UltiMaker S5 0.4 nozzle - Single Extruder" + ] +} diff --git a/resources/profiles/UltiMaker/process/0.2mm Fast @UltiMaker S5.json b/resources/profiles/UltiMaker/process/0.2mm Fast @UltiMaker S5.json new file mode 100644 index 0000000000..16fb188ca1 --- /dev/null +++ b/resources/profiles/UltiMaker/process/0.2mm Fast @UltiMaker S5.json @@ -0,0 +1,45 @@ +{ + "type": "process", + "name": "0.2mm Fast @UltiMaker S5", + "inherits": "0.18mm Standard @UltiMaker S5", + "from": "system", + "setting_id": "Ai0I1drAIYiCbydj", + "instantiation": "true", + "default_acceleration": "3500", + "default_jerk": "20", + "enable_support": "1", + "gap_infill_speed": "25", + "infill_combination": "1", + "infill_combination_max_layer_height": "80%", + "infill_jerk": "20", + "initial_layer_acceleration": "1500", + "initial_layer_infill_speed": "40", + "initial_layer_jerk": "10", + "initial_layer_speed": "30", + "initial_layer_travel_speed": "150", + "inner_wall_acceleration": "3500", + "inner_wall_jerk": "20", + "inner_wall_speed": "100", + "layer_height": "0.2", + "outer_wall_acceleration": "1500", + "outer_wall_jerk": "10", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "print_settings_id": "", + "sparse_infill_pattern": "adaptivecubic", + "sparse_infill_speed": "100", + "support_style": "default", + "support_type": "tree(auto)", + "top_shell_layers": "1", + "top_surface_acceleration": "1500", + "top_surface_jerk": "10", + "top_surface_speed": "45", + "travel_acceleration": "3500", + "travel_jerk": "20", + "travel_speed": "150", + "wall_loops": "2" +} diff --git a/resources/profiles/UltiMaker/ultimaker_s5_buildplate_model.stl b/resources/profiles/UltiMaker/ultimaker_s5_buildplate_model.stl new file mode 100644 index 0000000000..4105f1fe3a Binary files /dev/null and b/resources/profiles/UltiMaker/ultimaker_s5_buildplate_model.stl differ diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 02d1b50198..75df1a42a6 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -152,6 +152,7 @@ static t_config_enum_values s_keys_map_PrintHostType { { "octoprint", htOctoPrint }, { "crealityprint", htCrealityPrint }, { "duet", htDuet }, + { "ultimaker", htUltiMaker }, { "flashair", htFlashAir }, { "astrobox", htAstroBox }, { "repetier", htRepetier }, @@ -5401,6 +5402,7 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("prusaconnect"); def->enum_values.push_back("octoprint"); def->enum_values.push_back("duet"); + def->enum_values.push_back("ultimaker"); def->enum_values.push_back("flashair"); def->enum_values.push_back("astrobox"); def->enum_values.push_back("repetier"); @@ -5417,6 +5419,7 @@ void PrintConfigDef::init_fff_params() def->enum_labels.push_back("PrusaConnect"); def->enum_labels.push_back("Octo/Klipper"); def->enum_labels.push_back("Duet"); + def->enum_labels.push_back("UltiMaker"); def->enum_labels.push_back("FlashAir"); def->enum_labels.push_back("AstroBox"); def->enum_labels.push_back("Repetier"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 727246ef73..eb03c8798e 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -98,7 +98,7 @@ enum class WipeTowerType { }; enum PrintHostType { - htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink, ht3DPrinterOS, htMoonraker + htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htUltiMaker, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink, ht3DPrinterOS, htMoonraker }; enum AuthorizationType { diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index e689548896..d04e38086f 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -776,6 +776,8 @@ set(SLIC3R_GUI_SOURCES Utils/SimplyPrint.hpp Utils/TCPConsole.cpp Utils/TCPConsole.hpp + Utils/UltiMaker.cpp + Utils/UltiMaker.hpp Utils/UndoRedo.cpp Utils/UndoRedo.hpp Utils/WebSocketClient.hpp diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 04317ca46b..1facddbff1 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -265,6 +265,43 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr return sizer; }; + + auto ultimaker_generate_creds = [=](wxWindow* parent) { + auto sizer = create_sizer_with_btn(parent, &m_printhost_generate_creds_btn, "ultimaker_generate_creds", _L("Generate API Key")); + + m_printhost_generate_creds_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + std::unique_ptr host(PrintHost::get_print_host(m_config)); + if (!host) { + const wxString text = _L("Could not get a valid Printer Host reference"); + show_error(this, text); + return; + } + + wxString msg = "generate_auth_creds"; + bool result; + { + // Show a wait cursor during the connection test, as it is blocking UI. + wxBusyCursor wait; + // Send request to printer for api key and such + result = host->test(msg); // using test with special input because I don't want to create the generate_auth_creds func for every printer + + // Prompt user to approve access on the machine. + show_info(this, "API Key created. Go to the physical printer and hit \"authorize\" on the screen, then run \"Test\" again.\n"+msg, "API Key created."); + + + } + if (result) + show_info(this, host->get_test_ok_msg(), _L("Success!")); + else + show_error(this, host->get_test_failed_msg(msg)); + + update(); + }); + + return sizer; + }; + + auto print_host_logout = [&](wxWindow* parent) { auto sizer = create_sizer_with_btn(parent, &m_printhost_logout_btn, "", _L("Log Out")); @@ -303,6 +340,7 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr Line host_line = m_optgroup->create_single_option_line(option); host_line.append_widget(printhost_browse); host_line.append_widget(print_host_test); + host_line.append_widget(ultimaker_generate_creds); host_line.append_widget(print_host_logout); m_optgroup->append_line(host_line); @@ -606,8 +644,7 @@ void PhysicalPrinterDialog::update(bool printer_change) m_optgroup->hide_field("bbl_use_print_host_webui"); m_optgroup->enable_field("printhost_cafile"); m_optgroup->enable_field("printhost_ssl_ignore_revoke"); - if (m_printhost_cafile_browse_btn) - m_printhost_cafile_browse_btn->Enable(); + if (m_printhost_cafile_browse_btn) { m_printhost_cafile_browse_btn->Enable(); } // hide pre-configured address, in case user switched to a different host type if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) { @@ -704,7 +741,24 @@ void PhysicalPrinterDialog::update(bool printer_change) m_optgroup->hide_field("printhost_authorization_type"); } else { m_optgroup->hide_field("flashforge_serial_number"); - } + } + + if (opt->value == htUltiMaker) { + m_optgroup->hide_field("printhost_apikey"); + m_optgroup->hide_field("printhost_authorization_type"); + m_optgroup->hide_field("bbl_use_print_host_webui"); + m_optgroup->hide_field("printhost_cafile"); + m_optgroup->show_field("printhost_user"); + m_optgroup->show_field("printhost_password"); + m_optgroup->enable_field("print_host"); + m_optgroup->show_field("print_host_webui"); + if (m_printhost_cafile_browse_btn) { + m_printhost_cafile_browse_btn->Disable(); + } + if (m_printhost_generate_creds_btn) { + m_printhost_generate_creds_btn->Enable(); + } + } } else { m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false); @@ -720,6 +774,10 @@ void PhysicalPrinterDialog::update(bool printer_change) m_optgroup->show_field(opt_key, auth_type == AuthorizationType::atUserPassword); } + // The "Generate API Key" button is only meaningful for UltiMaker printers. + if (m_printhost_generate_creds_btn) + m_printhost_generate_creds_btn->Show(tech == ptFFF && m_config->opt_enum("host_type") == htUltiMaker); + m_optgroup->show_field("printhost_port", supports_multiple_printers); m_printhost_port_browse_btn->Show(supports_multiple_printers); @@ -787,6 +845,7 @@ void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect) m_printhost_browse_btn->Rescale(); m_printhost_test_btn->Rescale(); + m_printhost_generate_creds_btn->Rescale(); m_printhost_logout_btn->Rescale(); if (m_printhost_cafile_browse_btn) m_printhost_cafile_browse_btn->Rescale(); diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index c32da334be..276e62416e 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -31,6 +31,7 @@ class PhysicalPrinterDialog : public DPIDialog Button* m_printhost_browse_btn {nullptr}; Button* m_printhost_test_btn {nullptr}; + Button* m_printhost_generate_creds_btn {nullptr}; Button* m_printhost_logout_btn {nullptr}; Button* m_printhost_cafile_browse_btn {nullptr}; Button* m_printhost_port_browse_btn {nullptr}; diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index cc15805256..37e0a41f83 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -15,6 +15,7 @@ #include "libslic3r/Channel.hpp" #include "OctoPrint.hpp" #include "Duet.hpp" +#include "UltiMaker.hpp" #include "FlashAir.hpp" #include "AstroBox.hpp" #include "Repetier.hpp" @@ -57,6 +58,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config) switch (host_type) { case htOctoPrint: return new OctoPrint(config); case htDuet: return new Duet(config); + case htUltiMaker: return new UltiMaker(config); case htFlashAir: return new FlashAir(config); case htAstroBox: return new AstroBox(config); case htRepetier: return new Repetier(config); diff --git a/src/slic3r/Utils/UltiMaker.cpp b/src/slic3r/Utils/UltiMaker.cpp new file mode 100644 index 0000000000..0376578db0 --- /dev/null +++ b/src/slic3r/Utils/UltiMaker.cpp @@ -0,0 +1,666 @@ +#include "UltiMaker.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "Http.hpp" + +#include +#include +#include +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + +namespace Slic3r { + +UltiMaker::UltiMaker(DynamicPrintConfig *config) : + + m_host(config->opt_string("print_host")), + host(config->opt_string("print_host")), + + m_api_username(config->opt_string("printhost_user")), + m_api_password(config->opt_string("printhost_password")), + m_ssl_revoke_best_effort(config->opt_bool("printhost_ssl_ignore_revoke")) +{} + +const char* UltiMaker::get_name() const { return "UltiMaker"; } + +// Modified from OctoPrint::test in OctoPrint.cpp +bool UltiMaker::test(wxString &msg) const +{ + // Returns true on success, false on error. + // If called with specific arg, just generate the auth creds. + if (msg == "generate_auth_creds") { + BOOST_LOG_TRIVIAL(debug) << "UltiMaker: test called with generate_auth_creds! Generating the auth credentials."; + return this->generate_auth_creds(msg); + } + + // Since the request is performed synchronously here, + // it is ok to refer to `msg` from within the closure + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: Attempting to test machine"); + + const char* name = get_name(); + + bool res = true; + auto url = (boost::format("http://%1%/api/v1/system/variant") % host).str(); + + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Get system variant at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + set_auth(http); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting system variant: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + res = false; + msg = format_error(body, error, status); + }) + .on_complete([name, &res](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got system variant: %2%") % name % body; + + // Validate that response is correct ("UltiMaker 3", "UltiMaker 3 extended" or "UltiMaker S5") + res = (boost::starts_with(body, "\"Ultimaker 3") || body == "\"Ultimaker S5\""); + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .on_ip_resolve([&](std::string address) { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + // Remember resolved address to be reused at successive REST API call. + msg = GUI::from_u8(address); + }) +#endif // WIN32 + .perform_sync(); + + + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Testing auth.") % name; + std::string auth_result = test_auth(); + if ( !boost::starts_with(auth_result,"OK") ) { msg = auth_result; res = false; }; + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Auth test result: %2%") % name % auth_result; + return res; +} + + +wxString UltiMaker::get_test_ok_msg () const +{ + return _("Connection to UltiMaker works correctly."); +} + +wxString UltiMaker::get_test_failed_msg (wxString &msg) const +{ + return GUI::from_u8((boost::format("%s: %s") + % _utf8("Could not connect to UltiMaker") + % std::string(msg.ToUTF8())).str()); +} + + +#pragma region Auth +// Modified from PrusaLink::set_auth in OctoPrint.cpp +void UltiMaker::set_auth(Http& http) const +{ + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: set_auth, m_api_username of %1% and m_api_password of %2%") % get_api_username() % get_api_password(); + http.auth_digest(m_api_username, m_api_password); +} + + +bool UltiMaker::has_auth_creds() const{ + // True if has authentication credentials, false otherwise + return (!m_api_username.empty()) && (!m_api_password.empty()); +} + + +bool UltiMaker::is_authorized() const{ + + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: is_authorized() checking if creds are valid."); + + const char* name = get_name(); + + bool rtn = false; + auto url = (boost::format("http://%1%/api/v1/auth/verify") % host).str(); + + auto http = Http::get(std::move(url)); + set_auth(http); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error verifying credentials: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + + if (status == 403 || body == "{\"message\": \"Authorization required.\"}") { + rtn = false; + } + + }) + .on_complete([&](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: url completed without error: %1%") % url; + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got credential verification: %2%") % name % body; + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: [auth] rtn=%2%") % name % rtn; // DEBUG + + try { + // Validate that response is correct + rtn = (boost::starts_with(body, "\"ok\"") || body == "{\"message\": \"ok\"}"); + } + catch (const std::exception &) { + rtn = false; + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: is_authorized Caught error") % name; + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .on_ip_resolve([&](std::string address) { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + // Remember resolved address to be reused at successive REST API call. + // msg = GUI::from_u8(address); + }) +#endif // WIN32 + .perform_sync(); + + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: [auth] rtn=%2%") % name % rtn; + return rtn; +} + + + + +std::string UltiMaker::auth_status() const{ + // returns 'authorized', 'unauthorized', or 'waiting', or 'ERROR: unknown value' on a different output + + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: auth_status() verifying credentials."); + + const char* name = get_name(); + + std::string rtn = "ERROR: unknown value"; + auto url = (boost::format("http://%1%/api/v1/auth/check/%2%") % host % m_api_username).str(); + + auto http = Http::get(std::move(url)); + set_auth(http); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error verifying credentials: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + + }) + .on_complete([&](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: url completed without error: %1%") % url; + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got credential verification: %2%") % name % body; + + try { + if (body == "{\"message\": \"unknown\"}") { rtn = "waiting";} + if (body == "{\"message\": \"authorized\"}" ) {rtn = "authorized";} + if (body == "{\"message\": \"unauthorized\"}") {rtn = "unauthorized";} + + } + catch (const std::exception &) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: auth_status Caught error") % name; + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .on_ip_resolve([&](std::string address) { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + // Remember resolved address to be reused at successive REST API call. + // msg = GUI::from_u8(address); + }) +#endif // WIN32 + .perform_sync(); + + if (boost::starts_with(rtn, "ERROR")) { BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: [auth][auth_status] Unknown value encountered in api/v1/auth/check") % name;} + return rtn; +} + + + +bool UltiMaker::generate_auth_creds(wxString &msg) const { + // Returns true if no error, false otherwise. + + BOOST_LOG_TRIVIAL(debug) << "UltiMaker: generate_auth_creds called!"; + bool ret = true; + /* + The auth POST request returns json of the form + { + "id": "string", + "key": "string" + } + where id is the m_api_username and key is the m_api_password + */ + const std::string name = get_name(); + std::string application = "OrcaSlicer"; + std::string application_key = "application"; + std::string user = "OrcaSlicer"; + std::string user_key = "user"; + std::string exclusionKey = "OrcaSlicer"; + std::string exclusionKey_key = "OrcaSlicer"; + auto url = (boost::format("http://%1%/api/v1/auth/request") % host).str(); + auto http = Http::post(std::move(url)); + + // Note: the body/form cannot be blank or OrcaSlicer's Http library crashes on perform_sync() + http.form_clear(); + http.mime_form_add_text(application_key,application); + http.mime_form_add_text(user_key,user); + http.mime_form_add_text(exclusionKey_key,exclusionKey); + + http.on_error([name, &msg, &ret](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error generating credentials: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + msg = "Error generating credentials! See the log for details."; + ret = false; + }) + .on_complete([name, &msg, &ret](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: body: %2%") % name % body; + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: POST request completed without error") % name; + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got credential verification: %2%") % name % body; + + std::stringstream sin(body); + boost::property_tree::ptree pt; + + try { + // Parse JSON into property tree + boost::property_tree::read_json(sin, pt); + // Extract the keys from the json + std::string id = pt.get("id", ""); + std::string key = pt.get("key", ""); + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: Got id and key. ID=%1%") % id; + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: KEY=%1%") % key; + + if (id.empty() || key.empty()) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Either ID or KEY is empty.") % name; + msg = GUI::from_u8("ERROR: Either ID or KEY is empty. ID=" + id + " KEY=" + key); + ret = false; + } else { + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: ID and KEY are both present. Setting message appropriately.") % name; + msg = GUI::from_u8("Username: " + id + " Password: " + key); + } + } + catch (const std::exception & e) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: generate_auth_creds Caught error") % name; + BOOST_LOG_TRIVIAL(error) << "Json parse err: " << e.what(); + ret = false; + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .on_ip_resolve([&](std::string address) { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + // Remember resolved address to be reused at successive REST API call. + // msg = GUI::from_u8(address); + }) +#endif // WIN32 + .perform_sync(); + + + // Wait for user to authorize on the physical machine + BOOST_LOG_TRIVIAL(debug) << "UltiMaker: generate_auth_creds done! ret=" << ret; + return ret; +} + + + +std::string UltiMaker::test_auth() const { + /* + Used in UltiMaker::test(), creates creds if they don't already + exist, and returns various error strings if errors. + Returns "OK" if no errors and creds are valid. + */ + const char* name = get_name(); + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Testing for presence of auth creds...") % name; + + // Auth credentials are alreay existing (user-entered) + if (has_auth_creds()) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Has auth credentials. Testing validity...") % name; + + std::string auth_stat = auth_status(); + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: auth_status returned %2%") % name % auth_stat; + + if (auth_stat == "waiting") { + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Auth credentials are waiting for user approval at the physical machine.") % name; + return "Authentication creds waiting on approval; Please approve access via the dialog box on the printer's screen."; + + } else if (auth_stat == "authorized" && is_authorized()) { + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Auth credentials are valid. Returning OK") % name; + return "OK"; + + } else { + BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Auth credentials are INVALID. auth_status returned %2%") % name % auth_stat; + return "Invalid authentication credentials"; + } + + } else { // Auth credentials do not exist + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Auth credentials do NOT already exist. Returning error string.") % name; + return "Error: No authentication credentials found!"; + } + +} + +#pragma endregion Auth + + +int UltiMaker::getPrintTime(std::string filepath) const { + // Get the total print time in seconds from the provided gcode filepath + std::ifstream file_in(filepath); + std::string line; + int ret = 0; + bool found = false; + int h = 0; + int m = 0; + int s = 0; + + if (file_in.is_open()){ + while (getline(file_in, line) && !found) { + if (boost::starts_with(line, "; estimated printing time (normal mode) =")) { + if (sscanf(line.c_str(), "; estimated printing time (normal mode) = %dh %dm %ds", &h, &m, &s) == 3) { + ret = 3600*h + 60*m + s; + BOOST_LOG_TRIVIAL(warning) << boost::format("UltiMaker: Hours=%1% Minutes=%2% Seconds=%3% Total Seconds=%4%") % h % m % s % ret; + found = true; + } else if (sscanf(line.c_str(), "; estimated printing time (normal mode) = %dm %ds", &m, &s) == 2) { + ret = 60*m + s; + BOOST_LOG_TRIVIAL(warning) << boost::format("UltiMaker: No Hours, Minutes=%1% Seconds=%2% Total Seconds=%3%") % m % s % ret; + found = true; + } else { + BOOST_LOG_TRIVIAL(warning) << boost::format("UltiMaker: ERROR: sscanf error while getting total print time, no vars assigned."); + } + } + } + } else { + BOOST_LOG_TRIVIAL(error) << boost::format("UltiMaker postprocessing ERROR: file %1% or file %2% was unable to open!") % filepath % (filepath+".temp"); + return 0; + } + + if (!found) { + BOOST_LOG_TRIVIAL(error) << boost::format("UltiMaker: Print time estimate not found!!"); + } + + file_in.close(); + + return ret; +} + + +bool UltiMaker::makeGriffinCompatible(std::string filepath) const { + // Modifies the header of the given file path to remove the double colon + // at the timestamp because >1 colons in a line makes UltiMakers crash. + + // Opening and modifying files code was modified from https://www.w3resource.com/cpp-exercises/file-handling/cpp-file-handling-exercise-6.php + std::string file_temp = filepath+".temp"; + std::ifstream file_in(filepath); + std::ofstream file_out(file_temp); + std::string line; + bool write_on = false; + + if (file_in.is_open() && file_out.is_open()){ + while (getline(file_in, line)) { + + if (write_on) { + if (boost::starts_with(line, "; generated by OrcaSlicer")) { + boost::replace_all(line, ":", "-"); // Replace the colons in the date with dashes to prevent printer crash. + } + + // UM machines need total print time in seconds. + if (boost::starts_with(line, ";PRINT.TIME:")) { + file_out << ";PRINT.TIME:"<< getPrintTime(filepath) << "\n"; + + } else { + file_out << line << "\n"; // Write the modified line to the output file + } + + } else if (boost::starts_with(line, ";START_OF_HEADER")) { + write_on = true; + file_out << line << "\n"; + } + } + } else { + BOOST_LOG_TRIVIAL(error) << boost::format("UltiMaker: file %1% or file %2% was unable to open!") % filepath % (filepath+".temp"); + return false; + } + + if (!write_on) { + BOOST_LOG_TRIVIAL(warning) << boost::format("UltiMaker: while postprocessing to make compatible with Griffin, headers not found!"); + } + + file_in.close(); + file_out.close(); + + // Now replace the upload file with the temp one + std::remove(filepath.c_str()); + std::rename(file_temp.c_str(), filepath.c_str()); + // boost::filesystem::copy_file(file_temp, filepath); // replacement of the above line to keep in /tmp/ + return true; +} + + +bool UltiMaker::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const +{ + // Preprocess the file so the machine doesn't crash + if (makeGriffinCompatible(upload_data.source_path.string())) { + BOOST_LOG_TRIVIAL(info) << "UltiMaker: [upload] Griffin compatibility appeared successful."; + } else { + BOOST_LOG_TRIVIAL(warning) << "UltiMaker: [upload] Griffin compatibility FAILED."; + return false; + } + + wxString connect_msg; + auto connectionType = connect(connect_msg); + if (connectionType == ConnectionType::error) { + BOOST_LOG_TRIVIAL(warning) << boost::format("UltiMaker: [upload] connectionType is of type error!"); + error_fn(std::move(connect_msg)); + return false; + } + + bool res = true; + bool dsf = (connectionType == ConnectionType::dsf); + + auto upload_cmd = get_upload_url(upload_data.upload_path.string(), connectionType); + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: Uploading file %1%, filepath: %2%, post_action: %3%, command: %4%") + % upload_data.source_path + % upload_data.upload_path + % int(upload_data.post_action) + % upload_cmd; + + auto http = (dsf ? Http::put(std::move(upload_cmd)) : Http::post(std::move(upload_cmd))); + http.form_add("jobname",upload_data.upload_path.string()); + http.form_add_file("file", upload_data.source_path, upload_data.upload_path.string()); + + set_auth(http); + http.on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: File uploaded: HTTP %1%: %2%") % status % body; + + int err_code = dsf ? (status == 201 ? 0 : 1) : get_err_code_from_body(body); + if (err_code != 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("UltiMaker: Request completed but error code was received: %1%") % err_code; + error_fn(format_error(body, L("Unknown error occurred"), 0)); + res = false; + } else if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) { + wxString errormsg; + res = start_print(errormsg, upload_data.upload_path.string(), connectionType); + if (! res) { + error_fn(std::move(errormsg)); + } + } + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("UltiMaker: Error uploading file: %1%, HTTP %2%, body: `%3%`") % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool &cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << "UltiMaker: Upload canceled"; + res = false; + } + }) + .perform_sync(); + + disconnect(connectionType); + + return res; +} + +UltiMaker::ConnectionType UltiMaker::connect(wxString &msg) const +{ + BOOST_LOG_TRIVIAL(info) << boost::format("UltiMaker: Attempting to connect"); + + auto res = ConnectionType::error; + auto url = get_connect_url(); + + auto http = Http::get(std::move(url)); + set_auth(http); + + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: Connecting to url %1%") % url; + + http.on_error([&](std::string body, std::string error, unsigned status) { + auto dsfUrl = get_connect_url(); + auto dsfHttp = Http::get(std::move(dsfUrl)); + dsfHttp.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("UltiMaker: Error connecting: %1%, HTTP %2%, body: `%3%`") % error % status % body; + msg = format_error(body, error, status); + }) + .on_complete([&](std::string body, unsigned) { + res = ConnectionType::dsf; + }) + .perform_sync(); + }) + .on_complete([&](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: Got: %1%") % body; + + int err_code = get_err_code_from_body(body); + switch (err_code) { + case 0: + res = ConnectionType::rrf; + break; + case 1: + msg = format_error(body, L("Wrong password"), 0); + break; + case 2: + msg = format_error(body, L("Could not get resources to create a new connection"), 0); + break; + default: + msg = format_error(body, L("Unknown error occurred"), 0); + break; + } + + }) + .perform_sync(); + + return res; +} + +void UltiMaker::disconnect(ConnectionType connectionType) const +{ + // we don't need to disconnect from DSF or if it failed anyway + if (connectionType != ConnectionType::rrf) { + return; + } + auto url = (boost::format("%1%rr_disconnect") + % get_base_url()).str(); + + auto http = Http::get(std::move(url)); + http.on_error([&](std::string body, std::string error, unsigned status) { + // we don't care about it, if disconnect is not working UltiMaker will disconnect automatically after some time + BOOST_LOG_TRIVIAL(error) << boost::format("UltiMaker: Error disconnecting: %1%, HTTP %2%, body: `%3%`") % error % status % body; + }) + .perform_sync(); +} + + +#pragma region Get Constants +std::string UltiMaker::get_upload_url(const std::string &filename, ConnectionType connectionType) const +{ + assert(connectionType != ConnectionType::error); + + if (connectionType == ConnectionType::dsf) { + return (boost::format("%1%/machine/file/gcodes/%2%") + % get_base_url() + % Http::url_encode(filename)).str(); + } else { + return get_base_url()+"/print_job"; + } +} + +std::string UltiMaker::get_status_url() const +{ + return (boost::format("%1%/printer/status") % get_base_url()).str(); +} + +std::string UltiMaker::get_connect_url() const +{ + return (boost::format("%1%/printer/status") % get_base_url()).str(); +} + +std::string UltiMaker::get_base_url() const +{ + if (host.find("http://") == 0 || host.find("https://") == 0) { + if (host.back() == '/') { + return host; + } else { + return (boost::format("%1%/api/v1") % host).str(); + } + } else { + return (boost::format("http://%1%/api/v1") % host).str(); + } +} + +std::string UltiMaker::timestamp_str() const +{ + enum { BUFFER_SIZE = 32 }; + + auto t = std::time(nullptr); + auto tm = *std::localtime(&t); + + char buffer[BUFFER_SIZE]; + std::strftime(buffer, BUFFER_SIZE, "time=%Y-%m-%dT%H:%M:%S", &tm); + + return std::string(buffer); +} + +#pragma endregion Get Constants + +bool UltiMaker::start_print(wxString &msg, const std::string &filename, ConnectionType connectionType) const +{ + assert(connectionType != ConnectionType::error); + + bool res = false; + bool dsf = (connectionType == ConnectionType::dsf); + + auto url = dsf + ? (boost::format("%1%machine/code") % get_base_url()).str() + : (boost::format("%1%rr_gcode?gcode=M32%%20\"0:/gcodes/%2%\"") % get_base_url() % Http::url_encode(filename)).str(); + + auto http = (dsf ? Http::post(std::move(url)) : Http::get(std::move(url))); + set_auth(http); + if (dsf) { + http.set_post_body( + (boost::format("M32 \"0:/gcodes/%1%\"") % filename).str() + ); + } + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("UltiMaker: Error starting print: %1%, HTTP %2%, body: `%3%`") % error % status % body; + msg = format_error(body, error, status); + }) + .on_complete([&](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("UltiMaker: Got: %1%") % body; + res = true; + }) + .perform_sync(); + + return res; +} + +int UltiMaker::get_err_code_from_body(const std::string &body) const +{ + pt::ptree root; + std::istringstream iss (body); // wrap returned json to istringstream + pt::read_json(iss, root); + + return root.get("err", 0); +} + +} diff --git a/src/slic3r/Utils/UltiMaker.hpp b/src/slic3r/Utils/UltiMaker.hpp new file mode 100644 index 0000000000..b32b87434f --- /dev/null +++ b/src/slic3r/Utils/UltiMaker.hpp @@ -0,0 +1,72 @@ +#ifndef slic3r_UltiMaker_hpp_ +#define slic3r_UltiMaker_hpp_ + +#include +#include + +#include "PrintHost.hpp" + +namespace Slic3r { + +class DynamicPrintConfig; +class Http; + +class UltiMaker : public PrintHost +{ +public: + explicit UltiMaker(DynamicPrintConfig *config); + ~UltiMaker() override = default; + + const char* get_name() const override; + + bool test(wxString &curl_msg) const override; + wxString get_test_ok_msg() const override; + wxString get_test_failed_msg(wxString &msg) const override; + + bool has_auth_creds() const; + bool is_authorized() const; + std::string auth_status() const; + bool generate_auth_creds(wxString &msg) const; + std::string test_auth() const; + + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; + bool has_auto_discovery() const override { return false; } + bool can_test() const override { return true; } + PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } + std::string get_host() const override { return host; } + const std::string& get_api_user() const { return m_api_username; } + const std::string& get_api_username() const { return m_api_username; } + const std::string& get_api_password() const { return m_api_password; } + + int getPrintTime(std::string filepath) const; + +protected: + std::string m_host; + + // username and password for HTTP Digest Authentization (RFC RFC2617) + std::string m_api_username; + std::string m_api_password; + + bool m_ssl_revoke_best_effort; + + bool makeGriffinCompatible(std::string filepath) const; + +private: + enum class ConnectionType { rrf, dsf, error }; + std::string host; + + std::string get_upload_url(const std::string &filename, ConnectionType connectionType) const; + std::string get_status_url() const; + std::string get_connect_url() const; + std::string get_base_url() const; + std::string timestamp_str() const; + ConnectionType connect(wxString &msg) const; + void set_auth(Http& http) const; + void disconnect(ConnectionType connectionType) const; + bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType) const; + int get_err_code_from_body(const std::string &body) const; +}; + +} + +#endif