diff --git a/.github/workflows/pr-label-bot.yml b/.github/workflows/pr-label-bot.yml index fe74841831..199b4dd7e7 100644 --- a/.github/workflows/pr-label-bot.yml +++ b/.github/workflows/pr-label-bot.yml @@ -35,6 +35,7 @@ jobs: // kind of change 'crash', 'bug-fix', + 'SECURITY', 'enhancement', 'QoL', 'optimization', @@ -193,6 +194,7 @@ jobs: // kind of change 'crash', 'bug-fix', + 'SECURITY', 'enhancement', 'QoL', 'optimization', diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a7b8adf5f..f462525030 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -834,6 +834,37 @@ find_package(OpenSSL REQUIRED) find_package(CURL REQUIRED) find_package(Freetype REQUIRED) +if (SLIC3R_GUI) + # LibDataChannel's installed export references its bundled dependencies, + # but does not install their CMake targets. Recreate those targets from + # the same dependency prefix before loading the LibDataChannel config. + if (NOT TARGET Usrsctp::usrsctp) + find_library(_ORCA_USRSCTP_LIBRARY NAMES usrsctp + PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH) + if (_ORCA_USRSCTP_LIBRARY) + add_library(Usrsctp::usrsctp UNKNOWN IMPORTED GLOBAL) + set_target_properties(Usrsctp::usrsctp PROPERTIES + IMPORTED_LOCATION "${_ORCA_USRSCTP_LIBRARY}" + IMPORTED_LINK_INTERFACE_LANGUAGES C + INTERFACE_LINK_LIBRARIES "Threads::Threads") + endif() + endif() + + if (NOT TARGET LibJuice::LibJuice) + find_library(_ORCA_LIBJUICE_LIBRARY NAMES juice + PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH) + if (_ORCA_LIBJUICE_LIBRARY) + add_library(LibJuice::LibJuice UNKNOWN IMPORTED GLOBAL) + set_target_properties(LibJuice::LibJuice PROPERTIES + IMPORTED_LOCATION "${_ORCA_LIBJUICE_LIBRARY}" + IMPORTED_LINK_INTERFACE_LANGUAGES C + INTERFACE_LINK_LIBRARIES "Threads::Threads") + endif() + endif() + + find_package(LibDataChannel CONFIG REQUIRED) +endif() + add_library(libcurl INTERFACE) target_link_libraries(libcurl INTERFACE CURL::libcurl) @@ -1114,6 +1145,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls) endif () file(COPY ${_occt_dlls} ${CMAKE_PREFIX_PATH}/bin/freetype.dll + ${CMAKE_PREFIX_PATH}/bin/avformat-61.dll ${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll ${CMAKE_PREFIX_PATH}/bin/swresample-5.dll ${CMAKE_PREFIX_PATH}/bin/swscale-8.dll @@ -1126,6 +1158,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls) ${_out_dir}/WebView2Loader.dll ${_out_dir}/freetype.dll + ${_out_dir}/avformat-61.dll ${_out_dir}/avcodec-61.dll ${_out_dir}/swresample-5.dll ${_out_dir}/swscale-8.dll @@ -1149,7 +1182,10 @@ function(orcaslicer_copy_sos target config postfix output_sos) set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}") endif () - file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so + file(COPY ${CMAKE_PREFIX_PATH}/lib/libavformat.so + ${CMAKE_PREFIX_PATH}/lib/libavformat.so.61 + ${CMAKE_PREFIX_PATH}/lib/libavformat.so.61.1.100 + ${CMAKE_PREFIX_PATH}/lib/libavcodec.so ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61 ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100 ${CMAKE_PREFIX_PATH}/lib/libavutil.so @@ -1164,6 +1200,9 @@ function(orcaslicer_copy_sos target config postfix output_sos) DESTINATION ${_out_dir}) set(${output_sos} + ${_out_dir}/libavformat.so + ${_out_dir}/libavformat.so.61 + ${_out_dir}/libavformat.so.61.1.100 ${_out_dir}/libavcodec.so ${_out_dir}/libavcodec.so.61 ${_out_dir}/libavcodec.so.61.3.100 @@ -1293,6 +1332,8 @@ endif () if (CMAKE_SYSTEM_NAME STREQUAL "Linux") set(LIBRARY_FILES + ${LIBDIR_BIN}/libavformat.so.61 + ${LIBDIR_BIN}/libavformat.so.61.1.100 ${LIBDIR_BIN}/libavcodec.so.61 ${LIBDIR_BIN}/libavcodec.so.61.3.100 ${LIBDIR_BIN}/libavutil.so.59 diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 502e5df071..aa64ad4dc9 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -164,7 +164,7 @@ if (NOT _is_multi AND NOT CMAKE_BUILD_TYPE) endif () function(orcaslicer_add_cmake_project projectname) - cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN}) + cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND;SOURCE_DIR" "CMAKE_ARGS" ${ARGN}) # MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the # generator. A non-Visual-Studio superbuild passes its own generator down, and with @@ -210,12 +210,18 @@ function(orcaslicer_add_cmake_project projectname) set(_build_j "-j${NPROC}") endif () + set(_source_dir_arg "") + if (P_ARGS_SOURCE_DIR) + set(_source_dir_arg SOURCE_DIR ${P_ARGS_SOURCE_DIR}) + endif () + if (NOT IS_CROSS_COMPILE OR NOT APPLE) ExternalProject_Add( dep_${projectname} EXCLUDE_FROM_ALL ON INSTALL_DIR ${DESTDIR} DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname} + ${_source_dir_arg} ${_gen} CMAKE_ARGS -DCMAKE_POLICY_VERSION_MINIMUM=3.5 @@ -249,12 +255,14 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE) # note for future devs: shared libs may actually create a size reduction # but orcaslicer_deps tends to get really funny regarding linking after that (notably boost) # so, as much as I would like to use that, it's not happening - ExternalProject_Add_Step(dep_${projectname} free_download_space - DEPENDEES download # do after download - COMMENT "Freeing Space: Removing source archive" - WORKING_DIRECTORY ${DEP_DOWNLOAD_DIR} - COMMAND ${CMAKE_COMMAND} -E rm -r ${projectname} - ) + if (NOT P_ARGS_SOURCE_DIR) + ExternalProject_Add_Step(dep_${projectname} free_download_space + DEPENDEES download # do after download + COMMENT "Freeing Space: Removing source archive" + WORKING_DIRECTORY ${DEP_DOWNLOAD_DIR} + COMMAND ${CMAKE_COMMAND} -E rm -rf ${projectname} + ) + endif () ExternalProject_Add_Step(dep_${projectname} free_build_space DEPENDEES install # do after install COMMENT "Freeing Space: Removing source and build files" @@ -268,6 +276,7 @@ else() EXCLUDE_FROM_ALL ON INSTALL_DIR ${DESTDIR} DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname} + ${_source_dir_arg} ${_gen} CMAKE_ARGS -DCMAKE_POLICY_VERSION_MINIMUM=3.5 @@ -396,10 +405,6 @@ include(libnoise/libnoise.cmake) include(Draco/Draco.cmake) -include(FFMPEG/FFMPEG.cmake) -include(Assimp/Assimp.cmake) - - # I *think* 1.1 is used for *just* md5 hashing? # 3.1 has everything in the right place, but the md5 funcs used are deprecated # a grep across the repo shows it is used for other things @@ -410,6 +415,12 @@ if(NOT OPENSSL_FOUND) set(OPENSSL_PKG dep_OpenSSL) endif() +include(FFMPEG/FFMPEG.cmake) +include(Assimp/Assimp.cmake) + +include(DataChannel/DataChannel.cmake) +set(DATACHANNEL_PKG dep_DataChannel) + # we don't want to load a "wrong" openssl when loading curl # so, just don't even bother # ...i think this is how it works? change if wrong @@ -483,6 +494,7 @@ set(_dep_list dep_wxInspector dep_FFMPEG dep_Assimp + ${DATACHANNEL_PKG} ) if (MSVC) diff --git a/deps/DataChannel/DataChannel.cmake b/deps/DataChannel/DataChannel.cmake new file mode 100644 index 0000000000..c5ab8a8f1a --- /dev/null +++ b/deps/DataChannel/DataChannel.cmake @@ -0,0 +1,37 @@ +# libdatachannel is the native ICE/DTLS/SCTP implementation used by the +# GUI WebRTC camera controller. Keep the source revision fixed: the signaling +# protocol is evolving independently of this transport dependency. +# +# It vendors plog, usrsctp and libjuice as git submodules, which a plain +# GitHub tag tarball does not include. The flatpak sandbox has no network +# access during the build, so there the manifest itself clones the repo +# (submodules and all) into the dependency download directory before the +# sandbox closes. ExternalProject_Add is pointed at that existing checkout +# instead of being given its own network-dependent download method. +if (FLATPAK) + set(_datachannel_source + SOURCE_DIR ${DEP_DOWNLOAD_DIR}/DataChannel + ) +else() + set(_datachannel_source + GIT_REPOSITORY https://github.com/paullouisageneau/libdatachannel.git + GIT_TAG v0.24.5 + GIT_SHALLOW ON + GIT_SUBMODULES_RECURSE ON + ) +endif() + +orcaslicer_add_cmake_project(DataChannel + DEPENDS ${OPENSSL_PKG} + CMAKE_ARGS + -DNO_EXAMPLES=ON + -DNO_TESTS=ON + -DNO_WEBSOCKET=ON + -DNO_MEDIA=ON + -DUSE_NICE=OFF + -DUSE_SYSTEM_JUICE=OFF + -DUSE_SYSTEM_USRSCTP=OFF + -DOPENSSL_ROOT_DIR:PATH=${DESTDIR} + -DOPENSSL_USE_STATIC_LIBS=ON + ${_datachannel_source} +) diff --git a/deps/FFMPEG/FFMPEG.cmake b/deps/FFMPEG/FFMPEG.cmake index 38e952bbdd..4fd521321d 100644 --- a/deps/FFMPEG/FFMPEG.cmake +++ b/deps/FFMPEG/FFMPEG.cmake @@ -1,14 +1,26 @@ set(_conf_cmd ./configure) +set(_ffmpeg_depends) +set(_ffmpeg_configure_command ${_conf_cmd}) +if (TARGET dep_OpenSSL) + set(_ffmpeg_depends DEPENDS dep_OpenSSL) + set(_ffmpeg_configure_command + ${CMAKE_COMMAND} -E env + "PKG_CONFIG_PATH=${DESTDIR}/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}" + ${_conf_cmd} + ) +endif() + if (MSVC) set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG") - set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-winarm64-orca-shared-7.0.zip") - set(PREBUILD_HASH_arm64 "12f4140279f2f8469885e1b5b2e8be9d788882914c21523cacd56989f3548054") - set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-win64-orca-shared-7.0.zip") - set(PREBUILD_HASH_x64 "e65916020ddb9ef84b2666dfbcbfc9b1d67f69d15b4a66db53754637bf2d498c") + set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-winarm64-orca-shared-7.0.zip") + set(PREBUILD_HASH_arm64 "da480cbb39680056de824c57ec4dc3bd577b479ebbc310ff1f9dc55cf014b4c1") + set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-win64-orca-shared-7.0.zip") + set(PREBUILD_HASH_x64 "85da19daf198f5548259d8aabb349db84997a3f6e6886d8d7764114add9c6dae") ExternalProject_Add(dep_FFMPEG + ${_ffmpeg_depends} URL ${PREBUILD_URL_${DEPS_ARCH}} URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}} DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG @@ -21,6 +33,8 @@ if (MSVC) ) else () + set(_openssl_cmd --enable-openssl) + if (APPLE) set(_minos_cmd "--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}" @@ -52,10 +66,11 @@ else () endif() ExternalProject_Add(dep_FFMPEG + ${_ffmpeg_depends} URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG - CONFIGURE_COMMAND ${_conf_cmd} + CONFIGURE_COMMAND ${_ffmpeg_configure_command} ${_cross_cmd} ${_pic_cmd} ${_arch_cmd} @@ -63,20 +78,21 @@ else () "--prefix=${DESTDIR}" ${_link_cmd} ${_minos_cmd} + ${_openssl_cmd} --disable-doc --enable-small --disable-outdevs --disable-filters --enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test* --disable-protocols - --enable-protocol=file,fd,pipe,rtp,udp + --enable-protocol=file,fd,pipe,http,https,rtp,tcp,udp --disable-muxers --enable-muxer=rtp --disable-encoders --disable-decoders --enable-decoder=*aac*,h264*,mp3*,mjpeg,rv* --disable-demuxers - --enable-demuxer=h264,mp3,mov + --enable-demuxer=h264,mp3,mov,mpjpeg,rtsp,sdp --disable-zlib --disable-avdevice BUILD_IN_SOURCE ON diff --git a/docs/HLSD/printer-agent.md b/docs/HLSD/printer-agent.md new file mode 100644 index 0000000000..7062fab966 --- /dev/null +++ b/docs/HLSD/printer-agent.md @@ -0,0 +1,240 @@ +# Printer agents + +Printer agents isolate printer-specific communication from the rest of OrcaSlicer. The GUI and +`DeviceManager` operate on a shared set of printer operations and device state; a selected printer +agent implements those operations for a particular printer ecosystem. The agent boundary allows +Bambu, Moonraker-based printers, built-in integrations, and Python-provided integrations to use the +same application workflow without making the GUI understand every printer protocol. + +The current boundary is an adapter boundary around the existing application contract. In particular, +some request fields and message payloads still use the Bambu-shaped representation that existing +`MachineObject` and `DeviceManager` code consumes. The printer agent is responsible for translating +that representation into the protocol spoken by its printer. This is an intentional compatibility +constraint of the current design; the interface is not yet a neutral printer protocol. + +The v1 dialect migration path is deliberately narrow. `DeviceManager` currently speaks the Bambu JSON +dialect because that is the payload shape already used throughout the command and state workflow. The +v1 `OrcaPrinterAgent` also accepts that Bambu dialect. Its transport path places the small translation +needed for the target printer at `deliver_to_sink`, keeping the compatibility code at the edge rather +than spreading it through `DeviceManager` or the agent interface. + +The eventual direction is for `DeviceManager` to produce an Orca JSON dialect. The Bambu agent will then +own the translation from Orca JSON to Bambu's protocol, while `OrcaPrinterAgent` can forward the Orca +payload directly to its sink. The v1 translation at `deliver_to_sink` can then be removed without +changing `DeviceManager`, the command callers, or the rest of the agent workflow. + +## Components + +The system has four relevant layers: + +```text +GUI / DeviceManager / MachineObject + | + NetworkAgent + / \ + IPrinterAgent ICloudServiceAgent + | | + printer protocol authentication and cloud services +``` + +### `DeviceManager` and `MachineObject` + +`DeviceManager` owns the application-facing printer workflow. It maintains `MachineObject` instances, +updates their state, filters devices for the active printer agent, and initiates operations such as +homing, temperature changes, printing, subscriptions, and camera playback. + +`MachineObject` remains the shared state model used by the GUI. It does not contain the implementation +of a printer protocol. When a device is discovered or returned by a cloud query, the device is tagged +with the active `printer_agent_id`. Device lists and selected-machine operations use that tag to avoid +sending an operation through an agent that does not own the device. + +### `NetworkAgent` + +`NetworkAgent` is the façade used by the GUI and `DeviceManager`. It owns: + +- the currently selected `IPrinterAgent`; +- the registered cloud-service instances, indexed by provider; +- callbacks shared by the active printer agent and the application; +- the forwarding methods for printer commands and cloud operations. + +There is one active printer agent for the currently selected printer preset. Switching the preset +increments the machine-list generation, disconnects the old printer agent, removes its callbacks, and +installs the newly selected agent. The façade then forwards printer operations to that agent. + +Cloud operations are selected separately using a provider key. `NetworkAgent` forwards a cloud request +to the matching `ICloudServiceAgent`, and forwards cloud camera operations with a device ID. The +printer agent receives a cloud-agent pointer through `set_cloud_agent()` when it is created, allowing +printer communication to obtain cloud tokens without depending on a concrete cloud implementation. + +### `IPrinterAgent` + +`IPrinterAgent` is the printer-facing contract. It covers: + +- cloud-relay and direct-LAN message delivery; +- LAN connection, discovery, binding, and certificates; +- printer subscriptions and callbacks; +- print operations; +- filament synchronization; +- camera capability and local camera URL reporting; +- printer command methods. + +Concrete built-in implementations include the Bambu wrapper, the native Orca/Moonraker path, and +other printer-agent implementations registered by the application. A printer agent may use either +the cloud agent, a direct LAN connection, or both. + +### `ICloudServiceAgent` + +`ICloudServiceAgent` owns authentication and services provided by a cloud backend. It covers login +state, tokens, user and printer lists, settings synchronization, model services, cloud messages, and +cloud camera operations. + +Cloud camera operations are device-scoped: + +- `get_camera_url(dev_id, callback)` obtains a stream URL for one device; +- `create_camera_signaling_channel(dev_id)` creates signaling for one device where the provider + supports it. + +This is separate from the local camera URL exposed by `IPrinterAgent`, which is currently scoped to +the active printer agent because a normal LAN agent represents one physical printer connection. + +## Agent registration and selection + +`NetworkAgentFactory` maintains the printer-agent registry. Each registry entry contains an agent ID, +a display name, and a factory function. Built-in agents register during application initialization. +Python printer-agent capabilities register dynamically and contribute an agent ID and factory entry. + +The selected printer preset contains the printer-agent choice. If no explicit choice is stored, the +application preserves the existing default behavior: Bambu presets select the Bambu agent and other +presets select the native Orca agent. When a preset is changed, `GUI_App` resolves the effective agent +ID, obtains the corresponding cloud agent, creates the printer agent through the registry, and installs +it in `NetworkAgent`. + +The registry rejects conflicting agent IDs. This matters for Python plugins because an agent ID is the +stable identity used by presets and device ownership; two enabled plugin capabilities must not claim +the same ID. + +## Message and command flow + +There are two low-level message paths: + +- `send_message()` publishes a command through the printer's cloud relay; +- `send_message_to_printer()` sends a command directly to the printer over the LAN path. + +Both paths accept a JSON string, quality-of-service and flag values, and return the existing network +status code domain. The agent owns the conversion from that JSON contract to its native transport. + +The typed `command_*` methods are the application-facing convenience layer. The five generic defaults +currently implemented by `IPrinterAgent` construct the existing JSON dialect and route through the +same message path: + +| Method | Default operation | +| --- | --- | +| `command_xyz_abs()` | Send `G90` for absolute positioning | +| `command_auto_leveling()` | Send `G29` for bed leveling | +| `command_go_home()` | Use the supported homing operation or send `G28` | +| `command_set_bed()` | Use the supported bed control or send `M140` | +| `command_set_nozzle()` | Send `M104` for nozzle temperature | + +These are compatibility defaults for common printer workflows, not a guarantee that every firmware +implements every command identically. An agent can override a method when its protocol needs another +operation. For example, a Klipper configuration may use `BED_MESH_CALIBRATE` instead of `G29`. + +The remaining common command methods default to `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` because their +existing behavior is vendor-specific or has no portable implementation: + +- AMS RFID refresh; +- AMS calibration; +- AMS tray selection; +- camera start; +- axis control. + +The methods remain on the common interface so an agent that supports them can override them explicitly. +`sequence_id` remains part of the command contract because `DeviceManager` creates and tracks it as +the command ID. + +## Device ownership and stale responses + +Printer-agent ownership is represented by `printer_agent_id` on device records and `MachineObject` +instances. The active agent ID is attached when a device is discovered, returned by a cloud list, or +reused after a preset switch. Local-machine configuration also persists the agent ID so a saved LAN +device is not silently reused by an unrelated agent. + +Cloud printer-list responses carry three pieces of request context added by `NetworkAgent`: + +```text +provider cloud provider used for the request +agent_id active printer agent when the request was made +generation machine-list generation when the request was made +``` + +`DeviceManager` accepts the response only when those values still match the current provider, active +agent, and generation. This prevents a slow response from the previous preset or provider from +repopulating the current device list. + +The provider mapping is currently selected by `GUI_App`: the Bambu agent maps to the Bambu cloud +provider and other agents map to the Orca cloud provider. The generation check protects that existing +selection from races; it does not make cloud-provider ownership intrinsic to an agent. Cloud-printer +ownership and the broader Orca cloud services are therefore still separate architectural concerns. + +## Python printer agents + +`PrinterAgentPluginCapability` implements `IPrinterAgent` directly. The live capability object is +registered with `NetworkAgentFactory` and handed out as the printer agent when its agent ID is selected. +The plugin receives the selected `ICloudServiceAgent` through `set_cloud_agent()` just like a built-in +printer agent. + +Python plugins must implement the core communication and lifecycle methods required by the interface, +including agent metadata, printer connection, discovery callbacks, and the two message-send methods. +Methods that are meaningful only to a particular printer are optional overrides where the C++ base +class provides a default. + +All ten `command_*` methods are available in the Python binding and in the trampoline. Their override +status is intentionally optional: + +- the five generic commands use the C++ default when Python does not override them; +- the five vendor-specific commands return `NOT_SUPPORTED` unless Python supplies an implementation; +- a Python implementation can replace either behavior for its own protocol. + +The Python camera binding exposes HTTP, HTTPS, RTSP, and HTTP-snapshot modes. WebRTC remains a +built-in C++ camera mode, but is not exposed as a Python mode because the current Python capability +does not provide the corresponding cloud signaling-channel contract. + +## Camera playback boundary + +The camera stream mode describes how a stream is obtained; it does not by itself define ownership of +the wxWidgets view that renders it. `MediaPlayCtrl` selects and tears down the active backend, while +the wx parent owns the child window or renderer. This is important because a web view, native media +control, and frame-based/WebRTC renderer have different wx window-lifetime requirements. + +Cloud URL and signaling requests are routed through `NetworkAgent` to the cloud provider selected for +the device. Local URL requests are routed to the active printer agent. The distinction keeps cloud +account services device-scoped while preserving the current one-LAN-agent/one-printer model. + +## Compatibility constraints + +The printer-agent boundary intentionally preserves several existing application contracts: + +- Bambu-shaped JSON is still the shared command representation; +- existing network status codes are reused, with Orca-specific unsupported/capability errors added + in the Orca-reserved range; +- `MachineObject` remains the shared device-state model; +- preset and local-machine data retain compatibility with the existing agent-selection behavior; +- Python plugins use the existing capability and pybind11 registration system. + +The agent abstraction is therefore responsible for containing vendor differences, not for pretending +that all vendor protocols are identical. The planned Orca JSON dialect is the protocol-neutral command +model for the `DeviceManager`/agent boundary. Once it is introduced, Bambu-specific translation remains +inside the Bambu agent and the Orca agent's v1 sink adapter can be removed as a self-contained cleanup. + +## Main implementation locations + +- [`IPrinterAgent`](../../src/slic3r/Utils/IPrinterAgent.hpp) — printer-agent contract and generic command defaults +- [`ICloudServiceAgent`](../../src/slic3r/Utils/ICloudServiceAgent.hpp) — cloud service and per-device + cloud camera contract +- [`NetworkAgent`](../../src/slic3r/Utils/NetworkAgent.hpp) — façade and dispatch between active agents +- [`NetworkAgentFactory`](../../src/slic3r/Utils/NetworkAgentFactory.hpp) — built-in and Python agent registry +- [`DeviceManager`](../../src/slic3r/GUI/DeviceCore/DevManager.cpp) — device ownership, filtering, and + stale-response checks +- [`PrinterAgentPluginCapability`](../../src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp) + — Python bindings +- [`MediaPlayCtrl`](../../src/slic3r/GUI/MediaPlayCtrl.cpp) — camera backend selection and playback lifecycle diff --git a/resources/web/dialog/PluginsDialog/index.js b/resources/web/dialog/PluginsDialog/index.js index ad1242e4f9..f02cc08855 100644 --- a/resources/web/dialog/PluginsDialog/index.js +++ b/resources/web/dialog/PluginsDialog/index.js @@ -575,7 +575,7 @@ function CapabilityCanRun(plugin, capability) { } function IsPluginChecked(plugin) { - return GetStatus(plugin) === "Activated"; + return plugin.is_loaded; } function HasMixedCapabilityState(plugin) { @@ -1347,6 +1347,8 @@ function StatusDescription(plugin) { return "This plugin is still loading."; case "Error": return "This plugin is blocked until its error is fixed."; + case "RuntimeError": + return "This plugin is loaded but a capability reported an error."; case "Inactive": default: return "This plugin is inactive. Activate it to install or load it."; diff --git a/resources/web/dialog/PluginsDialog/styles.css b/resources/web/dialog/PluginsDialog/styles.css index 3d8c18be9e..9eef78760d 100644 --- a/resources/web/dialog/PluginsDialog/styles.css +++ b/resources/web/dialog/PluginsDialog/styles.css @@ -424,6 +424,11 @@ body.pane-resizing { font-weight: 600; } +.status-cell.status-runtimeerror { + color: var(--plugin-status-warn); + font-weight: 600; +} + .status-cell.status-loading { color: var(--plugin-status-warn); font-weight: 600; @@ -680,6 +685,11 @@ body.pane-resizing { color: var(--plugin-status-danger); } +.detail-status-chip.status-runtimeerror { + background: var(--plugin-status-warn-bg); + color: var(--plugin-status-warn); +} + .detail-status-chip.status-loading { background: var(--plugin-status-warn-bg); color: var(--plugin-status-warn); diff --git a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml index 404dbf5fe4..25cb4fdcc5 100644 --- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml +++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml @@ -331,6 +331,15 @@ modules: sha256: deedcabe339165214a3637df4c86a507aef0d793cf8774ff68735f4737e8ddbc dest: external-packages/FFMPEG + # libdatachannel v0.24.5 + # The Git checkout includes the submodules that are missing from the + # GitHub source archive. DataChannel.cmake consumes it as SOURCE_DIR. + - type: git + url: https://github.com/paullouisageneau/libdatachannel.git + tag: v0.24.5 + commit: 443f6934d9007eb7076ab7825ba330f355fcbead + dest: external-packages/DataChannel + # --------------------------------------------------------------- # Fallback archives for deps normally provided by the GNOME SDK. # These are only used if find_package() fails to locate them. diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 3ed76b8976..87352f505e 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1913,7 +1913,8 @@ int CLI::run(int argc, char **argv) old_printable_width = static_cast(old_printable_bbox.size().x()); old_printable_depth = static_cast(old_printable_bbox.size().y()); } - old_printable_height = (int)(config.opt_float("printable_height")); + if (config.option("printable_height")) + old_printable_height = (int)(config.opt_float("printable_height")); if (config.option("extruder_clearance_height_to_rod")) old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod"); diff --git a/src/libslic3r/Fill/FillGyroid.cpp b/src/libslic3r/Fill/FillGyroid.cpp index c740dd2b4b..c9a108573d 100644 --- a/src/libslic3r/Fill/FillGyroid.cpp +++ b/src/libslic3r/Fill/FillGyroid.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "FillBase.hpp" #include "FillGyroid.hpp" @@ -123,44 +124,41 @@ static inline double f(double x, double z_sin, double z_cos, bool vertical, bool } } +// Repeats one period of a wave from the last sample at or before x_min to the first one at or after x_max. static inline Polyline make_wave( - const std::vector& one_period, double width, double height, double offset, double scaleFactor, - double z_cos, double z_sin, bool vertical, bool flip) + const std::vector& one_period, double x_min, double x_max, double offset, double scaleFactor, bool vertical) { - std::vector points = one_period; - double period = points.back()(0); - if (width != period) // do not extend if already truncated - { - points.reserve(one_period.size() * size_t(floor(width / period))); - points.pop_back(); + const double period = one_period.back().x(); + // The last sample of a period is the first one of the next. + const size_t n = one_period.size() - 1; + double x0 = std::floor(x_min / period) * period; + size_t i = 0; + while (i + 1 < n && x0 + one_period[i + 1].x() <= x_min) + ++i; - size_t n = points.size(); - do { - points.emplace_back(points[points.size()-n].x() + period, points[points.size()-n].y()); - } while (points.back()(0) < width - EPSILON); - - points.emplace_back(Vec2d(width, f(width, z_sin, z_cos, vertical, flip))); - } - - // and construct the final polyline to return: Polyline polyline; - polyline.points.reserve(points.size()); - for (auto& point : points) { - point(1) += offset; - point(1) = std::clamp(double(point.y()), 0., height); + polyline.points.reserve(size_t((x_max - x0) / period + 1.) * n + 1); + for (;;) { + Vec2d point(x0 + one_period[i].x(), one_period[i].y() + offset); + const bool last = point.x() >= x_max; if (vertical) std::swap(point(0), point(1)); polyline.points.emplace_back((point * scaleFactor).cast()); + if (last) + break; + if (++i == n) { + i = 0; + x0 += period; + } } - return polyline; } -static std::vector make_one_period(double width, double scaleFactor, double z_cos, double z_sin, bool vertical, bool flip, double tolerance) +static std::vector make_one_period(double scaleFactor, double z_cos, double z_sin, bool vertical, bool flip, double tolerance) { std::vector points; double dx = M_PI_2; // exact coordinates on main inflexion lobes - double limit = std::min(2*M_PI, width); + double limit = 2*M_PI; points.reserve(coord_t(ceil(limit / tolerance / 3))); for (double x = 0.; x < limit - EPSILON; x += dx) { @@ -239,7 +237,8 @@ static inline double compute_omega_factor(double density_adjusted, double line_s return std::clamp(raw, 1.0, 2.0); } -static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height) +// Waves covering bbox, with the pattern anchored at origin. +static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double line_spacing, const BoundingBox &bbox, const Point &origin) { const double scaleFactor = scale_(line_spacing) / density_adjusted; @@ -254,29 +253,36 @@ static Polylines make_gyroid_waves(double gridZ, double density_adjusted, double const double z_cos = cos(z); bool vertical = (std::abs(z_sin) <= std::abs(z_cos)); + // Range to cover in pattern units, with the waves running along x. + Vec2d lo = (bbox.min - origin).cast() / scaleFactor; + Vec2d hi = (bbox.max - origin).cast() / scaleFactor; double lower_bound = 0.; - double upper_bound = height; bool flip = true; if (vertical) { flip = false; lower_bound = -M_PI; - upper_bound = width - M_PI_2; - std::swap(width,height); + std::swap(lo(0), lo(1)); + std::swap(hi(0), hi(1)); } - std::vector one_period_odd = make_one_period(width, scaleFactor, z_cos, z_sin, vertical, flip, tolerance); // creates one period of the waves, so it doesn't have to be recalculated all the time + std::vector one_period_odd = make_one_period(scaleFactor, z_cos, z_sin, vertical, flip, tolerance); // creates one period of the waves, so it doesn't have to be recalculated all the time flip = !flip; // even polylines are a bit shifted - std::vector one_period_even = make_one_period(width, scaleFactor, z_cos, z_sin, vertical, flip, tolerance); - Polylines result; + std::vector one_period_even = make_one_period(scaleFactor, z_cos, z_sin, vertical, flip, tolerance); - for (double y0 = lower_bound; y0 < upper_bound + EPSILON; y0 += M_PI) { - // creates odd polylines - result.emplace_back(make_wave(one_period_odd, width, height, y0, scaleFactor, z_cos, z_sin, vertical, flip)); - // creates even polylines - y0 += M_PI; - if (y0 < upper_bound + EPSILON) { - result.emplace_back(make_wave(one_period_even, width, height, y0, scaleFactor, z_cos, z_sin, vertical, flip)); + // Every wave spans [offset + f_min, offset + f_max] across. + double f_min = std::numeric_limits::max(); + double f_max = std::numeric_limits::lowest(); + for (const std::vector *one_period : { &one_period_odd, &one_period_even }) + for (const Vec2d &point : *one_period) { + f_min = std::min(f_min, point.y()); + f_max = std::max(f_max, point.y()); } + + Polylines result; + for (int i = int(std::ceil((lo.y() - f_max - lower_bound) / M_PI)); lower_bound + i * M_PI + f_min <= hi.y(); ++i) { + Polyline &wave = result.emplace_back(make_wave(i % 2 == 0 ? one_period_odd : one_period_even, lo.x(), hi.x(), + lower_bound + i * M_PI, scaleFactor, vertical)); + wave.translate(origin); } return result; @@ -302,12 +308,12 @@ void FillGyroid::_fill_surface_single( // Distance between the gyroid waves in scaled coordinates. coord_t distance = coord_t(scale_(this->spacing) / density_adjusted); - // align bounding box to a multiple of our grid module - bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance))); + // Anchor the pattern to our grid module; the 10-line shift keeps its established phase. + const coord_t shift = coord_t(10 * scale_(this->spacing)); + const Point origin = align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)) - Point(shift, shift); - // Expand the bounding box to avoid artifacts at the edges - coord_t expand = 10 * (scale_(this->spacing)); - bb.offset(expand); + // Keep the pattern ends and the multiline copies outside the contour. + bb.offset(scale_(this->spacing * params.multiline)); // generate pattern Polylines polylines; @@ -327,23 +333,14 @@ void FillGyroid::_fill_surface_single( const float density_factor = std::max(0.001f, float(params.density * DensityAdjust / params.multiline)); const float period = float(2.0 * M_PI) * float(this->spacing) / density_factor; - // bb is already expanded above by 10 * scale_(spacing) for edge artifacts; - // skip a second offset here to avoid raster-area bloat in the marching squares pass. + // A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region. + const coord_t cell = scaled(marchsq::GyroidField::gsizef); + bb.offset(cell); + bb.merge(align_to_grid(bb.min, Point(cell, cell))); marchsq::GyroidField sf(bb, this->z, period, float(omega)); polylines = marchsq::get_gyroid_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION); } else { - polylines = make_gyroid_waves( - scale_(this->z), - density_adjusted, - this->spacing, - ceil(bb.size()(0) / distance) + 1., - ceil(bb.size()(1) / distance) + 1.); - - // The parametric generator produces wave coords relative to the grid origin; - // shift them into absolute layer coords. The marching-squares branch above - // already emits absolute coords via GyroidField::to_Point, so it skips this. - for (Polyline &pl : polylines) - pl.translate(bb.min); + polylines = make_gyroid_waves(scale_(this->z), density_adjusted, this->spacing, bb, origin); } // Apply multiline offset if needed diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 63e3ba6105..d2913db55e 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -3954,7 +3954,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector("filament_colour_type"); ConfigOptionInts * filament_map = project_config.option("filament_map"); ConfigOptionInts * filament_volume_map = project_config.option("filament_volume_map"); - // Snapshot and temporarily strip mixed filament slots so AMS sync operates on physical // filaments only. A mixed slot is virtual and has no tray to sync against; leaving it in // would let AMS mapping overwrite it and would break the physical-first slot ordering the diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index e86aa38702..e37772dc2f 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -236,6 +236,7 @@ set(SLIC3R_GUI_SOURCES GUI/GLToolbar.hpp GUI/ImageDPIFrame.cpp GUI/ImageDPIFrame.hpp + GUI/IMediaController.hpp GUI/GUI_App.cpp GUI/GUI_App.hpp GUI/GUI_AuxiliaryList.cpp @@ -365,6 +366,8 @@ set(SLIC3R_GUI_SOURCES GUI/MediaFilePanel.h GUI/MediaPlayCtrl.cpp GUI/MediaPlayCtrl.h + GUI/WebRtcMediaController.cpp + GUI/WebRtcMediaController.hpp GUI/MeshUtils.cpp GUI/MeshUtils.hpp GUI/ModelMall.cpp @@ -560,6 +563,8 @@ set(SLIC3R_GUI_SOURCES GUI/WebUserLoginDialog.hpp GUI/WebViewDialog.cpp GUI/WebViewDialog.hpp + GUI/WebMediaController.hpp + GUI/WebMediaController.cpp GUI/Widgets/AMSControl.cpp GUI/Widgets/AMSControl.hpp GUI/Widgets/AMSItem.cpp @@ -754,6 +759,7 @@ set(SLIC3R_GUI_SOURCES Utils/NetworkAgentFactory.cpp Utils/ICloudServiceAgent.hpp Utils/IPrinterAgent.hpp + Utils/ICameraSignalingChannel.hpp Utils/OrcaCloudServiceAgent.cpp Utils/OrcaCloudServiceAgent.hpp Utils/OrcaPrinterAgent.cpp @@ -924,7 +930,7 @@ else() set(_opengl_link_lib OpenGL::GL) endif() -target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise pybind11::embed) +target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto LibDataChannel::LibDataChannel noise::noise pybind11::embed) if (CMAKE_SYSTEM_NAME STREQUAL "Linux") # Linux finds wxWidgets in module mode, whose include dirs and definitions @@ -981,29 +987,32 @@ endif () if (APPLE) # Static FFmpeg from the deps install: nothing to bundle into the .app, - # no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil. + # no rpath/install_name handling. Order matters: avformat -> avcodec -> swscale -> avutil. + find_library(LIBAVFORMAT_LIBRARY NAMES libavformat.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) - if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) - message(FATAL_ERROR "Static FFmpeg (libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.") + if (NOT LIBAVFORMAT_LIBRARY OR NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) + message(FATAL_ERROR "Static FFmpeg (libavformat.a/libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.") endif () - target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) + target_link_libraries(libslic3r_gui ${LIBAVFORMAT_LIBRARY} ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) elseif (WIN32) # Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config, # so resolve the import libraries out of the deps prefix directly; the DLLs # are copied next to the executable by the top level CMakeLists. + find_library(LIBAVFORMAT_LIBRARY NAMES avformat PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) - if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) - message(FATAL_ERROR "FFmpeg (avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.") + if (NOT LIBAVFORMAT_LIBRARY OR NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) + message(FATAL_ERROR "FFmpeg (avformat/avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.") endif () - target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) + target_link_libraries(libslic3r_gui ${LIBAVFORMAT_LIBRARY} ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) else () pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET + libavformat libavcodec libswscale libavutil diff --git a/src/slic3r/GUI/AVVideoDecoder.cpp b/src/slic3r/GUI/AVVideoDecoder.cpp index d7b8432bd3..76965fedef 100644 --- a/src/slic3r/GUI/AVVideoDecoder.cpp +++ b/src/slic3r/GUI/AVVideoDecoder.cpp @@ -45,6 +45,25 @@ int AVVideoDecoder::open(Bambu_StreamInfo const &info) return 0; } +int AVVideoDecoder::open(AVCodecParameters const ¶meters) +{ + if (avcodec_parameters_to_context(codec_ctx_, ¶meters) < 0) + return -1; + + auto codec = avcodec_find_decoder(codec_ctx_->codec_id); + if (codec == nullptr) { + fprintf(stderr, "AVVideoDecoder: unsupported codec!\n"); + return -1; + } + if (avcodec_open2(codec_ctx_, codec, nullptr) < 0) { + fprintf(stderr, "AVVideoDecoder: could not open codec\n"); + return -1; + } + + frame_ = av_frame_alloc(); + return frame_ == nullptr ? -1 : 0; +} + int AVVideoDecoder::decode(const Bambu_Sample &sample) { int ret = -1; @@ -71,6 +90,14 @@ int AVVideoDecoder::decode(const Bambu_Sample &sample) return ret; } +int AVVideoDecoder::decode(const AVPacket &packet) +{ + int ret = avcodec_send_packet(codec_ctx_, &packet); + if (ret == 0) + got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0; + return ret; +} + int AVVideoDecoder::flush() { int ret = avcodec_send_packet(codec_ctx_, nullptr); diff --git a/src/slic3r/GUI/AVVideoDecoder.hpp b/src/slic3r/GUI/AVVideoDecoder.hpp index 4111e860a2..d3d16a11c1 100644 --- a/src/slic3r/GUI/AVVideoDecoder.hpp +++ b/src/slic3r/GUI/AVVideoDecoder.hpp @@ -23,8 +23,10 @@ public: public: int open(Bambu_StreamInfo const &info); + int open(AVCodecParameters const ¶meters); int decode(Bambu_Sample const &sample); + int decode(AVPacket const &packet); int flush(); @@ -34,6 +36,14 @@ public: bool toWxBitmap(wxBitmap &bitmap, wxSize const & size); + // Native size of the most recently decoded frame, or an unspecified size if + // nothing has decoded yet. Lets a caller learn the video dimensions when the + // container/probe could not report them up front. + wxSize decoded_frame_size() const + { + return got_frame_ && frame_ ? wxSize{frame_->width, frame_->height} : wxSize{}; + } + private: AVCodecContext *codec_ctx_ = nullptr; AVFrame * frame_ = nullptr; diff --git a/src/slic3r/GUI/CalibrationPanel.cpp b/src/slic3r/GUI/CalibrationPanel.cpp index d906a8211c..14e1d5dea2 100644 --- a/src/slic3r/GUI/CalibrationPanel.cpp +++ b/src/slic3r/GUI/CalibrationPanel.cpp @@ -328,7 +328,7 @@ void SelectMObjectPopup::update_user_devices() } m_bind_machine_list.clear(); - m_bind_machine_list = dev->get_my_machine_list(); + m_bind_machine_list = dev->get_my_machine_list(dev->get_current_printer_agent_id()); //sort list std::vector> user_machine_list; diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index f4160885cb..01c9843305 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -6,8 +6,10 @@ #include "Widgets/Label.hpp" #include "MsgDialog.hpp" #include "libslic3r/Print.hpp" +#include "PrePrintChecker.hpp" #include "DeviceCore/DevConfig.h" +#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevFilaBlackList.h" #include "DeviceCore/DevFilaSystem.h" @@ -1637,29 +1639,6 @@ void CalibrationPresetPage::update_combobox_filaments(MachineObject* obj) select_default_compatible_filament(); } -bool CalibrationPresetPage::is_blocking_printing() -{ - DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (!dev) return true; - - MachineObject* obj_ = dev->get_selected_machine(); - if (obj_ == nullptr) return true; - - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); - auto target_model = obj_->printer_type; - - if (source_model != target_model) { - std::vector compatible_machine = obj_->get_compatible_machine(); - vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); - if (it == compatible_machine.end()) { - return true; - } - } - - return false; -} - bool CalibrationPresetPage::is_nozzle_info_synced() const { if (!curr_obj || !curr_obj->is_info_ready()) @@ -1741,11 +1720,13 @@ void CalibrationPresetPage::update_show_status() } } - //if (is_blocking_printing()) { - // show_status(CaliPresetPageStatus::CaliPresetStatusUnsupportedPrinter); - // return; - //} - //else + bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type); + if (PresetBundle *preset_bundle = wxGetApp().preset_bundle) { + has_optional_printer_model = has_optional_printer_model || + DevPrinterConfigUtil::is_optional_printer_model_id( + preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle)); + } + if (obj_->is_connecting() || !obj_->is_connected()) { show_status(CaliPresetPageStatus::CaliPresetStatusInConnecting); return; @@ -1797,7 +1778,9 @@ void CalibrationPresetPage::update_show_status() return; } - show_status(CaliPresetPageStatus::CaliPresetStatusNormal); + show_status(has_optional_printer_model ? + CaliPresetPageStatus::CaliPresetStatusOptionalPrinterModel : + CaliPresetPageStatus::CaliPresetStatusNormal); } @@ -1851,6 +1834,10 @@ void CalibrationPresetPage::show_status(CaliPresetPageStatus status) Layout(); Fit(); } + else if (status == CaliPresetPageStatus::CaliPresetStatusOptionalPrinterModel) { + update_print_status_msg(PrePrintChecker::get_pre_state_msg(PrintDialogStatus::PrintStatusOptionalPrinterModel), true); + Enable_Send_Button(true); + } else if (status == CaliPresetPageStatus::CaliPresetStatusNoUserLogin) { wxString msg_text = _L("No login account, only printers in LAN mode are displayed."); update_print_status_msg(msg_text, false); diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.hpp b/src/slic3r/GUI/CalibrationWizardPresetPage.hpp index adb4ce52ab..4d1adb1004 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.hpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.hpp @@ -152,7 +152,8 @@ enum CaliPresetPageStatus CaliPresetStatusInConnecting, CaliPresetStatusFilamentIncompatible, CaliPresetStatusLanModeSDcardNotAvailable, - CaliPresetStatusDifferentNozzleDiameters + CaliPresetStatusDifferentNozzleDiameters, + CaliPresetStatusOptionalPrinterModel }; class CalibrationPresetPage : public CalibrationWizardPage @@ -268,7 +269,6 @@ protected: bool is_nozzle_info_synced() const; void show_status(CaliPresetPageStatus status); void Enable_Send_Button(bool enable); - bool is_blocking_printing(); bool need_check_sdcard(MachineObject* obj); CaliPresetPageStatus get_status() { return m_page_status; } diff --git a/src/slic3r/GUI/CameraPopup.cpp b/src/slic3r/GUI/CameraPopup.cpp index c4e3c5486c..099edd7de0 100644 --- a/src/slic3r/GUI/CameraPopup.cpp +++ b/src/slic3r/GUI/CameraPopup.cpp @@ -28,7 +28,6 @@ wxEND_EVENT_TABLE() wxDEFINE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDEFINE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); -wxDEFINE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent); #define CAMERAPOPUP_CLICK_INTERVAL 20 @@ -102,34 +101,6 @@ CameraPopup::CameraPopup(wxWindow *parent) top_sizer->Add(0, 0, wxALL, 0); } - // Orca: custom IP camera source — lets the user point Live Video at any camera URL (Orca feature; not in the reference) - m_custom_camera_input_confirm = new Button(m_panel, _L("Enable")); - m_custom_camera_input_confirm->SetBackgroundColor(wxColour(38, 166, 154)); - m_custom_camera_input_confirm->SetBorderColor(wxColour(38, 166, 154)); - m_custom_camera_input_confirm->SetTextColor(wxColour(0xFFFFFE)); - m_custom_camera_input_confirm->SetFont(Label::Body_14); - m_custom_camera_input_confirm->SetMinSize(wxSize(FromDIP(90), FromDIP(30))); - m_custom_camera_input_confirm->SetPosition(wxDefaultPosition); - m_custom_camera_input_confirm->SetCornerRadius(FromDIP(12)); - m_custom_camera_input = new TextInput(m_panel, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, wxDefaultSize); - m_custom_camera_input->GetTextCtrl()->SetHint(_L("Hostname or IP")); - m_custom_camera_input->GetTextCtrl()->SetFont(Label::Body_14); - m_custom_camera_hint = new wxStaticText(m_panel, wxID_ANY, _L("Custom camera source")); - m_custom_camera_hint->Wrap(-1); - m_custom_camera_hint->SetFont(Label::Head_14); - m_custom_camera_hint->SetForegroundColour(TEXT_COL); - - m_custom_camera_input_confirm->Bind(wxEVT_BUTTON, &CameraPopup::on_camera_source_changed, this); - - if (!wxGetApp().app_config->get("camera", "custom_source").empty()) { - m_custom_camera_input->GetTextCtrl()->SetValue(wxGetApp().app_config->get("camera", "custom_source")); - set_custom_cam_button_state(wxGetApp().app_config->get("camera", "enable_custom_source") == "true"); - } - - top_sizer->Add(m_custom_camera_hint, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5)); - top_sizer->Add(0, 0, wxALL, 0); - top_sizer->Add(m_custom_camera_input, 2, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxALL, FromDIP(5)); - top_sizer->Add(m_custom_camera_input_confirm, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5)); main_sizer->Add(top_sizer, 0, wxALL, FromDIP(10)); auto url = wxString(L"https://www.orcaslicer.com/wiki/"); // Orca: neutral wiki link (vendor URL removed) @@ -184,37 +155,6 @@ void CameraPopup::sdcard_absent_hint() GetEventHandler()->ProcessEvent(evt); } -void CameraPopup::on_camera_source_changed(wxCommandEvent &event) -{ - if (m_obj && !m_custom_camera_input->GetTextCtrl()->IsEmpty()) { - handle_camera_source_change(); - } -} - -void CameraPopup::handle_camera_source_change() -{ - m_custom_camera_enabled = !m_custom_camera_enabled; - - set_custom_cam_button_state(m_custom_camera_enabled); - - wxGetApp().app_config->set("camera", "custom_source", m_custom_camera_input->GetTextCtrl()->GetValue().ToStdString()); - wxGetApp().app_config->set("camera", "enable_custom_source", m_custom_camera_enabled); - - wxCommandEvent evt(EVT_CAM_SOURCE_CHANGE); - evt.SetEventObject(this); - GetEventHandler()->ProcessEvent(evt); -} - -void CameraPopup::set_custom_cam_button_state(bool state) -{ - m_custom_camera_enabled = state; - auto stateColour = state ? wxColour(170, 0, 0) : wxColour(38, 166, 154); - auto stateText = state ? "Disable" : "Enable"; - m_custom_camera_input_confirm->SetBackgroundColor(stateColour); - m_custom_camera_input_confirm->SetBorderColor(stateColour); - m_custom_camera_input_confirm->SetLabel(_L(stateText)); -} - void CameraPopup::on_switch_recording(wxCommandEvent& event) { if (!m_obj) return; diff --git a/src/slic3r/GUI/CameraPopup.hpp b/src/slic3r/GUI/CameraPopup.hpp index f45ebb6e42..c9b1540d3b 100644 --- a/src/slic3r/GUI/CameraPopup.hpp +++ b/src/slic3r/GUI/CameraPopup.hpp @@ -15,14 +15,12 @@ #include "Widgets/SwitchButton.hpp" #include "Widgets/RadioBox.hpp" #include "Widgets/PopupWindow.hpp" -#include "Widgets/TextInput.hpp" namespace Slic3r { namespace GUI { wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); -wxDECLARE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent); class CameraPopup : public PopupWindow { @@ -53,9 +51,6 @@ protected: void on_switch_recording(wxCommandEvent& event); void on_set_resolution(); void sdcard_absent_hint(); - void on_camera_source_changed(wxCommandEvent& event); - void handle_camera_source_change(); - void set_custom_cam_button_state(bool state); wxWindow * create_item_radiobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left); void select_curr_radiobox(int btn_idx); @@ -76,10 +71,10 @@ private: wxStaticText* m_text_liveview_retry; SwitchButton* m_switch_liveview_retry; #endif //BBL_RELEASE_TO_PUBLIC - wxStaticText* m_custom_camera_hint; - TextInput* m_custom_camera_input; - Button* m_custom_camera_input_confirm; - bool m_custom_camera_enabled{ false }; + // wxStaticText* m_custom_camera_hint; + // TextInput* m_custom_camera_input; + // Button* m_custom_camera_input_confirm; + // bool m_custom_camera_enabled{ false }; wxStaticText* m_text_resolution; wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM]; wxScrolledWindow *m_panel; diff --git a/src/slic3r/GUI/ConnectPrinter.cpp b/src/slic3r/GUI/ConnectPrinter.cpp index 3e78e7fe5c..8e3d58b413 100644 --- a/src/slic3r/GUI/ConnectPrinter.cpp +++ b/src/slic3r/GUI/ConnectPrinter.cpp @@ -35,7 +35,9 @@ ConnectPrinterDialog::ConnectPrinterDialog(wxWindow *parent, wxWindowID id, cons sizer_connect = new wxBoxSizer(wxHORIZONTAL); m_textCtrl_code = new TextInput(this, wxEmptyString); - m_textCtrl_code->GetTextCtrl()->SetMaxLength(10); + // OrcaSonar uses a 12-character base32 access code. Keep this field long + // enough for it while retaining the existing validation for LAN codes. + m_textCtrl_code->GetTextCtrl()->SetMaxLength(12); m_textCtrl_code->SetFont(Label::Body_14); m_textCtrl_code->SetCornerRadius(FromDIP(5)); m_textCtrl_code->SetSize(wxSize(FromDIP(330), FromDIP(40))); diff --git a/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp b/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp index 25008cad29..159fca6285 100644 --- a/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp +++ b/src/slic3r/GUI/DeviceCore/DevConfigUtil.cpp @@ -1,5 +1,7 @@ #include "DevConfigUtil.h" +#include "slic3r/GUI/DeviceManager.hpp" + #include #include #include "../I18N.hpp" @@ -41,6 +43,19 @@ static void _toolhead_translation_markers() std::string DevPrinterConfigUtil::m_resource_file_path = ""; +bool DevPrinterConfigUtil::is_printer_model_compatible(const std::string& source_model, MachineObject& machine) +{ + const std::string& target_model = machine.printer_type; + if (is_optional_printer_model_id(source_model) || is_optional_printer_model_id(target_model)) + return true; + + if (source_model == target_model) + return true; + + const auto compatible_machine = machine.get_compatible_machine(); + return std::find(compatible_machine.begin(), compatible_machine.end(), source_model) != compatible_machine.end(); +} + std::map DevPrinterConfigUtil::get_all_model_id_with_name() { @@ -405,4 +420,4 @@ std::string DevPrinterConfigUtil::get_toolhead_display_name( return result; } -}; \ No newline at end of file +}; diff --git a/src/slic3r/GUI/DeviceCore/DevConfigUtil.h b/src/slic3r/GUI/DeviceCore/DevConfigUtil.h index 2b4af44061..e42f2806d1 100644 --- a/src/slic3r/GUI/DeviceCore/DevConfigUtil.h +++ b/src/slic3r/GUI/DeviceCore/DevConfigUtil.h @@ -25,6 +25,8 @@ namespace Slic3r { +class MachineObject; + /// Toolhead component type (extruder / nozzle / hotend) enum class ToolHeadComponent { Extruder, @@ -59,6 +61,10 @@ public: /*printer*/ // info static std::map get_all_model_id_with_name(); + // A printer agent may not know the physical model. Keep that case optional so + // model compatibility checks do not turn missing identity into a hard error. + static bool is_printer_model_compatible(const std::string& source_model, MachineObject& machine); + static bool is_optional_printer_model_id(const std::string& model_id) { return model_id.empty(); } static std::string get_printer_type(const std::string& type_str) { return get_value_from_config(type_str, "printer_type"); } static std::string get_printer_display_name(const std::string& type_str) { return get_value_from_config(type_str, "display_name"); } static std::string get_printer_series_str(std::string type_str) { return get_value_from_config(type_str, "printer_series"); } @@ -227,4 +233,4 @@ static std::string _parse_printer_type(const std::string &type_str) return type_str; } -};// namespace Slic3r \ No newline at end of file +};// namespace Slic3r diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 881aa75d20..2cce41db9d 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -762,9 +762,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS { curr_tray->remain = -1; } - if (tray_it->contains("tray_slot_placeholder")) { - curr_tray->is_slot_placeholder = true; - } + // The tray objects are reused across status updates. Reset this + // state when a previously empty slot receives a filament again. + curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder"); int ams_id_int = 0; int tray_id_int = 0; try @@ -999,4 +999,4 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob } } -} \ No newline at end of file +} diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index c03f0cfe59..5a0e4d768f 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -1,8 +1,10 @@ +#include "DevManager.h" #include #include -#include "DevManager.h" +#include +#include "CloudProvider.hpp" #include "DevUtil.h" // TODO: remove this include @@ -15,6 +17,8 @@ #include "libslic3r/Time.hpp" +#include "IPrinterAgent.hpp" + using namespace nlohmann; namespace { @@ -46,10 +50,12 @@ namespace { namespace Slic3r { - DeviceManager::DeviceManager(NetworkAgent* agent) + DeviceManager::DeviceManager(NetworkAgent* agent, bool enable_refresher, AppConfig* app_config) { - m_agent = agent; - m_refresher = new DeviceManagerRefresher(this); + m_agent = agent; + m_app_config = app_config; + if (enable_refresher) + m_refresher = new DeviceManagerRefresher(this); DevPrinterConfigUtil::InitFilePath(resources_dir()); @@ -60,9 +66,14 @@ namespace Slic3r } } + AppConfig* DeviceManager::get_app_config() const + { + return m_app_config ? m_app_config : GUI::wxGetApp().app_config; + } + void DeviceManager::load_local_machines_from_config() { - AppConfig* config = GUI::wxGetApp().app_config; + AppConfig* config = get_app_config(); if (!config) return; const auto local_machines = config->get_local_machines(); @@ -88,9 +99,8 @@ namespace Slic3r } } - void DeviceManager::update_local_machine(const MachineObject& m) + void DeviceManager::update_local_machine(const MachineObject& m, AppConfig* config) { - AppConfig* config = GUI::wxGetApp().app_config; if (config) { if (m.is_lan_mode_printer()) { if (m.has_access_right()) { @@ -111,7 +121,8 @@ namespace Slic3r DeviceManager::~DeviceManager() { - delete m_refresher; + if (m_refresher) + delete m_refresher; for (auto it = localMachineList.begin(); it != localMachineList.end(); it++) { @@ -171,14 +182,22 @@ namespace Slic3r return printer_agent ? printer_agent->get_agent_info().id : ""; } + std::string DeviceManager::get_current_cloud_provider() const + { + const std::string agent_id = get_current_printer_agent_id(); + if (!agent_id.empty()) + return agent_id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER; + return GUI::wxGetApp().get_printer_cloud_provider(); + } + void DeviceManager::EnableMultiMachine(bool enable) { m_agent->enable_multi_machine(enable); m_enable_mutil_machine = enable; } - void DeviceManager::start_refresher() { m_refresher->Start(); } - void DeviceManager::stop_refresher() { m_refresher->Stop(); } + void DeviceManager::start_refresher() { if (m_refresher) m_refresher->Start(); } + void DeviceManager::stop_refresher() { if (m_refresher) m_refresher->Stop(); } void DeviceManager::keep_alive() @@ -265,6 +284,10 @@ namespace Slic3r /* update userMachineList info */ auto it = userMachineList.find(dev_id); if (it != userMachineList.end()) { + // A reused entry may have been created while another printer agent was active. + // The response was obtained through the current agent, so move ownership with + // the entry; otherwise agent-scoped lists hide it after a preset switch. + it->second->printer_agent_id = get_current_printer_agent_id(); if (it->second->get_dev_ip() != dev_ip || it->second->bind_state != bind_state || it->second->bind_sec_link != sec_link || @@ -291,10 +314,15 @@ namespace Slic3r /* update localMachineList */ it = localMachineList.find(dev_id); + AppConfig* config = get_app_config(); + if (it != localMachineList.end()) { // update properties /* ip changed */ obj = it->second; + // A reused LAN entry may have been discovered while another printer agent was + // active. The current discovery message establishes ownership for this agent. + obj->printer_agent_id = get_current_printer_agent_id(); if (obj->get_dev_ip().compare(dev_ip) != 0) { if ( connection_name.empty() ) { @@ -384,7 +412,6 @@ namespace Slic3r obj->m_is_online = true; //load access code - AppConfig* config = Slic3r::GUI::wxGetApp().app_config; if (config) { obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false); } @@ -407,7 +434,7 @@ namespace Slic3r fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx); } } - update_local_machine(*obj); + update_local_machine(*obj, config); } catch (...) { ; @@ -422,6 +449,9 @@ namespace Slic3r auto it = localMachineList.find(machine.dev_id); if (it != localMachineList.end()) { obj = it->second; + // insert_local_device is called by the active agent, so a reused entry must follow + // that agent as well; otherwise the agent-scoped printer list hides it. + obj->printer_agent_id = get_current_printer_agent_id(); } else { obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip); obj->printer_agent_id = get_current_printer_agent_id(); @@ -439,11 +469,16 @@ namespace Slic3r obj->last_alive = Slic3r::Utils::get_current_time_utc(); obj->set_access_code(access_code, false); - update_local_machine(*obj); + update_local_machine(*obj, get_app_config()); return obj; } + void DeviceManager::update_local_machine(const MachineObject& m) + { + update_local_machine(m, GUI::wxGetApp().app_config); + } + int DeviceManager::query_bind_status(std::string& msg, const std::string& provider) { if (!m_agent) @@ -514,7 +549,7 @@ namespace Slic3r MachineObject* DeviceManager::get_my_machine(std::string dev_id) { - auto list = get_my_machine_list(); + auto list = get_my_machine_list(get_current_printer_agent_id()); auto it = list.find(dev_id); if (it != list.end()) { @@ -551,25 +586,15 @@ namespace Slic3r OnSelectedMachineChanged(previous_selected_machine, selected_machine); } - void DeviceManager::clear_other_devices(const std::string& target_agent_id) + void DeviceManager::clear_other_devices() { - // why: on agent swap, keep "My Devices" but drop the transient "Other Devices" - // Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own. - // - // Also drop "My Devices" stamped by a different agent than the one we're swapping to - // (target_agent_id, passed by the caller since the live agent hasn't been repointed yet - // at this point): otherwise a device first discovered under agent A survives every swap - // with a stale printer_agent_id, stays hidden from every agent's filtered list, and only - // gets re-tagged if something happens to delete and re-create it (e.g. account logout). - // Dropping it here instead lets the new agent's start_discovery re-insert and re-stamp it - // like any other fresh device. - const auto my = get_my_machine_list(); + // Device entries are now scoped by printer_agent_id when they are presented. Keep + // agent-owned discoveries across a switch so agents without automatic discovery (and + // plugins whose devices have not received an access code yet) do not lose their list. + // Entries without an owner are legacy/unscoped and cannot safely be shown. for (auto it = localMachineList.begin(); it != localMachineList.end();) { - const bool is_my_device = my.find(it->first) != my.end(); - const bool agent_mismatch = !target_agent_id.empty() && it->second && - it->second->printer_agent_id != target_agent_id; - if (!is_my_device || agent_mismatch) + if (!it->second || it->second->printer_agent_id.empty()) { delete it->second; it = localMachineList.erase(it); @@ -585,8 +610,22 @@ namespace Slic3r { BOOST_LOG_TRIVIAL(info) << "set_selected_machine=" << dev_id << " cur_selected=" << selected_machine; - auto my_machine_list = get_my_machine_list(); + auto my_machine_list = get_my_machine_list(get_current_printer_agent_id()); auto it = my_machine_list.find(dev_id); + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: set_selected_machine lookup dev_id=" << dev_id + << " found=" << (it != my_machine_list.end()) + << " my_machine_count=" << my_machine_list.size() + << " current_agent=" << get_current_printer_agent_id() + << " provider=" << GUI::wxGetApp().get_printer_cloud_provider(); + if (it != my_machine_list.end() && it->second) { + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: target machine dev_id=" << it->second->get_dev_id() + << " 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()) { + BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: target machine was not found in the current agent's machine list"; + return false; + } // disconnect last if dev_id difference from previous one auto last_selected = my_machine_list.find(selected_machine); @@ -597,7 +636,9 @@ namespace Slic3r m_agent->disconnect_printer(); } else if (last_selected->second->connection_type() == "cloud") { - m_agent->set_user_selected_machine(""); + const int result = m_agent->set_user_selected_machine(""); + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: cleared previous cloud selection dev_id=" + << selected_machine << " result=" << result; } } @@ -632,11 +673,7 @@ namespace Slic3r m_agent->disconnect_printer(); it->second->reset(); -#if !BBL_RELEASE_TO_PUBLIC - it->second->connect(Slic3r::GUI::wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false); -#else - it->second->connect(it->second->local_use_ssl); -#endif + it->second->connect(); it->second->set_lan_mode_connection_state(true); } } @@ -649,18 +686,16 @@ namespace Slic3r { // diff dev_id, cloud => set_user_selected_machine(new) BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new cloud machine, dev_id =" << dev_id; - m_agent->set_user_selected_machine(dev_id); + const int result = m_agent->set_user_selected_machine(dev_id); + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: set new cloud selection dev_id=" + << dev_id << " result=" << result; it->second->reset(); } else { BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new lan machine, dev_id =" << dev_id; it->second->reset(); -#if !BBL_RELEASE_TO_PUBLIC - it->second->connect(Slic3r::GUI::wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false); -#else - it->second->connect(it->second->local_use_ssl); -#endif + it->second->connect(); it->second->set_lan_mode_connection_state(true); } } @@ -677,6 +712,8 @@ namespace Slic3r selected_machine = dev_id; record_user_last_machine(selected_machine); + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: DeviceManager selection complete selected_machine=" + << selected_machine; return true; } @@ -707,7 +744,9 @@ namespace Slic3r dev_list.push_back(it->first); BOOST_LOG_TRIVIAL(trace) << "add_user_subscribe: " << it->first; } - m_agent->add_subscribe(dev_list); + const int result = m_agent->add_subscribe(dev_list); + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: add_user_subscribe count=" << dev_list.size() + << " result=" << result; } @@ -720,7 +759,9 @@ namespace Slic3r dev_list.push_back(it->first); BOOST_LOG_TRIVIAL(trace) << "del_user_subscribe: " << it->first; } - m_agent->del_subscribe(dev_list); + const int result = m_agent->del_subscribe(dev_list); + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: del_user_subscribe count=" << dev_list.size() + << " result=" << result; } void DeviceManager::subscribe_device_list(std::vector dev_list) @@ -839,7 +880,24 @@ namespace Slic3r try { json j = json::parse(body); - const std::string provider = GUI::wxGetApp().get_printer_cloud_provider(); + + const bool has_request_context = j.contains("provider") && j.contains("agent_id") && j.contains("generation"); + const std::string current_provider = get_current_cloud_provider(); + const std::string provider = j.contains("provider") ? j["provider"].get() + : current_provider; + const std::string agent_id = j.contains("agent_id") ? j["agent_id"].get() + : get_current_printer_agent_id(); + const std::uint64_t generation = j.value("generation", std::uint64_t(0)); + + if (has_request_context && + (provider != current_provider || + agent_id != get_current_printer_agent_id() || + generation != (m_agent ? m_agent->get_user_machine_list_generation() : 0))) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ignoring stale response provider=" + << provider << " agent_id=" << agent_id + << " generation=" << generation; + return; + } #if !BBL_RELEASE_TO_PUBLIC BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << j; @@ -862,11 +920,14 @@ namespace Slic3r /* update field */ obj = iter->second; obj->set_dev_id(dev_id); + // A device can be rediscovered by a different agent after a preset + // switch while retaining the same MachineObject instance. + obj->printer_agent_id = agent_id; } else { obj = new MachineObject(this, m_agent, "", "", ""); - obj->printer_agent_id = get_current_printer_agent_id(); + obj->printer_agent_id = agent_id; if (m_agent) { obj->set_bind_status(m_agent->get_user_name(provider)); @@ -874,13 +935,20 @@ namespace Slic3r if (obj->get_dev_ip().empty()) { - obj->get_dev_ip() = Slic3r::GUI::wxGetApp().app_config->get("ip_address", dev_id); + if (AppConfig* config = get_app_config()) + obj->get_dev_ip() = config->get("ip_address", dev_id); } userMachineList.insert(std::make_pair(dev_id, obj)); } if (!obj) continue; + // Orca cloud printers are only ever delivered through this REST + // account list; tag them so DeviceManager's cloud/lan branches + // (subscribe + deselect in set_selected_machine) treat them right. + if (provider == ORCA_CLOUD_PROVIDER) + obj->dev_connection_type = "cloud"; + if (!elem["dev_id"].is_null()) obj->set_dev_id(elem["dev_id"].get()); if (!elem["dev_name"].is_null()) @@ -912,6 +980,12 @@ namespace Slic3r acc_code.erase(std::remove(acc_code.begin(), acc_code.end(), '\n'), acc_code.end()); obj->set_access_code(acc_code); } + + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: parsed cloud machine dev_id=" << dev_id + << " name=" << obj->get_dev_name() + << " agent_id=" << obj->printer_agent_id + << " connection_type=" << obj->connection_type() + << " online=" << obj->m_is_online; } //remove MachineObject from userMachineList @@ -927,6 +1001,9 @@ namespace Slic3r iterat++; } } + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: parse_user_print_info complete provider=" << provider + << " parsed_count=" << new_list.size() + << " stored_count=" << userMachineList.size(); } } catch (std::exception& e) @@ -943,25 +1020,29 @@ namespace Slic3r unsigned int http_code; std::string body; int result = m_agent->get_user_print_info(&http_code, &body, provider); + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: get_user_print_info provider=" << provider + << " result=" << result << " http_code=" << http_code + << " body_bytes=" << body.size(); if (result == 0) { // parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map. // on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info. + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: queueing parse_user_print_info on UI thread"; Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); }); } } void DeviceManager::record_user_last_machine(const std::string& dev_id) { - if (Slic3r::GUI::wxGetApp().app_config) { - Slic3r::GUI::wxGetApp().app_config->set("user_last_selected_machine", dev_id); + if (AppConfig* config = get_app_config()) { + config->set("user_last_selected_machine", dev_id); } } std::string DeviceManager::get_user_last_machine() const { - if (Slic3r::GUI::wxGetApp().app_config) { - const auto& user_last_machine = Slic3r::GUI::wxGetApp().app_config->get("user_last_selected_machine"); + if (AppConfig* config = get_app_config()) { + const auto& user_last_machine = config->get("user_last_selected_machine"); if (!user_last_machine.empty()) { return user_last_machine; } else if (m_agent) { diff --git a/src/slic3r/GUI/DeviceCore/DevManager.h b/src/slic3r/GUI/DeviceCore/DevManager.h index 1f48baba98..1ba4631881 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.h +++ b/src/slic3r/GUI/DeviceCore/DevManager.h @@ -12,6 +12,7 @@ namespace Slic3r struct BBLocalMachine; class MachineObject; class NetworkAgent; +class AppConfig; namespace GUI { class GUI_App; @@ -24,6 +25,7 @@ class DeviceManager friend class DeviceManagerRefresher; private: NetworkAgent* m_agent{ nullptr }; + AppConfig* m_app_config{ nullptr }; DeviceManagerRefresher* m_refresher{ nullptr }; bool m_enable_mutil_machine = false; @@ -35,11 +37,13 @@ private: std::map userMachineList; /* dev_id -> MachineObject* cloudMachine of User */ public: - DeviceManager(NetworkAgent* agent = nullptr); + DeviceManager(NetworkAgent* agent = nullptr, bool enable_refresher = true, + AppConfig* app_config = nullptr); ~DeviceManager(); public: NetworkAgent* get_agent() const { return m_agent; } + AppConfig* get_app_config() const; void set_agent(NetworkAgent* agent); void start_refresher(); @@ -74,10 +78,9 @@ public: void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); } void clean_user_info(bool keep_local_selection = false); - // target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check, - // just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the - // live one - this runs before the live agent is repointed. - void clear_other_devices(const std::string& target_agent_id = ""); + // Retain agent-owned LAN discoveries across a switch; the active-agent list filter keeps + // entries from other agents hidden while allowing them to reappear when switched back. + void clear_other_devices(); void load_last_machine(); void update_user_machine_list_info(const std::string& provider); @@ -122,6 +125,7 @@ private: void keep_alive(); void check_pushing(); + std::string get_current_cloud_provider() const; void OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state); void OnSelectedMachineChanged(const std::string& pre_dev_id, const std::string& new_dev_id); @@ -134,14 +138,15 @@ public: std::string connection_type, std::string bind_state, std::string version, std::string access_code); static void update_local_machine(const MachineObject& m); + static void update_local_machine(const MachineObject& m, AppConfig* config); }; class DeviceManagerRefresher : public wxObject { - wxTimer* m_timer{ nullptr }; - int m_timer_interval_msec = 5000; + wxTimer* m_timer{nullptr}; + int m_timer_interval_msec = 5000; - DeviceManager* m_manager{ nullptr }; + DeviceManager* m_manager{nullptr}; public: DeviceManagerRefresher(DeviceManager* manger); @@ -154,4 +159,4 @@ public: protected: virtual void on_timer(wxTimerEvent& event); }; -}; \ No newline at end of file +}; diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 013e411574..7d9abdee12 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -4,6 +4,7 @@ #include "I18N.hpp" #include "libslic3r/Time.hpp" #include "libslic3r/Thread.hpp" +#include "slic3r/Utils/Http.hpp" #include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/plugin/PluginManager.hpp" #include "slic3r/Utils/NetworkAgentFactory.hpp" @@ -17,6 +18,7 @@ #include "ReleaseNote.hpp" #include #include +#include #include #include #include @@ -54,6 +56,7 @@ #include "DeviceCore/DevStatus.h" #include "DeviceCore/DevUpgrade.h" +#include "IPrinterAgent.hpp" #define CALI_DEBUG #define MINUTE_30 1800000 //ms @@ -380,8 +383,22 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str) wxString MachineObject::get_printer_type_display_str() const { std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); + + // Bambu printers use m_resource_file_path + "/printers/" + type_str + ".json", which is a semantic that only works for their profiles. + // For any other profile, we can simply consult preset bundle if the model_id exists. + if (display_name.empty()) { + for (const auto& [vendor_id, vendor] : GUI::wxGetApp().preset_bundle->vendors) { + for (const auto& model : vendor.models) { + if (printer_type == model.model_id) + display_name = model.name; + } + } + } + if (!display_name.empty()) return display_name; + else if (printer_type == "orcasonar") + return "OrcaSonar Printer"; else return _L("Unknown"); } @@ -468,7 +485,7 @@ void MachineObject::set_access_code(std::string code, bool only_refresh) { this->access_code = code; if (only_refresh) { - AppConfig* config = GUI::wxGetApp().app_config; + AppConfig* config = m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config; if (config) { if (is_lan_mode_printer()) { // why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and @@ -481,7 +498,7 @@ void MachineObject::set_access_code(std::string code, bool only_refresh) // fresh from the cloud API's current response, so there's no cross-agent leakage // risk to guard against there. if (!code.empty()) { - DeviceManager::update_local_machine(*this); + DeviceManager::update_local_machine(*this, config); } else { // Only patch an existing record's code - don't persist a brand-new // never-bound entry just because set_access_code("") was called on it. @@ -1348,7 +1365,6 @@ int MachineObject::command_get_access_code() { return this->publish_json(j); } - int MachineObject::command_request_push_all(bool request_now) { auto curr_time = std::chrono::system_clock::now(); @@ -1480,26 +1496,20 @@ int MachineObject::command_upgrade_module(std::string url, std::string module_ty int MachineObject::command_xyz_abs() { - return this->publish_gcode("G90 \n"); + if (!m_agent) return -1; + return command_with_dialog(m_agent->command_xyz_abs(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer())); } int MachineObject::command_auto_leveling() { - return this->publish_gcode("G29 \n"); + if (!m_agent) return -1; + return command_with_dialog(m_agent->command_auto_leveling(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer())); } int MachineObject::command_go_home() { - if (m_support_mqtt_homing) - { - json j; - j["print"]["command"] = "back_to_center"; - j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); - return this->publish_json(j); - } - - // gcode command - return this->is_in_printing() ? this->publish_gcode("G28 X\n") : this->publish_gcode("G28 \n"); + if (!m_agent) return -1; + 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())); } int MachineObject::command_task_partskip(std::vector part_ids) @@ -1621,23 +1631,14 @@ int MachineObject::command_stop_buzzer() int MachineObject::command_set_bed(int temp) { - if (m_support_mqtt_bet_ctrl) - { - json j; - j["print"]["command"] = "set_bed_temp"; - j["print"]["temp"] = temp; - j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); - return this->publish_json(j); - } - - std::string gcode_str = (boost::format("M140 S%1%\n") % temp).str(); - return this->publish_gcode(gcode_str); + if (!m_agent) return -1; + 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())); } int MachineObject::command_set_nozzle(int temp) { - std::string gcode_str = (boost::format("M104 S%1%\n") % temp).str(); - return this->publish_gcode(gcode_str); + if (!m_agent) return -1; + return command_with_dialog(m_agent->command_set_nozzle(get_dev_id(), temp, MachineObject::m_sequence_id++, is_lan_mode_printer())); } int MachineObject::command_set_nozzle_new(int nozzle_id, int temp) @@ -1742,9 +1743,8 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read int MachineObject::command_ams_calibrate(int ams_id) { - std::string gcode_cmd = (boost::format("M620 C%1% \n") % ams_id).str(); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; - return this->publish_gcode(gcode_cmd); + if (!m_agent) return -1; + return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer())); } 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) @@ -1780,29 +1780,23 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s return this->publish_json(j); } -int MachineObject::command_ams_refresh_rfid(std::string tray_id) +int MachineObject::command_ams_refresh_rfid(int ams_id, int slot_id) { - std::string gcode_cmd = (boost::format("M620 R%1% \n") % tray_id).str(); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; - return this->publish_gcode(gcode_cmd); + if (!m_agent) return -1; + return command_with_dialog(m_agent->command_ams_refresh_rfid(get_dev_id(), ams_id, slot_id, MachineObject::m_sequence_id++, is_lan_mode_printer())); } -int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id) +int MachineObject::command_start_camera() { - json j; - j["print"]["command"] = "ams_get_rfid"; - j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); - j["print"]["ams_id"] = ams_id; - j["print"]["slot_id"] = slot_id; - return this->publish_json(j); + if (!m_agent) return -1; + return m_agent->command_start_camera(get_dev_id()); } int MachineObject::command_ams_select_tray(std::string tray_id) { - std::string gcode_cmd = (boost::format("M620 P%1% \n") % tray_id).str(); - BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; - return this->publish_gcode(gcode_cmd); + if (!m_agent) return -1; + return command_with_dialog(m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer())); } int MachineObject::command_ams_control(std::string action) @@ -1961,47 +1955,10 @@ 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) { - if (m_support_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.compare("Y") == 0 || axis.compare("Z") == 0)) { - dir = -dir; - } - - json j; - j["print"]["command"] = "xyz_ctrl"; - j["print"]["axis"] = axis; - j["print"]["dir"] = dir; - j["print"]["mode"] = (std::abs(input_val) >= 10) ? 1 : 0; - j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); - return this->publish_json(j); - } - - double value = input_val; - if (!is_core_xy()) { - if ( axis.compare("Y") == 0 - || axis.compare("Z") == 0) { - value = -1.0 * input_val; - } - } - - char cmd[256]; - if (axis.compare("X") == 0 - || axis.compare("Y") == 0 - || axis.compare("Z") == 0) { - sprintf(cmd, "M211 S \nM211 X1 Y1 Z1\nM1002 push_ref_mode\nG91 \nG1 %s%0.1f F%d\nM1002 pop_ref_mode\nM211 R\n", axis.c_str(), value * unit, speed); - } - else if (axis.compare("E") == 0) { - sprintf(cmd, "M83 \nG0 %s%0.1f F%d\n", axis.c_str(), value * unit, speed); - } - else { - return -1; - } - - - return this->publish_gcode(cmd); + if (!m_agent) return -1; + 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())); } int MachineObject::command_extruder_control(int nozzle_id, double val) @@ -2626,7 +2583,12 @@ void MachineObject::reset() vt_slot.erase(vt_slot.begin() + 1); } } - subtask_ = nullptr; + // why: reset reuses MachineObject, so release its lazy subtask + // before dropping the pointer to prevent reconnect leaks. + if (subtask_) { + delete subtask_; + subtask_ = nullptr; + } has_extra_flow_type = false; m_partskip_ids.clear(); } @@ -2644,15 +2606,63 @@ void MachineObject::set_print_state(std::string status) } } -int MachineObject::connect(bool use_openssl) +// why: printer agents can report progress without BBL cloud task identity. +void MachineObject::update_print_progress(const json& value) +{ + if (value.is_string()) { + const std::string progress = value.get(); + int parsed_progress; + const auto result = std::from_chars(progress.data(), progress.data() + progress.size(), parsed_progress); + if (result.ec != std::errc{} || result.ptr != progress.data() + progress.size()) + return; + mc_print_percent = parsed_progress; + } else if (value.is_number_integer()) + mc_print_percent = value.get(); + else + return; + + if (BBLSubTask* curr_task = get_subtask()) + curr_task->task_progress = mc_print_percent; +} + +int MachineObject::connect() { if (get_dev_ip().empty()) return -1; - std::string username = "bblp"; + std::string username = m_agent ? m_agent->default_lan_username() : std::string(); std::string password = get_access_code(); + std::string port; + std::string input = get_dev_ip(); + + const bool use_ssl = input.rfind("https", 0) == 0; + + // This strips out the http/https prefix + std::string host = Http::get_host_from_url(input, &port); + std::string ca_file; + + if (GUI::wxGetApp().preset_bundle) { + const auto& config = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (port.empty()) + port = config.opt_string("printhost_port"); + ca_file = config.opt_string("printhost_cafile"); + } + + if (host.empty()) + host = get_dev_ip(); + + if (m_agent) { try { - return m_agent->connect_printer(get_dev_id(), get_dev_ip(), username, password, use_openssl); + PrinterConnectionParams params{ + get_dev_id(), + host, + port, + username, + password, + use_ssl, + ca_file + }; + return m_agent->connect_printer(params); } catch (...) { ; } @@ -2768,6 +2778,14 @@ int MachineObject::publish_json(const json& json_item, int qos, int flag) BOOST_LOG_TRIVIAL(error) << "publish_json: " << json_item.dump() << " code: " << rtn; } + // why: the agent is the only thing that knows what it can translate, so it reports + // not-supported in its return value and this - the single funnel every command_* builder + // passes through - is the one place that turns it into something the user sees. No list of + // unsupported commands is needed anywhere: an agent that has no case for a command says so. + if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) { + show_unsupported_dlg(rtn); + } + return rtn; } @@ -3086,6 +3104,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ } } catch (...) {} + try { + if (j.contains("info")) + parse_new_info2(j["info"]); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "parse_json: failed to parse OrcaSonar capability info"; + } + try { if (auto ptr = m_fila_system->GetAmsFirmwareSwitch().lock()) { ptr->ParseFirmwareSwitch(j); @@ -3338,10 +3363,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ print_type = jj["print_type"].get(); } if (jj.contains("mc_percent")) { - if (jj["mc_percent"].is_string()) - mc_print_percent = stoi(j["print"]["mc_percent"].get()); - else if (jj["mc_percent"].is_number_integer()) - mc_print_percent = j["print"]["mc_percent"].get(); + update_print_progress(jj["mc_percent"]); } if (jj.contains("mc_print_sub_stage")) { if (jj["mc_print_sub_stage"].is_number_integer()) @@ -3511,6 +3533,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ this->task_id_ = jj["task_id"].get(); } + if (jj.contains("thumbnail_url") && jj["thumbnail_url"].is_string()) + m_agent_thumbnail_url = jj["thumbnail_url"].get(); + if (jj.contains("job_attr")) { int jobAttr = jj["job_attr"].get(); jobState_ = get_flag_bits(jobAttr, 4, 4); @@ -3556,7 +3581,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ update_slice_info(jj["project_id"].get(), jj["profile_id"].get(), jj["subtask_id"].get(), plate_index); BBLSubTask* curr_task = get_subtask(); if (curr_task) { - curr_task->task_progress = mc_print_percent; curr_task->printing_status = print_status; curr_task->task_id = jj["subtask_id"].get(); } @@ -3859,6 +3883,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ has_ipcam = true; } else { has_ipcam = false; + webcam_stream_url.clear(); } } if (ipcam.contains("resolution")) { @@ -3893,6 +3918,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ liveview_local = local_rtsp_url.empty() ? LVL_None : local_rtsp_url == "disable" ? LVL_Disable : boost::algorithm::starts_with(local_rtsp_url, "rtsps") ? LVL_Rtsps : LVL_Rtsp; } + if (ipcam.contains("stream_url") && ipcam["stream_url"].is_string()) { + webcam_stream_url = ipcam["stream_url"].get(); + } if (ipcam.contains("tutk_server")) { tutk_state = ipcam["tutk_server"].get(); } @@ -4651,7 +4679,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ if (diff.count() > 10.0f) { BOOST_LOG_TRIVIAL(trace) << "parse_json timeout = " << diff.count(); } - DeviceManager::update_local_machine(*this); + DeviceManager::update_local_machine(*this, m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config); return 0; } @@ -5460,6 +5488,86 @@ void MachineObject::parse_new_info(json print) } } +void MachineObject::parse_new_info2(const json& info) +{ + if (!info.is_object() || info.value("command", "") != "get_capabilities") + return; + + const auto capabilities_it = info.find("capabilities"); + if (capabilities_it == info.end() || !capabilities_it->is_object()) + return; + const auto flags_it = capabilities_it->find("flags"); + if (flags_it == capabilities_it->end() || !flags_it->is_object()) + return; + + const json& flags = *flags_it; + BOOST_LOG_TRIVIAL(info) << "parse_new_info2: OrcaSonar capability flags=" << flags.dump(); + + auto parse_bool = [&flags](const char* name, bool& target) { + const auto it = flags.find(name); + if (it != flags.end() && it->is_boolean()) + target = it->get(); + }; + + parse_bool("support_send_to_sd", is_support_send_to_sdcard); + parse_bool("support_filament_backup", is_support_filament_backup); + parse_bool("support_update_remain", is_support_update_remain); + parse_bool("support_auto_recovery_step_loss", is_support_auto_recovery_step_loss); + parse_bool("support_ams_humidity", is_support_ams_humidity); + parse_bool("support_prompt_sound", is_support_prompt_sound); + parse_bool("support_filament_tangle_detect", is_support_filament_tangle_detect); + parse_bool("support_1080dpi", is_support_1080dpi); + parse_bool("support_cloud_print_only", is_support_cloud_print_only); + parse_bool("support_command_ams_switch", is_support_command_ams_switch); + parse_bool("support_mqtt_alive", is_support_mqtt_alive); + parse_bool("support_motor_noise_cali", is_support_motor_noise_cali); + parse_bool("support_timelapse", is_support_timelapse); + parse_bool("support_user_preset", is_support_user_preset); + parse_bool("support_refresh_nozzle", is_support_refresh_nozzle); + parse_bool("support_flow_calibration", is_support_flow_calibration); + parse_bool("support_build_plate_marker_detect", is_support_build_plate_marker_detect); + parse_bool("support_nozzle_blob_detect", is_support_nozzle_blob_detection); + + if (!m_manager->IsMultiMachineEnabled() && !is_support_agora) + parse_bool("support_tunnel_mqtt", is_support_tunnel_mqtt); + + const auto bed_leveling_it = flags.find("support_bed_leveling"); + if (bed_leveling_it != flags.end() && bed_leveling_it->is_number_integer()) + is_support_bed_leveling = bed_leveling_it->get(); + + auto copy_bool = [&flags](json& target, const char* name) { + const auto it = flags.find(name); + if (it != flags.end() && it->is_boolean()) + target[name] = *it; + }; + + // The capability manifest uses an object for this range, while the legacy + // DeviceCore parser consumes a boolean plus a two-element range array. + json device_config; + copy_bool(device_config, "support_chamber"); + copy_bool(device_config, "support_first_layer_inspect"); + copy_bool(device_config, "support_ai_monitoring"); + copy_bool(device_config, "support_lidar_calibration"); + const auto chamber_edit_it = flags.find("support_chamber_temp_edit"); + if (chamber_edit_it != flags.end() && chamber_edit_it->is_boolean()) { + device_config["support_chamber_temp_edit"] = *chamber_edit_it; + } else if (chamber_edit_it != flags.end() && chamber_edit_it->is_object()) { + const auto min_it = chamber_edit_it->find("min"); + const auto max_it = chamber_edit_it->find("max"); + if (min_it != chamber_edit_it->end() && max_it != chamber_edit_it->end() && min_it->is_number() && max_it->is_number()) { + device_config["support_chamber_temp_edit"] = true; + device_config["support_chamber_temp_edit_range"] = {*min_it, *max_it}; + } + } + + json fan_config; + copy_bool(fan_config, "support_aux_fan"); + copy_bool(fan_config, "support_chamber_fan"); + + m_config->ParseConfig(device_config); + m_fan->ParseV2_0(fan_config); +} + static bool is_hex_digit(char c) { return std::isxdigit(static_cast(c)) != 0; } @@ -5965,6 +6073,15 @@ 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) { diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index ca3b7c3c44..b69e2f8f5a 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -18,6 +18,7 @@ #include "boost/bimap/bimap.hpp" #include "libslic3r/calib.hpp" #include "libslic3r/Utils.hpp" +#include "slic3r/Utils/PrinterNetworkTypes.hpp" #include "DeviceCore/DevDefs.h" #include "DeviceCore/DevConfigUtil.h" @@ -100,7 +101,6 @@ struct DevPrintTaskRatingInfo; // given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware. bool is_stringing_prone_filament(const std::string& filament_id, float nozzle_diameter); - class MachineObject { private: @@ -307,7 +307,7 @@ public: bool ams_support_virtual_tray { true }; time_t ams_user_setting_start = 0; time_t ams_switch_filament_start = 0; - AmsStatusMain ams_status_main; + AmsStatusMain ams_status_main = AmsStatusMain::AMS_STATUS_MAIN_IDLE; int ams_status_sub; int ams_version = 0; @@ -547,30 +547,12 @@ public: bool xcam_first_layer_inspector { false }; time_t xcam_first_layer_hold_start = 0; std::string local_rtsp_url; + std::string webcam_stream_url; std::string tutk_state; - enum LiveviewLocal { - LVL_None, - LVL_Disable, - LVL_Local, - LVL_Rtsps, - LVL_Rtsp - } liveview_local{ LVL_None }; - enum LiveviewRemote { - LVR_None, - LVR_Tutk, - LVR_Agora, - LVR_TutkAgora - } liveview_remote{ LVR_None }; - enum FileLocal { - FL_None, - FL_Local - } file_local{ FL_None }; - enum FileRemote { - FR_None, - FR_Tutk, - FR_Agora, - FR_TutkAgora - } file_remote{ FR_None }; + LiveviewLocal liveview_local{ LiveviewLocal::LVL_None }; + LiveviewRemote liveview_remote{ LiveviewRemote::LVR_None}; + FileLocal file_local{ FileLocal::FL_None }; + FileRemote file_remote{ FileRemote::FR_None }; enum PlateMakerDectect : int { @@ -696,7 +678,7 @@ public: /* machine mqtt apis */ - int connect(bool use_openssl = true); + int connect(); int disconnect(); json_diff print_json; @@ -708,6 +690,8 @@ public: std::string subtask_id_; std::string job_id_; std::string last_subtask_id_; + // note: printer-agent-supplied thumbnail url, empty when the agent supplies none. + std::string m_agent_thumbnail_url; BBLSliceInfo* slice_info {nullptr}; boost::thread* get_slice_info_thread { nullptr }; boost::thread* get_model_task_thread { nullptr }; @@ -765,6 +749,7 @@ public: int command_set_printer_nozzle(std::string nozzle_type, float diameter); int command_set_printer_nozzle2(int id, std::string nozzle_type, float diameter); int command_get_access_code(); + int command_start_camera(); int command_ack_proceed(json& proceed); int command_purification_disable(); int command_dont_remind_next_time(json& mqtt_guard_json); @@ -811,8 +796,7 @@ public: int command_ams_calibrate(int ams_id); int 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); int command_ams_select_tray(std::string tray_id); - int command_ams_refresh_rfid(std::string tray_id); - int command_ams_refresh_rfid2(int ams_id, int slot_id); + int command_ams_refresh_rfid(int ams_id, int slot_id); int command_ams_control(std::string action); int command_ams_drying_stop(); int command_start_extrusion_cali(int tray_index, int nozzle_temp, int bed_temp, float max_volumetric_speed, std::string setting_id = ""); @@ -894,6 +878,7 @@ public: static bool is_in_printing_status(std::string status); void set_print_state(std::string status); + void update_print_progress(const json& value); bool is_connected(); bool is_connecting(); @@ -962,6 +947,7 @@ public: /*for parse new info*/ bool check_enable_np(const json& print) const; void parse_new_info(json print); + void parse_new_info2(const json& info); int get_flag_bits(std::string str, int start, int count = 1) const; uint32_t get_flag_bits_no_border(std::string str, int start_idx, int count = 1) const; int get_flag_bits(int num, int start, int count = 1, int base = 10) const; @@ -990,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; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 72849dd78e..27135a44d7 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -4,6 +4,7 @@ #include "libslic3r/Platform.hpp" #include "GUI_App.hpp" #include "Shortcuts.hpp" +#include "DeviceCore/DevConfigUtil.h" #include "BindDialog.hpp" #include "DeviceManager.hpp" #include "HMS.hpp" @@ -24,7 +25,6 @@ #include #include #include -#include #include #include @@ -41,9 +41,11 @@ #include #include #include +#include #include #include #include + #include #include #include @@ -88,7 +90,6 @@ #include "libslic3r/Thread.hpp" #include "libslic3r/miniz_extension.hpp" #include "libslic3r/Utils.hpp" -#include "libslic3r/Color.hpp" #include "slic3r/plugin/PluginManager.hpp" #include "slic3r/plugin/host/PluginHostUi.hpp" #include "slic3r/plugin/PythonInterpreter.hpp" @@ -108,19 +109,18 @@ #include "../Utils/PrintHost.hpp" #include "../Utils/Process.hpp" #include "../Utils/wxInspectorPlugins/Registration.hpp" -#include "../Utils/MacDarkMode.hpp" #include "../Utils/Http.hpp" #include "../Utils/InstanceID.hpp" #include "../Utils/UndoRedo.hpp" #include "slic3r/Config/Snapshot.hpp" #include "Preferences.hpp" #include "Tab.hpp" -#include "SysInfoDialog.hpp" #include "UpdateDialogs.hpp" #include "Mouse3DController.hpp" #include "RemovableDriveManager.hpp" #include "InstanceCheck.hpp" #ifdef __APPLE__ +#include "../Utils/MacDarkMode.hpp" #include "DeepLinkHandlerMac.h" #endif #include "NotificationManager.hpp" @@ -129,8 +129,6 @@ #include "PrintHostDialogs.hpp" #include "NetworkPluginDialog.hpp" #include "DesktopIntegrationDialog.hpp" -#include "SendSystemInfoDialog.hpp" -#include "ParamsDialog.hpp" #include "KBShortcutsDialog.hpp" #include "DownloadProgressDialog.hpp" #include "TroubleshootDialog.hpp" @@ -141,7 +139,6 @@ #include "Widgets/ProgressDialog.hpp" //BBS: DailyTip and UserGuide Dialog -#include "WebDownPluginDlg.hpp" #include "WebGuideDialog.hpp" #include "ReleaseNote.hpp" #include "PrivacyUpdateDialog.hpp" @@ -2419,34 +2416,23 @@ GUI_App::~GUI_App() bool GUI_App::is_blocking_printing(MachineObject *obj_) { - DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (!dev) return true; - std::string target_model; - if (obj_ == nullptr) { - obj_ = dev->get_selected_machine(); - if (obj_) { - target_model = obj_->printer_type; - } - } else { - target_model = obj_->printer_type; - } - - if (!obj_) - { - return false; - } - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - std::string source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); + const std::string source_model = preset_bundle + ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) + : std::string(); + return is_blocking_printing(obj_, source_model); +} - if (source_model != target_model) { - std::vector compatible_machine = obj_->get_compatible_machine(); - vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); - if (it == compatible_machine.end()) { - return true; - } - } - return false; +bool GUI_App::is_blocking_printing(MachineObject *obj_, const std::string& source_model) +{ + DeviceManager *dev = getDeviceManager(); + if (!dev) return true; + if (obj_ == nullptr) + obj_ = dev->get_selected_machine(); + if (!obj_) + return false; + + return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_); } // If formatted for github, plaintext with OpenGL extensions enclosed into
. @@ -4007,20 +3993,16 @@ void GUI_App::set_live_printer_agent(std::shared_ptr agent) m_agent->set_user_selected_machine(""); // note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer) dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS - // why: drop stale LAN discoveries; keep My Devices, but only those belonging to the - // agent we're about to swap to, so a device stamped by the outgoing agent doesn't - // linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh. - // agent is null when clearing the live agent entirely (e.g. plugin unload); there's no - // target to filter against then, so fall back to the original "keep all My Devices" - // behavior rather than guessing. - dev->clear_other_devices(agent ? agent->get_agent_info().id : std::string()); + // why: retain agent-owned LAN discoveries so agents without automatic discovery (for + // example the Moonraker-based Qidi/Snapmaker agents) can reuse them after a switch. + dev->clear_other_devices(); } m_agent->set_printer_agent(agent); sidebar().update_all_preset_comboboxes(); } -std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) +std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) const { if (!stored_id.empty()) return stored_id; @@ -4056,6 +4038,7 @@ void GUI_App::switch_printer_agent() std::string log_dir = data_dir(); std::string cloud_agent_id = agent_info.id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << agent_info.id; std::shared_ptr cloud_agent = m_agent->get_cloud_agent(cloud_agent_id); // Create new printer agent via registry @@ -4069,8 +4052,10 @@ void GUI_App::switch_printer_agent() return; } - // The factory caches agents per ID, so an identical pointer means the agent type is unchanged. - if (m_agent->get_printer_agent() == new_printer_agent) { + // Compare the registered IDs, not only the implementation pointer. Different registry IDs + // may intentionally be backed by the same implementation object (especially for plugins). + const auto current_printer_agent = m_agent->get_printer_agent(); + if (current_printer_agent && current_printer_agent->get_agent_info().id == effective_agent_id) { // Orca: the agent type is unchanged (e.g. switching between two Moonraker/Klipper // printer presets), so the selected machine and the agent's cached device_info still // point at the previously active printer preset. Re-select the machine when the new @@ -5053,14 +5038,16 @@ bool GUI_App::is_user_login(const std::string& provider/* = ORCA_CLOUD_PROVIDER* return false; } -const std::string& GUI_App::get_printer_cloud_provider() const +std::string GUI_App::get_printer_cloud_provider() const { - // Orca todo: this need to be revisted. currently it is mainly used for device manager and related clausses and only bambu machines use them. - // - return BBL_CLOUD_PROVIDER; + const std::string agent_id = resolve_printer_agent_id( + preset_bundle ? preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent") + : std::string()); + return agent_id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER; } + bool GUI_App::check_login(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/) { bool result = false; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index a8fa8432b1..da37c79242 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -24,9 +24,6 @@ #include #include -#include -#include - //#define BBL_HAS_FIRST_PAGE 1 #define STUDIO_INACTIVE_TIMEOUT 15*60*1000 #define LOG_FILES_MAX_NUM 30 @@ -373,6 +370,7 @@ public: EAppMode get_app_mode() const { return m_app_mode; } Slic3r::DeviceManager* getDeviceManager() { return m_device_manager; } bool is_blocking_printing(MachineObject *obj_ = nullptr); + bool is_blocking_printing(MachineObject *obj_, const std::string& source_model); Slic3r::TaskManager* getTaskManager() { return m_task_manager; } HMSQuery* get_hms_query() { return hms_query; } NetworkAgent* getAgent() { return m_agent; } @@ -380,7 +378,7 @@ public: // Reconcile the live printer agent with the stored preset selection. void switch_printer_agent(); - std::string resolve_printer_agent_id(const std::string& stored_id); + std::string resolve_printer_agent_id(const std::string& stored_id) const; // ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id // then, all resolve and canonical would just be ORCA<->"" std::string canonical_printer_agent_id(const std::string& picked_id); @@ -511,7 +509,7 @@ public: bool check_login(const std::string& provider = ORCA_CLOUD_PROVIDER); void get_login_info(const std::string& provider = ORCA_CLOUD_PROVIDER); bool is_user_login(const std::string& provider = ORCA_CLOUD_PROVIDER); - const std::string& get_printer_cloud_provider() const; + std::string get_printer_cloud_provider() const; void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER); void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER); diff --git a/src/slic3r/GUI/IMediaController.hpp b/src/slic3r/GUI/IMediaController.hpp new file mode 100644 index 0000000000..3b4cbf7d24 --- /dev/null +++ b/src/slic3r/GUI/IMediaController.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +namespace Slic3r { namespace GUI { + +class IMediaController +{ +public: + virtual ~IMediaController() = default; + + virtual void Load(wxURI url) = 0; + + virtual void Play() = 0; + + virtual void Stop() = 0; + + virtual wxMediaState GetState() { return wxMediaState{}; } + + virtual int GetLastError() const { return {}; }; + + virtual wxSize GetVideoSize() const { return {}; }; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index 76e45eb1d7..2dea8801b5 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -15,6 +15,7 @@ #include "slic3r/Utils/FileTransferUtils.hpp" #include "slic3r/Utils/BBLNetworkPlugin.hpp" +#include "NetworkAgent.hpp" namespace Slic3r { namespace GUI { @@ -216,7 +217,7 @@ void PrintJob::process(Ctl &ctl) params.dev_ip = m_dev_ip; params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; params.use_ssl_for_mqtt = m_local_use_ssl; - params.username = "bblp"; + params.username = m_agent->default_lan_username(); params.password = m_access_code; // check access code and ip address diff --git a/src/slic3r/GUI/Jobs/SendJob.cpp b/src/slic3r/GUI/Jobs/SendJob.cpp index 505475e4a9..a06144878c 100644 --- a/src/slic3r/GUI/Jobs/SendJob.cpp +++ b/src/slic3r/GUI/Jobs/SendJob.cpp @@ -127,7 +127,7 @@ void SendJob::process(Ctl &ctl) if (m_is_check_mode) { PrintParams verify_params; verify_params.dev_ip = m_dev_ip; - verify_params.username = "bblp"; + verify_params.username = agent->default_lan_username(); verify_params.password = m_access_code; verify_params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; verify_params.use_ssl_for_mqtt = m_local_use_ssl; @@ -219,7 +219,7 @@ void SendJob::process(Ctl &ctl) // local print access params.dev_ip = m_dev_ip; - params.username = "bblp"; + params.username = agent->default_lan_username(); params.password = m_access_code; params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; params.use_ssl_for_mqtt = m_local_use_ssl; diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 343d95d3ff..08da15d3e3 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -4377,7 +4377,7 @@ void MainFrame::load_printer_url() if (auto *device_manager = wxGetApp().getDeviceManager()) { auto *machine = device_manager->get_selected_machine(); if (!machine) { - auto machines = device_manager->get_my_machine_list(); + auto machines = device_manager->get_my_machine_list(device_manager->get_current_printer_agent_id()); if (machines.size() == 1) machine = machines.begin()->second; } diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index 557d859cf7..c854683368 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -1,4 +1,6 @@ #include "MediaPlayCtrl.h" +#include "WebMediaController.hpp" +#include "IPrinterAgent.hpp" #include "Widgets/Button.hpp" #include "Widgets/CheckBox.hpp" #include "Widgets/Label.hpp" @@ -7,6 +9,7 @@ #include "DeviceManager.hpp" #include "DeviceCore/DevConfigUtil.h" #include "slic3r/Utils/NetworkAgent.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "libslic3r/Thread.hpp" #include "libslic3r/AppConfig.hpp" #include "I18N.hpp" @@ -15,11 +18,15 @@ #include "slic3r/Utils/BBLNetworkPlugin.hpp" +#include + #include #include #include #include #include +#include +#include #undef pid_t #include #ifdef __WIN32__ @@ -48,6 +55,7 @@ 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); @@ -94,7 +102,10 @@ 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) { m_media_ctrl->Play(); }); + m_button_play->Bind(wxEVT_RIGHT_UP, [this](auto & e) { + if (m_active_media_controller) + m_active_media_controller->Play(); + }); // Orca: live-view FAQ link binding removed (vendor URL) Bind(wxEVT_RIGHT_UP, [this](auto & e) { @@ -139,9 +150,12 @@ MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const w MediaPlayCtrl::~MediaPlayCtrl() { + if (m_webrtc_ctrl) + m_webrtc_ctrl->Stop(); + m_media_ctrl->EndExternalStream(); { boost::unique_lock lock(m_mutex); - m_tasks.push_back(""); + m_tasks.push_back({"", nullptr}); m_cond.notify_all(); } while (!m_thread.try_join_for(boost::chrono::milliseconds(10))) { @@ -151,59 +165,144 @@ MediaPlayCtrl::~MediaPlayCtrl() BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << this; } +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(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( + [this](const wxImage& image, wxSize size) { m_media_ctrl->SetExternalFrame(image, size); }, + [this, token = std::weak_ptr(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 +{ + auto agent = wxGetApp().getAgent(); + return agent ? agent->get_camera_stream_mode() : CameraStreamMode::none; +} + void MediaPlayCtrl::SetMachineObject(MachineObject* obj) { - 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 = MachineObject::LVR_None; + const CameraStreamMode mode = current_mode(); + if (mode != m_last_mode) { + if (m_last_state != MEDIASTATE_IDLE) { + m_failed_code = 0; // a mode switch is not a stream failure - don't arm back-off + Stop(" "); } - } else { - m_camera_exists = false; - m_lan_mode = false; - m_lan_proto = MachineObject::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; + m_last_mode = mode; } - Enable(obj && obj->is_info_ready() && obj->m_push_count > 0); - if (machine == m_machine) { - if (m_last_state == MEDIASTATE_IDLE && IsEnabled()) - Play(); + 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) { + 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; + 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) + 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); 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 (!changed) + return; + + // 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(); } 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) @@ -254,13 +353,71 @@ 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) + if ((m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry) || !IsShownOnScreen() || m_last_state != MEDIASTATE_IDLE) return; - if (!IsShownOnScreen()) - return; - if (m_last_state != MEDIASTATE_IDLE) { + + 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) + return; + + auto *webrtc_ctrl = is_webrtc ? dynamic_cast(m_active_media_controller) : nullptr; + if (is_webrtc && (!webrtc_ctrl || !m_media_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, wxGetApp().get_printer_cloud_provider()) : 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(); + } + if (webrtc_ctrl) { + m_webrtc_epoch = webrtc_ctrl->epoch(); + } return; } + m_failed_code = 0; if (m_machine.empty()) { Stop(_L("Please confirm if the printer is connected.")); @@ -281,16 +438,15 @@ 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 > MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) { + 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 std::string url; - if (m_lan_proto == MachineObject::LVL_Local) + if (m_lan_proto == LiveviewLocal::LVL_Local) url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd; - else if (m_lan_proto == MachineObject::LVL_Rtsps) + else if (m_lan_proto == LiveviewLocal::LVL_Rtsps) url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps"; - else if (m_lan_proto == MachineObject::LVL_Rtsp) + else if (m_lan_proto == LiveviewLocal::LVL_Rtsp) url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp"; url += "&device=" + m_machine; url += "&net_ver=" + agent_version; @@ -312,8 +468,8 @@ void MediaPlayCtrl::Play() // !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_Disable (*) // !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_None (x) - if (m_lan_proto <= MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto)) { - Stop(m_lan_proto == MachineObject::LVL_None + if (m_lan_proto <= LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto)) { + Stop(m_lan_proto == LiveviewLocal::LVL_None ? _L("A problem occurred. Please update the printer firmware and try again.") : _L("LAN Only Liveview is off. Please turn on the liveview on printer screen.")); return; @@ -381,15 +537,78 @@ void MediaPlayCtrl::Play() void start_ping_test(); +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(); + m_button_play->SetIcon("media_play"); + m_last_state = MEDIASTATE_IDLE; + SetStatus(_L("Video Stopped."), false); +} + void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2) { + const bool webrtc_active = m_last_mode == CameraStreamMode::webrtc && + dynamic_cast(m_active_media_controller) != nullptr; + 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_media_ctrl->EndExternalStream(); + } + switch (m_last_mode) { + case CameraStreamMode::http: + case CameraStreamMode::https: + case CameraStreamMode::http_snapshot: { + 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(); + } 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({"", m_active_media_controller}); + m_cond.notify_all(); + } + m_button_play->SetIcon("media_play"); + m_last_state = MEDIASTATE_IDLE; + if (!msg.IsEmpty()) + SetStatus(msg); + else + SetStatus(_L("Video Stopped."), false); + // Keep retries bounded for an explicit or retry-driven playback attempt. + // m_failed_retry is cleared on success (onStateChanged) and on a deliberate + // machine switch (SetMachineObject); manual playback via TogglePlay resets it. + if (m_failed_code != 0) { + const bool auto_retry = wxGetApp().app_config->get("liveview", "auto_retry") != "false"; + ++m_failed_retry; + m_next_retry = auto_retry + ? wxDateTime::Now() + wxTimeSpan::Seconds(std::min(5 * m_failed_retry, 30)) + : wxDateTime::Now() + wxTimeSpan::Days(1); // "off": wait for a manual retry + } + } else if (!msg.IsEmpty()) { + SetStatus(msg, false); + } + return; + } + default: + break; + } + int last_state = m_last_state; if (m_last_state != MEDIASTATE_IDLE) { m_media_ctrl->InvalidateBestSize(); m_button_play->SetIcon("media_play"); boost::unique_lock lock(m_mutex); - m_tasks.push_back(""); + if (!webrtc_active) + m_tasks.push_back({"", m_active_media_controller}); m_cond.notify_all(); if (!msg.IsEmpty()) SetStatus(msg); @@ -454,15 +673,38 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2) m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(5 * m_failed_retry); } +void MediaPlayCtrl::on_webrtc_status(WebRtcMediaController::Status status) +{ + // Drop CallAfter-queued events from a superseded Play attempt. + if (status.epoch != m_webrtc_epoch) + return; + if (status.kind == WebRtcMediaController::Status::Connecting) { + m_last_state = MEDIASTATE_INITIALIZING; + SetStatus(_L("Initializing..."), false); + } else if (status.kind == WebRtcMediaController::Status::Playing) { + m_last_state = wxMEDIASTATE_PLAYING; + m_failed_code = 0; + m_failed_retry = 0; + SetStatus(_L("Playing..."), false); + } else if (status.kind == WebRtcMediaController::Status::Failed) { + m_failed_code = static_cast(status.code) + 1; + Stop(); + } + // Status::Stopped needs no action: a genuine failure arrives as Failed, and + // a stop we initiated is already handled by Stop() itself. +} + void MediaPlayCtrl::TogglePlay() { BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::TogglePlay"; if (m_last_state != MEDIASTATE_IDLE) { m_next_retry = wxDateTime(); + m_web_user_stopped = true; Stop(); } else { m_failed_retry = 0; m_user_triggered = true; + m_web_user_stopped = false; if (m_last_user_play + wxTimeSpan::Minutes(5) < wxDateTime::Now()) { m_last_failed_codes.clear(); m_last_user_play = wxDateTime::Now(); @@ -528,13 +770,13 @@ void MediaPlayCtrl::ToggleStream() wxGetApp().app_config->set("not_show_vcamera_stop_prev", "1"); if (res == wxID_CANCEL) return; } - if (m_lan_proto > MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) { + if (m_lan_proto > LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) { std::string url; - if (m_lan_proto == MachineObject::LVL_Local) + if (m_lan_proto == LiveviewLocal::LVL_Local) url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd; - else if (m_lan_proto == MachineObject::LVL_Rtsps) + else if (m_lan_proto == LiveviewLocal::LVL_Rtsps) url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps"; - else if (m_lan_proto == MachineObject::LVL_Rtsp) + else if (m_lan_proto == LiveviewLocal::LVL_Rtsp) url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp"; url += "&device=" + into_u8(m_machine); url += "&dev_ver=" + m_dev_ver; @@ -594,7 +836,9 @@ void MediaPlayCtrl::jump_to_play() void MediaPlayCtrl::onStateChanged(wxMediaEvent &event) { auto last_state = m_last_state; - auto state = m_media_ctrl->GetState(); + if (m_active_media_controller != m_media_ctrl) + return; + auto state = m_active_media_controller->GetState(); BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::onStateChanged: " << state << ", last_state: " << last_state; if ((int) state < 0) return; { @@ -606,14 +850,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_media_ctrl->GetLastError(); + m_failed_code = m_active_media_controller->GetLastError(); Stop(); return; } if (last_state == MEDIASTATE_LOADING && (state == wxMEDIASTATE_STOPPED || state == wxMEDIASTATE_PAUSED)) { - wxSize size = m_media_ctrl->GetVideoSize(); + wxSize size = m_active_media_controller->GetVideoSize(); BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::onStateChanged: size: " << size.x << "x" << size.y; - m_failed_code = m_media_ctrl->GetLastError(); + m_failed_code = m_active_media_controller->GetLastError(); if (size.GetWidth() >= 320) { m_last_state = state; m_failed_code = 0; @@ -623,7 +867,7 @@ void MediaPlayCtrl::onStateChanged(wxMediaEvent &event) m_failed_retry = 0; m_disable_lan = false; boost::unique_lock lock(m_mutex); - m_tasks.push_back(""); + m_tasks.push_back({"", m_active_media_controller}); m_cond.notify_all(); } else if (event.GetId()) { if (m_failed_code == 0) @@ -666,7 +910,8 @@ void MediaPlayCtrl::load() { m_last_state = MEDIASTATE_LOADING; SetStatus(_L("Loading...")); - if (wxGetApp().app_config->get("internal_developer_mode") == "true") { + const auto mode = current_mode(); + if (mode == CameraStreamMode::none && wxGetApp().app_config->get("internal_developer_mode") == "true") { std::string file_h264 = data_dir() + "/video.h264"; std::string file_info = data_dir() + "/video.info"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl dump video to " << file_h264; @@ -677,7 +922,7 @@ void MediaPlayCtrl::load() m_url = m_url + "&dump_info=" + boost::lexical_cast(dump_info_file); } boost::unique_lock lock(m_mutex); - m_tasks.push_back(m_url); + m_tasks.push_back({m_url, m_active_media_controller}); m_cond.notify_all(); } @@ -686,9 +931,8 @@ void MediaPlayCtrl::on_show_hide(wxShowEvent &evt) evt.Skip(); if (m_isBeingDeleted) return; m_failed_retry = 0; - if (m_next_retry.IsValid()) // Try open 2 seconds later, to avoid quick play/stop - m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(2); - IsShownOnScreen() ? Play() : Stop(); + if (!IsShownOnScreen()) + Stop(); } void MediaPlayCtrl::media_proc() @@ -698,30 +942,34 @@ void MediaPlayCtrl::media_proc() while (m_tasks.empty()) { m_cond.wait(lock); } - wxString url = m_tasks.front(); - if (m_tasks.size() >= 2 && !url.IsEmpty() && url[0] != '<' && m_tasks[1] == "") { - BOOST_LOG_TRIVIAL(trace) << "MediaPlayCtrl: busy skip url: " << url; + MediaTask task = m_tasks.front(); + if (m_tasks.size() >= 2 && !task.command.IsEmpty() && task.command[0] != '<' && + m_tasks[1].command == "" && task.controller == m_tasks[1].controller) { + BOOST_LOG_TRIVIAL(trace) << "MediaPlayCtrl: busy skip url: " << task.command; m_tasks.pop_front(); m_tasks.pop_front(); continue; } lock.unlock(); - if (url == "") { + if (task.command == "") { BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start stop"; - m_media_ctrl->Stop(); + if (task.controller) + task.controller->Stop(); BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end stop"; } - else if (url == "") { + else if (task.command == "") { break; } - else if (url == "") { + else if (task.command == "") { BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start play"; - m_media_ctrl->Play(); + if (task.controller) + task.controller->Play(); BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end play"; } else { BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start load"; - m_media_ctrl->Load(wxURI(url)); + if (task.controller) + task.controller->Load(wxURI(task.command)); BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end load"; } lock.lock(); diff --git a/src/slic3r/GUI/MediaPlayCtrl.h b/src/slic3r/GUI/MediaPlayCtrl.h index 0a01daefc9..1d426fc685 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.h +++ b/src/slic3r/GUI/MediaPlayCtrl.h @@ -9,6 +9,9 @@ #define MediaPlayCtrl_h #include "wxMediaCtrl3.h" +#include "IMediaController.hpp" +#include "WebRtcMediaController.hpp" +#include "slic3r/Utils/IPrinterAgent.hpp" #include @@ -36,6 +39,10 @@ public: void SetMachineObject(MachineObject * obj); + void SetWebMediaController(IMediaController *ctrl); + + void StopWebStream(); + bool IsStreaming() const; void ToggleStream(); @@ -54,6 +61,7 @@ protected: void TogglePlay(); void SetStatus(wxString const &msg, bool hyperlink = true); + void on_webrtc_status(WebRtcMediaController::Status status); private: void load(); @@ -66,6 +74,9 @@ 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(3); static inline const wxMediaState MEDIASTATE_INITIALIZING = static_cast(4); @@ -76,6 +87,13 @@ private: std::shared_ptr m_token = std::make_shared(0); wxMediaCtrl3 * m_media_ctrl; + IMediaController * m_active_media_controller = nullptr; + IMediaController * m_web_ctrl = nullptr; + std::unique_ptr m_webrtc_ctrl; + CameraStreamMode m_last_mode = CameraStreamMode::none; + std::uint64_t m_webrtc_epoch = 0; + std::string m_agent_camera_url; + bool m_web_user_stopped = false; wxMediaState m_last_state = MEDIASTATE_IDLE; std::string m_machine; int m_lan_proto = 0; @@ -91,7 +109,11 @@ private: bool m_disable_lan = false; wxString m_url; - std::deque m_tasks; + struct MediaTask { + wxString command; + IMediaController *controller = nullptr; + }; + std::deque m_tasks; boost::mutex m_mutex; boost::condition_variable m_cond; boost::thread m_thread; diff --git a/src/slic3r/GUI/Monitor.cpp b/src/slic3r/GUI/Monitor.cpp index 50eb97f3cc..91bbbf8930 100644 --- a/src/slic3r/GUI/Monitor.cpp +++ b/src/slic3r/GUI/Monitor.cpp @@ -34,6 +34,8 @@ #include "DeviceCore/DevManager.h" +#include + namespace Slic3r { namespace GUI { @@ -265,6 +267,7 @@ void MonitorPanel::msw_rescale() void MonitorPanel::select_machine(std::string machine_sn) { + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: MonitorPanel::select_machine queueing machine_sn=" << machine_sn; wxCommandEvent *event = new wxCommandEvent(wxEVT_COMMAND_CHOICE_SELECTED); event->SetString(machine_sn); wxQueueEvent(this, event); @@ -285,13 +288,20 @@ void MonitorPanel::on_timer(wxTimerEvent& event) void MonitorPanel::on_select_printer(wxCommandEvent& event) { Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + const std::string requested_dev_id = event.GetString().ToStdString(); + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: MonitorPanel::on_select_printer requested_dev_id=" + << requested_dev_id << " device_manager=" << (dev ? "set" : "null"); if (!dev) return; if ( dev->get_selected_machine() && (dev->get_selected_machine()->get_dev_id() != event.GetString().ToStdString()) && m_hms_panel) { m_hms_panel->clear_hms_tag(); } - if (!dev->set_selected_machine(event.GetString().ToStdString())) + const bool selected = dev->set_selected_machine(requested_dev_id); + BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: MonitorPanel::on_select_printer set_selected_machine result=" + << selected << " selected_dev_id=" + << (dev->get_selected_machine() ? dev->get_selected_machine()->get_dev_id() : ""); + if (!selected) return; set_default(); diff --git a/src/slic3r/GUI/MultiMachine.cpp b/src/slic3r/GUI/MultiMachine.cpp index c0e84f40e9..df6ccc1298 100644 --- a/src/slic3r/GUI/MultiMachine.cpp +++ b/src/slic3r/GUI/MultiMachine.cpp @@ -51,7 +51,7 @@ void DeviceItem::sync_state() state_printable = 6; } - if (is_blocking_printing(obj_)) { + if (wxGetApp().is_blocking_printing(obj_)) { state_printable = 5; } @@ -104,27 +104,6 @@ void DeviceItem::unselected() } } -bool DeviceItem::is_blocking_printing(MachineObject* obj_) -{ - DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (!dev) return true; - auto target_model = obj_->printer_type; - std::string source_model = ""; - - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); - - if (source_model != target_model) { - std::vector compatible_machine = obj_->get_compatible_machine(); - vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); - if (it == compatible_machine.end()) { - return true; - } - } - - return false; -} - void DeviceItem::update_item(const DeviceItem* item) { // Except for the selected status, everything else is updated diff --git a/src/slic3r/GUI/MultiMachine.hpp b/src/slic3r/GUI/MultiMachine.hpp index 51f70b9d96..b38918225c 100644 --- a/src/slic3r/GUI/MultiMachine.hpp +++ b/src/slic3r/GUI/MultiMachine.hpp @@ -59,7 +59,6 @@ public: void selected(); void unselected(); - bool is_blocking_printing(MachineObject* obj_); void update_item(const DeviceItem* item); }; diff --git a/src/slic3r/GUI/PartSkipDialog.hpp b/src/slic3r/GUI/PartSkipDialog.hpp index b95681dcde..37697ced31 100644 --- a/src/slic3r/GUI/PartSkipDialog.hpp +++ b/src/slic3r/GUI/PartSkipDialog.hpp @@ -159,4 +159,4 @@ private: void OnApplyDialog(wxCommandEvent &event); }; -}} // namespace Slic3r::GUI \ No newline at end of file +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 51fdba9bf8..bd9327ad61 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2236,12 +2236,14 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man std::string machine_print_name = obj->get_show_printer_type(); PresetBundle *preset_bundle = wxGetApp().preset_bundle; std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); - Preset* machine_preset = get_printer_preset(obj); - if (!machine_preset) { + const bool optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type); + const bool optional_target_model = DevPrinterConfigUtil::is_optional_printer_model_id(target_model_id); + Preset* machine_preset = optional_printer_model ? nullptr : get_printer_preset(obj); + if (!optional_printer_model && !optional_target_model && !machine_preset) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty"; return false; } - if (machine_print_name != target_model_id) { + if (!optional_printer_model && !optional_target_model && machine_print_name != target_model_id) { MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n" "Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); if (dlg.ShowModal() == wxID_NO) { @@ -2433,6 +2435,11 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj) return; } + if (DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type)) { + clear_all_sync_status(); + return; + } + bool printer_synced = false; // 1. update printer status const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); @@ -6066,11 +6073,30 @@ void Sidebar::load_ams_list(MachineObject* obj) filament_ams_list = build_filament_ams_list(obj); } - bool device_change = false; const std::string& device = obj ? obj->get_dev_id() : ""; - if (p->ams_list_device != device) { + const bool same_device = p->ams_list_device == device; + + // Keep sync metadata out of the device payload, but preserve it across a + // subscription refresh when the physical filament in a slot is unchanged. + // Otherwise the refreshed configs differ only by the missing + // filament_changed key, causing combo boxes to rebuild and lose their + // transient post-sync badges. + auto &previous_filament_ams_list = wxGetApp().preset_bundle->filament_ams_list; + for (auto &entry : filament_ams_list) { + auto previous = previous_filament_ams_list.find(entry.first); + const auto *previous_changed = previous == previous_filament_ams_list.end() ? nullptr : + dynamic_cast(previous->second.option("filament_changed")); + if (!same_device || previous_changed == nullptr || + previous->second.opt_string("filament_id", 0u) != entry.second.opt_string("filament_id", 0u)) { + continue; + } + entry.second.set_key_value("filament_changed", + new ConfigOptionBool{previous_changed->value}); + } + + bool device_change = !same_device; + if (device_change) { p->ams_list_device = device; - device_change = true; } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size(); if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) @@ -6080,9 +6106,27 @@ void Sidebar::load_ams_list(MachineObject* obj) wxGetApp().preset_bundle->filament_ams_list = filament_ams_list; for (auto c : p->combos_filament){ + c->set_sync_badge(false); c->update(); - if (device_change) { - c->ShowBadge(false);//change printer,then clear badge + } + + if (!device_change) { + size_t combo_index = 0; + for (const auto &entry : filament_ams_list) { + const auto &tray = entry.second; + const bool has_filament = !tray.opt_string("filament_id", 0u).empty(); + const bool is_placeholder = tray.has("filament_slot_placeholder") && + tray.opt_bool("filament_slot_placeholder", 0u); + if (!has_filament && !is_placeholder) { + continue; + } + if (combo_index >= p->combos_filament.size()) { + break; + } + const auto *filament_changed = dynamic_cast(tray.option("filament_changed")); + p->combos_filament[combo_index]->set_sync_badge( + has_filament && !is_placeholder && filament_changed != nullptr && filament_changed->value); + ++combo_index; } } @@ -6256,18 +6300,32 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") : _L("Filament type and color information have been synchronized, but slot information is not included."); c->SetToolTip(tip); - c->ShowBadge(true); + c->set_sync_badge(true); }; { // badge ams filament clear_combos_filament_badge(); if (sync_result.direct_sync) { - // Orca: PresetBundle::sync_ams_list rebuilds combos_filament - // 1:1 from the AMS trays that produce a combo (loaded trays + placeholders; non-placeholder - // empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The - // previous per-tray index walked the full filament_ams_list (including the skipped empties), - // so an empty slot before a loaded one dropped the badge for the trailing filaments. - for (auto &c : p->combos_filament) { - badge_combox_filament(c); + // A placeholder contributes a preserved project filament to the + // overwrite result, but it is not AMS-sourced and must not get a + // sync badge. Non-placeholder empty trays are omitted entirely. + size_t combo_index = 0; + for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) { + const auto &tray = entry.second; + const bool has_filament = !tray.opt_string("filament_id", 0u).empty(); + const bool is_placeholder = tray.has("filament_slot_placeholder") && + tray.opt_bool("filament_slot_placeholder", 0u); + if (!has_filament && !is_placeholder) { + continue; + } + if (combo_index >= p->combos_filament.size()) { + break; + } + if (is_placeholder) { + p->combos_filament[combo_index]->set_sync_badge(false); + } else { + badge_combox_filament(p->combos_filament[combo_index]); + } + ++combo_index; } } } @@ -6535,6 +6593,11 @@ template void setup_dialog_position(T& info) void Sidebar::pop_sync_nozzle_and_ams_dialog() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog"; + auto agent = wxGetApp().getAgent(); + if (!agent || agent->get_filament_sync_mode() == FilamentSyncMode::none) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " filament synchronization is not supported; skipping dialog"; + return; + } wxTheApp->CallAfter([this]() { SyncNozzleAndAmsDialog::InputInfo temp_na_info; wxPoint big_btn_pt; @@ -6666,17 +6729,14 @@ void Sidebar::clear_combos_filament_badge() { auto &combos_filament = p->combos_filament; for (auto &c : combos_filament) { // clear flag - c->ShowBadge(false); + c->set_sync_badge(false); } } void Sidebar::udpate_combos_filament_badge() { auto &combos_filament = p->combos_filament; for (auto &c : combos_filament) { - auto selection = c->GetSelection(); - auto select_flag = c->GetFlag(selection); - auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; - c->ShowBadge(ok); + c->update_badge_according_flag(); } } @@ -12661,7 +12721,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) sidebar->auto_calc_flushing_volumes(idx); } auto select_flag = combo->GetFlag(selection); - combo->ShowBadge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); + combo->set_sync_badge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); q->on_filament_change(idx); } bool select_preset = !combo->selection_is_changed_according_to_physical_printers(); @@ -21335,9 +21395,14 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning) } if (!check_printer_initialized(obj, true, popup_warning)) return false; - Preset * machine_preset = get_printer_preset(obj); - if (!machine_preset) + const std::string machine_model = obj->printer_type; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + const std::string selected_model = preset_bundle ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) : std::string(); + if (!DevPrinterConfigUtil::is_optional_printer_model_id(machine_model) && + !DevPrinterConfigUtil::is_optional_printer_model_id(selected_model) && + !get_printer_preset(obj)) { return false; + } if (wxGetApp().is_blocking_printing()) { if (popup_warning) { diff --git a/src/slic3r/GUI/PluginStatus.hpp b/src/slic3r/GUI/PluginStatus.hpp index 477e9d99fe..635e35f98a 100644 --- a/src/slic3r/GUI/PluginStatus.hpp +++ b/src/slic3r/GUI/PluginStatus.hpp @@ -11,6 +11,7 @@ namespace Slic3r // IMPORTANT: ordinal order is the Plugins dialog Status sort priority. Activated, Error, + RuntimeError, Inactive, Loading }; @@ -21,11 +22,28 @@ namespace Slic3r { case PluginStatus::Activated: return "Activated"; case PluginStatus::Error: return "Error"; + case PluginStatus::RuntimeError: return "RuntimeError"; case PluginStatus::Inactive: return "Inactive"; case PluginStatus::Loading: return "Loading"; } return "Inactive"; } + + // why: a plugin whose module is live but whose catalog carries an error is a + // RUNTIME fault (e.g. a capability rejected at register time) - it stays + // loaded/checked and is only flagged, distinct from a load-time Error where + // the module never came up. Loading wins over both so an in-flight reload + // never flashes an error. + inline PluginStatus resolve_plugin_status(bool loading, bool has_error, bool is_loaded) + { + if (loading) + return PluginStatus::Loading; + if (has_error) + return is_loaded ? PluginStatus::RuntimeError : PluginStatus::Error; + if (is_loaded) + return PluginStatus::Activated; + return PluginStatus::Inactive; + } } } // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PluginsDialog.cpp b/src/slic3r/GUI/PluginsDialog.cpp index 8b8a89a45a..6ac04a50da 100644 --- a/src/slic3r/GUI/PluginsDialog.cpp +++ b/src/slic3r/GUI/PluginsDialog.cpp @@ -193,6 +193,7 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item) payload_item["label"] = dialog_item.display_name; payload_item["source"] = to_string(dialog_item.source); payload_item["status"] = to_string(dialog_item.status); + payload_item["is_loaded"] = dialog_item.is_loaded; payload_item["error"] = dialog_item.error_text; payload_item["update_status"] = to_string(dialog_item.update_status); payload_item["unauthorized"] = dialog_item.unauthorized; @@ -337,14 +338,7 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor) item.sharing_token = descriptor.sharing_token; item.thumbnail_url = descriptor.thumbnail_url; - if (item.loading) - item.status = PluginStatus::Loading; - else if (item.has_error) - item.status = PluginStatus::Error; - else if (item.is_loaded) - item.status = PluginStatus::Activated; - else - item.status = PluginStatus::Inactive; + item.status = resolve_plugin_status(item.loading, item.has_error, item.is_loaded); item.available_actions = evaluate_action_policy(item); const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(), @@ -790,6 +784,9 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled) } BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key; + // A user-disabled plugin has no meaningful error state. + if (!manager.clear_plugin_error(plugin_key)) + BOOST_LOG_TRIVIAL(warning) << "Failed to clear plugin error for " << plugin_key << " (failed to find)"; // A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later. if (m_activating_plugin_key == plugin_key) m_activating_plugin_key.clear(); diff --git a/src/slic3r/GUI/PrePrintChecker.cpp b/src/slic3r/GUI/PrePrintChecker.cpp index 0575af23ed..7d0ae63cf1 100644 --- a/src/slic3r/GUI/PrePrintChecker.cpp +++ b/src/slic3r/GUI/PrePrintChecker.cpp @@ -63,6 +63,7 @@ std::string PrePrintChecker::get_print_status_info(PrintDialogStatus status) case PrintStatusRackReading: return "PrintStatusRackReading"; case PrintStatusRackNozzleNumUnmeetWarning: return "PrintStatusRackNozzleNumUnmeetWarning"; case PrintStatusHasUnreliableNozzleWarning: return "PrintStatusHasUnreliableNozzleWarning"; + case PrintStatusOptionalPrinterModel: return "PrintStatusOptionalPrinterModel"; case PrintStatusWarningExtFilamentNotMatch: return "PrintStatusWarningExtFilamentNotMatch"; case PrintStatusFilamentWarningNozzleHRC: return "PrintStatusFilamentWarningNozzleHRC"; case PrintStatusTPUUnsupportCaliOn: return "PrintStatusTPUUnsupportCaliOn"; @@ -104,6 +105,7 @@ wxString PrePrintChecker::get_pre_state_msg(PrintDialogStatus status) case PrintStatusNeedConsistencyUpgrading: return _L("Cannot send the print job to a printer whose firmware must be updated."); case PrintStatusBlankPlate: return _L("Cannot send a print job for an empty plate."); case PrintStatusTimelapseNoSdcard: return _L("Storage needs to be inserted to record timelapse."); + case PrintStatusOptionalPrinterModel: return _L("The selected printer model could not be identified, so compatibility with the print file configuration cannot be verified. Please verify the printer preset before sending."); case PrintStatusMixAmsAndVtSlotWarning: return _L("You have selected both external and AMS filaments for an extruder. You will need to manually switch the external filament during printing."); case PrintStatusTPUUnsupportAutoCali: return _L("TPU 90A/TPU 85A is too soft and does not support automatic Flow Dynamics calibration."); case PrintStatusWarningKvalueNotUsed: return _L("Set dynamic flow calibration to 'OFF' to enable custom dynamic flow value."); @@ -379,4 +381,3 @@ bool PrinterMsgPanel::UpdateInfos(const std::vector& infos) } }; - diff --git a/src/slic3r/GUI/PrePrintChecker.hpp b/src/slic3r/GUI/PrePrintChecker.hpp index f629b70a73..5dea00a88f 100644 --- a/src/slic3r/GUI/PrePrintChecker.hpp +++ b/src/slic3r/GUI/PrePrintChecker.hpp @@ -112,6 +112,7 @@ enum PrintDialogStatus : unsigned int { // Orca: a nozzle diameter that differs from the one the printer remembers is a warning, // not an error, so non-standard nozzles can still be printed with. PrintStatusNozzleDiameterMismatch, + PrintStatusOptionalPrinterModel, PrintStatusPrinterWarningEnd, // Warnings for filament @@ -170,7 +171,7 @@ public: void add_with_checkbox(PrintDialogStatus state, wxString msg, wxString checkbox_label, bool checked, std::function checkbox_callback); static ::std::string get_print_status_info(PrintDialogStatus status); - wxString get_pre_state_msg(PrintDialogStatus status); + static wxString get_pre_state_msg(PrintDialogStatus status); static bool is_error(PrintDialogStatus status) { return (PrintStatusErrorBegin < status) && (PrintStatusErrorEnd > status); }; static bool is_error_printer(PrintDialogStatus status) { return (PrintStatusPrinterErrorBegin < status) && (PrintStatusPrinterErrorEnd > status); }; static bool is_error_filament(PrintDialogStatus status) { return (PrintStatusFilamentErrorBegin < status) && (PrintStatusFilamentErrorEnd > status); }; diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index ac419073d7..c5ffb533e2 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -361,7 +361,8 @@ wxString PresetComboBox::get_preset_item_name(unsigned int index) return GetString(index); } - std::map machine_list = dev->get_my_machine_list(); + std::map machine_list = + dev->get_my_machine_list(dev->get_current_printer_agent_id()); if (machine_list.empty()) { assert(false); m_selected_dev_id.clear(); @@ -479,7 +480,8 @@ void PresetComboBox::add_connected_printers(std::string selected, bool alias_nam if (!dev) return; - std::map machine_list = dev->get_my_machine_list(); + std::map machine_list = + dev->get_my_machine_list(dev->get_current_printer_agent_id()); if (machine_list.empty()) return; @@ -999,7 +1001,13 @@ void PlaterPresetComboBox::update_badge_according_flag() { auto selection = GetSelection(); auto select_flag = GetFlag(selection); auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; - ShowBadge(ok); + ShowBadge(m_sync_badge || ok); +} + +void PlaterPresetComboBox::set_sync_badge(bool show) +{ + m_sync_badge = show; + ShowBadge(show); } bool PlaterPresetComboBox::switch_to_tab() diff --git a/src/slic3r/GUI/PresetComboBoxes.hpp b/src/slic3r/GUI/PresetComboBoxes.hpp index 20f75f4616..b63f51dd70 100644 --- a/src/slic3r/GUI/PresetComboBoxes.hpp +++ b/src/slic3r/GUI/PresetComboBoxes.hpp @@ -205,6 +205,7 @@ public: void msw_rescale() override; void OnSelect(wxCommandEvent& evt) override; void update_badge_according_flag(); + void set_sync_badge(bool show); FilamentColor get_cur_color_info(); void show_default_color_picker(); @@ -214,6 +215,7 @@ public: private: // BBS wxColor m_color; + bool m_sync_badge{false}; }; diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 7853149ccf..93a9029ff9 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -32,6 +32,7 @@ #include "DeviceCore/DevStorage.h" #include "../Utils/Http.hpp" #include "md4c/src/md4c-html.h" +#include "../Utils/Http.hpp" namespace Slic3r { namespace GUI { @@ -1778,7 +1779,7 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj) auto str_ip = m_input_ip->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); // ORCA enabling / disabling buttons with conditions enough to change its style - m_button_ok->Enable(isValidEndpoint(str_ip.ToStdString()) && + m_button_ok->Enable(isIp(str_ip.ToStdString()) && (str_access_code.IsEmpty() || str_access_code.Length() >= 8)); Layout(); @@ -2141,7 +2142,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt) // ORCA enabling / disabling buttons with conditions enough to change its style bool valid_access_code_length = str_access_code.IsEmpty() || str_access_code.Length() >= 8; - bool enable_btns = isValidEndpoint(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code; + bool enable_btns = isIp(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code; m_button_manual_setup->Enable(enable_btns); m_button_ok->Enable(enable_btns); diff --git a/src/slic3r/GUI/ReleaseNote.hpp b/src/slic3r/GUI/ReleaseNote.hpp index 0ac667ac0d..5d950005df 100644 --- a/src/slic3r/GUI/ReleaseNote.hpp +++ b/src/slic3r/GUI/ReleaseNote.hpp @@ -315,12 +315,16 @@ public: Button* m_button_manual_setup{ nullptr }; Label* m_tips_ip{ nullptr }; Label* m_tips_access_code{ nullptr }; + Label* m_tips_cafile{ nullptr }; + Label* m_cafile_hint{ nullptr }; Label* m_tips_sn{nullptr}; Label* m_tips_modelID{nullptr}; Label* m_test_right_msg{ nullptr }; Label* m_test_wrong_msg{ nullptr }; TextInput* m_input_ip{ nullptr }; TextInput* m_input_access_code{ nullptr }; + wxTextCtrl* m_input_cafile{ nullptr }; + Button* m_button_cafile{ nullptr }; TextInput* m_input_printer_name{ nullptr }; TextInput* m_input_sn{ nullptr }; ComboBox* m_input_modelID{ nullptr }; diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 04eb2b1612..db4eb723bc 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -21,6 +21,7 @@ #include "Jobs/PlaterWorker.hpp" #include "DeviceCore/DevConfig.h" +#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevNozzleSystem.h" #include "DeviceCore/DevNozzleRack.h" #include "DeviceCore/DevExtensionTool.h" @@ -1128,7 +1129,10 @@ bool SelectMachineDialog::do_ams_mapping(MachineObject *obj_,bool use_ams) int filament_result = 0; std::vector map_opt; //four values: use_left_ams, use_right_ams, use_left_ext, use_right_ext - if (nozzle_nums > 1){ + // Orca: only do the per-physical-extruder left/right split when the device actually reports + // 2+ extruders. A non-BBL multi-nozzle printer (e.g. Snapmaker U1) reports a single extruder + // with one filament pool, so it maps as a single surface via the else branch below. + if (nozzle_nums > 1 && obj_->GetExtderSystem()->GetTotalExtderCount() > 1){ //get nozzle property, the extders are same? if (true/*!can_hybrid_mapping(obj_get_extder_data())*/){ std::vector m_ams_mapping_result_left, m_ams_mapping_result_right; @@ -2287,9 +2291,7 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector 1) msg_text = wxString::Format(_L("Filament %s does not match the filament in AMS slot %s. Please update the printer firmware to support AMS slot assignment."), params[0], params[1]); @@ -2315,8 +2317,10 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vectorprinter_type; - std::string source_model = ""; + if (m_print_type == PrintFromType::FROM_NORMAL) + return wxGetApp().is_blocking_printing(obj_); - if (m_print_type == PrintFromType::FROM_NORMAL) { - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); - - - }else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW) { - if (m_required_data_plate_data_list.size() > 0) { - source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id; - } - } - - if (source_model != target_model) { - std::vector compatible_machine = obj_->get_compatible_machine(); - vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); - if (it == compatible_machine.end()) { - return true; - } - } - - return false; + std::string source_model; + if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty()) + source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id; + return wxGetApp().is_blocking_printing(obj_, source_model); } static std::unordered_set _get_used_nozzle_idxes() @@ -2625,6 +2615,10 @@ bool SelectMachineDialog::is_same_printer_model() if(preset_bundle == nullptr) return result; const auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); const auto target_model = obj_->printer_type; + if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) || + DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) { + return true; + } // Orca: ignore P1P -> P1S if (source_model != target_model) { if ((source_model == "C12" && target_model == "C11") || (source_model == "C11" && target_model == "C12") || @@ -3699,10 +3693,32 @@ void SelectMachineDialog::on_send_print() m_print_job->on_success([this]() { finish_mode(); }); m_print_job->on_check_ip_address_fail([this]() { - wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS); - wxQueueEvent(this, evt); - wxGetApp().show_ip_address_enter_dialog(); - }); + // Invoked from the PrintJob worker thread when the LAN pre-flight (file upload + // verification) fails. Marshal device/UI access to the main thread. + CallAfter([this]() + { + // Reset the dialog out of sending mode so the user can retry. + wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS); + wxQueueEvent(this, evt); + + DeviceManager* dev = wxGetApp().getDeviceManager(); + MachineObject* obj = dev ? dev->get_selected_machine() : nullptr; + + if (obj && obj->is_connected()) + { + // Connected: failed on file upload + MessageDialog dlg(this, + _L("Failed to upload the file to the printer's storage. Please try again."), + _L("Send Failed"), wxOK | wxICON_ERROR); + dlg.ShowModal(); + } + else + { + // Not connected: reenter ip and access code + wxGetApp().show_ip_address_enter_dialog(); + } + }); + }); // update ota version NetworkAgent* agent = wxGetApp().getAgent(); @@ -4536,11 +4552,13 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj // check nozzle data valid { - if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine || - installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) { - show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid); - return false; - } + // Commented out the following as ntUndefine and 0.0f are default values + // (signifying that the value is not given) that should PASS, not fail + // if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine || + // installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) { + // show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid); + // return false; + // } if (obj_->is_nozzle_flow_type_supported() && installed_ext_nozzle.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) { @@ -4569,7 +4587,10 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj // check nozzle diameter { - if (slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) { + // 0.0f is default when there is no nozzle diameter is given. + // In nozzle_diameter == 0.0f case, it passes and does not require a comparison + if (installed_ext_nozzle.GetNozzleDiameter() > 0.0f && + slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) { std::vector msg_params; if (ext_sys->GetTotalExtderCount() == 2) { const wxString& mismatch_nozzle_str = _get_nozzle_name(ext_sys->GetTotalExtderCount(), slicing_ext_idx); @@ -4763,6 +4784,22 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_) return; } + bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type); + if (m_print_type == PrintFromType::FROM_NORMAL) { + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + has_optional_printer_model = has_optional_printer_model || + (preset_bundle && DevPrinterConfigUtil::is_optional_printer_model_id( + preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle))); + } else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty()) { + has_optional_printer_model = has_optional_printer_model || + DevPrinterConfigUtil::is_optional_printer_model_id( + m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id); + } + + if (has_optional_printer_model) { + show_status(PrintDialogStatus::PrintStatusOptionalPrinterModel); + } + if (is_blocking_printing(obj_)) { show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter); return; @@ -5485,8 +5522,8 @@ void SelectMachineDialog::reset_and_sync_ams_list() item = new MaterialItem(m_filament_left_panel, colour_rgb, _L(display_materials[extruder])); m_sizer_ams_mapping_left->Add(item, 0, wxALL, FromDIP(5)); } - else if (m_filaments_map[extruder] == 2) - { + else // map == 2, or (non-BBL multi-nozzle) 3+; update_material_item_pos() collapses + { // these into the single panel when the device reports < 2 extruders. item = new MaterialItem(m_filament_right_panel, colour_rgb, _L(display_materials[extruder])); m_sizer_ams_mapping_right->Add(item, 0, wxALL, FromDIP(5)); } diff --git a/src/slic3r/GUI/SendMultiMachinePage.cpp b/src/slic3r/GUI/SendMultiMachinePage.cpp index b386d1f451..04becefeac 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.cpp +++ b/src/slic3r/GUI/SendMultiMachinePage.cpp @@ -738,7 +738,7 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event) if (obj && obj->is_online() && !obj->can_abort() && !obj->is_in_upgrading() && it->second->get_state_selected() == 1 && it->second->state_printable <= 2) { - if (!it->second->is_blocking_printing(obj)) { + if (!wxGetApp().is_blocking_printing(obj)) { PrintParams params = request_params(obj); print_params.push_back(params); } diff --git a/src/slic3r/GUI/SendToPrinter.cpp b/src/slic3r/GUI/SendToPrinter.cpp index 9071c0affd..d582cfdef7 100644 --- a/src/slic3r/GUI/SendToPrinter.cpp +++ b/src/slic3r/GUI/SendToPrinter.cpp @@ -24,6 +24,7 @@ #include "BitmapCache.hpp" #include "DeviceCore/DevManager.h" +#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevStorage.h" #include "slic3r/Utils/FileTransferUtils.hpp" @@ -300,7 +301,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater) m_storage_panel->Layout(); // try to connect - m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); + m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(400), -1), wxALIGN_CENTER_HORIZONTAL); m_statictext_printer_msg->SetFont(::Label::Body_13); m_statictext_printer_msg->SetForegroundColour(*wxBLACK); m_statictext_printer_msg->Hide(); @@ -760,9 +761,25 @@ void SendToPrinterDialog::update_priner_status_msg(wxString msg, bool is_warning if (str_new != str_old) { if (m_statictext_printer_msg->GetLabel() != msg) { m_statictext_printer_msg->SetLabel(msg); - m_statictext_printer_msg->SetMinSize(wxSize(FromDIP(400), -1)); - m_statictext_printer_msg->SetMaxSize(wxSize(FromDIP(400), -1)); - m_statictext_printer_msg->Wrap(FromDIP(400)); + const int wrap_width = FromDIP(400); + m_statictext_printer_msg->Wrap(wrap_width); + int line_count = 1; + const wxString wrapped_label = m_statictext_printer_msg->GetLabel(); + for (size_t i = 0; i < wrapped_label.length(); ++i) { + if (wrapped_label[i] == '\n') + ++line_count; + } + wxCoord text_width = 0; + wxCoord text_height = 0; + m_statictext_printer_msg->GetTextExtent(msg, &text_width, &text_height); + const int extent_line_count = text_width > 0 ? + std::max(1, (static_cast(text_width) + wrap_width - 1) / wrap_width) : 1; + line_count = std::max(line_count, extent_line_count); + const int line_height = std::max(m_statictext_printer_msg->GetCharHeight(), static_cast(text_height)); + const int min_height = std::max(m_statictext_printer_msg->GetBestSize().GetHeight(), + line_count * line_height + FromDIP(2)); + m_statictext_printer_msg->SetMinSize(wxSize(wrap_width, min_height)); + m_statictext_printer_msg->SetMaxSize(wxDefaultSize); m_statictext_printer_msg->Show(); Layout(); Fit(); @@ -1102,7 +1119,7 @@ void SendToPrinterDialog::update_user_printer() wxArrayString machine_list_name; std::map option_list; - option_list = dev->get_my_machine_list(); + option_list = dev->get_my_machine_list(dev->get_current_printer_agent_id()); // same machine only appear once for (auto it = option_list.begin(); it != option_list.end(); it++) { @@ -1260,7 +1277,7 @@ void SendToPrinterDialog::update_show_status() reset_timeout(); // reading done - if (is_blocking_printing(obj_)) { + if (wxGetApp().is_blocking_printing(obj_)) { show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter); return; } @@ -1325,26 +1342,6 @@ void SendToPrinterDialog::update_show_status() } } -bool SendToPrinterDialog::is_blocking_printing(MachineObject* obj_) -{ - DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (!dev) return true; - - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); - auto target_model = obj_->printer_type; - - if (source_model != target_model) { - std::vector compatible_machine = obj_->get_compatible_machine(); - vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); - if (it == compatible_machine.end()) { - return true; - } - } - - return false; -} - void SendToPrinterDialog::Enable_Refresh_Button(bool en) { if (!en) { @@ -1413,79 +1410,68 @@ void SendToPrinterDialog::show_status(PrintDialogStatus status, std::vectorget_selected_machine() : nullptr; - MachineObject *obj = dm->get_selected_machine(); - if (obj == nullptr) { + if (!obj) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : obj is empty"; - m_connection_status = ConnectionStatus::NOT_START; - } - - int remote_proto = obj->get_file_remote(); - if (!remote_proto) { + if (obj && !obj->get_file_remote()) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : remote_proto is not support"; - m_connection_status = ConnectionStatus::NOT_START; - } + if (obj && obj->is_camera_busy_off()) + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy"; - if (obj->is_camera_busy_off()) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy"; - m_connection_status = ConnectionStatus::NOT_START; - } + NetworkAgent* agent = wxGetApp().getAgent(); - NetworkAgent *agent = wxGetApp().getAgent(); - std::string agent_version = agent ? agent->get_version() : ""; - std::string dev_ver = obj->get_ota_version(); - std::string dev_id = obj->get_dev_id(); - - if (m_url_timer && m_url_timer->IsRunning()) + if (m_url_timer && m_url_timer->IsRunning()) { m_url_timer->Stop(); } @@ -1709,19 +1683,40 @@ void SendToPrinterDialog::GetConnection() m_url_timer->GetId()); m_url_timer->StartOnce(8000); - if (agent) { + if (obj && agent) + { + std::string dev_ver = obj->get_ota_version(); + std::string dev_id = obj->get_dev_id(); + if (m_tcp_try_connect) { std::string devIP = obj->get_dev_ip(); std::string accessCode = obj->get_access_code(); std::string url = "bambu:///local/" + devIP + "?port=6000&user=" + "bblp" + "&passwd=" + accessCode; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp"; - m_filetransfer_tunnel = std::make_unique(module(), url); - m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg) { - CallAfter([this, is_success, err_code, error_msg]() { - OnConnection(is_success, err_code, error_msg); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp, dev_id=" << dev_id + << ", dev_ip=" << devIP << ", access_code_len=" << accessCode.size(); + + try + { + m_filetransfer_tunnel = std::make_unique(module(), url); + m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg) + { + CallAfter([this, is_success, err_code, error_msg]() + { + OnConnection(is_success, err_code, error_msg); + }); }); - }); - m_filetransfer_tunnel->start_connect(); + m_filetransfer_tunnel->start_connect(); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": tcp FileTransferTunnel unavailable for dev_id=" << + dev_id + << " dev_ip=" << devIP << ": " << e.what(); + if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop(); + m_filetransfer_tunnel.reset(); + m_connection_status = ConnectionStatus::CONNECTION_FAILED; + show_status(PrintDialogStatus::PrintStatusPublicInitFailed); + } } else if (m_tutk_try_connect) { @@ -1749,11 +1744,28 @@ void SendToPrinterDialog::GetConnection() if (boost::algorithm::starts_with(url, "bambu:///")) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tutk"; - m_filetransfer_tunnel = std::make_unique(module(), url); - m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg) { - CallAfter([this, is_success, err_code, error_msg]() { OnConnection(is_success, err_code, error_msg); }); - }); - m_filetransfer_tunnel->start_connect(); + try + { + m_filetransfer_tunnel = std::make_unique(module(), url); + m_filetransfer_tunnel->on_connection( + [this](bool is_success, int err_code, std::string error_msg) + { + CallAfter([this, is_success, err_code, error_msg]() + { + OnConnection(is_success, err_code, error_msg); + }); + }); + m_filetransfer_tunnel->start_connect(); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": tutk FileTransferTunnel unavailable: " << e. +what(); + if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop(); + m_filetransfer_tunnel.reset(); + m_connection_status = ConnectionStatus::CONNECTION_FAILED; + show_status(PrintDialogStatus::PrintStatusPublicInitFailed); + } } else { @@ -1852,8 +1864,17 @@ void SendToPrinterDialog::ResetTunnelAndJob() void SendToPrinterDialog::CreateMediaAbilityJob() { - nlohmann::json media_ability = {{"cmd_type", 7}}; - m_filetransfer_mediability_job = std::make_unique(module(), std::string(media_ability.dump())); + nlohmann::json media_ability = {{"cmd_type", 7}}; + try + { + m_filetransfer_mediability_job = std::make_unique(module(), std::string(media_ability.dump())); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what(); + show_status(PrintDialogStatus::PrintStatusPublicInitFailed); + return; + } m_filetransfer_mediability_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector bin_res) { //this pl CallAfter([this, res, resp_ec, json_res] { @@ -1908,11 +1929,20 @@ void SendToPrinterDialog::CreateUploadFileJob(const std::string &path, const std {"cmd_type", 5}, }; upload_params["dest_storage"] = m_selected_storage; - upload_params["dest_name"] = name; // filenme no path - upload_params["file_path"] = path; + upload_params["dest_name"] = name; // filenme no path + upload_params["file_path"] = path; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob"; - m_filetransfer_uploadfile_job = std::make_unique(module(), std::string(upload_params.dump())); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob"; + try + { + m_filetransfer_uploadfile_job = std::make_unique(module(), std::string(upload_params.dump())); + } + catch (const std::exception& e) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what(); + show_status(PrintDialogStatus::PrintStatusPublicUploadFiled); + return; + } m_filetransfer_uploadfile_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector bin_res) { // CallAfter([this, res, resp_ec, json_res, bin_res] { UploadFileRessultCallback(res, resp_ec,json_res, bin_res); diff --git a/src/slic3r/GUI/SendToPrinter.hpp b/src/slic3r/GUI/SendToPrinter.hpp index 24e483ae33..ddfb017552 100644 --- a/src/slic3r/GUI/SendToPrinter.hpp +++ b/src/slic3r/GUI/SendToPrinter.hpp @@ -181,7 +181,6 @@ public: void reset_timeout(); void update_user_printer(); void update_show_status(); - bool is_blocking_printing(MachineObject* obj_); void prepare(int print_plate_idx); void check_focus(wxWindow* window); void check_fcous_state(wxWindow* window); diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 0b6e4b6577..e4a9d906c0 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "DeviceCore/DevBed.h" #include "DeviceCore/DevCtrl.h" @@ -1501,27 +1502,26 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page() m_setting_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); m_setting_button->SetBackgroundColour(STATUS_TITLE_BG); - m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0); - m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); - m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG); - m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp()); - m_camera_switch_button->Bind(wxEVT_LEFT_DOWN, &StatusBasePanel::on_camera_switch_toggled, this); - m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) { - const std::string js_request_pip = R"( - document.querySelector('video').requestPictureInPicture(); - )"; - m_custom_camera_view->RunScript(js_request_pip); - }); - m_camera_switch_button->Hide(); + // m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0); + // m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); + // m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG); + // m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp()); + // m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) { + // const std::string js_request_pip = R"( + // document.querySelector('video').requestPictureInPicture(); + // )"; + // m_custom_camera_view->RunScript(js_request_pip); + // }); + // m_camera_switch_button->Hide(); m_bitmap_sdcard_img->SetToolTip(_L("Storage")); m_bitmap_timelapse_img->SetToolTip(_L("Timelapse")); m_bitmap_recording_img->SetToolTip(_L("Video")); m_bitmap_vcamera_img->SetToolTip(_L("Go Live")); m_setting_button->SetToolTip(_L("Camera Setting")); - m_camera_switch_button->SetToolTip(_L("Switch Camera View")); + // m_camera_switch_button->SetToolTip(_L("Switch Camera View")); - bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); + // bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_sdcard_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_timelapse_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_recording_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); @@ -1544,19 +1544,18 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page() m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString); m_custom_camera_view->EnableContextMenu(false); Bind(wxEVT_WEBVIEW_NAVIGATING, &StatusBasePanel::on_webview_navigating, this, m_custom_camera_view->GetId()); + m_web_media_controller = std::make_unique(m_custom_camera_view); m_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40))); + m_media_play_ctrl->SetWebMediaController(m_web_media_controller.get()); m_custom_camera_view->Hide(); - m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) { - if (evt.GetString() == "leavepictureinpicture") { - // When leaving PiP, video gets paused in some cases and toggling play - // programmatically does not work. - m_custom_camera_view->Reload(); - } - else if (evt.GetString() == "enterpictureinpicture") { - toggle_builtin_camera(); - } - }); + // m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) { + // if (evt.GetString() == "leavepictureinpicture") { + // // When leaving PiP, video gets paused in some cases and toggling play + // // programmatically does not work. + // m_custom_camera_view->Reload(); + // } + // }); sizer->Add(m_media_ctrl, 1, wxEXPAND | wxALL, 0); sizer->Add(m_custom_camera_view, 1, wxEXPAND | wxALL, 0); @@ -1566,10 +1565,6 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page() // // sizer->Add(media_ctrl_panel, 1, wxEXPAND | wxALL, 1); - if (wxGetApp().app_config->get("camera", "enable_custom_source") == "true") { - handle_camera_source_change(); - } - return sizer; } @@ -2333,6 +2328,27 @@ void StatusPanel::update_camera_state(MachineObject* obj) { if (!obj) return; + auto agent = wxGetApp().getAgent(); + const auto camera_mode = agent ? agent->get_camera_stream_mode() : CameraStreamMode::none; + const bool use_webview = camera_mode == CameraStreamMode::http_snapshot; + if (use_webview) { + //m_camera_switch_button->Hide(); + if (!m_custom_camera_view->IsShown()) { + // why: do not reload the WebView URL per tick, or redirects can cause a reload loop. + // MediaPlayCtrl (via its WebMediaController) owns loading/playing the stream itself. + m_custom_camera_view->Show(); + m_media_ctrl->Hide(); + } + } else { + if (m_custom_camera_view->IsShown()) { + m_custom_camera_view->Hide(); + // Stop the snapshot WebView before switching to native playback + // or leaving the camera mode. + m_media_play_ctrl->StopWebStream(); + } + m_media_ctrl->Show(); + } + //sdcard auto sdcard_state = obj->GetStorage()->get_sdcard_state(); if (m_last_sdcard != sdcard_state) { @@ -2364,7 +2380,12 @@ void StatusPanel::update_camera_state(MachineObject* obj) m_last_recording = obj->is_recording() ? 1 : 0; } - if (!m_bitmap_recording_img->IsShown()) { + if (use_webview) { + if (m_bitmap_recording_img->IsShown()) { + m_bitmap_recording_img->Hide(); + m_panel_monitoring_title->Layout(); + } + } else if (!m_bitmap_recording_img->IsShown()) { m_bitmap_recording_img->Show(); m_panel_monitoring_title->Layout(); } @@ -2421,6 +2442,8 @@ void StatusPanel::update_camera_state(MachineObject* obj) bool show_vcamera = m_media_play_ctrl->IsStreaming(); m_camera_popup->update(show_vcamera); } + + m_setting_button->Show(!use_webview); } StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name) @@ -3951,39 +3974,60 @@ void StatusPanel::update_cloud_subtask(MachineObject *obj) update_calib_bitmap(); if (obj->slice_info) { m_request_url = wxString(obj->slice_info->thumbnail_url); - if (!m_request_url.IsEmpty()) { - wxImage img; - std::map::iterator it = img_list.find(m_request_url); - if (it != img_list.end()) { - if (m_current_print_mode != PrintingTaskType::CALIBRATION ||(m_calib_mode == CalibMode::Calib_Flow_Rate && m_calib_method == CalibrationMethod::CALI_METHOD_MANUAL)) { - img = it->second; - wxImage resize_img = img.Scale(m_project_task_panel->get_bitmap_thumbnail()->GetSize().x, m_project_task_panel->get_bitmap_thumbnail()->GetSize().y); - m_project_task_panel->set_thumbnail_img(resize_img, ""); - m_project_task_panel->set_brightness_value(get_brightness_value(resize_img)); - } - if (this->obj) { - m_project_task_panel->set_plate_index(obj->m_plate_index); - } else { - m_project_task_panel->set_plate_index(-1); - } - task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL; - BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image"; - } else { - web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url); - BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url; - web_request.Start(); - m_start_loading_thumbnail = false; - } - } + load_thumbnail_from_url(m_request_url, obj); } } } +bool StatusPanel::load_thumbnail_from_url(const wxString &url, MachineObject *obj) +{ + if (url.IsEmpty()) + return false; + + wxImage img; + std::map::iterator it = img_list.find(url); + if (it != img_list.end()) { + if (m_current_print_mode != PrintingTaskType::CALIBRATION ||(m_calib_mode == CalibMode::Calib_Flow_Rate && m_calib_method == CalibrationMethod::CALI_METHOD_MANUAL)) { + img = it->second; + wxImage resize_img = img.Scale(m_project_task_panel->get_bitmap_thumbnail()->GetSize().x, m_project_task_panel->get_bitmap_thumbnail()->GetSize().y); + m_project_task_panel->set_thumbnail_img(resize_img, ""); + m_project_task_panel->set_brightness_value(get_brightness_value(resize_img)); + } + if (this->obj) { + m_project_task_panel->set_plate_index(obj->m_plate_index); + } else { + m_project_task_panel->set_plate_index(-1); + } + task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL; + BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image"; + } else { + m_request_url = url; + web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url); + BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url; + web_request.Start(); + m_start_loading_thumbnail = false; + } + return true; +} + void StatusPanel::update_sdcard_subtask(MachineObject *obj) { if (!obj) return; - if (!m_load_sdcard_thumbnail) { + const wxString thumbnail_url = wxString(obj->m_agent_thumbnail_url); + if (!thumbnail_url.IsEmpty()) { + if (m_request_url != thumbnail_url || !m_load_sdcard_thumbnail) { + if (web_request.IsOk() && web_request.GetState() == wxWebRequest::State_Active) + web_request.Cancel(); + update_calib_bitmap(); + m_request_url = thumbnail_url; + load_thumbnail_from_url(thumbnail_url, obj); + m_load_sdcard_thumbnail = true; + } + return; + } + + if (!m_load_sdcard_thumbnail || !m_request_url.IsEmpty()) { update_calib_bitmap(); if (m_current_print_mode != PrintingTaskType::CALIBRATION) { m_project_task_panel->get_bitmap_thumbnail()->SetBitmap(m_thumbnail_sdcard.bmp()); @@ -3991,6 +4035,7 @@ void StatusPanel::update_sdcard_subtask(MachineObject *obj) } task_thumbnail_state = ThumbnailState::SDCARD_THUMBNAIL; m_load_sdcard_thumbnail = true; + m_request_url.clear(); } } @@ -4720,11 +4765,11 @@ void StatusPanel::on_ams_refresh_rfid(wxCommandEvent &event) try { if (!use_new_command) { int tray_index = atoi(curr_ams_id.c_str()) * 4 + atoi(slot_it->second->id.c_str()); - obj->command_ams_refresh_rfid(std::to_string(tray_index)); + obj->command_ams_refresh_rfid(-1, tray_index); } if (use_new_command) { - obj->command_ams_refresh_rfid2(stoi(curr_ams_id), stoi(curr_can_id)); + obj->command_ams_refresh_rfid(stoi(curr_ams_id), stoi(curr_can_id)); } } catch (...) { @@ -4990,7 +5035,6 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event) } sdcard_hint_dlg->on_show(); }); - m_camera_popup->Bind(EVT_CAM_SOURCE_CHANGE, &StatusPanel::on_camera_source_change, this); wxWindow* ctrl = (wxWindow*)event.GetEventObject(); wxPoint pos = ctrl->ClientToScreen(wxPoint(0, 0)); wxSize sz = ctrl->GetSize(); @@ -5002,54 +5046,6 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event) } } -void StatusBasePanel::on_camera_source_change(wxCommandEvent& event) -{ - handle_camera_source_change(); -} - -void StatusBasePanel::handle_camera_source_change() -{ - const auto new_cam_url = wxGetApp().app_config->get("camera", "custom_source"); - const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true"; - - if (enabled && !new_cam_url.empty()) { - m_custom_camera_view->LoadURL(new_cam_url); - toggle_custom_camera(); - m_camera_switch_button->Show(); - } else { - toggle_builtin_camera(); - m_camera_switch_button->Hide(); - } -} - -void StatusBasePanel::toggle_builtin_camera() -{ - m_custom_camera_view->Hide(); - m_media_ctrl->Show(); - m_media_play_ctrl->Show(); -} - -void StatusBasePanel::toggle_custom_camera() -{ - const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true"; - - if (enabled) { - m_custom_camera_view->Show(); - m_media_ctrl->Hide(); - m_media_play_ctrl->Hide(); - } -} - -void StatusBasePanel::on_camera_switch_toggled(wxMouseEvent& event) -{ - const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true"; - if (enabled && m_media_ctrl->IsShown()) { - toggle_custom_camera(); - } else { - toggle_builtin_camera(); - } -} - void StatusBasePanel::remove_controls() { const std::string js_cleanup_video_element = R"( @@ -5202,6 +5198,11 @@ bool StatusPanel::is_stage_list_info_changed(MachineObject *obj) void StatusPanel::set_default() { BOOST_LOG_TRIVIAL(trace) << "status_panel: set_default"; + if (m_custom_camera_view->IsShown()) { + m_custom_camera_view->Hide(); + m_media_ctrl->Show(); + m_media_play_ctrl->StopWebStream(); + } obj = nullptr; last_subtask = nullptr; last_tray_exist_bits = -1; diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index d79c3d3915..0666703475 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -14,7 +14,9 @@ #include #include #include +#include #include "MediaPlayCtrl.h" +#include "WebMediaController.hpp" #include "AMSSetting.hpp" #include "Calibration.hpp" #include "CalibrationWizardPage.hpp" @@ -440,7 +442,7 @@ protected: wxStaticBitmap *m_bitmap_sdcard_img; wxStaticBitmap *m_bitmap_static_use_time; wxStaticBitmap *m_bitmap_static_use_weight; - wxStaticBitmap* m_camera_switch_button; + // wxStaticBitmap* m_camera_switch_button; wxMediaCtrl3 * m_media_ctrl; @@ -462,6 +464,8 @@ protected: ScalableButton *m_button_abort; Button * m_button_clean; wxWebView * m_custom_camera_view{nullptr}; + std::unique_ptr m_web_media_controller; + wxSimplebook* m_extruder_book; std::vector m_extruderImage; @@ -577,13 +581,8 @@ protected: virtual void on_axis_ctrl_e_up_10(wxCommandEvent &event) { event.Skip(); } virtual void on_axis_ctrl_e_down_10(wxCommandEvent &event) { event.Skip(); } virtual void on_nozzle_selected(wxCommandEvent &event) { event.Skip(); } - void on_camera_source_change(wxCommandEvent& event); - void handle_camera_source_change(); void remove_controls(); void on_webview_navigating(wxWebViewEvent& evt); - void on_camera_switch_toggled(wxMouseEvent& event); - void toggle_custom_camera(); - void toggle_builtin_camera(); public: StatusBasePanel(wxWindow * parent, @@ -631,6 +630,7 @@ class StatusPanel : public StatusBasePanel private: friend class MonitorPanel; void wire_controls(); + bool load_thumbnail_from_url(const wxString &url, MachineObject *obj); protected: std::shared_ptr m_slice_info_popup; diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 07c75e809a..b481a1b71b 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -1746,6 +1746,8 @@ void SyncAmsInfoDialog::show_status(PrintDialogStatus status, std::vectorprinter_type; - std::string source_model = ""; + if (m_print_type == PrintFromType::FROM_NORMAL) + return wxGetApp().is_blocking_printing(obj_); - if (m_print_type == PrintFromType::FROM_NORMAL) { - PresetBundle *preset_bundle = wxGetApp().preset_bundle; - source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); - - } else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW) { - if (m_required_data_plate_data_list.size() > 0) { source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id; } - } - - if (source_model != target_model) { - std::vector compatible_machine = obj_->get_compatible_machine(); - vector::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); - if (it == compatible_machine.end()) { return true; } - } - - return false; + std::string source_model; + if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty()) + source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id; + return wxGetApp().is_blocking_printing(obj_, source_model); } bool SyncAmsInfoDialog::is_same_nozzle_type(std::string &filament_type, NozzleType &tag_nozzle_type) @@ -1934,7 +1923,13 @@ bool SyncAmsInfoDialog::is_same_printer_model() if (obj_ == nullptr) { return result; } PresetBundle *preset_bundle = wxGetApp().preset_bundle; - if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) != obj_->printer_type) { + const std::string source_model = preset_bundle ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) : std::string(); + if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) || + DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type)) { + return true; + } + + if (preset_bundle && source_model != obj_->printer_type) { if ((obj_->is_support_upgrade_kit && obj_->installed_upgrade_kit) && (preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == "C12")) { return true; } @@ -2135,7 +2130,7 @@ void SyncAmsInfoDialog::update_user_printer() std::map option_list; // user machine list - option_list = dev->get_my_machine_list(); + option_list = dev->get_my_machine_list(dev->get_current_printer_agent_id()); // same machine only appear once for (auto it = option_list.begin(); it != option_list.end(); it++) { @@ -2268,6 +2263,18 @@ void SyncAmsInfoDialog::update_show_status() reset_timeout(); + bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type); + if (m_print_type == PrintFromType::FROM_NORMAL) { + if (PresetBundle *preset_bundle = wxGetApp().preset_bundle) { + has_optional_printer_model = has_optional_printer_model || + DevPrinterConfigUtil::is_optional_printer_model_id( + preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle)); + } + } else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty()) { + has_optional_printer_model = has_optional_printer_model || + DevPrinterConfigUtil::is_optional_printer_model_id(m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id); + } + if (!obj_->GetConfig()->SupportPrintAllPlates() && m_print_plate_idx == PLATE_ALL_IDX) { show_status(PrintDialogStatus::PrintStatusNotSupportedPrintAll); return; @@ -2361,6 +2368,9 @@ void SyncAmsInfoDialog::update_show_status() } } } + + if (has_optional_printer_model) + show_status(PrintDialogStatus::PrintStatusOptionalPrinterModel); } bool SyncAmsInfoDialog::has_timelapse_warning() @@ -3210,6 +3220,7 @@ SyncAmsInfoDialog::~SyncAmsInfoDialog() { void SyncAmsInfoDialog::set_info(SyncInfo &info) { m_input_info = info; + reinit_dialog(); } void SyncAmsInfoDialog::update_lan_machine_list() diff --git a/src/slic3r/GUI/WebMediaController.cpp b/src/slic3r/GUI/WebMediaController.cpp new file mode 100644 index 0000000000..fe4b94d3ac --- /dev/null +++ b/src/slic3r/GUI/WebMediaController.cpp @@ -0,0 +1,90 @@ +#include "WebMediaController.hpp" + +#include + +namespace Slic3r { namespace GUI { + +namespace { + +void initialize_webview(wxWebView* webview) +{ + if (!webview) + return; + + webview->SetBackgroundColour(*wxBLACK); + webview->SetPage("", ""); +} + +} // namespace + +WebMediaController::WebMediaController(wxWebView* webview) + : m_webview(webview) +{ + initialize_webview(m_webview); +} + +void WebMediaController::Load(wxURI url) +{ + m_url = url.BuildURI().ToStdString(); +} + +void WebMediaController::set_mode(CameraStreamMode mode) +{ + m_stream_mode = mode; +} + +void WebMediaController::Play() +{ + if (!m_webview) + return; + + wxString url = wxString::FromUTF8(m_url); + wxString html = ""; + m_webview->SetPage(html, url); + } else { + // Load MJPEG streams as the top-level document. Some embedded WebView + // backends buffer a multipart stream when it is used as an resource, + // which introduces noticeable live-view latency. + m_webview->LoadURL(url); + } +} + +void WebMediaController::Stop() +{ + if (m_webview) { + m_webview->RunScript("if(typeof stopCameraRefresh==='function') stopCameraRefresh();"); + m_webview->Stop(); + m_webview->SetPage("", "about:blank"); + m_webview->ClearHistory(); + } + m_url.clear(); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/WebMediaController.hpp b/src/slic3r/GUI/WebMediaController.hpp new file mode 100644 index 0000000000..e7756d8977 --- /dev/null +++ b/src/slic3r/GUI/WebMediaController.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include + +class wxWebView; + +namespace Slic3r { namespace GUI { + +class WebMediaController : public IMediaController +{ +public: + explicit WebMediaController(wxWebView* webview); + + void Load(wxURI url) override; + + void set_mode(CameraStreamMode mode); + + void Play() override; + + void Stop() override; + +private: + wxWebView* m_webview = nullptr; + std::string m_url; + CameraStreamMode m_stream_mode = CameraStreamMode::http; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/WebRtcMediaController.cpp b/src/slic3r/GUI/WebRtcMediaController.cpp new file mode 100644 index 0000000000..9c03c7c8b1 --- /dev/null +++ b/src/slic3r/GUI/WebRtcMediaController.cpp @@ -0,0 +1,401 @@ +#include "WebRtcMediaController.hpp" + +#include +#include + +#include +#include + +#include + +namespace { +void init_rtc_logger_once() +{ + static std::once_flag flag; + std::call_once(flag, [] { + rtc::InitLogger(rtc::LogLevel::Warning, [](rtc::LogLevel level, std::string message) { + BOOST_LOG_TRIVIAL(trace) << "[rtc:" << static_cast(level) << "] " << message; + }); + }); +} +} // namespace + +namespace Slic3r { namespace GUI { + +WebRtcMediaController::WebRtcMediaController(std::function frame_sink, + std::function on_status) + : m_frame_sink(std::move(frame_sink)) + , m_on_status(std::move(on_status)) +{ +} + +WebRtcMediaController::~WebRtcMediaController() +{ + Stop(); +} + +void WebRtcMediaController::report(Status status) +{ + status.epoch = m_epoch.load(); + BOOST_LOG_TRIVIAL(info) << "WebRTC: report kind=" << static_cast(status.kind) + << " code=" << static_cast(status.code) << " epoch=" << status.epoch; + { + std::lock_guard lock(m_mutex); + if (status.kind == Status::Connecting) + m_state = static_cast(4); + else if (status.kind == Status::Playing) + m_state = wxMEDIASTATE_PLAYING; + else + m_state = static_cast(3); + } + if (m_on_status) + m_on_status(status); +} + +void WebRtcMediaController::set_signalling_channel(std::unique_ptr channel) +{ + m_pending_signaling = std::move(channel); +} + +void WebRtcMediaController::Play() +{ + std::unique_ptr 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. + teardown(false); + if (!channel) + return; + + m_epoch.fetch_add(1); + m_alive.store(true); + { + std::lock_guard lock(m_mutex); + m_signaling = std::move(channel); + m_jpeg_queue.clear(); + m_pending_candidates.clear(); + m_remote_description_set = false; + m_video_size = wxDefaultSize; + m_has_frame = false; + m_last_frame_time = {}; + } + + ICameraSignalingChannel* signaling = nullptr; + { + std::lock_guard lock(m_mutex); + signaling = m_signaling.get(); + } + signaling->on_ready = [this](std::vector servers) { + if (m_alive.load()) + on_ready(std::move(servers)); + }; + signaling->on_answer = [this](std::string sdp) { + if (m_alive.load()) + on_answer(std::move(sdp)); + }; + signaling->on_ice = [this](std::string candidate, std::string mid) { + if (m_alive.load()) + on_ice(std::move(candidate), std::move(mid)); + }; + signaling->on_unavailable = [this](CameraUnavailableReason reason, std::string detail) { + if (m_alive.load()) + on_unavailable(reason, std::move(detail)); + }; + + m_decode_thread = std::thread([this] { decode_loop(); }); + report({Status::Connecting}); + signaling->open(); +} + +void WebRtcMediaController::Stop() +{ + teardown(true); +} + +void WebRtcMediaController::teardown(bool notify) +{ + const bool was_alive = m_alive.exchange(false); + if (!was_alive && !m_decode_thread.joinable()) + return; + + m_cond.notify_all(); + std::unique_ptr signaling; + std::shared_ptr peer_connection; + { + std::lock_guard lock(m_mutex); + signaling = std::move(m_signaling); + peer_connection = std::move(m_peer_connection); + m_data_channel.reset(); + } + if (signaling) + signaling->close(); + if (peer_connection) + peer_connection->close(); + if (m_decode_thread.joinable()) + m_decode_thread.join(); + { + std::lock_guard lock(m_mutex); + m_jpeg_queue.clear(); + } + if (was_alive && notify) + report({Status::Stopped}); +} + +wxMediaState WebRtcMediaController::GetState() +{ + std::lock_guard lock(m_mutex); + return m_state; +} + +wxSize WebRtcMediaController::GetVideoSize() const +{ + std::lock_guard lock(m_mutex); + return m_video_size; +} + +void WebRtcMediaController::bind_data_channel(const std::shared_ptr& dc) +{ + const std::string label = dc->label(); + dc->onOpen([label] { BOOST_LOG_TRIVIAL(info) << "WebRTC: data channel '" << label << "' open"; }); + dc->onClosed([label] { BOOST_LOG_TRIVIAL(info) << "WebRTC: data channel '" << label << "' closed"; }); + dc->onError([label](std::string e) { + BOOST_LOG_TRIVIAL(warning) << "WebRTC: data channel '" << label << "' error: " << e; + }); + dc->onMessage( + [this](rtc::binary data) { + if (m_alive.load()) + enqueue_jpeg(std::vector(data.begin(), data.end())); + }, + [](rtc::string) {}); +} + +void WebRtcMediaController::on_ready(std::vector servers) +{ + init_rtc_logger_once(); + rtc::Configuration configuration; + // Allow complete-JPEG DataChannel messages up to 1 MiB. This value is + // advertised in SDP and becomes the upper bound for frames OrcaSonar can + // send to OrcaSlicer. + configuration.maxMessageSize = 1024 * 1024; + for (const CameraIceServer& server : servers) { + try { + rtc::IceServer ice_server(server.urls); + ice_server.username = server.username; + ice_server.password = server.credential; + configuration.iceServers.emplace_back(std::move(ice_server)); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "WebRTC: invalid ICE server: " << e.what(); + } + } + + BOOST_LOG_TRIVIAL(info) << "WebRTC: creating peer connection with " << configuration.iceServers.size() << " ice servers"; + auto peer_connection = std::make_shared(std::move(configuration)); + peer_connection->onLocalDescription([this](rtc::Description description) { + if (!m_alive.load()) + return; + const std::string sdp(description); + BOOST_LOG_TRIVIAL(info) << "WebRTC: local description ready (" << description.typeString() + << "), OFFER SDP:\n" << sdp; + std::lock_guard lock(m_mutex); + if (m_signaling) + m_signaling->send_offer(sdp); + }); + peer_connection->onLocalCandidate([this](rtc::Candidate candidate) { + if (!m_alive.load()) + return; + std::lock_guard lock(m_mutex); + if (m_signaling) + m_signaling->send_ice(std::string(candidate), candidate.mid()); + }); + peer_connection->onStateChange([this](rtc::PeerConnection::State state) { + BOOST_LOG_TRIVIAL(info) << "WebRTC: peer state -> " << static_cast(state); + if (!m_alive.load()) + return; + if (state == rtc::PeerConnection::State::Failed || state == rtc::PeerConnection::State::Disconnected) + report({Status::Failed, Status::ICE_FAILED}); + }); + peer_connection->onGatheringStateChange([](rtc::PeerConnection::GatheringState state) { + BOOST_LOG_TRIVIAL(info) << "WebRTC: gathering state -> " << static_cast(state); + }); + + // Accept a DataChannel opened by the remote peer (OrcaSonar may create the + // "camera" channel from its side rather than answering the one we offer). + peer_connection->onDataChannel([this](std::shared_ptr dc) { + BOOST_LOG_TRIVIAL(info) << "WebRTC: remote opened data channel '" << dc->label() << "'"; + bind_data_channel(dc); + std::lock_guard lock(m_mutex); + m_data_channel = std::move(dc); + }); + + rtc::DataChannelInit init; + init.reliability.unordered = true; + // init.reliability.maxPacketLifeTime = std::chrono::milliseconds(350); + // Request one complete JPEG frame per DataChannel message. OrcaSonar + // keeps the legacy chunked protocol for clients that omit this property. + init.protocol = "orca-jpeg"; + auto data_channel = peer_connection->createDataChannel("camera", init); + if (data_channel) + bind_data_channel(data_channel); + + // Camera media is carried as one complete JPEG per DataChannel message; + // no RTP video track or application-level framing is required. + + std::shared_ptr peer_for_description; + { + std::lock_guard lock(m_mutex); + if (!m_alive.load()) + return; + m_peer_connection = std::move(peer_connection); + peer_for_description = m_peer_connection; + m_data_channel = std::move(data_channel); + } + if (peer_for_description) + peer_for_description->setLocalDescription(); +} + +void WebRtcMediaController::on_answer(std::string sdp) +{ + std::shared_ptr peer_connection; + { + std::lock_guard lock(m_mutex); + peer_connection = m_peer_connection; + } + BOOST_LOG_TRIVIAL(info) << "WebRTC: applying remote answer (" << sdp.size() << " bytes), ANSWER SDP:\n" << sdp; + if (!peer_connection) + return; + try { + peer_connection->setRemoteDescription(rtc::Description(sdp, "answer")); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "WebRTC: setRemoteDescription failed: " << e.what(); + report({Status::Failed, Status::ICE_FAILED}); + return; + } + + // Flush any remote candidates that arrived before the answer. + std::vector> pending; + { + std::lock_guard lock(m_mutex); + m_remote_description_set = true; + pending.swap(m_pending_candidates); + } + BOOST_LOG_TRIVIAL(info) << "WebRTC: remote description set, flushing " << pending.size() + << " buffered candidate(s)"; + for (const auto& c : pending) { + try { + peer_connection->addRemoteCandidate(rtc::Candidate(c.first, c.second)); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "WebRTC: addRemoteCandidate (buffered) failed: " << e.what(); + } + } +} + +void WebRtcMediaController::on_ice(std::string candidate, std::string mid) +{ + std::shared_ptr peer_connection; + { + std::lock_guard lock(m_mutex); + if (!m_remote_description_set) { + m_pending_candidates.emplace_back(std::move(candidate), std::move(mid)); + return; + } + peer_connection = m_peer_connection; + } + if (!peer_connection) + return; + try { + peer_connection->addRemoteCandidate(rtc::Candidate(candidate, mid)); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "WebRTC: addRemoteCandidate failed: " << e.what(); + } +} + +void WebRtcMediaController::on_unavailable(CameraUnavailableReason reason, std::string detail) +{ + BOOST_LOG_TRIVIAL(warning) << "WebRTC camera unavailable: " << detail; + Status::Code code = Status::UNAVAILABLE_ERROR; + if (reason == CameraUnavailableReason::Busy) + code = Status::UNAVAILABLE_BUSY; + else if (reason == CameraUnavailableReason::Disabled) + code = Status::UNAVAILABLE_DISABLED; + else if (reason == CameraUnavailableReason::Closed) + code = Status::SIGNALING_CLOSED; + report({Status::Failed, code}); +} + +void WebRtcMediaController::enqueue_jpeg(std::vector jpeg) +{ + std::lock_guard lock(m_mutex); + if (m_jpeg_queue.size() >= 4) + m_jpeg_queue.pop_front(); + m_jpeg_queue.emplace_back(std::move(jpeg)); + m_cond.notify_one(); +} + +void WebRtcMediaController::deliver_jpeg(std::vector jpeg) +{ + const auto now = std::chrono::steady_clock::now(); + { + std::lock_guard lock(m_mutex); + if (m_last_frame_time != std::chrono::steady_clock::time_point{} && + now - m_last_frame_time < std::chrono::milliseconds(33)) + return; + m_last_frame_time = now; + } + wxMemoryInputStream stream(jpeg.data(), jpeg.size()); + wxImage image; + if (!image.LoadFile(stream, wxBITMAP_TYPE_JPEG)) { + report({Status::Failed, Status::DECODE_ERROR}); + return; + } + bool first_frame = false; + { + std::lock_guard lock(m_mutex); + m_video_size = image.GetSize(); + first_frame = !m_has_frame; + m_has_frame = true; + } + if (m_frame_sink) + m_frame_sink(image, image.GetSize()); + if (first_frame) + report({Status::Playing}); +} + +void WebRtcMediaController::decode_loop() +{ + int stall_polls = 0; + std::unique_lock lock(m_mutex); + while (m_alive.load()) { + const bool woke = m_cond.wait_for(lock, std::chrono::seconds(2), [this] { + return !m_alive.load() || !m_jpeg_queue.empty(); + }); + if (!m_alive.load()) + break; + if (!woke && !m_has_frame) { + const int pc_state = m_peer_connection ? static_cast(m_peer_connection->state()) : -1; + std::string dc = "none"; + if (m_data_channel) + dc = "label='" + m_data_channel->label() + "' open=" + + (m_data_channel->isOpen() ? "1" : "0"); + lock.unlock(); + BOOST_LOG_TRIVIAL(info) << "WebRTC: waiting for frames; peer_state=" << pc_state + << " data_channel=" << dc; + if (++stall_polls >= 8) { // ~16s connected with no frame -> give up so the UI can retry + report({Status::Failed, Status::TIMEOUT}); + lock.lock(); + break; + } + lock.lock(); + continue; + } + stall_polls = 0; + if (!m_jpeg_queue.empty()) { + auto jpeg = std::move(m_jpeg_queue.front()); + m_jpeg_queue.pop_front(); + lock.unlock(); + deliver_jpeg(std::move(jpeg)); + lock.lock(); + } + } +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/WebRtcMediaController.hpp b/src/slic3r/GUI/WebRtcMediaController.hpp new file mode 100644 index 0000000000..7a2ab02abb --- /dev/null +++ b/src/slic3r/GUI/WebRtcMediaController.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include "IMediaController.hpp" +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rtc { +class DataChannel; +class PeerConnection; +} + +namespace Slic3r { namespace GUI { + +class WebRtcMediaController : public IMediaController { +public: + struct Status { + enum Kind { Connecting, Playing, Stopped, Failed } kind = Stopped; + enum Code { + ICE_FAILED, + SIGNALING_CLOSED, + UNAVAILABLE_BUSY, + UNAVAILABLE_ERROR, + UNAVAILABLE_DISABLED, + DECODE_ERROR, + TIMEOUT, + } code = ICE_FAILED; + // Identifies the Play attempt this status belongs to, so the + // consumer can drop CallAfter-queued events from a superseded attempt. + std::uint64_t epoch = 0; + }; + + WebRtcMediaController(std::function frame_sink, + std::function on_status); + ~WebRtcMediaController() override; + + void set_signalling_channel(std::unique_ptr channel); + 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; + wxMediaState GetState() override; + wxSize GetVideoSize() const override; + +private: + void teardown(bool notify); + void report(Status status); + void bind_data_channel(const std::shared_ptr& dc); + void on_ready(std::vector servers); + void on_answer(std::string sdp); + void on_ice(std::string candidate, std::string mid); + void on_unavailable(CameraUnavailableReason reason, std::string detail); + void decode_loop(); + void enqueue_jpeg(std::vector jpeg); + void deliver_jpeg(std::vector jpeg); + + mutable std::mutex m_mutex; + std::condition_variable m_cond; + std::deque> m_jpeg_queue; + // Remote candidates can arrive before the answer; libdatachannel rejects + // addRemoteCandidate until a remote description is set, so buffer them. + std::vector> m_pending_candidates; + bool m_remote_description_set = false; + std::unique_ptr m_pending_signaling; + std::unique_ptr m_signaling; + std::shared_ptr m_peer_connection; + std::shared_ptr m_data_channel; + std::thread m_decode_thread; + std::atomic m_alive{false}; + std::atomic m_epoch{0}; + wxMediaState m_state = static_cast(3); + wxSize m_video_size = wxDefaultSize; + std::function m_frame_sink; + std::function m_on_status; + bool m_has_frame = false; + std::chrono::steady_clock::time_point m_last_frame_time{}; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/wxMediaCtrl3.cpp b/src/slic3r/GUI/wxMediaCtrl3.cpp index 098db68808..58e258d7f3 100644 --- a/src/slic3r/GUI/wxMediaCtrl3.cpp +++ b/src/slic3r/GUI/wxMediaCtrl3.cpp @@ -4,6 +4,14 @@ #include "libslic3r/Utils.hpp" #include #include +#include +#include +#include +#include +extern "C" { +#include +#include +} #ifdef __WIN32__ #include #include @@ -43,9 +51,13 @@ wxMediaCtrl3::~wxMediaCtrl3() m_thread.join(); } +static void adjust_frame_size(wxSize& frame, wxSize const& video, wxSize const& window); + void wxMediaCtrl3::Load(wxURI url) { std::unique_lock lk(m_mutex); + if (m_external) + return; m_video_size = wxDefaultSize; m_error = 0; m_url.reset(new wxURI(url)); @@ -55,6 +67,8 @@ void wxMediaCtrl3::Load(wxURI url) void wxMediaCtrl3::Play() { std::unique_lock lk(m_mutex); + if (m_external) + return; if (m_state != wxMEDIASTATE_PLAYING) { m_state = wxMEDIASTATE_PLAYING; wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); @@ -74,6 +88,62 @@ void wxMediaCtrl3::Stop() Refresh(); } +void wxMediaCtrl3::SetExternalFrame(const wxImage& frame, wxSize videoSize) +{ + if (!frame.IsOk()) + return; + { + std::unique_lock lk(m_mutex); + if (!m_external) + return; + m_frame = frame; + m_video_size = videoSize.IsFullySpecified() ? videoSize : frame.GetSize(); + adjust_frame_size(m_frame_size, m_video_size, GetSize()); + } + CallAfter([this] { Refresh(); }); +} + +#ifdef _WIN32 +void wxMediaCtrl3::SetExternalFrame(const wxBitmap& frame, wxSize videoSize) +{ + if (!frame.IsOk()) + return; + { + std::unique_lock lk(m_mutex); + if (!m_external) + return; + m_frame = frame; + m_video_size = videoSize.IsFullySpecified() ? videoSize : frame.GetSize(); + adjust_frame_size(m_frame_size, m_video_size, GetSize()); + } + CallAfter([this] { Refresh(); }); +} +#endif + +void wxMediaCtrl3::BeginExternalStream() +{ + std::unique_lock lk(m_mutex); + m_external = true; + m_url.reset(); + m_active_url.reset(); + m_video_size = wxDefaultSize; + m_frame = wxImage(m_idle_image); + m_cond.notify_all(); + Refresh(); +} + +void wxMediaCtrl3::EndExternalStream() +{ + std::unique_lock lk(m_mutex); + m_external = false; + m_url.reset(); + m_active_url.reset(); + m_video_size = wxDefaultSize; + m_frame = wxImage(m_idle_image); + m_cond.notify_all(); + Refresh(); +} + void wxMediaCtrl3::SetIdleImage(wxString const &image) { if (m_idle_image == image) @@ -93,13 +163,13 @@ wxMediaState wxMediaCtrl3::GetState() return m_state; } -int wxMediaCtrl3::GetLastError() +int wxMediaCtrl3::GetLastError() const { std::unique_lock lk(m_mutex); return m_error; } -wxSize wxMediaCtrl3::GetVideoSize() +wxSize wxMediaCtrl3::GetVideoSize() const { std::unique_lock lk(m_mutex); return m_video_size; @@ -185,6 +255,210 @@ void wxMediaCtrl3::bambu_log(void *ctx, int level, tchar const *msg2) BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data(); } +// FFmpeg's own diagnostics (HTTP status, "Invalid data found", demuxer choice, +// missing stream dimensions, ...) are otherwise swallowed: a failed camera open +// only surfaces as wxMediaCtrl3's generic error code, which MediaPlayCtrl maps to +// the misleading "Player is malfunctioning" string. Forward them to the Orca log +// instead. Verbosity defaults to AV_LOG_VERBOSE and can be raised at runtime with +// ORCA_FFMPEG_LOG_LEVEL=debug|trace|... (or lowered to warning/error/quiet). +static int ffmpeg_log_level_from_env() +{ + const char *env = std::getenv("ORCA_FFMPEG_LOG_LEVEL"); + if (env == nullptr || *env == '\0') + return AV_LOG_VERBOSE; + const wxString v = wxString(env).Lower(); + if (v == "quiet") return AV_LOG_QUIET; + if (v == "panic") return AV_LOG_PANIC; + if (v == "fatal") return AV_LOG_FATAL; + if (v == "error") return AV_LOG_ERROR; + if (v == "warning") return AV_LOG_WARNING; + if (v == "info") return AV_LOG_INFO; + if (v == "verbose") return AV_LOG_VERBOSE; + if (v == "debug") return AV_LOG_DEBUG; + if (v == "trace") return AV_LOG_TRACE; + return AV_LOG_VERBOSE; +} + +static void ffmpeg_log_callback(void *avcl, int level, const char *fmt, va_list vl) +{ + if (level > av_log_get_level()) + return; + thread_local int print_prefix = 1; + char line[1024]; + av_log_format_line2(avcl, level, fmt, vl, line, (int) sizeof(line), &print_prefix); + size_t len = std::strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r' || line[len - 1] == ' ')) + line[--len] = '\0'; + if (len == 0) + return; + if (level <= AV_LOG_ERROR) + BOOST_LOG_TRIVIAL(error) << "ffmpeg: " << line; + else if (level <= AV_LOG_WARNING) + BOOST_LOG_TRIVIAL(warning) << "ffmpeg: " << line; + else if (level <= AV_LOG_INFO) + BOOST_LOG_TRIVIAL(info) << "ffmpeg: " << line; + else + BOOST_LOG_TRIVIAL(debug) << "ffmpeg: " << line; +} + +static void install_ffmpeg_logger() +{ + av_log_set_level(ffmpeg_log_level_from_env()); + av_log_set_callback(&ffmpeg_log_callback); +} + +int wxMediaCtrl3::ffmpeg_interrupt_callback(void *opaque) +{ + auto *ctrl = static_cast(opaque); + std::lock_guard lock(ctrl->m_mutex); + return ctrl->m_url != ctrl->m_active_url; +} + +int wxMediaCtrl3::PlayFfmpeg(std::shared_ptr const &url, std::unique_lock &lock) +{ + static std::once_flag logger_once; + std::call_once(logger_once, install_ffmpeg_logger); + + if (avformat_network_init() < 0) + return 2; + + AVFormatContext *format_context = avformat_alloc_context(); + if (!format_context) { + avformat_network_deinit(); + return 2; + } + + format_context->interrupt_callback = {&wxMediaCtrl3::ffmpeg_interrupt_callback, this}; + format_context->flags |= AVFMT_FLAG_NOBUFFER; + format_context->max_delay = 0; + m_active_url = url; + + auto finish = [&](int error) { + lock.unlock(); + avformat_close_input(&format_context); + avformat_network_deinit(); + lock.lock(); + m_active_url.reset(); + return error; + }; + + const std::string uri = url->BuildURI().ToUTF8().data(); + const wxString scheme = url->GetScheme(); + const bool http_stream = scheme.CmpNoCase("http") == 0 || scheme.CmpNoCase("https") == 0; + AVDictionary *options = nullptr; + if (http_stream) { + // Live multipart MJPEG. fflags=nobuffer / AVFMT_FLAG_NOBUFFER / max_delay=0 + // (set above) are the low-latency levers - they disable the demuxer + // read-ahead queue. probesize / analyzeduration only bound the one-off + // avformat_find_stream_info() at open; a 32-byte budget returned before a + // whole JPEG frame was seen, so width/height came back unset and the open + // was rejected. Give it room to identify one frame (a startup cost only). + // rw_timeout / timeout bound a wedged connect or read so a stale stream + // fails fast and is retried, instead of the reader thread hanging. + // avioflags=direct is deliberately NOT set: unbuffered reads make the + // mpjpeg demuxer emit "Packet corrupt" and bail on any short read across + // a multipart boundary. + av_dict_set(&options, "fflags", "nobuffer", 0); + av_dict_set(&options, "probesize", "5000000", 0); + av_dict_set(&options, "analyzeduration", "1000000", 0); + av_dict_set(&options, "rw_timeout", "5000000", 0); + av_dict_set(&options, "timeout", "5000000", 0); + } else { + av_dict_set(&options, "rtsp_transport", "tcp", 0); + } + lock.unlock(); + int error = avformat_open_input(&format_context, uri.c_str(), nullptr, &options); + av_dict_free(&options); + lock.lock(); + if (error < 0) + return finish(2); + + lock.unlock(); + error = avformat_find_stream_info(format_context, nullptr); + lock.lock(); + if (error < 0) + return finish(2); + + const int video_stream = av_find_best_stream(format_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); + if (video_stream < 0) + return finish(2); + + AVVideoDecoder decoder; + if (decoder.open(*format_context->streams[video_stream]->codecpar) < 0) + return finish(2); + + // Prefer the dimensions the container reported. A small probe budget, or a + // camera that doesn't announce a size up front, can leave these unset - in + // that case fill them in from the first frame that decodes (below) rather + // than failing the open outright. + auto apply_video_size = [&](wxSize size) { + if (!size.IsFullySpecified() || size.x <= 0 || size.y <= 0) + return false; + m_video_size = size; + adjust_frame_size(m_frame_size, m_video_size, GetSize()); + NotifyStopped(); + return true; + }; + bool have_size = apply_video_size({format_context->streams[video_stream]->codecpar->width, + format_context->streams[video_stream]->codecpar->height}); + int size_probe_frames = 0; // frames spent still waiting for a usable size + + AVPacket *packet = av_packet_alloc(); + if (!packet) + return finish(2); + + while (m_url == url) { + lock.unlock(); + error = av_read_frame(format_context, packet); + lock.lock(); + if (m_url != url) + break; + if (error < 0) + break; + if (packet->stream_index == video_stream) { + const int decode_error = decoder.decode(*packet); + if (decode_error == 0) { + if (!have_size) { + have_size = apply_video_size(decoder.decoded_frame_size()); + if (!have_size) { + av_packet_unref(packet); + // MJPEG yields a sized frame on the first full packet; if + // several seconds of frames never do, treat it as a bad + // stream instead of sitting in "Loading..." forever. + if (++size_probe_frames > 120) + break; + continue; + } + } + auto frame_size = m_frame_size; + lock.unlock(); +#ifdef _WIN32 + wxBitmap frame; + decoder.toWxBitmap(frame, frame_size); +#else + wxImage frame; + decoder.toWxImage(frame, frame_size); +#endif + lock.lock(); + if (m_url != url) + break; + if (frame.IsOk()) + m_frame = frame; + if (!m_refresh_pending.exchange(true)) { + CallAfter([this] { + m_refresh_pending.store(false); + Refresh(); + }); + } + } + } + av_packet_unref(packet); + } + + av_packet_free(&packet); + return finish(m_url == url ? 2 : 1); +} + void wxMediaCtrl3::PlayThread() { using namespace std::chrono_literals; @@ -197,42 +471,22 @@ void wxMediaCtrl3::PlayThread() continue; if (!url->HasScheme()) break; - lk.unlock(); - Bambu_Tunnel tunnel = nullptr; - int error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8()); - if (error == 0) { - Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this); - error = Bambu_Open(tunnel); - if (error == 0) - error = Bambu_would_block; - } - lk.lock(); - while (error == int(Bambu_would_block)) { - m_cond.wait_for(lk, 100ms); - if (m_url != url) { - error = 1; - break; + const wxString scheme = url->GetScheme(); + const bool generic_ffmpeg = scheme.CmpNoCase("http") == 0 || scheme.CmpNoCase("https") == 0 || + scheme.CmpNoCase("rtsp") == 0 || scheme.CmpNoCase("rtsps") == 0; + int error = 0; + if (generic_ffmpeg) { + error = PlayFfmpeg(url, lk); + } else { + lk.unlock(); + Bambu_Tunnel tunnel = nullptr; + error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8()); + if (error == 0) { + Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this); + error = Bambu_Open(tunnel); + if (error == 0) + error = Bambu_would_block; } - lk.unlock(); - error = Bambu_StartStream(tunnel, true); - lk.lock(); - } - Bambu_StreamInfo info; - if (error == 0) - error = Bambu_GetStreamInfo(tunnel, 0, &info); - AVVideoDecoder decoder; - int minFrameDuration = 0; - if (error == 0) { - decoder.open(info); - m_video_size = { info.format.video.width, info.format.video.height }; - adjust_frame_size(m_frame_size, m_video_size, GetSize()); - minFrameDuration = 800 / info.format.video.frame_rate; // 80% - NotifyStopped(); - } - Bambu_Sample sample; - while (error == 0) { - lk.unlock(); - error = Bambu_ReadSample(tunnel, &sample); lk.lock(); while (error == int(Bambu_would_block)) { m_cond.wait_for(lk, 100ms); @@ -240,59 +494,87 @@ void wxMediaCtrl3::PlayThread() error = 1; break; } + lk.unlock(); + error = Bambu_StartStream(tunnel, true); + lk.lock(); + } + Bambu_StreamInfo info; + if (error == 0) + error = Bambu_GetStreamInfo(tunnel, 0, &info); + AVVideoDecoder decoder; + int minFrameDuration = 0; + if (error == 0) { + decoder.open(info); + m_video_size = { info.format.video.width, info.format.video.height }; + adjust_frame_size(m_frame_size, m_video_size, GetSize()); + minFrameDuration = 800 / info.format.video.frame_rate; // 80% + NotifyStopped(); + } + Bambu_Sample sample; + while (error == 0) { lk.unlock(); error = Bambu_ReadSample(tunnel, &sample); lk.lock(); - } - if (error == 0) { - auto frame_size = m_frame_size; - lk.unlock(); - decoder.decode(sample); -#ifdef _WIN32 - wxBitmap bm; - decoder.toWxBitmap(bm, frame_size); -#else - wxImage bm; - decoder.toWxImage(bm, frame_size); -#endif - lk.lock(); - if (m_url != url) { - error = 1; - break; - } - if (bm.IsOk()) { - auto now = std::chrono::system_clock::now(); - if (m_last_PTS && (sample.decode_time - m_last_PTS) < 30000000ULL) { // 3s - auto next_PTS_expected = m_last_PTS_expected + std::chrono::milliseconds((sample.decode_time - m_last_PTS) / 10000ULL); - // The frame is late, catch up a little - auto next_PTS_practical = m_last_PTS_practical + std::chrono::milliseconds(minFrameDuration); - auto next_PTS = std::max(next_PTS_expected, next_PTS_practical); - if(now < next_PTS) - std::this_thread::sleep_until(next_PTS); - else - next_PTS = now; - //auto text = wxString::Format(L"wxMediaCtrl3 pts diff %ld\n", std::chrono::duration_cast(next_PTS - next_PTS_expected).count()); - //OutputDebugString(text); - m_last_PTS = sample.decode_time; - m_last_PTS_expected = next_PTS_expected; - m_last_PTS_practical = next_PTS; - } else { - // Resync - m_last_PTS = sample.decode_time; - m_last_PTS_expected = now; - m_last_PTS_practical = now; + while (error == int(Bambu_would_block)) { + m_cond.wait_for(lk, 100ms); + if (m_url != url) { + error = 1; + break; } - m_frame = bm; + lk.unlock(); + error = Bambu_ReadSample(tunnel, &sample); + lk.lock(); + } + if (error == 0) { + auto frame_size = m_frame_size; + lk.unlock(); + decoder.decode(sample); +#ifdef _WIN32 + wxBitmap bm; + decoder.toWxBitmap(bm, frame_size); +#else + wxImage bm; + decoder.toWxImage(bm, frame_size); +#endif + lk.lock(); + if (m_url != url) { + error = 1; + break; + } + if (bm.IsOk()) { + auto now = std::chrono::system_clock::now(); + if (m_last_PTS && (sample.decode_time - m_last_PTS) < 30000000ULL) { // 3s + auto next_PTS_expected = m_last_PTS_expected + std::chrono::milliseconds((sample.decode_time - m_last_PTS) / 10000ULL); + // The frame is late, catch up a little + auto next_PTS_practical = m_last_PTS_practical + std::chrono::milliseconds(minFrameDuration); + auto next_PTS = std::max(next_PTS_expected, next_PTS_practical); + if(now < next_PTS) + std::this_thread::sleep_until(next_PTS); + else + next_PTS = now; + //auto text = wxString::Format(L"wxMediaCtrl3 pts diff %ld\n", std::chrono::duration_cast(next_PTS - next_PTS_expected).count()); + //OutputDebugString(text); + m_last_PTS = sample.decode_time; + m_last_PTS_expected = next_PTS_expected; + m_last_PTS_practical = next_PTS; + } else { + // Resync + m_last_PTS = sample.decode_time; + m_last_PTS_expected = now; + m_last_PTS_practical = now; + } + m_frame = bm; + } + CallAfter([this] { Refresh(); }); } - CallAfter([this] { Refresh(); }); } - } - if (tunnel) { - lk.unlock(); - Bambu_Close(tunnel); - Bambu_Destroy(tunnel); - tunnel = nullptr; - lk.lock(); + if (tunnel) { + lk.unlock(); + Bambu_Close(tunnel); + Bambu_Destroy(tunnel); + tunnel = nullptr; + lk.lock(); + } } if (m_url == url) m_error = error; diff --git a/src/slic3r/GUI/wxMediaCtrl3.h b/src/slic3r/GUI/wxMediaCtrl3.h index ff4595ad32..936d5e5ddf 100644 --- a/src/slic3r/GUI/wxMediaCtrl3.h +++ b/src/slic3r/GUI/wxMediaCtrl3.h @@ -13,41 +13,51 @@ #include "wx/bitmap.h" #include "wx/uri.h" #include "wx/mediactrl.h" +#include "IMediaController.hpp" wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height); #define BAMBU_DYNAMIC +#include #include #include -#ifndef _WIN32 #include -#endif #include "Printer/BambuTunnel.h" class AVVideoDecoder; -class wxMediaCtrl3 : public wxWindow, BambuLib +class wxMediaCtrl3 : public wxWindow, public Slic3r::GUI::IMediaController, BambuLib { public: wxMediaCtrl3(wxWindow *parent); - ~wxMediaCtrl3(); + ~wxMediaCtrl3() override; - void Load(wxURI url); + void Load(wxURI url) override; - void Play(); + void Play() override; - void Stop(); + void Stop() override; + + // 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 + // this method returns. + void SetExternalFrame(const wxImage& frame, wxSize videoSize); +#ifdef _WIN32 + void SetExternalFrame(const wxBitmap& frame, wxSize videoSize); +#endif + void BeginExternalStream(); + void EndExternalStream(); void SetIdleImage(wxString const & image); - wxMediaState GetState(); + wxMediaState GetState() override; - int GetLastError(); + int GetLastError() const override; - wxSize GetVideoSize(); + wxSize GetVideoSize() const override; protected: DECLARE_EVENT_TABLE() @@ -59,8 +69,10 @@ protected: void DoSetSize(int x, int y, int width, int height, int sizeFlags) override; static void bambu_log(void *ctx, int level, tchar const *msg); + static int ffmpeg_interrupt_callback(void *opaque); void PlayThread(); + int PlayFfmpeg(std::shared_ptr const &url, std::unique_lock &lock); void NotifyStopped(); @@ -77,12 +89,15 @@ private: #endif std::shared_ptr m_url; + std::shared_ptr m_active_url; + bool m_external = false; 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; - std::mutex m_mutex; + mutable std::mutex m_mutex; std::condition_variable m_cond; std::thread m_thread; + std::atomic_bool m_refresh_pending{false}; }; #endif /* wxMediaCtrl3_h */ diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index 0c6225cc55..4e2015d5d7 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -1,15 +1,23 @@ #include "BBLPrinterAgent.hpp" #include "BBLNetworkPlugin.hpp" +#include "IPrinterAgent.hpp" #include "NetworkAgentFactory.hpp" +#include "NetworkAgent.hpp" #include "libslic3r/Utils.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/DeviceCore/DevManager.h" +#include #include #include #include +#include +#include using json = nlohmann::json; #include #include +#include namespace Slic3r { @@ -133,7 +141,7 @@ BBLPrinterAgent::~BBLPrinterAgent() = default; void BBLPrinterAgent::set_cloud_agent(std::shared_ptr cloud) { - m_cloud_agent = cloud; + (void) cloud; // BBL DLL manages tokens internally, so this is just for interface compliance } @@ -141,6 +149,94 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr cloud) // Communication // ============================================================================ +int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode) +{ + nlohmann::json j; + if (ams_id == -1) { + const std::string gcode = (boost::format("M620 R%1% \n") % slot_id).str(); + 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); + } + + j["print"]["command"] = "ams_get_rfid"; + j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); + j["print"]["ams_id"] = ams_id; + j["print"]["slot_id"] = slot_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 = (boost::format("M620 C%1% \n") % ams_id).str(); + 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 = (boost::format("M620 P%1% \n") % tray_id).str(); + 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_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 = -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); + if (rtn == 0) + BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn; + else + BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn; + return rtn; +} + int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) { json_str = from_orca_payload(std::move(json_str)); @@ -165,13 +261,23 @@ int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int return -1; } -int BBLPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +int BBLPrinterAgent::connect_printer(const PrinterConnectionParams& params) { auto& plugin = BBLNetworkPlugin::instance(); auto agent = plugin.get_agent(); auto func = plugin.get_connect_printer(); +#if !BBL_RELEASE_TO_PUBLIC + const bool use_ssl_for_mqtt = GUI::wxGetApp().app_config && + GUI::wxGetApp().app_config->get_bool("enable_ssl_for_mqtt"); +#else + bool use_ssl_for_mqtt = true; + if (auto* dev_manager = GUI::wxGetApp().getDeviceManager()) { + if (auto* machine = dev_manager->get_my_machine(params.dev_id)) + use_ssl_for_mqtt = machine->local_use_ssl; + } +#endif if (func && agent) { - return func(agent, dev_id, dev_ip, username, password, use_ssl); + return func(agent, params.dev_id, params.host, params.username, params.password, use_ssl_for_mqtt); } return -1; } diff --git a/src/slic3r/Utils/BBLPrinterAgent.hpp b/src/slic3r/Utils/BBLPrinterAgent.hpp index 83e21bb6d8..4398602976 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.hpp +++ b/src/slic3r/Utils/BBLPrinterAgent.hpp @@ -5,6 +5,7 @@ #include "ICloudServiceAgent.hpp" #include #include +#include namespace Slic3r { @@ -28,7 +29,13 @@ public: // Communication int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; - int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; + int command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_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_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; + std::string default_lan_username() const override { return "bblp"; } + int connect_printer(const PrinterConnectionParams& params) override; int disconnect_printer() override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; @@ -99,7 +106,7 @@ public: static std::string from_orca_payload(std::string json_text); private: - std::shared_ptr m_cloud_agent; + int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode); }; } // namespace Slic3r diff --git a/src/slic3r/Utils/CrealityPrintAgent.cpp b/src/slic3r/Utils/CrealityPrintAgent.cpp index f340a61267..4730903c59 100644 --- a/src/slic3r/Utils/CrealityPrintAgent.cpp +++ b/src/slic3r/Utils/CrealityPrintAgent.cpp @@ -282,7 +282,7 @@ bool CrealityPrintAgent::parse_cfs_response(const std::string& response, return true; } -bool CrealityPrintAgent::fetch_filament_info(std::string dev_id) +bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) { if (device_info.dev_ip.empty()) { BOOST_LOG_TRIVIAL(warning) diff --git a/src/slic3r/Utils/CrealityPrintAgent.hpp b/src/slic3r/Utils/CrealityPrintAgent.hpp index e1096db536..cf22d104ec 100644 --- a/src/slic3r/Utils/CrealityPrintAgent.hpp +++ b/src/slic3r/Utils/CrealityPrintAgent.hpp @@ -41,7 +41,7 @@ public: static AgentInfo get_agent_info_static(); AgentInfo get_agent_info() override { return get_agent_info_static(); } - bool fetch_filament_info(std::string dev_id) override; + bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; // Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into // a flat list of loaded slots, plus the count of CFS boxes the printer reports. diff --git a/src/slic3r/Utils/FileTransferUtils.cpp b/src/slic3r/Utils/FileTransferUtils.cpp index ebca741af5..822ccdf09d 100644 --- a/src/slic3r/Utils/FileTransferUtils.cpp +++ b/src/slic3r/Utils/FileTransferUtils.cpp @@ -224,4 +224,4 @@ void FileTransferJob::solve_result(ft_job_result result) res_json_.assign(result.json ? result.json : ""); } -} // namespace Slic3r \ No newline at end of file +} // namespace Slic3r diff --git a/src/slic3r/Utils/FileTransferUtils.hpp b/src/slic3r/Utils/FileTransferUtils.hpp index 037a1bef0c..ba91621a91 100644 --- a/src/slic3r/Utils/FileTransferUtils.hpp +++ b/src/slic3r/Utils/FileTransferUtils.hpp @@ -252,4 +252,4 @@ inline FileTransferModule &module() return *detail::g_mod; } -} // namespace Slic3r \ No newline at end of file +} // namespace Slic3r diff --git a/src/slic3r/Utils/ICameraSignalingChannel.hpp b/src/slic3r/Utils/ICameraSignalingChannel.hpp new file mode 100644 index 0000000000..c7971729b6 --- /dev/null +++ b/src/slic3r/Utils/ICameraSignalingChannel.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +namespace Slic3r { + +struct CameraIceServer { + std::string urls; + std::string username; + std::string credential; +}; + +enum class CameraUnavailableReason { + Busy, + Error, + Disabled, + Closed, +}; + +class ICameraSignalingChannel { +public: + virtual ~ICameraSignalingChannel() = default; + + virtual void open() = 0; + virtual void close() = 0; + virtual void send_offer(std::string sdp) = 0; + virtual void send_ice(std::string candidate, std::string mid) = 0; + + // These callbacks are invoked by the channel's worker thread. Consumers + // must marshal UI work to the GUI thread themselves. + std::function)> on_ready; + std::function on_answer; + std::function on_ice; + std::function on_unavailable; +}; + +} // namespace Slic3r diff --git a/src/slic3r/Utils/ICloudServiceAgent.hpp b/src/slic3r/Utils/ICloudServiceAgent.hpp index 7d326eb4c6..6457b3fda6 100644 --- a/src/slic3r/Utils/ICloudServiceAgent.hpp +++ b/src/slic3r/Utils/ICloudServiceAgent.hpp @@ -1,11 +1,11 @@ #ifndef __I_CLOUD_SERVICE_AGENT_HPP__ #define __I_CLOUD_SERVICE_AGENT_HPP__ +#include "ICameraSignalingChannel.hpp" #include "bambu_networking.hpp" #include "CloudProvider.hpp" #include "../../libslic3r/ProjectTask.hpp" #include -#include #include #include #include @@ -328,6 +328,17 @@ public: */ virtual int get_camera_url(std::string dev_id, std::function callback) = 0; + /** + * Create a camera signaling channel object for P2P camera streaming over WebRTC. + * + */ + virtual std::unique_ptr + create_camera_signaling_channel(const std::string& dev_id) + { + (void) dev_id; + return nullptr; + } + /** * Fetch staff-picked designs from model mall. */ diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 1f0d45dc1e..293b0af2b8 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -11,6 +11,11 @@ #define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability #include #include +#include +#include +#include +#include +#include namespace Slic3r { @@ -29,6 +34,17 @@ struct AgentInfo { std::string description; ///< Brief description of the agent's capabilities, e.g. "Orca printer agent" }; +struct PrinterConnectionParams +{ + std::string dev_id; + std::string host; // host address, usually the IP address without the http/https protocol + std::string port; // optional + std::string username; + std::string password; + bool use_ssl = false; // indicates if http or https + std::string ca_file; +}; + /** * FilamentSyncMode - Modes for filament data synchronization. * @@ -43,6 +59,15 @@ enum class FilamentSyncMode { pull ///< On-demand fetch via REST API (blocking call) }; +enum class CameraStreamMode { + none = 0, + http, // LAN or Cloud + https, // LAN or Cloud over TLS + rtsp, // LAN only + webrtc, // Cloud only + http_snapshot // HTTP endpoint returning one image per request +}; + /** * IPrinterAgent - Interface for printer operations. * @@ -84,10 +109,112 @@ public: */ virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0; + // why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect + // gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's + // publish funnel turns into a dialog. + virtual int command_ams_refresh_rfid(std::string, int, int, int, bool) + { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } + virtual int command_ams_calibrate(std::string, int, int, bool) + { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } + virtual int command_ams_select_tray(std::string, std::string, int, bool) + { return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; } + 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); + } + 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); + } + 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); + } + 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); + } + 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); + } + + 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) + { + (void) supports_mqtt_axis_control; + + double value = input_val; + if (!is_core_xy && (axis == "Y" || axis == "Z")) + value = -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("G91 \nG1 %1%%2% F%3%\nG90 \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; + } + + nlohmann::json j; + j["print"]["command"] = "gcode_line"; + j["print"]["param"] = gcode; + j["print"]["sequence_id"] = std::to_string(sequence_id); + return publish_command_json(dev_id, j, lan_mode); + } + + /** + * Default LAN account username for this agent's protocol, if it has a fixed one. + * Returns an empty string if the agent has no fixed default (e.g. caller must supply one). + */ + virtual std::string default_lan_username() const { return {}; } + /** * Establish a direct LAN connection to a printer. */ - virtual int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) = 0; + virtual int connect_printer(const PrinterConnectionParams& params) = 0; /** * Tear down the active LAN printer connection. @@ -105,12 +232,16 @@ public: /** * Validate current user certificates for the printer. */ - virtual int check_cert() = 0; + virtual int check_cert() { return BAMBU_NETWORK_SUCCESS; } /** * Install or refresh device certificate for LAN TLS. */ - virtual void install_device_cert(std::string dev_id, bool lan_only) = 0; + virtual void install_device_cert(std::string dev_id, bool lan_only) + { + (void) dev_id; + (void) lan_only; + } // ======================================================================== // Discovery @@ -126,7 +257,11 @@ public: /** * Ping the binding endpoint to check printer readiness. */ - virtual int ping_bind(std::string ping_code) = 0; + virtual int ping_bind(std::string ping_code) + { + (void) ping_code; + return BAMBU_NETWORK_SUCCESS; + } /** * Perform binding detection/handshake on a LAN printer. @@ -136,23 +271,50 @@ public: /** * Execute the multi-stage printer binding workflow. */ - virtual int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) = 0; + virtual int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, + std::string timezone, bool improved, OnUpdateStatusFn update_fn) + { + (void) dev_ip; + (void) dev_id; + (void) dev_model; + (void) sec_link; + (void) timezone; + (void) improved; + (void) update_fn; + return BAMBU_NETWORK_SUCCESS; + } /** * Remove the association between account and printer. */ - virtual int unbind(std::string dev_id) = 0; + virtual int unbind(std::string dev_id) + { + (void) dev_id; + return BAMBU_NETWORK_SUCCESS; + } /** * Request a one-time bind ticket from the server. */ - virtual int request_bind_ticket(std::string* ticket) = 0; + virtual int request_bind_ticket(std::string* ticket) + { + if (ticket) + *ticket = {}; + return BAMBU_NETWORK_SUCCESS; + } /** * Fetch the cloud snapshot image captured at a print failure. * Returns 0 if the request was dispatched; the image body arrives via callback(body, http_status). */ - virtual int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) = 0; + virtual int get_hms_snapshot(std::string dev_id, std::string file_name, + std::function callback) + { + (void) dev_id; + (void) file_name; + (void) callback; + return -1; + } /** * Register callback for fatal HTTP errors. @@ -168,7 +330,7 @@ public: virtual std::string get_user_selected_machine() = 0; /** - * Update the selected machine preference. + * Update the selected cloud machine preference. */ virtual int set_user_selected_machine(std::string dev_id) = 0; @@ -284,12 +446,20 @@ public: */ virtual FilamentSyncMode get_filament_sync_mode() const { return FilamentSyncMode::none; } + /** + * Get the camera stream mode for this agent. This value can be deterministic and derived at + * runtime if the printer supports multiple camera stream modes. E.g. LAN => HTTP/HTTPS/RTSP, Cloud => WebRTC. + * + * @return CameraStreamMode indicating how the camera stream is obtained or used: + */ + virtual CameraStreamMode get_camera_stream_mode() const { return CameraStreamMode::none; } + /** * Refresh filament info from the printer synchronously. * Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull. * Populates the MachineObject's DevFilaSystem with fetched filament data. */ - virtual bool fetch_filament_info(std::string dev_id) { return false; } + virtual bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) { return false; } /** * Translate one filament id across the printer boundary. @@ -300,6 +470,12 @@ public: */ virtual std::string to_orca_filament_id(const std::string& printer_filament_id) const { return printer_filament_id; } virtual std::string from_orca_filament_id(const std::string& orca_filament_id) const { return orca_filament_id; } + + /** + * Get the current camera stream URL for this agent's active machine. + * Only meaningful when get_camera_stream_mode() returns an HTTP, HTTPS, or RTSP mode. + */ + virtual std::string get_camera_url() const { return {}; } }; } // namespace Slic3r diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 384a69d51e..5d75955a1e 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -1,5 +1,6 @@ #include "MoonrakerPrinterAgent.hpp" #include "Http.hpp" +#include "IPrinterAgent.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" @@ -136,9 +137,9 @@ int MoonrakerPrinterAgent::send_message_to_printer(std::string dev_id, std::stri return handle_request(dev_id, json_str); } -int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +int MoonrakerPrinterAgent::connect_printer(const PrinterConnectionParams& params) { - if (dev_id.empty() || dev_ip.empty()) { + if (params.dev_id.empty() || params.host.empty()) { BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing dev_id or dev_ip"; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } @@ -148,7 +149,7 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i uint64_t gen; { std::lock_guard lock(connect_mutex); - init_device_info(dev_id, dev_ip, username, password, use_ssl); + init_device_info(params.dev_id, params.host, params.username, params.password, params.use_ssl, params.port); gen = ++connect_generation; base_url = device_info.base_url; api_key = device_info.api_key; @@ -170,7 +171,7 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i // Launch connection in background thread (capture by value to avoid data races) { std::lock_guard lock(connect_mutex); - connect_thread = std::thread([this, dev_id, base_url, api_key, gen]() { perform_connection_async(dev_id, base_url, api_key, gen); }); + connect_thread = std::thread([this, dev_id = params.dev_id, base_url, api_key, gen]() { perform_connection_async(dev_id, base_url, api_key, gen); }); } return BAMBU_NETWORK_SUCCESS; @@ -191,14 +192,6 @@ int MoonrakerPrinterAgent::disconnect_printer() return BAMBU_NETWORK_SUCCESS; } -int MoonrakerPrinterAgent::check_cert() { return BAMBU_NETWORK_SUCCESS; } - -void MoonrakerPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) -{ - (void) dev_id; - (void) lan_only; -} - bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending) { (void) sending; @@ -208,12 +201,6 @@ bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending) return true; } -int MoonrakerPrinterAgent::ping_bind(std::string ping_code) -{ - (void) ping_code; - return BAMBU_NETWORK_SUCCESS; -} - int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) { (void) sec_link; @@ -221,7 +208,7 @@ int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detect.dev_id = device_info.dev_id.empty() ? dev_ip : device_info.dev_id; detect.model_id = device_info.model_id.empty() ? device_info.model_name : device_info.model_id; // Prefer fetched hostname, then preset model name, then generic fallback - detect.dev_name = device_info.dev_name; + detect.dev_name = device_info.dev_name.empty() ? dev_ip : device_info.dev_name; detect.model_id = device_info.model_id; detect.version = device_info.version; detect.connect_type = "lan"; @@ -230,41 +217,6 @@ int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, return BAMBU_NETWORK_SUCCESS; } -int MoonrakerPrinterAgent::bind( - std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) -{ - (void) dev_ip; - (void) dev_id; - (void) dev_model; - (void) sec_link; - (void) timezone; - (void) improved; - (void) update_fn; - return BAMBU_NETWORK_SUCCESS; -} - -int MoonrakerPrinterAgent::unbind(std::string dev_id) -{ - (void) dev_id; - return BAMBU_NETWORK_SUCCESS; -} - -int MoonrakerPrinterAgent::request_bind_ticket(std::string* ticket) -{ - if (ticket) - *ticket = ""; - return BAMBU_NETWORK_SUCCESS; -} - -int MoonrakerPrinterAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) -{ - // No BBL cloud snapshot source; report failure so the caller falls back. - (void) dev_id; - (void) file_name; - (void) callback; - return -1; -} - int MoonrakerPrinterAgent::set_server_callback(OnServerErrFn fn) { std::lock_guard lock(state_mutex); @@ -385,7 +337,7 @@ int MoonrakerPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusF if (update_fn) update_fn(PrintingStageSending, 0, "Starting print..."); std::string gcode = "SDCARD_PRINT_FILE FILENAME=" + upload_filename; - if (!send_gcode(device_info.dev_id, gcode)) { + if (!send_gcode_sync(device_info.dev_id, gcode)) { return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED; } @@ -578,7 +530,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, } } -bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id) +bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) { std::vector trays; int max_lane_index = 0; @@ -1035,14 +987,11 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: } response["print"]["param"] = gcode; - if (send_gcode(dev_id, gcode)) { - response["print"]["result"] = "success"; + send_gcode_async(dev_id, gcode, [this, dev_id, response](bool success) mutable { + response["print"]["result"] = success ? "success" : "failed"; dispatch_message(dev_id, response.dump()); - return BAMBU_NETWORK_SUCCESS; - } - response["print"]["result"] = "failed"; - dispatch_message(dev_id, response.dump()); - return BAMBU_NETWORK_ERR_CONNECTION_TO_PRINTER_FAILED; + }); + return BAMBU_NETWORK_SUCCESS; } // Print control commands @@ -1061,7 +1010,7 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: if (json["print"].contains("temp") && json["print"]["temp"].is_number()) { int temp = json["print"]["temp"].get(); std::string gcode = "SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=" + std::to_string(temp); - send_gcode(dev_id, gcode); + send_gcode_async(dev_id, gcode); return BAMBU_NETWORK_SUCCESS; } } @@ -1076,20 +1025,21 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std:: } std::string heater = (extruder_idx == 0) ? "extruder" : "extruder" + std::to_string(extruder_idx); std::string gcode = "SET_HEATER_TEMPERATURE HEATER=" + heater + " TARGET=" + std::to_string(temp); - send_gcode(dev_id, gcode); + send_gcode_async(dev_id, gcode); return BAMBU_NETWORK_SUCCESS; } } if (cmd == "home") { - return send_gcode(dev_id, "G28") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + send_gcode_async(dev_id, "G28"); + return BAMBU_NETWORK_SUCCESS; } } return BAMBU_NETWORK_SUCCESS; } -bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +bool MoonrakerPrinterAgent::init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port) { device_info = MoonrakerDeviceInfo{}; auto* preset_bundle = GUI::wxGetApp().preset_bundle; @@ -1099,12 +1049,12 @@ bool MoonrakerPrinterAgent::init_device_info(std::string dev_id, std::string dev auto& preset = preset_bundle->printers.get_edited_preset(); const auto& printer_cfg = preset.config; - device_info.dev_ip = dev_ip; + device_info.dev_ip = dev_ip; device_info.api_key = password; device_info.model_name = printer_cfg.opt_string("printer_model"); device_info.model_id = preset.get_printer_type(preset_bundle); - device_info.base_url = use_ssl ? "https://" + dev_ip : "http://" + dev_ip; + device_info.base_url = normalize_base_url(use_ssl, dev_ip, port); device_info.dev_id = dev_id; device_info.version = ""; device_info.dev_name = device_info.dev_id; @@ -1224,7 +1174,45 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url, return true; } -bool MoonrakerPrinterAgent::send_gcode(const std::string& dev_id, const std::string& gcode) const +void MoonrakerPrinterAgent::send_gcode_async(const std::string& dev_id, const std::string& gcode, + std::function on_result) const +{ + (void) dev_id; + const std::string base_url = device_info.base_url; + const std::string api_key = device_info.api_key; + auto http = Http::post(join_url(base_url, "/printer/gcode/script")); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.header("Content-Type", "application/json") + .set_post_body(nlohmann::json{{"script", gcode}}.dump()) + .timeout_connect(5) + .timeout_max(10) + .on_complete([on_result](std::string body, unsigned status_code) { + (void) body; + const bool success = status_code == 200; + if (!success) { + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode failed: HTTP error " << status_code; + } + if (on_result) { + on_result(success); + } + }) + .on_error([on_result](std::string body, std::string err, unsigned status_code) { + (void) body; + std::string error = err; + if (status_code > 0) { + error += " (HTTP " + std::to_string(status_code) + ")"; + } + BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: send_gcode failed: " << error; + if (on_result) { + on_result(false); + } + }) + .perform(); +} + +bool MoonrakerPrinterAgent::send_gcode_sync(const std::string& dev_id, const std::string& gcode) const { nlohmann::json payload; payload["script"] = gcode; @@ -2029,17 +2017,20 @@ bool MoonrakerPrinterAgent::upload_gcode(const std::string& local_path, int MoonrakerPrinterAgent::pause_print(const std::string& dev_id) { - return send_gcode(dev_id, "PAUSE") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + send_gcode_async(dev_id, "PAUSE"); + return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::resume_print(const std::string& dev_id) { - return send_gcode(dev_id, "RESUME") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + send_gcode_async(dev_id, "RESUME"); + return BAMBU_NETWORK_SUCCESS; } int MoonrakerPrinterAgent::cancel_print(const std::string& dev_id) { - return send_gcode(dev_id, "CANCEL_PRINT") ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_SEND_MSG_FAILED; + send_gcode_async(dev_id, "CANCEL_PRINT"); + return BAMBU_NETWORK_SUCCESS; } bool MoonrakerPrinterAgent::send_jsonrpc_command(const std::string& base_url, @@ -2106,13 +2097,13 @@ void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id, if (is_stale()) { return; } - device_info.dev_name = fetched_info.dev_name; + device_info.dev_name = fetched_info.dev_name.empty() ? dev_id : fetched_info.dev_name; device_info.version = fetched_info.version; device_info.klippy_state = fetched_info.klippy_state; } // Orca todo: disable websocket for now, as we don't use MonitorPanel for Moonraker printers yet -#if 0 +#if 1 // Query initial status nlohmann::json initial_status; if (query_printer_status(base_url, api_key, initial_status, error_msg)) { @@ -2153,26 +2144,11 @@ bool MoonrakerPrinterAgent::is_numeric(const std::string& value) return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; }); } -std::string MoonrakerPrinterAgent::normalize_base_url(std::string host, const std::string& port) +std::string MoonrakerPrinterAgent::normalize_base_url(bool use_ssl, const std::string& host, const std::string& port) { - boost::trim(host); - if (host.empty()) { - return ""; - } - - std::string value = host; - if (is_numeric(port) && value.find("://") == std::string::npos && value.find(':') == std::string::npos) { - value += ":" + port; - } - - if (!boost::istarts_with(value, "http://") && !boost::istarts_with(value, "https://")) { - value = "http://" + value; - } - - if (value.size() > 1 && value.back() == '/') { - value.pop_back(); - } - + std::string value = use_ssl ? "https://" : "http://"; + value += host; + value += port.empty() ? "" : (":" + port); return value; } diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index ae63570063..a25a088ada 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -28,24 +28,15 @@ public: // Communication int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; - int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; + int connect_printer(const PrinterConnectionParams& params) override; int disconnect_printer() override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; - // Certificates - int check_cert() override; - void install_device_cert(std::string dev_id, bool lan_only) override; - // Discovery bool start_discovery(bool start, bool sending) override; // Binding - int ping_bind(std::string ping_code) override; int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; - int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; - int unbind(std::string dev_id) override; - int request_bind_ticket(std::string* ticket) override; - int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) override; int set_server_callback(OnServerErrFn fn) override; // Machine Selection @@ -71,7 +62,7 @@ public: // Pull-mode agent (on-demand filament sync) FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } - bool fetch_filament_info(std::string dev_id) override; + bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; protected: struct MoonrakerDeviceInfo @@ -103,7 +94,7 @@ protected: void build_ams_payload(int ams_count, int max_lane_index, const std::vector& trays); // 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); + virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port); virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; // State access for derived classes @@ -111,7 +102,7 @@ protected: // Helpers bool is_numeric(const std::string& value); - std::string normalize_base_url(std::string host, const std::string& port); + std::string normalize_base_url(bool use_ssl, const std::string& host, const std::string& port); std::string sanitize_filename(const std::string& filename); std::string join_url(const std::string& base_url, const std::string& path) const; @@ -128,7 +119,9 @@ private: bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; - bool send_gcode(const std::string& dev_id, const std::string& gcode) const; + bool send_gcode_sync(const std::string& dev_id, const std::string& gcode) const; + void send_gcode_async(const std::string& dev_id, const std::string& gcode, + std::function on_result = {}) const; void announce_printhost_device(); void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index b15e10c591..1f7705658e 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -1,11 +1,10 @@ -#include +#include "NetworkAgent.hpp" + #include -#include -#include #include -#include "libslic3r/Utils.hpp" -#include "NetworkAgent.hpp" +#include +#include "IPrinterAgent.hpp" #include "BBLNetworkPlugin.hpp" namespace Slic3r { @@ -115,6 +114,8 @@ void NetworkAgent::add_cloud_agent(const std::string& provider, std::shared_ptr< void NetworkAgent::set_printer_agent(std::shared_ptr printer_agent) { + m_user_machine_list_generation.fetch_add(1); + // Disconnect all callbacks from the old agent auto old_printer_agent = m_printer_agent; @@ -432,10 +433,26 @@ int NetworkAgent::check_user_task_report(int* task_id, bool* printable, const st int NetworkAgent::get_user_print_info(unsigned int* http_code, std::string* http_body, const std::string& provider) { + const std::string request_agent_id = m_printer_agent_id; + const std::uint64_t request_generation = m_user_machine_list_generation.fetch_add(1) + 1; const auto cloud_agent = get_cloud_agent(provider); - if (cloud_agent) - return cloud_agent->get_user_print_info(http_code, http_body); - return -1; + if (!cloud_agent) + return -1; + + const int result = cloud_agent->get_user_print_info(http_code, http_body); + if (result == 0 && http_body) { + try { + nlohmann::json response = nlohmann::json::parse(*http_body); + response["provider"] = provider; + response["agent_id"] = request_agent_id; + response["generation"] = request_generation; + *http_body = response.dump(); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " metadata injection exception=" << e.what(); + } + } + return result; } int NetworkAgent::get_user_tasks(TaskQueryParams params, std::string* http_body, const std::string& provider) @@ -515,6 +532,15 @@ int NetworkAgent::get_camera_url(std::string dev_id, std::function +NetworkAgent::create_camera_signaling_channel(const std::string& dev_id, const std::string& provider) +{ + const auto cloud_agent = get_cloud_agent(provider); + if (cloud_agent) + return cloud_agent->create_camera_signaling_channel(dev_id); + return nullptr; +} + int NetworkAgent::get_design_staffpick(int offset, int limit, std::function callback, const std::string& provider) { const auto cloud_agent = get_cloud_agent(provider); @@ -767,10 +793,81 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos return -1; } -int NetworkAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode) { if (m_printer_agent) - return m_printer_agent->connect_printer(dev_id, dev_ip, username, password, use_ssl); + return m_printer_agent->command_ams_refresh_rfid(dev_id, ams_id, slot_id, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_ams_calibrate(dev_id, ams_id, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_ams_select_tray(dev_id, tray_id, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::command_start_camera(std::string dev_id) +{ + if (m_printer_agent) + return m_printer_agent->command_start_camera(dev_id); + return -1; +} + +int NetworkAgent::command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_xyz_abs(dev_id, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_auto_leveling(dev_id, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_go_home(dev_id, is_printing, supports_mqtt_homing, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_set_bed(dev_id, temp, supports_mqtt_bed_ctrl, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) +{ + if (m_printer_agent) + return m_printer_agent->command_set_nozzle(dev_id, temp, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::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) +{ + if (m_printer_agent) + return m_printer_agent->command_axis_control(dev_id, axis, unit, input_val, speed, is_core_xy, supports_mqtt_axis_control, sequence_id, lan_mode); + return -1; +} + +int NetworkAgent::connect_printer(const PrinterConnectionParams& params) +{ + if (m_printer_agent) + return m_printer_agent->connect_printer(params); return -1; } @@ -788,6 +885,13 @@ int NetworkAgent::send_message_to_printer(std::string dev_id, std::string json_s return -1; } +std::string NetworkAgent::default_lan_username() const +{ + if (m_printer_agent) + return m_printer_agent->default_lan_username(); + return {}; +} + int NetworkAgent::check_cert() { if (m_printer_agent) @@ -846,8 +950,14 @@ std::string NetworkAgent::get_user_selected_machine() int NetworkAgent::set_user_selected_machine(std::string dev_id) { - if (m_printer_agent) - return m_printer_agent->set_user_selected_machine(dev_id); + BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::set_user_selected_machine: dev_id=" << dev_id + << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : ""); + if (m_printer_agent) { + const int result = m_printer_agent->set_user_selected_machine(dev_id); + BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::set_user_selected_machine: result=" << result; + return result; + } + BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::set_user_selected_machine: no printer agent"; return -1; } @@ -867,15 +977,27 @@ int NetworkAgent::stop_subscribe(std::string module) int NetworkAgent::add_subscribe(std::vector dev_list) { - if (m_printer_agent) - return m_printer_agent->add_subscribe(std::move(dev_list)); + BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::add_subscribe: count=" << dev_list.size() + << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : ""); + if (m_printer_agent) { + const int result = m_printer_agent->add_subscribe(std::move(dev_list)); + BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::add_subscribe: result=" << result; + return result; + } + BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::add_subscribe: no printer agent"; return -1; } int NetworkAgent::del_subscribe(std::vector dev_list) { - if (m_printer_agent) - return m_printer_agent->del_subscribe(std::move(dev_list)); + BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::del_subscribe: count=" << dev_list.size() + << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : ""); + if (m_printer_agent) { + const int result = m_printer_agent->del_subscribe(std::move(dev_list)); + BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::del_subscribe: result=" << result; + return result; + } + BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::del_subscribe: no printer agent"; return -1; } @@ -921,10 +1043,10 @@ FilamentSyncMode NetworkAgent::get_filament_sync_mode() const return FilamentSyncMode::none; } -bool NetworkAgent::fetch_filament_info(std::string dev_id) +bool NetworkAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) { if (m_printer_agent) { - return m_printer_agent->fetch_filament_info(dev_id); + return m_printer_agent->fetch_filament_info(dev_id, sync_mode); } return false; } @@ -943,6 +1065,20 @@ std::string NetworkAgent::from_orca_filament_id(const std::string& orca_filament return orca_filament_id; } +CameraStreamMode NetworkAgent::get_camera_stream_mode() const +{ + if (m_printer_agent) + return m_printer_agent->get_camera_stream_mode(); + return CameraStreamMode::none; +} + +std::string NetworkAgent::get_local_camera_stream_url() const +{ + if (m_printer_agent) + return m_printer_agent->get_camera_url(); + return {}; +} + int NetworkAgent::request_bind_ticket(std::string* ticket) { if (m_printer_agent) diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index 76128073a4..f93178db8b 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -2,16 +2,22 @@ #define __NETWORK_Agent_HPP__ #include "bambu_networking.hpp" + #include "libslic3r/ProjectTask.hpp" #include "ICloudServiceAgent.hpp" #include "IPrinterAgent.hpp" + #include +#include +#include #include #include #include namespace Slic3r { +class IPrinterAgent; + // Forward declaration class BBLNetworkPlugin; @@ -50,6 +56,7 @@ public: // Sub-agent accessors std::shared_ptr get_cloud_agent(const std::string& provider = ORCA_CLOUD_PROVIDER) const; std::shared_ptr get_printer_agent() const { return m_printer_agent; } + std::uint64_t get_user_machine_list_generation() const { return m_user_machine_list_generation.load(); } // Shared agent management void add_cloud_agent(const std::string& provider, std::shared_ptr agent); @@ -109,6 +116,7 @@ public: int query_bind_status(std::vector query_list, unsigned int* http_code, std::string* http_body, const std::string& provider = ORCA_CLOUD_PROVIDER); int modify_printer_name(std::string dev_id, std::string dev_name, const std::string& provider = ORCA_CLOUD_PROVIDER); int get_camera_url(std::string dev_id, std::function callback, const std::string& provider = ORCA_CLOUD_PROVIDER); + std::unique_ptr create_camera_signaling_channel(const std::string& dev_id, const std::string& provider = ORCA_CLOUD_PROVIDER); int get_design_staffpick(int offset, int limit, std::function callback, const std::string& provider = ORCA_CLOUD_PROVIDER); int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out, const std::string& provider = ORCA_CLOUD_PROVIDER); int get_model_publish_url(std::string* url, const std::string& provider = ORCA_CLOUD_PROVIDER); @@ -142,9 +150,21 @@ public: int set_on_local_message_fn(OnMessageFn fn); int set_server_callback(OnServerErrFn fn); int send_message(std::string dev_id, std::string json_str, int qos, int flag); - int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); + int command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode); + int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode); + int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode); + int command_start_camera(std::string dev_id); + int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode); + int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode); + int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode); + int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode); + int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode); + 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); + int connect_printer(const PrinterConnectionParams& params); int disconnect_printer(); int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag); + std::string default_lan_username() const; int check_cert(); void install_device_cert(std::string dev_id, bool lan_only); bool start_discovery(bool start, bool sending); @@ -164,7 +184,9 @@ public: int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); FilamentSyncMode get_filament_sync_mode() const; - bool fetch_filament_info(std::string dev_id); + bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull); + CameraStreamMode get_camera_stream_mode() const; + std::string get_local_camera_stream_url() const; std::string to_orca_filament_id(const std::string& printer_filament_id) const; std::string from_orca_filament_id(const std::string& orca_filament_id) const; int request_bind_ticket(std::string* ticket); @@ -195,6 +217,7 @@ private: std::map> m_cloud_agents; std::shared_ptr m_printer_agent; std::string m_printer_agent_id; + std::atomic m_user_machine_list_generation{0}; }; } diff --git a/src/slic3r/Utils/NetworkAgentFactory.cpp b/src/slic3r/Utils/NetworkAgentFactory.cpp index ff950d0946..e2e58b7510 100644 --- a/src/slic3r/Utils/NetworkAgentFactory.cpp +++ b/src/slic3r/Utils/NetworkAgentFactory.cpp @@ -16,8 +16,10 @@ #include #include #include +#include #include #include +#include namespace Slic3r { namespace { @@ -314,6 +316,21 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi std::shared_ptr cached_agent; + auto reject_conflicting_capability = [plugin_key, capability_name](const std::string& error_message) { + if (!wxTheApp || GUI::wxGetApp().is_closing()) + return; + GUI::wxGetApp().CallAfter([plugin_key, capability_name, error_message]() { + if (GUI::wxGetApp().is_closing()) + return; + PluginManager& manager = PluginManager::instance(); + manager.set_plugin_error(plugin_key, error_message); + // note: the unload callback triggered by disabling will call deregister, + // which will be a no-op since the printer agent is never registered + manager.set_capability_enabled({PluginCapabilityType::PrinterConnection, capability_name, plugin_key}, false); + wxMessageBox(wxString::FromUTF8(error_message.c_str()), _L("Plugins"), wxOK | wxICON_WARNING, GUI::wxGetApp().GetTopWindow()); + }); + }; + { std::lock_guard lock(s_registry_mutex); @@ -332,9 +349,10 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi auto& python_agent_ids = get_python_printer_agent_ids(); for (const auto& pair : python_agent_ids) { if (pair.first != capability_key && pair.second == info.id) { - BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses duplicate agent ID '" << info.id - << "' already registered by capability '" << pair.first.second << "' from plugin '" - << pair.first.first << "'"; + const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id + + "' is already registered by capability '" + pair.first.second + "' from plugin '" + pair.first.first + "'."; + BOOST_LOG_TRIVIAL(warning) << error_message; + reject_conflicting_capability(error_message); return; } } @@ -354,12 +372,22 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi auto& agents = get_printer_agents(); auto agent_it = agents.find(info.id); + // why: reject only when the ID is owned by SOMEONE ELSE - a built-in has an empty + // plugin_identifier, another plugin/capability has a different plugin_full_ref. When it + // IS the same plugin_full_ref, this capability is just re-registering itself, so fall + // through and refresh. if (agent_it != agents.end() && agent_it->second.plugin_identifier != plugin_full_ref) { - BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses agent ID '" << info.id - << "' already registered by '" << agent_it->second.display_name << "'"; + const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id + + "' is already registered by '" + agent_it->second.display_name + "'."; + BOOST_LOG_TRIVIAL(warning) << error_message; + reject_conflicting_capability(error_message); return; } + // why: insert_or_assign, not emplace - reaching here means the ID is new, or the same + // capability is re-registering (same plugin_full_ref). In the re-register case we WANT to + // overwrite so the factory closure points at the current live capability instance; emplace + // would silently keep the stale entry. agents.insert_or_assign(info.id, PrinterAgentInfo(info.id, info.name, plugin_full_ref, std::move(factory))); python_agent_ids[capability_key] = info.id; diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index 5d129a490e..f84d250c0d 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -83,6 +83,7 @@ constexpr const char* ORCA_UNSUBSCRIBE_PLUGINS = "/api/v1/plugins/subscriptions" constexpr const char* ORCA_PLUGINS_MINE = "/api/v1/plugins/mine"; constexpr const char* ORCA_PLUGINS_BASE = "/api/v1/plugins"; constexpr const char* ORCA_PLUGIN_DOWNLOAD_URL = "/api/v1/plugins/download"; +constexpr const char* ORCA_CLOUD_PRINTER = "/api/v1/printers"; constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login"; @@ -2623,11 +2624,51 @@ int OrcaCloudServiceAgent::check_user_task_report(int* task_id, bool* printable) int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::string* http_body) { - BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info (stub)"; + std::string response; + unsigned int code = 0; + int result = http_get(ORCA_CLOUD_PRINTER, &response, &code); + if (http_code) - *http_code = 200; - if (http_body) - *http_body = "{}"; + *http_code = code; + + if (result != 0 || code != 200) + return result != 0 ? result : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; + + try { + auto resp_json = nlohmann::json::parse(response); + nlohmann::json devices = nlohmann::json::array(); + + for (const auto& printer : resp_json.value("data", nlohmann::json::array())) { + const std::string role = printer.value("access_role", ""); + if (role.empty() || role == "viewer") + continue; + + nlohmann::json device; + device["dev_id"] = printer.value("id", ""); + device["dev_name"] = printer.value("name", ""); + if (printer.contains("model") && printer["model"].is_string()) + device["dev_model_name"] = printer["model"].get(); + + bool online = false; + if (printer.contains("status_snapshot") && printer["status_snapshot"].is_object()) { + const auto& status = printer["status_snapshot"].value("status", nlohmann::json::object()); + online = status.value("connection", nlohmann::json::object()).value("state", "") == "online"; + if (status.contains("job") && status["job"].is_object()) + device["task_status"] = status["job"].value("state", ""); + } + device["dev_online"] = online; + devices.push_back(std::move(device)); + } + + if (http_body) { + nlohmann::json out; + out["devices"] = std::move(devices); + *http_body = out.dump(); + } + } catch (const std::exception&) { + return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; + } + return BAMBU_NETWORK_SUCCESS; } diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp index 3ae86ec27c..565772cf96 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp @@ -1,6 +1,7 @@ #ifndef __ORCA_CLOUD_SERVICE_AGENT_HPP__ #define __ORCA_CLOUD_SERVICE_AGENT_HPP__ +#include "ICameraSignalingChannel.hpp" #include "ICloudServiceAgent.hpp" #include #include @@ -241,6 +242,7 @@ public: // ICloudServiceAgent Interface Implementation - Model Mall & Publishing // ======================================================================== int get_camera_url(std::string dev_id, std::function callback) override; + // std::unique_ptr create_camera_signaling_channel(const std::string& dev_id) override; int get_design_staffpick(int offset, int limit, std::function callback) override; int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) override; int get_model_publish_url(std::string* url) override; diff --git a/src/slic3r/Utils/OrcaPrinterAgent.cpp b/src/slic3r/Utils/OrcaPrinterAgent.cpp index cb70dafcca..987cc0efdb 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.cpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.cpp @@ -1,4 +1,5 @@ #include "OrcaPrinterAgent.hpp" +#include "IPrinterAgent.hpp" #include "NetworkAgentFactory.hpp" namespace Slic3r { @@ -26,7 +27,7 @@ int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int return BAMBU_NETWORK_SUCCESS; } -int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) +int OrcaPrinterAgent::connect_printer(const PrinterConnectionParams& params) { return BAMBU_NETWORK_SUCCESS; } @@ -41,19 +42,6 @@ int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string js return BAMBU_NETWORK_SUCCESS; } -// ============================================================================ -// Certificates - All Stubs -// ============================================================================ - -int OrcaPrinterAgent::check_cert() -{ - return BAMBU_NETWORK_SUCCESS; -} - -void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only) -{ -} - // ============================================================================ // Discovery - Stub // ============================================================================ @@ -63,47 +51,11 @@ bool OrcaPrinterAgent::start_discovery(bool start, bool sending) return true; } -// ============================================================================ -// Binding - All Stubs -// ============================================================================ - -int OrcaPrinterAgent::ping_bind(std::string ping_code) -{ - return BAMBU_NETWORK_SUCCESS; -} - int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) { return BAMBU_NETWORK_SUCCESS; } -int OrcaPrinterAgent::bind( - std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) -{ - return BAMBU_NETWORK_SUCCESS; -} - -int OrcaPrinterAgent::unbind(std::string dev_id) -{ - return BAMBU_NETWORK_SUCCESS; -} - -int OrcaPrinterAgent::request_bind_ticket(std::string* ticket) -{ - if (ticket) - *ticket = ""; - return BAMBU_NETWORK_SUCCESS; -} - -int OrcaPrinterAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) -{ - // No BBL cloud snapshot source; report failure so the caller falls back. - (void) dev_id; - (void) file_name; - (void) callback; - return -1; -} - int OrcaPrinterAgent::set_server_callback(OnServerErrFn fn) { std::lock_guard lock(state_mutex); diff --git a/src/slic3r/Utils/OrcaPrinterAgent.hpp b/src/slic3r/Utils/OrcaPrinterAgent.hpp index a1613420a5..7d2a75fe0f 100644 --- a/src/slic3r/Utils/OrcaPrinterAgent.hpp +++ b/src/slic3r/Utils/OrcaPrinterAgent.hpp @@ -28,24 +28,15 @@ public: // Communication int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; - int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; + int connect_printer(const PrinterConnectionParams& params) override; int disconnect_printer() override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; - // Certificates - int check_cert() override; - void install_device_cert(std::string dev_id, bool lan_only) override; - // Discovery bool start_discovery(bool start, bool sending) override; // Binding - int ping_bind(std::string ping_code) override; int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; - int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; - int unbind(std::string dev_id) override; - int request_bind_ticket(std::string* ticket) override; - int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) override; int set_server_callback(OnServerErrFn fn) override; // Machine Selection diff --git a/src/slic3r/Utils/PrinterNetworkTypes.hpp b/src/slic3r/Utils/PrinterNetworkTypes.hpp new file mode 100644 index 0000000000..d87be6cdcb --- /dev/null +++ b/src/slic3r/Utils/PrinterNetworkTypes.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace Slic3r { + +enum LiveviewLocal { + LVL_None, + LVL_Disable, + LVL_Local, + LVL_Rtsps, + LVL_Rtsp +}; + +enum LiveviewRemote { + LVR_None, + LVR_Tutk, + LVR_Agora, + LVR_TutkAgora +}; + +enum FileLocal { + FL_None, + FL_Local +}; + +enum FileRemote { + FR_None, + FR_Tutk, + FR_Agora, + FR_TutkAgora +}; + +} // namespace Slic3r diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 1f437853ba..4a90ec3771 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -40,7 +40,7 @@ AgentInfo QidiPrinterAgent::get_agent_info_static() return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"}; } -bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) +bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) { std::string error; diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp index a8326edb53..ce7b4cda0d 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.hpp +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -19,7 +19,7 @@ public: AgentInfo get_agent_info() override { return get_agent_info_static(); } // Override filament sync (Qidi-specific implementation) - bool fetch_filament_info(std::string dev_id) override; + bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; private: struct QidiFilamentDict diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp index 5783738af7..6039eb260f 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -104,7 +104,7 @@ std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type return base; } -bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id) +bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) { std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect"); diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp index 04c0a66b3f..d49fe21239 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp @@ -15,7 +15,7 @@ public: static AgentInfo get_agent_info_static(); AgentInfo get_agent_info() override { return get_agent_info_static(); } - bool fetch_filament_info(std::string dev_id) override; + bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; private: // Combine filament_type + filament_sub_type into a unified type string diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp index d428775c12..d0c88404fe 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp @@ -23,6 +23,14 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module) .value("Pull", FilamentSyncMode::pull) .export_values(); + py::enum_(printer_agent_module, "CameraStreamMode") + .value("None_", CameraStreamMode::none) + .value("HTTP", CameraStreamMode::http) + .value("HTTPS", CameraStreamMode::https) + .value("HTTP_SNAPSHOT", CameraStreamMode::http_snapshot) + .value("RTSP", CameraStreamMode::rtsp) + .export_values(); + py::class_(printer_agent_module, "AgentInfo") .def(py::init<>()) .def(py::init([](std::string id, std::string name, std::string version, std::string description) { @@ -89,6 +97,16 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module) .def_readwrite("task_ext_change_assist", &PrintParams::task_ext_change_assist) .def_readwrite("try_emmc_print", &PrintParams::try_emmc_print); + py::class_(printer_agent_module, "PrinterConnectionParams") + .def(py::init<>()) + .def_readwrite("dev_id", &PrinterConnectionParams::dev_id) + .def_readwrite("host", &PrinterConnectionParams::host) + .def_readwrite("port", &PrinterConnectionParams::port) + .def_readwrite("username", &PrinterConnectionParams::username) + .def_readwrite("password", &PrinterConnectionParams::password) + .def_readwrite("use_ssl", &PrinterConnectionParams::use_ssl) + .def_readwrite("ca_file", &PrinterConnectionParams::ca_file); + py::class_>( printer_agent_module, "PrinterAgentBase") .def(py::init<>()) @@ -98,6 +116,16 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module) .def("disconnect_printer", &PrinterAgentPluginCapability::disconnect_printer) .def("send_message", &PrinterAgentPluginCapability::send_message) .def("send_message_to_printer", &PrinterAgentPluginCapability::send_message_to_printer) + .def("command_ams_refresh_rfid", &PrinterAgentPluginCapability::command_ams_refresh_rfid) + .def("command_ams_calibrate", &PrinterAgentPluginCapability::command_ams_calibrate) + .def("command_ams_select_tray", &PrinterAgentPluginCapability::command_ams_select_tray) + .def("command_start_camera", &PrinterAgentPluginCapability::command_start_camera) + .def("command_xyz_abs", &PrinterAgentPluginCapability::command_xyz_abs) + .def("command_auto_leveling", &PrinterAgentPluginCapability::command_auto_leveling) + .def("command_go_home", &PrinterAgentPluginCapability::command_go_home) + .def("command_set_bed", &PrinterAgentPluginCapability::command_set_bed) + .def("command_set_nozzle", &PrinterAgentPluginCapability::command_set_nozzle) + .def("command_axis_control", &PrinterAgentPluginCapability::command_axis_control) .def("start_discovery", &PrinterAgentPluginCapability::start_discovery) .def("bind_detect", &PrinterAgentPluginCapability::bind_detect) .def("get_user_selected_machine", &PrinterAgentPluginCapability::get_user_selected_machine) @@ -105,7 +133,9 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module) .def("start_send_gcode_to_sdcard", &PrinterAgentPluginCapability::start_send_gcode_to_sdcard) .def("start_local_print", &PrinterAgentPluginCapability::start_local_print) .def("get_filament_sync_mode", &PrinterAgentPluginCapability::get_filament_sync_mode) + .def("get_camera_stream_mode", &PrinterAgentPluginCapability::get_camera_stream_mode) .def("fetch_filament_info", &PrinterAgentPluginCapability::fetch_filament_info) + .def("get_camera_url", &PrinterAgentPluginCapability::get_camera_url) .def("check_cert", &PrinterAgentPluginCapability::check_cert) .def("install_device_cert", &PrinterAgentPluginCapability::install_device_cert) .def("ping_bind", &PrinterAgentPluginCapability::ping_bind) diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp index ede7c6a9b8..d33cee7afe 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp @@ -5,7 +5,6 @@ #include "IPrinterAgent.hpp" -#include #include #include @@ -24,60 +23,39 @@ public: PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; } // set_cloud_agent is the host-managed dependency injection point — the host hands the - // capability its ICloudServiceAgent — so it is the one operation kept native here. Every - // other IPrinterAgent operation is pure: the Python plugin must implement all of them. + // capability its ICloudServiceAgent — so it is the one operation kept native here. Optional + // operations inherit IPrinterAgent's defaults; the remaining operations are plugin-defined. void set_cloud_agent(std::shared_ptr cloud) final override { (void) cloud; } AgentInfo get_agent_info() override = 0; - int connect_printer( - std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override = 0; - int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0; - int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0; - bool start_discovery(bool start, bool sending) override = 0; - int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0; - std::string get_user_selected_machine() override = 0; - int set_user_selected_machine(std::string dev_id) override = 0; - int start_send_gcode_to_sdcard(PrintParams params, - OnUpdateStatusFn update_fn, - WasCancelledFn cancel_fn, - OnWaitFn wait_fn) override = 0; - int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0; - FilamentSyncMode get_filament_sync_mode() const override = 0; - bool fetch_filament_info(std::string dev_id) override = 0; + int connect_printer(const PrinterConnectionParams& params) override = 0; + int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0; + int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0; + bool start_discovery(bool start, bool sending) override = 0; + int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0; + std::string get_user_selected_machine() override = 0; + int set_user_selected_machine(std::string dev_id) override = 0; + int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0; + int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0; - int check_cert() override = 0; - void install_device_cert(std::string dev_id, bool lan_only) override = 0; - int ping_bind(std::string ping_code) override = 0; - int bind(std::string dev_ip, - std::string dev_id, - std::string dev_model, - std::string sec_link, - std::string timezone, - bool improved, - OnUpdateStatusFn update_fn) override = 0; - int unbind(std::string dev_id) override = 0; - // request_bind_ticket has a std::string* out-param that cannot round-trip through a - // pybind11 override directly; the trampoline wraps it (the Python plugin returns a - // (result, ticket) tuple), so it stays pure here like the rest. - int request_bind_ticket(std::string* ticket) override = 0; - int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) override = 0; - int set_server_callback(OnServerErrFn fn) override = 0; + int set_server_callback(OnServerErrFn fn) override = 0; int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0; int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, - OnWaitFn wait_fn) override = 0; - int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0; + OnWaitFn wait_fn) override = 0; + int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0; + + int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override = 0; + int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override = 0; + int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override = 0; + int set_on_message_fn(OnMessageFn fn) override = 0; + int set_on_user_message_fn(OnMessageFn fn) override = 0; + int set_on_local_connect_fn(OnLocalConnectedFn fn) override = 0; + int set_on_local_message_fn(OnMessageFn fn) override = 0; + int set_queue_on_main_fn(QueueOnMainFn fn) override = 0; - int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override = 0; - int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override = 0; - int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override = 0; - int set_on_message_fn(OnMessageFn fn) override = 0; - int set_on_user_message_fn(OnMessageFn fn) override = 0; - int set_on_local_connect_fn(OnLocalConnectedFn fn) override = 0; - int set_on_local_message_fn(OnMessageFn fn) override = 0; - int set_queue_on_main_fn(QueueOnMainFn fn) override = 0; }; } // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp index cdb533d2cb..2e15523430 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp @@ -5,7 +5,10 @@ #include "../../PyPluginTrampoline.hpp" #include "IPrinterAgent.hpp" +#include "pybind11/pybind11.h" +#include #include +#include #include @@ -22,6 +25,12 @@ } ORCA_PY_AGENT_CATCH(name) \ return printer_agent_failure() +#define ORCA_PY_AGENT_OVERRIDE_DEFAULT(ret, name, ...) \ + try { \ + ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, ret, PrinterAgentPluginCapability, name, ##__VA_ARGS__); \ + } ORCA_PY_AGENT_CATCH(name) \ + return printer_agent_failure() + namespace Slic3r { // NetworkAgent's no-agent answer: -1 for a status code, the empty value (false, "", none) otherwise. template T printer_agent_failure() @@ -42,9 +51,9 @@ public: ORCA_PY_AGENT_OVERRIDE(AgentInfo, get_agent_info); } - int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override + int connect_printer(const PrinterConnectionParams& params) override { - ORCA_PY_AGENT_OVERRIDE(int, connect_printer, dev_id, dev_ip, username, password, use_ssl); + ORCA_PY_AGENT_OVERRIDE(int, connect_printer, params); } int disconnect_printer() override @@ -62,6 +71,61 @@ public: ORCA_PY_AGENT_OVERRIDE(int, send_message_to_printer, dev_id, json_str, qos, flag); } + int command_ams_refresh_rfid(std::string dev_id, int ams_id,int slot_id, int sequence_id, bool lan_mode) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_ams_refresh_rfid, dev_id, ams_id, slot_id, sequence_id, lan_mode); + } + + int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_ams_calibrate, dev_id, ams_id, sequence_id, lan_mode); + } + + int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_ams_select_tray, dev_id, tray_id, sequence_id, lan_mode); + } + + int command_start_camera(std::string dev_id) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_start_camera, dev_id); + } + + int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_xyz_abs, dev_id, sequence_id, lan_mode); + } + + int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_auto_leveling, dev_id, sequence_id, lan_mode); + } + + int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, + int sequence_id, bool lan_mode) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_go_home, dev_id, is_printing, supports_mqtt_homing, sequence_id, lan_mode); + } + + int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, + int sequence_id, bool lan_mode) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_set_bed, dev_id, temp, supports_mqtt_bed_ctrl, sequence_id, lan_mode); + } + + int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_set_nozzle, dev_id, temp, sequence_id, lan_mode); + } + + 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 + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_axis_control, dev_id, axis, unit, input_val, speed, + is_core_xy, supports_mqtt_axis_control, sequence_id, lan_mode); + } + bool start_discovery(bool start, bool sending) override { ORCA_PY_AGENT_OVERRIDE(bool, start_discovery, start, sending); @@ -95,12 +159,22 @@ public: FilamentSyncMode get_filament_sync_mode() const override { - ORCA_PY_AGENT_OVERRIDE(FilamentSyncMode, get_filament_sync_mode); + ORCA_PY_AGENT_OVERRIDE_DEFAULT(FilamentSyncMode, get_filament_sync_mode); } - bool fetch_filament_info(std::string dev_id) override + CameraStreamMode get_camera_stream_mode() const override { - ORCA_PY_AGENT_OVERRIDE(bool, fetch_filament_info, dev_id); + ORCA_PY_AGENT_OVERRIDE_DEFAULT(CameraStreamMode, get_camera_stream_mode); + } + + std::string get_camera_url() const override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(std::string, get_camera_url); + } + + bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) override + { + ORCA_PY_AGENT_OVERRIDE_DEFAULT(bool, fetch_filament_info, dev_id, sync_mode); } int check_cert() override @@ -110,7 +184,7 @@ public: void install_device_cert(std::string dev_id, bool lan_only) override { - ORCA_PY_AGENT_OVERRIDE(void, install_device_cert, dev_id, lan_only); + ORCA_PY_AGENT_OVERRIDE_DEFAULT(void, install_device_cert, dev_id, lan_only); } int ping_bind(std::string ping_code) override @@ -145,7 +219,7 @@ public: int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback) override { - ORCA_PY_AGENT_OVERRIDE(int, get_hms_snapshot, dev_id, file_name, callback); + ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, get_hms_snapshot, dev_id, file_name, callback); } int set_server_callback(OnServerErrFn fn) override @@ -196,6 +270,8 @@ public: // request_bind_ticket returns its ticket through a std::string* out-param, which pybind11 // cannot marshal back through a plain override. We dispatch manually: the Python plugin // returns a (result, ticket) tuple, which we unpack into the int result and the out-param. + // Not required to be implemented, so a missing override answers with the same failure value + // as the other printer-agent operations. Leave the out-param untouched on failure. int request_bind_ticket(std::string* ticket) override { try { @@ -207,7 +283,7 @@ public: pybind11::function override = pybind11::get_override(static_cast(this), "request_bind_ticket"); if (!override) - pybind11::pybind11_fail("Tried to call pure virtual function \"PrinterAgentPluginCapability::request_bind_ticket\""); + return printer_agent_failure(); try { pybind11::tuple result = override().cast(); if (ticket) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f79a3c54c2..21077cd2c1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,18 +57,28 @@ function(orcaslicer_copy_test_dlls) # into it). Copy them next to the executable and give it an $ORIGIN # rpath so the loader finds them when the tests run on the CI unit-test # runner, which only receives this build/tests tree. - get_target_property(_linked_libs ${_TEST_NAME}_tests LINK_LIBRARIES) + # + # Optional first arg: the target to patch, for directories that define + # more than one test executable. Defaults to ${_TEST_NAME}_tests so + # existing single-executable callers don't need to pass it. + if (ARGC GREATER 0) + set(_target ${ARGV0}) + else() + set(_target ${_TEST_NAME}_tests) + endif() + + get_target_property(_linked_libs ${_target} LINK_LIBRARIES) if (NOT "libslic3r_gui" IN_LIST _linked_libs) return() endif() - set_property(TARGET ${_TEST_NAME}_tests PROPERTY BUILD_RPATH "$ORIGIN") + set_property(TARGET ${_target} PROPERTY BUILD_RPATH "$ORIGIN") set(_configs ${CMAKE_CONFIGURATION_TYPES}) if (NOT _configs) set(_configs "${CMAKE_BUILD_TYPE}") endif() foreach(_cfg IN LISTS _configs) - orcaslicer_copy_sos(${_TEST_NAME}_tests "${_cfg}" "" _unused_sos) + orcaslicer_copy_sos(${_target} "${_cfg}" "" _unused_sos) endforeach() endif() endfunction() diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 789ab84457..7680206dc8 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -13,6 +13,7 @@ #include "libslic3r/AABBTreeLines.hpp" #include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Fill/FillAdaptive.hpp" +#include "libslic3r/Fill/FillGyroid.hpp" #include "libslic3r/Flow.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/IntersectionPoints.hpp" @@ -1257,6 +1258,61 @@ TEST_CASE("Multiline infill of an object matches the infill of a larger object w } } +TEST_CASE("Gyroid infill of an object matches the infill of a larger object with the same center", "[Fill]") +{ + const bool optimized = GENERATE(false, true); + const int multiline = GENERATE(1, 2); + const float density = GENERATE(0.05f, 0.2f); + const double spacing = 0.45; + CAPTURE(optimized, multiline, density); + + auto circle = [](double radius) { + Polygon contour = make_circle_num_segments(scale_(radius), 120); + contour.translate(Point::new_scale(100., 60.)); + return ExPolygon(std::move(contour)); + }; + const ExPolygon object = circle(20.); + const ExPolygon larger = circle(30.); + auto fill = [optimized, multiline, density, spacing](const ExPolygon ®ion, double z) { + std::unique_ptr filler(Fill::new_from_type(ipGyroid)); + filler->spacing = spacing; + filler->angle = float(M_PI / 7.); + filler->z = z; + + FillParams params; + params.density = density; + params.multiline = multiline; + params.gyroid_optimized = optimized; + params.dont_adjust = true; + Surface surface(stInternal, region); + return filler->fill_surface(&surface, params); + }; + // Away from the boundary of the object, where both are clipped and connected the same way. + const Polygons inner = shrink(to_polygons(object), scale_(1.)); + auto farthest = [&inner](const Polylines &from, const Polylines &to) { + const AABBTreeLines::LinesDistancer tree(to_lines(to)); + double distance = 0.; + for (const Polyline &path : intersection_pl(from, inner)) + for (const Point &point : path.equally_spaced_points(scale_(0.2))) + distance = std::max(distance, tree.distance_from_lines(point)); + return unscale(distance); + }; + + // Marching squares simplifies rings that start elsewhere in each object. + const double tolerance = optimized ? SPARSE_INFILL_RESOLUTION + 0.01 : 0.01; + // Half a z period of the waves, through both switches between horizontal and vertical waves. + const double wave_distance = spacing * multiline / (density * FillGyroid::DensityAdjust); + for (int step = 0; step <= 8; ++step) { + const double z = wave_distance * M_PI * step / 8.; + CAPTURE(z); + const Polylines paths = fill(object, z); + REQUIRE_FALSE(paths.empty()); + const Polylines reference = fill(larger, z); + CHECK(farthest(reference, paths) < tolerance); + CHECK(farthest(paths, reference) < tolerance); + } +} + TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]") { const int multiline = GENERATE(2, 3); diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index a3863134d5..f38dfeaa98 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -5,6 +5,9 @@ add_executable(${_TEST_NAME}_tests test_creality_cfs_match.cpp test_dev_mapping.cpp test_filament_bitmap_utils.cpp + test_device_progress.cpp + test_device_manager_integration.cpp + test_web_media_controller.cpp test_scene_raycaster.cpp test_lazy.cpp test_prebuild_queue.cpp @@ -17,6 +20,8 @@ add_executable(${_TEST_NAME}_tests test_plugin_capability_config.cpp test_plugin_config.cpp test_plugin_capabilities_in_use.cpp + test_plugin_status.cpp + test_printer_agent.cpp test_plugin_install.cpp test_plugin_lifecycle.cpp test_plugin_printer_agent.cpp @@ -39,7 +44,7 @@ endif () target_link_libraries(${_TEST_NAME}_tests test_common libslic3r_gui libslic3r pybind11::embed Catch2::Catch2WithMain) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") -orcaslicer_copy_test_dlls() +orcaslicer_copy_test_dlls(${_TEST_NAME}_tests) if (WIN32) add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD diff --git a/tests/slic3rutils/test_dev_mapping.cpp b/tests/slic3rutils/test_dev_mapping.cpp index 1c1528f818..658117d2e4 100644 --- a/tests/slic3rutils/test_dev_mapping.cpp +++ b/tests/slic3rutils/test_dev_mapping.cpp @@ -26,6 +26,37 @@ using json = nlohmann::json; using namespace Slic3r; +TEST_CASE("AMS tray placeholder state follows the latest status", "[DevFilaSystem]") +{ + MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); + + const json empty_slot = json::parse(R"({ + "ams": { + "tray_exist_bits": "0", + "ams": [ { "id": "0", "info": "00000001", "tray": [ + { "id": "0", "tray_slot_placeholder": "1", "tray_color": "00000000" } + ] } ] + } + })"); + DevFilaSystemParser::ParseV1_0(empty_slot, &obj, obj.GetFilaSystem().get(), false); + + DevAmsTray* tray = obj.GetFilaSystem()->GetAmsTray("0", "0"); + REQUIRE(tray != nullptr); + REQUIRE(tray->is_slot_placeholder); + + const json loaded_slot = json::parse(R"({ + "ams": { + "tray_exist_bits": "1", + "ams": [ { "id": "0", "info": "00000001", "tray": [ + { "id": "0", "tray_color": "FF0000FF" } + ] } ] + } + })"); + DevFilaSystemParser::ParseV1_0(loaded_slot, &obj, obj.GetFilaSystem().get(), false); + + CHECK_FALSE(tray->is_slot_placeholder); +} + TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]") { MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); diff --git a/tests/slic3rutils/test_device_manager_integration.cpp b/tests/slic3rutils/test_device_manager_integration.cpp new file mode 100644 index 0000000000..2b042d4f8e --- /dev/null +++ b/tests/slic3rutils/test_device_manager_integration.cpp @@ -0,0 +1,159 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +using namespace Slic3r; +using json = nlohmann::json; + +namespace { + +class StubCloudAgent final : public OrcaCloudServiceAgent +{ +public: + StubCloudAgent() : OrcaCloudServiceAgent("") {} + + int get_user_print_info(unsigned int* http_code, std::string* http_body) override + { + if (http_code) + *http_code = 200; + if (http_body) + *http_body = R"({"devices":[]})"; + return 0; + } + + std::string get_user_name() override { return "integration-test-user"; } +}; + +class TestPrinterAgent final : public OrcaPrinterAgent +{ +public: + explicit TestPrinterAgent(std::string id) + : OrcaPrinterAgent(""), m_info{std::move(id), "Integration Test Agent", "1.0", "test agent"} + { + } + + AgentInfo get_agent_info() override { return m_info; } + +private: + AgentInfo m_info; +}; + +struct ScopedAppConfig +{ + AppConfig config; +}; + +std::string machine_list_response(const std::string& provider, const std::string& agent_id, + std::uint64_t generation, const std::string& name) +{ + json machine; + machine["dev_id"] = "integration-device"; + machine["dev_name"] = name; + machine["dev_online"] = true; + machine["task_status"] = "idle"; + + json response; + response["provider"] = provider; + response["agent_id"] = agent_id; + response["generation"] = generation; + response["devices"] = json::array({machine}); + return response.dump(); +} + +} // namespace + +TEST_CASE("Network agent stamps user-machine responses with request context", "[DeviceManager][integration]") +{ + auto cloud = std::make_shared(); + NetworkAgent network(cloud, nullptr); + network.set_printer_agent(std::make_shared("integration-agent-a")); + + const std::uint64_t generation_before = network.get_user_machine_list_generation(); + unsigned int http_code = 0; + std::string body; + REQUIRE(network.get_user_print_info(&http_code, &body, ORCA_CLOUD_PROVIDER) == 0); + + const json response = json::parse(body); + CHECK(http_code == 200); + CHECK(response["provider"] == ORCA_CLOUD_PROVIDER); + CHECK(response["agent_id"] == "integration-agent-a"); + CHECK(response["generation"] == generation_before + 1); +} + +TEST_CASE("Device manager ignores stale cloud machine responses", "[DeviceManager][integration]") +{ + ScopedAppConfig app_config; + NetworkAgent network(nullptr, std::make_shared("integration-agent")); + network.set_printer_agent(std::make_shared("integration-agent")); + DeviceManager manager(&network, false, &app_config.config); + + const std::uint64_t current_generation = network.get_user_machine_list_generation(); + manager.parse_user_print_info(machine_list_response(ORCA_CLOUD_PROVIDER, "integration-agent", + current_generation, "Fresh name")); + + auto machines = manager.get_user_machinelist(); + REQUIRE(machines.size() == 1); + REQUIRE(machines.at("integration-device") != nullptr); + CHECK(machines.at("integration-device")->get_dev_name() == "Fresh name"); + + manager.parse_user_print_info(machine_list_response(BBL_CLOUD_PROVIDER, "integration-agent", + current_generation, "Stale provider")); + manager.parse_user_print_info(machine_list_response(ORCA_CLOUD_PROVIDER, "other-agent", + current_generation, "Stale agent")); + manager.parse_user_print_info(machine_list_response(ORCA_CLOUD_PROVIDER, "integration-agent", + current_generation + 1, "Stale generation")); + + machines = manager.get_user_machinelist(); + REQUIRE(machines.size() == 1); + CHECK(machines.at("integration-device")->get_dev_name() == "Fresh name"); +} + +TEST_CASE("Device manager filters and rehomes devices by printer-agent ownership", "[DeviceManager][integration]") +{ + ScopedAppConfig app_config; + auto agent_a = std::make_shared("integration-agent-a"); + auto agent_b = std::make_shared("integration-agent-b"); + NetworkAgent network(nullptr, agent_a); + DeviceManager manager(&network, false, &app_config.config); + + BBLocalMachine machine; + machine.dev_id = "integration-lan-device"; + machine.dev_name = "Integration LAN device"; + machine.dev_ip = "192.0.2.10"; + machine.printer_type = "C11"; + + MachineObject* object = manager.insert_local_device(machine, "lan", "free", "", "access-code"); + REQUIRE(object != nullptr); + CHECK(object->printer_agent_id == "integration-agent-a"); + CHECK(manager.get_my_machine_list("integration-agent-a").count(machine.dev_id) == 1); + CHECK(manager.get_my_machine_list("integration-agent-b").empty()); + + network.set_printer_agent(agent_b); + CHECK(manager.get_my_machine_list("integration-agent-b").empty()); + + manager.on_machine_alive(R"({ + "dev_name":"Rediscovered device", + "dev_id":"integration-lan-device", + "dev_ip":"192.0.2.10", + "dev_type":"C11", + "dev_signal":"strong", + "connect_type":"lan", + "bind_state":"free" + })"); + + CHECK(object->printer_agent_id == "integration-agent-b"); + CHECK(manager.get_my_machine_list("integration-agent-a").empty()); + CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1); +} diff --git a/tests/slic3rutils/test_device_progress.cpp b/tests/slic3rutils/test_device_progress.cpp new file mode 100644 index 0000000000..96dead46f0 --- /dev/null +++ b/tests/slic3rutils/test_device_progress.cpp @@ -0,0 +1,141 @@ +// why: match the GUI include order to avoid rpcndr.h byte/std::byte +// ambiguity in the Windows COM headers. +// why: wx/timer.h must precede DeviceManager.hpp because +// DeviceErrorDialog.hpp uses wxTimerEvent. +#ifdef WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#endif + +#include + +#include + +#include + +#include "slic3r/GUI/DeviceManager.hpp" + +#include + +using json = nlohmann::json; +using namespace Slic3r; + +TEST_CASE("Integer progress reaches the shared subtask", "[DeviceManager][Progress]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + + machine.update_print_progress(json(37)); + + REQUIRE(machine.mc_print_percent == 37); + BBLSubTask* subtask = machine.get_subtask(); + REQUIRE(subtask != nullptr); + CHECK(subtask->task_progress == 37); +} + +TEST_CASE("String progress reaches the shared subtask", "[DeviceManager][Progress]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + + machine.update_print_progress(json("41")); + + REQUIRE(machine.mc_print_percent == 41); + BBLSubTask* subtask = machine.get_subtask(); + REQUIRE(subtask != nullptr); + CHECK(subtask->task_progress == 41); +} + +TEST_CASE("Floating-point progress preserves the previous shared value", "[DeviceManager][Progress]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + + machine.update_print_progress(json(29)); + REQUIRE(machine.mc_print_percent == 29); + BBLSubTask* subtask = machine.get_subtask(); + REQUIRE(subtask != nullptr); + REQUIRE(subtask->task_progress == 29); + + machine.update_print_progress(json(29.5)); + BBLSubTask* current_subtask = machine.get_subtask(); + REQUIRE(current_subtask != nullptr); + REQUIRE(current_subtask == subtask); + CHECK(machine.mc_print_percent == 29); + CHECK(current_subtask->task_progress == 29); +} + +TEST_CASE("Unsupported progress values leave a fresh machine unchanged", "[DeviceManager][Progress]") +{ + SECTION("boolean") { + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + REQUIRE(machine.subtask_ == nullptr); + + machine.update_print_progress(json(true)); + + CHECK(machine.mc_print_percent == 0); + CHECK(machine.subtask_ == nullptr); + } + + SECTION("null") { + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + REQUIRE(machine.subtask_ == nullptr); + + machine.update_print_progress(json(nullptr)); + + CHECK(machine.mc_print_percent == 0); + CHECK(machine.subtask_ == nullptr); + } + + SECTION("object") { + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + REQUIRE(machine.subtask_ == nullptr); + + machine.update_print_progress(json::object()); + + CHECK(machine.mc_print_percent == 0); + CHECK(machine.subtask_ == nullptr); + } + + SECTION("array") { + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + REQUIRE(machine.subtask_ == nullptr); + + machine.update_print_progress(json::array()); + + CHECK(machine.mc_print_percent == 0); + CHECK(machine.subtask_ == nullptr); + } +} + +TEST_CASE("Malformed string progress leaves a fresh machine unchanged", "[DeviceManager][Progress]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + REQUIRE(machine.subtask_ == nullptr); + + CHECK_NOTHROW(machine.update_print_progress(json("not-a-percent"))); + CHECK(machine.mc_print_percent == 0); + CHECK(machine.subtask_ == nullptr); +} + +TEST_CASE("Zero progress replaces active shared progress", "[DeviceManager][Progress]") +{ + MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1"); + + machine.update_print_progress(json(63)); + BBLSubTask* subtask = machine.get_subtask(); + REQUIRE(subtask != nullptr); + REQUIRE(machine.mc_print_percent == 63); + REQUIRE(subtask->task_progress == 63); + + machine.set_print_state("FAILED"); + machine.update_print_progress(json(0)); + + BBLSubTask* current_subtask = machine.get_subtask(); + REQUIRE(current_subtask != nullptr); + REQUIRE(current_subtask == subtask); + REQUIRE(machine.mc_print_percent == 0); + CHECK(current_subtask->task_progress == 0); +} diff --git a/tests/slic3rutils/test_plugin_lifecycle.cpp b/tests/slic3rutils/test_plugin_lifecycle.cpp index bac3a19ea9..0c96f8c0b3 100644 --- a/tests/slic3rutils/test_plugin_lifecycle.cpp +++ b/tests/slic3rutils/test_plugin_lifecycle.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -77,6 +78,96 @@ class EchoPackage(orca.base): orca.register_capability(Echo) )PY"; +// A minimal printer-agent plugin exposing exactly one PrinterConnection capability. +const char* const PRINTER_AGENT_PLUGIN_SOURCE = R"PY(# /// script +# requires-python = ">=3.12" +# dependencies = [] +# +# [tool.orcaslicer.plugin] +# name = "Lifecycle Test Agent" +# description = "Minimal printer-agent plugin for the lifecycle test." +# author = "tests" +# version = "1.0.0" +# type = "printer-connection" +# /// +import orca + + +class LifecycleTestAgentCapability(orca.printer_agent.PrinterAgentBase): + def get_name(self): + return "Lifecycle Test Agent" + + def get_agent_info(self): + return orca.printer_agent.AgentInfo( + id="lifecycle-test-agent", + name="Lifecycle Test Agent", + version="1.0.0", + description="Lifecycle test printer agent", + ) + + +@orca.plugin +class LifecycleTestPlugin(orca.base): + def register_capabilities(self): + orca.register_capability(LifecycleTestAgentCapability) +)PY"; + +// In production GUI_App::init_plugin_gui_wiring subscribes the agent-registry +// callbacks. The test binary has no GUI, so install the same unload-side wiring +// once so these tests exercise the production deregister-on-unload path. +// +// The load-side (register) wiring is deliberately not installed: duplicate-id +// coverage registers agents manually in a deterministic order instead. +void install_agent_registry_wiring() +{ + static bool installed = false; + if (installed) + return; + installed = true; + + PluginManager& manager = PluginManager::instance(); + manager.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); + manager.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) { + if (capability.type == PluginCapabilityType::PrinterConnection) + NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); + }); +} + +// Duplicate-id coverage needs two distinct plugins with different package keys +// and classes while both return the same AgentInfo.id. +std::string make_agent_plugin_source(const std::string& suffix, + const std::string& display_name, + const std::string& agent_id) +{ + return std::string{} + + "# /// script\n" + + "# requires-python = \">=3.12\"\n" + + "# dependencies = []\n" + + "#\n" + + "# [tool.orcaslicer.plugin]\n" + + "# name = \"" + display_name + "\"\n" + + "# description = \"Duplicate-id fake printer-agent plugin.\"\n" + + "# author = \"tests\"\n" + + "# version = \"1.0.0\"\n" + + "# type = \"printer-connection\"\n" + + "# ///\n" + + "import orca\n" + + "\n\n" + + "class Cap" + suffix + "(orca.printer_agent.PrinterAgentBase):\n" + + " def get_name(self):\n" + + " return \"" + display_name + "\"\n" + + "\n" + + " def get_agent_info(self):\n" + + " return orca.printer_agent.AgentInfo(\n" + + " id=\"" + agent_id + "\", name=\"" + display_name + "\",\n" + + " version=\"1.0.0\", description=\"duplicate id test\")\n" + + "\n\n" + + "@orca.plugin\n" + + "class Plugin" + suffix + "(orca.base):\n" + + " def register_capabilities(self):\n" + + " orca.register_capability(Cap" + suffix + ")\n"; +} + // Writes {data_dir}/orca_plugins//.py and returns the plugin directory. fs::path write_plugin(const ScopedDataDir& data_dir_guard, const std::string& stem, const std::string& source) { @@ -222,6 +313,157 @@ TEST_CASE("A discovered script plugin loads and materializes its capability", "[ manager.unload_plugin("Echo_Plugin"); } +TEST_CASE("A printer-agent plugin registers and deregisters with its lifecycle", "[PluginLifecycle][Python]") +{ + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + ScopedDataDir data_dir_guard("printer-agent-load"); + write_plugin(data_dir_guard, "LifecycleTestAgent", PRINTER_AGENT_PLUGIN_SOURCE); + + PluginManager& manager = PluginManager::instance(); + install_agent_registry_wiring(); + manager.discover_plugins(/*async=*/false, /*clear=*/true); + + PluginDescriptor descriptor; + REQUIRE(manager.try_get_valid_plugin_descriptor("LifecycleTestAgent", descriptor)); + + std::string error; + REQUIRE(load_and_wait(manager, "LifecycleTestAgent", error)); + INFO("load error: " << error); + + const auto capabilities = manager.get_plugin_capabilities( + "LifecycleTestAgent", PluginCapabilityType::PrinterConnection); + REQUIRE(capabilities.size() == 1); + NetworkAgentFactory::register_python_printer_agent("LifecycleTestAgent", capabilities.front()->name()); + + CHECK(NetworkAgentFactory::is_printer_agent_registered("lifecycle-test-agent")); + REQUIRE(manager.unload_plugin("LifecycleTestAgent")); + CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered("lifecycle-test-agent")); +} + +TEST_CASE("A duplicate printer-agent id is rejected without clobbering its owner", "[PluginLifecycle][Python]") +{ + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + const std::string key_a = "DuplicateIdAgentA"; + const std::string key_b = "DuplicateIdAgentB"; + const std::string dup_id = "duplicate-id-agent"; + + ScopedDataDir data_dir_guard("printer-agent-duplicate"); + write_plugin(data_dir_guard, key_a, make_agent_plugin_source("A", "Duplicate Id Agent A", dup_id)); + write_plugin(data_dir_guard, key_b, make_agent_plugin_source("B", "Duplicate Id Agent B", dup_id)); + + PluginManager& manager = PluginManager::instance(); + install_agent_registry_wiring(); + manager.discover_plugins(/*async=*/false, /*clear=*/true); + + PluginDescriptor descriptor_a; + PluginDescriptor descriptor_b; + REQUIRE(manager.try_get_valid_plugin_descriptor(key_a, descriptor_a)); + REQUIRE(manager.try_get_valid_plugin_descriptor(key_b, descriptor_b)); + + std::string error; + REQUIRE(load_and_wait(manager, key_a, error)); + REQUIRE(load_and_wait(manager, key_b, error)); + + const auto capabilities_a = manager.get_plugin_capabilities(key_a, PluginCapabilityType::PrinterConnection); + const auto capabilities_b = manager.get_plugin_capabilities(key_b, PluginCapabilityType::PrinterConnection); + REQUIRE(capabilities_a.size() == 1); + REQUIRE(capabilities_b.size() == 1); + + NetworkAgentFactory::register_python_printer_agent(key_a, capabilities_a.front()->name()); + NetworkAgentFactory::register_python_printer_agent(key_b, capabilities_b.front()->name()); + + CHECK(NetworkAgentFactory::is_printer_agent_registered(dup_id)); + const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(dup_id); + REQUIRE(info != nullptr); + CHECK(info->plugin_identifier.find(key_a) != std::string::npos); + CHECK(info->plugin_identifier.find(key_b) == std::string::npos); + + REQUIRE(manager.unload_plugin(key_a)); + CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(dup_id)); + manager.unload_plugin(key_b); +} + +TEST_CASE("A printer-agent plugin cannot claim a built-in agent id", "[PluginLifecycle][Python]") +{ + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + NetworkAgentFactory::register_all_agents(); + REQUIRE(NetworkAgentFactory::is_printer_agent_registered(BBL_PRINTER_AGENT_ID)); + + const std::string plugin_key = "BuiltinClashAgent"; + ScopedDataDir data_dir_guard("printer-agent-builtin-clash"); + write_plugin(data_dir_guard, plugin_key, + make_agent_plugin_source("Clash", "Builtin Clash", BBL_PRINTER_AGENT_ID)); + + PluginManager& manager = PluginManager::instance(); + install_agent_registry_wiring(); + manager.discover_plugins(/*async=*/false, /*clear=*/true); + + PluginDescriptor descriptor; + REQUIRE(manager.try_get_valid_plugin_descriptor(plugin_key, descriptor)); + + std::string error; + REQUIRE(load_and_wait(manager, plugin_key, error)); + const auto capabilities = manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection); + REQUIRE(capabilities.size() == 1); + NetworkAgentFactory::register_python_printer_agent(plugin_key, capabilities.front()->name()); + + const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(BBL_PRINTER_AGENT_ID); + REQUIRE(info != nullptr); + CHECK_FALSE(info->is_plugin()); + CHECK(info->plugin_identifier.find(plugin_key) == std::string::npos); + + manager.unload_plugin(plugin_key); +} + +TEST_CASE("Re-registering a printer-agent capability preserves its registration", "[PluginLifecycle][Python]") +{ + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + + const std::string plugin_key = "ReRegisterAgent"; + const std::string agent_id = "re-register-agent"; + + ScopedDataDir data_dir_guard("printer-agent-reregister"); + write_plugin(data_dir_guard, plugin_key, + make_agent_plugin_source("Re", "Re Register", agent_id)); + + PluginManager& manager = PluginManager::instance(); + install_agent_registry_wiring(); + manager.discover_plugins(/*async=*/false, /*clear=*/true); + + PluginDescriptor descriptor; + REQUIRE(manager.try_get_valid_plugin_descriptor(plugin_key, descriptor)); + + std::string error; + REQUIRE(load_and_wait(manager, plugin_key, error)); + const auto capabilities = manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection); + REQUIRE(capabilities.size() == 1); + + NetworkAgentFactory::register_python_printer_agent(plugin_key, capabilities.front()->name()); + REQUIRE(NetworkAgentFactory::is_printer_agent_registered(agent_id)); + const PrinterAgentInfo* first = NetworkAgentFactory::get_printer_agent_info(agent_id); + REQUIRE(first != nullptr); + const std::string owner = first->plugin_identifier; + + NetworkAgentFactory::register_python_printer_agent(plugin_key, capabilities.front()->name()); + CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id)); + const PrinterAgentInfo* second = NetworkAgentFactory::get_printer_agent_info(agent_id); + REQUIRE(second != nullptr); + CHECK(second->plugin_identifier == owner); + + manager.unload_plugin(plugin_key); +} + TEST_CASE("Plugin manager can initialize again after shutdown", "[PluginLifecycle][Python]") { ScopedPluginManager plugin_system; diff --git a/tests/slic3rutils/test_plugin_printer_agent.cpp b/tests/slic3rutils/test_plugin_printer_agent.cpp index 2065360080..fc85363a39 100644 --- a/tests/slic3rutils/test_plugin_printer_agent.cpp +++ b/tests/slic3rutils/test_plugin_printer_agent.cpp @@ -115,6 +115,22 @@ TEST_CASE("A printer agent that omits its operations answers like a missing agen check_answers_like_no_agent(*agent); } +TEST_CASE("A printer agent uses IPrinterAgent defaults for omitted commands", "[PluginPrinterAgent][Python]") +{ + ScopedPluginManager plugin_system; + if (!plugin_system.initialized) + SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); + py::gil_scoped_acquire gil; + + auto agent = make_agent(" def send_message(self, dev_id, json_str, qos, flag): return 7\n" + " def send_message_to_printer(self, dev_id, json_str, qos, flag): return 8\n"); + REQUIRE(agent.agent); + + CHECK(agent->command_xyz_abs("dev", 1, false) == 7); + CHECK(agent->command_set_nozzle("dev", 200, 2, true) == 8); + CHECK(agent->command_ams_refresh_rfid("dev", -1, 0, 3, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED); +} + TEST_CASE("A printer agent operation returning the wrong type answers like a missing agent", "[PluginPrinterAgent][Python]") { ScopedPluginManager plugin_system; diff --git a/tests/slic3rutils/test_plugin_status.cpp b/tests/slic3rutils/test_plugin_status.cpp new file mode 100644 index 0000000000..c51c54c6c6 --- /dev/null +++ b/tests/slic3rutils/test_plugin_status.cpp @@ -0,0 +1,23 @@ +#include + +#include + +using Slic3r::GUI::PluginStatus; +using Slic3r::GUI::resolve_plugin_status; + +TEST_CASE("resolve_plugin_status precedence", "[plugin][status]") { + // the new branch: loaded module + error -> runtime fault, not a load failure. + REQUIRE(resolve_plugin_status(/*loading*/ false, /*has_error*/ true, /*is_loaded*/ true) == PluginStatus::RuntimeError); + + // error without a live module is a load-time Error. + REQUIRE(resolve_plugin_status(false, true, false) == PluginStatus::Error); + + // loading wins over a pending error so a reload never flashes red. + REQUIRE(resolve_plugin_status(true, true, true) == PluginStatus::Loading); + + // healthy loaded plugin. + REQUIRE(resolve_plugin_status(false, false, true) == PluginStatus::Activated); + + // nothing loaded, no error. + REQUIRE(resolve_plugin_status(false, false, false) == PluginStatus::Inactive); +} diff --git a/tests/slic3rutils/test_printer_agent.cpp b/tests/slic3rutils/test_printer_agent.cpp new file mode 100644 index 0000000000..1418001205 --- /dev/null +++ b/tests/slic3rutils/test_printer_agent.cpp @@ -0,0 +1,173 @@ +#include + +#include +#include + +#include "python_test_support.hpp" + +#include +#include + +#include + +using namespace Slic3r; +namespace py = pybind11; + +// =========================================================================== +// UNIT - printer-agent registry duplicate handling. +// Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in +// or previously registered agent. +// =========================================================================== +TEST_CASE("unit: printer-agent registry register / lookup / duplicate-reject", "[registry][unit]") +{ + // why: the registry is process-global state shared by the test binary, and + // Catch2 may run cases in any order. Use an id that cannot collide with + // built-ins or other cases. Avoid SECTIONs because each section re-runs the + // body and would register the same id twice. + const std::string id = "orca-test::registry-probe-7f3a"; + auto stub_factory = [](std::shared_ptr, const std::string&) + -> std::shared_ptr { return nullptr; }; + + REQUIRE_FALSE(NetworkAgentFactory::is_printer_agent_registered(id)); + + REQUIRE(NetworkAgentFactory::register_printer_agent(id, "Registry Probe", stub_factory)); + REQUIRE(NetworkAgentFactory::is_printer_agent_registered(id)); + + // Re-registering the same id is rejected and does not replace the entry. + REQUIRE_FALSE(NetworkAgentFactory::register_printer_agent(id, "Impostor", stub_factory)); + + // The first registration's display name survives the rejected duplicate. + const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(id); + REQUIRE(info != nullptr); + CHECK(info->display_name == "Registry Probe"); + + // It appears exactly once in the UI-population list. + auto agents = NetworkAgentFactory::get_registered_printer_agents(); + int count = 0; + for (const auto& a : agents) + if (a.id == id) + ++count; + CHECK(count == 1); +} + +// =========================================================================== +// INTEGRATION - the orca.printer_agent Python binding surface. +// Boots the embedded interpreter and asserts the C++ to Python contract that +// every printer-agent plugin subclasses. If a binding is renamed or removed, +// plugins fail at runtime even though C++ still compiles. +// =========================================================================== +// ensure_python_initialized()/import_orca_module() come from python_test_support.hpp: +// a bare scoped_interpreter with no PyConfig.home falls back to the build-time +// stdlib path, which doesn't exist beside this test binary, so Py_Initialize() +// fails to load the codecs needed for the filesystem encoding. The shared helper +// points PyConfig.home at the python/ runtime staged next to the test executable. + +TEST_CASE("integration: orca.printer_agent binding surface", "[integration][Python]") +{ + py::module_ orca = import_orca_module(); + + REQUIRE(py::hasattr(orca, "printer_agent")); + py::object pa = orca.attr("printer_agent"); + + // The base class every printer-agent plugin subclasses. + REQUIRE(py::hasattr(pa, "PrinterAgentBase")); + py::object base = pa.attr("PrinterAgentBase"); + for (const char* method : { "get_agent_info", "connect_printer", "disconnect_printer", + "send_message", "send_message_to_printer", + "command_ams_refresh_rfid", "command_ams_calibrate", + "command_ams_select_tray", "command_start_camera", + "command_xyz_abs", "command_auto_leveling", "command_go_home", + "command_set_bed", "command_set_nozzle", "command_axis_control", + "start_discovery", "bind_detect", + "start_print", "get_filament_sync_mode" }) { + CAPTURE(method); + CHECK(py::hasattr(base, method)); + } + + // AgentInfo value type - the registry identity the host reads (id is the key). + REQUIRE(py::hasattr(pa, "AgentInfo")); + py::object info = pa.attr("AgentInfo")("moonraker", "Moonraker", "1.0", "test agent"); + CHECK(info.attr("id").cast() == "moonraker"); + CHECK(info.attr("name").cast() == "Moonraker"); + + // FilamentSyncMode enum the host queries to pick pull vs subscription. + REQUIRE(py::hasattr(pa, "FilamentSyncMode")); + py::object mode = pa.attr("FilamentSyncMode"); + CHECK(py::hasattr(mode, "Pull")); + CHECK(py::hasattr(mode, "Subscription")); + CHECK(py::hasattr(mode, "None_")); + + REQUIRE(py::hasattr(pa, "CameraStreamMode")); + py::object camera_mode = pa.attr("CameraStreamMode"); + CHECK(py::hasattr(camera_mode, "HTTPS")); + CHECK_FALSE(py::hasattr(camera_mode, "WebRTC")); + + // Plugin-type enum exposed at module root (host reads it without the GIL). + CHECK(py::hasattr(orca, "PluginType")); +} + +// =========================================================================== +// INTEGRATION - plugin-registration API and discovery-context guards. +// These are the symbols every plugin package uses: the @orca.plugin decorator, +// orca.base, orca.register_capability, and the capability base modules. Checking +// them in the lightweight embedded-interpreter test catches binding breakage +// before the plugin-loader test needs to run. +// =========================================================================== +TEST_CASE("integration: orca plugin-registration API surface + discovery-context guards", "[integration][Python]") +{ + py::module_ orca = import_orca_module(); + + // Module-level surface every plugin package relies on. + // note: no "gcode" module here - this branch has no G-code capability module; + // PostProcessing exists only as a PluginType value. + for (const char* name : { "plugin", "register_capability", "base", "PythonPluginBase", + "PluginType", "PluginResult", "script", "printer_agent", "host" }) { + CAPTURE(name); + CHECK(py::hasattr(orca, name)); + } + + // Plugin package base and capability base contract. + CHECK(py::hasattr(orca.attr("base"), "register_capabilities")); + py::object cap_base = orca.attr("PythonPluginBase"); + for (const char* method : { "get_name", "get_type", "on_load", "on_unload" }) { + CAPTURE(method); + CHECK(py::hasattr(cap_base, method)); + } + + // The script capability module exposes its own base class. + CHECK(py::hasattr(orca.attr("script"), "ScriptPluginCapabilityBase")); + + // PluginType enum carries the values that route a capability, including PrinterConnection. + // note: no PostProcessing value on this branch's binding. + py::object types = orca.attr("PluginType"); + for (const char* value : { "PrinterConnection", "Script" }) { + CAPTURE(value); + CHECK(py::hasattr(types, value)); + } + + // note: this is testing behavior, not normal plugin loading. + // These APIs should only work while Orca is actively loading a plugin. + try { + // Calls Python's orca.register_capability(0) from C++. + // 0 is intentionally bogus. The important part is that there is no active + // plugin load context, so the function should reject the call immediately. + orca.attr("register_capability")(py::int_(0)); + + // If the call above does NOT throw, the test fails here. + FAIL("register_capability outside discovery context must raise"); + } catch (const py::error_already_set& error) { + // pybind11 wraps Python exceptions as py::error_already_set. + // This checks the Python exception type is ValueError. + CHECK(error.matches(PyExc_ValueError)); + } + + try { + // This is the function behind @orca.plugin. + // Same logic as above. + orca.attr("plugin")(py::int_(0)); + + FAIL("@orca.plugin outside discovery context must raise"); + } catch (const py::error_already_set& error) { + CHECK(error.matches(PyExc_ValueError)); + } +} diff --git a/tests/slic3rutils/test_web_media_controller.cpp b/tests/slic3rutils/test_web_media_controller.cpp new file mode 100644 index 0000000000..cdf756ca3a --- /dev/null +++ b/tests/slic3rutils/test_web_media_controller.cpp @@ -0,0 +1,113 @@ +#include + +#include + +#include + +#include +#include + +using namespace Slic3r; +using namespace Slic3r::GUI; + +namespace { + +class StubWebView final : public wxWebView +{ +public: + bool SetBackgroundColour(const wxColour&) override + { + events.emplace_back("background"); + return true; + } + + bool Create(wxWindow*, wxWindowID, const wxString&, const wxPoint&, const wxSize&, long, const wxString&) override { return true; } + wxString GetCurrentTitle() const override { return {}; } + wxString GetCurrentURL() const override { return {}; } + bool IsBusy() const override { return false; } + bool IsEditable() const override { return false; } + void LoadURL(const wxString& url) override + { + events.emplace_back("url"); + loaded_url = url.ToStdString(); + } + void Print() override {} + void RegisterHandler(wxSharedPtr) override {} + void Reload(wxWebViewReloadFlags) override {} + void SetEditable(bool) override {} + void Stop() override { events.emplace_back("stop"); } + bool CanGoBack() const override { return false; } + bool CanGoForward() const override { return false; } + void GoBack() override {} + void GoForward() override {} + void ClearHistory() override { events.emplace_back("history"); } + void EnableHistory(bool) override {} + wxVector> GetBackwardHistory() override { return {}; } + wxVector> GetForwardHistory() override { return {}; } + void LoadHistoryItem(wxSharedPtr) override {} + bool CanSetZoomType(wxWebViewZoomType) const override { return false; } + float GetZoomFactor() const override { return 1.0f; } + wxWebViewZoomType GetZoomType() const override { return wxWEBVIEW_ZOOM_TYPE_LAYOUT; } + void SetZoomFactor(float) override {} + void SetZoomType(wxWebViewZoomType) override {} + bool CanUndo() const override { return false; } + bool CanRedo() const override { return false; } + void Undo() override {} + void Redo() override {} + void* GetNativeBackend() const override { return nullptr; } + + bool RunScript(const wxString& javascript, wxString*) const override + { + events.emplace_back("script"); + script = javascript.ToStdString(); + return true; + } + +protected: + void DoSetPage(const wxString& html, const wxString& base_url) override + { + events.emplace_back("page"); + page = html.ToStdString(); + page_base = base_url.ToStdString(); + } + +public: + mutable std::vector events; + std::string page; + std::string page_base; + mutable std::string script; + std::string loaded_url; +}; + +} // namespace + +TEST_CASE("Web media controller tears down a snapshot lifecycle", "[WebMediaController][integration]") +{ + StubWebView view; + WebMediaController controller(&view); + + controller.set_mode(CameraStreamMode::http_snapshot); + controller.Load(wxURI("http://camera.example/frame.jpg")); + controller.Play(); + + REQUIRE(view.events.size() == 3); + CHECK(view.events[0] == "background"); + CHECK(view.events[1] == "page"); + CHECK(view.events[2] == "page"); + CHECK(view.page.find("stopCameraRefresh") != std::string::npos); + CHECK(view.page.find("http://camera.example/frame.jpg") != std::string::npos); + + controller.Stop(); + + REQUIRE(view.events.size() == 7); + CHECK(view.events[3] == "script"); + CHECK(view.events[4] == "stop"); + CHECK(view.events[5] == "page"); + CHECK(view.events[6] == "history"); + CHECK(view.script == "if(typeof stopCameraRefresh==='function') stopCameraRefresh();"); + CHECK(view.page.empty()); + CHECK(view.page_base == "about:blank"); + + controller.Play(); + CHECK(view.page.find("http://camera.example/frame.jpg") == std::string::npos); +}