fix: make Klipper macro lamp control reliable

This commit is contained in:
Andrew
2026-07-27 17:38:32 +08:00
committed by Ian Chua
parent cd6d2cbf5e
commit 76f9de4cfb
3 changed files with 64 additions and 18 deletions

View File

@@ -94,6 +94,25 @@ namespace Slic3r {
const std::string MoonrakerPrinterAgent_VERSION = "1.0.0"; const std::string MoonrakerPrinterAgent_VERSION = "1.0.0";
bool moonraker_is_light_name(const std::string& name)
{
std::string lower_name = name;
std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
size_t pos = lower_name.find("led");
while (pos != std::string::npos) {
const bool at_start = pos == 0 || lower_name[pos - 1] == '_' || lower_name[pos - 1] == '-';
const size_t end = pos + 3;
const bool at_end = end == lower_name.size() || lower_name[end] == '_' || lower_name[end] == '-';
if (at_start && at_end) {
return true;
}
pos = lower_name.find("led", pos + 1);
}
return lower_name.find("light") != std::string::npos;
}
MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agent(nullptr) { (void) log_dir; } MoonrakerPrinterAgent::MoonrakerPrinterAgent(std::string log_dir) : m_cloud_agent(nullptr) { (void) log_dir; }
MoonrakerPrinterAgent::~MoonrakerPrinterAgent() MoonrakerPrinterAgent::~MoonrakerPrinterAgent()
@@ -1023,31 +1042,28 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::
!system["led_mode"].is_string()) { !system["led_mode"].is_string()) {
return BAMBU_NETWORK_ERR_INVALID_RESULT; return BAMBU_NETWORK_ERR_INVALID_RESULT;
} }
const std::string led_node = system["led_node"].get<std::string>();
// why: DevLamp::CtrlSetChamberLight publishes chamber_light and chamber_light2 per click.
// That is idempotent for absolute writes, but toggle macros fired twice are a net no-op.
if (led_node != "chamber_light") {
return BAMBU_NETWORK_SUCCESS;
}
const std::string led_mode = system["led_mode"].get<std::string>(); const std::string led_mode = system["led_mode"].get<std::string>();
// why: Klipper has no flash primitive, so flashing collapses to full brightness. // why: Klipper has no flash primitive, so flashing collapses to full brightness.
const std::string value = (led_mode == "on" || led_mode == "flashing") ? "1" : "0"; const std::string value = (led_mode == "on" || led_mode == "flashing") ? "1" : "0";
const bool requested_light_on = value == "1";
std::string gcode; std::string gcode;
{ {
std::lock_guard<std::recursive_mutex> lock(payload_mutex); std::lock_guard<std::recursive_mutex> lock(payload_mutex);
// why: only light-named objects may be driven - available_objects is alphabetical, so an if (requested_light_on == assumed_light_on) {
// unqualified "first output_pin" match would happily toggle a beeper or a fan power pin. return BAMBU_NETWORK_SUCCESS;
const auto is_light = [](const std::string& name) { }
size_t pos = name.find("led"); // why: available_objects is a std::set, so first-match-and-break means alphabetical priority.
while (pos != std::string::npos) { // That deliberately prefers FLASHLIGHT_SWITCH over MODLELIGHT_SWITCH on Elegoo Neptune 4.
const bool at_start = pos == 0 || name[pos - 1] == '_' || name[pos - 1] == '-';
const size_t end = pos + 3;
const bool at_end = end == name.size() || name[end] == '_' || name[end] == '-';
if (at_start && at_end) {
return true;
}
pos = name.find("led", pos + 1);
}
return name.find("light") != std::string::npos;
};
for (const auto& object : available_objects) { for (const auto& object : available_objects) {
// why: Klipper wants the bare pin name, not the "output_pin " section prefix. // why: Klipper wants the bare pin name, not the "output_pin " section prefix.
const size_t prefix = object.rfind("output_pin ", 0) == 0 ? 11 : 0; const size_t prefix = object.rfind("output_pin ", 0) == 0 ? 11 : 0;
if (prefix != 0 && object.size() > prefix && is_light(object.substr(prefix))) { if (prefix != 0 && object.size() > prefix && moonraker_is_light_name(object.substr(prefix))) {
gcode = "SET_PIN PIN=" + object.substr(prefix) + " VALUE=" + value; gcode = "SET_PIN PIN=" + object.substr(prefix) + " VALUE=" + value;
break; break;
} }
@@ -1055,20 +1071,36 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::
if (gcode.empty()) { if (gcode.empty()) {
for (const auto& object : available_objects) { for (const auto& object : available_objects) {
const size_t prefix = object.rfind("led ", 0) == 0 ? 4 : object.rfind("neopixel ", 0) == 0 ? 9 : 0; const size_t prefix = object.rfind("led ", 0) == 0 ? 4 : object.rfind("neopixel ", 0) == 0 ? 9 : 0;
if (prefix != 0 && object.size() > prefix && is_light(object.substr(prefix))) { if (prefix != 0 && object.size() > prefix && moonraker_is_light_name(object.substr(prefix))) {
gcode = "SET_LED LED=" + object.substr(prefix) + " RED=" + value + " GREEN=" + value + gcode = "SET_LED LED=" + object.substr(prefix) + " RED=" + value + " GREEN=" + value +
" BLUE=" + value + " WHITE=" + value; " BLUE=" + value + " WHITE=" + value;
break; break;
} }
} }
} }
if (gcode.empty()) {
for (const auto& object : available_objects) {
const size_t prefix = object.rfind("gcode_macro ", 0) == 0 ? 12 : 0;
if (prefix != 0 && object.size() > prefix && moonraker_is_light_name(object.substr(prefix))) {
gcode = object.substr(prefix);
break;
}
}
}
} }
if (gcode.empty()) { if (gcode.empty()) {
// why: match the other unmapped commands - the UI treats a failure code as a printer error dialog. // why: match the other unmapped commands - the UI treats a failure code as a printer error dialog.
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: ledctrl - no light object found, dropping"; BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: ledctrl - no light object found, dropping";
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
return send_gcode(dev_id, gcode) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; if (!send_gcode(dev_id, gcode)) {
return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED;
}
{
std::lock_guard<std::recursive_mutex> lock(payload_mutex);
assumed_light_on = requested_light_on;
}
return BAMBU_NETWORK_SUCCESS;
} }
} }
@@ -2051,6 +2083,7 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
// Default to 0 (not supported) if neither object exists // Default to 0 (not supported) if neither object exists
bool has_bed_leveling = (available_objects.count("bed_mesh") != 0 || available_objects.count("probe") != 0); bool has_bed_leveling = (available_objects.count("bed_mesh") != 0 || available_objects.count("probe") != 0);
payload["print"]["support_bed_leveling"] = has_bed_leveling ? 1 : 0; payload["print"]["support_bed_leveling"] = has_bed_leveling ? 1 : 0;
payload["print"]["lights_report"] = {{{"node", "chamber_light"}, {"mode", assumed_light_on ? "on" : "off"}}};
const nlohmann::json* extruder = nullptr; const nlohmann::json* extruder = nullptr;
if (status_cache.contains("extruder") && status_cache["extruder"].is_object()) { if (status_cache.contains("extruder") && status_cache["extruder"].is_object()) {

View File

@@ -14,6 +14,8 @@
namespace Slic3r { namespace Slic3r {
bool moonraker_is_light_name(const std::string& name);
class MoonrakerPrinterAgent : public IPrinterAgent class MoonrakerPrinterAgent : public IPrinterAgent
{ {
public: public:
@@ -201,6 +203,7 @@ private:
std::atomic<int> next_jsonrpc_id{1}; std::atomic<int> next_jsonrpc_id{1};
std::set<std::string> available_objects; // Track for feature detection std::set<std::string> available_objects; // Track for feature detection
bool assumed_light_on = false;
std::atomic<bool> ws_stop{false}; std::atomic<bool> ws_stop{false};
std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection

View File

@@ -1,5 +1,6 @@
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <slic3r/Utils/MoonrakerPrinterAgent.hpp>
#include <slic3r/Utils/NetworkAgentFactory.hpp> #include <slic3r/Utils/NetworkAgentFactory.hpp>
#include <slic3r/plugin/PythonPluginBridge.hpp> #include <slic3r/plugin/PythonPluginBridge.hpp>
@@ -12,6 +13,15 @@
using namespace Slic3r; using namespace Slic3r;
namespace py = pybind11; namespace py = pybind11;
TEST_CASE("unit: Moonraker light name matching", "[unit][moonraker]")
{
CHECK(moonraker_is_light_name("caselight"));
CHECK(moonraker_is_light_name("LED_STRIP"));
CHECK_FALSE(moonraker_is_light_name("beeper"));
CHECK(moonraker_is_light_name("FLASHLIGHT_SWITCH"));
CHECK(moonraker_is_light_name("MODLELIGHT_SWITCH"));
}
// =========================================================================== // ===========================================================================
// UNIT - printer-agent registry duplicate handling. // UNIT - printer-agent registry duplicate handling.
// Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in // Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in