diff --git a/src/libslic3r/Exception.hpp b/src/libslic3r/Exception.hpp
index 8ec9f20c8..2bef204ad 100644
--- a/src/libslic3r/Exception.hpp
+++ b/src/libslic3r/Exception.hpp
@@ -19,6 +19,7 @@ SLIC3R_DERIVE_EXCEPTION(InvalidArgument,    LogicError);
 SLIC3R_DERIVE_EXCEPTION(OutOfRange,         LogicError);
 SLIC3R_DERIVE_EXCEPTION(IOError,            CriticalException);
 SLIC3R_DERIVE_EXCEPTION(FileIOError,        IOError);
+SLIC3R_DERIVE_EXCEPTION(HostNetworkError,   IOError);
 // Runtime exception produced by Slicer. Such exception cancels the slicing process and it shall be shown in notifications.
 SLIC3R_DERIVE_EXCEPTION(SlicingError,       Exception);
 #undef SLIC3R_DERIVE_EXCEPTION
diff --git a/src/libslic3r/Format/SL1.cpp b/src/libslic3r/Format/SL1.cpp
index 274f84f00..c4a9f5864 100644
--- a/src/libslic3r/Format/SL1.cpp
+++ b/src/libslic3r/Format/SL1.cpp
@@ -383,6 +383,7 @@ void fill_slicerconf(ConfMap &m, const SLAPrint &print)
     static constexpr auto banned_keys = { 
 		"compatible_printers"sv,
         "compatible_prints"sv,
+        //FIXME The print host keys should not be exported to full_print_config anymore. The following keys may likely be removed.
         "print_host"sv,
         "printhost_apikey"sv,
         "printhost_cafile"sv
diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp
index 7de07cd73..c5b363bd3 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -1619,7 +1619,7 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu
     // Append full config.
     _write(file, "\n");
     {
-        std::string full_config = "";
+        std::string full_config;
         append_full_config(print, full_config);
         if (!full_config.empty())
             _write(file, full_config);
@@ -2475,6 +2475,7 @@ void GCode::append_full_config(const Print &print, std::string &str)
     static constexpr auto banned_keys = {
         "compatible_printers"sv,
         "compatible_prints"sv,
+        //FIXME The print host keys should not be exported to full_print_config anymore. The following keys may likely be removed.
         "print_host"sv,
         "printhost_apikey"sv,
         "printhost_cafile"sv
diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp
index 421a1f39e..1982a91fb 100644
--- a/src/libslic3r/Preset.cpp
+++ b/src/libslic3r/Preset.cpp
@@ -475,8 +475,9 @@ const std::vector<std::string>& Preset::printer_options()
     if (s_opts.empty()) {
         s_opts = {
             "printer_technology",
-            "bed_shape", "bed_custom_texture", "bed_custom_model", "z_offset", "gcode_flavor", "use_relative_e_distances", "serial_port", "serial_speed",
+            "bed_shape", "bed_custom_texture", "bed_custom_model", "z_offset", "gcode_flavor", "use_relative_e_distances",
             "use_firmware_retraction", "use_volumetric_e", "variable_layer_height",
+            //FIXME the print host keys are left here just for conversion from the Printer preset to Physical Printer preset.
             "host_type", "print_host", "printhost_apikey", "printhost_cafile",
             "single_extruder_multi_material", "start_gcode", "end_gcode", "before_layer_gcode", "layer_gcode", "toolchange_gcode",
             "color_change_gcode", "pause_print_gcode", "template_custom_gcode",
@@ -595,6 +596,7 @@ const std::vector<std::string>& Preset::sla_printer_options()
             "gamma_correction",
             "min_exposure_time", "max_exposure_time",
             "min_initial_exposure_time", "max_initial_exposure_time",
+            //FIXME the print host keys are left here just for conversion from the Printer preset to Physical Printer preset.
             "print_host", "printhost_apikey", "printhost_cafile",
             "printer_notes",
             "inherits"
@@ -711,38 +713,6 @@ Preset& PresetCollection::load_preset(const std::string &path, const std::string
     return this->load_preset(path, name, std::move(cfg), select);
 }
 
-enum class ProfileHostParams
-{
-	Same,
-	Different,
-	Anonymized,
-};
-
-static ProfileHostParams profile_host_params_same_or_anonymized(const DynamicPrintConfig &cfg_old, const DynamicPrintConfig &cfg_new)
-{
-	auto opt_print_host_old 	  = cfg_old.option<ConfigOptionString>("print_host");
-	auto opt_printhost_apikey_old = cfg_old.option<ConfigOptionString>("printhost_apikey");
-	auto opt_printhost_cafile_old = cfg_old.option<ConfigOptionString>("printhost_cafile");
-
-	auto opt_print_host_new 	  = cfg_new.option<ConfigOptionString>("print_host");
-	auto opt_printhost_apikey_new = cfg_new.option<ConfigOptionString>("printhost_apikey");
-	auto opt_printhost_cafile_new = cfg_new.option<ConfigOptionString>("printhost_cafile");
-
-	// If the new print host data is undefined, use the old data.
-	bool new_print_host_undefined = (opt_print_host_new 		== nullptr || opt_print_host_new		->empty()) &&
-									(opt_printhost_apikey_new 	== nullptr || opt_printhost_apikey_new	->empty()) &&
-									(opt_printhost_cafile_new 	== nullptr || opt_printhost_cafile_new	->empty());
-	if (new_print_host_undefined)
-		return ProfileHostParams::Anonymized;
-
-	auto opt_same = [](const ConfigOptionString *l, const ConfigOptionString *r) {
-		return ((l == nullptr || l->empty()) && (r == nullptr || r->empty())) ||
-			   (l != nullptr && r != nullptr && l->value == r->value);
-	};
-	return (opt_same(opt_print_host_old, opt_print_host_new) && opt_same(opt_printhost_apikey_old, opt_printhost_apikey_new) && 
-		    opt_same(opt_printhost_cafile_old, opt_printhost_cafile_new)) ? ProfileHostParams::Same : ProfileHostParams::Different;
-}
-
 static bool profile_print_params_same(const DynamicPrintConfig &cfg_old, const DynamicPrintConfig &cfg_new)
 {
     t_config_option_keys diff = cfg_old.diff(cfg_new);
@@ -752,10 +722,11 @@ static bool profile_print_params_same(const DynamicPrintConfig &cfg_old, const D
                              "compatible_printers", "compatible_printers_condition", "inherits",
                              "print_settings_id", "filament_settings_id", "sla_print_settings_id", "sla_material_settings_id", "printer_settings_id",
                              "printer_model", "printer_variant", "default_print_profile", "default_filament_profile", "default_sla_print_profile", "default_sla_material_profile",
+                             //FIXME remove the print host keys?
                              "print_host", "printhost_apikey", "printhost_cafile" })
         diff.erase(std::remove(diff.begin(), diff.end(), key), diff.end());
     // Preset with the same name as stored inside the config exists.
-    return diff.empty() && profile_host_params_same_or_anonymized(cfg_old, cfg_new) != ProfileHostParams::Different;
+    return diff.empty();
 }
 
 // Load a preset from an already parsed config file, insert it into the sorted sequence of presets
@@ -784,25 +755,11 @@ Preset& PresetCollection::load_external_preset(
 		it = this->find_preset_renamed(original_name);
 		found = it != m_presets.end();
     }
-    if (found) {
-    	if (profile_print_params_same(it->config, cfg)) {
-	        // The preset exists and it matches the values stored inside config.
-	        if (select)
-	            this->select_preset(it - m_presets.begin());
-	        return *it;
-	    }
-	    if (profile_host_params_same_or_anonymized(it->config, cfg) == ProfileHostParams::Anonymized) {
-	    	// The project being loaded is anonymized. Replace the empty host keys of the loaded profile with the data from the original profile.
-	    	// See "Octoprint Settings when Opening a .3MF file" GH issue #3244
-	    	auto opt_update = [it, &cfg](const std::string &opt_key) {
-				auto opt = it->config.option<ConfigOptionString>(opt_key);
-				if (opt != nullptr)
-					cfg.set_key_value(opt_key, opt->clone());
-	    	};
-	    	opt_update("print_host");
-	    	opt_update("printhost_apikey");
-	    	opt_update("printhost_cafile");
-	    }
+    if (found && profile_print_params_same(it->config, cfg)) {
+        // The preset exists and it matches the values stored inside config.
+        if (select)
+            this->select_preset(it - m_presets.begin());
+        return *it;
     }
     // Update the "inherits" field.
     std::string &inherits = Preset::inherits(cfg);
@@ -1374,31 +1331,25 @@ const std::vector<std::string>& PhysicalPrinter::printer_options()
             "preset_name",
             "printer_technology",
 //            "printer_model",
-            "host_type", 
-            "print_host", 
-            "printhost_apikey", 
+            "host_type",
+            "print_host",
+            "printhost_apikey",
             "printhost_cafile",
+            "printhost_port",
             "printhost_authorization_type",
             // HTTP digest authentization (RFC 2617)
-            "printhost_user", 
+            "printhost_user",
             "printhost_password"
         };
     }
     return s_opts;
 }
 
-const std::vector<std::string>& PhysicalPrinter::print_host_options()
-{
-    static std::vector<std::string> s_opts;
-    if (s_opts.empty()) {
-        s_opts = {
-            "print_host",
-            "printhost_apikey",
-            "printhost_cafile"
-        };
-    }
-    return s_opts;
-}
+static constexpr auto legacy_print_host_options = {
+    "print_host",
+    "printhost_apikey",
+    "printhost_cafile",
+};
 
 std::vector<std::string> PhysicalPrinter::presets_with_print_host_information(const PrinterPresetCollection& printer_presets)
 {
@@ -1412,7 +1363,7 @@ std::vector<std::string> PhysicalPrinter::presets_with_print_host_information(co
 
 bool PhysicalPrinter::has_print_host_information(const DynamicPrintConfig& config)
 {
-    for (const std::string& opt : print_host_options())
+    for (const char *opt : legacy_print_host_options)
         if (!config.opt_string(opt).empty())
             return true;
 
@@ -1429,6 +1380,7 @@ bool PhysicalPrinter::has_empty_config() const
     return  config.opt_string("print_host"        ).empty() && 
             config.opt_string("printhost_apikey"  ).empty() && 
             config.opt_string("printhost_cafile"  ).empty() && 
+            config.opt_string("printhost_port"    ).empty() &&
             config.opt_string("printhost_user"    ).empty() && 
             config.opt_string("printhost_password").empty();
 }
@@ -1614,9 +1566,7 @@ void PhysicalPrinterCollection::load_printers_from_presets(PrinterPresetCollecti
     int cnt=0;
     for (Preset& preset: printer_presets) {
         DynamicPrintConfig& config = preset.config;
-        const std::vector<std::string>& options = PhysicalPrinter::print_host_options();
-
-        for(const std::string& option : options) {
+        for(const char* option : legacy_print_host_options) {
             if (!config.opt_string(option).empty()) {
                 // check if printer with those "Print Host upload" options already exist
                 PhysicalPrinter* existed_printer = find_printer_with_same_config(config);
@@ -1635,7 +1585,7 @@ void PhysicalPrinterCollection::load_printers_from_presets(PrinterPresetCollecti
                 }
 
                 // erase "Print Host upload" information from the preset
-                for (const std::string& opt : options)
+                for (const char *opt : legacy_print_host_options)
                     config.opt_string(opt).clear();
                 // save changes for preset
                 preset.save();
@@ -1643,7 +1593,7 @@ void PhysicalPrinterCollection::load_printers_from_presets(PrinterPresetCollecti
                 // update those changes for edited preset if it's equal to the preset
                 Preset& edited = printer_presets.get_edited_preset();
                 if (preset.name == edited.name) {
-                    for (const std::string& opt : options)
+                    for (const char *opt : legacy_print_host_options)
                         edited.config.opt_string(opt).clear();
                 }
 
@@ -1692,7 +1642,7 @@ PhysicalPrinter* PhysicalPrinterCollection::find_printer_with_same_config(const
 {
     for (const PhysicalPrinter& printer :*this) {
         bool is_equal = true;
-        for (const std::string& opt : PhysicalPrinter::print_host_options())
+        for (const char *opt : legacy_print_host_options)
             if (is_equal && printer.config.opt_string(opt) != config.opt_string(opt))
                 is_equal = false;
                 
diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp
index b1166d2a4..44df000c3 100644
--- a/src/libslic3r/PresetBundle.cpp
+++ b/src/libslic3r/PresetBundle.cpp
@@ -37,11 +37,11 @@ static std::vector<std::string> s_project_options {
 const char *PresetBundle::PRUSA_BUNDLE = "PrusaResearch";
 
 PresetBundle::PresetBundle() :
-    prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast<const HostConfig&>(FullPrintConfig::defaults())), 
-    filaments(Preset::TYPE_FILAMENT, Preset::filament_options(), static_cast<const HostConfig&>(FullPrintConfig::defaults())), 
+    prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast<const PrintRegionConfig&>(FullPrintConfig::defaults())),
+    filaments(Preset::TYPE_FILAMENT, Preset::filament_options(), static_cast<const PrintRegionConfig&>(FullPrintConfig::defaults())),
     sla_materials(Preset::TYPE_SLA_MATERIAL, Preset::sla_material_options(), static_cast<const SLAMaterialConfig&>(SLAFullPrintConfig::defaults())), 
     sla_prints(Preset::TYPE_SLA_PRINT, Preset::sla_print_options(), static_cast<const SLAPrintObjectConfig&>(SLAFullPrintConfig::defaults())),
-    printers(Preset::TYPE_PRINTER, Preset::printer_options(), static_cast<const HostConfig&>(FullPrintConfig::defaults()), "- default FFF -"),
+    printers(Preset::TYPE_PRINTER, Preset::printer_options(), static_cast<const PrintRegionConfig&>(FullPrintConfig::defaults()), "- default FFF -"),
     physical_printers(PhysicalPrinter::printer_options())
 {
     // The following keys are handled by the UI, they do not have a counterpart in any StaticPrintConfig derived classes,
@@ -77,11 +77,12 @@ PresetBundle::PresetBundle() :
     for (size_t i = 0; i < 2; ++ i) {
 		// The following ugly switch is to avoid printers.preset(0) to return the edited instance, as the 0th default is the current one.
 		Preset &preset = this->printers.default_preset(i);
-        preset.config.optptr("printer_settings_id", true);
-        preset.config.optptr("printer_vendor", true);
-        preset.config.optptr("printer_model", true);
-        preset.config.optptr("printer_variant", true);
-		preset.config.optptr("thumbnails", true);
+        for (const char *key : { 
+            "printer_settings_id", "printer_vendor", "printer_model", "printer_variant", "thumbnails",
+            //FIXME the following keys are only created here for compatibility to be able to parse legacy Printer profiles.
+            // These keys are converted to Physical Printer profile. After the conversion, they shall be removed.
+            "host_type", "print_host", "printhost_apikey", "printhost_cafile"})
+            preset.config.optptr(key, true);
         if (i == 0) {
             preset.config.optptr("default_print_profile", true);
             preset.config.option<ConfigOptionStrings>("default_filament_profile", true)->values = { "" };
@@ -495,6 +496,7 @@ DynamicPrintConfig PresetBundle::full_config() const
 DynamicPrintConfig PresetBundle::full_config_secure() const
 {
     DynamicPrintConfig config = this->full_config();
+    //FIXME legacy, the keys should not be there after conversion to a Physical Printer profile.
     config.erase("print_host");
     config.erase("printhost_apikey");
     config.erase("printhost_cafile");
diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp
index 095a1b390..ee4a0945e 100644
--- a/src/libslic3r/PrintConfig.cpp
+++ b/src/libslic3r/PrintConfig.cpp
@@ -108,7 +108,14 @@ void PrintConfigDef::init_common_params()
                    "the API Key or the password required for authentication.");
     def->mode = comAdvanced;
     def->set_default_value(new ConfigOptionString(""));
-
+    
+    def = this->add("printhost_port", coString);
+    def->label = L("Printer");
+    def->tooltip = L("Name of the printer");
+    def->gui_type = "select_open";
+    def->mode = comAdvanced;
+    def->set_default_value(new ConfigOptionString(""));
+    
     def = this->add("printhost_cafile", coString);
     def->label = L("HTTPS CA File");
     def->tooltip = L("Custom CA certificate file can be specified for HTTPS OctoPrint connections, in crt/pem format. "
@@ -1447,10 +1454,12 @@ void PrintConfigDef::init_fff_params()
     def->enum_values.push_back("duet");
     def->enum_values.push_back("flashair");
     def->enum_values.push_back("astrobox");
+    def->enum_values.push_back("repetier");
     def->enum_labels.push_back("OctoPrint");
     def->enum_labels.push_back("Duet");
     def->enum_labels.push_back("FlashAir");
     def->enum_labels.push_back("AstroBox");
+    def->enum_labels.push_back("Repetier");
     def->mode = comAdvanced;
     def->set_default_value(new ConfigOptionEnum<PrintHostType>(htOctoPrint));
 
@@ -1765,26 +1774,6 @@ void PrintConfigDef::init_fff_params()
     def->set_default_value(new ConfigOptionFloat(30));
 #endif
 
-    def = this->add("serial_port", coString);
-    def->gui_type = "select_open";
-    def->label = "";
-    def->full_label = L("Serial port");
-    def->tooltip = L("USB/serial port for printer connection.");
-    def->width = 20;
-    def->set_default_value(new ConfigOptionString(""));
-
-    def = this->add("serial_speed", coInt);
-    def->gui_type = "i_enum_open";
-    def->label = L("Speed");
-    def->full_label = L("Serial port speed");
-    def->tooltip = L("Speed (baud) of USB/serial port for printer connection.");
-    def->min = 1;
-    def->max = 300000;
-    def->enum_values.push_back("115200");
-    def->enum_values.push_back("250000");
-    def->mode = comAdvanced;
-    def->set_default_value(new ConfigOptionInt(250000));
-
     def = this->add("skirt_distance", coFloat);
     def->label = L("Distance from object");
     def->tooltip = L("Distance between skirt and object(s). Set this to zero to attach the skirt "
@@ -3176,8 +3165,9 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
         "seal_position", "vibration_limit", "bed_size",
         "print_center", "g0", "threads", "pressure_advance", "wipe_tower_per_color_wipe"
 #ifndef HAS_PRESSURE_EQUALIZER
-        , "max_volumetric_extrusion_rate_slope_positive", "max_volumetric_extrusion_rate_slope_negative"
+        , "max_volumetric_extrusion_rate_slope_positive", "max_volumetric_extrusion_rate_slope_negative",
 #endif /* HAS_PRESSURE_EQUALIZER */
+        "serial_port", "serial_speed"
     };
 
     // In PrusaSlicer 2.3.0-alpha0 the "monotonous" infill was introduced, which was later renamed to "monotonic".
@@ -3498,7 +3488,6 @@ StaticPrintConfig::StaticCache<class Slic3r::PrintRegionConfig> PrintRegionConfi
 StaticPrintConfig::StaticCache<class Slic3r::MachineEnvelopeConfig> MachineEnvelopeConfig::s_cache_MachineEnvelopeConfig;
 StaticPrintConfig::StaticCache<class Slic3r::GCodeConfig>       GCodeConfig::s_cache_GCodeConfig;
 StaticPrintConfig::StaticCache<class Slic3r::PrintConfig>       PrintConfig::s_cache_PrintConfig;
-StaticPrintConfig::StaticCache<class Slic3r::HostConfig>        HostConfig::s_cache_HostConfig;
 StaticPrintConfig::StaticCache<class Slic3r::FullPrintConfig>   FullPrintConfig::s_cache_FullPrintConfig;
 
 StaticPrintConfig::StaticCache<class Slic3r::SLAMaterialConfig>     SLAMaterialConfig::s_cache_SLAMaterialConfig;
diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp
index 99b5feb95..89c9c7a97 100644
--- a/src/libslic3r/PrintConfig.hpp
+++ b/src/libslic3r/PrintConfig.hpp
@@ -11,7 +11,6 @@
 //    PrintRegionConfig
 //    PrintConfig
 //        GCodeConfig
-//    HostConfig
 //
 
 #ifndef slic3r_PrintConfig_hpp_
@@ -37,7 +36,7 @@ enum class MachineLimitsUsage {
 };
 
 enum PrintHostType {
-    htOctoPrint, htDuet, htFlashAir, htAstroBox
+    htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier
 };
 
 enum AuthorizationType {
@@ -127,6 +126,7 @@ template<> inline const t_config_enum_values& ConfigOptionEnum<PrintHostType>::g
         keys_map["duet"]            = htDuet;
         keys_map["flashair"]        = htFlashAir;
         keys_map["astrobox"]        = htAstroBox;
+        keys_map["repetier"]        = htRepetier;
     }
     return keys_map;
 }
@@ -962,38 +962,14 @@ protected:
     }
 };
 
-class HostConfig : public StaticPrintConfig
-{
-    STATIC_PRINT_CONFIG_CACHE(HostConfig)
-public:
-    ConfigOptionEnum<PrintHostType> host_type;
-    ConfigOptionString              print_host;
-    ConfigOptionString              printhost_apikey;
-    ConfigOptionString              printhost_cafile;
-    ConfigOptionString              serial_port;
-    ConfigOptionInt                 serial_speed;
-
-protected:
-    void initialize(StaticCacheBase &cache, const char *base_ptr)
-    {
-        OPT_PTR(host_type);
-        OPT_PTR(print_host);
-        OPT_PTR(printhost_apikey);
-        OPT_PTR(printhost_cafile);
-        OPT_PTR(serial_port);
-        OPT_PTR(serial_speed);
-    }
-};
-
 // This object is mapped to Perl as Slic3r::Config::Full.
 class FullPrintConfig :
     public PrintObjectConfig,
     public PrintRegionConfig,
-    public PrintConfig,
-    public HostConfig
+    public PrintConfig
 {
     STATIC_PRINT_CONFIG_CACHE_DERIVED(FullPrintConfig)
-    FullPrintConfig() : PrintObjectConfig(0), PrintRegionConfig(0), PrintConfig(0), HostConfig(0) { initialize_cache(); *this = s_cache_FullPrintConfig.defaults(); }
+    FullPrintConfig() : PrintObjectConfig(0), PrintRegionConfig(0), PrintConfig(0) { initialize_cache(); *this = s_cache_FullPrintConfig.defaults(); }
 
 public:
     // Validate the FullPrintConfig. Returns an empty string on success, otherwise an error message is returned.
@@ -1001,13 +977,12 @@ public:
 
 protected:
     // Protected constructor to be called to initialize ConfigCache::m_default.
-    FullPrintConfig(int) : PrintObjectConfig(0), PrintRegionConfig(0), PrintConfig(0), HostConfig(0) {}
+    FullPrintConfig(int) : PrintObjectConfig(0), PrintRegionConfig(0), PrintConfig(0) {}
     void initialize(StaticCacheBase &cache, const char *base_ptr)
     {
         this->PrintObjectConfig::initialize(cache, base_ptr);
         this->PrintRegionConfig::initialize(cache, base_ptr);
         this->PrintConfig      ::initialize(cache, base_ptr);
-        this->HostConfig       ::initialize(cache, base_ptr);
     }
 };
 
diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index 3cb8dbfd9..105d02613 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -193,6 +193,8 @@ set(SLIC3R_GUI_SOURCES
     Utils/FlashAir.hpp
     Utils/AstroBox.cpp
     Utils/AstroBox.hpp
+    Utils/Repetier.cpp
+    Utils/Repetier.hpp
     Utils/PrintHost.cpp
     Utils/PrintHost.hpp
     Utils/Bonjour.cpp
diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp
index 95fd13577..0bb3f0068 100644
--- a/src/slic3r/GUI/Field.cpp
+++ b/src/slic3r/GUI/Field.cpp
@@ -1048,13 +1048,33 @@ void Choice::set_values(const std::vector<std::string>& values)
 	auto value = ww->GetValue();
 	ww->Clear();
 	ww->Append("");
-	for (auto el : values)
+	for (const auto &el : values)
 		ww->Append(wxString(el));
 	ww->SetValue(value);
 
 	m_disable_change_event = false;
 }
 
+void Choice::set_values(const wxArrayString &values)
+{
+	if (values.empty())
+		return;
+
+	m_disable_change_event = true;
+
+	// 	# it looks that Clear() also clears the text field in recent wxWidgets versions,
+	// 	# but we want to preserve it
+	auto ww = dynamic_cast<wxBitmapComboBox*>(window);
+	auto value = ww->GetValue();
+	ww->Clear();
+	ww->Append("");
+	for (const auto &el : values)
+		ww->Append(el);
+	ww->SetValue(value);
+
+	m_disable_change_event = false;
+}
+
 boost::any& Choice::get_value()
 {
     wxBitmapComboBox* field = dynamic_cast<wxBitmapComboBox*>(window);
diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp
index d919304a9..aa047d030 100644
--- a/src/slic3r/GUI/Field.hpp
+++ b/src/slic3r/GUI/Field.hpp
@@ -405,6 +405,7 @@ public:
 	void			set_value(const std::string& value, bool change_event = false);
 	void			set_value(const boost::any& value, bool change_event = false);
 	void			set_values(const std::vector<std::string> &values);
+	void			set_values(const wxArrayString &values);
 	boost::any&		get_value() override;
 
     void            msw_rescale(bool rescale_sidetext = false) override;
diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp
index 2d1f87a2f..27d288a8e 100644
--- a/src/slic3r/GUI/MainFrame.cpp
+++ b/src/slic3r/GUI/MainFrame.cpp
@@ -750,14 +750,11 @@ bool MainFrame::can_export_gcode() const
 
 bool MainFrame::can_send_gcode() const
 {
-    if (m_plater == nullptr)
-        return false;
-
-    if (m_plater->model().objects.empty())
-        return false;
-
-    const auto print_host_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionString>("print_host");
-    return print_host_opt != nullptr && !print_host_opt->value.empty();
+    if (m_plater && ! m_plater->model().objects.empty())
+        if (const DynamicPrintConfig *cfg = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); cfg)
+            if (const auto *print_host_opt = cfg->option<ConfigOptionString>("print_host"); print_host_opt)
+                return ! print_host_opt->value.empty();
+    return false;
 }
 
 bool MainFrame::can_export_gcode_sd() const
diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp
index 7456ee800..5a440b4f2 100644
--- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp
+++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp
@@ -32,10 +32,6 @@
 #include "BitmapCache.hpp"
 #include "BonjourDialog.hpp"
 
-using Slic3r::GUI::format_wxstr;
-
-//static const std::pair<unsigned int, unsigned int> THUMBNAIL_SIZE_3MF = { 256, 256 };
-
 namespace Slic3r {
 namespace GUI {
 
@@ -248,10 +244,30 @@ PhysicalPrinterDialog::~PhysicalPrinterDialog()
     }
 }
 
+void PhysicalPrinterDialog::update_printers()
+{
+    wxBusyCursor wait;
+
+    std::unique_ptr<PrintHost> host(PrintHost::get_print_host(m_config));
+
+    wxArrayString printers;
+    Field *rs = m_optgroup->get_field("printhost_port");
+    try {
+        if (! host->get_printers(printers))
+            printers.clear();
+    } catch (const HostNetworkError &err) {
+        printers.clear();
+        show_error(this, _L("Querying printers connected to a print host failed.") + "\n\n" + from_u8(err.what()));
+    }
+    Choice *choice = dynamic_cast<Choice*>(rs);
+    choice->set_values(printers);
+    printers.empty() ? rs->disable() : rs->enable();
+}
+
 void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgroup)
 {
     m_optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
-        if (opt_key == "printhost_authorization_type")
+        if (opt_key == "host_type" || opt_key == "printhost_authorization_type")
             this->update();
     };
 
@@ -291,17 +307,30 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
                 return;
             }
             wxString msg;
-            if (host->test(msg)) {
+            bool result;
+            {
+                // Show a wait cursor during the connection test, as it is blocking UI.
+                wxBusyCursor wait;
+                result = host->test(msg);
+            }
+            if (result)
                 show_info(this, host->get_test_ok_msg(), _L("Success!"));
-            }
-            else {
+            else
                 show_error(this, host->get_test_failed_msg(msg));
-            }
             });
 
         return sizer;
     };
 
+    auto print_host_printers = [this, create_sizer_with_btn](wxWindow* parent) {
+        //add_scaled_button(parent, &m_printhost_port_browse_btn, "browse", _(L("Refresh Printers")), wxBU_LEFT | wxBU_EXACTFIT);
+        auto sizer = create_sizer_with_btn(parent, &m_printhost_port_browse_btn, "browse", _(L("Refresh Printers")));
+        ScalableButton* btn = m_printhost_port_browse_btn;
+        btn->SetFont(Slic3r::GUI::wxGetApp().normal_font());
+        btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent e) { update_printers(); });
+        return sizer;
+    };
+
     // Set a wider width for a better alignment
     Option option = m_optgroup->get_option("print_host");
     option.opt.width = Field::def_width_wider();
@@ -316,6 +345,12 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
     option.opt.width = Field::def_width_wider();
     m_optgroup->append_single_option_line(option);
 
+    option = m_optgroup->get_option("printhost_port");
+    option.opt.width = Field::def_width_wider();
+    Line port_line = m_optgroup->create_single_option_line(option);
+    port_line.append_widget(print_host_printers);
+    m_optgroup->append_line(port_line);
+
     const auto ca_file_hint = _u8L("HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate.");
 
     if (Http::ca_file_supported()) {
@@ -377,6 +412,14 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
     }
 
     m_optgroup->activate();
+
+    // Always fill in the "printhost_port" combo box from the config and select it.
+    {
+        Choice* choice = dynamic_cast<Choice*>(m_optgroup->get_field("printhost_port"));
+        choice->set_values({ m_config->opt_string("printhost_port") });
+        choice->set_selection();
+    }
+
     update();
 }
 
@@ -386,11 +429,14 @@ void PhysicalPrinterDialog::update()
 
     const PrinterTechnology tech = Preset::printer_technology(*m_config);
     // Only offer the host type selection for FFF, for SLA it's always the SL1 printer (at the moment)
+    bool supports_multiple_printers = false;
     if (tech == ptFFF) {
         m_optgroup->show_field("host_type");
         m_optgroup->hide_field("printhost_authorization_type");
         for (const std::string& opt_key : std::vector<std::string>{ "printhost_user", "printhost_password" })
             m_optgroup->hide_field(opt_key);
+        const auto opt = m_config->option<ConfigOptionEnum<PrintHostType>>("host_type");
+        supports_multiple_printers = opt && opt->value == htRepetier;
     }
     else {
         m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false);
@@ -401,10 +447,12 @@ void PhysicalPrinterDialog::update()
         AuthorizationType auth_type = m_config->option<ConfigOptionEnum<AuthorizationType>>("printhost_authorization_type")->value;
         m_optgroup->show_field("printhost_apikey", auth_type == AuthorizationType::atKeyPassword);
 
-        for (const std::string& opt_key : std::vector<std::string>{ "printhost_user", "printhost_password" })
+        for (const char *opt_key : { "printhost_user", "printhost_password" })
             m_optgroup->show_field(opt_key, auth_type == AuthorizationType::atUserPassword);
     }
 
+    m_optgroup->show_field("printhost_port", supports_multiple_printers);
+    m_printhost_port_browse_btn->Show(supports_multiple_printers);    
     this->Layout();
 }
 
diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp
index 3d0cf2d9f..6d089415f 100644
--- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp
+++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp
@@ -73,6 +73,7 @@ class PhysicalPrinterDialog : public DPIDialog
     ScalableButton*     m_printhost_browse_btn          {nullptr};
     ScalableButton*     m_printhost_test_btn            {nullptr};
     ScalableButton*     m_printhost_cafile_browse_btn   {nullptr};
+    ScalableButton*     m_printhost_port_browse_btn     {nullptr};
 
     wxBoxSizer*         m_presets_sizer                 {nullptr};
 
@@ -85,6 +86,7 @@ public:
     ~PhysicalPrinterDialog();
 
     void        update();
+    void        update_printers();
     wxString    get_printer_name();
     void        update_full_printer_names();
     PhysicalPrinter*    get_printer() {return &m_printer; }
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 24f8e5ed6..6ed3e1c1e 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -1819,8 +1819,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
     , main_frame(main_frame)
     , config(Slic3r::DynamicPrintConfig::new_from_defaults_keys({
         "bed_shape", "bed_custom_texture", "bed_custom_model", "complete_objects", "duplicate_distance", "extruder_clearance_radius", "skirts", "skirt_distance",
-        "brim_width", "variable_layer_height", "serial_port", "serial_speed", "host_type", "print_host",
-        "printhost_apikey", "printhost_cafile", "nozzle_diameter", "single_extruder_multi_material",
+        "brim_width", "variable_layer_height", "nozzle_diameter", "single_extruder_multi_material",
         "wipe_tower", "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_rotation_angle",
         "extruder_colour", "filament_colour", "max_print_height", "printer_model", "printer_technology",
         // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor.
@@ -4292,11 +4291,8 @@ void Plater::priv::show_action_buttons(const bool ready_to_slice) const
     wxWindowUpdateLocker noUpdater(sidebar);
 
     DynamicPrintConfig* selected_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config();
-    if (!selected_printer_config)
-        selected_printer_config = config;
-
-    const auto prin_host_opt = selected_printer_config->option<ConfigOptionString>("print_host");
-    const bool send_gcode_shown = prin_host_opt != nullptr && !prin_host_opt->value.empty();
+    const auto print_host_opt = selected_printer_config ? selected_printer_config->option<ConfigOptionString>("print_host") : nullptr;
+    const bool send_gcode_shown = print_host_opt != nullptr && !print_host_opt->value.empty();
     
     // when a background processing is ON, export_btn and/or send_btn are showing
     if (wxGetApp().app_config->get("background_processing") == "1")
@@ -5304,12 +5300,14 @@ void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &
 
 void Plater::send_gcode()
 {
-    if (p->model.objects.empty()) { return; }
-
     // if physical_printer is selected, send gcode for this printer
     DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config();
-    PrintHostJob upload_job(physical_printer_config ? physical_printer_config : p->config);
-    if (upload_job.empty()) { return; }
+    if (! physical_printer_config || p->model.objects.empty())
+        return;
+
+    PrintHostJob upload_job(physical_printer_config);
+    if (upload_job.empty())
+        return;
 
     // Obtain default output path
     fs::path default_output_file;
@@ -5327,11 +5325,18 @@ void Plater::send_gcode()
     }
     default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string()));
 
-    PrintHostSendDialog dlg(default_output_file, upload_job.printhost->can_start_print());
+    // Repetier specific: Query the server for the list of file groups.
+    wxArrayString groups;
+    {
+        wxBusyCursor wait;
+        upload_job.printhost->get_groups(groups);
+    }
+    
+    PrintHostSendDialog dlg(default_output_file, upload_job.printhost->can_start_print(), groups);
     if (dlg.ShowModal() == wxID_OK) {
         upload_job.upload_data.upload_path = dlg.filename();
         upload_job.upload_data.start_print = dlg.start_print();
-
+        upload_job.upload_data.group       = dlg.group();
         p->export_gcode(fs::path(), false, std::move(upload_job));
     }
 }
diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp
index 96299c381..dcf7115ca 100644
--- a/src/slic3r/GUI/PrintHostDialogs.cpp
+++ b/src/slic3r/GUI/PrintHostDialogs.cpp
@@ -28,18 +28,20 @@ namespace GUI {
 
 static const char *CONFIG_KEY_PATH  = "printhost_path";
 static const char *CONFIG_KEY_PRINT = "printhost_print";
+static const char *CONFIG_KEY_GROUP = "printhost_group";
 
-PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, bool can_start_print)
-    : MsgDialog(nullptr, _(L("Send G-Code to printer host")), _(L("Upload to Printer Host with the following filename:")), wxID_NONE)
+PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, bool can_start_print, const wxArrayString &groups)
+    : MsgDialog(nullptr, _L("Send G-Code to printer host"), _L("Upload to Printer Host with the following filename:"), wxID_NONE)
     , txt_filename(new wxTextCtrl(this, wxID_ANY))
-    , box_print(can_start_print ? new wxCheckBox(this, wxID_ANY, _(L("Start printing after upload"))) : nullptr)
+    , box_print(can_start_print ? new wxCheckBox(this, wxID_ANY, _L("Start printing after upload")) : nullptr)
+    , combo_groups(!groups.IsEmpty() ? new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, groups, wxCB_READONLY) : nullptr)
 {
 #ifdef __APPLE__
     txt_filename->OSXDisableAllSmartSubstitutions();
 #endif
     const AppConfig *app_config = wxGetApp().app_config;
 
-    auto *label_dir_hint = new wxStaticText(this, wxID_ANY, _(L("Use forward slashes ( / ) as a directory separator if needed.")));
+    auto *label_dir_hint = new wxStaticText(this, wxID_ANY, _L("Use forward slashes ( / ) as a directory separator if needed."));
     label_dir_hint->Wrap(CONTENT_WIDTH * wxGetApp().em_unit());
 
     content_sizer->Add(txt_filename, 0, wxEXPAND);
@@ -49,10 +51,19 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, bool can_start_pr
         content_sizer->Add(box_print, 0, wxBOTTOM, 2*VERT_SPACING);
         box_print->SetValue(app_config->get("recent", CONFIG_KEY_PRINT) == "1");
     }
+    
+    if (combo_groups != nullptr) {
+        // Repetier specific: Show a selection of file groups.
+        auto *label_group = new wxStaticText(this, wxID_ANY, _L("Group"));
+        content_sizer->Add(label_group);
+        content_sizer->Add(combo_groups, 0, wxBOTTOM, 2*VERT_SPACING);        
+        wxString recent_group = from_u8(app_config->get("recent", CONFIG_KEY_GROUP));
+        if (! recent_group.empty())
+            combo_groups->SetValue(recent_group);
+    }
 
     btn_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL));
 
-
     wxString recent_path = from_u8(app_config->get("recent", CONFIG_KEY_PATH));
     if (recent_path.Length() > 0 && recent_path[recent_path.Length() - 1] != '/') {
         recent_path += '/';
@@ -64,7 +75,7 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, bool can_start_pr
 
     txt_filename->SetValue(recent_path);
     txt_filename->SetFocus();
-
+    
     Fit();
 
     Bind(wxEVT_SHOW, [=](const wxShowEvent &) {
@@ -86,6 +97,16 @@ bool PrintHostSendDialog::start_print() const
     return box_print != nullptr ? box_print->GetValue() : false;
 }
 
+std::string PrintHostSendDialog::group() const
+{
+     if (combo_groups == nullptr) {
+         return "";
+     } else {
+         wxString group = combo_groups->GetValue();
+         return into_u8(group);
+    }
+}
+
 void PrintHostSendDialog::EndModal(int ret)
 {
     if (ret == wxID_OK) {
@@ -96,9 +117,15 @@ void PrintHostSendDialog::EndModal(int ret)
 			path.clear();
 		else
             path = path.SubString(0, last_slash);
+                
 		AppConfig *app_config = wxGetApp().app_config;
 		app_config->set("recent", CONFIG_KEY_PATH, into_u8(path));
         app_config->set("recent", CONFIG_KEY_PRINT, start_print() ? "1" : "0");
+        
+        if (combo_groups != nullptr) {
+            wxString group = combo_groups->GetValue();
+            app_config->set("recent", CONFIG_KEY_GROUP, into_u8(group));
+        }
     }
 
     MsgDialog::EndModal(ret);
@@ -133,7 +160,7 @@ wxEvent *PrintHostQueueDialog::Event::Clone() const
 }
 
 PrintHostQueueDialog::PrintHostQueueDialog(wxWindow *parent)
-    : DPIDialog(parent, wxID_ANY, _(L("Print host upload queue")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
+    : DPIDialog(parent, wxID_ANY, _L("Print host upload queue"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
     , on_progress_evt(this, EVT_PRINTHOST_PROGRESS, &PrintHostQueueDialog::on_progress, this)
     , on_error_evt(this, EVT_PRINTHOST_ERROR, &PrintHostQueueDialog::on_error, this)
     , on_cancel_evt(this, EVT_PRINTHOST_CANCEL, &PrintHostQueueDialog::on_cancel, this)
@@ -144,19 +171,20 @@ PrintHostQueueDialog::PrintHostQueueDialog(wxWindow *parent)
 
     job_list = new wxDataViewListCtrl(this, wxID_ANY);
     // Note: Keep these in sync with Column
-    job_list->AppendTextColumn(_(L("ID")), wxDATAVIEW_CELL_INERT);
-    job_list->AppendProgressColumn(_(L("Progress")), wxDATAVIEW_CELL_INERT);
-    job_list->AppendTextColumn(_(L("Status")), wxDATAVIEW_CELL_INERT);
-    job_list->AppendTextColumn(_(L("Host")), wxDATAVIEW_CELL_INERT);
-    job_list->AppendTextColumn(_(L("Filename")), wxDATAVIEW_CELL_INERT);
-    job_list->AppendTextColumn(_(L("Error Message")), wxDATAVIEW_CELL_INERT, -1, wxALIGN_CENTER, wxDATAVIEW_COL_HIDDEN);
+    job_list->AppendTextColumn(_L("ID"), wxDATAVIEW_CELL_INERT);
+    job_list->AppendProgressColumn(_L("Progress"), wxDATAVIEW_CELL_INERT);
+    job_list->AppendTextColumn(_L("Status"), wxDATAVIEW_CELL_INERT);
+    job_list->AppendTextColumn(_L("Host"), wxDATAVIEW_CELL_INERT);
+    job_list->AppendTextColumn(_L("Filename"), wxDATAVIEW_CELL_INERT);
+    job_list->AppendTextColumn(_L("Error Message"), wxDATAVIEW_CELL_INERT, -1, wxALIGN_CENTER, wxDATAVIEW_COL_HIDDEN);
 
     auto *btnsizer = new wxBoxSizer(wxHORIZONTAL);
-    btn_cancel = new wxButton(this, wxID_DELETE, _(L("Cancel selected")));
+    btn_cancel = new wxButton(this, wxID_DELETE, _L("Cancel selected"));
     btn_cancel->Disable();
-    btn_error = new wxButton(this, wxID_ANY, _(L("Show error message")));
+    btn_error = new wxButton(this, wxID_ANY, _L("Show error message"));
     btn_error->Disable();
-    auto *btn_close = new wxButton(this, wxID_CANCEL, _(L("Close")));  // Note: The label needs to be present, otherwise we get accelerator bugs on Mac
+    // Note: The label needs to be present, otherwise we get accelerator bugs on Mac
+    auto *btn_close = new wxButton(this, wxID_CANCEL, _L("Close"));
     btnsizer->Add(btn_cancel, 0, wxRIGHT, SPACING);
     btnsizer->Add(btn_error, 0);
     btnsizer->AddStretchSpacer();
@@ -195,7 +223,7 @@ void PrintHostQueueDialog::append_job(const PrintHostJob &job)
     wxVector<wxVariant> fields;
     fields.push_back(wxVariant(wxString::Format("%d", job_list->GetItemCount() + 1)));
     fields.push_back(wxVariant(0));
-    fields.push_back(wxVariant(_(L("Enqueued"))));
+    fields.push_back(wxVariant(_L("Enqueued")));
     fields.push_back(wxVariant(job.printhost->get_host()));
     fields.push_back(wxVariant(job.upload_data.upload_path.string()));
     fields.push_back(wxVariant(""));
@@ -226,12 +254,12 @@ void PrintHostQueueDialog::set_state(int idx, JobState state)
     job_list->SetItemData(job_list->RowToItem(idx), static_cast<wxUIntPtr>(state));
 
     switch (state) {
-        case ST_NEW:        job_list->SetValue(_(L("Enqueued")), idx, COL_STATUS); break;
-        case ST_PROGRESS:   job_list->SetValue(_(L("Uploading")), idx, COL_STATUS); break;
-        case ST_ERROR:      job_list->SetValue(_(L("Error")), idx, COL_STATUS); break;
-        case ST_CANCELLING: job_list->SetValue(_(L("Cancelling")), idx, COL_STATUS); break;
-        case ST_CANCELLED:  job_list->SetValue(_(L("Cancelled")), idx, COL_STATUS); break;
-        case ST_COMPLETED:  job_list->SetValue(_(L("Completed")), idx, COL_STATUS); break;
+        case ST_NEW:        job_list->SetValue(_L("Enqueued"), idx, COL_STATUS); break;
+        case ST_PROGRESS:   job_list->SetValue(_L("Uploading"), idx, COL_STATUS); break;
+        case ST_ERROR:      job_list->SetValue(_L("Error"), idx, COL_STATUS); break;
+        case ST_CANCELLING: job_list->SetValue(_L("Cancelling"), idx, COL_STATUS); break;
+        case ST_CANCELLED:  job_list->SetValue(_L("Cancelled"), idx, COL_STATUS); break;
+        case ST_COMPLETED:  job_list->SetValue(_L("Completed"), idx, COL_STATUS); break;
     }
 }
 
diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp
index e5f96f2b6..71973b64e 100644
--- a/src/slic3r/GUI/PrintHostDialogs.hpp
+++ b/src/slic3r/GUI/PrintHostDialogs.hpp
@@ -15,6 +15,7 @@
 
 class wxButton;
 class wxTextCtrl;
+class wxComboBox;
 class wxCheckBox;
 class wxDataViewListCtrl;
 
@@ -29,14 +30,16 @@ namespace GUI {
 class PrintHostSendDialog : public GUI::MsgDialog
 {
 public:
-    PrintHostSendDialog(const boost::filesystem::path &path, bool can_start_print);
+    PrintHostSendDialog(const boost::filesystem::path &path, bool can_start_print, const wxArrayString& groups);
     boost::filesystem::path filename() const;
     bool start_print() const;
+    std::string group() const;
 
     virtual void EndModal(int ret) override;
 private:
     wxTextCtrl *txt_filename;
     wxCheckBox *box_print;
+    wxComboBox *combo_groups;
 };
 
 
diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp
index 54c137b2c..33c5adca4 100644
--- a/src/slic3r/GUI/Tab.hpp
+++ b/src/slic3r/GUI/Tab.hpp
@@ -435,9 +435,6 @@ private:
     std::vector<PageShp>			m_pages_sla;
 
 public:
-	wxButton*	m_serial_test_btn = nullptr;
-	ScalableButton*	m_print_host_test_btn = nullptr;
-	ScalableButton*	m_printhost_browse_btn = nullptr;
 	ScalableButton*	m_reset_to_filament_color = nullptr;
 
 	size_t		m_extruders_count;
diff --git a/src/slic3r/Utils/AstroBox.hpp b/src/slic3r/Utils/AstroBox.hpp
index 38542275c..f24018b1b 100644
--- a/src/slic3r/Utils/AstroBox.hpp
+++ b/src/slic3r/Utils/AstroBox.hpp
@@ -28,7 +28,7 @@ public:
     bool can_test() const override { return true; }
     bool can_start_print() const override { return true; }
     std::string get_host() const override { return host; }
-
+    
 protected:
     bool validate_version_text(const boost::optional<std::string> &version_text) const;
 
diff --git a/src/slic3r/Utils/Duet.hpp b/src/slic3r/Utils/Duet.hpp
index 7fdd8ea10..e5aec548b 100644
--- a/src/slic3r/Utils/Duet.hpp
+++ b/src/slic3r/Utils/Duet.hpp
@@ -27,7 +27,7 @@ public:
 	bool can_test() const override { return true; }
 	bool can_start_print() const override { return true; }
 	std::string get_host() const override { return host; }
-
+   
 private:
 	enum class ConnectionType { rrf, dsf, error };
 	std::string host;
diff --git a/src/slic3r/Utils/FlashAir.hpp b/src/slic3r/Utils/FlashAir.hpp
index 40af48da1..181405d46 100644
--- a/src/slic3r/Utils/FlashAir.hpp
+++ b/src/slic3r/Utils/FlashAir.hpp
@@ -28,7 +28,7 @@ public:
 	bool can_test() const override { return true; }
 	bool can_start_print() const override { return false; }
 	std::string get_host() const override { return host; }
-
+    
 private:
 	std::string host;
 
diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp
index 0a49b7815..ace7e2fe4 100644
--- a/src/slic3r/Utils/PrintHost.cpp
+++ b/src/slic3r/Utils/PrintHost.cpp
@@ -9,6 +9,7 @@
 
 #include <wx/string.h>
 #include <wx/app.h>
+#include <wx/arrstr.h>
 
 #include "libslic3r/PrintConfig.hpp"
 #include "libslic3r/Channel.hpp"
@@ -16,6 +17,7 @@
 #include "Duet.hpp"
 #include "FlashAir.hpp"
 #include "AstroBox.hpp"
+#include "Repetier.hpp"
 #include "../GUI/PrintHostDialogs.hpp"
 
 namespace fs = boost::filesystem;
@@ -47,6 +49,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config)
             case htDuet:      return new Duet(config);
             case htFlashAir:  return new FlashAir(config);
             case htAstroBox:  return new AstroBox(config);
+            case htRepetier:  return new Repetier(config);
             default:          return nullptr;
         }
     } else {
diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp
index e0aeb463c..35a870b29 100644
--- a/src/slic3r/Utils/PrintHost.hpp
+++ b/src/slic3r/Utils/PrintHost.hpp
@@ -10,6 +10,7 @@
 
 #include "Http.hpp"
 
+class wxArrayString;
 
 namespace Slic3r {
 
@@ -20,6 +21,9 @@ struct PrintHostUpload
 {
     boost::filesystem::path source_path;
     boost::filesystem::path upload_path;
+    
+    std::string group;
+    
     bool start_print = false;
 };
 
@@ -41,8 +45,15 @@ public:
     virtual bool has_auto_discovery() const = 0;
     virtual bool can_test() const = 0;
     virtual bool can_start_print() const = 0;
+    // A print host usually does not support multiple printers, with the exception of Repetier server.
+    virtual bool supports_multiple_printers() const { return false; }
     virtual std::string get_host() const = 0;
 
+    // Support for Repetier server multiple groups & printers. Not supported by other print hosts.
+    // Returns false if not supported. May throw HostNetworkError.
+    virtual bool get_groups(wxArrayString & /* groups */) const { return false; }
+    virtual bool get_printers(wxArrayString & /* printers */) const { return false; }
+
     static PrintHost* get_print_host(DynamicPrintConfig *config);
 
 protected:
diff --git a/src/slic3r/Utils/Repetier.cpp b/src/slic3r/Utils/Repetier.cpp
new file mode 100644
index 000000000..ddf0a06e0
--- /dev/null
+++ b/src/slic3r/Utils/Repetier.cpp
@@ -0,0 +1,268 @@
+#include "Repetier.hpp"
+
+#include <algorithm>
+#include <sstream>
+#include <exception>
+#include <boost/foreach.hpp>
+#include <boost/format.hpp>
+#include <boost/log/trivial.hpp>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/json_parser.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+#include <wx/progdlg.h>
+
+
+#include "libslic3r/PrintConfig.hpp"
+#include "slic3r/GUI/I18N.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/format.hpp"
+#include "Http.hpp"
+
+
+namespace fs = boost::filesystem;
+namespace pt = boost::property_tree;
+
+
+namespace Slic3r {
+
+Repetier::Repetier(DynamicPrintConfig *config) :
+    host(config->opt_string("print_host")),
+    apikey(config->opt_string("printhost_apikey")),
+    cafile(config->opt_string("printhost_cafile")),
+    port(config->opt_string("printhost_port"))
+{}
+
+const char* Repetier::get_name() const { return "Repetier"; }
+
+bool Repetier::test(wxString &msg) const
+{
+    // Since the request is performed synchronously here,
+    // it is ok to refer to `msg` from within the closure
+
+    const char *name = get_name();
+
+    bool res = true;
+    auto url = make_url("printer/info");
+
+    BOOST_LOG_TRIVIAL(info) << boost::format("%1%: List version 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 version: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
+            res = false;
+            msg = format_error(body, error, status);
+        })
+        .on_complete([&, this](std::string body, unsigned) {
+            BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body;
+
+            try {
+                std::stringstream ss(body);
+                pt::ptree ptree;
+                pt::read_json(ss, ptree);
+                
+                const auto text = ptree.get_optional<std::string>("name");
+                res = validate_version_text(text);
+                if (! res) {
+                    msg = GUI::from_u8((boost::format(_utf8(L("Mismatched type of print host: %s"))) % (text ? *text : "Repetier")).str());
+                }
+            }
+            catch (const std::exception &) {
+                res = false;
+                msg = "Could not parse server response";
+            }
+        })
+        .perform_sync();
+
+    return res;
+}
+
+wxString Repetier::get_test_ok_msg () const
+{
+    return _(L("Connection to Repetier works correctly."));
+}
+
+wxString Repetier::get_test_failed_msg (wxString &msg) const
+{
+        return GUI::from_u8((boost::format("%s: %s\n\n%s")
+        % _utf8(L("Could not connect to Repetier"))
+        % std::string(msg.ToUTF8())
+        % _utf8(L("Note: Repetier version at least 0.90.0 is required."))).str());
+}
+
+bool Repetier::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const
+{
+    const char *name = get_name();
+
+    const auto upload_filename = upload_data.upload_path.filename();
+    const auto upload_parent_path = upload_data.upload_path.parent_path();
+
+    wxString test_msg;
+    if (! test(test_msg)) {
+        error_fn(std::move(test_msg));
+        return false;
+    }
+
+    bool res = true;
+
+    auto url = make_url((boost::format("printer/model/%1%") % port).str());
+
+    BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%, group: %7%")
+        % name
+        % upload_data.source_path
+        % url
+        % upload_filename.string()
+        % upload_parent_path.string()
+        % upload_data.start_print
+        % upload_data.group;
+
+    auto http = Http::post(std::move(url));
+    set_auth(http);
+    
+    if (! upload_data.group.empty() && upload_data.group != _utf8(L("Default"))) {
+        http.form_add("group", upload_data.group);
+    }
+    
+    http.form_add("a", "upload")
+        .form_add_file("filename", upload_data.source_path.string(), upload_filename.string())
+        .on_complete([&](std::string body, unsigned status) {
+            BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body;
+        })
+        .on_error([&](std::string body, std::string error, unsigned status) {
+            BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % 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) << "Repetier: Upload canceled";
+                res = false;
+            }
+        })
+        .perform_sync();
+
+    return res;
+}
+
+bool Repetier::validate_version_text(const boost::optional<std::string> &version_text) const
+{
+    return version_text ? boost::starts_with(*version_text, "Repetier") : true;
+}
+
+void Repetier::set_auth(Http &http) const
+{
+    http.header("X-Api-Key", apikey);
+
+    if (! cafile.empty()) {
+        http.ca_file(cafile);
+    }
+}
+
+std::string Repetier::make_url(const std::string &path) const
+{
+    if (host.find("http://") == 0 || host.find("https://") == 0) {
+        if (host.back() == '/') {
+            return (boost::format("%1%%2%") % host % path).str();
+        } else {
+            return (boost::format("%1%/%2%") % host % path).str();
+        }
+    } else {
+        return (boost::format("http://%1%/%2%") % host % path).str();
+    }
+}
+
+bool Repetier::get_groups(wxArrayString& groups) const
+{
+    bool res = true;
+    
+    const char *name = get_name();
+    auto url = make_url((boost::format("printer/api/%1%") % port).str());
+
+    BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get groups at: %2%") % name % url;
+
+    auto http = Http::get(std::move(url));
+    set_auth(http);
+    http.form_add("a", "listModelGroups");
+    http.on_error([&](std::string body, std::string error, unsigned status) {
+            BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
+        })
+        .on_complete([&, this](std::string body, unsigned) {
+            BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got groups: %2%") % name % body;
+
+            try {
+                std::stringstream ss(body);
+                pt::ptree ptree;
+                pt::read_json(ss, ptree);
+
+                BOOST_FOREACH(boost::property_tree::ptree::value_type &v, ptree.get_child("groupNames.")) {
+                    if (v.second.data() == "#") {
+                        groups.push_back(_utf8(L("Default")));
+                    } else {
+                        // Is it safe to assume that the data are utf-8 encoded?
+                        groups.push_back(wxString::FromUTF8(v.second.data()));
+                    }
+                }
+            }
+            catch (const std::exception &) {
+                //msg = "Could not parse server response";
+                res = false;
+            }
+        })
+        .perform_sync();
+
+    return res;
+}
+
+bool Repetier::get_printers(wxArrayString& printers) const
+{
+    const char *name = get_name();
+
+    bool res = true;
+    auto url = make_url("printer/list");
+
+    BOOST_LOG_TRIVIAL(info) << boost::format("%1%: List printers 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 listing printers: %2%, HTTP %3%, body: `%4%`") % name % error % status % body;
+            res = false;
+        })
+        .on_complete([&, this](std::string body, unsigned http_status) {
+            BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got printers: %2%, HTTP status: %3%") % name % body % http_status;
+            
+            if (http_status != 200)
+                throw HostNetworkError(GUI::format(_L("HTTP status: %1%\nMessage body: \"%2%\""), http_status, body));
+
+            std::stringstream ss(body);
+            pt::ptree ptree;
+            try {
+                pt::read_json(ss, ptree);
+            } catch (const pt::ptree_error &err) {
+                throw HostNetworkError(GUI::format(_L("Parsing of host response failed.\nMessage body: \"%1%\"\nError: \"%2%\""), body, err.what()));
+            }
+            
+            const auto error = ptree.get_optional<std::string>("error");
+            if (error)
+                throw HostNetworkError(*error);
+
+            try {
+                BOOST_FOREACH(boost::property_tree::ptree::value_type &v, ptree.get_child("data.")) {
+                    const auto port = v.second.get<std::string>("slug");
+                    printers.push_back(Slic3r::GUI::from_u8(port));
+                }
+            } catch (const std::exception &err) {
+                throw HostNetworkError(GUI::format(_L("Enumeration of host printers failed.\nMessage body: \"%1%\"\nError: \"%2%\""), body, err.what()));
+            }
+        })
+        .perform_sync();
+
+    return res;
+}
+
+}
diff --git a/src/slic3r/Utils/Repetier.hpp b/src/slic3r/Utils/Repetier.hpp
new file mode 100644
index 000000000..d94d7ec56
--- /dev/null
+++ b/src/slic3r/Utils/Repetier.hpp
@@ -0,0 +1,52 @@
+#ifndef slic3r_Repetier_hpp_
+#define slic3r_Repetier_hpp_
+
+#include <string>
+#include <wx/string.h>
+#include <boost/optional.hpp>
+
+#include "PrintHost.hpp"
+
+
+namespace Slic3r {
+
+class DynamicPrintConfig;
+class Http;
+
+class Repetier : public PrintHost
+{
+public:
+    Repetier(DynamicPrintConfig *config);
+    ~Repetier() override = default;
+
+    const char* get_name() const;
+
+    bool test(wxString &curl_msg) const override;
+    wxString get_test_ok_msg () const override;
+    wxString get_test_failed_msg (wxString &msg) const override;
+    bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override;
+    bool has_auto_discovery() const override { return false; }
+    bool can_test() const override { return true; }
+    bool can_start_print() const override { return false; }
+    bool supports_multiple_printers() const override { return true; }
+    std::string get_host() const override { return host; }
+    
+    bool get_groups(wxArrayString &groups) const override;
+    bool get_printers(wxArrayString &printers) const override;
+
+protected:
+    virtual bool validate_version_text(const boost::optional<std::string> &version_text) const;
+
+private:
+    std::string host;
+    std::string apikey;
+    std::string cafile;
+    std::string port;
+
+    void set_auth(Http &http) const;
+    std::string make_url(const std::string &path) const;
+};
+
+}
+
+#endif