Compare commits

...

5 Commits

Author SHA1 Message Date
SoftFever
50c518f379 fix: force push empty setting_id preset on 409 tombstone 2026-06-10 22:01:53 +08:00
Ian Chua
4dce803df4 fix: generate setting id if it is empty for 409 tombstone 2026-06-09 21:24:29 +08:00
Ian Chua
6bd30977c5 fix: add resolution for undefined conflicts 2026-06-09 18:12:32 +08:00
Ian Chua
dedd878af1 fix: tombstone resolution for 409 status code with error code -3 2026-06-09 16:41:58 +08:00
Ian Bassi
c1f3125f75 Qidi q2 start gcode Update (#14111)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-06-08 16:47:47 -03:00
8 changed files with 85 additions and 34 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "Qidi",
"version": "02.04.00.01",
"version": "02.04.00.02",
"force_update": "0",
"description": "Qidi configurations",
"machine_model_list": [

View File

@@ -48,7 +48,7 @@
"20"
],
"machine_pause_gcode": "PAUSE",
"machine_start_gcode": "INIT_MAPPING_VALUE\nPRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature] EXTRUDER=[initial_no_support_extruder]\nSET_PRINT_STATS_INFO TOTAL_LAYER=[total_layer_count]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nM141 S[chamber_temperature]\nG4 P3000\nT[initial_tool]\nG1 X108.000 Y1 F30000\nG0 Z[initial_layer_print_height] F600\n;G1 E3 F1800\nG90\nM83\nG0 X128 E8 F{outer_wall_volumetric_speed/(24/20) * 60}\nG0 X133 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG0 X138 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5) * 60}\nG0 X143 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG0 X148 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5) * 60}\nG0 X153 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG91\nG1 X1 Z-0.300\nG1 X4\nG1 Z1 F1200\nG90\nM400\nG1 X108.000 Y2.5 F30000\nG0 Z[initial_layer_print_height] F600\nM83\nG0 X128 E10 F{outer_wall_volumetric_speed/(24/20) * 60}\nG0 X133 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG0 X138 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5) * 60}\nG0 X143 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG0 X148 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5) * 60}\nG0 X153 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG91\nG1 X1 Z-0.300\nG1 X4\nG1 Z1 F1200\nG90\nM400\nG1 Z1 F600",
"machine_start_gcode": "INIT_MAPPING_VALUE\nPRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature] EXTRUDER=[initial_no_support_extruder]\nSET_PRINT_STATS_INFO TOTAL_LAYER=[total_layer_count]\nM83\nT[initial_tool]\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nM141 S[chamber_temperature]\nG4 P3000\nG1 X108 Y1 F30000\nG0 Z[initial_layer_print_height] F600\nG0 X128 E8 F{outer_wall_volumetric_speed/(24/20) * 60}\nG0 X133 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG0 X138 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5) * 60}\nG0 X143 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG0 X148 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5) * 60}\nG0 X153 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG1 X154 Z-0.1\nG1 X158\nG1 Z1 F600\nG1 X108 Y2.5 F30000\nG0 Z[initial_layer_print_height] F600\nG0 X128 E10 F{outer_wall_volumetric_speed/(24/20) * 60}\nG0 X133 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG0 X138 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5) * 60}\nG0 X143 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG0 X148 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5) * 60}\nG0 X153 E.3742 F{outer_wall_volumetric_speed/(0.3*0.5)/4 * 60}\nG1 X154 Z-0.1\nG1 X158\nG1 Z1 F600",
"machine_unload_filament_time": "35",
"nozzle_diameter": [
"0.4"

View File

@@ -4940,6 +4940,7 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
// Parse the conflict body to extract the error code and server profile id
int conflict_code = 0;
std::string conflict_setting_id;
std::string conflict_preset_name;
try {
json conflict_body = json::parse(body_str);
if (conflict_body.contains("code"))
@@ -4947,21 +4948,40 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
if (conflict_body.contains("server_profile") && conflict_body["server_profile"].contains("id")
&& conflict_body["server_profile"]["id"].is_string())
conflict_setting_id = conflict_body["server_profile"]["id"].get<std::string>();
// The local preset name is injected into the conflict body by the agent (sync_push),
// since the server response itself omits it for tombstone (-3) conflicts.
if (conflict_body.contains("name") && conflict_body["name"].is_string())
conflict_preset_name = conflict_body["name"].get<std::string>();
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << "Failed to parse 409 conflict body.";
}
// Capture the user id up front so the force-push closure does not have to touch m_agent.
std::string conflict_user_id = m_agent ? m_agent->get_user_id() : std::string();
auto* plater = wxGetApp().plater();
if (plater != nullptr && wxGetApp().imgui()->display_initialized()) {
std::string text;
if (conflict_code == -1) {
switch (conflict_code) {
case -1:
text = _u8L("Cloud sync conflict: this preset has a newer version in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
} else {
break;
case -2:
text = _u8L("Cloud sync conflict: a preset with this name already exists in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
}
break;
case -3:
text = _u8L("Cloud sync conflict: a preset with the same name was previously deleted from the cloud.\n"
"Delete will delete your local preset. Force push overwrites it with your local preset.");
break;
default:
text = _u8L("Cloud sync conflict: there was an unexpected or unidentified preset conflict.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
break;
};
plater->get_notification_manager()->push_orca_sync_conflict_notification(
text,
text, conflict_code,
[this](wxEvtHandler*) {
// Runs on the GUI thread (on_http_error is a queued wx event); restart_sync_user_preset()
// already joins the old sync thread off the UI thread, so no extra thread is needed here.
@@ -4971,7 +4991,7 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
restart_sync_user_preset();
return true;
},
[this, conflict_setting_id](wxEvtHandler*) {
[this, conflict_setting_id, conflict_preset_name, conflict_user_id](wxEvtHandler*) {
if (mainframe == nullptr)
return false;
MessageDialog
@@ -4981,7 +5001,13 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
if (dlg.ShowModal() != wxID_YES)
return false;
force_push_conflicting_preset(conflict_setting_id);
std::string setting_id = conflict_setting_id;
if (setting_id.empty()) {
setting_id = OrcaCloudServiceAgent::generate_uuid_for_setting_id(conflict_preset_name, conflict_user_id);
BOOST_LOG_TRIVIAL(info) << "conflict setting id empty, generated one: " << setting_id;
}
force_push_conflicting_preset(setting_id, conflict_preset_name);
return true;
});
}
@@ -7044,7 +7070,7 @@ void GUI_App::restart_sync_user_preset()
}).detach();
}
void GUI_App::force_push_conflicting_preset(const std::string& setting_id)
void GUI_App::force_push_conflicting_preset(const std::string& setting_id, const std::string& preset_name)
{
if (setting_id.empty() || !preset_bundle)
return;
@@ -7059,11 +7085,17 @@ void GUI_App::force_push_conflicting_preset(const std::string& setting_id)
// "update" so the next push-sync re-includes it and consumes the queued force flag.
// (We must NOT pull from the cloud here as the Pull path does — that would overwrite
// the local changes the user is trying to force-push.)
// For a -3 tombstone on a newly created preset the on-disk setting_id is EMPTY (it only
// gets assigned after a successful first push), so match by name and stamp the generated
// setting_id onto the preset — otherwise sync_with_lock's `id == preset.setting_id` check
// never fires and the force-push silently no-ops.
PresetCollection* collections[] = {&preset_bundle->prints, &preset_bundle->filaments, &preset_bundle->printers};
for (PresetCollection* coll : collections) {
for (const Preset& preset : coll->get_presets()) {
if (preset.setting_id == setting_id && preset.sync_info == "hold") {
coll->set_sync_info_and_save(preset.name, preset.setting_id, "update", 0);
const bool id_match = !preset.setting_id.empty() && preset.setting_id == setting_id;
const bool name_match = preset.setting_id.empty() && !preset_name.empty() && preset.name == preset_name;
if ((id_match || name_match) && preset.sync_info == "hold") {
coll->set_sync_info_and_save(preset.name, setting_id, "update", 0);
break;
}
}

View File

@@ -539,7 +539,7 @@ public:
void restart_sync_user_preset();
// Resolve a cloud sync 409 by force-pushing the conflicting preset: clears the "hold"
// state the conflict left behind and queues it to be re-uploaded with force=true.
void force_push_conflicting_preset(const std::string& setting_id);
void force_push_conflicting_preset(const std::string& setting_id, const std::string& preset_name);
void on_stealth_mode_enter();
// Bundle subscription sync

View File

@@ -2416,7 +2416,7 @@ void NotificationManager::OrcaSyncConflictNotification::render_text(ImGuiWrapper
}
const float action_y = starting_y + m_endlines.size() * shift_y;
const std::string pull_text = _u8L("Pull");
const std::string pull_text = conflict_code == -3 ? _u8L("Delete") : _u8L("Pull");
render_hyperlink_action(imgui, x_offset, action_y, pull_text, "##orca_sync_pull",
[this] { if (m_pull_callback && m_pull_callback(m_evt_handler)) close(); });
if (m_force_push_callback) {
@@ -2437,13 +2437,14 @@ void NotificationManager::push_shared_profiles_notification(const std::string& e
}
void NotificationManager::push_orca_sync_conflict_notification(const std::string& text,
int conflict_code,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback)
{
close_notification_of_type(NotificationType::OrcaSyncConflict);
NotificationData data{ NotificationType::OrcaSyncConflict, NotificationLevel::WarningNotificationLevel, 0, text };
push_notification_data(std::make_unique<NotificationManager::OrcaSyncConflictNotification>(
data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback)), 0);
data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback), conflict_code), 0);
}
void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback)

View File

@@ -279,6 +279,7 @@ public:
// Shared profiles available for selected printer
void push_shared_profiles_notification(const std::string& explore_url);
void push_orca_sync_conflict_notification(const std::string& text,
int conflict_code,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback);
@@ -905,10 +906,12 @@ private:
public:
OrcaSyncConflictNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback)
std::function<bool(wxEvtHandler*)> force_push_callback,
int conflict_code)
: PopNotification(n, id_provider, evt_handler)
, m_pull_callback(std::move(pull_callback))
, m_force_push_callback(std::move(force_push_callback))
, conflict_code(conflict_code)
{
m_multiline = true;
}
@@ -920,6 +923,7 @@ private:
std::function<bool(wxEvtHandler*)> m_pull_callback;
std::function<bool(wxEvtHandler*)> m_force_push_callback;
int conflict_code;
};
class SlicingProgressNotification;

View File

@@ -101,24 +101,6 @@ std::string resolve_display_name(
return username;
}
std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "")
{
if (name.empty()) {
return "";
}
// Mix user_id into the hashed input so two different users generating a setting_id
// for an identically-named preset get distinct UUIDs. Without this, the cloud's ID
// space collides across accounts and the second user's create gets HTTP 409 with
// server_profile=null on every sync (the foreign owner's record is not exposed).
static const boost::uuids::uuid orca_namespace =
boost::uuids::string_generator()("f47ac10b-58cc-4372-a567-0e02b2c3d479");
boost::uuids::name_generator_sha1 gen(orca_namespace);
boost::uuids::uuid id = user_id.empty() ? gen(name) : gen(user_id + "/" + name);
return boost::uuids::to_string(id);
}
std::string base64url_encode(const std::vector<unsigned char>& data)
{
std::string out;
@@ -412,6 +394,24 @@ OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
}
}
std::string OrcaCloudServiceAgent::generate_uuid_for_setting_id(const std::string& name, const std::string& user_id)
{
if (name.empty()) {
return "";
}
// Mix user_id into the hashed input so two different users generating a setting_id
// for an identically-named preset get distinct UUIDs. Without this, the cloud's ID
// space collides across accounts and the second user's create gets HTTP 409 with
// server_profile=null on every sync (the foreign owner's record is not exposed).
static const boost::uuids::uuid orca_namespace =
boost::uuids::string_generator()("f47ac10b-58cc-4372-a567-0e02b2c3d479");
boost::uuids::name_generator_sha1 gen(orca_namespace);
boost::uuids::uuid id = user_id.empty() ? gen(name) : gen(user_id + "/" + name);
return boost::uuids::to_string(id);
}
void OrcaCloudServiceAgent::configure_urls(AppConfig* app_config)
{
if (!app_config) return;
@@ -1250,8 +1250,10 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
if (http_code == 409) {
// Conflict - parse server version
nlohmann::json err_body;
try {
auto json = nlohmann::json::parse(response);
err_body = json;
if (json.is_null()) {
result.server_deleted = true;
} else {
@@ -1261,6 +1263,13 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0);
}
} catch (...) {}
// Surface the conflict via the http-error callback with the local preset name injected.
// The raw server body omits the name for tombstone (-3) conflicts (server_profile is null),
// but the GUI needs it to regenerate the deterministic setting_id for a force push.
if (!err_body.is_object())
err_body = nlohmann::json::object();
err_body["name"] = name;
invoke_http_error_callback(409, err_body.dump());
result.error_message = response;
return result;
}
@@ -1937,7 +1946,10 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
if (response_body) *response_body = res.body;
if (http_code) *http_code = res.status;
if (!suppress && (!res.success || res.status >= 400)) {
// 409 is a push-only domain conflict; sync_push re-fires the error callback with the
// local preset name injected (the raw server body omits it for tombstone conflicts),
// so skip the generic nameless auto-fire here to avoid a duplicate, nameless event.
if (!suppress && (!res.success || res.status >= 400) && res.status != 409) {
invoke_http_error_callback(res.status, res.body);
}

View File

@@ -300,6 +300,8 @@ public:
bool set_user_session(const nlohmann::json& session_json, bool notify_login = true);
void clear_session();
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
private:
// Sync protocol helpers
int sync_pull(