feat: cloud download via HTTP

This commit is contained in:
peachismomo
2026-09-12 02:31:27 +08:00
parent af6be5858e
commit 58e000d842
3 changed files with 153 additions and 25 deletions

View File

@@ -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<int>((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)

View File

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

View File

@@ -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<char>(input)), std::istreambuf_iterator<char>());
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");