diff --git a/CMakeLists.txt b/CMakeLists.txt
index 43d7dee70..b5f7cdec2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -42,6 +42,21 @@ message("PATH: $ENV{PATH}")
 message("PERL5LIB: $ENV{PERL5LIB}")
 find_package(Perl REQUIRED)
 
+# CMAKE_PREFIX_PATH is used to point CMake to the remaining dependencies (Boost, TBB, ...)
+# We pick it from environment if it is not defined in another way
+if(NOT DEFINED CMAKE_PREFIX_PATH)
+	if(DEFINED ENV{CMAKE_PREFIX_PATH})
+		set(CMAKE_PREFIX_PATH "$ENV{CMAKE_PREFIX_PATH}")
+	endif()
+endif()
+
+if (MSVC)
+	# By default the startup project in MSVC is the 'ALL_BUILD' cmake-created project,
+	# but we want 'slic3r' as the startup one because debugging run command is associated with it.
+	# (Unfortunatelly it cannot be associated with ALL_BUILD using CMake.)
+	set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT slic3r)
+endif ()
+
 add_subdirectory(xs)
 
 enable_testing ()
diff --git a/cmake/modules/FindCURL.cmake b/cmake/modules/FindCURL.cmake
new file mode 100644
index 000000000..b8724858c
--- /dev/null
+++ b/cmake/modules/FindCURL.cmake
@@ -0,0 +1,59 @@
+# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+# file Copyright.txt or https://cmake.org/licensing for details.
+
+#.rst:
+# FindCURL
+# --------
+#
+# Find curl
+#
+# Find the native CURL headers and libraries.
+#
+# ::
+#
+#   CURL_INCLUDE_DIRS   - where to find curl/curl.h, etc.
+#   CURL_LIBRARIES      - List of libraries when using curl.
+#   CURL_FOUND          - True if curl found.
+#   CURL_VERSION_STRING - the version of curl found (since CMake 2.8.8)
+
+# Look for the header file.
+find_path(CURL_INCLUDE_DIR NAMES curl/curl.h)
+mark_as_advanced(CURL_INCLUDE_DIR)
+
+# Look for the library (sorted from most current/relevant entry to least).
+find_library(CURL_LIBRARY NAMES
+    curl
+  # Windows MSVC Makefile:
+    libcurl_a
+  # Windows MSVC prebuilts:
+    curllib
+    libcurl_imp
+    curllib_static
+  # Windows older "Win32 - MSVC" prebuilts (libcurl.lib, e.g. libcurl-7.15.5-win32-msvc.zip):
+    libcurl
+)
+mark_as_advanced(CURL_LIBRARY)
+
+if(CURL_INCLUDE_DIR)
+  foreach(_curl_version_header curlver.h curl.h)
+    if(EXISTS "${CURL_INCLUDE_DIR}/curl/${_curl_version_header}")
+      file(STRINGS "${CURL_INCLUDE_DIR}/curl/${_curl_version_header}" curl_version_str REGEX "^#define[\t ]+LIBCURL_VERSION[\t ]+\".*\"")
+
+      string(REGEX REPLACE "^#define[\t ]+LIBCURL_VERSION[\t ]+\"([^\"]*)\".*" "\\1" CURL_VERSION_STRING "${curl_version_str}")
+      unset(curl_version_str)
+      break()
+    endif()
+  endforeach()
+endif()
+
+find_package_handle_standard_args(CURL
+                                  REQUIRED_VARS CURL_LIBRARY CURL_INCLUDE_DIR
+                                  VERSION_VAR CURL_VERSION_STRING)
+
+if(CURL_FOUND)
+  set(CURL_LIBRARIES ${CURL_LIBRARY})
+  set(CURL_INCLUDE_DIRS ${CURL_INCLUDE_DIR})
+
+  message(STATUS "  Curl libraries:        = ${CURL_LIBRARIES}")
+  message(STATUS "  Curl include dirs:     = ${CURL_INCLUDE_DIRS}")
+endif()
diff --git a/cmake/msvc/slic3r.wperl64d.props b/cmake/msvc/slic3r.wperl64d.props
new file mode 100644
index 000000000..68dac2085
--- /dev/null
+++ b/cmake/msvc/slic3r.wperl64d.props
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <LocalDebuggerCommand>C:\wperl64d\bin\perl.exe</LocalDebuggerCommand>
+    <LocalDebuggerCommandArguments>slic3r.pl</LocalDebuggerCommandArguments>
+    <DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
+    <LocalDebuggerWorkingDirectory>..\..</LocalDebuggerWorkingDirectory>
+  </PropertyGroup>
+</Project>
diff --git a/cmake/msvc/xs.wperl64d.props b/cmake/msvc/xs.wperl64d.props
new file mode 100644
index 000000000..101923581
--- /dev/null
+++ b/cmake/msvc/xs.wperl64d.props
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ImportGroup Label="PropertySheets">
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup>
+    <ExecutablePath>$(VC_ExecutablePath_x64);$(WindowsSDK_ExecutablePath);$(VS_ExecutablePath);$(MSBuild_ExecutablePath);$(FxCopDir);$(PATH);c:\wperl64d\bin\;</ExecutablePath>
+  </PropertyGroup>
+  <ItemDefinitionGroup />
+  <ItemGroup />
+</Project>
diff --git a/doc/How to build - UNIX.md b/doc/How to build - UNIX.md
new file mode 100644
index 000000000..77ce54419
--- /dev/null
+++ b/doc/How to build - UNIX.md	
@@ -0,0 +1,2 @@
+# Building Slic3r PE on Linux/UNIX
+
diff --git a/doc/How to build - Windows.md b/doc/How to build - Windows.md
new file mode 100644
index 000000000..8209954bd
--- /dev/null
+++ b/doc/How to build - Windows.md	
@@ -0,0 +1,86 @@
+# Building Slic3r PE on Microsoft Windows
+
+The currently supported way of building Slic3r PE on Windows is with MS Visual Studio 2013
+using our Perl binary distribution (compiled from official Perl sources).
+You can use the free [Visual Studio 2013 Community Edition](https://www.visualstudio.com/vs/older-downloads/).
+
+Other setups (such as mingw + Strawberry Perl) _may_ work, but we cannot guarantee this will work
+and cannot provide guidance.
+
+
+### Geting the dependencies
+
+First, download and upnack our Perl + wxWidgets binary distribution:
+
+  - 32 bit, release mode: [wperl32-5.24.0-2018-03-02.7z](https://bintray.com/vojtechkral/Slic3r-PE/download_file?file_path=wperl32-5.24.0-2018-03-02.7z)
+  - 64 bit, release mode: [wperl64-5.24.0-2018-03-02.7z](https://bintray.com/vojtechkral/Slic3r-PE/download_file?file_path=wperl64-5.24.0-2018-03-02.7z)
+  - 64 bit, release mode + debug symbols: [wperl64d-5.24.0-2018-03-02.7z](https://bintray.com/vojtechkral/Slic3r-PE/download_file?file_path=wperl64d-5.24.0-2018-03-02.7z)
+
+It is recommended to unpack this package into `C:\`.
+
+Apart from wxWidgets and Perl, you will also need additional dependencies:
+
+  - Boost
+  - Intel TBB
+  - libcurl
+
+We have prepared a binary package of the listed libraries:
+
+  - 32 bit: [slic3r-destdir-32.7z](https://bintray.com/vojtechkral/Slic3r-PE/download_file?file_path=slic3r-destdir-32.7z)
+  - 64 bit: [slic3r-destdir-64.7z](https://bintray.com/vojtechkral/Slic3r-PE/download_file?file_path=slic3r-destdir-64.7z)
+
+It is recommended you unpack this package into `C:\local\` as the environment
+setup script expects it there.
+
+Alternatively you can also compile the additional dependencies yourself.
+There is a [powershell script](./deps-build/windows/slic3r-makedeps.ps1) which automates this process.
+
+### Building Slic3r PE
+
+Once the dependencies are set up in their respective locations,
+go to the `wperl*` directory extracted earlier and launch the `cmdline.lnk` file
+which opens a command line prompt with appropriate environment variables set up.
+
+In this command line, `cd` into the directory with Slic3r sources
+and use these commands to build the Slic3r from the command line:
+
+    perl Build.PL
+    perl Build.PL --gui
+    mkdir build
+    cd build
+    cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
+    nmake
+    ctest --verbose    # TODO: ???
+    cd ..
+    perl slic3r.pl
+
+The above commands use `nmake` Makefiles.
+You may also build Slic3r PE with other build tools:
+
+
+### Building with Visual Studio
+
+To build, lanuch and/or debug Slic3r PE with Visual Studio (64 bits), replace the `cmake` command with:
+
+    cmake .. -G "Visual Studio 12 Win64" -DCMAKE_CONFIGURATION_TYPES=Release;RelWithDebInfo || exit /b
+
+For the 32-bit variant, use:
+
+    cmake .. -G "Visual Studio 12" -DCMAKE_CONFIGURATION_TYPES=Release;RelWithDebInfo || exit /b
+
+After `cmake` has finished, go to the `Slic3r\build` directory and open the `Slic3r.sln` solution file.
+This should open Visual Studio and load all the Slic3r solution containing all the projects.
+Make sure you use Visual Studio 2013 to open the solution.
+
+You can then use the usual Visual Studio controls to build Slic3r.
+If you want to run or debug Slic3r from within Visual Studio, make sure the `slic3r` project is activated.
+There are multiple projects in the Slic3r solution, but only the `slic3r` project is configured with the right
+commands to run Slic3r.
+
+
+### Building with ninja
+
+To use [Ninja](TODO), replace the `cmake` and `nmake` commands with:
+
+    cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
+    ninja
diff --git a/doc/deps-build/unix-static/Makefile b/doc/deps-build/unix-static/Makefile
new file mode 100644
index 000000000..e7588b994
--- /dev/null
+++ b/doc/deps-build/unix-static/Makefile
@@ -0,0 +1,132 @@
+
+#
+# This makefile downloads, configures and builds Slic3r PE dependencies for Unix.
+# (That is, all dependencies except perl + wxWidgets.)
+# The libraries are installed in DESTDIR, which you can customize like so:
+#
+#     DESTDIR=foo/bar make
+#
+# The default DESTDIR is ~/slic3r-destdir
+# If the DESTDIR doesn't exits, the makefile tries to create it
+#
+# To pass the DESTDIR path along to cmake, set the use CMAKE_PREFIX_PATH variable
+# and set it to $DESTDIR/usr/local
+#
+# You can also customize the NPROC variable in the same way to configure the number
+# of cores the build process uses. By default this is set to what the `nproc` command says.
+#
+
+
+DESTDIR ?= $(HOME)/slic3r-destdir
+NPROC ?= $(shell nproc)
+
+
+BOOST = boost_1_66_0
+TBB_SHA = a0dc9bf76d0120f917b641ed095360448cabc85b
+TBB = tbb-$(TBB_SHA)
+OPENSSL = openssl-OpenSSL_1_1_0g
+CURL = curl-7.58.0
+
+.PHONY: all destdir boost libcurl libopenssl libtbb
+
+all: destdir boost libtbb libcurl
+	@echo
+	@echo "All done!"
+	@echo
+
+destdir:
+	mkdir -p $(DESTDIR)
+
+
+
+boost: $(BOOST).tar.gz
+	tar -zxvf $(BOOST).tar.gz
+	cd $(BOOST) && ./bootstrap.sh --with-libraries=system,filesystem,thread,log,locale,regex --prefix=$(DESTDIR)/usr/local
+	cd $(BOOST) && ./b2 \
+		-j $(NPROC) \
+		link=static \
+		variant=release \
+		threading=multi \
+		boost.locale.icu=off \
+		cxxflags=-fPIC cflags=-fPIC \
+		install
+
+$(BOOST).tar.gz:
+	curl -L -o $@ https://dl.bintray.com/boostorg/release/1.66.0/source/$@
+
+
+
+libtbb: $(TBB).tar.gz
+	tar -zxvf $(TBB).tar.gz
+	mkdir -p $(TBB)/mybuild
+	cd $(TBB)/mybuild && cmake .. -DTBB_BUILD_SHARED=OFF -DTBB_BUILD_TESTS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON
+	$(MAKE) -C $(TBB)/mybuild -j$(NPROC)
+	$(MAKE) -C $(TBB)/mybuild install DESTDIR=$(DESTDIR)
+
+$(TBB).tar.gz:
+	curl -L -o $@ https://github.com/wjakob/tbb/archive/$(TBB_SHA).tar.gz
+
+
+
+libopenssl: $(OPENSSL).tar.gz
+	tar -zxvf $(OPENSSL).tar.gz
+	cd $(OPENSSL) && ./config --openssldir=/etc/ssl shared no-ssl3-method no-dynamic-engine '-Wa,--noexecstack'
+	make -C $(OPENSSL) depend
+	make -C $(OPENSSL) -j$(NPROC)
+	make -C $(OPENSSL) install DESTDIR=$(DESTDIR)
+
+$(OPENSSL).tar.gz:
+	curl -L -o $@ 'https://github.com/openssl/openssl/archive/OpenSSL_1_1_0g.tar.gz'
+
+
+
+libcurl: libopenssl $(CURL).tar.gz
+	tar -zxvf $(CURL).tar.gz
+# XXX: disable shared?
+# Setting PKG_CONFIG_PATH should make libcurl find our previously built openssl
+	cd $(CURL) && PKG_CONFIG_PATH=$(DESTDIR)/usr/local/lib/pkgconfig ./configure \
+		--enable-static \
+		--enable-shared \
+		--with-pic \
+		--enable-ipv6 \
+		--enable-versioned-symbols \
+		--enable-threaded-resolver \
+		--with-random=/dev/urandom \
+		--with-ca-bundle=/etc/ssl/certs/ca-certificates.crt \
+		--disable-ldap \
+		--disable-ldaps \
+		--disable-manual \
+		--disable-rtsp \
+		--disable-dict \
+		--disable-telnet \
+		--disable-pop3 \
+		--disable-imap \
+		--disable-smb \
+		--disable-smtp \
+		--disable-gopher \
+		--disable-crypto-auth \
+		--without-gssapi \
+		--without-libpsl \
+		--without-libidn2 \
+		--without-gnutls \
+		--without-polarssl \
+		--without-mbedtls \
+		--without-cyassl \
+		--without-nss \
+		--without-axtls \
+		--without-brotli \
+		--without-libmetalink \
+		--without-libssh \
+		--without-libssh2 \
+		--without-librtmp \
+		--without-nghttp2 \
+		--without-zsh-functions-dir
+	$(MAKE) -C $(CURL) -j$(NPROC)
+	$(MAKE) -C $(CURL) install DESTDIR=$(DESTDIR)
+
+$(CURL).tar.gz:
+	curl -L -o $@ https://curl.haxx.se/download/$@
+
+
+clean:
+	rm -rf $(BOOST) $(BOOST).tar.gz $(TBB) $(TBB).tar.gz $(OPENSSL) $(OPENSSL).tar.gz $(CURL) $(CURL).tar.gz
diff --git a/doc/deps-build/windows/slic3r-makedeps.ps1 b/doc/deps-build/windows/slic3r-makedeps.ps1
new file mode 100644
index 000000000..8b39cae30
--- /dev/null
+++ b/doc/deps-build/windows/slic3r-makedeps.ps1
@@ -0,0 +1,141 @@
+#!powershell
+#
+# This script downloads, configures and builds Slic3r PE dependencies for Unix.
+# (That is, all dependencies except perl + wxWidgets.)
+#
+# To use this script, launch the Visual Studio command line,
+# `cd` into the directory containing this script and use this command:
+#
+#     powershell .\slic3r-makedeps.ps1
+#
+# The dependencies will be downloaded and unpacked into the current dir.
+# This script WILL NOT try to guess the build architecture (64 vs 32 bits),
+# it will by default build the 64-bit variant. To build the 32-bit variant, use:
+#
+#     powershell .\slic3r-makedeps.ps1 -b32
+#
+# Built libraries are installed into $destdir,
+# which by default is C:\local\slic3r-destdir-$bits
+# You can customize the $destdir using:
+#
+#     powershell .\slic3r-makedeps.ps1 -destdir C:\foo\bar
+#
+# To pass the $destdir path along to cmake, set the use CMAKE_PREFIX_PATH variable
+# and set it to $destdir\usr\local
+#
+# Script requirements: PowerShell 3.0, .NET 4.5
+#
+
+
+param(
+    [switch]$b32 = $false,
+    [string]$destdir = ""
+)
+
+if ($destdir -eq "") {
+    $destdir = "C:\local\slic3r-destdir-" + ('32', '64')[!$b32]
+}
+
+$BOOST = 'boost_1_63_0'
+$CURL = 'curl-7.28.0'
+$TBB_SHA = 'a0dc9bf76d0120f917b641ed095360448cabc85b'
+$TBB = "tbb-$TBB_SHA"
+
+
+try
+{
+
+
+# Set up various settings and utilities:
+[Environment]::CurrentDirectory = Get-Location
+$NPROC = (Get-WmiObject -class Win32_processor).NumberOfLogicalProcessors
+Add-Type -A System.IO.Compression.FileSystem
+#   This fxies SSL/TLS errors, credit goes to Ansible; see their `win_get_url.ps1` file
+$security_protcols = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::SystemDefault
+if ([Net.SecurityProtocolType].GetMember('Tls11').Count -gt 0) {
+    $security_protcols = $security_protcols -bor [Net.SecurityProtocolType]::Tls11
+}
+if ([Net.SecurityProtocolType].GetMember('Tls12').Count -gt 0) {
+    $security_protcols = $security_protcols -bor [Net.SecurityProtocolType]::Tls12
+}
+[Net.ServicePointManager]::SecurityProtocol = $security_protcols
+$webclient = New-Object System.Net.WebClient
+
+
+# Ensure DESTDIR exists:
+mkdir $destdir -ea 0
+mkdir "$destdir\usr\local" -ea 0
+
+
+# Download sources:
+echo 'Downloading sources ...'
+if (!(Test-Path "$BOOST.zip")) { $webclient.DownloadFile("https://dl.bintray.com/boostorg/release/1.63.0/source/$BOOST.zip", "$BOOST.zip") }
+if (!(Test-Path "$TBB.zip")) { $webclient.DownloadFile("https://github.com/wjakob/tbb/archive/$TBB_SHA.zip", "$TBB.zip") }
+if (!(Test-Path "$CURL.zip")) { $webclient.DownloadFile("https://curl.haxx.se/download/$CURL.zip", ".\$CURL.zip") }
+
+
+# Unpack sources:
+echo 'Unpacking ...'
+if (!(Test-Path $BOOST)) { [IO.Compression.ZipFile]::ExtractToDirectory("$BOOST.zip", '.') }
+if (!(Test-Path $TBB)) { [IO.Compression.ZipFile]::ExtractToDirectory("$TBB.zip", '.') }
+if (!(Test-Path $CURL)) { [IO.Compression.ZipFile]::ExtractToDirectory("$CURL.zip", '.') }
+
+
+# Build libraries:
+echo 'Building ...'
+
+# Build boost
+pushd "$BOOST"
+.\bootstrap
+$adr_mode = ('32', '64')[!$b32]
+.\b2 `
+    -j "$NPROC" `
+    --with-system `
+    --with-filesystem `
+    --with-thread `
+    --with-log `
+    --with-locale `
+    --with-regex `
+    "--prefix=$destdir/usr/local" `
+    "address-model=$adr_mode" `
+    toolset=msvc-12.0 `
+    link=static `
+    variant=release `
+    threading=multi `
+    boost.locale.icu=off `
+    install
+popd
+
+# Build TBB
+pushd "$TBB"
+mkdir 'mybuild' -ea 0
+cd 'mybuild'
+$generator = ('Visual Studio 12', 'Visual Studio 12 Win64')[!$b32]
+cmake .. `
+    -G "$generator" `
+    -DCMAKE_CONFIGURATION_TYPES=Release `
+    -DTBB_BUILD_SHARED=OFF `
+    -DTBB_BUILD_TESTS=OFF "-DCMAKE_INSTALL_PREFIX:PATH=$destdir\usr\local"
+msbuild /P:Configuration=Release INSTALL.vcxproj
+popd
+
+# Build libcurl:
+pushd "$CURL\winbuild"
+$machine = ("x86", "x64")[!$b32]
+nmake /f Makefile.vc mode=static VC=12 GEN_PDB=yes DEBUG=no "MACHINE=$machine"
+Copy-Item -R -Force ..\builds\libcurl-*-winssl\include\* "$destdir\usr\local\include\"
+Copy-Item -R -Force ..\builds\libcurl-*-winssl\lib\* "$destdir\usr\local\lib\"
+popd
+
+
+echo ""
+echo "All done!"
+echo ""
+
+
+}
+catch [Exception]
+{
+    # This prints errors in a verbose manner
+    echo $_.Exception|format-list -force
+}
diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm
index 572bdac32..442d0abc9 100644
--- a/lib/Slic3r/GUI/MainFrame.pm
+++ b/lib/Slic3r/GUI/MainFrame.pm
@@ -174,7 +174,6 @@ sub _init_tabpanel {
     EVT_COMMAND($self, -1, $BUTTON_BROWSE_EVENT, sub {
         my ($self, $event) = @_;
         my $msg = $event->GetString;
-        print "BUTTON_BROWSE_EVENT: ", $msg, "\n";
 
         # look for devices
         my $entries;
@@ -197,7 +196,6 @@ sub _init_tabpanel {
     EVT_COMMAND($self, -1, $BUTTON_TEST_EVENT, sub {
         my ($self, $event) = @_;
         my $msg = $event->GetString;
-        print "BUTTON_TEST_EVENT: ", $msg, "\n";
 
         my $ua = LWP::UserAgent->new;
         $ua->timeout(10);
@@ -409,7 +407,7 @@ sub _init_menubar {
             wxTheApp->about;
         });
     }
-    
+
     # menubar
     # assign menubar to frame after appending items, otherwise special items
     # will not be handled correctly
@@ -424,6 +422,7 @@ sub _init_menubar {
         # (Select application language from the list of installed languages)
         Slic3r::GUI::add_debug_menu($menubar, $self->{lang_ch_event});
         $menubar->Append($helpMenu, L("&Help"));
+        # Add an optional debug menu. In production code, the add_debug_menu() call should do nothing.
         $self->SetMenuBar($menubar);
     }
 }
diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm
index 5cedfbb08..d48e31462 100644
--- a/lib/Slic3r/GUI/Plater.pm
+++ b/lib/Slic3r/GUI/Plater.pm
@@ -52,7 +52,7 @@ sub new {
     my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
     $self->{config} = Slic3r::Config::new_from_defaults_keys([qw(
         bed_shape complete_objects extruder_clearance_radius skirts skirt_distance brim_width variable_layer_height
-        serial_port serial_speed octoprint_host octoprint_apikey
+        serial_port serial_speed octoprint_host octoprint_apikey octoprint_cafile
         nozzle_diameter single_extruder_multi_material 
         wipe_tower wipe_tower_x wipe_tower_y wipe_tower_width wipe_tower_per_color_wipe extruder_colour filament_colour
     )]);
@@ -1458,8 +1458,13 @@ sub on_export_completed {
     wxTheApp->notify($message);
     
     $self->do_print if $do_print;
+
     # Send $self->{send_gcode_file} to OctoPrint.
-    $self->send_gcode if $send_gcode;
+    if ($send_gcode) {
+        my $op = Slic3r::OctoPrint->new($self->{config});
+        $op->send_gcode($self->GetId(), $PROGRESS_BAR_EVENT, $ERROR_EVENT, $self->{send_gcode_file});
+    }
+
     $self->{print_file} = undef;
     $self->{send_gcode_file} = undef;
     $self->{"print_info_cost"}->SetLabel(sprintf("%.2f" , $self->{print}->total_cost));
@@ -1488,45 +1493,6 @@ sub do_print {
     my $filament_names = wxTheApp->{preset_bundle}->filament_presets;
     $filament_stats = { map { $filament_names->[$_] => $filament_stats->{$_} } keys %$filament_stats };
     $printer_panel->load_print_job($self->{print_file}, $filament_stats);
-    
-    $self->GetFrame->select_tab(1);
-}
-
-# Send $self->{send_gcode_file} to OctoPrint.
-#FIXME Currently this call blocks the UI. Make it asynchronous.
-sub send_gcode {
-    my ($self) = @_;
-    
-    $self->statusbar->StartBusy;
-    
-    my $ua = LWP::UserAgent->new;
-    $ua->timeout(180);
-    
-    my $res = $ua->post(
-        "http://" . $self->{config}->octoprint_host . "/api/files/local",
-        Content_Type => 'form-data',
-        'X-Api-Key' => $self->{config}->octoprint_apikey,
-        Content => [
-            file => [ 
-                # On Windows, the path has to be encoded in local code page for perl to be able to open it.
-                Slic3r::encode_path($self->{send_gcode_file}),
-                # Remove the UTF-8 flag from the perl string, so the LWP::UserAgent can insert 
-                # the UTF-8 encoded string into the request as a byte stream.
-                Slic3r::path_to_filename_raw($self->{send_gcode_file})
-            ],
-            print => $self->{send_gcode_file_print} ? 1 : 0,
-        ],
-    );
-    
-    $self->statusbar->StopBusy;
-    
-    if ($res->is_success) {
-        $self->statusbar->SetStatusText(L("G-code file successfully uploaded to the OctoPrint server"));
-    } else {
-        my $message = L("Error while uploading to the OctoPrint server: ") . $res->status_line;
-        Slic3r::GUI::show_error($self, $message);
-        $self->statusbar->SetStatusText($message);
-    }
 }
 
 sub export_stl {
diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt
index 35d1fc7de..590b5d24f 100644
--- a/xs/CMakeLists.txt
+++ b/xs/CMakeLists.txt
@@ -199,6 +199,12 @@ add_library(libslic3r_gui STATIC
     ${LIBDIR}/slic3r/GUI/2DBed.hpp
     ${LIBDIR}/slic3r/GUI/wxExtensions.cpp
     ${LIBDIR}/slic3r/GUI/wxExtensions.hpp
+    ${LIBDIR}/slic3r/Utils/Http.cpp
+    ${LIBDIR}/slic3r/Utils/Http.hpp
+    ${LIBDIR}/slic3r/Utils/OctoPrint.cpp
+    ${LIBDIR}/slic3r/Utils/OctoPrint.hpp
+    ${LIBDIR}/slic3r/Utils/Bonjour.cpp
+    ${LIBDIR}/slic3r/Utils/Bonjour.hpp
 )
 
 add_library(admesh STATIC
@@ -338,6 +344,7 @@ set(XS_XSP_FILES
     ${XSP_DIR}/Surface.xsp
     ${XSP_DIR}/SurfaceCollection.xsp
     ${XSP_DIR}/TriangleMesh.xsp
+    ${XSP_DIR}/Utils_OctoPrint.xsp
     ${XSP_DIR}/XS.xsp
 )
 foreach (file ${XS_XSP_FILES})
@@ -523,6 +530,27 @@ if (SLIC3R_PRUSACONTROL)
     target_link_libraries(XS ${wxWidgets_LIBRARIES})
 endif()
 
+find_package(CURL REQUIRED)
+include_directories(${CURL_INCLUDE_DIRS})
+target_link_libraries(XS ${CURL_LIBRARIES})
+
+if (SLIC3R_STATIC)
+    if (NOT APPLE)
+        # libcurl is always linked dynamically to the system libcurl on OSX.
+        # On other systems, libcurl is linked statically if SLIC3R_STATIC is set.
+        add_definitions(-DCURL_STATICLIB)
+    endif()
+    if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+        # As of now, our build system produces a statically linked libcurl,
+        # which links the OpenSSL library dynamically.
+        find_package(OpenSSL REQUIRED)
+        message("OpenSSL include dir: ${OPENSSL_INCLUDE_DIR}")
+        message("OpenSSL libraries: ${OPENSSL_LIBRARIES}")
+        include_directories(${OPENSSL_INCLUDE_DIR})
+        target_link_libraries(XS ${OPENSSL_LIBRARIES})
+    endif()
+endif()
+
 ## OPTIONAL packages
 
 # Find eigen3 or use bundled version
@@ -597,6 +625,17 @@ elseif (NOT MSVC)
     target_link_libraries(slic3r -lstdc++)
 endif ()
 
+if (MSVC)
+    # Here we associate some additional properties with the MSVC projects to enable compilation and debugging out of the box.
+    # It seems a props file needs to be copied to the same dir as the proj file, otherwise MSVC doesn't load it up.
+    # For copying, the configure_file() function seems to work much better than the file() function.
+    configure_file("${PROJECT_SOURCE_DIR}/cmake/msvc/xs.wperl64d.props" ${CMAKE_CURRENT_BINARY_DIR} COPYONLY)
+    set_target_properties(XS PROPERTIES VS_USER_PROPS "xs.wperl64d.props")
+    configure_file("${PROJECT_SOURCE_DIR}/cmake/msvc/slic3r.wperl64d.props" ${CMAKE_CURRENT_BINARY_DIR} COPYONLY)
+    set_target_properties(slic3r PROPERTIES VS_USER_PROPS "slic3r.wperl64d.props")
+endif ()
+
+
 # Installation
-install(TARGETS XS DESTINATION lib/slic3r-prusa3d/auto/Slic3r/XS)
-install(FILES lib/Slic3r/XS.pm DESTINATION lib/slic3r-prusa3d/Slic3r)
+install(TARGETS XS DESTINATION ${PERL_VENDORARCH}/auto/Slic3r/XS)
+install(FILES lib/Slic3r/XS.pm DESTINATION ${PERL_VENDORLIB}/Slic3r)
diff --git a/xs/src/libslic3r/PrintConfig.cpp b/xs/src/libslic3r/PrintConfig.cpp
index 4077668f8..2b95ffa84 100644
--- a/xs/src/libslic3r/PrintConfig.cpp
+++ b/xs/src/libslic3r/PrintConfig.cpp
@@ -904,10 +904,17 @@ PrintConfigDef::PrintConfigDef()
     def->cli = "octoprint-apikey=s";
     def->default_value = new ConfigOptionString("");
     
+    def = this->add("octoprint_cafile", coString);
+    def->label = "HTTPS CA file";
+    def->tooltip = "Custom CA certificate file can be specified for HTTPS OctoPrint connections, in crt/pem format. "
+                   "If left blank, the default OS CA certificate repository is used.";
+    def->cli = "octoprint-cafile=s";
+    def->default_value = new ConfigOptionString("");
+
     def = this->add("octoprint_host", coString);
-    def->label = L("Host or IP");
+    def->label = L("Hostname, IP or URL");
     def->tooltip = L("Slic3r can upload G-code files to OctoPrint. This field should contain "
-                   "the hostname or IP address of the OctoPrint instance.");
+                   "the hostname, IP address or URL of the OctoPrint instance.");
     def->cli = "octoprint-host=s";
     def->default_value = new ConfigOptionString("");
 
diff --git a/xs/src/libslic3r/PrintConfig.hpp b/xs/src/libslic3r/PrintConfig.hpp
index 7d2fb0145..c589d917a 100644
--- a/xs/src/libslic3r/PrintConfig.hpp
+++ b/xs/src/libslic3r/PrintConfig.hpp
@@ -684,6 +684,7 @@ class HostConfig : public StaticPrintConfig
 public:
     ConfigOptionString              octoprint_host;
     ConfigOptionString              octoprint_apikey;
+    ConfigOptionString              octoprint_cafile;
     ConfigOptionString              serial_port;
     ConfigOptionInt                 serial_speed;
     
@@ -692,6 +693,7 @@ protected:
     {
         OPT_PTR(octoprint_host);
         OPT_PTR(octoprint_apikey);
+        OPT_PTR(octoprint_cafile);
         OPT_PTR(serial_port);
         OPT_PTR(serial_speed);
     }
diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp
index cb2ef7368..d7c9a590a 100644
--- a/xs/src/perlglue.cpp
+++ b/xs/src/perlglue.cpp
@@ -64,6 +64,7 @@ REGISTER_CLASS(PresetCollection, "GUI::PresetCollection");
 REGISTER_CLASS(PresetBundle, "GUI::PresetBundle");
 REGISTER_CLASS(PresetHints, "GUI::PresetHints");
 REGISTER_CLASS(TabIface, "GUI::Tab");
+REGISTER_CLASS(OctoPrint, "OctoPrint");
 
 SV* ConfigBase__as_hash(ConfigBase* THIS)
 {
diff --git a/xs/src/slic3r/GUI/GUI.cpp b/xs/src/slic3r/GUI/GUI.cpp
index 699f17e82..6b8613cb0 100644
--- a/xs/src/slic3r/GUI/GUI.cpp
+++ b/xs/src/slic3r/GUI/GUI.cpp
@@ -5,9 +5,9 @@
 #include <boost/algorithm/string/predicate.hpp>
 #include <boost/filesystem.hpp>
 #include <boost/lexical_cast.hpp>
-
 #include <boost/algorithm/string/split.hpp>
 #include <boost/algorithm/string/classification.hpp>
+#include <boost/format.hpp>
 
 #if __APPLE__
 #import <IOKit/pwr_mgt/IOPMLib.h>
@@ -573,4 +573,18 @@ wxString from_u8(std::string str)
 	return wxString::FromUTF8(str.c_str());
 }
 
+wxWindow *get_widget_by_id(int id)
+{
+    if (g_wxMainFrame == nullptr) {
+        throw std::runtime_error("Main frame not set");
+    }
+
+    wxWindow *window = g_wxMainFrame->FindWindow(id);
+    if (window == nullptr) {
+        throw std::runtime_error((boost::format("Could not find widget by ID: %1%") % id).str());
+    }
+
+    return window;
+}
+
 } }
diff --git a/xs/src/slic3r/GUI/GUI.hpp b/xs/src/slic3r/GUI/GUI.hpp
index c6922cebc..d9760ebf3 100644
--- a/xs/src/slic3r/GUI/GUI.hpp
+++ b/xs/src/slic3r/GUI/GUI.hpp
@@ -6,6 +6,7 @@
 #include "Config.hpp"
 
 class wxApp;
+class wxWindow;
 class wxFrame;
 class wxWindow;
 class wxMenuBar;
@@ -121,6 +122,8 @@ wxString	L_str(std::string str);
 // Return wxString from std::string in UTF8
 wxString	from_u8(std::string str);
 
+wxWindow *get_widget_by_id(int id);
+
 }
 }
 
diff --git a/xs/src/slic3r/GUI/Preset.cpp b/xs/src/slic3r/GUI/Preset.cpp
index c28c989fb..52717e1fc 100644
--- a/xs/src/slic3r/GUI/Preset.cpp
+++ b/xs/src/slic3r/GUI/Preset.cpp
@@ -224,7 +224,7 @@ const std::vector<std::string>& Preset::printer_options()
     if (s_opts.empty()) {
         s_opts = {
             "bed_shape", "z_offset", "gcode_flavor", "use_relative_e_distances", "serial_port", "serial_speed", 
-            "octoprint_host", "octoprint_apikey", "use_firmware_retraction", "use_volumetric_e", "variable_layer_height", 
+            "octoprint_host", "octoprint_apikey", "octoprint_cafile", "use_firmware_retraction", "use_volumetric_e", "variable_layer_height",
             "single_extruder_multi_material", "start_gcode", "end_gcode", "before_layer_gcode", "layer_gcode", "toolchange_gcode",
             "between_objects_gcode", "printer_notes"
         };
diff --git a/xs/src/slic3r/Utils/Bonjour.cpp b/xs/src/slic3r/Utils/Bonjour.cpp
new file mode 100644
index 000000000..6107e2c60
--- /dev/null
+++ b/xs/src/slic3r/Utils/Bonjour.cpp
@@ -0,0 +1,704 @@
+#include "Bonjour.hpp"
+
+#include <iostream>  // XXX
+#include <cstdint>
+#include <algorithm>
+#include <unordered_map>
+#include <array>
+#include <vector>
+#include <string>
+#include <random>
+#include <thread>
+#include <boost/optional.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/endian/conversion.hpp>
+#include <boost/asio.hpp>
+#include <boost/date_time/posix_time/posix_time_duration.hpp>
+#include <boost/format.hpp>
+
+using boost::optional;
+using boost::system::error_code;
+namespace endian = boost::endian;
+namespace asio = boost::asio;
+using boost::asio::ip::udp;
+
+
+// TODO: Fuzzing test (done without TXT)
+// FIXME: check char retype to unsigned
+
+
+namespace Slic3r {
+
+
+// Minimal implementation of a MDNS/DNS-SD client
+// This implementation is extremely simple, only the bits that are useful
+// for very basic MDNS discovery are present.
+
+struct DnsName: public std::string
+{
+	enum
+	{
+		MAX_RECURSION = 10,     // Keep this low
+	};
+
+	static optional<DnsName> decode(const std::vector<char> &buffer, size_t &offset, unsigned depth = 0)
+	{
+		// Check offset sanity:
+		if (offset + 1 >= buffer.size()) {
+			return boost::none;
+		}
+
+		// Check for recursion depth to prevent parsing names that are nested too deeply
+		// or end up cyclic:
+		if (depth >= MAX_RECURSION) {
+			return boost::none;
+		}
+
+		DnsName res;
+		const size_t bsize = buffer.size();
+
+		while (true) {
+			const char* ptr = buffer.data() + offset;
+			unsigned len = static_cast<unsigned char>(*ptr);
+			if (len & 0xc0) {
+				// This is a recursive label
+				unsigned len_2 = static_cast<unsigned char>(ptr[1]);
+				size_t pointer = (len & 0x3f) << 8 | len_2;
+				const auto nested = decode(buffer, pointer, depth + 1);
+				if (!nested) {
+					return boost::none;
+				} else {
+					if (res.size() > 0) {
+						res.push_back('.');
+					}
+					res.append(*nested);
+					offset += 2;
+					return std::move(res);
+				}
+			} else if (len == 0) {
+				// This is a name terminator
+				offset++;
+				break;
+			} else {
+				// This is a regular label
+				len &= 0x3f;
+				if (len + offset + 1 >= bsize) {
+					return boost::none;
+				}
+
+				res.reserve(len);
+				if (res.size() > 0) {
+					res.push_back('.');
+				}
+
+				ptr++;
+				for (const auto end = ptr + len; ptr < end; ptr++) {
+					char c = *ptr;
+					if (c >= 0x20 && c <= 0x7f) {
+						res.push_back(c);
+					} else {
+						return boost::none;
+					}
+				}
+
+				offset += len + 1;
+			}
+		}
+
+		if (res.size() > 0) {
+			return std::move(res);
+		} else {
+			return boost::none;
+		}
+	}
+};
+
+struct DnsHeader
+{
+	uint16_t id;
+	uint16_t flags;
+	uint16_t qdcount;
+	uint16_t ancount;
+	uint16_t nscount;
+	uint16_t arcount;
+
+	enum
+	{
+		SIZE = 12,
+	};
+
+	static DnsHeader decode(const std::vector<char> &buffer) {
+		DnsHeader res;
+		const uint16_t *data_16 = reinterpret_cast<const uint16_t*>(buffer.data());
+		res.id = endian::big_to_native(data_16[0]);
+		res.flags = endian::big_to_native(data_16[1]);
+		res.qdcount = endian::big_to_native(data_16[2]);
+		res.ancount = endian::big_to_native(data_16[3]);
+		res.nscount = endian::big_to_native(data_16[4]);
+		res.arcount = endian::big_to_native(data_16[5]);
+		return res;
+	}
+
+	uint32_t rrcount() const {
+		return ancount + nscount + arcount;
+	}
+};
+
+struct DnsQuestion
+{
+	enum
+	{
+		MIN_SIZE = 5,
+	};
+
+	DnsName name;
+	uint16_t type;
+	uint16_t qclass;
+
+	DnsQuestion() :
+		type(0),
+		qclass(0)
+	{}
+
+	static optional<DnsQuestion> decode(const std::vector<char> &buffer, size_t &offset)
+	{
+		auto qname = DnsName::decode(buffer, offset);
+		if (!qname) {
+			return boost::none;
+		}
+
+		DnsQuestion res;
+		res.name = std::move(*qname);
+		const uint16_t *data_16 = reinterpret_cast<const uint16_t*>(buffer.data() + offset);
+		res.type = endian::big_to_native(data_16[0]);
+		res.qclass = endian::big_to_native(data_16[1]);
+
+		offset += 4;
+		return std::move(res);
+	}
+};
+
+struct DnsResource
+{
+	DnsName name;
+	uint16_t type;
+	uint16_t rclass;
+	uint32_t ttl;
+	std::vector<char> data;
+
+	DnsResource() :
+		type(0),
+		rclass(0),
+		ttl(0)
+	{}
+
+	static optional<DnsResource> decode(const std::vector<char> &buffer, size_t &offset, size_t &dataoffset)
+	{
+		const size_t bsize = buffer.size();
+		if (offset + 1 >= bsize) {
+			return boost::none;
+		}
+
+		auto rname = DnsName::decode(buffer, offset);
+		if (!rname) {
+			return boost::none;
+		}
+
+		if (offset + 10 >= bsize) {
+			return boost::none;
+		}
+
+		DnsResource res;
+		res.name = std::move(*rname);
+		const uint16_t *data_16 = reinterpret_cast<const uint16_t*>(buffer.data() + offset);
+		res.type = endian::big_to_native(data_16[0]);
+		res.rclass = endian::big_to_native(data_16[1]);
+		res.ttl = endian::big_to_native(*reinterpret_cast<const uint32_t*>(data_16 + 2));
+		uint16_t rdlength = endian::big_to_native(data_16[4]);
+
+		offset += 10;
+		if (offset + rdlength > bsize) {
+			return boost::none;
+		}
+
+		dataoffset = offset;
+		res.data = std::move(std::vector<char>(buffer.begin() + offset, buffer.begin() + offset + rdlength));
+		offset += rdlength;
+
+		return std::move(res);
+	}
+};
+
+struct DnsRR_A
+{
+	enum { TAG = 0x1 };
+
+	asio::ip::address_v4 ip;
+
+	static void decode(optional<DnsRR_A> &result, const DnsResource &rr)
+	{
+		if (rr.data.size() == 4) {
+			DnsRR_A res;
+			const uint32_t ip = endian::big_to_native(*reinterpret_cast<const uint32_t*>(rr.data.data()));
+			res.ip = asio::ip::address_v4(ip);
+			result = std::move(res);
+		}
+	}
+};
+
+struct DnsRR_AAAA
+{
+	enum { TAG = 0x1c };
+
+	asio::ip::address_v6 ip;
+
+	static void decode(optional<DnsRR_AAAA> &result, const DnsResource &rr)
+	{
+		if (rr.data.size() == 16) {
+			DnsRR_AAAA res;
+			std::array<unsigned char, 16> ip;
+			std::copy_n(rr.data.begin(), 16, ip.begin());
+			res.ip = asio::ip::address_v6(ip);
+			result = std::move(res);
+		}
+	}
+};
+
+struct DnsRR_SRV
+{
+	enum
+	{
+		TAG = 0x21,
+		MIN_SIZE = 8,
+	};
+
+	uint16_t priority;
+	uint16_t weight;
+	uint16_t port;
+	DnsName hostname;
+
+	static optional<DnsRR_SRV> decode(const std::vector<char> &buffer, const DnsResource &rr, size_t dataoffset)
+	{
+		if (rr.data.size() < MIN_SIZE) {
+			return boost::none;
+		}
+
+		DnsRR_SRV res;
+
+		const uint16_t *data_16 = reinterpret_cast<const uint16_t*>(rr.data.data());
+		res.priority = endian::big_to_native(data_16[0]);
+		res.weight = endian::big_to_native(data_16[1]);
+		res.port = endian::big_to_native(data_16[2]);
+
+		size_t offset = dataoffset + 6;
+		auto hostname = DnsName::decode(buffer, offset);
+
+		if (hostname) {
+			res.hostname = std::move(*hostname);
+			return std::move(res);
+		} else {
+			return boost::none;
+		}
+	}
+};
+
+struct DnsRR_TXT
+{
+	enum
+	{
+		TAG = 0x10,
+	};
+
+	std::vector<std::string> values;
+
+	static optional<DnsRR_TXT> decode(const DnsResource &rr)
+	{
+		const size_t size = rr.data.size();
+		if (size < 2) {
+			return boost::none;
+		}
+
+		DnsRR_TXT res;
+
+		for (auto it = rr.data.begin(); it != rr.data.end(); ) {
+			unsigned val_size = static_cast<unsigned char>(*it);
+			if (val_size == 0 || it + val_size >= rr.data.end()) {
+				return boost::none;
+			}
+			++it;
+
+			std::string value(val_size, ' ');
+			std::copy(it, it + val_size, value.begin());
+			res.values.push_back(std::move(value));
+
+			it += val_size;
+		}
+
+		return std::move(res);
+	}
+};
+
+struct DnsSDPair
+{
+	optional<DnsRR_SRV> srv;
+	optional<DnsRR_TXT> txt;
+};
+
+struct DnsSDMap : public std::map<std::string, DnsSDPair>
+{
+	void insert_srv(std::string &&name, DnsRR_SRV &&srv)
+	{
+		auto hit = this->find(name);
+		if (hit != this->end()) {
+			hit->second.srv = std::move(srv);
+		} else {
+			DnsSDPair pair;
+			pair.srv = std::move(srv);
+			this->insert(std::make_pair(std::move(name), std::move(pair)));
+		}
+	}
+
+	void insert_txt(std::string &&name, DnsRR_TXT &&txt)
+	{
+		auto hit = this->find(name);
+		if (hit != this->end()) {
+			hit->second.txt = std::move(txt);
+		} else {
+			DnsSDPair pair;
+			pair.txt = std::move(txt);
+			this->insert(std::make_pair(std::move(name), std::move(pair)));
+		}
+	}
+};
+
+struct DnsMessage
+{
+	enum
+	{
+		MAX_SIZE = 4096,
+		MAX_ANS = 30,
+	};
+
+	DnsHeader header;
+	optional<DnsQuestion> question;
+
+	optional<DnsRR_A> rr_a;
+	optional<DnsRR_AAAA> rr_aaaa;
+	std::vector<DnsRR_SRV> rr_srv;
+
+	DnsSDMap sdmap;
+
+	static optional<DnsMessage> decode(const std::vector<char> &buffer, optional<uint16_t> id_wanted = boost::none)
+	{
+		const auto size = buffer.size();
+		if (size < DnsHeader::SIZE + DnsQuestion::MIN_SIZE || size > MAX_SIZE) {
+			return boost::none;
+		}
+
+		DnsMessage res;
+		res.header = DnsHeader::decode(buffer);
+
+		if (id_wanted && *id_wanted != res.header.id) {
+			return boost::none;
+		}
+
+		if (res.header.qdcount > 1 || res.header.ancount > MAX_ANS) {
+			return boost::none;
+		}
+
+		size_t offset = DnsHeader::SIZE;
+		if (res.header.qdcount == 1) {
+			res.question = DnsQuestion::decode(buffer, offset);
+		}
+
+		for (unsigned i = 0; i < res.header.rrcount(); i++) {
+			size_t dataoffset = 0;
+			auto rr = DnsResource::decode(buffer, offset, dataoffset);
+			if (!rr) {
+				return boost::none;
+			} else {
+				res.parse_rr(buffer, std::move(*rr), dataoffset);
+			}
+		}
+
+		return std::move(res);
+	}
+private:
+	void parse_rr(const std::vector<char> &buffer, DnsResource &&rr, size_t dataoffset)
+	{
+		switch (rr.type) {
+			case DnsRR_A::TAG: DnsRR_A::decode(this->rr_a, rr); break;
+			case DnsRR_AAAA::TAG: DnsRR_AAAA::decode(this->rr_aaaa, rr); break;
+			case DnsRR_SRV::TAG: {
+				auto srv = DnsRR_SRV::decode(buffer, rr, dataoffset);
+				if (srv) { this->sdmap.insert_srv(std::move(rr.name), std::move(*srv)); }
+				break;
+			}
+			case DnsRR_TXT::TAG: {
+				auto txt = DnsRR_TXT::decode(rr);
+				if (txt) { this->sdmap.insert_txt(std::move(rr.name), std::move(*txt)); }
+				break;
+			}
+		}
+	}
+};
+
+
+struct BonjourRequest
+{
+	static const asio::ip::address_v4 MCAST_IP4;
+	static const uint16_t MCAST_PORT;
+
+	uint16_t id;
+	std::vector<char> data;
+
+	static optional<BonjourRequest> make(const std::string &service, const std::string &protocol);
+
+private:
+	BonjourRequest(uint16_t id, std::vector<char> &&data) :
+		id(id),
+		data(std::move(data))
+	{}
+};
+
+const asio::ip::address_v4 BonjourRequest::MCAST_IP4{0xe00000fb};
+const uint16_t BonjourRequest::MCAST_PORT = 5353;
+
+optional<BonjourRequest> BonjourRequest::make(const std::string &service, const std::string &protocol)
+{
+	if (service.size() > 15 || protocol.size() > 15) {
+		return boost::none;
+	}
+
+	std::random_device dev;
+	std::uniform_int_distribution<uint16_t> dist;
+	uint16_t id = dist(dev);
+	uint16_t id_big = endian::native_to_big(id);
+	const char *id_char = reinterpret_cast<char*>(&id_big);
+
+	std::vector<char> data;
+	data.reserve(service.size() + 18);
+
+	// Add the transaction ID
+	data.push_back(id_char[0]);
+	data.push_back(id_char[1]);
+
+	// Add metadata
+	static const unsigned char rq_meta[] = {
+		0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+	};
+	std::copy(rq_meta, rq_meta + sizeof(rq_meta), std::back_inserter(data));
+
+	// Add PTR query name
+	data.push_back(service.size() + 1);
+	data.push_back('_');
+	data.insert(data.end(), service.begin(), service.end());
+	data.push_back(protocol.size() + 1);
+	data.push_back('_');
+	data.insert(data.end(), protocol.begin(), protocol.end());
+
+	// Add the rest of PTR record
+	static const unsigned char ptr_tail[] = {
+		0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00, 0x00, 0x0c, 0x00, 0xff,
+	};
+	std::copy(ptr_tail, ptr_tail + sizeof(ptr_tail), std::back_inserter(data));
+
+	return BonjourRequest(id, std::move(data));
+}
+
+
+// API - private part
+
+struct Bonjour::priv
+{
+	const std::string service;
+	const std::string protocol;
+	const std::string service_dn;
+	unsigned timeout;
+	uint16_t rq_id;
+
+	std::vector<char> buffer;
+	std::thread io_thread;
+	Bonjour::ReplyFn replyfn;
+	Bonjour::CompleteFn completefn;
+
+	priv(std::string service, std::string protocol);
+
+	void udp_receive(udp::endpoint from, size_t bytes);
+	void lookup_perform();
+};
+
+Bonjour::priv::priv(std::string service, std::string protocol) :
+	service(std::move(service)),
+	protocol(std::move(protocol)),
+	service_dn((boost::format("_%1%._%2%.local") % this->service % this->protocol).str()),
+	timeout(10),
+	rq_id(0)
+{
+	buffer.resize(DnsMessage::MAX_SIZE);
+}
+
+void Bonjour::priv::udp_receive(udp::endpoint from, size_t bytes)
+{
+	if (bytes == 0 || !replyfn) {
+		return;
+	}
+
+	buffer.resize(bytes);
+	const auto dns_msg = DnsMessage::decode(buffer, rq_id);
+	if (dns_msg) {
+		asio::ip::address ip = from.address();
+		if (dns_msg->rr_a) { ip = dns_msg->rr_a->ip; }
+		else if (dns_msg->rr_aaaa) { ip = dns_msg->rr_aaaa->ip; }
+
+		for (const auto &sdpair : dns_msg->sdmap) {
+			if (! sdpair.second.srv) {
+				continue;
+			}
+
+			const auto &srv = *sdpair.second.srv;
+			BonjourReply reply(ip, sdpair.first, srv.hostname);
+
+			if (sdpair.second.txt) {
+				static const std::string tag_path = "path=";
+				static const std::string tag_version = "version=";
+
+				for (const auto &value : sdpair.second.txt->values) {
+					if (value.size() > tag_path.size() && value.compare(0, tag_path.size(), tag_path) == 0) {
+						reply.path = value.substr(tag_path.size());
+					} else if (value.size() > tag_version.size() && value.compare(0, tag_version.size(), tag_version) == 0) {
+						reply.version = value.substr(tag_version.size());
+					}
+				}
+			}
+
+			replyfn(std::move(reply));
+		}
+	}
+}
+
+void Bonjour::priv::lookup_perform()
+{
+	const auto brq = BonjourRequest::make(service, protocol);
+	if (!brq) {
+		return;
+	}
+
+	auto self = this;
+	rq_id = brq->id;
+
+	try {
+		boost::asio::io_service io_service;
+		udp::socket socket(io_service);
+		socket.open(udp::v4());
+		socket.set_option(udp::socket::reuse_address(true));
+		udp::endpoint mcast(BonjourRequest::MCAST_IP4, BonjourRequest::MCAST_PORT);
+		socket.send_to(asio::buffer(brq->data), mcast);
+
+		bool timeout = false;
+		asio::deadline_timer timer(io_service);
+		timer.expires_from_now(boost::posix_time::seconds(10));
+		timer.async_wait([=, &timeout](const error_code &error) {
+			timeout = true;
+			if (self->completefn) {
+				self->completefn();
+			}
+		});
+
+		udp::endpoint recv_from;
+		const auto recv_handler = [&](const error_code &error, size_t bytes) {
+			if (!error) { self->udp_receive(recv_from, bytes); }
+		};
+		socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler);
+
+		while (io_service.run_one()) {
+			if (timeout) {
+				socket.cancel();
+			} else {
+				buffer.resize(DnsMessage::MAX_SIZE);
+				socket.async_receive_from(asio::buffer(buffer, buffer.size()), recv_from, recv_handler);
+			}
+		}
+	} catch (std::exception& e) {
+	}
+}
+
+
+// API - public part
+
+BonjourReply::BonjourReply(boost::asio::ip::address ip, std::string service_name, std::string hostname) :
+	ip(std::move(ip)),
+	service_name(std::move(service_name)),
+	hostname(std::move(hostname)),
+	path("/"),
+	version("Unknown")
+{}
+
+std::ostream& operator<<(std::ostream &os, const BonjourReply &reply)
+{
+	os << "BonjourReply(" << reply.ip.to_string() << ", " << reply.service_name << ", "
+		<< reply.hostname << ", " << reply.path << ", " << reply.version << ")";
+	return os;
+}
+
+Bonjour::Bonjour(std::string service, std::string protocol) :
+	p(new priv(std::move(service), std::move(protocol)))
+{}
+
+Bonjour::Bonjour(Bonjour &&other) : p(std::move(other.p)) {}
+
+Bonjour::~Bonjour()
+{
+	if (p && p->io_thread.joinable()) {
+		p->io_thread.detach();
+	}
+}
+
+Bonjour& Bonjour::set_timeout(unsigned timeout)
+{
+	if (p) { p->timeout = timeout; }
+	return *this;
+}
+
+Bonjour& Bonjour::on_reply(ReplyFn fn)
+{
+	if (p) { p->replyfn = std::move(fn); }
+	return *this;
+}
+
+Bonjour& Bonjour::on_complete(CompleteFn fn)
+{
+	if (p) { p->completefn = std::move(fn); }
+	return *this;
+}
+
+Bonjour::Ptr Bonjour::lookup()
+{
+	auto self = std::make_shared<Bonjour>(std::move(*this));
+
+	if (self->p) {
+		auto io_thread = std::thread([self](){
+				self->p->lookup_perform();
+			});
+		self->p->io_thread = std::move(io_thread);
+	}
+
+	return self;
+}
+
+
+void Bonjour::pokus()   // XXX
+{
+	auto bonjour = Bonjour("octoprint")
+		.set_timeout(15)
+		.on_reply([](BonjourReply &&reply) {
+			std::cerr << "BonjourReply: " << reply << std::endl;
+		})
+		.on_complete([](){
+			std::cerr << "MDNS lookup complete" << std::endl;
+		})
+		.lookup();
+}
+
+
+}
diff --git a/xs/src/slic3r/Utils/Bonjour.hpp b/xs/src/slic3r/Utils/Bonjour.hpp
new file mode 100644
index 000000000..285625c04
--- /dev/null
+++ b/xs/src/slic3r/Utils/Bonjour.hpp
@@ -0,0 +1,56 @@
+#ifndef slic3r_Bonjour_hpp_
+#define slic3r_Bonjour_hpp_
+
+#include <memory>
+#include <string>
+#include <functional>
+// #include <ostream>
+#include <boost/asio/ip/address.hpp>
+
+
+namespace Slic3r {
+
+
+// TODO: reply data structure
+struct BonjourReply
+{
+	boost::asio::ip::address ip;
+	std::string service_name;
+	std::string hostname;
+	std::string path;
+	std::string version;
+
+	BonjourReply(boost::asio::ip::address ip, std::string service_name, std::string hostname);
+};
+
+std::ostream& operator<<(std::ostream &, const BonjourReply &);
+
+
+/// Bonjour lookup performer
+class Bonjour : public std::enable_shared_from_this<Bonjour> {
+private:
+	struct priv;
+public:
+	typedef std::shared_ptr<Bonjour> Ptr;
+	typedef std::function<void(BonjourReply &&reply)> ReplyFn;
+	typedef std::function<void()> CompleteFn;
+
+	Bonjour(std::string service, std::string protocol = "tcp");
+	Bonjour(Bonjour &&other);
+	~Bonjour();
+
+	Bonjour& set_timeout(unsigned timeout);
+	Bonjour& on_reply(ReplyFn fn);
+	Bonjour& on_complete(CompleteFn fn);
+
+	Ptr lookup();
+
+	static void pokus();    // XXX: remove
+private:
+	std::unique_ptr<priv> p;
+};
+
+
+}
+
+#endif
diff --git a/xs/src/slic3r/Utils/Http.cpp b/xs/src/slic3r/Utils/Http.cpp
new file mode 100644
index 000000000..45a350a59
--- /dev/null
+++ b/xs/src/slic3r/Utils/Http.cpp
@@ -0,0 +1,261 @@
+#include "Http.hpp"
+
+#include <cstdlib>
+#include <functional>
+#include <thread>
+#include <iostream>
+#include <tuple>
+#include <boost/format.hpp>
+
+#include <curl/curl.h>
+
+#include "../../libslic3r/libslic3r.h"
+
+
+namespace Slic3r {
+
+
+// Private
+
+class CurlGlobalInit
+{
+	static const CurlGlobalInit instance;
+
+	CurlGlobalInit()  { ::curl_global_init(CURL_GLOBAL_DEFAULT); }
+	~CurlGlobalInit() { ::curl_global_cleanup(); }
+};
+
+struct Http::priv
+{
+	enum {
+		DEFAULT_SIZE_LIMIT = 5 * 1024 * 1024,
+	};
+
+	::CURL *curl;
+	::curl_httppost *form;
+	::curl_httppost *form_end;
+	::curl_slist *headerlist;
+	std::string buffer;
+	size_t limit;
+
+	std::thread io_thread;
+	Http::CompleteFn completefn;
+	Http::ErrorFn errorfn;
+
+	priv(const std::string &url);
+	~priv();
+
+	static size_t writecb(void *data, size_t size, size_t nmemb, void *userp);
+	std::string body_size_error();
+	void http_perform();
+};
+
+Http::priv::priv(const std::string &url) :
+	curl(::curl_easy_init()),
+	form(nullptr),
+	form_end(nullptr),
+	headerlist(nullptr)
+{
+	if (curl == nullptr) {
+		throw std::runtime_error(std::string("Could not construct Curl object"));
+	}
+
+	::curl_easy_setopt(curl, CURLOPT_URL, url.c_str());   // curl makes a copy internally
+	::curl_easy_setopt(curl, CURLOPT_USERAGENT, SLIC3R_FORK_NAME "/" SLIC3R_VERSION);
+}
+
+Http::priv::~priv()
+{
+	::curl_easy_cleanup(curl);
+	::curl_formfree(form);
+	::curl_slist_free_all(headerlist);
+}
+
+size_t Http::priv::writecb(void *data, size_t size, size_t nmemb, void *userp)
+{
+	auto self = static_cast<priv*>(userp);
+	const char *cdata = static_cast<char*>(data);
+	const size_t realsize = size * nmemb;
+
+	const size_t limit = self->limit > 0 ? self->limit : DEFAULT_SIZE_LIMIT;
+	if (self->buffer.size() + realsize > limit) {
+		// This makes curl_easy_perform return CURLE_WRITE_ERROR
+		return 0;
+	}
+
+	self->buffer.append(cdata, realsize);
+
+	return realsize;
+}
+
+std::string Http::priv::body_size_error()
+{
+	return (boost::format("HTTP body data size exceeded limit (%1% bytes)") % limit).str();
+}
+
+void Http::priv::http_perform()
+{
+	::curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
+	::curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+	::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writecb);
+	::curl_easy_setopt(curl, CURLOPT_WRITEDATA, static_cast<void*>(this));
+
+#ifndef NDEBUG
+	::curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
+#endif
+
+	if (headerlist != nullptr) {
+		::curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerlist);
+	}
+
+	if (form != nullptr) {
+		::curl_easy_setopt(curl, CURLOPT_HTTPPOST, form);
+	}
+
+	CURLcode res = ::curl_easy_perform(curl);
+	long http_status = 0;
+	::curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status);
+
+	if (res != CURLE_OK) {
+		std::string error;
+		if (res == CURLE_WRITE_ERROR) {
+			error = std::move(body_size_error());
+		} else {
+			error = ::curl_easy_strerror(res);
+		};
+
+		if (errorfn) {
+			errorfn(std::move(buffer), std::move(error), http_status);
+		}
+	} else {
+		if (completefn) {
+			completefn(std::move(buffer), http_status);
+		}
+	}
+}
+
+Http::Http(const std::string &url) : p(new priv(url)) {}
+
+
+// Public
+
+Http::Http(Http &&other) : p(std::move(other.p)) {}
+
+Http::~Http()
+{
+	if (p && p->io_thread.joinable()) {
+		p->io_thread.detach();
+	}
+}
+
+
+Http& Http::size_limit(size_t sizeLimit)
+{
+	if (p) { p->limit = sizeLimit; }
+	return *this;
+}
+
+Http& Http::header(std::string name, const std::string &value)
+{
+	if (!p) { return * this; }
+
+	if (name.size() > 0) {
+		name.append(": ").append(value);
+	} else {
+		name.push_back(':');
+	}
+	p->headerlist = curl_slist_append(p->headerlist, name.c_str());
+	return *this;
+}
+
+Http& Http::remove_header(std::string name)
+{
+	if (p) {
+		name.push_back(':');
+		p->headerlist = curl_slist_append(p->headerlist, name.c_str());
+	}
+
+	return *this;
+}
+
+Http& Http::ca_file(const std::string &name)
+{
+	if (p) {
+		::curl_easy_setopt(p->curl, CURLOPT_CAINFO, name.c_str());
+	}
+
+	return *this;
+}
+
+Http& Http::form_add(const std::string &name, const std::string &contents)
+{
+	if (p) {
+		::curl_formadd(&p->form, &p->form_end,
+			CURLFORM_COPYNAME, name.c_str(),
+			CURLFORM_COPYCONTENTS, contents.c_str(),
+			CURLFORM_END
+		);
+	}
+
+	return *this;
+}
+
+Http& Http::form_add_file(const std::string &name, const std::string &filename)
+{
+	if (p) {
+		::curl_formadd(&p->form, &p->form_end,
+			CURLFORM_COPYNAME, name.c_str(),
+			CURLFORM_FILE, filename.c_str(),
+			CURLFORM_CONTENTTYPE, "application/octet-stream",
+			CURLFORM_END
+		);
+	}
+
+	return *this;
+}
+
+Http& Http::on_complete(CompleteFn fn)
+{
+	if (p) { p->completefn = std::move(fn); }
+	return *this;
+}
+
+Http& Http::on_error(ErrorFn fn)
+{
+	if (p) { p->errorfn = std::move(fn); }
+	return *this;
+}
+
+Http::Ptr Http::perform()
+{
+	auto self = std::make_shared<Http>(std::move(*this));
+
+	if (self->p) {
+		auto io_thread = std::thread([self](){
+				self->p->http_perform();
+			});
+		self->p->io_thread = std::move(io_thread);
+	}
+
+	return self;
+}
+
+void Http::perform_sync()
+{
+	if (p) { p->http_perform(); }
+}
+
+Http Http::get(std::string url)
+{
+	return std::move(Http{std::move(url)});
+}
+
+Http Http::post(std::string url)
+{
+	Http http{std::move(url)};
+	curl_easy_setopt(http.p->curl, CURLOPT_POST, 1L);
+	return http;
+}
+
+
+}
diff --git a/xs/src/slic3r/Utils/Http.hpp b/xs/src/slic3r/Utils/Http.hpp
new file mode 100644
index 000000000..c591e17c5
--- /dev/null
+++ b/xs/src/slic3r/Utils/Http.hpp
@@ -0,0 +1,53 @@
+#ifndef slic3r_Http_hpp_
+#define slic3r_Http_hpp_
+
+#include <memory>
+#include <string>
+#include <functional>
+
+
+namespace Slic3r {
+
+
+/// Represetns a Http request
+class Http : public std::enable_shared_from_this<Http> {
+private:
+	struct priv;
+public:
+	typedef std::shared_ptr<Http> Ptr;
+	typedef std::function<void(std::string /* body */, unsigned /* http_status */)> CompleteFn;
+	typedef std::function<void(std::string /* body */, std::string /* error */, unsigned /* http_status */)> ErrorFn;
+
+	Http(Http &&other);
+
+	static Http get(std::string url);
+	static Http post(std::string url);
+	~Http();
+
+	Http(const Http &) = delete;
+	Http& operator=(const Http &) = delete;
+	Http& operator=(Http &&) = delete;
+
+	Http& size_limit(size_t sizeLimit);
+	Http& header(std::string name, const std::string &value);
+	Http& remove_header(std::string name);
+	Http& ca_file(const std::string &filename);
+	Http& form_add(const std::string &name, const std::string &contents);
+	Http& form_add_file(const std::string &name, const std::string &filename);
+
+	Http& on_complete(CompleteFn fn);
+	Http& on_error(ErrorFn fn);
+
+	Ptr perform();
+	void perform_sync();
+
+private:
+	Http(const std::string &url);
+
+	std::unique_ptr<priv> p;
+};
+
+
+}
+
+#endif
diff --git a/xs/src/slic3r/Utils/OctoPrint.cpp b/xs/src/slic3r/Utils/OctoPrint.cpp
new file mode 100644
index 000000000..58530833b
--- /dev/null
+++ b/xs/src/slic3r/Utils/OctoPrint.cpp
@@ -0,0 +1,105 @@
+#include "OctoPrint.hpp"
+
+#include <iostream>
+#include <boost/format.hpp>
+
+#include <wx/frame.h>
+#include <wx/event.h>
+
+#include "libslic3r/PrintConfig.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "Http.hpp"
+
+
+namespace Slic3r {
+
+
+OctoPrint::OctoPrint(DynamicPrintConfig *config) :
+	host(config->opt_string("octoprint_host")),
+	apikey(config->opt_string("octoprint_apikey")),
+	cafile(config->opt_string("octoprint_cafile"))
+{}
+
+std::string  OctoPrint::test() const
+{
+	// Since the request is performed synchronously here,
+	// it is ok to refer to `res` from within the closure
+	std::string res;
+
+	auto http = Http::get(std::move(make_url("api/version")));
+	set_auth(http);
+	http.on_error([&](std::string, std::string error, unsigned status) {
+			res = format_error(error, status);
+		})
+		.perform_sync();
+
+	return res;
+}
+
+void OctoPrint::send_gcode(int windowId, int completeEvt, int errorEvt, const std::string &filename, bool print) const
+{
+	auto http = Http::post(std::move(make_url("api/files/local")));
+	set_auth(http);
+	http.form_add("print", print ? "true" : "false")
+		.form_add_file("file", filename)
+		.on_complete([=](std::string body, unsigned status) {
+			wxWindow *window = GUI::get_widget_by_id(windowId);
+			wxCommandEvent* evt = new wxCommandEvent(completeEvt);
+			evt->SetString("G-code file successfully uploaded to the OctoPrint server");
+			evt->SetInt(100);
+			wxQueueEvent(window, evt);
+		})
+		.on_error([=](std::string body, std::string error, unsigned status) {
+			wxWindow *window = GUI::get_widget_by_id(windowId);
+
+			wxCommandEvent* evt_complete = new wxCommandEvent(completeEvt);
+			evt_complete->SetInt(100);
+			wxQueueEvent(window, evt_complete);
+
+			wxCommandEvent* evt_error = new wxCommandEvent(errorEvt);
+			evt_error->SetString(wxString::Format("Error while uploading to the OctoPrint server: %s", format_error(error, status)));
+			wxQueueEvent(window, evt_error);
+		})
+		.perform();
+}
+
+void OctoPrint::set_auth(Http &http) const
+{
+	http.header("X-Api-Key", apikey);
+
+	if (! cafile.empty()) {
+		http.ca_file(cafile);
+	}
+}
+
+std::string OctoPrint::make_url(const std::string &path) const
+{
+	if (host.find("http://") == 0 || host.find("https://") == 0) {
+		if (host.back() == '/') {
+			return std::move((boost::format("%1%%2%") % host % path).str());
+		} else {
+			return std::move((boost::format("%1%/%2%") % host % path).str());
+		}
+	} else {
+		return std::move((boost::format("http://%1%/%2%") % host % path).str());
+	}
+}
+
+std::string OctoPrint::format_error(std::string error, unsigned status)
+{
+	if (status != 0) {
+		std::string res{"HTTP "};
+		res.append(std::to_string(status));
+
+		if (status == 401) {
+			res.append(": Invalid API key");
+		}
+
+		return std::move(res);
+	} else {
+		return std::move(error);
+	}
+}
+
+
+}
diff --git a/xs/src/slic3r/Utils/OctoPrint.hpp b/xs/src/slic3r/Utils/OctoPrint.hpp
new file mode 100644
index 000000000..eca3baa63
--- /dev/null
+++ b/xs/src/slic3r/Utils/OctoPrint.hpp
@@ -0,0 +1,35 @@
+#ifndef slic3r_OctoPrint_hpp_
+#define slic3r_OctoPrint_hpp_
+
+#include <string>
+
+// #include "Http.hpp"    // XXX: ?
+
+namespace Slic3r {
+
+
+class DynamicPrintConfig;
+class Http;
+
+class OctoPrint
+{
+public:
+	OctoPrint(DynamicPrintConfig *config);
+
+	std::string test() const;
+	// XXX: style
+	void send_gcode(int windowId, int completeEvt, int errorEvt, const std::string &filename, bool print = false) const;
+private:
+	std::string host;
+	std::string apikey;
+	std::string cafile;
+
+	void set_auth(Http &http) const;
+	std::string make_url(const std::string &path) const;
+	static std::string format_error(std::string error, unsigned status);
+};
+
+
+}
+
+#endif
diff --git a/xs/src/xsinit.h b/xs/src/xsinit.h
index 49981b74b..96c4b74d7 100644
--- a/xs/src/xsinit.h
+++ b/xs/src/xsinit.h
@@ -59,6 +59,15 @@ extern "C" {
     #undef seek
     #undef send
     #undef write
+    #undef open
+    #undef close
+    #undef seekdir
+    #undef setbuf
+    #undef fread
+    #undef fseek
+    #undef fputc
+    #undef fwrite
+    #undef fclose
 #endif /* _MSC_VER */
 }
 #endif
diff --git a/xs/xsp/Utils_OctoPrint.xsp b/xs/xsp/Utils_OctoPrint.xsp
new file mode 100644
index 000000000..062af4e0c
--- /dev/null
+++ b/xs/xsp/Utils_OctoPrint.xsp
@@ -0,0 +1,14 @@
+%module{Slic3r::XS};
+
+%{
+#include <xsinit.h>
+#include "slic3r/Utils/OctoPrint.hpp"
+%}
+
+%name{Slic3r::OctoPrint} class OctoPrint {
+    OctoPrint(DynamicPrintConfig *config);
+    ~OctoPrint();
+
+    std::string test() const;
+    void send_gcode(int windowId, int completeEvt, int errorEvt, std::string filename, bool print = false) const;
+};
diff --git a/xs/xsp/my.map b/xs/xsp/my.map
index e54601632..87a8d8d86 100644
--- a/xs/xsp/my.map
+++ b/xs/xsp/my.map
@@ -236,6 +236,10 @@ Ref<PresetHints>    		O_OBJECT_SLIC3R_T
 TabIface*	   				O_OBJECT_SLIC3R
 Ref<TabIface> 				O_OBJECT_SLIC3R_T
 
+OctoPrint*                  O_OBJECT_SLIC3R
+Ref<OctoPrint>              O_OBJECT_SLIC3R_T
+Clone<OctoPrint>            O_OBJECT_SLIC3R_T
+
 Axis                  T_UV
 ExtrusionLoopRole     T_UV
 ExtrusionRole     T_UV