Merge branch 'octoprint'

This commit is contained in:
bubnikv 2018-03-06 11:39:44 +01:00
commit 4a90ab1f6a
15 changed files with 978 additions and 56 deletions

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -201,6 +201,10 @@ add_library(libslic3r_gui STATIC
${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
@ -340,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})
@ -529,14 +534,21 @@ find_package(CURL REQUIRED)
include_directories(${CURL_INCLUDE_DIRS})
target_link_libraries(XS ${CURL_LIBRARIES})
if (SLIC3R_STATIC AND 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})
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

View file

@ -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("");

View file

@ -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);
}

View file

@ -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)
{

View file

@ -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;
}
} }

View file

@ -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);
}
}

View file

@ -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"
};

View file

@ -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();
}
}

View file

@ -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

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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;
};

View file

@ -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