Compare commits

..
Author SHA1 Message Date
Lam Wei Lun 6f5856f06c Initial Commit for OrcaPrinterAgent AMS Sync 2026-09-18 15:46:35 +08:00
36 changed files with 2119 additions and 804 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ orcaslicer_add_cmake_project(DataChannel
-DOPENSSL_ROOT_DIR:PATH=${DESTDIR}
-DOPENSSL_USE_STATIC_LIBS=ON
GIT_REPOSITORY https://github.com/paullouisageneau/libdatachannel.git
GIT_TAG v0.24.5
GIT_TAG v0.22.2
GIT_SHALLOW ON
GIT_SUBMODULES_RECURSE ON
)
@@ -325,10 +325,11 @@ modules:
sha256: deedcabe339165214a3637df4c86a507aef0d793cf8774ff68735f4737e8ddbc
dest: external-packages/FFMPEG
# libdatachannel v0.24.5
- type: file
url: https://github.com/paullouisageneau/libdatachannel/archive/refs/tags/v0.24.5.tar.gz
sha256: 454537c3cd526bed935d847bb2dff4046f266eef84d43b2a5f2f2f293c0026f4
# libdatachannel v0.22.2
- type: git
url: https://github.com/paullouisageneau/libdatachannel.git
tag: v0.22.2
commit: b3390b4e01e97071dd054684870c4bb5221794bf
dest: deps/build_flatpak/dep_DataChannel-prefix/src/dep_DataChannel
# ---------------------------------------------------------------
+6 -6
View File
@@ -24,7 +24,7 @@ NAMESPACE = "lane_data"
# Repo-relative paths for the offline generic-map check
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MOONRAKER_AGENT_CPP = os.path.join(REPO_ROOT, "src", "slic3r", "Utils", "MoonrakerPrinterAgent.cpp")
AMS_PAYLOAD_CPP = os.path.join(REPO_ROOT, "src", "slic3r", "Utils", "AmsPayload.cpp")
OFL_FILAMENT_DIR = os.path.join(REPO_ROOT, "resources", "profiles", "OrcaFilamentLibrary", "filament")
LANE_KEYS = [f"lane{i}" for i in range(1, 9)] # lane1-lane8
MATERIALS = ["PLA", "ABS", "PETG", "ASA", "ASA Sparkle", "TPU", ""]
@@ -41,16 +41,16 @@ MATERIAL_TEMPS = {
}
def parse_cpp_type_map():
"""Extract the normalized-type -> OFL generic family table from MoonrakerPrinterAgent.cpp.
"""Extract the normalized-type -> OFL generic family table from AmsPayload.cpp.
Reads MoonrakerPrinterAgent::map_filament_type_to_generic_id's type_to_ofl_family
initializer so the check tracks the C++ normalization without a duplicated list.
Reads map_filament_type_to_generic_id's type_to_ofl_family initializer so the
check tracks the C++ normalization without a duplicated list.
"""
with open(MOONRAKER_AGENT_CPP, encoding="utf-8") as f:
with open(AMS_PAYLOAD_CPP, encoding="utf-8") as f:
src = f.read()
m = re.search(r"type_to_ofl_family\s*=\s*\{(.*?)\n\s*\};", src, re.DOTALL)
if not m:
raise RuntimeError(f"type_to_ofl_family table not found in {MOONRAKER_AGENT_CPP}")
raise RuntimeError(f"type_to_ofl_family table not found in {AMS_PAYLOAD_CPP}")
pairs = re.findall(r'\{\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\}', m.group(1))
if not pairs:
raise RuntimeError("type_to_ofl_family table parsed empty")
+3 -1
View File
@@ -740,6 +740,8 @@ set(SLIC3R_GUI_SOURCES
Utils/OrcaMqttConnection.hpp
Utils/OrcaPrinterAgent.cpp
Utils/OrcaPrinterAgent.hpp
Utils/AmsPayload.cpp
Utils/AmsPayload.hpp
Utils/OrcaCloudSignalingChannel.cpp
Utils/OrcaCloudSignalingChannel.hpp
Utils/QidiPrinterAgent.cpp
@@ -856,7 +858,7 @@ target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DI
if (WIN32)
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include)
target_link_libraries(libslic3r_gui Advapi32 Crypt32)
target_link_libraries(libslic3r_gui Advapi32)
endif()
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES})
+1 -1
View File
@@ -583,7 +583,7 @@ namespace Slic3r
<< " printer_agent_id=" << it->second->printer_agent_id
<< " connection_type=" << it->second->connection_type()
<< " dev_connection_type=" << it->second->dev_connection_type;
} else if (!dev_id.empty()) {
} else {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: target machine was not found in the current agent's machine list";
return false;
}
+46 -20
View File
@@ -1487,17 +1487,29 @@ int MachineObject::command_upgrade_module(std::string url, std::string module_ty
int MachineObject::command_xyz_abs()
{
return command_with_dialog(m_agent->command_xyz_abs(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer()));
if (!m_agent) return -1;
int rtn = m_agent->command_xyz_abs(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_auto_leveling()
{
return command_with_dialog(m_agent->command_auto_leveling(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer()));
if (!m_agent) return -1;
int rtn = m_agent->command_auto_leveling(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_go_home()
{
return command_with_dialog(m_agent->command_go_home(get_dev_id(), this->is_in_printing(), m_support_mqtt_homing, MachineObject::m_sequence_id++, is_lan_mode_printer()));
if (!m_agent) return -1;
int rtn = m_agent->command_go_home(get_dev_id(), this->is_in_printing(), m_support_mqtt_homing, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_task_partskip(std::vector<int> part_ids)
@@ -1619,12 +1631,20 @@ int MachineObject::command_stop_buzzer()
int MachineObject::command_set_bed(int temp)
{
return command_with_dialog(m_agent->command_set_bed(get_dev_id(), temp, m_support_mqtt_bet_ctrl, MachineObject::m_sequence_id++, is_lan_mode_printer()));
if (!m_agent) return -1;
int rtn = m_agent->command_set_bed(get_dev_id(), temp, m_support_mqtt_bet_ctrl, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_set_nozzle(int temp)
{
return command_with_dialog(m_agent->command_set_nozzle(get_dev_id(), temp, MachineObject::m_sequence_id++, is_lan_mode_printer()));
if (!m_agent) return -1;
int rtn = m_agent->command_set_nozzle(get_dev_id(), temp, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_set_nozzle_new(int nozzle_id, int temp)
@@ -1729,7 +1749,11 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
int MachineObject::command_ams_calibrate(int ams_id)
{
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
if (!m_agent) return -1;
int rtn = m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
@@ -1767,7 +1791,11 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
int MachineObject::command_ams_refresh_rfid(std::string tray_id)
{
return command_with_dialog(m_agent->command_ams_refresh_rfid(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
if (!m_agent) return -1;
int rtn = m_agent->command_ams_refresh_rfid(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id)
@@ -1789,7 +1817,11 @@ int MachineObject::command_start_camera()
int MachineObject::command_ams_select_tray(std::string tray_id)
{
return command_with_dialog(m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
if (!m_agent) return -1;
int rtn = m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_ams_control(std::string action)
@@ -1948,9 +1980,12 @@ int MachineObject::command_ams_air_print_detect(bool air_print_detect)
int MachineObject::command_axis_control(std::string axis, double unit, double input_val, int speed)
{
return command_with_dialog(m_agent->command_axis_control(get_dev_id(), axis, unit, input_val, speed, is_core_xy(),
m_support_mqtt_axis_control, MachineObject::m_sequence_id++,
is_lan_mode_printer()));
if (!m_agent) return -1;
int rtn = m_agent->command_axis_control(get_dev_id(), axis, unit, input_val, speed, is_core_xy(),
m_support_mqtt_axis_control, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_extruder_control(int nozzle_id, double val)
@@ -6010,15 +6045,6 @@ bool MachineObject::HasAms() const
return m_fila_system->HasAms();
}
int MachineObject::command_with_dialog(int cmd_result)
{
if (!m_agent)
return -1;
if (cmd_result == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || cmd_result == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(cmd_result);
return cmd_result;
}
void change_the_opacity(wxColour& colour)
{
if (colour.Alpha() == 255) {
+1 -1
View File
@@ -976,7 +976,7 @@ public:
void command_set_save_remote_print_file_to_storage(bool save);
private:
int command_with_dialog(int cmd_result);
/* xcam door open check*/
bool is_support_door_open_check = false;
DoorOpenCheckState xcam_door_open_check = DoorOpenCheckState::DOOR_OPEN_CHECK_DISABLE;
+20
View File
@@ -3,6 +3,10 @@
#include <wx/mediactrl.h>
#include <wx/uri.h>
#include <memory>
#include <slic3r/Utils/IPrinterAgent.hpp>
namespace Slic3r { namespace GUI {
class IMediaController
@@ -12,6 +16,13 @@ public:
virtual void Load(wxURI url) = 0;
// The default keeps existing media controllers unaware of camera-specific modes.
virtual void Load(wxURI url, CameraStreamMode mode)
{
(void) mode;
Load(url);
}
virtual void Play() = 0;
virtual void Stop() = 0;
@@ -21,6 +32,15 @@ public:
virtual int GetLastError() const { return {}; };
virtual wxSize GetVideoSize() const { return {}; };
virtual void StartSession(std::unique_ptr<ICameraSignalingChannel> channel)
{
(void) channel;
}
virtual void StopSession() {}
private:
};
}} // namespace Slic3r::GUI
+194 -197
View File
@@ -1,6 +1,4 @@
#include "MediaPlayCtrl.h"
#include "WebMediaController.hpp"
#include "IPrinterAgent.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/Label.hpp"
@@ -26,7 +24,6 @@
#include <boost/nowide/fstream.hpp>
#include <boost/nowide/utf8_codecvt.hpp>
#include <slic3r/GUI/DeviceManager.hpp>
#include <wx/mediactrl.h>
#undef pid_t
#include <boost/process.hpp>
#ifdef __WIN32__
@@ -55,7 +52,6 @@ namespace GUI {
MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const wxPoint &pos, const wxSize &size)
: wxPanel(parent, wxID_ANY, pos, size)
, m_media_ctrl(media_ctrl)
, m_active_media_controller(media_ctrl)
{
SetLabel("MediaPlayCtrl");
SetBackgroundColour(*wxWHITE);
@@ -102,10 +98,7 @@ MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const w
});
m_button_play->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &e) { TogglePlay(); });
m_button_play->Bind(wxEVT_RIGHT_UP, [this](auto & e) {
if (m_active_media_controller)
m_active_media_controller->Play();
});
m_button_play->Bind(wxEVT_RIGHT_UP, [this](auto & e) { m_media_ctrl->Play(); });
// Orca: live-view FAQ link binding removed (vendor URL)
Bind(wxEVT_RIGHT_UP, [this](auto & e) {
@@ -150,12 +143,14 @@ MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const w
MediaPlayCtrl::~MediaPlayCtrl()
{
m_webrtc_stopping = true;
if (m_webrtc_ctrl)
m_webrtc_ctrl->Stop();
m_webrtc_ctrl->StopSession();
m_media_ctrl->EndExternalStream();
m_webrtc_stopping = false;
{
boost::unique_lock lock(m_mutex);
m_tasks.push_back({"<exit>", nullptr});
m_tasks.push_back("<exit>");
m_cond.notify_all();
}
while (!m_thread.try_join_for(boost::chrono::milliseconds(10))) {
@@ -168,33 +163,6 @@ MediaPlayCtrl::~MediaPlayCtrl()
void MediaPlayCtrl::SetWebMediaController(IMediaController *ctrl)
{
m_web_ctrl = ctrl;
set_active_media_controller(current_mode());
}
void MediaPlayCtrl::set_active_media_controller(CameraStreamMode mode)
{
switch (mode) {
case CameraStreamMode::http_snapshot:
if (auto *web_ctrl = dynamic_cast<WebMediaController *>(m_web_ctrl))
web_ctrl->set_mode(mode);
m_active_media_controller = m_web_ctrl;
break;
case CameraStreamMode::webrtc:
if (!m_webrtc_ctrl) {
m_webrtc_ctrl = std::make_unique<WebRtcMediaController>(
[this](const wxImage& image, wxSize size) { m_media_ctrl->SetExternalFrame(image, size); },
[this, token = std::weak_ptr<int>(m_token)](WebRtcMediaController::Status status) {
if (token.expired())
return;
CallAfter([this, status] { on_webrtc_status(status); });
});
}
m_active_media_controller = m_webrtc_ctrl.get();
break;
default:
m_active_media_controller = m_media_ctrl;
break;
}
}
CameraStreamMode MediaPlayCtrl::current_mode() const
@@ -213,96 +181,105 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
}
m_last_mode = mode;
}
set_active_media_controller(mode);
const bool uses_local_camera_url = mode == CameraStreamMode::http || mode == CameraStreamMode::https ||
mode == CameraStreamMode::http_snapshot || mode == CameraStreamMode::rtsp;
const bool uses_webrtc = mode == CameraStreamMode::webrtc;
const std::string machine = obj ? obj->get_dev_id() : "";
bool changed = false;
if (uses_local_camera_url) {
switch (mode) {
case CameraStreamMode::http:
case CameraStreamMode::https:
case CameraStreamMode::http_snapshot:
case CameraStreamMode::rtsp: {
std::string machine = obj ? obj->get_dev_id() : "";
auto agent = wxGetApp().getAgent();
std::string url = agent ? agent->get_local_camera_stream_url() : "";
m_camera_exists = !url.empty();
Enable(obj && m_camera_exists);
changed = machine != m_machine || url != m_agent_camera_url;
bool changed = machine != m_machine || url != m_agent_camera_url;
m_machine = machine;
m_agent_camera_url = url;
m_url = from_u8(url);
} else if (uses_webrtc) {
m_camera_exists = obj != nullptr;
Enable(obj != nullptr);
changed = machine != m_machine;
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::SetMachineObject webrtc: changed=" << changed
<< " last_state=" << m_last_state << " web_user_stopped=" << m_web_user_stopped;
m_url.clear();
m_agent_camera_url.clear();
} else {
if (obj) {
m_camera_exists = obj->has_ipcam;
m_dev_ver = obj->get_ota_version();
m_lan_mode = obj->is_lan_mode_printer();
m_lan_proto = obj->liveview_local;
m_remote_proto = obj->get_liveview_remote();
m_lan_ip = obj->get_dev_ip();
m_lan_passwd = obj->get_access_code();
m_device_busy = obj->is_camera_busy_off();
m_tutk_state = obj->tutk_state;
if (DevPrinterConfigUtil::get_printer_series_str(obj->printer_type) == "series_o" && BBLNetworkPlugin::instance().use_legacy_network()) {
// Legacy plugin cannot support remote play for H2D, force using local mode
m_remote_proto = LiveviewRemote::LVR_None;
}
} else {
m_camera_exists = false;
m_lan_mode = false;
m_lan_proto = LiveviewLocal::LVL_None;
m_lan_ip.clear();
m_lan_passwd.clear();
m_dev_ver.clear();
m_tutk_state.clear();
m_remote_proto = 0;
m_device_busy = false;
}
Enable(obj && obj->is_info_ready() && obj->m_push_count > 0);
if (machine == m_machine)
m_url = from_u8(url);
if (!changed) {
return;
m_machine = machine;
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl switch machine: " << m_machine;
m_disable_lan = false;
m_failed_retry = 0;
m_last_failed_codes.clear();
m_last_user_play = wxDateTime::Now();
std::string stream_url;
if (get_stream_url(&stream_url)) {
m_streaming = boost::algorithm::contains(stream_url, "device=" + m_machine);
} else {
m_streaming = false;
}
// A genuine machine/URL switch: not a failure, so drop any pending
// failure back-off before (re)starting on the new target.
m_web_user_stopped = false;
m_failed_code = 0;
m_failed_retry = 0;
m_next_retry = wxDateTime();
if (m_last_state != MEDIASTATE_IDLE)
Stop(" ");
if (m_next_retry.IsValid()) // Try open 2 seconds later, to avoid state conflict
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(2);
else
SetStatus("", false);
return;
}
m_machine = machine;
if (!changed)
case CameraStreamMode::webrtc: {
std::string machine = obj ? obj->get_dev_id() : "";
m_camera_exists = obj != nullptr;
Enable(obj != nullptr);
const bool changed = machine != m_machine;
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::SetMachineObject webrtc: changed=" << changed
<< " last_state=" << m_last_state << " web_user_stopped=" << m_web_user_stopped;
m_machine = machine;
m_url.clear();
m_agent_camera_url.clear();
if (!changed) {
return;
}
m_web_user_stopped = false;
if (m_last_state != MEDIASTATE_IDLE)
Stop(" ");
return;
}
default:
break;
}
// A genuine target switch is not a stream failure and should clear the
// manual-stop state before the new target is allowed to play.
m_web_user_stopped = false;
if (uses_local_camera_url) {
m_failed_code = 0;
m_failed_retry = 0;
m_next_retry = wxDateTime();
std::string machine = obj ? obj->get_dev_id() : "";
if (obj) {
m_camera_exists = obj->has_ipcam;
m_dev_ver = obj->get_ota_version();
m_lan_mode = obj->is_lan_mode_printer();
m_lan_proto = obj->liveview_local;
m_remote_proto = obj->get_liveview_remote();
m_lan_ip = obj->get_dev_ip();
m_lan_passwd = obj->get_access_code();
m_device_busy = obj->is_camera_busy_off();
m_tutk_state = obj->tutk_state;
if (DevPrinterConfigUtil::get_printer_series_str(obj->printer_type) == "series_o" && BBLNetworkPlugin::instance().use_legacy_network()) {
// Legacy plugin cannot support remote play for H2D, force using local mode
m_remote_proto = LiveviewRemote::LVR_None;
}
} else {
m_camera_exists = false;
m_lan_mode = false;
m_lan_proto = LiveviewLocal::LVL_None;
m_lan_ip.clear();
m_lan_passwd.clear();
m_dev_ver.clear();
m_tutk_state.clear();
m_remote_proto = 0;
m_device_busy = false;
}
Enable(obj && obj->is_info_ready() && obj->m_push_count > 0);
if (machine == m_machine) {
return;
}
m_machine = machine;
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl switch machine: " << m_machine;
m_disable_lan = false;
m_failed_retry = 0;
m_last_failed_codes.clear();
m_last_user_play = wxDateTime::Now();
std::string stream_url;
if (get_stream_url(&stream_url)) {
m_streaming = boost::algorithm::contains(stream_url, "device=" + m_machine);
} else {
m_streaming = false;
}
if (m_last_state != MEDIASTATE_IDLE)
Stop(" ");
if (m_next_retry.IsValid()) // Try open 2 seconds later, to avoid state conflict
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(2);
else
SetStatus("", false);
}
wxString hide_id_middle_string(wxString const &str, size_t offset = 0, size_t length = -1)
@@ -353,71 +330,97 @@ void refresh_agora_url(char const* device, char const* dev_ver, char const* chan
void MediaPlayCtrl::Play()
{
if ((m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry) || !IsShownOnScreen() || m_last_state != MEDIASTATE_IDLE)
return;
const CameraStreamMode mode = current_mode();
set_active_media_controller(mode);
m_last_mode = mode;
auto agent = wxGetApp().getAgent();
auto printer_agent = agent ? agent->get_printer_agent() : nullptr;
const bool is_bbl = (printer_agent ? printer_agent->get_agent_info().id : "") == BBL_PRINTER_AGENT_ID;
if (!is_bbl) {
const bool is_webrtc = mode == CameraStreamMode::webrtc;
const bool is_snapshot = mode == CameraStreamMode::http_snapshot;
const bool is_http_stream = mode == CameraStreamMode::http || mode == CameraStreamMode::https || mode == CameraStreamMode::rtsp;
if (!is_webrtc && !is_snapshot && !is_http_stream)
switch (current_mode()) {
case CameraStreamMode::http_snapshot:
if (!m_next_retry.IsValid() || wxDateTime::Now() < m_next_retry)
return;
auto *webrtc_ctrl = is_webrtc ? dynamic_cast<WebRtcMediaController *>(m_active_media_controller) : nullptr;
if (is_webrtc && (!webrtc_ctrl || !m_media_ctrl)) {
if (!IsShownOnScreen()) return;
if (m_last_state != MEDIASTATE_IDLE) return;
if (m_machine.empty() || !IsEnabled() || !m_camera_exists || m_url.IsEmpty() || !m_web_ctrl) {
Stop(_L("Please confirm if the printer is connected."));
return;
}
if (webrtc_ctrl && webrtc_ctrl->is_active())
return;
m_failed_code = 0;
if (!m_active_media_controller || m_machine.empty() || !IsEnabled() || !m_camera_exists ||
(!is_webrtc && m_url.IsEmpty())) {
Stop(_L("Please confirm if the printer is connected."));
return;
}
if (is_webrtc) {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play webrtc: last_state=" << m_last_state << " failed_retry=" << m_failed_retry
<< " shown=" << IsShownOnScreen();
auto channel = agent ? agent->create_camera_signaling_channel(m_machine) : nullptr;
if (!channel) {
Stop(_L("Sign in to OrcaCloud to view the camera."));
return;
}
webrtc_ctrl->set_signalling_channel(std::move(channel));
m_media_ctrl->BeginExternalStream();
m_last_state = MEDIASTATE_INITIALIZING;
SetStatus(_L("Initializing..."), false);
} else if (is_snapshot) {
m_last_state = wxMEDIASTATE_PLAYING;
SetStatus(_L("Playing..."), false);
}
m_button_play->SetIcon("media_stop");
if (m_active_media_controller == m_media_ctrl) {
// wxMediaCtrl3 reports when it has a decoded frame; load() waits for
// that event before queuing Play so the stream is not marked stopped.
load();
} else {
m_active_media_controller->Load(wxURI(m_url));
m_active_media_controller->Play();
m_web_ctrl->Load(wxURI(m_url), current_mode());
m_web_ctrl->Play();
m_last_state = wxMEDIASTATE_PLAYING;
SetStatus(_L("Playing..."), false);
return;
case CameraStreamMode::http:
case CameraStreamMode::https:
case CameraStreamMode::rtsp:
if (m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry)
return;
if (!IsShownOnScreen()) return;
if (m_last_state != MEDIASTATE_IDLE) return;
m_failed_code = 0;
if (m_machine.empty() || !IsEnabled() || !m_camera_exists || m_url.IsEmpty()) {
Stop(_L("Please confirm if the printer is connected."));
return;
}
if (webrtc_ctrl) {
m_webrtc_epoch = webrtc_ctrl->epoch();
m_button_play->SetIcon("media_stop");
load();
return;
case CameraStreamMode::webrtc: {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play webrtc: last_state=" << m_last_state
<< " next_retry_valid=" << m_next_retry.IsValid()
<< " next_retry_future=" << (m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry)
<< " failed_retry=" << m_failed_retry << " shown=" << IsShownOnScreen();
if (m_webrtc_ctrl && m_webrtc_ctrl->is_active()) {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play webrtc: session already active, ignoring";
return;
}
if (m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry)
return;
if (!IsShownOnScreen() || m_last_state != MEDIASTATE_IDLE)
return;
m_failed_code = 0;
if (m_machine.empty() || !IsEnabled() || !m_camera_exists) {
Stop(_L("Please confirm if the printer is connected."));
return;
}
auto agent = wxGetApp().getAgent();
auto channel = agent ? agent->create_camera_signaling_channel(m_machine) : nullptr;
if (!channel) {
Stop(_L("Sign in to OrcaCloud to view the camera."));
return;
}
if (!m_webrtc_ctrl) {
m_webrtc_ctrl = std::make_unique<WebRtcMediaController>(
[this](const wxImage& image, wxSize size) { m_media_ctrl->SetExternalFrame(image, size); },
[this, token = std::weak_ptr<int>(m_token)](WebRtcMediaController::Status status) {
if (token.expired())
return;
CallAfter([this, status] { on_webrtc_status(status); });
});
}
m_button_play->SetIcon("media_stop");
m_media_ctrl->BeginExternalStream();
m_last_state = MEDIASTATE_INITIALIZING;
SetStatus(_L("Initializing..."), false);
m_webrtc_stopping = false;
m_webrtc_ctrl->StartSession(std::move(channel));
m_webrtc_epoch = m_webrtc_ctrl->epoch();
return;
}
default: // assumed to be CameraStreamMode::none
if (NetworkAgent* agent = wxGetApp().getAgent()) {
if (auto printer_agent = agent->get_printer_agent()) {
if (printer_agent->get_agent_info().id != BBL_PRINTER_AGENT_ID) {
return;
}
}
}
break;
}
if (!m_next_retry.IsValid() || wxDateTime::Now() < m_next_retry)
return;
if (!IsShownOnScreen())
return;
if (m_last_state != MEDIASTATE_IDLE) {
return;
}
m_failed_code = 0;
if (m_machine.empty()) {
Stop(_L("Please confirm if the printer is connected."));
@@ -438,6 +441,7 @@ void MediaPlayCtrl::Play()
}
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play: " << m_lan_proto << m_remote_proto << m_disable_lan;
NetworkAgent *agent = wxGetApp().getAgent();
std::string agent_version = agent ? agent->get_version() : "";
if (m_lan_proto > LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) {
m_disable_lan = m_remote_proto && !m_lan_mode; // try remote next time
@@ -541,8 +545,8 @@ void MediaPlayCtrl::StopWebStream()
{
if (m_last_state == MEDIASTATE_IDLE)
return;
if (m_active_media_controller && m_active_media_controller == m_web_ctrl)
m_active_media_controller->Stop();
if (m_web_ctrl)
m_web_ctrl->Stop();
m_button_play->SetIcon("media_play");
m_last_state = MEDIASTATE_IDLE;
SetStatus(_L("Video Stopped."), false);
@@ -550,14 +554,15 @@ void MediaPlayCtrl::StopWebStream()
void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
{
const bool webrtc_active = m_last_mode == CameraStreamMode::webrtc &&
dynamic_cast<WebRtcMediaController *>(m_active_media_controller) != nullptr;
const bool webrtc_active = m_webrtc_ctrl && m_last_mode == CameraStreamMode::webrtc;
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Stop: last_state=" << m_last_state
<< " webrtc_active=" << webrtc_active << " failed_code=" << m_failed_code
<< " msg='" << msg.ToUTF8().data() << "'";
if (webrtc_active) {
m_active_media_controller->Stop();
m_webrtc_stopping = true;
m_webrtc_ctrl->StopSession();
m_media_ctrl->EndExternalStream();
m_webrtc_stopping = false;
}
switch (m_last_mode) {
case CameraStreamMode::http:
@@ -566,14 +571,13 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
const bool snapshot = m_last_mode == CameraStreamMode::http_snapshot;
if (m_last_state != MEDIASTATE_IDLE) {
if (snapshot) {
if (m_active_media_controller)
m_active_media_controller->Stop();
if (m_web_ctrl) m_web_ctrl->Stop();
} else {
// http/https mode plays through the ffmpeg backend (m_media_ctrl), not
// the webview - tear its read thread down too, otherwise it keeps
// pulling and painting frames after the UI says "Video Stopped".
boost::unique_lock lock(m_mutex);
m_tasks.push_back({"<stop>", m_active_media_controller});
m_tasks.push_back("<stop>");
m_cond.notify_all();
}
m_button_play->SetIcon("media_play");
@@ -607,8 +611,7 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
m_media_ctrl->InvalidateBestSize();
m_button_play->SetIcon("media_play");
boost::unique_lock lock(m_mutex);
if (!webrtc_active)
m_tasks.push_back({"<stop>", m_active_media_controller});
m_tasks.push_back("<stop>");
m_cond.notify_all();
if (!msg.IsEmpty())
SetStatus(msg);
@@ -675,7 +678,7 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
void MediaPlayCtrl::on_webrtc_status(WebRtcMediaController::Status status)
{
// Drop CallAfter-queued events from a superseded Play attempt.
// Drop CallAfter-queued events from a superseded StartSession attempt.
if (status.epoch != m_webrtc_epoch)
return;
if (status.kind == WebRtcMediaController::Status::Connecting) {
@@ -836,9 +839,7 @@ void MediaPlayCtrl::jump_to_play()
void MediaPlayCtrl::onStateChanged(wxMediaEvent &event)
{
auto last_state = m_last_state;
if (m_active_media_controller != m_media_ctrl)
return;
auto state = m_active_media_controller->GetState();
auto state = m_media_ctrl->GetState();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::onStateChanged: " << state << ", last_state: " << last_state;
if ((int) state < 0) return;
{
@@ -850,14 +851,14 @@ void MediaPlayCtrl::onStateChanged(wxMediaEvent &event)
}
if ((last_state == MEDIASTATE_IDLE || last_state == MEDIASTATE_INITIALIZING) && state == wxMEDIASTATE_STOPPED) { return; }
if ((last_state == wxMEDIASTATE_PAUSED || last_state == wxMEDIASTATE_PLAYING) && state == wxMEDIASTATE_STOPPED) {
m_failed_code = m_active_media_controller->GetLastError();
m_failed_code = m_media_ctrl->GetLastError();
Stop();
return;
}
if (last_state == MEDIASTATE_LOADING && (state == wxMEDIASTATE_STOPPED || state == wxMEDIASTATE_PAUSED)) {
wxSize size = m_active_media_controller->GetVideoSize();
wxSize size = m_media_ctrl->GetVideoSize();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::onStateChanged: size: " << size.x << "x" << size.y;
m_failed_code = m_active_media_controller->GetLastError();
m_failed_code = m_media_ctrl->GetLastError();
if (size.GetWidth() >= 320) {
m_last_state = state;
m_failed_code = 0;
@@ -867,7 +868,7 @@ void MediaPlayCtrl::onStateChanged(wxMediaEvent &event)
m_failed_retry = 0;
m_disable_lan = false;
boost::unique_lock lock(m_mutex);
m_tasks.push_back({"<play>", m_active_media_controller});
m_tasks.push_back("<play>");
m_cond.notify_all();
} else if (event.GetId()) {
if (m_failed_code == 0)
@@ -922,7 +923,7 @@ void MediaPlayCtrl::load()
m_url = m_url + "&dump_info=" + boost::lexical_cast<std::string>(dump_info_file);
}
boost::unique_lock lock(m_mutex);
m_tasks.push_back({m_url, m_active_media_controller});
m_tasks.push_back(m_url);
m_cond.notify_all();
}
@@ -942,34 +943,30 @@ void MediaPlayCtrl::media_proc()
while (m_tasks.empty()) {
m_cond.wait(lock);
}
MediaTask task = m_tasks.front();
if (m_tasks.size() >= 2 && !task.command.IsEmpty() && task.command[0] != '<' &&
m_tasks[1].command == "<stop>" && task.controller == m_tasks[1].controller) {
BOOST_LOG_TRIVIAL(trace) << "MediaPlayCtrl: busy skip url: " << task.command;
wxString url = m_tasks.front();
if (m_tasks.size() >= 2 && !url.IsEmpty() && url[0] != '<' && m_tasks[1] == "<stop>") {
BOOST_LOG_TRIVIAL(trace) << "MediaPlayCtrl: busy skip url: " << url;
m_tasks.pop_front();
m_tasks.pop_front();
continue;
}
lock.unlock();
if (task.command == "<stop>") {
if (url == "<stop>") {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start stop";
if (task.controller)
task.controller->Stop();
m_media_ctrl->Stop();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end stop";
}
else if (task.command == "<exit>") {
else if (url == "<exit>") {
break;
}
else if (task.command == "<play>") {
else if (url == "<play>") {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start play";
if (task.controller)
task.controller->Play();
m_media_ctrl->Play();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end play";
}
else {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start load";
if (task.controller)
task.controller->Load(wxURI(task.command));
m_media_ctrl->Load(wxURI(url));
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end load";
}
lock.lock();
+2 -7
View File
@@ -75,7 +75,6 @@ private:
static bool get_stream_url(std::string *url = nullptr);
CameraStreamMode current_mode() const;
void set_active_media_controller(CameraStreamMode mode);
private:
static inline const wxMediaState MEDIASTATE_IDLE = static_cast<wxMediaState>(3);
@@ -87,10 +86,10 @@ private:
std::shared_ptr<int> m_token = std::make_shared<int>(0);
wxMediaCtrl3 * m_media_ctrl;
IMediaController * m_active_media_controller = nullptr;
IMediaController * m_web_ctrl = nullptr;
std::unique_ptr<WebRtcMediaController> m_webrtc_ctrl;
CameraStreamMode m_last_mode = CameraStreamMode::none;
bool m_webrtc_stopping = false;
std::uint64_t m_webrtc_epoch = 0;
std::string m_agent_camera_url;
bool m_web_user_stopped = false;
@@ -109,11 +108,7 @@ private:
bool m_disable_lan = false;
wxString m_url;
struct MediaTask {
wxString command;
IMediaController *controller = nullptr;
};
std::deque<MediaTask> m_tasks;
std::deque<wxString> m_tasks;
boost::mutex m_mutex;
boost::condition_variable m_cond;
boost::thread m_thread;
+23 -6
View File
@@ -727,6 +727,7 @@ struct Sidebar::priv
ScalableButton * m_bpButton_add_filament;
ScalableButton * m_bpButton_del_filament;
ScalableButton * m_bpButton_ams_filament;
bool m_ams_sync_button_show{true}; // last applied AMS sync button visibility
ScalableButton * m_bpButton_set_filament;
int m_menu_filament_id = -1;
@@ -3427,6 +3428,22 @@ void Sidebar::remove_unused_filament_combos(const size_t current_extruder_count)
}
}
void Sidebar::update_ams_sync_button()
{
if (!p->m_bpButton_ams_filament || !wxGetApp().preset_bundle)
return;
// BBL printers always advertise AMS sync; other agents follow the active
// agent's filament sync mode, which flips to subscription only after the
// printer's get_capabilities reply arrives.
const bool show = wxGetApp().preset_bundle->use_bbl_network() ||
(wxGetApp().getAgent() && wxGetApp().getAgent()->get_filament_sync_mode() != FilamentSyncMode::none);
if (p->m_ams_sync_button_show == show)
return;
p->m_ams_sync_button_show = show;
p->m_bpButton_ams_filament->Show(show);
p->m_bpButton_ams_filament->GetParent()->Layout();
}
void Sidebar::update_all_preset_comboboxes()
{
PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
@@ -3444,7 +3461,7 @@ void Sidebar::update_all_preset_comboboxes()
//p->btn_connect_printer->Hide();
p->m_printer_connect->Hide();
//only show sync-ams button for BBL printer
p->m_bpButton_ams_filament->Show();
update_ams_sync_button();
//update print button default value for bbl or third-party printer
p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate);
} else {
@@ -3453,11 +3470,7 @@ void Sidebar::update_all_preset_comboboxes()
p->m_printer_connect->Show(!use_printer_agents);
// ORCA: show/hide sync-ams button based on filament sync mode
auto agent = wxGetApp().getAgent();
if (agent && agent->get_filament_sync_mode() != FilamentSyncMode::none)
p->m_bpButton_ams_filament->Show();
else
p->m_bpButton_ams_filament->Hide();
update_ams_sync_button();
// Orca: with "Support 3MF as gcode" (use_3mf) the local export is a .gcode.3mf bundle, so when no
// printer host/IP is configured the default action is "Export plate sliced file" (mirrors the
@@ -22152,10 +22165,14 @@ void Plater::update_machine_sync_status()
DeviceManager *dev_maneger = wxGetApp().getDeviceManager();
if (!dev_maneger) {
GUI::wxGetApp().sidebar().update_sync_status(nullptr);
GUI::wxGetApp().sidebar().update_ams_sync_button();
return;
}
MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine();
GUI::wxGetApp().sidebar().update_sync_status(obj);
// The agent's sync mode flips once the printer's get_capabilities reply
// arrives; re-evaluate the filament-sync button on every device update.
GUI::wxGetApp().sidebar().update_ams_sync_button();
}
bool Plater::get_machine_sync_status()
+2
View File
@@ -169,6 +169,8 @@ public:
void init_filament_combo(PlaterPresetComboBox **combo, const int filament_idx);
void remove_unused_filament_combos(const size_t current_extruder_count);
void update_all_preset_comboboxes();
// Show/hide the AMS filament-sync button from the active agent's sync mode.
void update_ams_sync_button();
//void update_partplate(PartPlateList& list);
void update_presets(Slic3r::Preset::Type preset_type);
//BBS
+3 -5
View File
@@ -13,13 +13,11 @@ WebMediaController::WebMediaController(wxWebView* webview) : m_webview(webview)
m_webview->SetPage("<html><head><style>html,body{margin:0;height:100%;background:#000;}</style></head><body></body></html>", "");
}
void WebMediaController::Load(wxURI url)
{
m_url = url.BuildURI().ToStdString();
}
void WebMediaController::Load(wxURI url) { Load(url, CameraStreamMode::http); }
void WebMediaController::set_mode(CameraStreamMode mode)
void WebMediaController::Load(wxURI url, CameraStreamMode mode)
{
m_url = url.BuildURI().ToStdString();
m_stream_mode = mode;
}
+1 -2
View File
@@ -1,7 +1,6 @@
#pragma once
#include <slic3r/GUI/IMediaController.hpp>
#include <slic3r/Utils/IPrinterAgent.hpp>
#include <string>
@@ -16,7 +15,7 @@ public:
void Load(wxURI url) override;
void set_mode(CameraStreamMode mode);
void Load(wxURI url, CameraStreamMode mode) override;
void Play() override;
+3 -9
View File
@@ -31,7 +31,7 @@ WebRtcMediaController::WebRtcMediaController(std::function<void(const wxImage&,
WebRtcMediaController::~WebRtcMediaController()
{
Stop();
StopSession();
}
void WebRtcMediaController::report(Status status)
@@ -52,14 +52,8 @@ void WebRtcMediaController::report(Status status)
m_on_status(status);
}
void WebRtcMediaController::set_signalling_channel(std::unique_ptr<ICameraSignalingChannel> channel)
void WebRtcMediaController::StartSession(std::unique_ptr<ICameraSignalingChannel> channel)
{
m_pending_signaling = std::move(channel);
}
void WebRtcMediaController::Play()
{
std::unique_ptr<ICameraSignalingChannel> channel = std::move(m_pending_signaling);
// Tear down any previous attempt WITHOUT notifying: the Stopped that would
// otherwise be delivered (async, via CallAfter) races the new attempt's
// Connecting and makes the consumer cancel a session that is mid-connect.
@@ -107,7 +101,7 @@ void WebRtcMediaController::Play()
signaling->open();
}
void WebRtcMediaController::Stop()
void WebRtcMediaController::StopSession()
{
teardown(true);
}
+5 -6
View File
@@ -1,7 +1,6 @@
#pragma once
#include "IMediaController.hpp"
#include <slic3r/Utils/IPrinterAgent.hpp>
#include <wx/image.h>
@@ -37,7 +36,7 @@ public:
DECODE_ERROR,
TIMEOUT,
} code = ICE_FAILED;
// Identifies the Play attempt this status belongs to, so the
// Identifies the StartSession attempt this status belongs to, so the
// consumer can drop CallAfter-queued events from a superseded attempt.
std::uint64_t epoch = 0;
};
@@ -46,13 +45,14 @@ public:
std::function<void(Status)> on_status);
~WebRtcMediaController() override;
void set_signalling_channel(std::unique_ptr<ICameraSignalingChannel> channel);
void StartSession(std::unique_ptr<ICameraSignalingChannel> channel) override;
void StopSession() override;
std::uint64_t epoch() const { return m_epoch.load(); }
bool is_active() const { return m_alive.load(); }
void Load(wxURI) override {}
void Play() override;
void Stop() override;
void Play() override {}
void Stop() override { StopSession(); }
wxMediaState GetState() override;
wxSize GetVideoSize() const override;
@@ -75,7 +75,6 @@ private:
// addRemoteCandidate until a remote description is set, so buffer them.
std::vector<std::pair<std::string, std::string>> m_pending_candidates;
bool m_remote_description_set = false;
std::unique_ptr<ICameraSignalingChannel> m_pending_signaling;
std::unique_ptr<ICameraSignalingChannel> m_signaling;
std::shared_ptr<rtc::PeerConnection> m_peer_connection;
std::shared_ptr<rtc::DataChannel> m_data_channel;
+2 -2
View File
@@ -163,13 +163,13 @@ wxMediaState wxMediaCtrl3::GetState()
return m_state;
}
int wxMediaCtrl3::GetLastError() const
int wxMediaCtrl3::GetLastError()
{
std::unique_lock<std::mutex> lk(m_mutex);
return m_error;
}
wxSize wxMediaCtrl3::GetVideoSize() const
wxSize wxMediaCtrl3::GetVideoSize()
{
std::unique_lock<std::mutex> lk(m_mutex);
return m_video_size;
+9 -10
View File
@@ -13,7 +13,6 @@
#include "wx/bitmap.h"
#include "wx/uri.h"
#include "wx/mediactrl.h"
#include "IMediaController.hpp"
wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
@@ -28,18 +27,18 @@ void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, in
class AVVideoDecoder;
class wxMediaCtrl3 : public wxWindow, public Slic3r::GUI::IMediaController, BambuLib
class wxMediaCtrl3 : public wxWindow, BambuLib
{
public:
wxMediaCtrl3(wxWindow *parent);
~wxMediaCtrl3() override;
~wxMediaCtrl3();
void Load(wxURI url) override;
void Load(wxURI url);
void Play() override;
void Play();
void Stop() override;
void Stop();
// Render frames supplied by a controller which owns its own transport.
// The frame is copied while m_mutex is held; callers may release it after
@@ -53,11 +52,11 @@ public:
void SetIdleImage(wxString const & image);
wxMediaState GetState() override;
wxMediaState GetState();
int GetLastError() const override;
int GetLastError();
wxSize GetVideoSize() const override;
wxSize GetVideoSize();
protected:
DECLARE_EVENT_TABLE()
@@ -94,7 +93,7 @@ private:
std::uint64_t m_last_PTS{0};
std::chrono::system_clock::time_point m_last_PTS_expected;
std::chrono::system_clock::time_point m_last_PTS_practical;
mutable std::mutex m_mutex;
std::mutex m_mutex;
std::condition_variable m_cond;
std::thread m_thread;
std::atomic_bool m_refresh_pending{false};
+493
View File
@@ -0,0 +1,493 @@
#include "AmsPayload.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
#include "slic3r/GUI/DeviceCore/DevManager.h"
#include "slic3r/GUI/DeviceCore/DevStorage.h"
#include "slic3r/GUI/DeviceCore/DevFirmware.h"
#include <boost/algorithm/string.hpp>
#include <boost/log/trivial.hpp>
#include <wx/thread.h>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <map>
#include <mutex>
#include <sstream>
namespace Slic3r {
std::string map_filament_type_to_generic_id(const std::string& filament_type)
{
std::string upper = filament_type;
boost::trim(upper);
std::transform(upper.begin(), upper.end(), upper.begin(),
[](unsigned char c) { return static_cast<char>(std::toupper(c)); });
// Normalize reported material names to an OrcaFilamentLibrary generic family. The
// family's filament_id is resolved from the loaded system presets below rather than
// hardcoded, so profile id re-mints never require touching this table.
// scripts/test_moonraker_lane_data.py parses this initializer; keep the {"A", "B"} format.
static const std::map<std::string, std::string> type_to_ofl_family = {
// PLA variants
{"PLA", "PLA"},
{"PLA-CF", "PLA-CF"},
{"PLA SILK", "PLA Silk"},
{"PLA-SILK", "PLA Silk"},
{"PLA HIGH SPEED", "PLA High Speed"},
{"PLA-HS", "PLA High Speed"},
{"PLA HS", "PLA High Speed"},
// ABS/ASA variants
{"ABS", "ABS"},
{"ASA", "ASA"},
// PETG/PET variants
{"PETG", "PETG"},
{"PET", "PETG"},
{"PCTG", "PCTG"},
// PA/Nylon variants
{"PA", "PA"},
{"NYLON", "PA"},
{"PA-CF", "PA-CF"},
{"PPA", "PPA-CF"},
{"PPA-CF", "PPA-CF"},
{"PPA-GF", "PPA-GF"},
// PC variants
{"PC", "PC"},
// PP/PE variants
{"PE", "PE"},
{"PP", "PP"},
// Support materials
{"PVA", "PVA"},
{"HIPS", "HIPS"},
{"BVOH", "BVOH"},
// TPU variants
{"TPU", "TPU"},
// Other materials
{"EVA", "EVA"},
{"PHA", "PHA"},
{"COPE", "CoPE"},
{"SBS", "SBS"},
};
auto it = type_to_ofl_family.find(upper);
if (it == type_to_ofl_family.end())
return UNKNOWN_FILAMENT_ID;
if (auto* bundle = GUI::wxGetApp().preset_bundle) {
const Preset* preset = bundle->filaments.find_preset("Generic " + it->second + " @System");
if (preset != nullptr && preset->is_system && !preset->filament_id.empty())
return preset->filament_id;
}
// Unknown material, or no loaded preset data to resolve against
return UNKNOWN_FILAMENT_ID;
}
std::string normalize_ams_color(const std::string& color)
{
std::string value = color;
boost::trim(value);
// Remove 0x or 0X prefix if present
if (value.size() >= 2 && (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0)) {
value = value.substr(2);
}
// Remove # prefix if present
if (!value.empty() && value[0] == '#') {
value = value.substr(1);
}
// Extract only hex digits
std::string normalized;
for (char c : value) {
if (std::isxdigit(static_cast<unsigned char>(c))) {
normalized.push_back(static_cast<char>(std::toupper(static_cast<unsigned char>(c))));
}
}
// If 6 hex digits, add FF alpha
if (normalized.size() == 6) {
normalized += "FF";
}
// Validate length - return default if invalid
if (normalized.size() != 8) {
return "00000000";
}
return normalized;
}
bool parse_moonraker_lane_data(const nlohmann::json& body,
std::vector<AmsTrayData>& trays,
int& max_lane_index)
{
// Expected structure: { "result": { "namespace": "lane_data", "value": { "lane1": {...}, ... } } }
if (!body.is_object() || !body.contains("result") || !body["result"].is_object() ||
!body["result"].contains("value") || !body["result"]["value"].is_object()) {
BOOST_LOG_TRIVIAL(warning) << "AmsPayload: unexpected lane_data response structure";
return false;
}
const auto& value = body["result"]["value"];
trays.clear();
max_lane_index = 0;
for (const auto& lane_item : value.items()) {
const auto& lane_obj = lane_item.value();
if (!lane_obj.is_object()) {
continue;
}
// Extract lane index from the "lane" field (tool number, 0-based)
int lane_index = -1;
const auto lane_it = lane_obj.find("lane");
if (lane_it != lane_obj.end() && lane_it->is_string()) {
try {
lane_index = std::stoi(lane_it->get<std::string>());
} catch (...) {
lane_index = -1;
}
}
if (lane_index < 0) {
continue;
}
AmsTrayData tray;
tray.slot_index = lane_index;
if (const auto it = lane_obj.find("color"); it != lane_obj.end() && it->is_string())
tray.tray_color = it->get<std::string>();
if (const auto it = lane_obj.find("material"); it != lane_obj.end() && it->is_string())
tray.tray_type = it->get<std::string>();
if (const auto it = lane_obj.find("bed_temp"); it != lane_obj.end() && it->is_number())
tray.bed_temp = it->get<int>();
if (const auto it = lane_obj.find("nozzle_temp"); it != lane_obj.end() && it->is_number())
tray.nozzle_temp = it->get<int>();
// Presence is independent of declared material: a physically loaded lane
// with no user-declared material must not read as empty. Prefer an
// explicit signal, fall back to the legacy "has a material type" rule.
const auto has_it = lane_obj.find("has_filament");
const auto loaded_it = lane_obj.find("loaded");
if (has_it != lane_obj.end() && has_it->is_boolean())
tray.has_filament = has_it->get<bool>();
else if (loaded_it != lane_obj.end() && loaded_it->is_boolean())
tray.has_filament = loaded_it->get<bool>();
else
tray.has_filament = !tray.tray_type.empty();
max_lane_index = std::max(max_lane_index, lane_index);
trays.push_back(tray);
}
if (trays.empty()) {
BOOST_LOG_TRIVIAL(info) << "AmsPayload: no lanes found";
return false;
}
return true;
}
void resolve_tray_info_idx(std::vector<AmsTrayData>& trays)
{
auto* bundle = GUI::wxGetApp().preset_bundle;
for (auto& tray : trays) {
// Absent lanes render as placeholders; resolving an empty type is busy
// work at best and could bind a bogus id.
if (!tray.has_filament)
continue;
// A vendor-aware resolver may already have matched this lane; only fill
// what is still empty so the generic fallback cannot overwrite it.
if (!tray.tray_info_idx.empty())
continue;
tray.tray_info_idx = bundle
? bundle->filaments.filament_id_by_type(tray.tray_type)
: map_filament_type_to_generic_id(tray.tray_type);
}
}
nlohmann::json build_bbl_ams_json(const std::vector<AmsTrayData>& trays,
int ams_count,
int max_lane_index)
{
nlohmann::json ams_array = nlohmann::json::array();
// ams_exist_bits marks the units; tray_exist_bits marks the occupied
// slots. uint64_t: unsigned long is 32-bit on MSVC, so 1UL << 32+ is UB.
// BBL's fields are 64-bit; a lane past 63 cannot be represented at all.
uint64_t ams_exist_bits = 0;
uint64_t tray_exist_bits = 0;
auto set_exist_bit = [](uint64_t& bits, int index) {
if (index < 0)
return;
if (index >= 64) {
BOOST_LOG_TRIVIAL(warning) << "AmsPayload: lane index " << index << " exceeds the 64-bit exist-bits field; bit dropped";
return;
}
bits |= (uint64_t{1} << index);
};
for (int ams_id = 0; ams_id < ams_count; ++ams_id) {
set_exist_bit(ams_exist_bits, ams_id);
nlohmann::json ams_unit = nlohmann::json::object();
ams_unit["id"] = std::to_string(ams_id);
ams_unit["info"] = "0002"; // treat as AMS_LITE
nlohmann::json tray_array = nlohmann::json::array();
int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4);
for (int slot_id = 0; slot_id <= max_slot_in_this_ams; ++slot_id) {
int slot_index = ams_id * 4 + slot_id;
// Find tray with matching slot_index
const AmsTrayData* tray = nullptr;
for (const auto& t : trays) {
if (t.slot_index == slot_index) {
tray = &t;
break;
}
}
nlohmann::json tray_json = nlohmann::json::object();
tray_json["id"] = std::to_string(slot_id);
tray_json["tag_uid"] = "0000000000000000";
if (tray && tray->has_filament) {
set_exist_bit(tray_exist_bits, slot_index);
// Present lanes never carry tray_slot_placeholder: a loaded but
// undeclared lane stays occupied with an empty type, which is
// what distinguishes it from an absent slot.
tray_json["tray_info_idx"] = tray->tray_info_idx;
tray_json["tray_type"] = tray->tray_type;
tray_json["tray_color"] = normalize_ams_color(tray->tray_color);
// Add temperature data if provided
if (tray->bed_temp > 0) {
tray_json["bed_temp"] = std::to_string(tray->bed_temp);
}
if (tray->nozzle_temp > 0) {
tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp);
}
} else {
tray_json["tray_info_idx"] = "";
tray_json["tray_type"] = "";
tray_json["tray_color"] = "00000000";
tray_json["tray_slot_placeholder"] = "1";
}
tray_array.push_back(tray_json);
}
ams_unit["tray"] = tray_array;
ams_array.push_back(ams_unit);
}
// Format as hex strings (matching BBL protocol)
std::ostringstream ams_exist_ss;
ams_exist_ss << std::hex << std::uppercase << ams_exist_bits;
std::ostringstream tray_exist_ss;
tray_exist_ss << std::hex << std::uppercase << tray_exist_bits;
nlohmann::json ams_json = nlohmann::json::object();
ams_json["ams"] = ams_array;
ams_json["ams_exist_bits"] = ams_exist_ss.str();
ams_json["tray_exist_bits"] = tray_exist_ss.str();
return ams_json;
}
// --- Removal/absence state and op capability ---------------------------------
// Last ams_count rendered per device: clear_ams_payload_for_device walks the
// same unit set to mark them all absent.
static std::mutex g_ams_state_mutex;
static std::map<std::string, int> g_ams_last_count;
static std::map<std::string, std::vector<std::string>> g_ams_ops;
static std::map<std::string, bool> g_ams_capability;
static void remember_ams_count(const std::string& dev_id, int ams_count)
{
if (dev_id.empty() || ams_count <= 0)
return;
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
g_ams_last_count[dev_id] = std::max(g_ams_last_count[dev_id], ams_count);
}
void register_ams_ops(const std::string& dev_id, const std::vector<std::string>& ops)
{
if (dev_id.empty())
return;
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
g_ams_ops[dev_id] = ops;
}
bool ams_op_supported(const std::string& dev_id, const std::string& op)
{
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
auto it = g_ams_ops.find(dev_id);
if (it == g_ams_ops.end())
return true; // no OrcaSonar capabilities seen: never gate
return std::find(it->second.begin(), it->second.end(), op) != it->second.end();
}
void register_ams_capability(const std::string& dev_id, bool has_ams)
{
if (dev_id.empty())
return;
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
g_ams_capability[dev_id] = has_ams;
}
bool has_ams_capability(const std::string& dev_id)
{
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
auto it = g_ams_capability.find(dev_id);
return it != g_ams_capability.end() && it->second;
}
void build_ams_payload_for_device(const std::string& dev_id,
const std::optional<std::string>& printer_type,
int ams_count,
int max_lane_index,
const std::vector<AmsTrayData>& trays,
const QueueOnMainFn& queue_fn,
const TrayInfoResolver& vendor_resolver)
{
remember_ams_count(dev_id, ams_count);
// A caller on the GUI thread must mutate DeviceManager inline: invoking
// queue_fn (CallAfter) would defer the work until after the caller has
// already read DevFilaSystem. Background callers route through queue_fn.
const bool on_main = wxIsMainThread();
auto apply = [dev_id, printer_type, ams_count, max_lane_index, trays, vendor_resolver]() {
// Look up MachineObject via DeviceManager
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
if (!dev_manager) {
return;
}
MachineObject* obj = dev_manager->get_my_machine(dev_id);
if (!obj) {
return;
}
// Resolve preset ids here: reads the GUI preset bundle, which is only safe
// on the main thread. trays is a copy, so mutating it is fine. The optional
// vendor resolver runs first; the generic resolver fills the rest.
std::vector<AmsTrayData> resolved = trays;
if (vendor_resolver)
vendor_resolver(resolved);
resolve_tray_info_idx(resolved);
// Wrap in the expected structure for ParseV1_0
nlohmann::json print_json = nlohmann::json::object();
print_json["ams"] = build_bbl_ams_json(resolved, ams_count, max_lane_index);
// Call the parser to populate DevFilaSystem
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
BOOST_LOG_TRIVIAL(info) << "AmsPayload: parsed " << resolved.size() << " trays";
// Set printer_type so update_sync_status() can match it against the preset's
// printer type. nullopt = leave it (the caller's push_status already carries
// it); a value = assign, including empty, preserving the old behavior.
if (printer_type.has_value()) {
obj->printer_type = *printer_type;
}
// Set push counters so is_info_ready() returns true for pull-mode agents.
if (obj->m_push_count == 0) {
obj->m_push_count = 1;
}
if (obj->m_full_msg_count == 0) {
obj->m_full_msg_count = 1;
}
obj->last_push_time = std::chrono::system_clock::now();
// Set storage state - Moonraker printers use virtual_sdcard, storage is always available.
// This is required for SelectMachineDialog to allow printing (otherwise it blocks with "No SD card").
obj->GetStorage()->set_sdcard_state(DevStorage::HAS_SDCARD_NORMAL);
// Populate module_vers so is_info_ready() passes the version check.
if (obj->module_vers.empty()) {
DevFirmwareVersionInfo ota_info;
ota_info.name = "ota";
ota_info.sw_ver = "1.0.0"; // Placeholder version
obj->module_vers.emplace("ota", ota_info);
}
};
if (queue_fn && !on_main) {
queue_fn(apply);
} else {
apply();
}
}
void clear_ams_payload_for_device(const std::string& dev_id, const QueueOnMainFn& queue_fn)
{
int count = 0;
{
std::lock_guard<std::mutex> lock(g_ams_state_mutex);
auto it = g_ams_last_count.find(dev_id);
if (it != g_ams_last_count.end())
count = it->second;
g_ams_last_count[dev_id] = 0;
}
if (count == 0)
return; // nothing was ever rendered: nothing to clear
const bool on_main = wxIsMainThread();
auto apply = [dev_id, count]() {
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
if (!dev_manager)
return;
MachineObject* obj = dev_manager->get_my_machine(dev_id);
if (!obj)
return;
// All units present-but-empty: exist bits 0 marks them absent while
// placeholder trays flush stale type/color data out of DevFilaSystem.
nlohmann::json units = nlohmann::json::array();
for (int ams_id = 0; ams_id < count; ++ams_id) {
nlohmann::json trays = nlohmann::json::array();
for (int slot_id = 0; slot_id < 4; ++slot_id) {
trays.push_back(nlohmann::json{
{"id", std::to_string(slot_id)},
{"tag_uid", "0000000000000000"},
{"tray_info_idx", ""},
{"tray_type", ""},
{"tray_color", "00000000"},
{"tray_slot_placeholder", "1"},
});
}
units.push_back(nlohmann::json{{"id", std::to_string(ams_id)}, {"info", "0002"}, {"tray", trays}});
}
nlohmann::json ams_json;
ams_json["ams"] = units;
ams_json["ams_exist_bits"] = "0";
ams_json["tray_exist_bits"] = "0";
nlohmann::json print_json;
print_json["ams"] = ams_json;
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
BOOST_LOG_TRIVIAL(info) << "AmsPayload: cleared " << count << " AMS units for " << dev_id;
};
if (queue_fn && !on_main)
queue_fn(apply);
else
apply();
}
} // namespace Slic3r
+93
View File
@@ -0,0 +1,93 @@
#ifndef __AMS_PAYLOAD_HPP__
#define __AMS_PAYLOAD_HPP__
#include "bambu_networking.hpp"
#include <functional>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
#include <vector>
namespace Slic3r {
// One lane of a multi-material system in the neutral shape every printer agent
// shares before it is rendered into the BBL AMS payload.
struct AmsTrayData {
int slot_index = 0; // 0-based global slot index
bool has_filament = false;
std::string tray_type; // Material type (e.g. "PLA", "ASA")
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
std::string tray_info_idx; // OrcaFilamentLibrary preset id (optional)
int bed_temp = 0; // Optional
int nozzle_temp = 0; // Optional
};
// Normalize a driver color string to the BBL RRGGBBAA form; "00000000" when invalid.
std::string normalize_ams_color(const std::string& color);
// Map a reported material type to an OrcaFilamentLibrary generic family id.
std::string map_filament_type_to_generic_id(const std::string& filament_type);
// Parse a Moonraker `/server/database/item?namespace=lane_data` response body:
// result.value is keyed by lane, each entry carrying "lane", optional
// "material"/"color"/"bed_temp"/"nozzle_temp" and an optional presence signal
// ("has_filament" or "loaded"). Presence is preferred over material: a loaded
// lane with no declared material must not read as empty. Returns false when
// malformed or empty. Pure: tray_info_idx is left for resolve_tray_info_idx().
bool parse_moonraker_lane_data(const nlohmann::json& body,
std::vector<AmsTrayData>& trays,
int& max_lane_index);
// Fill each tray's tray_info_idx from the loaded preset bundle (falling back to
// the generic family map). Reads GUI preset state, so it MUST run on the main
// thread. Ids already set by a vendor-aware resolver are left untouched.
void resolve_tray_info_idx(std::vector<AmsTrayData>& trays);
// Optional vendor-aware resolver, run on the main thread before the generic
// resolver. Agents with brand/color matching (Snapmaker, Creality) supply one
// so their results survive the shared path; the generic resolver only fills
// ids this leaves empty.
using TrayInfoResolver = std::function<void(std::vector<AmsTrayData>&)>;
// Assemble the BBL-format AMS JSON (ams[] units + ams_exist_bits/tray_exist_bits)
// that DevFilaSystemParser::ParseV1_0 consumes. Pure and GUI-free: the
// MachineObject mutation stays in build_ams_payload_for_device.
nlohmann::json build_bbl_ams_json(const std::vector<AmsTrayData>& trays,
int ams_count,
int max_lane_index);
// Render trays into the BBL AMS payload and populate the device's DevFilaSystem.
// The resolver and the MachineObject mutation both run on the main thread via
// queue_fn when set. printer_type: nullopt leaves obj->printer_type untouched
// (OrcaSonar, whose push_status already carries it); a value assigns it
// verbatim (Moonraker).
void build_ams_payload_for_device(const std::string& dev_id,
const std::optional<std::string>& printer_type,
int ams_count,
int max_lane_index,
const std::vector<AmsTrayData>& trays,
const QueueOnMainFn& queue_fn,
const TrayInfoResolver& vendor_resolver = {});
// Clear the device's AMS view: render every previously-seen unit as absent
// with placeholder trays so a removed/absent material system never leaves
// stale filament data behind (lane_data read as authoritative empty).
void clear_ams_payload_for_device(const std::string& dev_id, const QueueOnMainFn& queue_fn);
// Process-wide canonical AMS write capability (OrcaSonar REQ-STS-008), parsed
// from the info.get_capabilities reply. Devices with no record (Bambu, cloud
// profiles) report every op supported: gating only ever applies to OrcaSonar
// printers that answered.
void register_ams_ops(const std::string& dev_id, const std::vector<std::string>& ops);
bool ams_op_supported(const std::string& dev_id, const std::string& op);
// Whether the device has a material system, from the get_capabilities reply's
// protocol.features.fms (falling back to a non-empty ams_ops). No record reads
// false: an unconfirmed printer must not advertise filament sync.
void register_ams_capability(const std::string& dev_id, bool has_ams);
bool has_ams_capability(const std::string& dev_id);
} // namespace Slic3r
#endif
+146
View File
@@ -148,6 +148,152 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
// Communication
// ============================================================================
std::string BBLPrinterAgent::ams_refresh_rfid_gcode(const std::string& tray_id)
{
return (boost::format("M620 R%1% \n") % tray_id).str();
}
std::string BBLPrinterAgent::ams_calibrate_gcode(int ams_id)
{
return (boost::format("M620 C%1% \n") % ams_id).str();
}
std::string BBLPrinterAgent::ams_select_tray_gcode(const std::string& tray_id)
{
return (boost::format("M620 P%1% \n") % tray_id).str();
}
int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
const std::string gcode = ams_refresh_rfid_gcode(tray_id);
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode)
{
const std::string gcode = ams_calibrate_gcode(ams_id);
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
const std::string gcode = ams_select_tray_gcode(tray_id);
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G90 \n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G29 \n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_homing) {
j["print"]["command"] = "back_to_center";
return publish(dev_id, j, lan_mode);
}
j["print"]["command"] = "gcode_line";
j["print"]["param"] = is_printing ? "G28 X\n" : "G28 \n";
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_bed_ctrl) {
j["print"]["command"] = "set_bed_temp";
j["print"]["temp"] = temp;
return publish(dev_id, j, lan_mode);
}
j["print"]["command"] = "gcode_line";
j["print"]["param"] = (boost::format("M140 S%1%\n") % temp).str();
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = (boost::format("M104 S%1%\n") % temp).str();
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_axis_control) {
int dir = input_val > 0 ? 1 : -1;
// i3-arch printers move the bed for Y/Z, so the on-screen direction is
// reversed -- same negation the g-code fallback below applies.
if (!is_core_xy && (axis == "Y" || axis == "Z")) {
dir = -dir;
}
j["print"]["command"] = "xyz_ctrl";
j["print"]["axis"] = axis;
j["print"]["dir"] = dir;
j["print"]["mode"] = (std::abs(input_val) >= 10) ? 1 : 0;
return publish(dev_id, j, lan_mode);
}
double value = input_val;
if (!is_core_xy && (axis == "Y" || axis == "Z")) {
value = -1.0 * input_val;
}
std::string value_str = (boost::format("%.1f") % (value * unit)).str();
std::string gcode;
if (axis == "X" || axis == "Y" || axis == "Z") {
gcode = (boost::format("M211 S \nM211 X1 Y1 Z1\nM1002 push_ref_mode\nG91 \nG1 %1%%2% F%3%\nM1002 pop_ref_mode\nM211 R\n")
% axis % value_str % speed).str();
} else if (axis == "E") {
gcode = (boost::format("M83 \nG0 %1%%2% F%3%\n") % axis % value_str % speed).str();
} else {
return -1;
}
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode)
{
const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0);
+14 -1
View File
@@ -29,10 +29,23 @@ public:
// Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
std::string default_lan_username() const override { return "bblp"; }
static std::string ams_refresh_rfid_gcode(const std::string& tray_id);
static std::string ams_calibrate_gcode(int ams_id);
static std::string ams_select_tray_gcode(const std::string& tray_id);
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override;
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override;
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override;
int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode) override;
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override;
int disconnect_printer() override;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
std::string default_lan_username() const override { return "bblp"; }
// Certificates
int check_cert() override;
+1 -51
View File
@@ -14,19 +14,8 @@
#include <curl/curl.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#ifdef OPENSSL_CERT_OVERRIDE
#include <openssl/x509.h>
#include <openssl/x509err.h>
#ifdef _WIN32
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
# include <wincrypt.h>
// wincrypt.h uses this token for a certificate-name property identifier.
# undef X509_NAME
#endif
namespace fs = boost::filesystem;
@@ -950,45 +939,6 @@ std::string Http::tls_system_cert_store()
return ret;
}
void Http::add_platform_root_certificates(SSL_CTX* ssl_context)
{
#ifdef _WIN32
X509_STORE* openssl_store = SSL_CTX_get_cert_store(ssl_context);
if (!openssl_store)
throw std::runtime_error("unable to get OpenSSL certificate store");
const auto load_store = [&](DWORD location) {
HCERTSTORE windows_store = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0,
location | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG,
L"ROOT");
if (!windows_store)
return;
PCCERT_CONTEXT windows_certificate = nullptr;
while ((windows_certificate = CertEnumCertificatesInStore(windows_store, windows_certificate)) != nullptr) {
const unsigned char* encoded = windows_certificate->pbCertEncoded;
X509* certificate = d2i_X509(nullptr, &encoded, static_cast<long>(windows_certificate->cbCertEncoded));
if (!certificate) {
ERR_clear_error();
continue;
}
ERR_clear_error();
if (X509_STORE_add_cert(openssl_store, certificate) != 1)
ERR_clear_error();
X509_free(certificate);
}
CertCloseStore(windows_store, 0);
};
load_store(CERT_SYSTEM_STORE_CURRENT_USER);
load_store(CERT_SYSTEM_STORE_LOCAL_MACHINE);
#else
(void)ssl_context;
#endif
}
std::string Http::url_encode(const std::string &str)
{
::CURL *curl = ::curl_easy_init();
-6
View File
@@ -11,8 +11,6 @@
#include "libslic3r/Exception.hpp"
#include "libslic3r_version.h"
typedef struct ssl_ctx_st SSL_CTX;
#define MAX_SIZE_TO_FILE 3*1024
namespace Slic3r {
@@ -200,10 +198,6 @@ public:
// Return empty string on success or error message on fail.
static std::string tls_global_init();
static std::string tls_system_cert_store();
// Add platform root certificates to a standalone OpenSSL context. This
// supplements set_default_verify_paths() on platforms where OpenSSL does
// not use the native certificate store.
static void add_platform_root_certificates(SSL_CTX* ssl_context);
// converts the given string to an url_encoded_string
static std::string url_encode(const std::string &str);
+5 -56
View File
@@ -14,9 +14,6 @@
#include <vector>
#include <functional>
#include <cstdint>
#include <cmath>
#include <nlohmann/json.hpp>
#include <boost/format.hpp>
#include "ICameraSignalingChannel.hpp"
namespace Slic3r {
@@ -112,64 +109,16 @@ public:
virtual int command_start_camera(std::string)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
private:
int publish_command_json(std::string dev_id, const nlohmann::json& j, bool lan_mode)
{
return lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0)
: send_message(dev_id, j.dump(), 0, 0);
}
public:
virtual int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G90 \n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish_command_json(dev_id, j, lan_mode);
}
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G29 \n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish_command_json(dev_id, j, lan_mode);
}
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_homing) {
j["print"]["command"] = "back_to_center";
} else {
j["print"]["command"] = "gcode_line";
j["print"]["param"] = is_printing ? "G28 X\n" : "G28 \n";
}
return publish_command_json(dev_id, j, lan_mode);
}
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_bed_ctrl) {
j["print"]["command"] = "set_bed_temp";
j["print"]["temp"] = temp;
} else {
j["print"]["command"] = "gcode_line";
j["print"]["param"] = (boost::format("M140 S%1%\n") % temp).str();
}
return publish_command_json(dev_id, j, lan_mode);
}
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = (boost::format("M104 S%1%\n") % temp).str();
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish_command_json(dev_id, j, lan_mode);
}
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
+17 -304
View File
@@ -533,142 +533,27 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn)
void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays)
{
// This may be called from a background thread (e.g. run_status_stream's read loop,
// for subscription-mode agents) as well as from the GUI thread (Sidebar's pull-mode
// path). Everything below touches MachineObject/DevFilaSystem, which the GUI thread
// reads without locking — so the actual mutation must run on the main thread. Snapshot
// queue_on_main_fn and the two device_info fields we need up front, then defer the rest,
// mirroring dispatch_message's existing queue_fn ? queue_fn(x) : x() idiom.
// May run from the status-stream thread (subscription agents) or the GUI
// thread (Sidebar's pull path). Snapshot queue_on_main_fn here; the shared
// builder defers the preset lookup and MachineObject mutation to the main
// thread. A heavy-handed empty model_id must still be assigned, so the
// optional wrapper always carries the value here.
QueueOnMainFn queue_fn;
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
queue_fn = queue_on_main_fn;
}
build_ams_payload_for_device(device_info.dev_id, std::optional<std::string>(device_info.model_id), ams_count, max_lane_index, trays, queue_fn);
}
std::string dev_id = device_info.dev_id;
std::string model_id = device_info.model_id;
auto apply = [dev_id, model_id, ams_count, max_lane_index, trays]() {
// Look up MachineObject via DeviceManager
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
if (!dev_manager) {
return;
}
MachineObject* obj = dev_manager->get_my_machine(dev_id);
if (!obj) {
return;
}
// Build BBL-format JSON for DevFilaSystemParser::ParseV1_0
nlohmann::json ams_json = nlohmann::json::object();
nlohmann::json ams_array = nlohmann::json::array();
// Calculate ams_exist_bits and tray_exist_bits
unsigned long ams_exist_bits = 0;
unsigned long tray_exist_bits = 0;
for (int ams_id = 0; ams_id < ams_count; ++ams_id) {
ams_exist_bits |= (1 << ams_id);
nlohmann::json ams_unit = nlohmann::json::object();
ams_unit["id"] = std::to_string(ams_id);
ams_unit["info"] = "0002"; // treat as AMS_LITE
nlohmann::json tray_array = nlohmann::json::array();
int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4);
for (int slot_id = 0; slot_id <= max_slot_in_this_ams; ++slot_id) {
int slot_index = ams_id * 4 + slot_id;
// Find tray with matching slot_index
const AmsTrayData* tray = nullptr;
for (const auto& t : trays) {
if (t.slot_index == slot_index) {
tray = &t;
break;
}
}
nlohmann::json tray_json = nlohmann::json::object();
tray_json["id"] = std::to_string(slot_id);
tray_json["tag_uid"] = "0000000000000000";
if (tray && tray->has_filament) {
tray_exist_bits |= (1 << slot_index);
tray_json["tray_info_idx"] = tray->tray_info_idx;
tray_json["tray_type"] = tray->tray_type;
tray_json["tray_color"] = normalize_color_value(tray->tray_color);
// Add temperature data if provided
if (tray->bed_temp > 0) {
tray_json["bed_temp"] = std::to_string(tray->bed_temp);
}
if (tray->nozzle_temp > 0) {
tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp);
}
} else {
tray_json["tray_info_idx"] = "";
tray_json["tray_type"] = "";
tray_json["tray_color"] = "00000000";
tray_json["tray_slot_placeholder"] = "1";
}
tray_array.push_back(tray_json);
}
ams_unit["tray"] = tray_array;
ams_array.push_back(ams_unit);
}
// Format as hex strings (matching BBL protocol)
std::ostringstream ams_exist_ss;
ams_exist_ss << std::hex << std::uppercase << ams_exist_bits;
std::ostringstream tray_exist_ss;
tray_exist_ss << std::hex << std::uppercase << tray_exist_bits;
ams_json["ams"] = ams_array;
ams_json["ams_exist_bits"] = ams_exist_ss.str();
ams_json["tray_exist_bits"] = tray_exist_ss.str();
// Wrap in the expected structure for ParseV1_0
nlohmann::json print_json = nlohmann::json::object();
print_json["ams"] = ams_json;
// Call the parser to populate DevFilaSystem
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
// Set printer_type so update_sync_status() can match it against the preset's printer type.
// Without this, the comparison fails and all sync badges are cleared.
obj->printer_type = model_id;
// Set push counters so is_info_ready() returns true for pull-mode agents.
if (obj->m_push_count == 0) {
obj->m_push_count = 1;
}
if (obj->m_full_msg_count == 0) {
obj->m_full_msg_count = 1;
}
obj->last_push_time = std::chrono::system_clock::now();
// Set storage state - Moonraker printers use virtual_sdcard, storage is always available.
// This is required for SelectMachineDialog to allow printing (otherwise it blocks with "No SD card").
obj->GetStorage()->set_sdcard_state(DevStorage::HAS_SDCARD_NORMAL);
// Populate module_vers so is_info_ready() passes the version check.
// Moonraker printers don't have BBL-style version info, but we need a non-empty map.
if (obj->module_vers.empty()) {
DevFirmwareVersionInfo ota_info;
ota_info.name = "ota";
ota_info.sw_ver = "1.0.0"; // Placeholder version for Moonraker printers
obj->module_vers.emplace("ota", ota_info);
}
};
if (queue_fn) {
queue_fn(apply);
} else {
apply();
void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays, const TrayInfoResolver& vendor_resolver)
{
QueueOnMainFn queue_fn;
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
queue_fn = queue_on_main_fn;
}
build_ams_payload_for_device(device_info.dev_id, std::optional<std::string>(device_info.model_id), ams_count, max_lane_index, trays, queue_fn, vendor_resolver);
}
bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
@@ -764,94 +649,7 @@ std::string MoonrakerPrinterAgent::trim_and_upper(const std::string& input)
return result;
}
std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::string& filament_type)
{
const std::string upper = trim_and_upper(filament_type);
// Normalize reported material names (trimmed, uppercased) to an OrcaFilamentLibrary
// generic family. The family's filament_id is resolved from the loaded system presets
// below rather than hardcoded, so profile id re-mints never require touching this table.
// scripts/test_moonraker_lane_data.py parses this initializer; keep the {"A", "B"} format.
static const std::map<std::string, std::string> type_to_ofl_family = {
// PLA variants
{"PLA", "PLA"},
{"PLA-CF", "PLA-CF"},
{"PLA SILK", "PLA Silk"},
{"PLA-SILK", "PLA Silk"},
{"PLA HIGH SPEED", "PLA High Speed"},
{"PLA-HS", "PLA High Speed"},
{"PLA HS", "PLA High Speed"},
// ABS/ASA variants
{"ABS", "ABS"},
{"ASA", "ASA"},
// PETG/PET variants
{"PETG", "PETG"},
{"PET", "PETG"},
{"PCTG", "PCTG"},
// PA/Nylon variants
{"PA", "PA"},
{"NYLON", "PA"},
{"PA-CF", "PA-CF"},
{"PPA", "PPA-CF"},
{"PPA-CF", "PPA-CF"},
{"PPA-GF", "PPA-GF"},
// PC variants
{"PC", "PC"},
// PP/PE variants
{"PE", "PE"},
{"PP", "PP"},
// Support materials
{"PVA", "PVA"},
{"HIPS", "HIPS"},
{"BVOH", "BVOH"},
// TPU variants
{"TPU", "TPU"},
// Other materials
{"EVA", "EVA"},
{"PHA", "PHA"},
{"COPE", "CoPE"},
{"SBS", "SBS"},
};
auto it = type_to_ofl_family.find(upper);
if (it == type_to_ofl_family.end())
return UNKNOWN_FILAMENT_ID;
if (auto* bundle = GUI::wxGetApp().preset_bundle) {
const Preset* preset = bundle->filaments.find_preset("Generic " + it->second + " @System");
if (preset != nullptr && preset->is_system && !preset->filament_id.empty())
return preset->filament_id;
}
// Unknown material, or no loaded preset data to resolve against
return UNKNOWN_FILAMENT_ID;
}
// JSON helper methods - null-safe accessors
std::string MoonrakerPrinterAgent::safe_json_string(const nlohmann::json& obj, const char* key)
{
auto it = obj.find(key);
if (it != obj.end() && it->is_string())
return it->get<std::string>();
return "";
}
int MoonrakerPrinterAgent::safe_json_int(const nlohmann::json& obj, const char* key)
{
auto it = obj.find(key);
if (it != obj.end() && it->is_number())
return it->get<int>();
return 0;
}
std::string MoonrakerPrinterAgent::safe_array_string(const nlohmann::json& arr, int idx)
{
if (arr.is_array() && idx >= 0 && idx < static_cast<int>(arr.size()) && arr[idx].is_string())
@@ -866,41 +664,6 @@ int MoonrakerPrinterAgent::safe_array_int(const nlohmann::json& arr, int idx)
return 0;
}
std::string MoonrakerPrinterAgent::normalize_color_value(const std::string& color)
{
std::string value = color;
boost::trim(value);
// Remove 0x or 0X prefix if present
if (value.size() >= 2 && (value.rfind("0x", 0) == 0 || value.rfind("0X", 0) == 0)) {
value = value.substr(2);
}
// Remove # prefix if present
if (!value.empty() && value[0] == '#') {
value = value.substr(1);
}
// Extract only hex digits
std::string normalized;
for (char c : value) {
if (std::isxdigit(static_cast<unsigned char>(c))) {
normalized.push_back(static_cast<char>(std::toupper(static_cast<unsigned char>(c))));
}
}
// If 6 hex digits, add FF alpha
if (normalized.size() == 6) {
normalized += "FF";
}
// Validate length - return default if invalid
if (normalized.size() != 8) {
return "00000000";
}
return normalized;
}
// Fetch filament info from moonraker database
bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index)
{
@@ -945,56 +708,11 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
}
// Expected structure: { "result": { "namespace": "lane_data", "value": { "lane1": {...}, ... } } }
if (!json.contains("result") || !json["result"].contains("value") || !json["result"]["value"].is_object()) {
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: Unexpected JSON structure or no lane_data found";
return false;
}
// Parse response into AmsTrayData
const auto& value = json["result"]["value"];
trays.clear();
max_lane_index = 0;
for (const auto& [lane_key, lane_obj] : value.items()) {
if (!lane_obj.is_object()) {
continue;
}
// Extract lane index from the "lane" field (tool number, 0-based)
std::string lane_str = safe_json_string(lane_obj, "lane");
int lane_index = -1;
if (!lane_str.empty()) {
try {
lane_index = std::stoi(lane_str);
} catch (...) {
lane_index = -1;
}
}
if (lane_index < 0) {
continue;
}
AmsTrayData tray;
tray.slot_index = lane_index;
tray.tray_color = safe_json_string(lane_obj, "color");
tray.tray_type = safe_json_string(lane_obj, "material");
tray.bed_temp = safe_json_int(lane_obj, "bed_temp");
tray.nozzle_temp = safe_json_int(lane_obj, "nozzle_temp");
tray.has_filament = !tray.tray_type.empty();
auto* bundle = GUI::wxGetApp().preset_bundle;
tray.tray_info_idx = bundle
? bundle->filaments.filament_id_by_type(tray.tray_type)
: map_filament_type_to_generic_id(tray.tray_type);
max_lane_index = std::max(max_lane_index, lane_index);
trays.push_back(tray);
}
if (trays.empty()) {
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: No lanes found";
if (!parse_moonraker_lane_data(json, trays, max_lane_index)) {
return false;
}
// tray_info_idx is resolved later, on the main thread, inside
// build_ams_payload_for_device.
return true;
}
@@ -1110,11 +828,6 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays
tray.has_filament = true;
auto* bundle = GUI::wxGetApp().preset_bundle;
tray.tray_info_idx = bundle
? bundle->filaments.filament_id_by_type(tray.tray_type)
: map_filament_type_to_generic_id(tray.tray_type);
max_lane_index = std::max(max_lane_index, gate_idx);
trays.push_back(tray);
}
+6 -18
View File
@@ -3,6 +3,7 @@
#include "IPrinterAgent.hpp"
#include "ICloudServiceAgent.hpp"
#include "AmsPayload.hpp"
#include <memory>
#include <mutex>
@@ -84,19 +85,12 @@ protected:
bool use_ssl = false;
} device_info;
// Tray data for AMS payload building
struct AmsTrayData {
int slot_index = 0; // 0-based slot index
bool has_filament = false;
std::string tray_type; // Material type (e.g., "PLA", "ASA")
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
std::string tray_info_idx; // Setting ID (optional)
int bed_temp = 0; // Optional
int nozzle_temp = 0; // Optional
};
// Build ams JSON and call parser
// Build ams JSON and call parser (AmsTrayData is shared; see AmsPayload.hpp).
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
// Overload for agents with vendor-aware matching (Snapmaker): the resolver
// runs on the main thread inside the shared builder, preserving the match
// without touching preset state on the fetch worker.
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays, const TrayInfoResolver& vendor_resolver);
// Methods that derived classes may need to override or access
virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl);
@@ -121,9 +115,6 @@ protected:
// Trim whitespace and convert to uppercase
static std::string trim_and_upper(const std::string& input);
// Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility
static std::string map_filament_type_to_generic_id(const std::string& filament_type);
// Send a G-code script via Moonraker (/printer/gcode/script)
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
bool send_gcode(const std::string& dev_id, const std::string& gcode,
@@ -188,11 +179,8 @@ private:
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index);
// JSON helper methods
static std::string safe_json_string(const nlohmann::json& obj, const char* key);
static int safe_json_int(const nlohmann::json& obj, const char* key);
static std::string safe_array_string(const nlohmann::json& arr, int idx);
static int safe_array_int(const nlohmann::json& arr, int idx);
static std::string normalize_color_value(const std::string& color);
std::string ssdp_announced_host;
std::string ssdp_announced_id;
+10 -4
View File
@@ -130,6 +130,7 @@ void OrcaCloudSignalingChannel::run()
nlohmann::json token_response;
std::string token_body;
std::string token_error;
unsigned int http_code = 0;
auto request = Http::post(live_token_url);
request.set_post_body(std::string("{}"))
@@ -141,13 +142,19 @@ void OrcaCloudSignalingChannel::run()
http_code = status;
token_body = std::move(body);
})
.on_error([&http_code](std::string, std::string, unsigned status) {
.on_error([&token_body, &token_error, &http_code](std::string body, std::string error, unsigned status) {
http_code = status;
token_body = std::move(body);
token_error = std::move(error);
})
.perform_sync();
BOOST_LOG_TRIVIAL(info) << "signaling: live-token HTTP " << http_code
<< (token_error.empty() ? "" : " error=" + token_error)
<< " body=" << token_body.substr(0, 512);
try {
token_response = nlohmann::json::parse(token_body);
} catch (const std::exception&) {
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: live-token body is not JSON: " << e.what();
}
if (http_code < 200 || http_code >= 300 || !token_response.contains("token")) {
unavailable(CameraUnavailableReason::Error, "Unable to mint camera live token");
@@ -182,9 +189,9 @@ void OrcaCloudSignalingChannel::run()
}
}
}
auto conn = std::make_shared<Connection>();
conn->ssl_context.set_default_verify_paths();
Http::add_platform_root_certificates(conn->ssl_context.native_handle());
{
std::lock_guard<std::mutex> lock(m_mutex);
m_conn = conn;
@@ -196,7 +203,6 @@ void OrcaCloudSignalingChannel::run()
if (!SSL_set_tlsext_host_name(websocket.next_layer().native_handle(), host.c_str()))
throw std::runtime_error("Unable to configure TLS server name");
websocket.next_layer().set_verify_mode(boost::asio::ssl::verify_peer);
websocket.next_layer().set_verify_callback(boost::asio::ssl::host_name_verification(host));
websocket.next_layer().handshake(boost::asio::ssl::stream_base::client);
const std::string ws_target = "/api/v1/printers/" + encode_path_component(m_dev_id) +
"/camera/live?token=" +
+145 -4
View File
@@ -1,11 +1,11 @@
#include "OrcaMqttConnection.hpp"
#include "Http.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/log/trivial.hpp>
#include <openssl/ssl.h>
@@ -52,6 +52,15 @@ OrcaMqttConnection::~OrcaMqttConnection() { stop(); }
bool OrcaMqttConnection::start(const Config& config, MessageHandler on_message, StateHandler on_state) {
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start url=" << config.url
<< " use_tls=" << config.use_tls
<< " bearer_provider=" << (config.bearer_provider ? "set" : "null")
<< " username_present=" << (!config.username.empty())
<< " password_present=" << (!config.password.empty())
<< " client_id=" << config.client_id
<< " keepalive_seconds=" << config.keepalive_seconds
<< " message_callback=" << (on_message ? "set" : "null")
<< " state_callback=" << (on_state ? "set" : "null");
stop();
{
std::lock_guard<std::mutex> lock(mutex);
@@ -70,12 +79,26 @@ bool OrcaMqttConnection::start(const Config& config, MessageHandler on_message,
if (!initial_cv.wait_for(lock, std::chrono::seconds(10), [this] { return initial_completed; })) {
initial_completed = true;
initial_result = false;
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT initial connection timed out after 10 seconds"
<< " url=" << current_config.url
<< " last_connack_rc=" << m_last_connack_rc.load()
<< " connected=" << connected.load()
<< "; worker will retry";
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start initial_result=" << initial_result
<< " initial_completed=" << initial_completed
<< " last_connack_rc=" << m_last_connack_rc.load()
<< " worker_running=" << (worker.joinable() && !stopping.load());
return initial_result;
}
void OrcaMqttConnection::stop() {
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT stop requested"
<< " url=" << current_config.url
<< " connected=" << connected.load()
<< " worker_joinable=" << worker.joinable()
<< " last_connack_rc=" << m_last_connack_rc.load();
stopping.store(true);
state_cv.notify_all();
{
@@ -135,26 +158,35 @@ void OrcaMqttConnection::flush_subscription_change() {
// websocket.read(); every write is serialised by write_mutex inside ws_write().
try {
send_pending_subscriptions(*conn);
} catch (const std::exception&) {
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: direct subscription write failed (" << e.what()
<< "); worker will resend the full set on reconnect";
}
}
bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
if (dev_id.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected empty dev_id";
return false;
}
const std::string topic = report_topic(dev_id);
if (topic.size() > 96) { // MQTT topic filter cap enforced by the service
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected oversized topic=" << topic;
return false;
}
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(topic) != 0 && pending_unsubscriptions.count(topic) == 0) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe already queued or active topic=" << topic
<< " acknowledged=" << (acknowledged_subscriptions.count(topic) != 0);
return true;
}
subscriptions.insert(topic);
pending_unsubscriptions.erase(topic);
pending_subscriptions.insert(topic);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe queued topic=" << topic
<< " total_subscriptions=" << subscriptions.size()
<< " connected=" << connected.load();
}
state_cv.notify_all();
flush_subscription_change(); // emit SUBSCRIBE now on the live socket (no reconnect)
@@ -181,6 +213,9 @@ bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) {
else
++it;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe queued topic=" << topic
<< " total_subscriptions=" << subscriptions.size()
<< " connected=" << connected.load();
}
state_cv.notify_all();
flush_subscription_change(); // emit UNSUBSCRIBE now on the live socket (no reconnect)
@@ -189,6 +224,7 @@ bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) {
void OrcaMqttConnection::clear_subscriptions() {
std::lock_guard<std::mutex> lock(mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT clear subscriptions count=" << subscriptions.size();
subscriptions.clear();
pending_subscriptions.clear();
pending_unsubscriptions.clear();
@@ -296,12 +332,16 @@ std::vector<uint8_t> OrcaMqttConnection::make_ping_packet() { return {0xc0, 0};
void OrcaMqttConnection::ws_write(Connection& conn, const std::vector<uint8_t>& packet) {
if (packet.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: attempted to send empty MQTT packet";
return;
}
// Writes come from the worker thread AND, for dynamic (un)subscribes, the
// caller thread. Serialise them; the worker's concurrent read is fine (beast
// allows one reader + one writer).
std::lock_guard<std::mutex> lock(write_mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending MQTT packet type=0x" << std::hex
<< static_cast<unsigned int>(packet[0] >> 4) << std::dec
<< " bytes=" << packet.size();
if (conn.wss) {
conn.wss->binary(true);
conn.wss->write(boost::asio::buffer(packet));
@@ -327,14 +367,22 @@ void OrcaMqttConnection::ws_close(Connection& conn) {
conn.wss->close(boost::beast::websocket::close_code::normal, close_error);
else if (conn.ws)
conn.ws->close(boost::beast::websocket::close_code::normal, close_error);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection closed code=" << close_error.value()
<< " message=" << close_error.message();
}
void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: WebSocket resolve starting host=" << endpoint.host
<< " port=" << endpoint.port << " target=" << endpoint.target
<< " tls=" << config.use_tls;
const auto results = conn.resolver.resolve(endpoint.host, endpoint.port);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT DNS resolution succeeded host=" << endpoint.host;
std::string token;
if (config.bearer_provider) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: requesting bearer token for WebSocket upgrade";
token = config.bearer_provider();
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: bearer token callback completed token_present=" << !token.empty();
}
auto decorator = [token](boost::beast::websocket::request_type& request) {
request.set(boost::beast::http::field::user_agent, "OrcaSlicer");
@@ -356,17 +404,21 @@ void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, co
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host
<< " port=" << endpoint.port;
// Set SNI before the TLS handshake so the cloud edge selects the correct
// certificate.
auto& tls_stream = websocket.next_layer();
if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), endpoint.host.c_str()))
throw std::runtime_error("failed to set Orca Cloud TLS server name");
conn.ssl_context.set_default_verify_paths();
Http::add_platform_root_certificates(conn.ssl_context.native_handle());
tls_stream.set_verify_mode(boost::asio::ssl::verify_peer);
tls_stream.set_verify_callback(boost::asio::ssl::host_name_verification(endpoint.host));
tls_stream.handshake(boost::asio::ssl::stream_base::client);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TLS handshake completed host=" << endpoint.host;
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending TLS WebSocket upgrade target=" << endpoint.target
<< " bearer_header=" << (!token.empty());
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
} else {
{
@@ -377,23 +429,39 @@ void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, co
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host
<< " port=" << endpoint.port << " (plaintext)";
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending plaintext WebSocket upgrade target=" << endpoint.target
<< " bearer_header=" << (!token.empty());
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
}
if (handshake_error) {
// Surface the server's HTTP status so a persistent rejection (stale token,
// missing api key, wrong route) is diagnosable from the log rather than an
// opaque "handshake declined".
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: WS handshake rejected http="
<< response.result_int() << " (" << response.reason() << "), "
<< handshake_error.message();
throw boost::system::system_error(handshake_error, "Orca WebSocket handshake");
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: WebSocket handshake completed http=" << response.result_int()
<< " negotiated_protocol=" << response["Sec-WebSocket-Protocol"];
if (response["Sec-WebSocket-Protocol"] != "mqtt") {
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: WebSocket handshake did not negotiate MQTT";
throw std::runtime_error("Orca WebSocket did not negotiate MQTT");
}
}
bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::string& payload) {
if (dev_id.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected empty dev_id";
return false;
}
if (!connected.load()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected because connection is not ready"
<< " dev_id=" << dev_id << " last_connack_rc=" << m_last_connack_rc.load();
return false;
}
std::shared_ptr<Connection> conn;
@@ -402,6 +470,8 @@ bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::stri
conn = active_connection;
}
if (!conn) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected because active connection is null"
<< " dev_id=" << dev_id;
return false;
}
const std::string report = report_topic(dev_id);
@@ -409,19 +479,28 @@ bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::stri
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(report) != 0 && acknowledged_subscriptions.count(report) == 0) {
pending_requests.emplace_back(dev_id, payload);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT request queued until SUBACK"
<< " dev_id=" << dev_id << " payload_bytes=" << payload.size()
<< " pending_requests=" << pending_requests.size();
return true;
}
}
try {
// ws_write() serialises the write via write_mutex; do not lock it here.
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PUBLISH request dev_id=" << dev_id
<< " topic=" << request_topic(dev_id)
<< " payload_bytes=" << payload.size();
ws_write(*conn, make_publish_packet(request_topic(dev_id), payload));
} catch (const std::exception&) {
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_request failed dev_id=" << dev_id
<< " (" << e.what() << ")";
return false;
}
return true;
}
void OrcaMqttConnection::connect_and_read() {
m_connection_stage = "creating connection";
auto connection = std::make_shared<Connection>();
{
std::lock_guard<std::mutex> lock(connection_mutex);
@@ -430,14 +509,20 @@ void OrcaMqttConnection::connect_and_read() {
return;
}
m_connection_stage = "parsing endpoint";
Endpoint endpoint;
if (!parse_endpoint(current_config.url, endpoint)) {
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: invalid MQTT endpoint=" << current_config.url;
throw std::runtime_error("invalid Orca Cloud WebSocket endpoint");
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connecting host=" << endpoint.host
<< " port=" << endpoint.port << " target=" << endpoint.target;
m_connection_stage = "WebSocket handshake";
ws_handshake(*connection, current_config, endpoint);
m_connection_stage = "sending MQTT CONNECT";
expires_never(*connection);
// Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the
// CONNECT username/password fields are omitted entirely (the cloud form).
@@ -446,7 +531,9 @@ void OrcaMqttConnection::connect_and_read() {
use_bearer ? std::string() : current_config.username,
use_bearer ? std::string() : current_config.password,
current_config.keepalive_seconds));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNECT packet sent";
m_connection_stage = "waiting for MQTT CONNACK";
boost::beast::flat_buffer buffer;
expires_after(*connection, std::chrono::seconds(10));
boost::system::error_code connack_error;
@@ -454,12 +541,16 @@ void OrcaMqttConnection::connect_and_read() {
if (connack_error)
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
const std::string connack = boost::beast::buffers_to_string(buffer.data());
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNACK received bytes=" << connack.size()
<< " header=" << (connack.empty() ? -1 : static_cast<int>(static_cast<uint8_t>(connack[0])))
<< " return_code=" << (connack.size() > 3 ? static_cast<int>(static_cast<uint8_t>(connack[3])) : -1);
// rc: 0 accepted, 1..5 refusal, -1 malformed/not a CONNACK.
const int rc = (connack.size() == 4 && static_cast<uint8_t>(connack[0]) == 0x20)
? static_cast<int>(static_cast<uint8_t>(connack[3]))
: -1;
m_last_connack_rc.store(rc);
if (rc != 0) {
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: MQTT CONNECT refused rc=" << rc;
if (rc == 4 || rc == 5) {
// Bad credentials / not authorized — retrying cannot help. Make run()'s
// loop exit and unblock any waiting start().
@@ -474,6 +565,7 @@ void OrcaMqttConnection::connect_and_read() {
throw std::runtime_error("Orca MQTT CONNECT refused rc=" + std::to_string(rc));
}
m_connection_stage = "reading MQTT messages";
// The subscription acknowledgement belongs to this MQTT session. Clear
// the previous session's state before notifying the owner, because the
// reconnect callback immediately queues the printer's initial requests.
@@ -484,6 +576,7 @@ void OrcaMqttConnection::connect_and_read() {
}
notify_state(true);
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection is ready; sending current subscriptions";
send_current_subscriptions(*connection);
std::chrono::steady_clock::time_point next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
@@ -494,15 +587,19 @@ void OrcaMqttConnection::connect_and_read() {
// read hot and the broker would drop us at 1.5 x keepalive.
if (std::chrono::steady_clock::now() >= next_ping) {
ws_write(*connection, make_ping_packet());
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PINGREQ sent";
next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
}
buffer.consume(buffer.size());
m_connection_stage = "reading MQTT frame";
expires_after(*connection, std::chrono::seconds(1));
boost::system::error_code error;
ws_read(*connection, buffer, error);
if (error == boost::beast::error::timeout)
continue;
if (error) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT WebSocket read failed code=" << error.value()
<< " message=" << error.message();
throw boost::system::system_error(error, "read Orca MQTT message");
}
handle_packet(boost::beast::buffers_to_string(buffer.data()));
@@ -523,12 +620,14 @@ void OrcaMqttConnection::send_current_subscriptions(Connection& conn) {
for (const std::string& topic : topics)
pending_subscriptions.erase(topic);
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending current MQTT subscriptions count=" << topics.size();
for (const std::string& topic : topics) {
const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending SUBSCRIBE topic=" << topic << " packet_id=" << packet_id;
ws_write(conn, make_subscribe_packet(packet_id, topic, 1));
}
}
@@ -549,20 +648,28 @@ void OrcaMqttConnection::send_pending_subscriptions(Connection& conn) {
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending SUBSCRIBE topic=" << topic
<< " packet_id=" << packet_id;
ws_write(conn, make_subscribe_packet(packet_id, topic, 1));
}
for (const std::string& topic : unsubscribe_topics) {
const uint16_t packet_id = next_packet_id++;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending UNSUBSCRIBE topic=" << topic
<< " packet_id=" << packet_id;
ws_write(conn, make_unsubscribe_packet(packet_id, topic));
}
}
void OrcaMqttConnection::handle_packet(const std::string& packet) {
if (packet.size() < 2) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: received undersized MQTT packet bytes=" << packet.size();
return;
}
const uint8_t header = static_cast<uint8_t>(packet[0]);
const uint8_t packet_type = header >> 4;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received MQTT packet type=" << static_cast<unsigned int>(packet_type)
<< " header=0x" << std::hex << static_cast<unsigned int>(header) << std::dec
<< " bytes=" << packet.size();
if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status.
if (packet_type == 9 && packet.size() >= 5) {
const uint16_t packet_id = (static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) |
@@ -573,6 +680,9 @@ void OrcaMqttConnection::handle_packet(const std::string& packet) {
result_codes << ',';
result_codes << "0x" << std::hex << static_cast<unsigned int>(static_cast<uint8_t>(packet[index]));
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received SUBACK packet_id="
<< packet_id
<< " result_codes=" << result_codes.str();
// Each production SUBSCRIBE packet currently contains one topic.
// MQTT grants QoS 0 or 1 for a requested QoS 1 subscription; 0x80
@@ -600,12 +710,21 @@ void OrcaMqttConnection::handle_packet(const std::string& packet) {
}
}
if (topic.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: SUBACK has no pending topic packet_id=" << packet_id;
} else if (result == 0 || result == 1) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: report subscription active topic=" << topic
<< " granted_qos=" << static_cast<unsigned int>(result)
<< " releasing_requests=" << requests.size();
for (const auto& request : requests) {
if (!send_request(request.first, request.second)) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: queued MQTT request could not be sent"
<< " after SUBACK dev_id=" << request.first;
}
}
} else {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: report subscription rejected topic=" << topic
<< " result_code=0x" << std::hex << static_cast<unsigned int>(result) << std::dec
<< " dropped_requests=" << requests.size();
}
}
return;
@@ -616,6 +735,7 @@ void OrcaMqttConnection::handle_packet(const std::string& packet) {
uint8_t encoded = 0;
do {
if (index >= packet.size() || multiplier > 128 * 128 * 128) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH remaining length";
return;
}
encoded = static_cast<uint8_t>(packet[index++]);
@@ -624,23 +744,30 @@ void OrcaMqttConnection::handle_packet(const std::string& packet) {
} while ((encoded & 0x80) != 0);
const size_t remaining_end = index + remaining;
if (remaining_end > packet.size() || remaining < 2 || index + 2 > remaining_end) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH body remaining=" << remaining
<< " packet_bytes=" << packet.size();
return;
}
const uint16_t topic_length = (static_cast<uint8_t>(packet[index]) << 8) |
static_cast<uint8_t>(packet[index + 1]);
index += 2;
if (topic_length > packet.size() - index) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH topic length=" << topic_length;
return;
}
const std::string topic(packet.data() + index, topic_length);
index += topic_length;
if (((header >> 1) & 0x03) != 0) {
if (index + 2 > remaining_end) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH packet identifier";
return;
}
index += 2; // QoS 1/2 packet identifier; the service currently sends QoS 0.
}
const size_t payload_size = remaining_end - index;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received PUBLISH topic=" << topic
<< " payload_bytes=" << payload_size
<< " message_callback=" << (on_message ? "set" : "null");
// topic is "device/<id>/report" (or "/request"); hand the id up, drop anything else.
std::string dev_id;
if (topic.rfind("device/", 0) == 0) {
@@ -650,9 +777,11 @@ void OrcaMqttConnection::handle_packet(const std::string& packet) {
dev_id = topic.substr(id_start, id_end - id_start);
}
if (dev_id.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH on unrecognized topic=" << topic;
} else if (on_message) {
on_message(dev_id, packet.substr(index, remaining_end - index));
} else {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH because message callback is not set";
}
}
@@ -669,6 +798,8 @@ void OrcaMqttConnection::notify_state(bool is_now_connected) {
}
callback = on_state;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT state changed connected=" << is_now_connected
<< " initial=" << initial << " state_callback=" << (callback ? "set" : "null");
if (initial)
initial_cv.notify_all();
else if (callback)
@@ -679,9 +810,18 @@ void OrcaMqttConnection::run() {
while (!stopping.load()) {
const int retry_seconds = reconnect_delay_seconds.load();
const uint64_t attempt = ++m_attempt_number;
m_connection_stage = "starting attempt";
try {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection attempt=" << attempt
<< " retry_delay=" << retry_seconds
<< " url=" << current_config.url;
connect_and_read();
} catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT connection attempt=" << attempt
<< " failed stage=" << m_connection_stage
<< " error=" << error.what()
<< " last_connack_rc=" << m_last_connack_rc.load()
<< " stopping=" << stopping.load();
if (!stopping.load())
notify_state(false);
}
@@ -691,6 +831,7 @@ void OrcaMqttConnection::run() {
// successful connection resets reconnect_delay_seconds to 1 (connect_and_read).
reconnect_delay_seconds.store(std::min(retry_seconds * 2, 30));
std::unique_lock<std::mutex> lock(mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT waiting before reconnect seconds=" << retry_seconds;
state_cv.wait_for(lock, std::chrono::seconds(retry_seconds), [this] { return stopping.load(); });
}
}
+1
View File
@@ -141,6 +141,7 @@ private:
std::atomic<uint16_t> next_packet_id{1};
std::atomic<int> m_last_connack_rc{-1};
uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence
std::string m_connection_stage; // worker-thread diagnostic stage
bool initial_result{false};
bool initial_completed{false};
std::atomic_bool connected{false};
+470 -55
View File
@@ -1,5 +1,6 @@
#include "OrcaPrinterAgent.hpp"
#include "OrcaCloudSignalingChannel.hpp"
#include "AmsPayload.hpp"
#include "Http.hpp"
#include "IPrinterAgent.hpp"
#include "NetworkAgentFactory.hpp"
@@ -448,6 +449,15 @@ OrcaPrinterAgent::~OrcaPrinterAgent()
start_discovery(false, false);
++m_lan_generation; // fence any late worker callback
++m_cloud_generation;
{
std::lock_guard<std::mutex> lock(state_mutex);
m_shutting_down = true; // workers stop arming new HTTP fetches
}
// Drain the detached filament-refresh workers: the flag and generation bump
// above end their loops, so this waits at most one in-flight HTTP fetch.
while (m_filament_in_flight.load() > 0)
std::this_thread::sleep_for(std::chrono::milliseconds(20));
// Drop the cloud status callback before anything else: it holds `this`, and the
// cloud agent outlives the printer agent (NetworkAgent::set_printer_agent swaps
@@ -541,7 +551,7 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
// removes its storage too. Process-wide and keyed by the globally-unique
// dev_id, with its own mutex; C++11 makes the one-time init thread-safe.
static std::unordered_map<std::string, double> nozzle_diameter_cache;
static std::mutex nozzle_diameter_cache_mutex;
static std::mutex nozzle_diameter_cache_mutex;
bool modified = false;
@@ -557,7 +567,7 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
// ("N/A" -> NozzleType::ntUndefine, as MoonrakerPrinterAgent
// does; Klipper has no Bambu nozzle type) onto later push_status
// frames that carry no real nozzle data.
const auto info_it = envelope.find("info");
const auto info_it = envelope.find("info");
const bool is_capabilities_reply = info_it != envelope.end() && info_it->is_object() &&
info_it->value("command", "") == "get_capabilities";
@@ -590,13 +600,46 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
std::lock_guard<std::mutex> l(nozzle_diameter_cache_mutex);
nozzle_diameter_cache[dev_id] = nozzle_dia;
}
// The capabilities reply itself is forwarded unchanged.
}
else {
const auto print_it = envelope.find("print");
if (print_it != envelope.end() && print_it->is_object() &&
print_it->value("command", "") == "push_status" && !print_it->contains("nozzle_diameter")) {
// The capabilities reply itself is forwarded unchanged. Its declared
// ams_ops (OPCP §7.8) gate every AMS control, and protocol.features.fms
// declares whether a material system actually exists.
{
const auto caps_it = info_it->find("capabilities");
if (caps_it != info_it->end() && caps_it->is_object()) {
const auto proto_it = caps_it->find("protocol");
if (proto_it != caps_it->end() && proto_it->is_object()) {
const auto ops_it = proto_it->find("ams_ops");
std::vector<std::string> ops;
if (ops_it != proto_it->end() && ops_it->is_array()) {
for (const auto& op : *ops_it)
if (op.is_string())
ops.push_back(op.get<std::string>());
register_ams_ops(dev_id, ops);
}
// features.fms is the authoritative "has a material system"
// flag (topology.material_units non-empty). Payloads without
// it fall back to a non-empty ams_ops.
bool has_ams = false;
bool fms_known = false;
const auto features_it = proto_it->find("features");
if (features_it != proto_it->end() && features_it->is_object()) {
const auto fms_it = features_it->find("fms");
if (fms_it != features_it->end() && fms_it->is_boolean()) {
has_ams = fms_it->get<bool>();
fms_known = true;
}
}
if (!fms_known)
has_ams = !ops.empty();
register_ams_capability(dev_id, has_ams);
}
}
}
} else {
const auto print_it = envelope.find("print");
if (print_it != envelope.end() && print_it->is_object() && print_it->value("command", "") == "push_status" &&
!print_it->contains("nozzle_diameter")) {
double nozzle_dia = 0.0;
{
std::lock_guard<std::mutex> l(nozzle_diameter_cache_mutex);
@@ -607,7 +650,7 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
if (nozzle_dia > 0.0) {
(*print_it)["nozzle_diameter"] = nozzle_dia;
(*print_it)["nozzle_type"] = "N/A";
modified = true;
modified = true;
}
}
}
@@ -618,6 +661,11 @@ std::string OrcaPrinterAgent::merge_capabilities(const std::string& dev_id, cons
void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local)
{
// Subscription doorbell, on the raw payload before the UI marshal so the
// (possibly blocking) lane_data refresh never queues behind it.
if (local && filament_doorbell_needed(dev_id, payload))
request_filament_refresh(dev_id);
parse_ipcam_info(dev_id, payload);
std::string merged_payload = merge_capabilities(dev_id, payload);
@@ -629,10 +677,11 @@ void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::str
q = queue_on_main_fn;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: delivering " << (local ? "local" : "cloud") << " report payload dev_id=" << dev_id
<< " payload=" << merged_payload
<< " callback=" << (fn ? "set" : "null") << " queue_on_main=" << (q ? "set" : "null");
<< " payload=" << merged_payload << " callback=" << (fn ? "set" : "null")
<< " queue_on_main=" << (q ? "set" : "null");
if (!fn) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping " << (local ? "local" : "cloud") << " message because on_message_fn is not set"
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping " << (local ? "local" : "cloud")
<< " message because on_message_fn is not set"
<< " dev_id=" << dev_id;
return;
}
@@ -654,6 +703,20 @@ void OrcaPrinterAgent::dispatch_local_connect(int state, const std::string& dev_
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: LAN connection callback state=" << state << " dev_id=" << dev_id << " message=" << message
<< " callback=" << (callback ? "set" : "null") << " queue_on_main=" << (queue ? "set" : "null");
// Eager filament sync on every (re)connect, like the Moonraker agents: the
// cached DevFilaSystem may predate the drop. The doorbell cache resets too,
// so the post-connect frame re-arms if the lane content moved meanwhile.
// Runs before the callback check so it also fires when no GUI listener is
// installed yet.
if (state == ConnectStatusOk) {
{
std::lock_guard<std::mutex> lock(state_mutex);
m_material_hash.clear();
}
request_filament_refresh(dev_id);
}
if (!callback)
return;
@@ -745,7 +808,9 @@ int OrcaPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tr
nlohmann::json j;
j["print"]["command"] = "ams_change_filament";
j["print"]["sequence_id"] = std::to_string(sequence_id);
j["print"]["target"] = tray_number;
// tray_id here is the flat global lane (DevFilaSystem slot index).
j["print"]["selector"] = "lane";
j["print"]["lane"] = tray_number;
return route_send(lan_mode, dev_id, j.dump());
}
@@ -843,6 +908,240 @@ int OrcaPrinterAgent::command_axis_control(std::string dev_id,
return route_send(lan_mode, dev_id, j.dump());
}
FilamentSyncMode OrcaPrinterAgent::get_filament_sync_mode() const
{
std::string dev_id;
{
std::lock_guard<std::mutex> lock(state_mutex);
dev_id = (m_current_connection == LAN) ? m_lan_dev_id : selected_machine;
}
if (!dev_id.empty() && has_ams_capability(dev_id)) {
return FilamentSyncMode::subscription;
}
return FilamentSyncMode::none;
}
bool OrcaPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{
if (sync_mode != get_filament_sync_mode())
return false;
return fetch_lane_data(dev_id) == LaneDataState::synced;
}
// Read the lane_data projection once and apply the REQ-STS-007 tri-state to
// DevFilaSystem. Only LaneDataState::error arms the retry latch: 404 means
// "not knowable yet" (pre-bootstrap or acknowledged-unknown topology) and {}
// means "authoritatively no lanes" — both are settled states, and a printer
// without a material system must not turn into a per-frame 404 poll.
OrcaPrinterAgent::LaneDataState OrcaPrinterAgent::fetch_lane_data(const std::string& dev_id)
{
std::string origin;
QueueOnMainFn queue_fn;
{
std::lock_guard<std::mutex> lock(state_mutex);
if (m_shutting_down || m_current_connection != LAN || m_lan_dev_id != dev_id)
return LaneDataState::unknown;
origin = m_lan_http_origin;
queue_fn = queue_on_main_fn;
}
if (origin.empty())
return LaneDataState::unknown;
const std::string api_key = lan_api_key(origin);
// OrcaSonar serves the canonical topology's lane projection on its
// Moonraker-compatible façade. Called on the refresh worker (subscription
// mode), so the payload mutation is marshalled onto the main thread through
// queue_fn; a GUI-thread caller reads DevFilaSystem inline.
const std::string url = origin + "/server/database/item?namespace=lane_data";
std::string response_body;
unsigned http_status = 0;
bool transport_error = false;
std::string http_error;
auto http = Http::get(url);
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) {
http_status = status;
if (status == 200) {
response_body = std::move(body);
} else {
http_error = "HTTP error: " + std::to_string(status);
}
})
.on_error([&](std::string, std::string err, unsigned status) {
transport_error = true;
http_status = status;
http_error = err;
if (status > 0)
http_error += " (HTTP " + std::to_string(status) + ")";
})
.perform_sync();
if (http_status == 404 && !transport_error) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::fetch_lane_data: lane_data not served yet (unknown topology)";
return LaneDataState::unknown;
}
if (http_status != 200) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::fetch_lane_data: lane_data fetch failed: " << http_error;
return LaneDataState::error;
}
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
if (json.is_discarded()) {
BOOST_LOG_TRIVIAL(warning) << "OrcaPrinterAgent::fetch_lane_data: invalid lane_data JSON";
return LaneDataState::error;
}
const bool empty_value = json.is_object() && json.contains("result") && json["result"].is_object() &&
json["result"].contains("value") && json["result"]["value"].is_object() && json["result"]["value"].empty();
if (empty_value) {
// Authoritative empty: flush stale trays so a removed AMS does not
// linger in the device panel.
clear_ams_payload_for_device(dev_id, queue_fn);
return LaneDataState::none;
}
std::vector<AmsTrayData> trays;
int max_lane_index = 0;
if (!parse_moonraker_lane_data(json, trays, max_lane_index))
return LaneDataState::error;
const int ams_count = (max_lane_index + 4) / 4;
// printer_type stays unset: push_status already carries the OrcaSonar printer
// type, and overwriting it here would clear it. build_ams_payload_for_device
// marshals the DevFilaSystem mutation through queue_fn when set.
build_ams_payload_for_device(dev_id, std::nullopt, ams_count, max_lane_index, trays, queue_fn);
return LaneDataState::synced;
}
// Subscription scheduler for filament sync. A single detached worker drains
// m_filament_wanted; extra requests arriving while it runs fold into its loop, so
// a burst of topology_state doorbells costs one extra fetch that picks up the
// trailing change. A failed fetch does not busy-retry: m_filament_failed makes
// the next inbound LAN frame request again (an idle printer is silent, but it
// cannot have changed lanes either), and a reconnect re-primes via the
// ConnectStatusOk path in dispatch_local_connect().
void OrcaPrinterAgent::request_filament_refresh(const std::string& dev_id)
{
uint64_t gen;
{
std::lock_guard<std::mutex> lock(state_mutex);
if (m_current_connection != LAN || m_lan_dev_id != dev_id)
return;
gen = m_lan_generation.load();
m_filament_wanted = true;
if (m_filament_working)
return; // the running worker will take the flag
m_filament_working = true;
}
m_filament_in_flight.fetch_add(1, std::memory_order_relaxed);
std::thread([this, dev_id, gen] {
struct InFlightGuard
{
std::atomic<int>& counter;
~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); }
} guard{m_filament_in_flight};
for (;;) {
bool needed;
{
std::lock_guard<std::mutex> lock(state_mutex);
needed = !m_shutting_down && m_filament_wanted && m_lan_generation.load() == gen && m_current_connection == LAN &&
m_lan_dev_id == dev_id;
if (needed)
m_filament_wanted = false;
else
m_filament_working = false; // same critical section that saw no work: no lost wake-up
}
if (!needed)
return;
const auto state = fetch_lane_data(dev_id);
std::lock_guard<std::mutex> lock(state_mutex);
m_filament_failed = state == LaneDataState::error; // latch read by filament_doorbell_needed()
}
}).detach();
}
// A LAN frame is a refresh trigger when it carries an OrcaSonar material
// change (a new print.topology_state.material_hash, spec REQ-STS-007 §7.7) or
// when the last fetch errored and this frame is the retry beat. The substring
// guard keeps the JSON parse off the steady temp-tick cadence, and hash
// equality absorbs the tick's topology_state re-emissions.
bool OrcaPrinterAgent::filament_doorbell_needed(const std::string& dev_id, const std::string& payload)
{
{
std::lock_guard<std::mutex> lock(state_mutex);
if (m_current_connection != LAN || m_lan_dev_id != dev_id)
return false;
if (m_filament_failed)
return true;
}
// The cheap guard is the §7.7 doorbell token itself, so the steady
// temperature-only tick never reaches the JSON parse.
if (payload.find("material_hash") == std::string::npos)
return false;
auto json = nlohmann::json::parse(payload, nullptr, false);
if (json.is_discarded() || !json.is_object())
return false;
const auto print_it = json.find("print");
if (print_it == json.end() || !print_it->is_object())
return false;
const auto topo_it = print_it->find("topology_state");
if (topo_it == print_it->end() || !topo_it->is_object())
return false;
const auto hash_it = topo_it->find("material_hash");
if (hash_it == topo_it->end() || !hash_it->is_string())
return false;
std::lock_guard<std::mutex> lock(state_mutex);
if (hash_it->get<std::string>() == m_material_hash)
return false; // content unchanged: not a doorbell
m_material_hash = hash_it->get<std::string>();
return true;
}
// Moonraker's client bootstrap: /access/api_key hands the façade key to a
// trusted source (the default LAN ranges include the slicer). Cache it per
// connection generation. If the request is refused — a hardened trusted_clients
// list, or no façade — fall back to the access code, which is what earlier
// builds sent, so behavior degrades to the status quo rather than breaking.
std::string OrcaPrinterAgent::lan_api_key(const std::string& origin)
{
if (origin.empty())
return {};
std::string fallback;
uint64_t gen;
{
std::lock_guard<std::mutex> lock(state_mutex);
if (!m_lan_api_key.empty() && m_lan_api_key_gen == m_lan_generation.load())
return m_lan_api_key;
fallback = m_lan_password;
gen = m_lan_generation.load();
}
std::string resolved;
if (std::string body; fetch_orcasonar_body(origin + "/access/api_key", body)) {
auto j = nlohmann::json::parse(body, nullptr, false, true);
if (!j.is_discarded() && j.contains("result") && j["result"].is_string())
resolved = j["result"].get<std::string>();
}
if (resolved.empty()) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent: /access/api_key unavailable; using the access code as X-Api-Key";
resolved = fallback;
}
std::lock_guard<std::mutex> lock(state_mutex);
if (m_lan_generation.load() == gen && !resolved.empty()) {
m_lan_api_key = resolved;
m_lan_api_key_gen = gen;
}
return resolved;
}
bool OrcaPrinterAgent::parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port)
{
std::string s = dev_ip;
@@ -1040,9 +1339,13 @@ int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, st
CurrentConn previous_connection;
{
std::lock_guard<std::mutex> l(state_mutex);
previous_connection = m_current_connection;
m_lan_dev_id = dev_id;
m_lan_url = cfg.url;
previous_connection = m_current_connection;
m_lan_dev_id = dev_id;
m_lan_url = cfg.url;
m_lan_http_origin = http_origin_from_lan_ws(cfg.url);
m_lan_password = password; // access code; the façade key is bootstrapped lazily
m_lan_api_key.clear();
m_lan_api_key_gen = gen;
m_camera_stream_mode = CameraStreamMode::none;
m_camera_url.clear();
m_current_connection = LAN;
@@ -1116,6 +1419,10 @@ int OrcaPrinterAgent::disconnect_printer()
doomed = std::move(lan_mqtt_connection);
prev_dev = m_lan_dev_id;
m_lan_dev_id.clear();
m_lan_http_origin.clear();
m_lan_api_key.clear();
m_filament_wanted = false; // a stale worker self-exits on the generation mismatch
m_filament_failed = false;
if (m_current_connection == LAN) {
m_current_connection = NONE;
m_camera_stream_mode = CameraStreamMode::none;
@@ -1145,11 +1452,107 @@ int OrcaPrinterAgent::disconnect_printer()
int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int /*qos*/, int /*flag*/)
{ return route_send(/*is_lan=*/true, dev_id, json_str); }
// Rewrite Bambu-convention print.ams_* payloads onto the canonical OrcaSonar
// bodies (OPCP spec §7.8) and enforce the device's declared ams_ops. DevFilaSystem
// is built from flat lane indices, so the Bambu encodings the shared
// MachineObject command builders emit — ams_id/slot_id 4-tray pseudo-groups,
// virtual tray ids 254/255 — are decoded here, at the single funnel all print
// commands cross, and never reach the server. A command whose op token the
// device did not declare short-circuits as *unsupported (CAP_NOT_AVAILABLE),
// which publish_json already renders as the friendly unsupported dialog.
std::string OrcaPrinterAgent::canonicalize_ams_payload(const std::string& dev_id, const std::string& json_str, bool* unsupported)
{
if (unsupported)
*unsupported = false;
try {
auto envelope = nlohmann::json::parse(json_str, nullptr, false);
if (envelope.is_discarded() || !envelope.is_object() || !envelope.contains("print") || !envelope["print"].is_object())
return json_str;
auto& print = envelope["print"];
const std::string cmd = print.value("command", std::string());
if (cmd.empty() || (cmd.rfind("ams_", 0) != 0 && cmd != "auto_stop_ams_dry"))
return json_str;
if (cmd == "ams_change_filament" && print.contains("selector"))
return json_str; // already canonical (e.g. command_ams_select_tray)
auto int_or = [&print](const char* key, int fallback) {
const auto it = print.find(key);
return (it != print.end() && it->is_number_integer()) ? it->get<int>() : fallback;
};
std::string op;
if (cmd == "ams_change_filament") {
const int target = int_or("target", -1);
const int slot = int_or("slot_id", -1);
const int ams = int_or("ams_id", -1);
print.erase("target");
print.erase("slot_id");
print.erase("tray_id");
print.erase("ams_id");
if (target == 255 && slot == 255) {
print["selector"] = "unload";
op = "unload";
} else if (target == 255 || ams == 254 || ams == 255) {
print["selector"] = "external";
op = "external";
} else {
const int lane = target >= 0 ? target : (ams >= 0 ? ams * 4 + slot : slot);
print["selector"] = "lane";
print["lane"] = lane;
op = "change_filament";
}
} else if (cmd == "ams_filament_setting") {
const int ams = int_or("ams_id", -1);
const int slot = int_or("slot_id", -1);
op = "filament_setting";
if (ams >= 254) {
if (unsupported)
*unsupported = true; // no lane for a virtual tray
return json_str;
}
print["lane"] = ams * 4 + slot;
print.erase("slot_id");
print.erase("tray_id");
print.erase("ams_id");
} else if (cmd == "ams_control") {
std::string action = print.value("action", print.value("param", std::string()));
op = action; // only "pause" is ever declared; the rest gate out
} else if (cmd == "ams_user_setting") {
op = "user_setting";
} else if (cmd == "ams_get_rfid") {
op = "get_rfid";
if (!print.contains("tray_id")) {
const int ams = int_or("ams_id", -1);
const int slot = int_or("slot_id", -1);
if (ams >= 0 && slot >= 0) {
print["tray_id"] = ams * 4 + slot; // legacy ams+slot call shape
print.erase("ams_id");
print.erase("slot_id");
}
}
} else if (cmd == "auto_stop_ams_dry") {
op = "stop_dry";
}
if (!op.empty() && !ams_op_supported(dev_id, op)) {
if (unsupported)
*unsupported = true;
}
return envelope.dump();
} catch (const std::exception&) {
return json_str;
}
}
int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const std::string& json_str)
{
bool unsupported = false;
const std::string canonical = canonicalize_ams_payload(dev_id, json_str, &unsupported);
if (unsupported) {
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send: ams op not declared by device capabilities, dev_id=" << dev_id;
return ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE;
}
std::string command = "<unparsed>";
try {
const nlohmann::json envelope = nlohmann::json::parse(json_str);
const nlohmann::json envelope = nlohmann::json::parse(canonical);
for (const char* namespace_name : {"pushing", "info", "print", "system", "camera", "xcam", "upgrade", "event", "files"}) {
const auto namespace_it = envelope.find(namespace_name);
if (namespace_it != envelope.end() && namespace_it->is_object()) {
@@ -1171,7 +1574,7 @@ int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const s
OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan);
if (!conn)
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
const bool queued = conn->send_request(dev_id, json_str);
const bool queued = conn->send_request(dev_id, canonical);
BOOST_LOG_TRIVIAL(info) << "OrcaPrinterAgent::route_send command=" << command << " queued=" << queued << " is_lan=" << is_lan
<< " dev_id=" << dev_id;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
@@ -1345,9 +1748,9 @@ int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id)
auto state_handler = [this](bool connected, bool initial) {
if (!connected || initial)
return;
auto* current_cloud = get_orca_cloud_agent();
auto* current_cloud = get_orca_cloud_agent();
OrcaMqttConnection* current_conn = current_cloud ? current_cloud->get_mqtt_connection() : nullptr;
const std::string selected = get_user_selected_machine();
const std::string selected = get_user_selected_machine();
if (current_conn && !selected.empty())
on_connected(selected, current_conn, m_cloud_generation.load());
};
@@ -1370,7 +1773,8 @@ AgentInfo OrcaPrinterAgent::get_agent_info_static()
// Print Job Operations - All Stubs
// ============================================================================
// Simply uploads the file to the printer via HTTP (cloud) then sends a HTTP request to start print. In the future, this might be a MQTT command to start print instead of HTTP.
// Simply uploads the file to the printer via HTTP (cloud) then sends a HTTP request to start print. In the future, this might be a MQTT
// command to start print instead of HTTP.
int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
{
(void) wait_fn;
@@ -1409,10 +1813,9 @@ int OrcaPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn
if (update_fn)
update_fn(PrintingStageSending, 0, "Starting print...");
result = start_sdcard_print(params, update_fn, cancel_fn);
if (result != BAMBU_NETWORK_SUCCESS)
return result;
const int start_rc = cloud->start_cloud_print_job(params.dev_id, job_id, remote_gcode_name(params), /*start=*/true);
if (start_rc != BAMBU_NETWORK_SUCCESS)
return start_rc;
if (update_fn)
update_fn(PrintingStageFinished, 100, "Print started");
@@ -1477,37 +1880,49 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params,
std::string http_error;
std::string response_body;
// One façade key for the whole upload exchange (directory probe + POST).
// If the bootstrapped key is unavailable (and there was no stored access
// code), fall back to the job's own password rather than sending none.
std::string api_key = lan_api_key(origin);
if (api_key.empty())
api_key = params.password;
// check if printer has enough storage
Http::get(origin + "/server/files/directory?path=gcodes")
.on_complete([&](std::string body, unsigned status) {
if (body.empty()) {
http_status = 400;
http_error = "Failed to get gcodes directory.";
}
int free = 0;
nlohmann::json json = nlohmann::json::parse(body);
if (json.contains("result")) {
json = json["result"];
if (json.contains("disk_usage")) {
json = json["disk_usage"];
if (json.contains("free"))
free = json["free"].get<int>();
{
auto dir_req = Http::get(origin + "/server/files/directory?path=gcodes");
if (!api_key.empty())
dir_req.header("X-Api-Key", api_key);
dir_req
.on_complete([&](std::string body, unsigned status) {
if (body.empty()) {
http_status = 400;
http_error = "Failed to get gcodes directory.";
}
}
if (free < file_size) {
http_status = 507;
http_error = "Not enough storage on the printer.";
}
})
.on_error([&](std::string body, std::string err, unsigned status) {
http_status = status;
http_error = std::move(err);
response_body = std::move(body);
})
.perform_sync();
int free = 0;
nlohmann::json json = nlohmann::json::parse(body);
if (json.contains("result")) {
json = json["result"];
if (json.contains("disk_usage")) {
json = json["disk_usage"];
if (json.contains("free"))
free = json["free"].get<int>();
}
}
if (free < file_size) {
http_status = 507;
http_error = "Not enough storage on the printer.";
}
})
.on_error([&](std::string body, std::string err, unsigned status) {
http_status = status;
http_error = std::move(err);
response_body = std::move(body);
})
.perform_sync();
}
if (http_status >= 400) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " failed with error code: " << http_status << ", " << http_error;
@@ -1515,8 +1930,8 @@ int OrcaPrinterAgent::start_send_gcode_to_sdcard(PrintParams params,
}
auto http = Http::post(origin + "/server/files/upload");
if (!params.password.empty())
http.header("X-Api-Key", params.password); // trusted LAN facades may not require it; harmless when they do not
if (!api_key.empty())
http.header("X-Api-Key", api_key); // bootstrapped façade key, or the access code fallback
http.form_add("root", "gcodes")
.form_add("print", "false")
.form_add_file("file", source, upload_name)
+63 -6
View File
@@ -36,8 +36,7 @@ public:
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override;
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
std::unique_ptr<ICameraSignalingChannel>
create_camera_signaling_channel(const std::string& dev_id) override;
std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id) override;
// Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
@@ -100,6 +99,14 @@ public:
int sequence_id,
bool lan_mode) override;
// Filament sync (subscription): `subscription` when the selected printer
// reports a material system (get_capabilities protocol.features.fms),
// `none` otherwise. The mode is transport-agnostic; the lane_data fetch and
// topology_state doorbell that keep DevFilaSystem fresh are LAN-only, and
// cloud printers get their AMS view from the mirrored push_status.
FilamentSyncMode get_filament_sync_mode() const override;
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
// Test-only: drive emit_connect_sequence directly (no socket).
void run_connect_sequence_for_test(const std::string& dev_id)
{
@@ -111,7 +118,22 @@ public:
// Test-only: the same for the (independent) cloud selection epoch.
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::subscription; }
// Test-only: observe the subscription doorbell policy without spawning the refresh worker.
bool filament_doorbell_needed_for_test(const std::string& dev_id, const std::string& payload)
{
return filament_doorbell_needed(dev_id, payload);
}
// Test-only: arm the fetch-failure latch (retry-on-next-LAN-frame rule).
void set_filament_failed_for_test(bool v)
{
std::lock_guard<std::mutex> lock(state_mutex);
m_filament_failed = v;
}
// Test-only: Bambu ams_* wire JSON -> canonical OrcaSonar bodies (§7.8).
// Returns the (possibly rewritten) payload; *unsupported is set when the
// device's declared ams_ops exclude the operation (CAP_NOT_AVAILABLE in
// the live route_send path).
static std::string canonicalize_ams_payload(const std::string& dev_id, const std::string& json_str, bool* unsupported);
protected:
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
@@ -191,10 +213,45 @@ private:
std::unique_ptr<OrcaSonarDiscovery> m_discovery;
std::string m_lan_dev_id; // guarded by state_mutex
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
std::string m_lan_dev_id; // guarded by state_mutex
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
std::string m_lan_http_origin; // guarded by state_mutex — http(s) origin of the LAN façade
std::string m_lan_password; // guarded by state_mutex — MQTT access code; X-Api-Key fallback
std::string m_lan_api_key; // guarded by state_mutex — cached Moonraker-façade key, "" = unresolved
uint64_t m_lan_api_key_gen = 0; // m_lan_generation the cached key belongs to
CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex
std::string m_camera_url; // guarded by state_mutex
std::string m_camera_url; // guarded by state_mutex
// The Moonraker-façade X-Api-Key, bootstrapped from /access/api_key (trusted
// clients only) and cached per connection generation; falls back to the
// access code when the endpoint is unavailable (untrusted/hardened config).
std::string lan_api_key(const std::string& origin);
// Subscription-mode filament sync. One detached worker per burst drains
// m_filament_wanted by re-reading lane_data; doorbell frames (a pushed
// print.topology_state change) and the failure latch are the only triggers —
// no timer: an idle OrcaSonar emits no frames, and cannot change lanes either.
void request_filament_refresh(const std::string& dev_id);
// OPCP §7.7: the doorbell is a CHANGE in print.topology_state.material_hash
// (lane content only — tool temperature churn inside topology_state must not
// re-fetch); consuming a new value updates m_material_hash.
bool filament_doorbell_needed(const std::string& dev_id, const std::string& payload);
// Tri-state lane_data fetch outcome (REQ-STS-007 §7.7 read semantics):
// synced — 200 with lane entries, payload built;
// none — 200 with an empty value: authoritative "no lanes", AMS view cleared;
// unknown — 404: topology not bootstrapped yet or material_units unknown;
// never latched, the next doorbell or reconnect retries;
// error — transport/5xx: latched, retried on the next inbound frame.
enum class LaneDataState { synced, none, unknown, error };
LaneDataState fetch_lane_data(const std::string& dev_id);
bool m_filament_wanted = false; // guarded by state_mutex; a fetch is pending
bool m_filament_working = false; // guarded by state_mutex; a worker owns the queue
bool m_filament_failed = false; // guarded by state_mutex; retry-on-next-LAN-frame rule
bool m_shutting_down = false; // guarded by state_mutex; workers stop taking fetches
std::string m_material_hash; // guarded by state_mutex; last doorbell value seen
std::atomic<int> m_filament_in_flight{0}; // detached workers; drained by the destructor
OrcaCloudServiceAgent* get_orca_cloud_agent();
+35 -20
View File
@@ -239,25 +239,6 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), safe_at(filament_sub_type, i, empty_str));
tray.tray_color = safe_at(filament_color, i, default_color);
auto* bundle = GUI::wxGetApp().preset_bundle;
// Try to find a matching preset for this filament based on vendor, type and color.
// If not found, default to traditional search by type only or generic type mapping.
if (bundle) {
std::string vendor = safe_at(filament_vendor, i, empty_str);
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type,
tray.tray_color);
if (!filament_id.empty()) {
tray.tray_info_idx = filament_id;
BOOST_LOG_TRIVIAL(warning)
<< "Filament sync: Found manufacturer-specific profile for slot " << i << ": " << filament_id;
} else {
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
}
} else {
tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type);
}
// Extract NFC temperature data if available
if (nfc_info.is_array() && i < static_cast<int>(nfc_info.size()) && nfc_info[i].is_object()) {
auto& nfc_slot = nfc_info[i];
@@ -272,12 +253,46 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
trays.emplace_back(std::move(tray));
}
build_ams_payload(1, slot_count - 1, trays);
// Preset matching (vendor + closest color) reads GUI preset state, so it
// runs on the main thread via this resolver, not on the fetch worker.
std::vector<std::string> vendors;
vendors.reserve(slot_count);
for (int i = 0; i < slot_count; ++i)
vendors.push_back(safe_at(filament_vendor, i, empty_str));
build_ams_payload(1, slot_count - 1, trays,
[vendors](std::vector<AmsTrayData>& resolved) { resolve_snapmaker_tray_info(resolved, vendors); });
}).detach();
return true;
}
void SnapmakerPrinterAgent::resolve_snapmaker_tray_info(std::vector<AmsTrayData>& trays,
const std::vector<std::string>& vendors)
{
auto* bundle = GUI::wxGetApp().preset_bundle;
for (auto& tray : trays) {
if (!tray.has_filament)
continue;
const std::string vendor = (tray.slot_index >= 0 && tray.slot_index < static_cast<int>(vendors.size()))
? vendors[tray.slot_index]
: std::string();
if (bundle) {
// Try a matching preset by vendor, type and color; fall back to
// type only, then to the generic family map.
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type, tray.tray_color);
if (!filament_id.empty()) {
tray.tray_info_idx = filament_id;
BOOST_LOG_TRIVIAL(warning) << "Filament sync: Found manufacturer-specific profile for slot " << tray.slot_index << ": " << filament_id;
} else {
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
}
} else {
tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type);
}
}
}
FilamentSyncMode SnapmakerPrinterAgent::get_filament_sync_mode() const
{
if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
@@ -6,6 +6,7 @@
#include <atomic>
#include <cstdint>
#include <string>
#include <vector>
namespace Slic3r {
@@ -28,6 +29,12 @@ private:
// Combine filament_type + filament_sub_type into a unified type string
static std::string combine_filament_type(const std::string& type, const std::string& sub_type);
// Resolve tray_info_idx for the collected trays. Reads the GUI preset
// bundle, so it is invoked on the main thread from within the shared
// build_ams_payload_for_device() (never on the fetch worker).
static void resolve_snapmaker_tray_info(std::vector<AmsTrayData>& trays,
const std::vector<std::string>& vendors);
void start_camera_monitor();
void on_status_loop_tick(const std::string& dev_id) override;
+156 -1
View File
@@ -1,4 +1,6 @@
#include <catch2/catch_test_macros.hpp>
#include <nlohmann/json.hpp>
#include <slic3r/Utils/AmsPayload.hpp>
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
#include <slic3r/Utils/OrcaPrinterAgent.hpp>
@@ -46,7 +48,7 @@ TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto pus
// is cached for the device.
agent.deliver_to_sink(
"dev-1",
R"({"info":{"command":"get_capabilities","capabilities":{"topology":{"tools":[{"id":"T0","nozzle":{"diameter_mm":0.4}}]}}}})",
R"({"info":{"command":"get_capabilities","capabilities":{"topology":{"tools":[{"id":"T0","nozzle":{"diameter_mm":0.4}}]},"protocol":{"ams_ops":["change_filament"]}}}})",
/*local=*/false);
CHECK(last_payload.find("\"command\":\"get_capabilities\"") != std::string::npos);
CHECK(last_payload.find("\"print\"") == std::string::npos);
@@ -65,6 +67,14 @@ TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto pus
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","nozzle_diameter":0.6}})", /*local=*/false);
CHECK(last_payload.find("\"nozzle_diameter\":0.6") != std::string::npos);
CHECK(last_payload.find("N/A") == std::string::npos);
// The reply's declared ams_ops register on the device (OPCP §7.8): the one
// declared op passes the client gate, an undeclared one is rejected.
bool unsupported = false;
OrcaPrinterAgent::canonicalize_ams_payload("dev-1", R"({"print":{"command":"ams_change_filament","target":1}})", &unsupported);
CHECK_FALSE(unsupported);
OrcaPrinterAgent::canonicalize_ams_payload("dev-1", R"({"print":{"command":"ams_control","param":"pause"}})", &unsupported);
CHECK(unsupported);
}
TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") {
@@ -92,6 +102,89 @@ TEST_CASE("connect_printer wires up a LAN Config", "[OrcaPrinterAgent][.integrat
agent.disconnect_printer();
}
// why hidden: connect_printer starts a detached connect worker, like the LAN
// test above. The sync mode follows the printer's own AMS capability, not the
// transport: a connected printer stays `none` until its get_capabilities reply
// declares a material system. The registry is process-wide, so this test uses
// ids no other test feeds capabilities for.
TEST_CASE("filament sync follows the printer's AMS capability", "[OrcaPrinterAgent][.integration]") {
Probe agent("/tmp");
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
REQUIRE(agent.connect_printer("dev-ams-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
// Connected, but no capability reply yet: still none.
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
// features.fms=true is the authoritative material-system flag.
agent.deliver_to_sink("dev-ams-1",
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":true},"ams_ops":["change_filament"]}}}})",
/*local=*/true);
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
// An explicit false clears it again.
agent.deliver_to_sink("dev-ams-1",
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"features":{"fms":false}}}}})",
/*local=*/true);
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
// Older payloads without features.fms fall back to a non-empty ams_ops.
agent.deliver_to_sink("dev-ams-1",
R"({"info":{"command":"get_capabilities","capabilities":{"protocol":{"ams_ops":["change_filament"]}}}})",
/*local=*/true);
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::subscription);
// The pull contract (Sidebar's blocking path) is gone: a pull-mode fetch is
// refused by the mode guard, and a subscription fetch for a device that is
// not the active printer is refused before any HTTP.
CHECK_FALSE(agent.fetch_filament_info("dev-ams-1", Slic3r::FilamentSyncMode::pull));
CHECK_FALSE(agent.fetch_filament_info("dev-ams-2", Slic3r::FilamentSyncMode::subscription));
agent.disconnect_printer();
CHECK(agent.get_filament_sync_mode() == Slic3r::FilamentSyncMode::none);
}
// The subscription refresh is self-triggered: a pushed frame whose print block
// carries a CHANGED topology_state.material_hash (spec REQ-STS-007 §7.7) is
// the doorbell; repeat hashes, temperature-only frames and hash-less blocks
// are not (no per-frame polling regression).
TEST_CASE("a material_hash change is the filament-sync doorbell", "[OrcaPrinterAgent][.integration]") {
Probe agent("/tmp");
REQUIRE(agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
const std::string frame_a = R"({"print":{"command":"push_status","topology_state":{"material_hash":"sha256:aaaaaaaaaaaaaaaa","units":[]}}})";
CHECK(agent.filament_doorbell_needed_for_test("dev-1", frame_a));
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", frame_a));
CHECK(agent.filament_doorbell_needed_for_test("dev-1",
R"({"print":{"command":"push_status","topology_state":{"material_hash":"sha256:bbbbbbbbbbbbbbbb","units":[]}}})"));
// Topology block without the doorbell token (older/foreign payload): stays quiet.
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1",
R"({"print":{"command":"push_status","topology_state":{"units":[]}}})"));
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", R"({"print":{"command":"push_status","mc_percent":10}})"));
// Not the active LAN device: never a doorbell.
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-2", frame_a));
agent.disconnect_printer();
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", frame_a));
}
// After a failed lane_data fetch there is no retry timer: any next LAN frame
// re-arms the refresh, because a changing printer keeps pushing.
TEST_CASE("a failed filament fetch retries on the next LAN frame", "[OrcaPrinterAgent][.integration]") {
Probe agent("/tmp");
REQUIRE(agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
const std::string telemetry = R"({"print":{"command":"push_status","mc_percent":10}})";
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", telemetry));
agent.set_filament_failed_for_test(true);
CHECK(agent.filament_doorbell_needed_for_test("dev-1", telemetry));
agent.disconnect_printer();
// disconnect clears the latch; the next connect eager-fetches instead.
REQUIRE(agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false) == BAMBU_NETWORK_SUCCESS);
CHECK_FALSE(agent.filament_doorbell_needed_for_test("dev-1", telemetry));
agent.disconnect_printer();
}
TEST_CASE("post-connect sequence is subscribe then 4 requests in order", "[OrcaPrinterAgent]") {
struct SeqProbe : OrcaPrinterAgent {
using OrcaPrinterAgent::OrcaPrinterAgent;
@@ -202,3 +295,65 @@ TEST_CASE("destroying an agent mid-connect does not hang or crash", "[OrcaPrinte
}
SUCCEED();
}
// OPCP spec §7.8: Bambu wire conventions must be decoded at the agent funnel,
// never smuggled through as numeric lanes (an external-spool select once read
// as slot_id=0 and physically loaded gate 0).
TEST_CASE("OrcaPrinterAgent rewrites Bambu ams_* payloads onto the canonical OrcaSonar bodies", "[OrcaPrinterAgent]") {
auto canon = [](const std::string& dev, const std::string& in, bool* unsupported = nullptr) {
bool local = false;
return OrcaPrinterAgent::canonicalize_ams_payload(dev, in, unsupported ? unsupported : &local);
};
auto out = nlohmann::json::parse(canon("dev-c1",
R"({"print":{"command":"ams_change_filament","sequence_id":"1","target":5,"slot_id":1,"ams_id":1,"curr_temp":210,"tar_temp":220}})"));
CHECK(out["print"]["selector"] == "lane");
CHECK(out["print"]["lane"] == 5);
CHECK(!out["print"].contains("target"));
CHECK(!out["print"].contains("slot_id"));
CHECK(!out["print"].contains("ams_id"));
CHECK(out["print"]["tar_temp"] == 220);
// External-spool selection ("254" arrives hacked to 255 with slot_id=0):
// must become the selector op, never a lane.
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":255,"target":255,"slot_id":0}})"));
CHECK(out["print"]["selector"] == "external");
CHECK(!out["print"].contains("lane"));
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":0,"target":255,"slot_id":255}})"));
CHECK(out["print"]["selector"] == "unload");
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_change_filament","ams_id":1,"slot_id":2}})"));
CHECK(out["print"]["lane"] == 6);
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_filament_setting","ams_id":1,"slot_id":2,"tray_id":2,"tray_type":"PLA"}})"));
CHECK(out["print"]["lane"] == 6);
CHECK(out["print"]["tray_type"] == "PLA");
// Legacy RFID call shape (ams_id+slot_id, no tray_id) flattens to tray_id.
out = nlohmann::json::parse(canon("dev-c1", R"({"print":{"command":"ams_get_rfid","ams_id":1,"slot_id":2}})"));
CHECK(out["print"]["tray_id"] == 6);
CHECK(!out["print"].contains("ams_id"));
const std::string canonical = R"({"print":{"command":"ams_change_filament","selector":"lane","lane":3}})";
CHECK(canon("dev-c1", canonical) == canonical);
CHECK(canon("dev-c1", R"({"print":{"command":"pause"}})") == R"({"print":{"command":"pause"}})");
CHECK(canon("dev-c1", "not json at all") == "not json at all");
// Declared ams_ops gate at the client: only change_filament is supported.
Slic3r::register_ams_ops("dev-c2", {"change_filament"});
bool unsupported = false;
canon("dev-c2", R"({"print":{"command":"ams_change_filament","target":255,"slot_id":255}})", &unsupported);
CHECK(unsupported);
unsupported = false;
canon("dev-c2", R"({"print":{"command":"ams_control","param":"pause"}})", &unsupported);
CHECK(unsupported);
unsupported = false;
canon("dev-c2", R"({"print":{"command":"ams_change_filament","target":1}})", &unsupported);
CHECK(!unsupported);
// A device with no capabilities record is never gated (server backstops).
unsupported = false;
canon("dev-c3", R"({"print":{"command":"ams_user_setting","ams_id":0}})", &unsupported);
CHECK(!unsupported);
}
+130
View File
@@ -68,6 +68,136 @@ TEST_CASE("Moonraker parses nozzle diameter from raw config and tolerates missin
CHECK(MoonrakerParserProbe::parse_nozzle_diameter(missing_response) == 0.0f);
}
// why: the lane_data projection is shared by Moonraker and OrcaSonar; the
// parser is pure so it can be checked without a live printer or a GUI.
TEST_CASE("unit: lane_data projection maps lanes into AMS trays", "[unit][moonraker]")
{
const auto body = nlohmann::json::parse(R"({
"result": {
"namespace": "lane_data",
"value": {
"lane0": {"lane": "0", "material": "PLA", "color": "FF0000FF"},
"lane1": {"lane": "1"},
"lane2": {"lane": "not-a-number"},
"lane3": {"lane": "3", "material": "PETG", "color": "#00FF00", "nozzle_temp": 240}
}
}
})");
std::vector<AmsTrayData> trays;
int max_lane_index = -1;
REQUIRE(parse_moonraker_lane_data(body, trays, max_lane_index));
REQUIRE(trays.size() == 3);
CHECK(trays[0].slot_index == 0);
CHECK(trays[0].has_filament);
CHECK(trays[0].tray_type == "PLA");
CHECK(trays[0].tray_color == "FF0000FF");
CHECK(trays[1].slot_index == 1);
CHECK_FALSE(trays[1].has_filament);
CHECK(trays[1].tray_type.empty());
CHECK(trays[2].slot_index == 3);
CHECK(trays[2].nozzle_temp == 240);
CHECK(max_lane_index == 3);
}
TEST_CASE("unit: lane_data projection rejects malformed and empty responses", "[unit][moonraker]")
{
std::vector<AmsTrayData> trays;
int max_lane_index = -1;
CHECK_FALSE(parse_moonraker_lane_data(nlohmann::json::object(), trays, max_lane_index));
CHECK_FALSE(parse_moonraker_lane_data(nlohmann::json::parse(R"({"result":{"value":{}}})"), trays, max_lane_index));
CHECK_FALSE(parse_moonraker_lane_data(
nlohmann::json::parse(R"({"result":{"value":{"lane0":{"lane":"x"},"lane1":42}}})"), trays, max_lane_index));
}
// why: OrcaSonar projects sensed presence separately from declared material, so a
// loaded lane with no type must not collapse to "empty".
TEST_CASE("unit: lane_data presence is independent of declared material", "[unit][moonraker]")
{
const auto body = nlohmann::json::parse(R"({
"result": {"value": {
"lane0": {"lane": "0", "has_filament": true},
"lane1": {"lane": "1", "has_filament": false, "material": "PLA"},
"lane2": {"lane": "2", "loaded": true},
"lane3": {"lane": "3", "material": "ASA"}
}}
})");
std::vector<AmsTrayData> trays;
int max_lane_index = -1;
REQUIRE(parse_moonraker_lane_data(body, trays, max_lane_index));
REQUIRE(trays.size() == 4);
CHECK(trays[0].has_filament);
CHECK(trays[0].tray_type.empty());
CHECK_FALSE(trays[1].has_filament);
CHECK(trays[1].tray_type == "PLA");
CHECK(trays[2].has_filament);
CHECK(trays[3].has_filament);
CHECK(trays[3].tray_type == "ASA");
}
// why: the exist bits and per-slot placeholder flag drive the sidebar's occupied
// vs empty rendering; the builder is pure so this needs no GUI.
TEST_CASE("unit: AMS payload sets exist bits and omits placeholder for loaded lanes", "[unit][moonraker]")
{
std::vector<AmsTrayData> trays = {
{0, true, "PLA", "FF0000FF", "", 0, 0},
{2, true, "", "", "", 0, 0}, // loaded but undeclared
{5, true, "PETG", "00FF00", "", 0, 0},
};
const auto ams = build_bbl_ams_json(trays, 2, 5);
// Units 0 and 1 exist; slots 0, 2 and 5 hold filament (0b100101).
CHECK(ams["ams_exist_bits"].get<std::string>() == "3");
CHECK(ams["tray_exist_bits"].get<std::string>() == "25");
const auto& u0 = ams["ams"][0]["tray"];
CHECK_FALSE(u0[0].contains("tray_slot_placeholder"));
CHECK(u0[1].contains("tray_slot_placeholder"));
CHECK_FALSE(u0[2].contains("tray_slot_placeholder"));
CHECK(u0[2]["tray_type"].get<std::string>() == "");
CHECK(u0[2]["tray_color"].get<std::string>() == "00000000");
const auto& u1 = ams["ams"][1]["tray"];
CHECK(u1[0].contains("tray_slot_placeholder")); // slot 4 absent
CHECK_FALSE(u1[1].contains("tray_slot_placeholder")); // slot 5 present
CHECK(u1[1]["tray_color"].get<std::string>() == "00FF00FF"); // 6-hex padded
}
// why: the exist-bits fields are 64-bit; multi-box rigs past lane 31 would be
// corrupted by a 32-bit unsigned long shift on MSVC. Guards the 64-bit path.
TEST_CASE("unit: AMS payload sets exist bits beyond 31 lanes", "[unit][moonraker]")
{
std::vector<AmsTrayData> trays = {
{33, true, "PLA", "FF0000FF", "", 0, 0},
};
const auto ams = build_bbl_ams_json(trays, 9, 33);
CHECK(ams["ams_exist_bits"].get<std::string>() == "1FF"); // units 0..8
CHECK(ams["tray_exist_bits"].get<std::string>() == "200000000"); // 1 << 33
const auto& u8 = ams["ams"][8]["tray"];
CHECK_FALSE(u8[1].contains("tray_slot_placeholder")); // slot 33 present
}
// why: the sync mode keys off the printer's declared material system; a device
// with no capability record must never read as AMS-capable.
TEST_CASE("unit: AMS capability registry reports only declared material systems", "[unit][moonraker]")
{
CHECK_FALSE(has_ams_capability("cap-none"));
register_ams_capability("cap-true", true);
CHECK(has_ams_capability("cap-true"));
register_ams_capability("cap-false", false);
CHECK_FALSE(has_ams_capability("cap-false"));
// Later replies overwrite: a removed material system must clear the flag.
register_ams_capability("cap-true", false);
CHECK_FALSE(has_ams_capability("cap-true"));
}
// why: these builders preserve the Bambu firmware dialect byte-for-byte, including its trailing space.
TEST_CASE("unit: BBL AMS gcode builders preserve command bytes", "[unit][bbl]")
{