diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index a418f639f3..279ac316d1 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -1792,6 +1792,14 @@ int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id) return this->publish_json(j); } +int MachineObject::command_start_camera() +{ + if (!m_agent) return -1; + // why: this fires from the camera view's renew timer, so a refusal must stay silent - + // show_unsupported_dlg() here would pop a dialog every ~5 min on every other printer. + return m_agent->command_start_camera(get_dev_id()); +} + int MachineObject::command_ams_select_tray(std::string tray_id) { diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 40909ef912..18f8599381 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -812,6 +812,7 @@ public: int command_ams_select_tray(std::string tray_id); int command_ams_refresh_rfid(std::string tray_id); int command_ams_refresh_rfid2(int ams_id, int slot_id); + int command_start_camera(); int command_ams_control(std::string action); int command_ams_drying_stop(); int command_start_extrusion_cali(int tray_index, int nozzle_temp, int bed_temp, float max_volumetric_speed, std::string setting_id = ""); diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 97c4920be7..6d92569366 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -2328,9 +2328,19 @@ void StatusPanel::update_camera_state(MachineObject* obj) m_media_ctrl->Hide(); m_media_play_ctrl->Hide(); } + // why: printers like the U1 capture only while asked and retire the capture task ~362 s + // after each start, so the open camera view has to renew ahead of that. 300 s matches + // Snapmaker's own client. Agents that do not need it refuse the call silently. + const auto now = std::chrono::steady_clock::now(); + if (m_camera_start_sent == std::chrono::steady_clock::time_point{} || + now - m_camera_start_sent >= std::chrono::seconds(300)) { + obj->command_start_camera(); + m_camera_start_sent = now; + } } else if (!m_printer_webcam_url.empty()) { handle_camera_source_change(); m_printer_webcam_url.clear(); + m_camera_start_sent = std::chrono::steady_clock::time_point{}; } //sdcard diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index 170a9d970d..4e516695c5 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -666,6 +666,8 @@ protected: int m_last_extrusion = -1; int m_last_vcamera = -1; std::string m_printer_webcam_url; + // note: zero = not started; see update_camera_state() for the renew interval. + std::chrono::steady_clock::time_point m_camera_start_sent{}; int m_model_mall_request_count = 0; bool m_is_load_with_temp = false; json m_rating_result; diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 85a1ffb8fc..df52ac0da8 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -93,6 +93,11 @@ public: { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } virtual int command_ams_select_tray(std::string, std::string, int, bool) { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } + // why: some printers emit camera frames only while explicitly asked, and retire the + // capture task on their own - the camera view starts it and renews it. Printers with an + // always-on stream need nothing here, hence the honest refusal by default. + virtual int command_start_camera(std::string) + { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } /** * Establish a direct LAN connection to a printer. diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 0d02a00eff..9e193e858d 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -1442,8 +1442,99 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, return true; } +bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohmann::json& params) +{ + std::string base_url; + std::string api_key; + { + std::lock_guard lock(connect_mutex); + base_url = device_info.base_url; + api_key = device_info.api_key; + } + + WsEndpoint endpoint; + if (!parse_ws_endpoint(base_url, endpoint) || endpoint.secure) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: send_ws_rpc has no usable websocket for base_url=" + << base_url; + return false; + } + + nlohmann::json request; + request["jsonrpc"] = "2.0"; + request["method"] = method; + if (!params.is_null()) { + request["params"] = params; + } + request["id"] = next_jsonrpc_id++; + const std::string body = request.dump(); + + // why: the configured address and Moonraker's own API port are both plausible websocket + // homes - the U1 answers on 7125 while its base_url points at port 80. Try what the user + // configured first, then the default API port. + std::vector ports{endpoint.port}; + if (endpoint.port != "7125") { + ports.emplace_back("7125"); + } + + for (const auto& port : ports) { + try { + net::io_context ioc; + tcp::resolver resolver{ioc}; + beast::tcp_stream stream{ioc}; + stream.expires_after(std::chrono::seconds(5)); + stream.connect(resolver.resolve(endpoint.host, port)); + + websocket::stream ws{std::move(stream)}; + ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) { + req.set(http::field::user_agent, "OrcaSlicer"); + if (!api_key.empty()) { + req.set("X-Api-Key", api_key); + } + })); + + std::string host_header = endpoint.host; + if (!port.empty() && port != "80") { + host_header += ":" + port; + } + ws.handshake(host_header, endpoint.target); + ws.text(true); + ws.write(net::buffer(body)); + + // why: some RPCs are answered on another transport entirely, so a reply may never + // come - read once with a short deadline purely to let the printer act on the + // request before we close, then drop the socket. A timeout here is the normal path. + ws.next_layer().expires_after(std::chrono::seconds(2)); + beast::flat_buffer buffer; + beast::error_code read_ec; + ws.read(buffer, read_ec); + + beast::error_code close_ec; + ws.close(websocket::close_code::normal, close_ec); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent " << method << " over ws to " + << endpoint.host << ":" << port; + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: " << method << " over ws to " << endpoint.host + << ":" << port << " failed: " << e.what(); + } + } + return false; +} + bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation) { + if (const std::string override_url = webcam_stream_override(base_url); !override_url.empty()) { + { + std::lock_guard lock(payload_mutex); + if (generation != connect_generation.load()) { + return false; + } + webcam_stream_url = override_url; + } + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: using printer-specific webcam URL " << override_url; + return true; + } + std::string stream_url; std::string webcam_name; std::string error; @@ -2763,7 +2854,7 @@ std::string MoonrakerPrinterAgent::join_url(const std::string& base_url, const s // Sanitize filename to prevent path traversal attacks // Extracts only the basename, removing any path components -std::string MoonrakerPrinterAgent::sanitize_filename(const std::string& filename) +std::string MoonrakerPrinterAgent::sanitize_filename(const std::string& filename) const { if (filename.empty()) { return "print.gcode"; diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index e35064cda8..86513b8931 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -117,7 +117,7 @@ protected: // Helpers bool is_numeric(const std::string& value); std::string normalize_base_url(std::string host, const std::string& port); - std::string sanitize_filename(const std::string& filename); + std::string sanitize_filename(const std::string& filename) const; std::string join_url(const std::string& base_url, const std::string& path) const; // Trim whitespace and convert to uppercase @@ -134,6 +134,14 @@ protected: bool post_print_action(const std::string& action, const std::string& base_url, const std::string& api_key) const; + // Send one JSON-RPC call over a short-lived Moonraker websocket. Returns true when the + // request was written; it never waits for a reply. + bool send_ws_rpc(const std::string& method, const nlohmann::json& params); + + // why: a printer with no /server/webcams/list entry can still name its stream directly; + // returning empty (the default) keeps the normal Moonraker discovery path. + virtual std::string webcam_stream_override(const std::string& base_url) const { return {}; } + private: int handle_request(const std::string& dev_id, const std::string& json_str); int send_version_info(const std::string& dev_id); diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index b169fca052..34566c0dfa 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -788,6 +788,13 @@ int NetworkAgent::command_ams_select_tray(std::string dev_id, std::string tray_i return -1; } +int NetworkAgent::command_start_camera(std::string dev_id) +{ + if (m_printer_agent) + return m_printer_agent->command_start_camera(dev_id); + return -1; +} + int NetworkAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) { if (m_printer_agent) diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index 317a357135..53660edc72 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -145,6 +145,7 @@ public: int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode); int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode); int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode); + int command_start_camera(std::string dev_id); int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); int disconnect_printer(); int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag); diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp index 9b9a6809fd..dee1d5ba50 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -1,10 +1,14 @@ #include "SnapmakerPrinterAgent.hpp" #include "Http.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Utils.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "nlohmann/json.hpp" +#include #include +#include +#include namespace Slic3r { @@ -67,6 +71,69 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection& SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} +int SnapmakerPrinterAgent::command_start_camera(std::string dev_id) +{ + (void) dev_id; + // why: the printer executes this over the websocket but answers only over MQTT, and the + // call itself blocks on socket I/O - it fires from the camera view's renew timer on the UI + // thread, so run it detached rather than block the caller on a reply that never comes. + // note: interval is dead time in SECONDS on top of a ~0.455 s capture, so 0 is the 2.15 fps + // ceiling (1 measures 0.63 fps), and it cannot be changed while a capture task is running. + std::thread([this] { + send_ws_rpc("camera.start_monitor", + {{"domain", "lan"}, {"interval", 0}, {"expect_pw", false}}); + }).detach(); + return BAMBU_NETWORK_SUCCESS; +} + +std::string SnapmakerPrinterAgent::webcam_stream_override(const std::string& base_url) const +{ + const std::string snapshot_url = join_url(base_url, "/server/files/camera/monitor.jpg"); + + // why: one wrapper file per printer - two U1s would otherwise overwrite each other's URL. + const boost::filesystem::path page = boost::filesystem::path(data_dir()) / "cache" / + ("snapmaker_camera_" + sanitize_filename(device_info.dev_ip) + ".html"); + + // why: the printer writes a still JPEG at ~2 fps, so the page polls it with a cache buster + // instead of consuming a stream. Chaining the next request off onload (never a bare + // setInterval) keeps requests from piling up when the printer is slow to answer. + const std::string html = + "Camera" + "\"\"\n"; + + std::string write_error; + try { + boost::filesystem::create_directories(page.parent_path()); + boost::nowide::ofstream out(page.string().c_str(), std::ios::binary | std::ios::trunc); + out << html; + out.close(); + // note: an ofstream reports a failed write in its state, not by throwing. + if (!out) { + write_error = "write failed"; + } + } catch (const std::exception& e) { + write_error = e.what(); + } + if (!write_error.empty()) { + // why: no wrapper means no camera - a raw monitor.jpg URL would render one frozen frame + // and read as a broken feed, so fall back to showing nothing and say why in the log. + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent: could not write camera page " << page.string() + << ": " << write_error; + return {}; + } + + return "file://" + page.generic_string(); +} + AgentInfo SnapmakerPrinterAgent::get_agent_info_static() { return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp index 04c0a66b3f..2d0bbee640 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp @@ -16,6 +16,10 @@ public: AgentInfo get_agent_info() override { return get_agent_info_static(); } bool fetch_filament_info(std::string dev_id) override; + int command_start_camera(std::string dev_id) override; + +protected: + std::string webcam_stream_override(const std::string& base_url) const override; private: // Combine filament_type + filament_sub_type into a unified type string