mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 17:17:42 +00:00
Surface Moonraker webcams and gate unrunnable controls
This commit is contained in:
@@ -2740,6 +2740,14 @@ int MachineObject::publish_json(const json& json_item, int qos, int flag)
|
||||
BOOST_LOG_TRIVIAL(error) << "publish_json: " << json_item.dump() << " code: " << rtn;
|
||||
}
|
||||
|
||||
// why: the agent is the only thing that knows what it can translate, so it reports
|
||||
// not-supported in its return value and this - the single funnel every command_* builder
|
||||
// passes through - is the one place that turns it into something the user sees. No list of
|
||||
// unsupported commands is needed anywhere: an agent that has no case for a command says so.
|
||||
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) {
|
||||
show_unsupported_dlg(rtn);
|
||||
}
|
||||
|
||||
return rtn;
|
||||
}
|
||||
|
||||
@@ -3821,6 +3829,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
has_ipcam = true;
|
||||
} else {
|
||||
has_ipcam = false;
|
||||
webcam_stream_url.clear();
|
||||
}
|
||||
}
|
||||
if (ipcam.contains("resolution")) {
|
||||
@@ -3855,6 +3864,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
liveview_local = local_rtsp_url.empty() ? LVL_None : local_rtsp_url == "disable"
|
||||
? LVL_Disable : boost::algorithm::starts_with(local_rtsp_url, "rtsps") ? LVL_Rtsps : LVL_Rtsp;
|
||||
}
|
||||
if (ipcam.contains("stream_url") && ipcam["stream_url"].is_string()) {
|
||||
webcam_stream_url = ipcam["stream_url"].get<std::string>();
|
||||
}
|
||||
if (ipcam.contains("tutk_server")) {
|
||||
tutk_state = ipcam["tutk_server"].get<std::string>();
|
||||
}
|
||||
@@ -4650,6 +4662,40 @@ void MachineObject::set_ctt_dlg( wxString text){
|
||||
}
|
||||
}
|
||||
|
||||
void MachineObject::show_unsupported_dlg(int code)
|
||||
{
|
||||
// why: a dead control invites repeat clicks, and the frame is modeless - without the guard
|
||||
// every click stacks another one. Same shape as set_ctt_dlg above, including the reset on
|
||||
// both hide and close so a dismissed dialog can reappear on the next attempt.
|
||||
if (m_unsupported_dlg_shown) {
|
||||
return;
|
||||
}
|
||||
m_unsupported_dlg_shown = true;
|
||||
|
||||
// why: two codes so the user learns which kind of dead end this is - the slicer having no
|
||||
// translation for the command, or the printer's own config lacking the hardware to run it.
|
||||
const wxString text = (code == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) ?
|
||||
_L("This printer is not configured with the hardware this control needs.") :
|
||||
_L("This control is not supported on this printer.");
|
||||
|
||||
// note: constructed directly rather than through CallAfter because every publish_json caller
|
||||
// is on the UI thread - clicks come from wx handlers, and the agent marshals its own push
|
||||
// callbacks back to main before parse_json runs. set_ctt_dlg relies on the same property.
|
||||
auto unsupported_dlg = new GUI::SecondaryCheckDialog(nullptr, wxID_ANY, _L("Warning"),
|
||||
GUI::SecondaryCheckDialog::VisibleButtons::ONLY_CONFIRM);
|
||||
unsupported_dlg->update_text(text);
|
||||
unsupported_dlg->Bind(wxEVT_SHOW, [this](auto& e) {
|
||||
if (!e.IsShown()) {
|
||||
m_unsupported_dlg_shown = false;
|
||||
}
|
||||
});
|
||||
unsupported_dlg->Bind(wxEVT_CLOSE_WINDOW, [this](auto& e) {
|
||||
e.Skip();
|
||||
m_unsupported_dlg_shown = false;
|
||||
});
|
||||
unsupported_dlg->on_show();
|
||||
}
|
||||
|
||||
int MachineObject::publish_gcode(std::string gcode_str)
|
||||
{
|
||||
json j;
|
||||
|
||||
@@ -272,9 +272,11 @@ public:
|
||||
bool m_is_online;
|
||||
bool m_lan_mode_connection_state{false};
|
||||
bool m_set_ctt_dlg{ false };
|
||||
bool m_unsupported_dlg_shown{ false };
|
||||
void set_lan_mode_connection_state(bool state) {m_lan_mode_connection_state = state;};
|
||||
bool get_lan_mode_connection_state() {return m_lan_mode_connection_state;};
|
||||
void set_ctt_dlg( wxString text);
|
||||
void show_unsupported_dlg(int code);
|
||||
int parse_msg_count = 0;
|
||||
int keep_alive_count = 0;
|
||||
std::chrono::system_clock::time_point last_update_time; /* last received print data from machine */
|
||||
@@ -541,6 +543,7 @@ public:
|
||||
bool xcam_first_layer_inspector { false };
|
||||
time_t xcam_first_layer_hold_start = 0;
|
||||
std::string local_rtsp_url;
|
||||
std::string webcam_stream_url;
|
||||
std::string tutk_state;
|
||||
enum LiveviewLocal {
|
||||
LVL_None,
|
||||
|
||||
@@ -2311,6 +2311,27 @@ void StatusPanel::update_camera_state(MachineObject* obj)
|
||||
{
|
||||
if (!obj) return;
|
||||
|
||||
const bool has_printer_webcam = !obj->webcam_stream_url.empty();
|
||||
if (has_printer_webcam) {
|
||||
if (m_printer_webcam_url != obj->webcam_stream_url) {
|
||||
m_custom_camera_view->LoadURL(obj->webcam_stream_url);
|
||||
m_custom_camera_view->Show();
|
||||
m_media_ctrl->Hide();
|
||||
m_media_play_ctrl->Hide();
|
||||
m_printer_webcam_url = obj->webcam_stream_url;
|
||||
}
|
||||
m_camera_switch_button->Hide();
|
||||
if (!m_custom_camera_view->IsShown()) {
|
||||
// why: do not compare or reload the WebView URL per tick, or redirects can cause a reload loop.
|
||||
m_custom_camera_view->Show();
|
||||
m_media_ctrl->Hide();
|
||||
m_media_play_ctrl->Hide();
|
||||
}
|
||||
} else if (!m_printer_webcam_url.empty()) {
|
||||
handle_camera_source_change();
|
||||
m_printer_webcam_url.clear();
|
||||
}
|
||||
|
||||
//sdcard
|
||||
auto sdcard_state = obj->GetStorage()->get_sdcard_state();
|
||||
if (m_last_sdcard != sdcard_state) {
|
||||
@@ -2342,7 +2363,12 @@ void StatusPanel::update_camera_state(MachineObject* obj)
|
||||
m_last_recording = obj->is_recording() ? 1 : 0;
|
||||
}
|
||||
|
||||
if (!m_bitmap_recording_img->IsShown()) {
|
||||
if (has_printer_webcam) {
|
||||
if (m_bitmap_recording_img->IsShown()) {
|
||||
m_bitmap_recording_img->Hide();
|
||||
m_panel_monitoring_title->Layout();
|
||||
}
|
||||
} else if (!m_bitmap_recording_img->IsShown()) {
|
||||
m_bitmap_recording_img->Show();
|
||||
m_panel_monitoring_title->Layout();
|
||||
}
|
||||
@@ -2399,6 +2425,8 @@ void StatusPanel::update_camera_state(MachineObject* obj)
|
||||
bool show_vcamera = m_media_play_ctrl->IsStreaming();
|
||||
m_camera_popup->update(show_vcamera);
|
||||
}
|
||||
|
||||
m_setting_button->Show(!has_printer_webcam);
|
||||
}
|
||||
|
||||
StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name)
|
||||
@@ -5194,6 +5222,10 @@ bool StatusPanel::is_stage_list_info_changed(MachineObject *obj)
|
||||
void StatusPanel::set_default()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << "status_panel: set_default";
|
||||
if (!m_printer_webcam_url.empty()) {
|
||||
handle_camera_source_change();
|
||||
m_printer_webcam_url.clear();
|
||||
}
|
||||
obj = nullptr;
|
||||
last_subtask = nullptr;
|
||||
last_tray_exist_bits = -1;
|
||||
|
||||
@@ -664,6 +664,7 @@ protected:
|
||||
int m_last_timelapse = -1;
|
||||
int m_last_extrusion = -1;
|
||||
int m_last_vcamera = -1;
|
||||
std::string m_printer_webcam_url;
|
||||
int m_model_mall_request_count = 0;
|
||||
bool m_is_load_with_temp = false;
|
||||
json m_rating_result;
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
#define __I_PRINTER_AGENT_HPP__
|
||||
|
||||
#include "bambu_networking.hpp"
|
||||
// why: these extend the BAMBU_NETWORK_* return space rather than opening a new one - the value
|
||||
// flows through the same int domain callers already compare against BAMBU_NETWORK_SUCCESS.
|
||||
// They live here and not in bambu_networking.hpp because that file is a vendor header replaced
|
||||
// wholesale by header-sync commits (see c09252ce11), which would silently clobber them.
|
||||
// -70xx is free: the vendor occupies -1..-25 and -10xx through -60xx.
|
||||
#define ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED -7010 // no translation exists for this command
|
||||
#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
|
||||
@@ -1089,9 +1089,8 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::
|
||||
}
|
||||
}
|
||||
if (gcode.empty()) {
|
||||
// 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";
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
return ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE;
|
||||
}
|
||||
if (!send_gcode(dev_id, gcode)) {
|
||||
return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED;
|
||||
@@ -1104,6 +1103,15 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::
|
||||
}
|
||||
}
|
||||
|
||||
// why: the whole "pushing" namespace asks the printer to (re)send status - pushall, start, stop.
|
||||
// The ws status stream already pushes unsolicited, so every member of it is genuinely satisfied
|
||||
// rather than dropped. Accepting the namespace instead of naming its members keeps this a
|
||||
// handled case rather than a suppression list; it also fires from the DevManager keepalive
|
||||
// timer roughly once a second, which the not-supported default below would otherwise warn on.
|
||||
if (json.contains("pushing") && json["pushing"].contains("command")) {
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
// Handle print commands
|
||||
if (json.contains("print") && json["print"].contains("command")) {
|
||||
const auto& command = json["print"]["command"];
|
||||
@@ -1188,7 +1196,29 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::
|
||||
}
|
||||
}
|
||||
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
std::string command_namespace = "unknown";
|
||||
std::string command_name = "unknown";
|
||||
if (json.is_object()) {
|
||||
for (const char* namespace_name : {"info", "system", "print", "camera", "xcam", "upgrade", "pushing"}) {
|
||||
if (!json.contains(namespace_name) || !json[namespace_name].is_object()) {
|
||||
continue;
|
||||
}
|
||||
const auto& namespace_object = json[namespace_name];
|
||||
if (!namespace_object.contains("command") || !namespace_object["command"].is_string()) {
|
||||
continue;
|
||||
}
|
||||
command_namespace = namespace_name;
|
||||
command_name = namespace_object["command"].get<std::string>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// why: reaching here means no case claimed the command, which is the honest verdict for
|
||||
// every control Klipper has no equivalent for. Returning SUCCESS instead made all of them
|
||||
// look like they worked. Nothing surfaces this code to the user yet.
|
||||
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: no translation for " << command_namespace << "." << command_name
|
||||
<< ", dropping";
|
||||
return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl)
|
||||
@@ -1204,6 +1234,7 @@ bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev
|
||||
device_info.dev_ip = dev_ip;
|
||||
|
||||
device_info.api_key = password;
|
||||
device_info.use_ssl = use_ssl;
|
||||
device_info.model_name = printer_cfg.opt_string("printer_model");
|
||||
device_info.model_id = preset.get_printer_type(preset_bundle);
|
||||
device_info.base_url = use_ssl ? "https://" + dev_ip : "http://" + dev_ip;
|
||||
@@ -1326,6 +1357,104 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation)
|
||||
{
|
||||
std::string stream_url;
|
||||
std::string webcam_name;
|
||||
std::string error;
|
||||
try {
|
||||
std::string response_body;
|
||||
bool success = false;
|
||||
std::string http_error;
|
||||
|
||||
auto http = Http::get(join_url(base_url, "/server/webcams/list"));
|
||||
if (!api_key.empty()) {
|
||||
http.header("X-Api-Key", api_key);
|
||||
}
|
||||
http.timeout_connect(5)
|
||||
.timeout_max(10)
|
||||
.on_complete([&](std::string body, unsigned status_code) {
|
||||
if (status_code == 200) {
|
||||
response_body = body;
|
||||
success = true;
|
||||
} else {
|
||||
http_error = "HTTP error: " + std::to_string(status_code);
|
||||
}
|
||||
})
|
||||
.on_error([&](std::string body, std::string err, unsigned status_code) {
|
||||
http_error = err;
|
||||
if (status_code > 0) {
|
||||
http_error += " (HTTP " + std::to_string(status_code) + ")";
|
||||
}
|
||||
})
|
||||
.perform_sync();
|
||||
|
||||
if (!success) {
|
||||
error = http_error.empty() ? "Connection failed" : http_error;
|
||||
} else {
|
||||
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
|
||||
if (json.is_discarded()) {
|
||||
error = "Invalid JSON response";
|
||||
} else {
|
||||
const auto result = json.contains("result") ? json["result"] : json;
|
||||
if (!result.contains("webcams") || !result["webcams"].is_array()) {
|
||||
error = "Unexpected JSON structure";
|
||||
} else {
|
||||
for (const auto& webcam : result["webcams"]) {
|
||||
if (webcam.is_object() && webcam.value("enabled", false) && webcam.contains("stream_url") &&
|
||||
webcam["stream_url"].is_string()) {
|
||||
stream_url = webcam["stream_url"].get<std::string>();
|
||||
if (webcam.contains("name") && webcam["name"].is_string()) {
|
||||
webcam_name = webcam["name"].get<std::string>();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stream_url.empty()) {
|
||||
error = "No enabled webcam";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error.empty()) {
|
||||
if (stream_url.rfind("http", 0) != 0 && !stream_url.empty() && stream_url.front() == '/') {
|
||||
// why: Moonraker's API port serves a JSON 404 for /webcam; relative streams use the printer web root.
|
||||
const size_t scheme_end = base_url.find("://");
|
||||
const size_t authority_start = scheme_end == std::string::npos ? 0 : scheme_end + 3;
|
||||
const size_t authority_end = base_url.find('/', authority_start);
|
||||
const std::string scheme = scheme_end == std::string::npos ? "" : base_url.substr(0, scheme_end + 3);
|
||||
std::string authority = base_url.substr(authority_start, authority_end - authority_start);
|
||||
const size_t port_start = authority.rfind(':');
|
||||
if (port_start != std::string::npos && port_start + 1 < authority.size() &&
|
||||
std::all_of(authority.begin() + port_start + 1, authority.end(), [](char c) { return c >= '0' && c <= '9'; })) {
|
||||
authority.erase(port_start);
|
||||
}
|
||||
stream_url = scheme + authority + stream_url;
|
||||
} else if (stream_url.rfind("http", 0) != 0) {
|
||||
error = "Unsupported webcam stream URL";
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
error = e.what();
|
||||
} catch (...) {
|
||||
error = "Unknown webcam discovery error";
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(payload_mutex);
|
||||
if (generation == connect_generation.load()) {
|
||||
webcam_stream_url = error.empty() ? stream_url : "";
|
||||
}
|
||||
}
|
||||
if (!error.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: webcam discovery failed: " << error;
|
||||
return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: selected webcam '" << webcam_name << "' with stream URL " << stream_url;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonrakerPrinterAgent::post_print_action(const std::string& action) const
|
||||
{
|
||||
// why: /printer/print/{pause,resume,cancel} map to Klipper's pause_resume
|
||||
@@ -2079,6 +2208,12 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
|
||||
payload["print"]["bed_temp_range"] = {0, 120}; // Typical bed range
|
||||
|
||||
payload["print"]["support_send_to_sd"] = true;
|
||||
if (!webcam_stream_url.empty()) {
|
||||
payload["print"]["ipcam"]["ipcam_dev"] = "1";
|
||||
payload["print"]["ipcam"]["stream_url"] = webcam_stream_url;
|
||||
} else {
|
||||
payload["print"]["ipcam"]["ipcam_dev"] = "0";
|
||||
}
|
||||
// Detect bed_leveling support from available objects (bed_mesh or probe)
|
||||
// Default to 0 (not supported) if neither object exists
|
||||
bool has_bed_leveling = (available_objects.count("bed_mesh") != 0 || available_objects.count("probe") != 0);
|
||||
@@ -2442,6 +2577,8 @@ void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id,
|
||||
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Initial status query failed: " << error_msg;
|
||||
}
|
||||
|
||||
fetch_webcam_info(base_url, api_key, generation);
|
||||
|
||||
// Start WebSocket status stream
|
||||
start_status_stream(dev_id, base_url, api_key);
|
||||
#endif
|
||||
|
||||
@@ -134,6 +134,7 @@ private:
|
||||
|
||||
bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set<std::string>& objects, std::string& error) const;
|
||||
bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const;
|
||||
bool fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation);
|
||||
|
||||
void announce_printhost_device();
|
||||
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg);
|
||||
@@ -199,6 +200,7 @@ private:
|
||||
// note: guarded by payload_mutex; filled by refresh_thumbnail_url(), empty url = looked up, none found
|
||||
std::string thumbnail_filename;
|
||||
std::string thumbnail_url;
|
||||
std::string webcam_stream_url;
|
||||
unsigned thumbnail_lookup_attempts = 0;
|
||||
|
||||
std::atomic<int> next_jsonrpc_id{1};
|
||||
|
||||
Reference in New Issue
Block a user