feat: initial draft of lifecycle events API for plugins (#15293)

# Description

This PR introduces lifecycle events to the plugin API. 
For all plugin capabilities, you can define a `on_lifecycle_event`
function in the plugin that takes in a event enum and a small payload
for some generic information on the lifecycle event.
The idea is to keep the payload generic and small, and if you want to
get more information, you should invoke other more targeted APIs to get
more information.
For example, lets say you are keeping track of the the `ObjectAdded*`
event hook for model transformation, addition or deletion. The payload
would tell you the name of the model, and you should use a targeted API
such as `orca.host.plater().model()` to get more information on the
model. This is the overall design principle of the API.

Currently the lifecycle events are the following:
```cpp
    enum class LifecycleEvent {
        // Project (3mf)
        NewProject,
        ProjectOpened,
        ProjectBeforeSave,
        ProjectAfterSave,
        ProjectClosed,
        ProjectDirtyChanged,

        // Slicing pipeline
        SliceStarted,
        SliceGeometryFinished,
        GCodeExportStarted,
        GCodeExportFinished,
        SlicingJobComplete,

        // Plate/model editing
        ObjectAdded,
        ObjectDeleted,
        ObjectTransformed,
        ObjectChanged,
        ObjectRenamed,
        PlateCreated,
        PlateDeleted,
        PlateSelected,
        PlateRenamed,

        // Preset
        PresetSelected,
        PresetSaved,

        // Printer/device
        PrintStateChanged,
        DeviceOnlineChanged,
        DeviceDiscovered,
        DeviceSelected,
        DeviceConnected,
        DeviceDisconnected,
        UploadStarted,
        UploadFinished,

        // Print/send jobs
        PrintJobStarted,
        PrintJobFinished,
        SendJobStarted,
        SendJobFinished,
    };
```

This is an initial draft and lifecycle events can be included later on.

[orca_telegram_notifier_plugin_any.py](https://github.com/user-attachments/files/31220973/orca_telegram_notifier_plugin_any.py)

If you're familiar with telegram bots, after you install the telegram
bot, in the config of this plugin, you can enter the Bot ID and the Chat
ID with said bot.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

For this plugin, I am testing it with a telegram bot that sends me a
message on lifecycle event.


<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
Ian Chua
2026-09-23 12:52:08 +08:00
committed by GitHub
32 changed files with 1047 additions and 61 deletions
+51 -5
View File
@@ -8,6 +8,7 @@
#include "I18N.hpp"
#include "GCode.hpp"
#include "Exception.hpp"
#include "LifecycleEvents.hpp"
#include "ExtrusionEntity.hpp"
#include "EdgeGrid.hpp"
#include "Geometry/ConvexHull.hpp"
@@ -2476,6 +2477,15 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
m_writer.set_is_bbl_machine(print->is_BBL_printer());
print->set_started(psGCodeExport);
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
}
// check if any custom gcode contains keywords used by the gcode processor to
// produce time estimation and gcode toolpaths
std::vector<std::pair<std::string, std::string>> validation_res = DoExport::validate_custom_gcode(*print);
@@ -2511,12 +2521,23 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
m_processor.set_print(print);
GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor);
if (! file.is_open()) {
BOOST_LOG_TRIVIAL(error) << std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n" << std::endl;
std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n";
BOOST_LOG_TRIVIAL(error) << err_msg << std::endl;
if (!fs::exists(folder)) {
//fs::create_directory(folder);
BOOST_LOG_TRIVIAL(error) << "the parent path " + folder.string() +" is not there!!!" << std::endl;
std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!";
BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl;
err_msg += add_err_msg;
}
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n");
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + err_msg;
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(err_msg);
}
try {
@@ -2527,11 +2548,19 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
boost::nowide::remove(path_tmp.c_str());
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n");
}
} catch (std::exception & /* ex */) {
} catch (std::exception &ex) {
// Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown.
// Close and remove the file.
file.close();
boost::nowide::remove(path_tmp.c_str());
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + ex.what();
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw;
}
file.close();
@@ -2637,6 +2666,14 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
std::error_code ret = rename_file(path_tmp, path);
if (ret) {
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(
std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' +
"Is " + path_tmp + " locked?" + '\n');
@@ -2647,7 +2684,16 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info();
print->set_done(psGCodeExport);
{
LifecycleEventContext ctx;
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
// Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT /
// M486), which is driven by exclude_object for every printer
if(result != nullptr)
+249
View File
@@ -0,0 +1,249 @@
#pragma once
// LifecycleEvents.hpp
// --------------------
// Application lifecycle events (project, slicing, plate editing, preset, printer connection, and
// job activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to
// observe. Lives in libslic3r rather than the plugin layer because some events fire from inside
// the slicing engine itself; see fire_lifecycle_event() below.
#include <functional>
#include <condition_variable>
#include <cstddef>
#include <mutex>
#include <string>
#include <utility>
namespace Slic3r
{
enum class LifecycleEvent {
// Project (3mf)
NewProject,
ProjectOpened,
ProjectBeforeSave,
ProjectAfterSave,
ProjectClosed,
ProjectDirtyChanged,
// Slicing pipeline
SliceStarted,
SliceGeometryFinished,
GCodeExportStarted,
GCodeExportFinished,
SlicingJobComplete,
// Plate/model editing
ObjectAdded,
ObjectDeleted,
ObjectTransformed,
ObjectChanged,
ObjectRenamed,
PlateCreated,
PlateDeleted,
PlateSelected,
PlateRenamed,
// Preset
PresetSelected,
PresetSaved,
// Printer/device
PrintStateChanged,
DeviceOnline,
DeviceOffline,
DeviceDiscovered,
DeviceSelected,
UploadStarted,
UploadFinished,
// Print/send jobs
PrintJobStarted,
PrintJobFinished,
SendJobStarted,
SendJobFinished,
};
// Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with
// Windows macros (wingdi.h) as unqualified names.
enum class LifecycleEvtCode { Ok, Error, Warn };
struct LifecycleEventContext
{
// The primary subject identifier for the event. This is event-specific (for example, a
// project/output path, preset name, device id, or object name), must not contain status or
// prose, and may be empty when the event has no single subject.
std::string name;
// Outcome of the operation represented by the event. For state-change and start events,
// Ok means that the event occurred; it does not imply that a future operation succeeded.
LifecycleEvtCode code = LifecycleEvtCode::Ok;
// Optional human-readable detail or diagnostic text. It is not a stable parsing contract;
// machine-readable data should be represented by a dedicated field or event instead.
std::string msg;
// Stable subject/object identifier, when the source model provides one.
std::string id;
// Previous value for rename and other before/after events.
std::string previous_name;
// Device identifier for printer and job events.
std::string device_id;
// Job identifier when the originating queue/task provides one.
std::string job_id;
// Source subsystem or operation detail, suitable for filtering but not guaranteed to be
// exhaustive across versions.
std::string source;
// Plate, object, or volume index when the source uses an index rather than a stable id.
int index = -1;
// Aggregate project dirty state for ProjectDirtyChanged.
bool dirty = false;
// Optional host-side cancellation probe. Background slicing and G-code export events set
// this to the originating Print's cancellation state so dispatch can stop before calling
// the next capability. It is intentionally not exposed through the Python payload API.
std::function<bool()> cancellation_check;
};
inline std::string lifecycle_event_to_string(LifecycleEvent event)
{
switch (event) {
case LifecycleEvent::NewProject: return "NewProject";
case LifecycleEvent::ProjectOpened: return "ProjectOpened";
case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave";
case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave";
case LifecycleEvent::ProjectClosed: return "ProjectClosed";
case LifecycleEvent::ProjectDirtyChanged: return "ProjectDirtyChanged";
case LifecycleEvent::SliceStarted: return "SliceStarted";
case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished";
case LifecycleEvent::GCodeExportStarted: return "GCodeExportStarted";
case LifecycleEvent::GCodeExportFinished: return "GCodeExportFinished";
case LifecycleEvent::SlicingJobComplete: return "SlicingJobComplete";
case LifecycleEvent::ObjectAdded: return "ObjectAdded";
case LifecycleEvent::ObjectDeleted: return "ObjectDeleted";
case LifecycleEvent::ObjectTransformed: return "ObjectTransformed";
case LifecycleEvent::ObjectChanged: return "ObjectChanged";
case LifecycleEvent::ObjectRenamed: return "ObjectRenamed";
case LifecycleEvent::PlateCreated: return "PlateCreated";
case LifecycleEvent::PlateDeleted: return "PlateDeleted";
case LifecycleEvent::PlateSelected: return "PlateSelected";
case LifecycleEvent::PlateRenamed: return "PlateRenamed";
case LifecycleEvent::PresetSelected: return "PresetSelected";
case LifecycleEvent::PresetSaved: return "PresetSaved";
case LifecycleEvent::PrintStateChanged: return "PrintStateChanged";
case LifecycleEvent::DeviceOnline: return "DeviceOnline";
case LifecycleEvent::DeviceOffline: return "DeviceOffline";
case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered";
case LifecycleEvent::DeviceSelected: return "DeviceSelected";
case LifecycleEvent::UploadStarted: return "UploadStarted";
case LifecycleEvent::UploadFinished: return "UploadFinished";
case LifecycleEvent::PrintJobStarted: return "PrintJobStarted";
case LifecycleEvent::PrintJobFinished: return "PrintJobFinished";
case LifecycleEvent::SendJobStarted: return "SendJobStarted";
case LifecycleEvent::SendJobFinished: return "SendJobFinished";
default: return "Unknown";
}
}
inline std::string lifecycle_evt_code_to_string(LifecycleEvtCode code)
{
switch (code) {
case LifecycleEvtCode::Ok: return "Ok";
case LifecycleEvtCode::Error: return "Error";
case LifecycleEvtCode::Warn: return "Warn";
default: return "Unknown";
}
}
// Global cross-layer seam (mirrors ConfigBase::set_resolve_capability_fn): any libslic3r code can
// fire a lifecycle event without depending on the plugin layer above it, which installs the
// dispatcher here at startup. Not tied to Print/GCode specifically, since nothing here should
// require callers to hold a Print& just to report an event.
using LifecycleHookFn = std::function<void(LifecycleEvent, const LifecycleEventContext&)>;
namespace detail {
struct LifecycleHookState
{
std::mutex mutex;
std::condition_variable cv;
LifecycleHookFn fn;
std::size_t active_dispatches = 0;
bool accepting = false;
};
inline LifecycleHookState& lifecycle_hook_state()
{
static LifecycleHookState state;
return state;
}
class LifecycleDispatchGuard
{
public:
explicit LifecycleDispatchGuard(LifecycleHookState& state) : m_state(state) {}
~LifecycleDispatchGuard()
{
std::lock_guard<std::mutex> lock(m_state.mutex);
--m_state.active_dispatches;
if (m_state.active_dispatches == 0)
m_state.cv.notify_all();
}
LifecycleDispatchGuard(const LifecycleDispatchGuard&) = delete;
LifecycleDispatchGuard& operator=(const LifecycleDispatchGuard&) = delete;
private:
LifecycleHookState& m_state;
};
} // namespace detail
// Installing a hook starts accepting dispatches. Passing an empty function stops accepting
// new dispatches, detaches the hook, and waits for callbacks already in progress to finish.
// This is used during plugin shutdown so plugin code cannot be unloaded while a lifecycle
// callback is still executing. The empty-function path must not be called from inside the
// lifecycle callback itself.
inline void set_lifecycle_hook_fn(LifecycleHookFn fn)
{
detail::LifecycleHookState& state = detail::lifecycle_hook_state();
if (fn) {
std::lock_guard<std::mutex> lock(state.mutex);
state.fn = std::move(fn);
state.accepting = true;
return;
}
std::unique_lock<std::mutex> lock(state.mutex);
state.accepting = false;
state.fn = nullptr;
state.cv.wait(lock, [&state] { return state.active_dispatches == 0; });
}
inline void fire_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx)
{
detail::LifecycleHookState& state = detail::lifecycle_hook_state();
LifecycleHookFn fn;
{
std::lock_guard<std::mutex> lock(state.mutex);
if (!state.accepting || !state.fn)
return;
fn = state.fn;
++state.active_dispatches;
}
detail::LifecycleDispatchGuard guard(state);
fn(event, ctx);
}
}
+10
View File
@@ -47,6 +47,7 @@
#include <boost/log/trivial.hpp>
#include "libslic3r.h"
#include "LifecycleEvents.hpp"
#include "Utils.hpp"
#include "Time.hpp"
#include "PlaceholderParser.hpp"
@@ -2972,6 +2973,7 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
// 1) Find the preset with a new_name or create a new one,
// initialize it with the edited config.
auto it = this->find_preset_internal(new_name);
const bool preset_existed = (it != m_presets.end() && it->name == new_name);
if (it != m_presets.end() && it->name == new_name) {
// Preset with the same name found.
Preset &preset = *it;
@@ -3079,6 +3081,14 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
this->get_selected_preset().save(&(parent_preset->config));
else
this->get_selected_preset().save(nullptr);
{
LifecycleEventContext ctx;
ctx.name = new_name;
ctx.msg = preset_existed ? "overwrite" : "new";
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::PresetSaved, ctx);
}
}
// A detached standalone preset for the Full Publish receiver: create a user preset holding
+44 -1
View File
@@ -14,6 +14,7 @@
#include "Flow.hpp"
#include "Geometry/ConvexHull.hpp"
#include "I18N.hpp"
#include "LifecycleEvents.hpp"
#include "ShortestPath.hpp"
#include "Thread.hpp"
#include "Time.hpp"
@@ -2694,6 +2695,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (m_objects.empty())
return;
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
}
for (PrintObject *obj : m_objects)
obj->clear_shared_object();
@@ -3312,6 +3321,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
BOOST_LOG_TRIVIAL(info) << "Slicing process finished." << log_memory_info();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
}
}
// G-code export process, running at a background thread.
@@ -4911,6 +4928,15 @@ void Print::set_gcode_file_invalidated()
//BBS: add gcode file preload logic
void Print::export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb)
{
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
}
try {
GCodeProcessor processor;
GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
@@ -4930,13 +4956,30 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
*result = std::move(processor.extract_result());
result->filament_change_sequence = filament_seq_loaded;
result->nozzle_change_sequence = nozzle_seq_loaded;
} catch (std::exception & /* ex */) {
} catch (std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = file + "\n" + ex.what();
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(
std::string("Failed to process the G-code file ") + file + " from previous 3mf\n");
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str();
{
LifecycleEventContext ctx;
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
}
std::tuple<float, float> Print::object_skirt_offset(double margin_height) const
+25 -2
View File
@@ -10,6 +10,7 @@
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Time.hpp"
@@ -355,7 +356,10 @@ namespace Slic3r
obj->bind_state = "free";
obj->last_alive = Slic3r::Utils::get_current_time_utc();
obj->m_is_online = true;
// Route through set_online_state() (rather than writing m_is_online directly) so the
// DeviceOnline lifecycle event fires consistently; same effective value/behavior
// here since the object was already online in the common case.
obj->set_online_state(true);
obj->set_dev_name(dev_name);
/* if (!obj->dev_ip.empty()) {
Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip);
@@ -373,6 +377,10 @@ namespace Slic3r
obj->bind_sec_link = sec_link;
obj->dev_connection_name = connection_name;
obj->bind_ssdp_version = ssdp_version;
// Discovery establishes the initial reachability state. Do not report it as an
// online transition; DeviceDiscovered below is the lifecycle event for a new
// device. Subsequent updates route through set_online_state(), so a known device
// still emits DeviceOnline/DeviceOffline when its reachability actually changes.
obj->m_is_online = true;
//load access code
@@ -389,6 +397,15 @@ namespace Slic3r
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " New Machine, dev_id= " << dev_id
<< ", ip = " << dev_ip <<", printer_name = " << dev_name
<< ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state;
// First discovery of a genuinely new device (not a periodic SSDP/heartbeat update to
// an already-known one, which is handled in the branch above).
{
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx);
}
}
update_local_machine(*obj);
}
@@ -984,8 +1001,14 @@ namespace Slic3r
}
void DeviceManager::OnSelectedMachineChanged(const std::string& /*pre_dev_id*/,
const std::string& /*new_dev_id*/)
const std::string& new_dev_id)
{
{
LifecycleEventContext ctx;
ctx.name = new_dev_id; // empty string is a valid deselection
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceSelected, ctx);
}
if (MachineObject* obj_ = get_selected_machine()) {
GUI::wxGetApp().sidebar().update_sync_status(obj_);
if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription)
+42 -21
View File
@@ -5,6 +5,7 @@
#include "libslic3r/Time.hpp"
#include "libslic3r/Thread.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "GuiColor.hpp"
@@ -2626,7 +2627,15 @@ void MachineObject::reset()
void MachineObject::set_print_state(std::string status)
{
const bool changed = (print_status != status);
print_status = status;
if (changed) {
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = print_status;
fire_lifecycle_event(LifecycleEvent::PrintStateChanged, ctx);
}
}
int MachineObject::connect(bool use_openssl)
@@ -2648,7 +2657,10 @@ int MachineObject::connect(bool use_openssl)
int MachineObject::disconnect()
{
if (m_agent) {
return m_agent->disconnect_printer();
const int result = m_agent->disconnect_printer();
if (result == 0)
set_online_state(false);
return result;
}
return -1;
}
@@ -2678,8 +2690,16 @@ bool MachineObject::is_connecting()
void MachineObject::set_online_state(bool on_off)
{
const bool changed = (m_is_online != on_off);
m_is_online = on_off;
if (!on_off) m_active_state = NotActive;
if (changed) {
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = on_off ? "online" : "offline";
fire_lifecycle_event(on_off ? LifecycleEvent::DeviceOnline : LifecycleEvent::DeviceOffline, ctx);
}
}
bool MachineObject::is_info_ready(bool check_version) const
@@ -2806,13 +2826,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
parse_msg_count++;
std::chrono::system_clock::time_point clock_start = std::chrono::system_clock::now();
this->set_online_state(true);
std::chrono::system_clock::time_point curr_time = std::chrono::system_clock::now();
auto diff1 = std::chrono::duration_cast<std::chrono::microseconds>(curr_time - last_update_time);
/* update last received time */
last_update_time = std::chrono::system_clock::now();
json j_pre;
bool parse_ok = false;
@@ -2825,8 +2838,29 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
/* post process payload */
sanitizeToUtf8(payload);
BOOST_LOG_TRIVIAL(info) << "parse_json: sanitize to utf8";
try {
j_pre = json::parse(payload);
parse_ok = true;
}
catch (...) {}
}
bool client_disconnected = false;
if (parse_ok && j_pre.is_object() && j_pre.contains("event") && j_pre["event"].is_object() &&
j_pre["event"].contains("event") && j_pre["event"]["event"].is_string()) {
client_disconnected = j_pre["event"]["event"].get<std::string>() == "client.disconnected";
}
// A disconnect notification is a transport message too, but it must not first mark an
// already-offline device as online through the generic message-received path.
set_online_state(!client_disconnected);
std::chrono::system_clock::time_point curr_time = std::chrono::system_clock::now();
auto diff1 = std::chrono::duration_cast<std::chrono::microseconds>(curr_time - last_update_time);
/* update last received time */
last_update_time = std::chrono::system_clock::now();
try {
bool restored_json = false;
json j;
@@ -4597,19 +4631,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
}
}
// event info
try {
if (j.contains("event")) {
if (j["event"].contains("event")) {
if (j["event"]["event"].get<std::string>() == "client.disconnected")
set_online_state(false);
else if (j["event"]["event"].get<std::string>() == "client.connected")
set_online_state(true);
}
}
}
catch (...) {}
if (!key_field_only) {
BOOST_LOG_TRIVIAL(trace) << "parse_json m_active_state =" << m_active_state;
parse_state_changed_event();
+1
View File
@@ -897,6 +897,7 @@ public:
bool is_connected();
bool is_connecting();
// Emits DeviceOnline or DeviceOffline only when the reachability state changes.
void set_online_state(bool on_off);
bool is_online() { return m_is_online; }
bool is_info_ready(bool check_version = true) const;
+28 -3
View File
@@ -43,6 +43,7 @@
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
#include "slic3r/Utils/MacDarkMode.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include <slic3r/GUI/GUI_Utils.hpp>
@@ -5028,8 +5029,16 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
//BBS: nofity object list to update
wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items();
if (object_moved)
if (object_moved) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "moved";
if (done.size() == 1)
ctx.name = m_model->objects[done.begin()->first]->name;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED));
}
// BBS: support wipe-tower for multi-plates
for (int plate_id = 0; plate_id < wipe_tower_origins.size(); plate_id++) {
@@ -5150,8 +5159,16 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
//BBS: nofity object list to update
wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items();
if (!done.empty())
if (!done.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "rotated";
if (done.size() == 1)
ctx.name = m_model->objects[done.begin()->first]->name;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED));
}
m_dirty = true;
}
@@ -5242,8 +5259,16 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
//BBS: notify object info update
wxGetApp().plater()->show_object_info();
if (!done.empty())
if (!done.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "scaled";
if (done.size() == 1)
ctx.name = m_model->objects[done.begin()->first]->name;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED));
}
m_dirty = true;
}
+8
View File
@@ -2182,6 +2182,8 @@ void GUI_App::init_networking_callbacks()
obj->command_get_access_code();
if (m_agent)
m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
obj->set_online_state(true);
}
});
});
@@ -2220,6 +2222,8 @@ void GUI_App::init_networking_callbacks()
obj->command_get_version();
event.SetInt(0);
event.SetString(obj->get_dev_id());
obj->set_online_state(true);
} else if (state == ConnectStatus::ConnectStatusFailed) {
// Orca: only update status if same device id
if (m_device_manager->selected_machine != dev_id) return;
@@ -2235,10 +2239,14 @@ void GUI_App::init_networking_callbacks()
wxGetApp().show_dialog(text);
}
event.SetInt(-1);
obj->set_online_state(false);
} else if (state == ConnectStatus::ConnectStatusLost) {
m_device_manager->set_selected_machine("");
event.SetInt(-1);
BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost";
obj->set_online_state(false);
} else {
event.SetInt(-1);
BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state;
+39 -1
View File
@@ -1,5 +1,6 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "GUI_ObjectList.hpp"
#include "GUI_Factories.hpp"
//#include "GUI_ObjectLayers.hpp"
@@ -10,6 +11,7 @@
#include "BitmapComboBox.hpp"
#include "MainFrame.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "OptionsGroup.hpp"
#include "Tab.hpp"
@@ -1159,17 +1161,39 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const
if (m_objects_model->GetItemType(item) & itObject) {
std::string name = m_objects_model->GetName(item).ToUTF8().data();
if (obj->name != name) {
const std::string previous_name = obj->name;
obj->name = name;
// if object has just one volume, rename this volume too
if (obj->volumes.size() == 1)
obj->volumes[0]->name = obj->name;
Slic3r::save_object_mesh(*obj);
LifecycleEventContext ctx;
ctx.name = name;
ctx.previous_name = previous_name;
ctx.id = std::to_string(obj->id().id);
ctx.index = obj_idx;
ctx.source = "object";
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
}
return;
}
if (volume_id < 0) return;
obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data();
std::string name = m_objects_model->GetName(item).ToUTF8().data();
if (obj->volumes[volume_id]->name == name)
return;
const std::string previous_name = obj->volumes[volume_id]->name;
obj->volumes[volume_id]->name = name;
LifecycleEventContext ctx;
ctx.name = name;
ctx.previous_name = previous_name;
ctx.id = std::to_string(obj->volumes[volume_id]->id().id);
ctx.index = volume_id;
ctx.source = "volume";
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
}
void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const
@@ -3519,7 +3543,14 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx)
obj->delete_connectors();
if (obj->volumes.empty() || !obj->has_solid_mesh()) {
const std::string deleted_obj_name = obj->name;
model.delete_object(idx);
{
Slic3r::LifecycleEventContext ctx;
ctx.name = deleted_obj_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
}
m_objects_model->Delete(m_objects_model->GetItemById(idx));
continue;
}
@@ -4088,6 +4119,13 @@ void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed,
const auto item = m_objects_model->AddObject(model_object, warning_bitmap, model_object->is_cut());
Expand(m_objects_model->GetParent(item));
{
Slic3r::LifecycleEventContext ctx;
ctx.name = model_object->name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectAdded, ctx);
}
if (!do_info_update)
return;
+8
View File
@@ -13,6 +13,7 @@
#include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/GUI/format.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "libnest2d/common.hpp"
@@ -698,6 +699,13 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
}
{
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "arranged";
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
}
m_plater->update();
// BBS
//wxGetApp().obj_manipul()->set_dirty();
+8
View File
@@ -6,6 +6,7 @@
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "libnest2d/common.hpp"
#include <numeric>
@@ -347,6 +348,13 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr)
m_plater->arrange();
}
m_plater->update();
{
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "arranged";
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
}
}
m_plater->mark_plate_toolbar_image_dirty();
+7
View File
@@ -5,6 +5,7 @@
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "libslic3r/PresetBundle.hpp"
@@ -254,6 +255,12 @@ void OrientJob::finalize(bool canceled, std::exception_ptr &eptr)
mesh.apply();
}
if (!m_selected.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
ctx.msg = "auto_oriented";
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx);
}
m_plater->update();
+39 -4
View File
@@ -1,4 +1,5 @@
#include "PrintJob.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/MTUtils.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/PresetBundle.hpp"
@@ -152,6 +153,18 @@ void PrintJob::process(Ctl &ctl)
int result = -1;
std::string http_body;
const auto mark_lifecycle_started = [this]() {
if (m_lifecycle_started)
return;
LifecycleEventContext start_ctx;
start_ctx.name = m_project_name;
start_ctx.device_id = m_dev_id;
start_ctx.source = "print_job";
fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx);
m_lifecycle_started = true;
};
int total_plate_num = plate_data.plate_count;
if (!plate_data.is_valid) {
total_plate_num = m_plater->get_partplate_list().get_plate_count();
@@ -173,7 +186,7 @@ void PrintJob::process(Ctl &ctl)
}
}
m_project_name = truncate_string(m_project_name, 100);
const std::string transport_project_name = truncate_string(m_project_name, 100);
int curr_plate_idx = 0;
if (m_print_type == "from_normal") {
@@ -372,11 +385,11 @@ void PrintJob::process(Ctl &ctl)
}
}
if (params.preset_name.empty() && m_print_type == "from_normal") { params.preset_name = wxString::Format("%s_plate_%d", m_project_name, curr_plate_idx).ToStdString(); }
if (params.project_name.empty()) {params.project_name = m_project_name;}
if (params.preset_name.empty() && m_print_type == "from_normal") { params.preset_name = wxString::Format("%s_plate_%d", transport_project_name, curr_plate_idx).ToStdString(); }
if (params.project_name.empty()) {params.project_name = transport_project_name;}
if (m_is_calibration_task) {
params.project_name = m_project_name;
params.project_name = transport_project_name;
params.origin_model_id = "";
}
@@ -543,6 +556,7 @@ void PrintJob::process(Ctl &ctl)
if (m_print_type == "from_sdcard_view") {
BOOST_LOG_TRIVIAL(info) << "print_job: try to send with cloud, model is sdcard view";
ctl.update_status(curr_percent, _u8L("Sending print job through cloud service"));
mark_lifecycle_started();
result = m_agent->start_sdcard_print(params, update_fn, cancel_fn);
} else if (params.connection_type != "lan") {
if (params.dev_ip.empty())
@@ -566,6 +580,7 @@ void PrintJob::process(Ctl &ctl)
BOOST_LOG_TRIVIAL(info) << "print_job: use ftp send print only";
ctl.update_status(curr_percent, _u8L("Sending print job over LAN"));
is_try_lan_mode = true;
mark_lifecycle_started();
result = m_agent->start_local_print_with_record(params, update_fn, cancel_fn, wait_fn);
if (result < 0) {
error_text = wxString::Format(_L("Access code:%s IP address:%s"), params.password, params.dev_ip);
@@ -582,6 +597,7 @@ void PrintJob::process(Ctl &ctl)
// try to send local with record
BOOST_LOG_TRIVIAL(info) << "print_job: try to start local print with record";
ctl.update_status(curr_percent, _u8L("Sending print job over LAN"));
mark_lifecycle_started();
result = m_agent->start_local_print_with_record(params, update_fn, cancel_fn, wait_fn);
if (result == 0) {
params.comments = "";
@@ -597,18 +613,21 @@ void PrintJob::process(Ctl &ctl)
// try to send with cloud
BOOST_LOG_TRIVIAL(warning) << "print_job: try to send with cloud";
ctl.update_status(curr_percent, _u8L("Sending print job through cloud service"));
// Started was already emitted before the local attempt.
result = m_agent->start_print(params, update_fn, cancel_fn, wait_fn);
}
}
else {
BOOST_LOG_TRIVIAL(info) << "print_job: send with cloud";
ctl.update_status(curr_percent, _u8L("Sending print job through cloud service"));
mark_lifecycle_started();
result = m_agent->start_print(params, update_fn, cancel_fn, wait_fn);
}
}
} else {
if (this->could_emmc_print) {
ctl.update_status(curr_percent, _u8L("Sending print job over LAN"));
mark_lifecycle_started();
result = m_agent->start_local_print(params, update_fn, cancel_fn);
} else {
switch(this->sdcard_state) {
@@ -619,6 +638,7 @@ void PrintJob::process(Ctl &ctl)
if(this->has_sdcard) {
// means the storage is abnormal but can be used option is enabled
ctl.update_status(curr_percent, _u8L("Sending print job over LAN, but the Storage in the printer is abnormal and print-issues may be caused by this."));
mark_lifecycle_started();
result = m_agent->start_local_print(params, update_fn, cancel_fn);
break;
}
@@ -629,6 +649,7 @@ void PrintJob::process(Ctl &ctl)
return;
case DevStorage::SdcardState::HAS_SDCARD_NORMAL:
ctl.update_status(curr_percent, _u8L("Sending print job over LAN"));
mark_lifecycle_started();
result = m_agent->start_local_print(params, update_fn, cancel_fn);
break;
default:
@@ -686,6 +707,7 @@ void PrintJob::process(Ctl &ctl)
}
wxQueueEvent(m_plater, evt);
m_job_finished = true;
m_lifecycle_success = true;
}
}
@@ -698,6 +720,19 @@ void PrintJob::finalize(bool canceled, std::exception_ptr &eptr) {
eptr = std::current_exception();
}
if (m_lifecycle_started && !m_lifecycle_finished) {
LifecycleEventContext finish_ctx;
finish_ctx.name = m_project_name;
finish_ctx.device_id = m_dev_id;
finish_ctx.source = "print_job";
finish_ctx.code = canceled ? LifecycleEvtCode::Warn :
(eptr || !m_lifecycle_success ? LifecycleEvtCode::Error : LifecycleEvtCode::Ok);
finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" :
(m_lifecycle_success ? "" : "failed"));
fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx);
m_lifecycle_finished = true;
}
if (canceled || eptr)
return;
}
+3
View File
@@ -43,6 +43,9 @@ class PrintJob : public Job
std::function<void()> m_success_fun{nullptr};
std::string m_dev_id;
bool m_job_finished{ false };
bool m_lifecycle_started{ false };
bool m_lifecycle_finished{ false };
bool m_lifecycle_success{ false };
int m_print_job_completed_id = 0;
wxString m_completed_evt_data;
std::function<void()> m_enter_ip_address_fun_fail{ nullptr };
+21
View File
@@ -1,4 +1,5 @@
#include "SendJob.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "libslic3r/MTUtils.hpp"
@@ -148,6 +149,13 @@ void SendJob::process(Ctl &ctl)
}
}
LifecycleEventContext start_ctx;
start_ctx.name = m_project_name;
start_ctx.device_id = m_dev_id;
start_ctx.source = "send_job";
fire_lifecycle_event(LifecycleEvent::SendJobStarted, start_ctx);
m_lifecycle_started = true;
int total_plate_num = m_plater->get_partplate_list().get_plate_count();
PartPlate* plate = m_plater->get_partplate_list().get_plate(job_data.plate_idx);
@@ -424,6 +432,19 @@ void SendJob::finalize(bool canceled, std::exception_ptr &eptr)
eptr = std::current_exception();
}
if (m_lifecycle_started && !m_lifecycle_finished) {
LifecycleEventContext finish_ctx;
finish_ctx.name = m_project_name;
finish_ctx.device_id = m_dev_id;
finish_ctx.source = "send_job";
finish_ctx.code = canceled ? LifecycleEvtCode::Warn :
(eptr ? LifecycleEvtCode::Error : (m_job_finished ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error));
finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" :
(m_job_finished ? "" : "failed"));
fire_lifecycle_event(LifecycleEvent::SendJobFinished, finish_ctx);
m_lifecycle_finished = true;
}
if (canceled || eptr)
return;
}
+2
View File
@@ -22,6 +22,8 @@ class SendJob : public Job
PrintPrepareData job_data;
std::string m_dev_id;
bool m_job_finished{ false };
bool m_lifecycle_started{ false };
bool m_lifecycle_finished{ false };
int m_print_job_completed_id = 0;
bool m_is_check_mode{false};
bool m_check_and_continue{false};
+34 -1
View File
@@ -29,6 +29,7 @@
#include "libslic3r/Tesselate.hpp"
#include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
@@ -2626,11 +2627,20 @@ void PartPlate::set_plate_name(const std::string& name)
if (boost::equals(m_name, name))
return;
const std::string previous_name = m_name;
m_name = name;
if (m_print != nullptr)
m_print->set_plate_name(name);
invalidate_plate_name_texture();
if (m_plater != nullptr && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = name;
ctx.previous_name = previous_name;
ctx.index = m_plate_index;
fire_lifecycle_event(LifecycleEvent::PlateRenamed, ctx);
}
}
//get the print's object, result and index
@@ -4741,9 +4751,16 @@ int PartPlateList::create_plate(bool adjust_position)
if (m_plater) {
// In GUI mode
wxGetApp().obj_list()->on_plate_added(plate);
wxGetApp().obj_list()->on_plate_added(plate);
}
if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = plate->get_plate_name();
ctx.index = new_index;
fire_lifecycle_event(LifecycleEvent::PlateCreated, ctx);
}
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":created a new plate %1%") % new_index;
return new_index;
}
@@ -4842,6 +4859,7 @@ int PartPlateList::delete_plate(int index)
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate %1%, has an invalid index %2%") % index % plate->get_index();
return -1;
}
const std::string plate_name = plate->get_plate_name();
if (m_plater) {
// In GUI mode
@@ -4924,6 +4942,13 @@ int PartPlateList::delete_plate(int index)
delete plate;
if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = plate_name;
ctx.index = index;
fire_lifecycle_event(LifecycleEvent::PlateDeleted, ctx);
}
// FIX: context of BackgroundSliceProcess and gcode preview need to be updated before ObjectList::reload_all_plates().
#if 0
if (m_plater != nullptr) {
@@ -5032,6 +5057,7 @@ int PartPlateList::select_plate(int index)
if (m_plate_list.empty() || index >= m_plate_list.size()) {
return -1;
}
const int previous_index = m_current_plate;
// BBS: erase unnecessary snapshot
if (get_curr_plate_index() != index && m_intialized) {
@@ -5058,6 +5084,13 @@ int PartPlateList::select_plate(int index)
//wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT));
}
if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = m_plate_list[index]->get_plate_name();
ctx.index = index;
fire_lifecycle_event(LifecycleEvent::PlateSelected, ctx);
}
return 0;
}
+105 -13
View File
@@ -57,6 +57,7 @@
#include <wx/aui/aui.h>
#include "libslic3r/libslic3r.h"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/Format/STL.hpp"
#include "libslic3r/Format/DRC.hpp"
#include "libslic3r/Format/STEP.hpp"
@@ -10264,7 +10265,14 @@ void Plater::priv::remove(size_t obj_idx)
view3D->enable_layers_editing(false);
m_worker.cancel_all();
std::string obj_name = (obj_idx < model.objects.size()) ? model.objects[obj_idx]->name : std::to_string(obj_idx);
model.delete_object(obj_idx);
{
Slic3r::LifecycleEventContext ctx;
ctx.name = obj_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
}
//BBS: notify partplate the instance removed
partplate_list.notify_instance_removed(obj_idx, -1);
update();
@@ -10297,7 +10305,14 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia
if (obj->is_cut())
sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx);
std::string obj_name = obj->name;
model.delete_object(obj_idx);
{
Slic3r::LifecycleEventContext ctx;
ctx.name = obj_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
}
//BBS: notify partplate the instance removed
partplate_list.notify_instance_removed(obj_idx, -1);
@@ -10343,10 +10358,22 @@ void Plater::priv::delete_all_objects_from_model()
void Plater::priv::reset(bool apply_presets_change)
{
// TakeSnapshot below and load_current_presets() further down each re-evaluate the
// aggregate dirty flag against a baseline that hasn't been reset yet, so they can toggle
// is_dirty() back and forth several times before it settles; coalesce those into one event.
ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(dirty_state);
Plater::TakeSnapshot snapshot(q, _u8L("Reset Project"), UndoRedo::SnapshotType::ProjectSeparator);
clear_warnings();
const std::string closed_project_name = into_u8(get_project_filename());
if (!closed_project_name.empty() || !model.objects.empty()) {
Slic3r::LifecycleEventContext ctx;
ctx.name = closed_project_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectClosed, ctx);
}
// A new project must not inherit the previous project's published selection (Feature A/B).
m_has_pending_published = false;
m_pending_published_keys.clear();
@@ -12774,11 +12801,15 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
notification_manager->set_slicing_progress_export_possible();
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
const std::string lifecycle_job_name = this->background_process.fff_print() ?
this->background_process.fff_print()->output_filename() : std::string();
this->background_process.reset_export();
// This bool stops showing export finished notification even when process_completed_with_error is false
bool has_error = false;
std::string lifecycle_error_msg;
if (evt.error()) {
auto message = evt.format_error_message();
lifecycle_error_msg = message.first;
if (evt.critical_error()) {
if (q->m_tracking_popup_menu) {
// We don't want to pop-up a message box when tracking a pop-up menu.
@@ -12816,6 +12847,14 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
is_finished = true;
}
{
Slic3r::LifecycleEventContext ctx;
ctx.name = lifecycle_job_name;
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
}
//BBS: set the current plater's slice result to valid
if (!this->background_process.empty())
this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success());
@@ -15349,21 +15388,33 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_
//get_partplate_list().reinit();
//get_partplate_list().update_slice_context_to_current_plate(p->background_process);
//p->preview->update_gcode_result(p->partplate_list.get_current_slice_result());
reset(transfer_preset_changes);
reset_project_dirty_after_save();
reset_project_dirty_initial_presets();
wxGetApp().update_saved_preset_from_current_preset();
update_project_dirty_from_presets();
if (!silent) {
Slic3r::LifecycleEventContext ctx;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::NewProject, ctx);
}
{
// Same rationale as in Plater::priv::reset(): the whole reset + preset-reload +
// baseline-reset sequence below settles into its final dirty state only once it
// completes, so hold notifications until then to avoid firing on transient flips.
ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(p->dirty_state);
//reset project
p->project.reset();
//set project name
if (project_name.empty())
p->set_project_name(_L("Untitled"));
else
p->set_project_name(project_name);
reset(transfer_preset_changes);
reset_project_dirty_after_save();
reset_project_dirty_initial_presets();
wxGetApp().update_saved_preset_from_current_preset();
update_project_dirty_from_presets();
Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator);
//reset project
p->project.reset();
//set project name
if (project_name.empty())
p->set_project_name(_L("Untitled"));
else
p->set_project_name(project_name);
Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator);
}
Model m;
model().load_from(m); // new id avoid same path name
@@ -15481,6 +15532,13 @@ void Plater::load_project(wxString const& filename2,
p->set_project_name(_L("Untitled"));
}
{
Slic3r::LifecycleEventContext ctx;
ctx.name = into_u8(load_restore ? originfile : filename);
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectOpened, ctx);
}
} else {
if (using_exported_file()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename;
@@ -15552,11 +15610,23 @@ int Plater::save_project(bool saveAs)
if (full_pathnames) {
save_strategy = save_strategy | SaveStrategy::FullPathSources;
}
{
Slic3r::LifecycleEventContext ctx;
ctx.name = into_u8(filename);
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectBeforeSave, ctx);
}
if (export_3mf(into_path(filename), save_strategy) < 0) {
MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."),
_L("Save project"), wxOK | wxICON_WARNING).ShowModal();
return wxID_CANCEL;
}
{
Slic3r::LifecycleEventContext ctx;
ctx.name = into_u8(filename);
ctx.code = Slic3r::LifecycleEvtCode::Ok;
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectAfterSave, ctx);
}
Slic3r::remove_backup(model(), false);
@@ -20889,6 +20959,14 @@ void Plater::changed_object(ModelObject &object){
// Check outside bed
get_current_canvas3D()->requires_check_outside_state();
if (!is_loading_project()) {
LifecycleEventContext ctx;
ctx.name = object.name;
ctx.id = std::to_string(object.id().id);
ctx.source = "geometry";
fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx);
}
}
void Plater::changed_object(int obj_idx)
@@ -20925,6 +21003,20 @@ void Plater::changed_objects(const std::vector<size_t>& object_idxs)
// update print
this->p->schedule_background_process();
if (!is_loading_project()) {
for (size_t obj_idx : object_idxs) {
if (obj_idx >= p->model.objects.size() || p->model.objects[obj_idx] == nullptr)
continue;
LifecycleEventContext ctx;
ctx.name = p->model.objects[obj_idx]->name;
ctx.id = std::to_string(p->model.objects[obj_idx]->id().id);
ctx.index = static_cast<int>(obj_idx);
ctx.source = "geometry";
fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx);
}
}
}
void Plater::schedule_background_process(bool schedule/* = true*/)
+46 -1
View File
@@ -6,6 +6,7 @@
#include "MainFrame.hpp"
#include "I18N.hpp"
#include "Plater.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include <boost/algorithm/string/predicate.hpp>
@@ -17,13 +18,23 @@ namespace GUI {
void ProjectDirtyStateManager::update_from_undo_redo_stack(bool dirty)
{
const bool was_dirty = is_dirty();
m_plater_dirty = dirty;
notify_dirty_change(was_dirty, "undo_redo");
if (const Plater *plater = wxGetApp().plater(); plater && wxGetApp().initialized())
wxGetApp().mainframe->update_title();
}
void ProjectDirtyStateManager::set_plater_dirty(bool is_dirty)
{
const bool was_dirty = this->is_dirty();
m_plater_dirty = is_dirty;
notify_dirty_change(was_dirty, "plater");
}
void ProjectDirtyStateManager::update_from_presets()
{
const bool was_dirty = is_dirty();
m_presets_dirty = false;
// check switching of the presets only for exist/loaded project, but not for new
GUI_App &app = wxGetApp();
@@ -45,18 +56,53 @@ void ProjectDirtyStateManager::update_from_presets()
}
m_presets_dirty |= app.has_unsaved_preset_changes();
m_project_config_dirty = m_initial_project_config != app.preset_bundle->project_config;
notify_dirty_change(was_dirty, "presets");
app.mainframe->update_title();
}
void ProjectDirtyStateManager::reset_after_save()
{
const bool was_dirty = is_dirty();
this->reset_initial_presets();
m_plater_dirty = false;
m_presets_dirty = false;
m_project_config_dirty = false;
notify_dirty_change(was_dirty, "save");
wxGetApp().mainframe->update_title();
}
void ProjectDirtyStateManager::notify_dirty_change(bool was_dirty, const char *source)
{
if (m_suppress_depth > 0)
return;
const bool dirty = is_dirty();
if (was_dirty == dirty)
return;
LifecycleEventContext ctx;
ctx.code = LifecycleEvtCode::Ok;
ctx.dirty = dirty;
ctx.source = source;
fire_lifecycle_event(LifecycleEvent::ProjectDirtyChanged, ctx);
}
void ProjectDirtyStateManager::begin_suppress_notifications()
{
if (m_suppress_depth == 0)
m_suppress_entry_dirty = is_dirty();
++m_suppress_depth;
}
void ProjectDirtyStateManager::end_suppress_notifications()
{
assert(m_suppress_depth > 0);
if (m_suppress_depth > 0)
--m_suppress_depth;
if (m_suppress_depth == 0)
notify_dirty_change(m_suppress_entry_dirty, "batch");
}
void ProjectDirtyStateManager::reset_initial_presets()
{
m_initial_presets.fill(std::string{});
@@ -168,4 +214,3 @@ void ProjectDirtyStateManager::render_debug_window() const
} // namespace GUI
} // namespace Slic3r
+22 -1
View File
@@ -14,21 +14,42 @@ public:
void reset_after_save();
void reset_initial_presets();
void set_plater_dirty(bool is_dirty) { m_plater_dirty = is_dirty; }
void set_plater_dirty(bool is_dirty);
bool is_dirty() const { return m_plater_dirty || m_project_config_dirty || m_presets_dirty; }
bool is_presets_dirty() const { return m_presets_dirty; }
// RAII guard coalescing dirty-state updates: while any guard is alive, ProjectDirtyChanged
// notifications are held back; when the outermost guard is destroyed, at most one
// notification fires, reflecting only the net change across the whole guarded scope.
class NotificationSuppressor
{
public:
explicit NotificationSuppressor(ProjectDirtyStateManager &owner) : m_owner(owner) { m_owner.begin_suppress_notifications(); }
~NotificationSuppressor() { m_owner.end_suppress_notifications(); }
NotificationSuppressor(const NotificationSuppressor &) = delete;
NotificationSuppressor &operator=(const NotificationSuppressor &) = delete;
private:
ProjectDirtyStateManager &m_owner;
};
#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
void render_debug_window() const;
#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
private:
void notify_dirty_change(bool was_dirty, const char *source);
void begin_suppress_notifications();
void end_suppress_notifications();
// Does the Undo / Redo stack indicate the project is dirty?
bool m_plater_dirty { false };
// Do the presets indicate the project is dirty?
bool m_presets_dirty { false };
// Is the project config dirty?
bool m_project_config_dirty { false };
// NotificationSuppressor nesting depth and the dirty state observed when the outermost guard began.
int m_suppress_depth { 0 };
bool m_suppress_entry_dirty { false };
// Keeps track of preset names selected at the time of last project save.
std::array<std::string, Preset::TYPE_COUNT> m_initial_presets;
DynamicPrintConfig m_initial_project_config;
+15
View File
@@ -38,6 +38,7 @@
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "slic3r/Utils/PresetUpdater.hpp"
#include "slic3r/plugin/PluginConfig.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "Plater.hpp"
#include "MainFrame.hpp"
#include "format.hpp"
@@ -6964,6 +6965,20 @@ bool Tab::select_preset(
}
load_current_preset();
{
Slic3r::LifecycleEventContext ctx;
ctx.name = preset_name;
ctx.code = Slic3r::LifecycleEvtCode::Ok;
switch (m_type) {
case Preset::TYPE_PRINT: ctx.msg = "print"; break;
case Preset::TYPE_SLA_PRINT: ctx.msg = "sla_print"; break;
case Preset::TYPE_FILAMENT: ctx.msg = "filament"; break;
case Preset::TYPE_SLA_MATERIAL: ctx.msg = "sla_material"; break;
case Preset::TYPE_PRINTER: ctx.msg = "printer"; break;
default: break;
}
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::PresetSelected, ctx);
}
if (delete_third_printer) {
wxGetApp().CallAfter([filament_presets, process_presets]() {
+34 -4
View File
@@ -1,10 +1,13 @@
#include "TaskManager.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "nlohmann/json.hpp"
#include "MainFrame.hpp"
#include "GUI_App.hpp"
#include <exception>
using namespace nlohmann;
namespace Slic3r {
@@ -214,17 +217,34 @@ int TaskManager::schedule(TaskStateInfo* task)
boost::thread* new_sending_thread = new boost::thread();
*new_sending_thread = Slic3r::create_thread(
[this, task] {
// Keep both lifecycle callbacks on this per-task worker thread. Plugin observers can
// therefore associate Started and Finished for one task with a single execution context.
LifecycleEventContext start_ctx;
start_ctx.name = task->params().project_name;
start_ctx.device_id = task->params().dev_id;
start_ctx.job_id = std::to_string(task->task_info_id);
start_ctx.source = "task_manager";
fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx);
int result = -1;
if (!m_agent) {
BOOST_LOG_TRIVIAL(trace) << "task_manager: NetworkAgent is nullptr";
return;
}
assert(m_agent);
else {
assert(m_agent);
try {
// DEBUG FOR TEST
#if 0
int result = start_print_test(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn);
result = start_print_test(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn);
#else
int result = m_agent->start_print(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn);
result = m_agent->start_print(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn);
#endif
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "task_manager: start_print threw: " << ex.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "task_manager: start_print threw an unknown exception";
}
}
if (result == 0) {
last_sent_timestamp = std::chrono::system_clock::now();
task->set_sent_time(last_sent_timestamp);
@@ -237,6 +257,16 @@ int TaskManager::schedule(TaskStateInfo* task)
task->set_state(TaskState::TS_SEND_CANCELED);
}
}
LifecycleEventContext finish_ctx;
finish_ctx.name = task->params().project_name;
finish_ctx.device_id = task->params().dev_id;
finish_ctx.job_id = std::to_string(task->task_info_id);
finish_ctx.source = "task_manager";
finish_ctx.code = result == 0 ? LifecycleEvtCode::Ok :
(task->is_canceled() ? LifecycleEvtCode::Warn : LifecycleEvtCode::Error);
finish_ctx.msg = result == 0 ? "" : (task->is_canceled() ? "cancelled" : "failed");
fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx);
/* remove from sending task list */
m_scedule_mutex.lock();
+18
View File
@@ -24,6 +24,7 @@
#include "CrealityPrint.hpp"
#include "../GUI/PrintHostDialogs.hpp"
#include "../GUI/MainFrame.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "Obico.hpp"
#include "Flashforge.hpp"
#include "SimplyPrint.hpp"
@@ -360,12 +361,29 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
{
emit_progress(0); // Indicate the upload is starting
// Captured before upload_data is moved into upload() below.
const std::string upload_filename = the_job.upload_data.source_path.filename().string();
{
LifecycleEventContext ctx;
ctx.name = upload_filename;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx);
}
bool success = the_job.printhost->upload(std::move(the_job.upload_data),
[this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); },
[this](wxString error) { this->error_fn(std::move(error)); },
[this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); }
);
{
LifecycleEventContext ctx;
ctx.name = upload_filename;
ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error;
fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx);
}
if (success) {
emit_progress(100);
if (the_job.switch_to_device_tab) {
+13
View File
@@ -7,6 +7,7 @@
#include "libslic3r/Config.hpp"
#include "libslic3r/Exception.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r_version.h"
@@ -46,6 +47,16 @@ void install_capability_resolver()
});
}
// Global libslic3r-side seam (Slic3r::fire_lifecycle_event, in libslic3r/LifecycleEvents.hpp):
// broadcasts to every loaded, enabled capability regardless of type, unlike the SlicingPipeline
// hook below which only targets picker-selected SlicingPipeline capabilities.
void install_lifecycle_event_hook()
{
set_lifecycle_hook_fn([](LifecycleEvent event, const LifecycleEventContext& ctx) {
PluginManager::instance().dispatch_lifecycle_event(event, ctx);
});
}
// Print::process() fires this hook at each pipeline seam on the slicing worker
// thread; here we run the picker-selected SlicingPipeline capabilities. Per
// capability we acquire the GIL, honor cancellation, and convert a plugin
@@ -122,12 +133,14 @@ void install()
{
install_capability_resolver();
install_slicing_pipeline_hook();
install_lifecycle_event_hook();
}
void uninstall()
{
ConfigBase::set_resolve_capability_fn(nullptr);
Print::set_slicing_pipeline_hook_fn(nullptr);
set_lifecycle_hook_fn(nullptr);
}
} // namespace Slic3r::plugin_hooks
+3 -2
View File
@@ -14,8 +14,9 @@ namespace Slic3r::plugin_hooks {
void install();
// Reset every hook to null so none can enter Python after the interpreter
// finalizes. Called from PluginManager::shutdown(); callers must have stopped
// background slicing first (resetting a hook while process() runs is a race).
// finalizes. The lifecycle-event hook drains callbacks already in progress
// before returning. Other hooks retain their existing caller-side shutdown
// requirements.
void uninstall();
} // namespace Slic3r::plugin_hooks
+32 -1
View File
@@ -1,5 +1,6 @@
#include "PluginManager.hpp"
#include <exception>
#include <libslic3r/Utils.hpp>
#include <memory>
#include <pybind11/embed.h>
@@ -134,7 +135,8 @@ void PluginManager::shutdown()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown enter";
// Detach the libslic3r hooks first so nothing dispatches into Python while (or after) plugins
// unload. Callers stop background slicing before this.
// unload. The lifecycle-event hook also drains callbacks already in progress before returning;
// the remaining hook seams retain their existing shutdown requirements.
plugin_hooks::uninstall();
// Reject new plugin loads before we drain.
@@ -2089,4 +2091,33 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k
return result;
}
void PluginManager::dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx) {
const auto is_canceled = [&ctx]() {
return ctx.cancellation_check && ctx.cancellation_check();
};
if (is_canceled())
return;
for (const auto& cap : get_plugin_capabilities()) {
if (!cap || !cap->is_enabled()) continue;
if (is_canceled())
break;
try {
cap->on_lifecycle_event(evt, ctx);
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name()
<< "' on_lifecycle_event(" << lifecycle_event_to_string(evt) << ") threw: " << ex.what()
<< " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code)
<< ", msg='" << ctx.msg << "']";
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name()
<< "' on_lifecycle_event(" << lifecycle_event_to_string(evt)
<< ") threw a non-standard exception"
<< " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code)
<< ", msg='" << ctx.msg << "']";
}
}
}
} // namespace Slic3r
+3 -1
View File
@@ -8,6 +8,7 @@
#include <condition_variable>
#include <functional>
#include <libslic3r/Config.hpp>
#include <libslic3r/LifecycleEvents.hpp>
#include <map>
#include <memory>
#include <mutex>
@@ -20,7 +21,6 @@
#include <pybind11/embed.h>
#include "CloudPluginService.hpp"
#include "PluginFsUtils.hpp"
#include "PluginDescriptor.hpp"
#include "PluginLoader.hpp"
#include "PluginConfig.hpp"
@@ -210,6 +210,8 @@ public:
ExecutionResult run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error);
void dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx);
private:
PluginManager() = default;
PluginManager(const PluginManager&) = delete;
+5
View File
@@ -132,6 +132,11 @@ public:
{
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
}
void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) override
{
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_lifecycle_event, event, ctx);
}
};
class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline<PluginCapabilityInterface>
+58
View File
@@ -377,6 +377,61 @@ void bind_python_api(pybind11::module_& m)
.value("FatalError", PluginResult::FatalError)
.export_values();
py::enum_<LifecycleEvent>(m, "LifecycleEvent", "Application lifecycle moment passed to on_lifecycle_event")
.value("NewProject", LifecycleEvent::NewProject)
.value("ProjectOpened", LifecycleEvent::ProjectOpened)
.value("ProjectBeforeSave", LifecycleEvent::ProjectBeforeSave)
.value("ProjectAfterSave", LifecycleEvent::ProjectAfterSave)
.value("ProjectClosed", LifecycleEvent::ProjectClosed)
.value("ProjectDirtyChanged", LifecycleEvent::ProjectDirtyChanged)
.value("SliceStarted", LifecycleEvent::SliceStarted)
.value("SliceGeometryFinished", LifecycleEvent::SliceGeometryFinished)
.value("GCodeExportStarted", LifecycleEvent::GCodeExportStarted)
.value("GCodeExportFinished", LifecycleEvent::GCodeExportFinished)
.value("SlicingJobComplete", LifecycleEvent::SlicingJobComplete)
.value("ObjectAdded", LifecycleEvent::ObjectAdded)
.value("ObjectDeleted", LifecycleEvent::ObjectDeleted)
.value("ObjectTransformed", LifecycleEvent::ObjectTransformed)
.value("ObjectChanged", LifecycleEvent::ObjectChanged)
.value("ObjectRenamed", LifecycleEvent::ObjectRenamed)
.value("PlateCreated", LifecycleEvent::PlateCreated)
.value("PlateDeleted", LifecycleEvent::PlateDeleted)
.value("PlateSelected", LifecycleEvent::PlateSelected)
.value("PlateRenamed", LifecycleEvent::PlateRenamed)
.value("PresetSelected", LifecycleEvent::PresetSelected)
.value("PresetSaved", LifecycleEvent::PresetSaved)
.value("PrintStateChanged", LifecycleEvent::PrintStateChanged)
.value("DeviceOnline", LifecycleEvent::DeviceOnline)
.value("DeviceOffline", LifecycleEvent::DeviceOffline)
.value("DeviceDiscovered", LifecycleEvent::DeviceDiscovered)
.value("DeviceSelected", LifecycleEvent::DeviceSelected)
.value("UploadStarted", LifecycleEvent::UploadStarted)
.value("UploadFinished", LifecycleEvent::UploadFinished)
.value("PrintJobStarted", LifecycleEvent::PrintJobStarted)
.value("PrintJobFinished", LifecycleEvent::PrintJobFinished)
.value("SendJobStarted", LifecycleEvent::SendJobStarted)
.value("SendJobFinished", LifecycleEvent::SendJobFinished)
.export_values();
py::enum_<LifecycleEvtCode>(m, "LifecycleEvtCode", "Outcome code accompanying a LifecycleEventContext")
.value("Ok", LifecycleEvtCode::Ok)
.value("Error", LifecycleEvtCode::Error)
.value("Warn", LifecycleEvtCode::Warn)
.export_values();
py::class_<LifecycleEventContext>(m, "LifecycleEventContext", "Payload accompanying a LifecycleEvent")
.def(py::init<>())
.def_readonly("name", &LifecycleEventContext::name)
.def_readonly("code", &LifecycleEventContext::code)
.def_readonly("msg", &LifecycleEventContext::msg)
.def_readonly("id", &LifecycleEventContext::id)
.def_readonly("previous_name", &LifecycleEventContext::previous_name)
.def_readonly("device_id", &LifecycleEventContext::device_id)
.def_readonly("job_id", &LifecycleEventContext::job_id)
.def_readonly("source", &LifecycleEventContext::source)
.def_readonly("index", &LifecycleEventContext::index)
.def_readonly("dirty", &LifecycleEventContext::dirty);
py::class_<PluginContext>(m, "PluginContext", "Context shared with plugin entry points")
.def(py::init<>())
.def_readwrite("orca_version", &PluginContext::orca_version);
@@ -401,6 +456,9 @@ void bind_python_api(pybind11::module_& m)
.def("get_type", &PluginCapabilityInterface::get_type)
.def("on_load", &PluginCapabilityInterface::on_load)
.def("on_unload", &PluginCapabilityInterface::on_unload)
.def("on_lifecycle_event", &PluginCapabilityInterface::on_lifecycle_event,
"Override to react to an application lifecycle moment (LifecycleEvent) and its\n"
"LifecycleEventContext payload. Available on every capability type.")
.def("has_config_ui", &PluginCapabilityInterface::has_config_ui,
"Override to return True to replace the host's default JSON editor with your own HTML\n"
"UI, returned by get_config_ui(). Every capability is configurable and appears in the\n"
@@ -10,6 +10,8 @@
#include <nlohmann/json.hpp>
#include <pybind11/embed.h>
#include <libslic3r/LifecycleEvents.hpp>
namespace Slic3r {
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
@@ -167,6 +169,8 @@ public:
virtual void on_unload() {}
virtual void on_cancelled() {}
virtual void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) { (void) event; (void) ctx; }
// ── C++-only host state, never exposed to Python. Set by the loader at materialization. ──
//
// The capability owns its own identity and enable flag: they are read once under the GIL, live
@@ -1,5 +1,6 @@
#include <catch2/catch_all.hpp>
#include <libslic3r/LifecycleEvents.hpp>
#include <libslic3r/Utils.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginManager.hpp>
@@ -12,8 +13,11 @@
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <fstream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using namespace Slic3r;
@@ -114,6 +118,72 @@ PluginDescriptor descriptor_of(PluginManager& manager, const std::string& plugin
} // namespace
TEST_CASE("Lifecycle hook shutdown drains concurrent dispatch", "[PluginLifecycle][LifecycleEvents]")
{
std::mutex mutex;
std::condition_variable cv;
bool callback_entered = false;
bool release_callback = false;
bool shutdown_started = false;
bool shutdown_finished = false;
set_lifecycle_hook_fn([&](LifecycleEvent, const LifecycleEventContext&) {
std::unique_lock<std::mutex> lock(mutex);
callback_entered = true;
cv.notify_all();
cv.wait(lock, [&] { return release_callback; });
});
std::thread dispatch_thread([] {
LifecycleEventContext ctx;
fire_lifecycle_event(LifecycleEvent::ProjectOpened, ctx);
});
bool callback_was_entered = false;
{
std::unique_lock<std::mutex> lock(mutex);
callback_was_entered = cv.wait_for(lock, std::chrono::seconds(5), [&] { return callback_entered; });
}
if (!callback_was_entered) {
{
std::lock_guard<std::mutex> lock(mutex);
release_callback = true;
cv.notify_all();
}
dispatch_thread.join();
set_lifecycle_hook_fn(nullptr);
FAIL("Lifecycle callback did not start");
}
std::thread shutdown_thread([&] {
{
std::lock_guard<std::mutex> lock(mutex);
shutdown_started = true;
cv.notify_all();
}
set_lifecycle_hook_fn(nullptr);
std::lock_guard<std::mutex> lock(mutex);
shutdown_finished = true;
cv.notify_all();
});
bool shutdown_was_started = false;
{
std::unique_lock<std::mutex> lock(mutex);
shutdown_was_started = cv.wait_for(lock, std::chrono::seconds(5), [&] { return shutdown_started; });
if (shutdown_was_started)
CHECK_FALSE(cv.wait_for(lock, std::chrono::milliseconds(100), [&] { return shutdown_finished; }));
release_callback = true;
cv.notify_all();
}
dispatch_thread.join();
shutdown_thread.join();
if (!shutdown_was_started)
FAIL("Lifecycle hook shutdown did not start");
CHECK(shutdown_finished);
}
TEST_CASE("A discovered script plugin loads and materializes its capability", "[PluginLifecycle][Python]")
{
ScopedPluginManager plugin_system;