From 58e000d8427e871a423d8abc214b6e17e86223e5 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Sat, 12 Sep 2026 02:31:27 +0800 Subject: [PATCH] feat: cloud download via HTTP --- src/slic3r/Utils/OrcaCloudServiceAgent.cpp | 114 +++++++++++++++++++-- src/slic3r/Utils/OrcaCloudServiceAgent.hpp | 28 ++++- src/slic3r/Utils/OrcaPrinterAgent.cpp | 36 +++++-- 3 files changed, 153 insertions(+), 25 deletions(-) diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index d28c392d59..848d95a08a 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -1127,21 +1127,113 @@ int OrcaCloudServiceAgent::send_printer_command(const std::string& dev_id, const : BAMBU_NETWORK_ERR_CONNECT_FAILED; } -int OrcaCloudServiceAgent::send_print_job(const std::string& dev_id, - const std::string& filename, - const std::string& gcode, - bool start) +int OrcaCloudServiceAgent::upload_gcode_via_cloud(const std::string& dev_id, + const std::string& local_gcode_path, + std::string* job_id, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn) { - if (dev_id.empty() || filename.empty() || !is_user_login()) + if (dev_id.empty() || local_gcode_path.empty() || !is_user_login()) + return BAMBU_NETWORK_ERR_INVALID_HANDLE; + if (cancel_fn && cancel_fn()) + return BAMBU_NETWORK_ERR_CANCELED; + + // Step 1: POST print-jobs/uploads -> a short-lived presigned R2 PUT URL. No + // metadata rides this request; filename/start are only relevant to the HTTP + // .../start finalize route, which this MQTT-driven flow does not call. + const std::string uploads_path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/uploads"; + std::string response; + unsigned int http_code = 0; + int result = http_post(uploads_path, "{}", &response, &http_code); + if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/uploads failed http_code=" << http_code; + return BAMBU_NETWORK_ERR_CONNECT_FAILED; + } + + std::string upload_job_id; + std::string upload_url; + try { + const nlohmann::json j = nlohmann::json::parse(response); + upload_job_id = j.value("job_id", ""); + upload_url = j.value("upload_url", ""); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: failed to parse print-jobs/uploads response: " << e.what(); + return BAMBU_NETWORK_ERR_CONNECT_FAILED; + } + if (upload_job_id.empty() || upload_url.empty()) { + BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: print-jobs/uploads response missing job_id/upload_url"; + return BAMBU_NETWORK_ERR_CONNECT_FAILED; + } + + if (cancel_fn && cancel_fn()) + return BAMBU_NETWORK_ERR_CANCELED; + + // Step 2: PUT the G-code straight to R2 with the one-time URL from step 1. This + // is a scoped, PUT-only, short-TTL capability with no bearer token of its own, + // so it bypasses http_put (which always prefixes api_base_url and attaches the + // cloud session's Authorization header - neither belongs on an R2 PUT). + bool canceled = false; + unsigned put_status = 0; + std::string put_error; + Http::put(upload_url) + .tls_verify(true) + .header("Content-Type", "text/x.gcode") + .set_put_body(boost::filesystem::path(local_gcode_path)) + .timeout_connect(5) + .timeout_max(300) // large G-code over a slow link + .on_progress([&](Http::Progress progress, bool& cancel) { + if (cancel_fn && cancel_fn()) { + cancel = true; + canceled = true; + return; + } + if (update_fn && progress.ultotal > 0) { + const int percent = static_cast((progress.ulnow * 100) / progress.ultotal); + update_fn(PrintingStageUpload, percent, "Uploading..."); + } + }) + .on_complete([&](std::string, unsigned status) { put_status = status; }) + .on_error([&](std::string, std::string err, unsigned status) { + put_status = status; + put_error = std::move(err); + }) + .perform_sync(); + + if (canceled) + return BAMBU_NETWORK_ERR_CANCELED; + if (put_status < 200 || put_status >= 300) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: R2 upload failed status=" << put_status << " error=" << put_error; + return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED; + } + + if (job_id) + *job_id = std::move(upload_job_id); + return BAMBU_NETWORK_SUCCESS; +} + +int OrcaCloudServiceAgent::start_cloud_print_job(const std::string& dev_id, + const std::string& job_id, + const std::string& filename, + bool start) +{ + if (dev_id.empty() || job_id.empty() || !is_user_login()) return BAMBU_NETWORK_ERR_INVALID_HANDLE; - const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs?filename=" + - Http::url_encode(filename) + "&start=" + (start ? "true" : "false"); + nlohmann::json body; + if (!filename.empty()) + body["filename"] = filename; + body["start"] = start; + + const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/" + + Http::url_encode(job_id) + "/start"; + std::string response; unsigned int http_code = 0; - const int result = http_post(path, gcode, nullptr, &http_code, "text/plain"); - if (result != BAMBU_NETWORK_SUCCESS) - return result; - return http_code >= 200 && http_code < 300 ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; + const int result = http_post(path, body.dump(), &response, &http_code); + if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) { + BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/" << job_id << "/start failed http_code=" << http_code; + return BAMBU_NETWORK_ERR_CONNECT_FAILED; + } + return BAMBU_NETWORK_SUCCESS; } void OrcaCloudServiceAgent::enable_multi_machine(bool enable) diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp index bd62169904..ef282dc132 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp @@ -230,11 +230,29 @@ public: // that need non-blocking behaviour run it on their own thread. int send_printer_command(const std::string& dev_id, const std::string& body); - // Submit raw sliced G-code to the cloud printer print-jobs endpoint. - int send_print_job(const std::string& dev_id, - const std::string& filename, - const std::string& gcode, - bool start = true); + // Upload sliced G-code to the cloud printer's print job storage: requests a + // short-lived presigned URL (POST print-jobs/uploads), then PUTs the file + // straight to R2 with that URL. Does not start the print or wait for + // OrcaSonar to download it - see OrcaPrinterAgent::start_print/start_sdcard_print + // for the MQTT hand-off that follows. *job_id receives the id to correlate + // with that hand-off; update_fn receives upload progress via Http's on_progress. + int upload_gcode_via_cloud(const std::string& dev_id, + const std::string& local_gcode_path, + std::string* job_id, + OnUpdateStatusFn update_fn, + WasCancelledFn cancel_fn); + + // Finalize a presigned print-job upload (step 3 of 3): POST + // print-jobs//start. The gateway HEAD-verifies the object landed in + // R2, then relays a print.project_file command (download URL + filename + + // start) to the printer over the gateway's OWN cloud relay connection to + // OrcaSonar - NOT this agent's MQTT session. See + // CLOUD_PRINT_JOB_MQTT_DESIGN.md for the MQTT-native alternative this + // stands in for until the gap documented there is closed. + int start_cloud_print_job(const std::string& dev_id, + const std::string& job_id, + const std::string& filename, + bool start = true); // ======================================================================== // ICloudServiceAgent Interface Implementation - Settings Synchronization diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index 8f70ce00ef..fc6d1320ee 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -1411,6 +1411,19 @@ AgentInfo OrcaPrinterAgent::get_agent_info_static() // Print Job Operations - All Stubs // ============================================================================ +// Orchestrates the cloud print workflow: upload the sliced G-code straight to R2 +// via a presigned URL (upload_gcode_via_cloud), then finalize over HTTP +// (start_cloud_print_job). The finalize call is what actually gets the file to +// the printer: the gateway HEAD-verifies the R2 object, then relays +// print.project_file to OrcaSonar over the gateway's OWN cloud relay connection - +// not this agent's MQTT session. See CLOUD_PRINT_JOB_MQTT_DESIGN.md for the +// MQTT-native alternative (publishing print.project_file directly over this +// agent's own cloud connection) and why it isn't used yet. +// +// This deliberately does NOT go through start_sdcard_print/print.gcode_file: +// that command is the generic "start this file already on the printer" primitive +// (also used by the LAN start_local_print path, and meant to stay that way as it +// grows params like filament mapping), and has no download-awareness to give it. int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) { (void) wait_fn; @@ -1426,28 +1439,33 @@ int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn return BAMBU_NETWORK_ERR_INVALID_HANDLE; const std::string local_path = resolve_local_gcode_path(params); - const fs::path source(local_path); - boost::filesystem::ifstream input(source, std::ios::in | std::ios::binary); - if (!input) { + boost::system::error_code ec; + if (!fs::exists(local_path, ec) || !fs::is_regular_file(local_path, ec)) { BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: G-code file does not exist: " << local_path; return BAMBU_NETWORK_ERR_FILE_NOT_EXIST; } - const std::string gcode((std::istreambuf_iterator(input)), std::istreambuf_iterator()); - if (input.bad()) { - BOOST_LOG_TRIVIAL(error) << "OrcaPrinterAgent: failed to read G-code file: " << local_path; - return BAMBU_NETWORK_ERR_PRINT_WR_POST_TASK_FAILED; - } if (cancel_fn && cancel_fn()) return BAMBU_NETWORK_ERR_CANCELED; if (update_fn) update_fn(PrintingStageUpload, 0, "Uploading G-code..."); - const int result = cloud->send_print_job(params.dev_id, remote_gcode_name(params), gcode, true); + std::string job_id; + int result = cloud->upload_gcode_via_cloud(params.dev_id, local_path, &job_id, update_fn, cancel_fn); if (result != BAMBU_NETWORK_SUCCESS) return result; + if (cancel_fn && cancel_fn()) + return BAMBU_NETWORK_ERR_CANCELED; + + if (update_fn) + update_fn(PrintingStageSending, 0, "Starting print..."); + + const int start_rc = cloud->start_cloud_print_job(params.dev_id, job_id, remote_gcode_name(params), /*start=*/true); + if (start_rc != BAMBU_NETWORK_SUCCESS) + return start_rc; + if (update_fn) update_fn(PrintingStageFinished, 100, "Print started");