diff --git a/.github/connect-bundle.lock.json b/.github/connect-bundle.lock.json new file mode 100644 index 000000000000..853e1017fe91 --- /dev/null +++ b/.github/connect-bundle.lock.json @@ -0,0 +1,11 @@ +{ + "releaseTag": "v1.7.0", + "sha256": { + "windows-x64": "348763829380b453948d626c706351b1187de145a784cb17d82c33bdf497e61a", + "windows-arm64": "b579dd3ecd483035fcef37a355b8c74233f442b9ab2668dc52fe671edbf3bf28", + "macos-x64": "5fe64de2d4b04243e57dd26805fc009f3d06e05f64002850b3f5c71f6a4c0ef9", + "macos-arm64": "eaa08813e2305ee818881b7fd2460892182ea5abb90dac0abd643d75cc8381a8", + "linux-x64": "2e9588135c2b43e4b80395896b3290bc0527a26f42cb264db9fd4c71e68c5ff0", + "linux-arm64": "e647b6f9062f712d35c66f7ace4eb6ec37553ed29b78555828b5cd0b72a2964d" + } +} diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 4c28ba0894cc..540e97a1d9a9 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -76,6 +76,18 @@ jobs: run: rm -rf /usr/local/lib/android/* /usr/share/dotnet/* /opt/ghc1/* "/usr/local/share/boost1/*" - uses: actions/checkout@v5 + - name: "Stage connect bundle" + shell: bash + run: | + case "${{ matrix.variant.arch }}" in + x86_64) platform="linux-x64" ;; + aarch64) platform="linux-arm64" ;; + *) echo "Unsupported Flatpak architecture: ${{ matrix.variant.arch }}" >&2; exit 1 ;; + esac + python3 scripts/packaging/stage_connect_bundle.py \ + --lock .github/connect-bundle.lock.json \ + --platform "$platform" \ + --resources-dir resources - name: Get the version and date run: | ver_pure=$(grep 'set(Snapmaker_VERSION' version.inc | cut -d '"' -f2) diff --git a/.gitignore b/.gitignore index 7e61d55181a5..6cf7cbb9bb20 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,6 @@ Orca-deps-wxWidgets/ sentry-native/ .claude/ .hermes/ -CLAUDE.md \ No newline at end of file +CLAUDE.md +# snapmaker_connection CLI binaries are placed into resources/ locally per platform/architecture and are not tracked +/resources/snapmaker_connection* \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index a6881ade1326..2309ac87212e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -938,7 +938,9 @@ endif() # Resources install target, configure fhs.hpp on UNIX if (WIN32) - install(DIRECTORY "${SLIC3R_RESOURCES_DIR}/" DESTINATION "./resources") + install(DIRECTORY "${SLIC3R_RESOURCES_DIR}/" DESTINATION "./resources" + PATTERN "*/gw_cache" EXCLUDE + ) set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) include(InstallRequiredSystemLibraries) install (PROGRAMS ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} DESTINATION ".") @@ -947,6 +949,7 @@ elseif (SLIC3R_FHS) set(SLIC3R_FHS_RESOURCES "${CMAKE_INSTALL_FULL_DATAROOTDIR}/Snapmaker_Orca") install(DIRECTORY ${SLIC3R_RESOURCES_DIR}/ DESTINATION ${SLIC3R_FHS_RESOURCES} PATTERN "*/udev" EXCLUDE + PATTERN "snapmaker_control_linux_*" PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) install(FILES src/dev-utils/platform/unix/Snapmaker_Orca.desktop DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications) foreach(SIZE 32 128 192) @@ -994,15 +997,31 @@ set (CPACK_NSIS_DEFINES "!define MUI_STARTMENUPAGE_DEFAULTFOLDER \"${CPACK_PACKA # .nsi template must stay ASCII -- the Chinese/English prompt texts therefore # live in the separate UTF-8 BOM .nsh instead of this inline block. file(READ "${CMAKE_SOURCE_DIR}/cmake/nsis/SnapmakerURLProtocols_install.nsh" CPACK_NSIS_EXTRA_INSTALL_COMMANDS) +file(READ "${CMAKE_SOURCE_DIR}/cmake/nsis/SnapmakerConnectFirewall_install.nsh" _snapmaker_connect_firewall_install) +string(APPEND CPACK_NSIS_EXTRA_INSTALL_COMMANDS "\n${_snapmaker_connect_firewall_install}") file(READ "${CMAKE_SOURCE_DIR}/cmake/nsis/SnapmakerURLProtocols_uninstall.nsh" _snapmaker_nsis_uninst_url) set(_snapmaker_nsis_uninst_kill [=[ - nsExec::Exec 'taskkill /F /IM snapmaker-orca.exe /T' + nsExec::Exec 'taskkill /F /T /IM snapmaker-orca.exe' + Pop $0 + nsExec::Exec 'taskkill /F /T /IM Snapmaker_Orca.exe' + Pop $0 + nsExec::Exec 'taskkill /F /T /IM snapmaker_control_windows_x64.exe' + Pop $0 + nsExec::Exec 'taskkill /F /T /IM snapmaker_control_windows_arm64.exe' + Pop $0 + nsExec::Exec 'taskkill /F /T /IM snapmaker_connection_windows_x64.exe' Pop $0 - nsExec::Exec 'taskkill /F /IM Snapmaker_Orca.exe /T' + nsExec::Exec 'taskkill /F /T /IM snapmaker_connection_windows_arm64.exe' Pop $0 ]=]) string(CONCAT CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS "${_snapmaker_nsis_uninst_kill}" "${_snapmaker_nsis_uninst_url}") +# Wait briefly for the connect CLI to release its executable after the forced +# termination above before generated files are removed. +file(READ "${CMAKE_SOURCE_DIR}/cmake/nsis/SnapmakerConnect_uninstall.nsh" _snapmaker_connect_uninstall) +string(APPEND CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS "\n${_snapmaker_connect_uninstall}") +file(READ "${CMAKE_SOURCE_DIR}/cmake/nsis/SnapmakerConnectFirewall_uninstall.nsh" _snapmaker_connect_firewall_uninstall) +string(APPEND CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS "\n${_snapmaker_connect_firewall_uninstall}") # Auto-close the uninstall details page so the wizard advances to the custom # finish page as soon as the uninstall section finishes (no extra click). string(APPEND CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " SetAutoClose true\n") diff --git a/build_linux.sh b/build_linux.sh index 15064046853f..c4366d06ae46 100755 --- a/build_linux.sh +++ b/build_linux.sh @@ -244,6 +244,24 @@ if [[ -n "${BUILD_DEPS}" ]] ; then fi if [[ -n "${BUILD_ORCA}" ]] ; then + echo "Staging snapmaker_connect..." + case "$(uname -m)" in + x86_64) + CONNECT_BUNDLE_PLATFORM="linux-x64" + ;; + aarch64|arm64) + CONNECT_BUNDLE_PLATFORM="linux-arm64" + ;; + *) + echo "Error: unsupported architecture $(uname -m) for snapmaker_connect." + exit 1 + ;; + esac + python3 ./scripts/packaging/stage_connect_bundle.py \ + --lock .github/connect-bundle.lock.json \ + --platform "${CONNECT_BUNDLE_PLATFORM}" \ + --resources-dir "${SCRIPT_PATH}/resources" + echo "Configuring Snapmaker_Orca..." if [[ -n "${CLEAN_BUILD}" ]] ; then rm -fr build diff --git a/build_release_macos.sh b/build_release_macos.sh index 5744cf21cf50..435a85e30979 100755 --- a/build_release_macos.sh +++ b/build_release_macos.sh @@ -155,6 +155,12 @@ function pack_deps() { } function build_slicer() { + echo "Staging snapmaker_connect..." + python3 "${PROJECT_DIR}/scripts/packaging/stage_connect_bundle.py" \ + --lock "${PROJECT_DIR}/.github/connect-bundle.lock.json" \ + --platform macos-universal \ + --resources-dir "${PROJECT_DIR}/resources" + # iterate over two architectures: x86_64 and arm64 for _ARCH in x86_64 arm64; do # if ARCH is universal or equal to _ARCH diff --git a/build_release_vs2022.bat b/build_release_vs2022.bat index db90338a8331..97da5ef58cf5 100644 --- a/build_release_vs2022.bat +++ b/build_release_vs2022.bat @@ -70,6 +70,9 @@ cd %WP% mkdir %build_dir% cd %build_dir% +python ..\scripts\packaging\stage_connect_bundle.py --lock ..\.github\connect-bundle.lock.json --platform windows-x64 --resources-dir ..\resources +if errorlevel 1 exit /b 1 + echo on cmake .. -G "Visual Studio 17 2022" -A x64 -DBBL_RELEASE_TO_PUBLIC=1 -DORCA_TOOLS=ON %SIG_FLAG% -DCMAKE_PREFIX_PATH="%DEPS%/usr/local" -DOPENSSL_ROOT_DIR="%DEPS%/usr/local" -DCMAKE_INSTALL_PREFIX="./Snapmaker_Orca" -DCMAKE_BUILD_TYPE=%build_type% -DWIN10SDK_PATH="%WindowsSdkDir%Include\%WindowsSDKVersion%\" cmake --build . --config %build_type% --target ALL_BUILD -- -m diff --git a/cmake/nsis/SnapmakerConnectFirewall_install.nsh b/cmake/nsis/SnapmakerConnectFirewall_install.nsh new file mode 100644 index 000000000000..2b29adc2862c --- /dev/null +++ b/cmake/nsis/SnapmakerConnectFirewall_install.nsh @@ -0,0 +1,17 @@ +; The connect CLI uses mDNS on UDP 5353 and transient UDP ports for LAN device +; discovery. Remove any prior user-created Block rule for this program path, +; then add a deterministic allow rule after the executable has been installed. +IfFileExists "$INSTDIR\resources\snapmaker_control_windows_x64.exe" 0 SnapmakerConnectFirewall_arm64 + nsExec::ExecToLog 'netsh advfirewall firewall delete rule name=all program="$INSTDIR\resources\snapmaker_control_windows_x64.exe"' + Pop $0 + nsExec::ExecToLog 'netsh advfirewall firewall add rule name="Snapmaker Control (x64)" dir=in action=allow program="$INSTDIR\resources\snapmaker_control_windows_x64.exe" enable=yes protocol=udp profile=any' + Pop $0 + +SnapmakerConnectFirewall_arm64: +IfFileExists "$INSTDIR\resources\snapmaker_control_windows_arm64.exe" 0 SnapmakerConnectFirewall_done + nsExec::ExecToLog 'netsh advfirewall firewall delete rule name=all program="$INSTDIR\resources\snapmaker_control_windows_arm64.exe"' + Pop $0 + nsExec::ExecToLog 'netsh advfirewall firewall add rule name="Snapmaker Control (ARM64)" dir=in action=allow program="$INSTDIR\resources\snapmaker_control_windows_arm64.exe" enable=yes protocol=udp profile=any' + Pop $0 + +SnapmakerConnectFirewall_done: diff --git a/cmake/nsis/SnapmakerConnectFirewall_uninstall.nsh b/cmake/nsis/SnapmakerConnectFirewall_uninstall.nsh new file mode 100644 index 000000000000..00841e7b15cd --- /dev/null +++ b/cmake/nsis/SnapmakerConnectFirewall_uninstall.nsh @@ -0,0 +1,6 @@ +; Remove both managed rules and legacy user-created Block rules scoped to the +; installed CLI paths. Filtering by program avoids removing unrelated rules. +nsExec::ExecToLog 'netsh advfirewall firewall delete rule name=all program="$INSTDIR\resources\snapmaker_control_windows_x64.exe"' +Pop $0 +nsExec::ExecToLog 'netsh advfirewall firewall delete rule name=all program="$INSTDIR\resources\snapmaker_control_windows_arm64.exe"' +Pop $0 diff --git a/cmake/nsis/SnapmakerConnect_uninstall.nsh b/cmake/nsis/SnapmakerConnect_uninstall.nsh new file mode 100644 index 000000000000..7d8132fce124 --- /dev/null +++ b/cmake/nsis/SnapmakerConnect_uninstall.nsh @@ -0,0 +1,33 @@ +; Wait for the connect CLI to release its executable after the installer has +; force-terminated it. Exact installed paths are checked for compatibility. +Push $R0 +StrCpy $R0 40 + +SnapmakerConnectUninstall_wait: + Delete "$INSTDIR\resources\snapmaker_connection_windows_x64.exe" + Delete "$INSTDIR\resources\snapmaker_connection_windows_arm64.exe" + Delete "$INSTDIR\resources\snapmaker_control_windows_x64.exe" + Delete "$INSTDIR\resources\snapmaker_control_windows_arm64.exe" + IfFileExists "$INSTDIR\resources\snapmaker_connection_windows_x64.exe" SnapmakerConnectUninstall_retry + IfFileExists "$INSTDIR\resources\snapmaker_connection_windows_arm64.exe" SnapmakerConnectUninstall_retry + IfFileExists "$INSTDIR\resources\snapmaker_control_windows_x64.exe" SnapmakerConnectUninstall_retry + IfFileExists "$INSTDIR\resources\snapmaker_control_windows_arm64.exe" SnapmakerConnectUninstall_retry + Goto SnapmakerConnectUninstall_done + +SnapmakerConnectUninstall_retry: + Sleep 1000 + IntOp $R0 $R0 - 1 + IntCmp $R0 0 SnapmakerConnectUninstall_timeout SnapmakerConnectUninstall_timeout SnapmakerConnectUninstall_wait + +SnapmakerConnectUninstall_timeout: + IfSilent SnapmakerConnectUninstall_silent SnapmakerConnectUninstall_failed +SnapmakerConnectUninstall_silent: + SetErrorLevel 8 + Abort +SnapmakerConnectUninstall_failed: + MessageBox MB_OK|MB_ICONSTOP "The Snapmaker Connect executable is still locked.$\nMake sure Snapmaker Orca is closed, check antivirus or indexing software, then run the uninstaller again." + Abort + +SnapmakerConnectUninstall_done: +ClearErrors +Pop $R0 diff --git a/cmake/nsis/SnapmakerPreinstall.nsh.in b/cmake/nsis/SnapmakerPreinstall.nsh.in index 9390794196a5..0bb56c5cf719 100644 --- a/cmake/nsis/SnapmakerPreinstall.nsh.in +++ b/cmake/nsis/SnapmakerPreinstall.nsh.in @@ -25,6 +25,16 @@ SnapmakerOrcaPre_try_legacy: Pop $0 IntCmp $0 0 SnapmakerOrcaPre_in_use SnapmakerOrcaPre_idle SnapmakerOrcaPre_idle SnapmakerOrcaPre_idle: + ; The detached connect CLI can outlive the app and lock the old executable. + ; This is the common cleanup point for overwrite and uninstall-first upgrades. + nsExec::Exec 'taskkill /F /T /IM snapmaker_control_windows_x64.exe' + Pop $0 + nsExec::Exec 'taskkill /F /T /IM snapmaker_control_windows_arm64.exe' + Pop $0 + nsExec::Exec 'taskkill /F /T /IM snapmaker_connection_windows_x64.exe' + Pop $0 + nsExec::Exec 'taskkill /F /T /IM snapmaker_connection_windows_arm64.exe' + Pop $0 Goto SnapmakerOrcaPre_done SnapmakerOrcaPre_in_use: IfSilent SnapmakerOrcaPre_silent SnapmakerOrcaPre_prompt @@ -136,6 +146,37 @@ SnapmakerOrcaPre_do_trim_slash: Goto SnapmakerOrcaPre_trim_inst_slash SnapmakerOrcaPre_trim_slash_done: IfFileExists "$0" 0 SnapmakerOrcaPre_uninst_launch_failed + ; The CLI was force-terminated above. Wait on the exact installed files while + ; antivirus software, indexers, and the OS release terminated-process handles. + StrCpy $4 40 +SnapmakerOrcaPre_connect_wait: + Delete "$3\resources\snapmaker_connection_windows_x64.exe" + Delete "$3\resources\snapmaker_connection_windows_arm64.exe" + Delete "$3\resources\snapmaker_control_windows_x64.exe" + Delete "$3\resources\snapmaker_control_windows_arm64.exe" + IfFileExists "$3\resources\snapmaker_connection_windows_x64.exe" SnapmakerOrcaPre_connect_retry + IfFileExists "$3\resources\snapmaker_connection_windows_arm64.exe" SnapmakerOrcaPre_connect_retry + IfFileExists "$3\resources\snapmaker_control_windows_x64.exe" SnapmakerOrcaPre_connect_retry + IfFileExists "$3\resources\snapmaker_control_windows_arm64.exe" SnapmakerOrcaPre_connect_retry + Goto SnapmakerOrcaPre_connect_done +SnapmakerOrcaPre_connect_retry: + Sleep 1000 + IntOp $4 $4 - 1 + IntCmp $4 0 SnapmakerOrcaPre_connect_timeout SnapmakerOrcaPre_connect_timeout SnapmakerOrcaPre_connect_wait +SnapmakerOrcaPre_connect_timeout: + IfSilent SnapmakerOrcaPre_connect_silent SnapmakerOrcaPre_connect_failed +SnapmakerOrcaPre_connect_silent: + SetErrorLevel 8 + Quit +SnapmakerOrcaPre_connect_failed: + StrCmp $LANGUAGE ${LANG_SIMPCHINESE} 0 SnapmakerOrcaPre_connect_failed_en + MessageBox MB_OK|MB_ICONSTOP "Snapmaker Connect 的可执行文件仍被占用。请确认 Snapmaker Orca 已关闭,检查杀毒软件或索引程序后重新运行安装程序。" + Abort +SnapmakerOrcaPre_connect_failed_en: + MessageBox MB_OK|MB_ICONSTOP "The Snapmaker Connect executable is still locked. Make sure Snapmaker Orca is closed, check antivirus or indexing software, then run the installer again." + Abort +SnapmakerOrcaPre_connect_done: + ClearErrors ; Same as CMake CPack NSIS.template.in and NSIS maintainer guidance: use _?=$DIR with NO ; extra quotes around the directory. _?="C:\Program Files\..." breaks parsing (log shows ; _?="C:\Program Fil...) and causes uninstall failure. See NSIS forums t-212468. diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 5c7e35a77e62..2649e8c6cb0c 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -7130,12 +7130,6 @@ msgstr "请在打印机屏幕上确认。" msgid "Log in failed. Please check the Pin Code." msgstr "登录失败,请检查Pin码。" -msgid "Log in" -msgstr "登录" - -msgid "Are you sure you want to log in?" -msgstr "确定要登录吗?" - msgid "Log in printer" msgstr "登录打印机" diff --git a/scripts/flatpak/entrypoint b/scripts/flatpak/entrypoint index 96c67c564395..711c5bbcc752 100644 --- a/scripts/flatpak/entrypoint +++ b/scripts/flatpak/entrypoint @@ -4,6 +4,57 @@ grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1 # Work-around https://github.com/bambulab/BambuStudio/issues/3440 -export LC_ALL=C.UTF-8 +canonical_locale() { + case "${1%%.*}" in + ""|"C"|"POSIX") return 1 ;; + esac + + LC_ALL=C locale -a | awk -v requested="$1" ' + BEGIN { + requested = tolower(requested) + gsub(/-/, "", requested) + } + { + available = tolower($0) + gsub(/-/, "", available) + if (available == requested || available == requested ".utf8") { + print $0 + exit + } + } + ' +} + +app_locale= +language= +for candidate in "${LC_ALL:-}" "${LC_MESSAGES:-}" "${LANG:-}"; do + [ -n "$language" ] || language=$candidate + app_locale=$(canonical_locale "$candidate") + [ -n "$app_locale" ] && break +done + +if [ -n "$app_locale" ]; then + export LANG="$app_locale" + language=$app_locale +else + export LANG=C.UTF-8 + language=${LANGUAGE:-$language} +fi + +language=${language%%:*} +language=${language%%.*} +language=${language%%@*} +case "$language" in + ""|"C"|"C".*|"POSIX"|"POSIX".*) language=en_US ;; +esac +printf '%s\n' "$language" | grep -Eq '^[[:alpha:]]{2,8}(_[[:alpha:]]{2,8})*$' || language=en_US +export LANGUAGE="$language" + +unset LC_ALL LC_COLLATE LC_CTYPE LC_MESSAGES LC_MONETARY LC_NUMERIC +unset LC_TIME LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION + +# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues +# while preserving the user's language/locale for translations. +export LC_NUMERIC=C exec /app/bin/snapmaker-orca "$@" diff --git a/scripts/packaging/stage_connect_bundle.py b/scripts/packaging/stage_connect_bundle.py new file mode 100644 index 000000000000..3e76b5d141c8 --- /dev/null +++ b/scripts/packaging/stage_connect_bundle.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""Download, hash-check, and stage the snapmaker_connect release bundle.""" + +from __future__ import annotations + +import argparse +import hashlib +import os +import shutil +import stat +import sys +import tarfile +import tempfile +import urllib.parse +import urllib.request +import zipfile +from pathlib import Path + + +RELEASE_REPOSITORY = "Snapmaker/snapmaker_control" +ASSET_KEYS = { + "windows-x64": ("zip", "snapmaker_control_windows_x64.exe"), + "windows-arm64": ("zip", "snapmaker_control_windows_arm64.exe"), + "macos-x64": ("zip", "snapmaker_control_macos_x64"), + "macos-arm64": ("zip", "snapmaker_control_macos_arm64"), + "linux-x64": ("tar.gz", "snapmaker_control_linux_x64"), + "linux-arm64": ("tar.gz", "snapmaker_control_linux_arm64"), +} +LEGACY_CLI_FILES = { + "snapmaker_connection_windows_x64.exe", + "snapmaker_connection_windows_arm64.exe", + "snapmaker_connection_macos_x64", + "snapmaker_connection_macos_arm64", + "snapmaker_connection_linux_x64", + "snapmaker_connection_linux_arm64", +} +STAGE_PLATFORMS = { + "windows-x64": ("windows-x64",), + "windows-arm64": ("windows-arm64",), + "macos-universal": ("macos-x64", "macos-arm64"), + "linux-x64": ("linux-x64",), + "linux-arm64": ("linux-arm64",), +} + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for block in iter(lambda: source.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def web_fingerprint(root: Path) -> dict[str, str]: + if not root.is_dir(): + raise RuntimeError(f"flutter_web directory does not exist: {root}") + return { + path.relative_to(root).as_posix(): sha256_file(path) + for path in root.rglob("*") + if path.is_file() + } + + +def default_cache_dir() -> Path: + if os.name == "nt": + base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) + return base / "OrcaSlicer" / "connect-bundle-cache" + base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) + return base / "OrcaSlicer" / "connect-bundles" + + +def load_lock(path: Path, asset_keys: tuple[str, ...]) -> tuple[str, dict[str, str]]: + import json + + try: + lock = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as error: + raise RuntimeError(f"cannot read lock file {path}: {error}") from error + + tag = lock.get("releaseTag") if isinstance(lock, dict) else None + hashes = lock.get("sha256") if isinstance(lock, dict) else None + if not isinstance(tag, str) or not tag.startswith("v") or len(tag) == 1: + raise RuntimeError("lock file releaseTag must be a release tag such as v1.3.0") + if not isinstance(hashes, dict): + raise RuntimeError("lock file must contain a sha256 object") + + selected = {} + for asset_key in asset_keys: + value = hashes.get(asset_key) + if not isinstance(value, str) or len(value) != 64 or any(c not in "0123456789abcdefABCDEF" for c in value): + raise RuntimeError(f"lock file has no valid SHA256 for {asset_key}") + selected[asset_key] = value.lower() + return tag, selected + + +def asset_filename(tag: str, asset_key: str) -> str: + archive_format, _ = ASSET_KEYS[asset_key] + return f"snapmaker_control-{tag[1:]}-{asset_key}.{archive_format}" + + +def download_url(tag: str, filename: str) -> str: + quoted_filename = urllib.parse.quote(filename) + return f"https://github.com/{RELEASE_REPOSITORY}/releases/download/{urllib.parse.quote(tag)}/{quoted_filename}" + + +def prepare_archive(cache_dir: Path, tag: str, asset_key: str, expected_hash: str) -> Path: + filename = asset_filename(tag, asset_key) + archive_path = cache_dir / filename + if archive_path.is_file() and sha256_file(archive_path) == expected_hash: + print(f"connect bundle cache hit: {archive_path}") + return archive_path + + if archive_path.exists(): + print(f"connect bundle cache mismatch, downloading again: {archive_path}") + + partial_path = cache_dir / f".{filename}.part" + partial_path.unlink(missing_ok=True) + request = urllib.request.Request(download_url(tag, filename), headers={"User-Agent": "OrcaSlicer-packaging"}) + try: + with urllib.request.urlopen(request, timeout=60) as response, partial_path.open("wb") as target: + shutil.copyfileobj(response, target, 1024 * 1024) + except Exception as error: + partial_path.unlink(missing_ok=True) + raise RuntimeError(f"cannot download {filename}; expected manual archive path: {archive_path}") from error + + actual_hash = sha256_file(partial_path) + if actual_hash != expected_hash: + partial_path.unlink(missing_ok=True) + raise RuntimeError(f"SHA256 mismatch for {filename}: expected {expected_hash}, got {actual_hash}") + archive_path.unlink(missing_ok=True) + partial_path.replace(archive_path) + return archive_path + + +def ensure_inside(root: Path, target: Path) -> None: + try: + target.resolve(strict=False).relative_to(root.resolve(strict=False)) + except ValueError as error: + raise RuntimeError(f"archive entry escapes the destination: {target}") from error + + +def extract_archive(archive_path: Path, destination: Path, asset_key: str) -> None: + destination.mkdir(parents=True, exist_ok=True) + if asset_key.startswith("linux-"): + with tarfile.open(archive_path, "r:gz") as archive: + for member in archive: + if member.issym() or member.islnk() or member.isdev(): + raise RuntimeError(f"unsupported archive entry type: {member.name}") + target = destination / member.name + ensure_inside(destination, target) + if member.isdir(): + target.mkdir(parents=True, exist_ok=True) + elif member.isreg(): + target.parent.mkdir(parents=True, exist_ok=True) + source = archive.extractfile(member) + if source is None: + raise RuntimeError(f"cannot extract archive entry: {member.name}") + with source, target.open("wb") as output: + shutil.copyfileobj(source, output, 1024 * 1024) + os.chmod(target, member.mode & 0o777 or 0o644) + else: + raise RuntimeError(f"unsupported archive entry type: {member.name}") + return + + with zipfile.ZipFile(archive_path, "r") as archive: + for info in archive.infolist(): + mode = info.external_attr >> 16 + if stat.S_ISLNK(mode): + raise RuntimeError(f"unsupported archive entry type: {info.filename}") + target = destination / info.filename + ensure_inside(destination, target) + if info.is_dir(): + target.mkdir(parents=True, exist_ok=True) + continue + target.parent.mkdir(parents=True, exist_ok=True) + with archive.open(info) as source, target.open("wb") as output: + shutil.copyfileobj(source, output, 1024 * 1024) + os.chmod(target, mode & 0o777 or 0o644) + + +def remove_stale_cli_files(resources_dir: Path, selected_keys: tuple[str, ...]) -> None: + expected = {ASSET_KEYS[key][1] for key in selected_keys} + for cli_name in {filename for _, filename in ASSET_KEYS.values()} | LEGACY_CLI_FILES: + if cli_name in expected: + continue + path = resources_dir / cli_name + if path.is_symlink() or path.is_file(): + path.unlink() + + +def replace_directory(source: Path, target: Path, resources_dir: Path) -> None: + ensure_inside(resources_dir, target) + if target.is_symlink() or target.is_file(): + target.unlink() + elif target.exists(): + shutil.rmtree(target) + shutil.copytree(source, target) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--lock", default=".github/connect-bundle.lock.json", help="connect bundle lock file") + parser.add_argument("--platform", required=True, choices=sorted(STAGE_PLATFORMS), help="target staging platform") + parser.add_argument("--resources-dir", default="resources", help="Orca resources directory") + parser.add_argument("--cache-dir", type=Path, help="archive cache directory") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + asset_keys = STAGE_PLATFORMS[args.platform] + lock_path = Path(args.lock) + resources_dir = Path(args.resources_dir) + cache_dir = args.cache_dir or default_cache_dir() + tag, hashes = load_lock(lock_path, asset_keys) + cache_dir.mkdir(parents=True, exist_ok=True) + resources_dir.mkdir(parents=True, exist_ok=True) + + print(f"Staging snapmaker_connect {tag} for {args.platform}") + print(f"Connect bundle cache: {cache_dir}") + with tempfile.TemporaryDirectory(prefix=".connect-stage-", dir=cache_dir) as temporary: + extracted_root = Path(temporary) + web_sources: list[Path] = [] + for asset_key in asset_keys: + archive_path = prepare_archive(cache_dir, tag, asset_key, hashes[asset_key]) + extract_destination = extracted_root / asset_key + extract_archive(archive_path, extract_destination, asset_key) + cli_source = extract_destination / ASSET_KEYS[asset_key][1] + if not cli_source.is_file(): + raise RuntimeError(f"archive {archive_path.name} does not contain {ASSET_KEYS[asset_key][1]}") + shutil.copy2(cli_source, resources_dir / ASSET_KEYS[asset_key][1]) + os.chmod(resources_dir / ASSET_KEYS[asset_key][1], 0o755) + web_source = extract_destination / "web" / "flutter_web" + if not web_source.is_dir(): + raise RuntimeError(f"archive {archive_path.name} does not contain web/flutter_web") + web_sources.append(web_source) + + web_fingerprints = [web_fingerprint(web_source) for web_source in web_sources] + if any(fingerprint != web_fingerprints[0] for fingerprint in web_fingerprints[1:]): + raise RuntimeError(f"flutter_web contents differ between archives for {args.platform}") + + remove_stale_cli_files(resources_dir, asset_keys) + replace_directory(web_sources[0], resources_dir / "web" / "flutter_web", resources_dir) + + print(f"Staged connect CLI and flutter_web into {resources_dir}") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except RuntimeError as error: + print(f"error: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/src/common_func/common_func.cpp b/src/common_func/common_func.cpp index 1466deae6ee7..1f8fa96c8b56 100644 --- a/src/common_func/common_func.cpp +++ b/src/common_func/common_func.cpp @@ -124,52 +124,6 @@ namespace common return str_version; } - std::string get_flutter_version() - { - - std::string versionFilePath = ""; - -#ifdef _WIN32 - PWSTR pszPath = nullptr; - char* path = new char[MAX_PATH](); - size_t pathLength = 0; - HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &pszPath); - if (SUCCEEDED(hr)) { - wcstombs_s(&pathLength, path, MAX_PATH, pszPath, MAX_PATH); - CoTaskMemFree(pszPath); - } - - std::string filePath = path; - versionFilePath = filePath + "\\" + std::string("Snapmaker_Orca\\web\\flutter_web\\version.json"); - - delete[] path; -#elif __APPLE__ - const char* home_env = getenv("HOME"); - versionFilePath = home_env; - versionFilePath = versionFilePath + "/Library/Application Support/Snapmaker_Orca/web/flutter_web/version.json"; -#else - std::string config_dir = get_linux_config_dir(); - if (!config_dir.empty()) - versionFilePath = config_dir + "/web/flutter_web/version.json"; -#endif - - std::ifstream json_file(versionFilePath); - if (!json_file.is_open()) { - std::ifstream json_file(versionFilePath); - return ""; - } - nlohmann::json json_data; - json_file >> json_data; - std::string str_version = json_data.value("version", ""); - std::string str_build_number = json_data.value("build_number", ""); - - std::string flutter_version = std::string("flutter_version: ") + str_version + std::string(" ") + std::string("build_number: ") + - str_build_number; - - return flutter_version; - } - - std::string getLocalArea() { std::string localArea = ""; diff --git a/src/common_func/common_func.hpp b/src/common_func/common_func.hpp index 06349b6f1dda..c11ef067e0f1 100644 --- a/src/common_func/common_func.hpp +++ b/src/common_func/common_func.hpp @@ -21,7 +21,6 @@ namespace common { std::string get_pc_name(); - std::string get_flutter_version(); std::string get_profile_version(); @@ -33,4 +32,4 @@ namespace common } // namespace common -#endif \ No newline at end of file +#endif diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 56515ef2c1fd..1d6717ed4778 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -41,7 +41,6 @@ namespace Slic3r { static const std::string VERSION_CHECK_URL_STABLE = "https://api.github.com/repos/Snapmaker/OrcaSlicer/releases/latest"; static const std::string VERSION_CHECK_URL = "https://api.github.com/repos/Snapmaker/OrcaSlicer/releases"; static const std::string PROFILE_UPDATE_URL = "/upgrade/profile/"; -static const std::string FLUTTER_UPDATE_URL = "/upgrade/flutter/"; static const std::string MODELS_STR = "models"; #define APP_UPDATE_URL_BASE_CN "https://meta-cfg.snapmaker.cn" @@ -701,10 +700,8 @@ std::string AppConfig::load() } } - // SM Orca - if (j.contains("devices")) { - m_device_list = j["devices"].get>(); - } + // Device ownership belongs to the connection gateway. Legacy devices in + // an existing config are intentionally ignored. } catch(std::exception err) { BOOST_LOG_TRIVIAL(info) << format("parse app config \"%1%\", error: %2%", AppConfig::loading_path(), err.what()); @@ -831,11 +828,6 @@ void AppConfig::save() continue; } for (const auto& kvp : category.second) { - // SM Orca - if (kvp.first == "use_new_connect") { - j[category.first][kvp.first] = false; - continue; - } if (kvp.second == "true") { j[category.first][kvp.first] = true; continue; @@ -873,18 +865,6 @@ void AppConfig::save() } j["devices"] = json::array(); - for (size_t i = 0; i < m_device_list.size(); ++i) { - if (m_device_list[i].link_mode != "wan") { - j["devices"].push_back(m_device_list[i]); - } - } - - // j["devices"] = m_device_list; - - - for (size_t i = 0; i < j["devices"].size(); ++i) { - j["devices"][i]["connected"] = false; - } for (const auto& local_machine : m_local_machines) { json m_json; m_json["dev_name"] = local_machine.second.dev_name; @@ -1465,25 +1445,6 @@ std::string AppConfig::get_preset_upgrade_url() return url; } -std::string AppConfig::get_web_resource_upgrade_url() -{ - - std::string resourceUrl = get("flutter_upgrade_url"); - - if(!resourceUrl.empty()) - return resourceUrl; - - std::string localLanguage = get("language"); - if (localLanguage != "zh_CN") - localLanguage = "en"; - std::string url = APP_UPDATE_URL_BASE_EN + FLUTTER_UPDATE_URL + localLanguage + std::string("/version.json"); - auto countryArea = get_country_code(); - if (countryArea == std::string("CN")) - url = APP_UPDATE_URL_BASE_CN + FLUTTER_UPDATE_URL + localLanguage + std::string("/version.json"); - - return url; -} - std::string AppConfig::get_version_upgrade_url(bool stable_only /* = false*/) { @@ -1528,56 +1489,6 @@ bool AppConfig::exists() return boost::filesystem::exists(config_path()); } -void AppConfig::save_device_info(const DeviceInfo& device) -{ - // 检查是否已存在该设备 - auto it = std::find_if(m_device_list.begin(), m_device_list.end(), - [&device](const DeviceInfo& d) { return d.dev_id == device.dev_id; }); - - if (it != m_device_list.end()) { - // 更新已存在的设备信息 - *it = device; - } else { - // 添加新设备 - m_device_list.push_back(device); - } - m_dirty = true; -} - -void AppConfig::clear_device_info() -{ - m_device_list.clear(); - m_dirty = true; -} - -void AppConfig::remove_device_info(const std::string& dev_id) -{ - auto it = std::find_if(m_device_list.begin(), m_device_list.end(), - [&dev_id](const DeviceInfo& d) { return d.dev_id == dev_id; }); - - if (it != m_device_list.end()) { - m_device_list.erase(it); - m_dirty = true; - } -} - -std::vector AppConfig::get_devices() const -{ - return m_device_list; -} - -bool AppConfig::get_device_info(const std::string& dev_id, DeviceInfo& info) const -{ - auto it = std::find_if(m_device_list.begin(), m_device_list.end(), - [&dev_id](const DeviceInfo& d) { return d.dev_id == dev_id; }); - - if (it != m_device_list.end()) { - info = *it; - return true; - } - return false; -} - void AppConfig::clear_filament_extruder_map() { filament_extruder_map.clear(); diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index a7e0d781d4d2..d40cad541f54 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -30,86 +30,6 @@ using namespace nlohmann; namespace Slic3r { -struct DeviceInfo { - std::string ip; - std::string dev_id; - std::string dev_name; - std::string model_name; - std::string preset_name; // 关联的打印机预设名称 - bool connected; - std::string img; - std::vector nozzle_sizes; - std::vector nozzle_volume_type; - std::string sn; - int protocol; - std::string api_key; - std::string user; - std::string password; - std::string ca; - std::string cert; - std::string key; - std::string clientId; - int port; - std::string link_mode; - std::string userid; - std::string id; - - - - // For JSON serialization, nozzle_volume_type needs to be compatible with scenarios where this field is missing in old configurations - friend void to_json(json& j, const DeviceInfo& device) - { - j = json{{"ip", device.ip}, - {"dev_id", device.dev_id}, - {"dev_name", device.dev_name}, - {"model_name", device.model_name}, - {"preset_name", device.preset_name}, - {"connected", device.connected}, - {"img", device.img}, - {"nozzle_sizes", device.nozzle_sizes}, - {"nozzle_volume_type", device.nozzle_volume_type}, - {"sn", device.sn}, - {"protocol", device.protocol}, - {"api_key", device.api_key}, - {"user", device.user}, - {"password", device.password}, - {"ca", device.ca}, - {"cert", device.cert}, - {"key", device.key}, - {"clientId", device.clientId}, - {"port", device.port}, - {"link_mode", device.link_mode}, - {"userid", device.userid}, - {"id", device.id}}; - } - - friend void from_json(const json& j, DeviceInfo& device) - { - j.at("ip").get_to(device.ip); - j.at("dev_id").get_to(device.dev_id); - j.at("dev_name").get_to(device.dev_name); - j.at("model_name").get_to(device.model_name); - j.at("preset_name").get_to(device.preset_name); - j.at("connected").get_to(device.connected); - j.at("img").get_to(device.img); - j.at("nozzle_sizes").get_to(device.nozzle_sizes); - device.nozzle_volume_type = j.value("nozzle_volume_type", std::vector{}); - j.at("sn").get_to(device.sn); - j.at("protocol").get_to(device.protocol); - j.at("api_key").get_to(device.api_key); - j.at("user").get_to(device.user); - j.at("password").get_to(device.password); - j.at("ca").get_to(device.ca); - j.at("cert").get_to(device.cert); - j.at("key").get_to(device.key); - j.at("clientId").get_to(device.clientId); - j.at("port").get_to(device.port); - j.at("link_mode").get_to(device.link_mode); - j.at("userid").get_to(device.userid); - j.at("id").get_to(device.id); - } -}; - // Connected LAN mode BambuLab printer struct BBLocalMachine { @@ -431,13 +351,6 @@ class AppConfig static const std::string SECTION_MATERIALS; static const std::string SECTION_EMBOSS_STYLE; - // 添加设备相关的方法 - void save_device_info(const DeviceInfo& device); - void remove_device_info(const std::string& dev_id); - std::vector get_devices() const; - bool get_device_info(const std::string& dev_id, DeviceInfo& info) const; - void clear_device_info(); - void clear_filament_extruder_map(); std::unordered_map& get_filament_extruder_map_ref(); @@ -485,9 +398,6 @@ class AppConfig // 添加耗材名称映射表 static const std::map filament_name_map; - // 添加设备信息存储 - std::vector m_device_list; - // 耗材喷嘴映射表 std::unordered_map filament_extruder_map; }; diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 83860bca67a5..a0dd0538393c 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -176,7 +176,6 @@ class PresetBundle MixedFilamentManager mixed_filaments; // Snapmaker - std::map> machine_filaments; std::vector m_connect_machine_info_list; // Calibrate diff --git a/src/libslic3r/SSWCPProtocol.cpp b/src/libslic3r/SSWCPProtocol.cpp index 984cad6fbab9..65c71a58dafb 100644 --- a/src/libslic3r/SSWCPProtocol.cpp +++ b/src/libslic3r/SSWCPProtocol.cpp @@ -9,13 +9,13 @@ namespace Slic3r { namespace SSWCPProtocol { namespace { -bool is_valid_flow_type(const std::string &value) +bool is_valid_flow_type(const std::string& value) { // Only the standard and high-flow nozzle modes are recognized flow types. return value == FLOW_MODE_STANDARD || value == FLOW_MODE_HIGH_FLOW; } -bool append_nozzle_diameter(const nlohmann::json &value, std::vector &out) +bool append_nozzle_diameter(const nlohmann::json& value, std::vector& out) { if (value.is_string()) { const std::string text = value.get(); @@ -30,8 +30,7 @@ bool append_nozzle_diameter(const nlohmann::json &value, std::vector(); - for (const auto &[number, text] : std::vector>{ - {0.2, "0.2"}, {0.4, "0.4"}, {0.6, "0.6"}, {0.8, "0.8"}}) { + for (const auto& [number, text] : std::vector>{{0.2, "0.2"}, {0.4, "0.4"}, {0.6, "0.6"}, {0.8, "0.8"}}) { if (std::abs(diameter - number) < 1e-6) { out.emplace_back(text); return true; @@ -40,105 +39,57 @@ bool append_nozzle_diameter(const nlohmann::json &value, std::vector &out) -{ - std::vector parsed; - const auto append = [&parsed](const nlohmann::json &entry) { - if (!entry.is_string()) return false; - const std::string type = entry.get(); - if (!is_valid_flow_type(type)) return false; - parsed.push_back(type); - return true; - }; - - if (value.is_array()) { - for (const nlohmann::json &entry : value) - if (!append(entry)) return false; - } else if (!append(value)) { - return false; - } - - out = std::move(parsed); - return true; -} - } // namespace -std::vector build_filament_volume_types(const std::vector *values, size_t count) +std::vector build_filament_volume_types(const std::vector* values, size_t count) { std::vector result(count, FLOW_MODE_STANDARD); - if (values == nullptr) return result; + if (values == nullptr) + return result; for (size_t i = 0; i < std::min(count, values->size()); ++i) - if ((*values)[i] == fvtHighFlow) result[i] = FLOW_MODE_HIGH_FLOW; + if ((*values)[i] == fvtHighFlow) + result[i] = FLOW_MODE_HIGH_FLOW; return result; } -OptionalFlowTypesStatus parse_optional_flow_types(const nlohmann::json &object, const char *key, size_t count, - std::vector &out) +OptionalFlowTypesStatus parse_optional_flow_types(const nlohmann::json& object, const char* key, size_t count, std::vector& out) { out.clear(); const auto it = object.find(key); - if (it == object.end()) return OptionalFlowTypesStatus::Missing; - if (!it->is_array() || it->size() < count) return OptionalFlowTypesStatus::Invalid; + if (it == object.end()) + return OptionalFlowTypesStatus::Missing; + if (!it->is_array() || it->size() < count) + return OptionalFlowTypesStatus::Invalid; std::vector parsed; parsed.reserve(count); for (size_t i = 0; i < count; ++i) { - if (!(*it)[i].is_string()) return OptionalFlowTypesStatus::Invalid; + if (!(*it)[i].is_string()) + return OptionalFlowTypesStatus::Invalid; const std::string type = (*it)[i].get(); - if (!is_valid_flow_type(type)) return OptionalFlowTypesStatus::Invalid; + if (!is_valid_flow_type(type)) + return OptionalFlowTypesStatus::Invalid; parsed.push_back(type); } out = std::move(parsed); return OptionalFlowTypesStatus::Valid; } -bool parse_machine_info_response(const nlohmann::json &response, MachineInfo &out) -{ - const nlohmann::json *root = &response; - if (const auto data = response.find("data"); data != response.end() && data->is_object()) - root = &*data; - - const auto system_info = root->find("system_info"); - if (system_info == root->end() || !system_info->is_object()) return false; - const auto product_info = system_info->find("product_info"); - if (product_info == system_info->end() || !product_info->is_object()) return false; - - MachineInfo parsed; - if (const auto model = product_info->find("machine_type"); model != product_info->end() && model->is_string()) - parsed.model = model->get(); - if (const auto name = product_info->find("device_name"); name != product_info->end() && name->is_string()) - parsed.device_name = name->get(); - - if (const auto diameters = product_info->find("nozzle_diameter"); diameters != product_info->end()) { - if (diameters->is_array()) { - for (const nlohmann::json &entry : *diameters) - append_nozzle_diameter(entry, parsed.nozzle_diameters); - } else { - append_nozzle_diameter(*diameters, parsed.nozzle_diameters); - } - } - - if (const auto flows = product_info->find("nozzle_volume_type"); flows != product_info->end() && - !parse_system_flow_types(*flows, parsed.nozzle_volume_types)) - parsed.nozzle_volume_types.clear(); - - out = std::move(parsed); - return true; -} - -bool select_complete_cached_nozzle_info(const std::vector> &slots, - std::vector &diameters, std::vector &flows) +bool select_complete_cached_nozzle_info(const std::vector>& slots, + std::vector& diameters, + std::vector& flows) { - if (slots.empty()) return false; + if (slots.empty()) + return false; std::vector cached_diameters; std::vector cached_flows; cached_diameters.reserve(slots.size()); cached_flows.reserve(slots.size()); bool flows_complete = true; - for (const auto &[diameter, flow] : slots) { - if (diameter.empty()) return false; + for (const auto& [diameter, flow] : slots) { + if (diameter.empty()) + return false; cached_diameters.push_back(diameter); if (!is_valid_flow_type(flow)) flows_complete = false; @@ -149,13 +100,12 @@ bool select_complete_cached_nozzle_info(const std::vector &diameters, - std::vector &volume_types) +bool parse_extruder_objects(const nlohmann::json& objects, std::vector& diameters, std::vector& volume_types) { diameters.clear(); volume_types.clear(); - // Navigate to data.status - const nlohmann::json *root = &response; - if (const auto data = response.find("data"); data != response.end() && data->is_object()) - root = &*data; - const auto status = root->find("status"); - if (status == root->end() || !status->is_object()) - return false; - // Dynamically discover extruder objects: "extruder", "extruder1", "extruder2", ... // Moonraker names the first toolhead "extruder" and subsequent ones with a numeric suffix. // Collect matching keys with their ordinal index for stable ordering. - struct ExtruderEntry { size_t index; std::string key; }; + struct ExtruderEntry + { + size_t index; + std::string key; + }; std::vector extruders; - for (auto it = status->begin(); it != status->end(); ++it) { - const std::string &key = it.key(); - size_t index; + for (auto it = objects.begin(); it != objects.end(); ++it) { + const std::string& key = it.key(); + size_t index; if (key == "extruder") { index = 0; } else if (key.size() > 8 && key.compare(0, 8, "extruder") == 0 && std::all_of(key.begin() + 8, key.end(), [](unsigned char c) { return std::isdigit(c); })) { // Cap suffix length to avoid stoul overflow on pathological firmware keys. - if (key.size() - 8 > 4) continue; + if (key.size() - 8 > 4) + continue; index = std::stoul(key.substr(8)); } else { continue; @@ -206,13 +151,12 @@ bool parse_extruder_nozzle_info(const nlohmann::json &response, extruders.push_back({index, key}); } // Sort by toolhead index: extruder(0), extruder1(1), extruder2(2), ... - std::sort(extruders.begin(), extruders.end(), - [](const ExtruderEntry &a, const ExtruderEntry &b) { return a.index < b.index; }); + std::sort(extruders.begin(), extruders.end(), [](const ExtruderEntry& a, const ExtruderEntry& b) { return a.index < b.index; }); bool all_flows_present = true; - for (const auto &entry : extruders) { - const auto extr = status->find(entry.key); - const auto nd = extr->find("nozzle_diameter"); + for (const auto& entry : extruders) { + const auto extr = objects.find(entry.key); + const auto nd = extr->find("nozzle_diameter"); if (nd == extr->end()) continue; diff --git a/src/libslic3r/SSWCPProtocol.hpp b/src/libslic3r/SSWCPProtocol.hpp index 386f972b707e..cf07e4da0c93 100644 --- a/src/libslic3r/SSWCPProtocol.hpp +++ b/src/libslic3r/SSWCPProtocol.hpp @@ -11,7 +11,8 @@ namespace Slic3r { /// Machine identification result parsed from the firmware system_info response. -struct MachineInfo { +struct MachineInfo +{ std::string model; std::string device_name; std::vector nozzle_diameters; @@ -28,52 +29,34 @@ enum class OptionalFlowTypesStatus { Missing, Valid, Invalid }; /// Build a filament_volume_type string array aligned with the logical filament count. /// Values other than fvtHighFlow are normalized to "standard". /// If *values* is null, every entry is "standard". -std::vector build_filament_volume_types(const std::vector *values, size_t count); +std::vector build_filament_volume_types(const std::vector* values, size_t count); /// Parse an optional flow-types array from a JSON object. /// - Missing key → Missing (caller treats as "no flow data") /// - Present but wrong type / short / invalid value → Invalid (caller rejects the payload) /// - Present and valid → Valid, fills *out* -OptionalFlowTypesStatus parse_optional_flow_types(const nlohmann::json &object, const char *key, size_t count, - std::vector &out); - -/// Parse a firmware system_info response into a MachineInfo. -/// Accepts both scalar and array nozzle_diameter, and scalar/array nozzle_volume_type. -/// Unknown flow strings leave nozzle_volume_types empty rather than defaulting to standard. -/// Returns false only when system_info or product_info is absent. -bool parse_machine_info_response(const nlohmann::json &response, MachineInfo &out); +OptionalFlowTypesStatus parse_optional_flow_types(const nlohmann::json& object, + const char* key, + size_t count, + std::vector& out); /// If every cached slot has a non-empty diameter, replace *diameters* with the cached /// values and replace *flows* only when every cached flow is also valid. /// Returns false (leaving outputs untouched) when any diameter is empty or slots is empty. -bool select_complete_cached_nozzle_info(const std::vector> &slots, - std::vector &diameters, std::vector &flows); +bool select_complete_cached_nozzle_info(const std::vector>& slots, + std::vector& diameters, + std::vector& flows); /// Normalize a machine model identifier across all sources (system_info, SSDP, DeviceManager). /// Applies known firmware aliases (e.g. "lava"/"Snapmaker test" -> "Snapmaker U1"). /// CRITICAL: empty input always returns empty — never defaults to a machine type. /// Idempotent: normalize(normalize(x)) == normalize(x). -std::string normalize_machine_model(const std::string &model); - -/// Resolution outcome distinguishing "no network response" from "got identity but missing fields". -enum class ResolveStatus { - NoResponse, ///< All network requests failed or timed out - GotIdentity, ///< model resolved (nozzle data may still be missing) - Complete ///< All fields (model + nozzle) filled -}; - -/// Result of resolve_machine_info. -struct ResolveResult { - ResolveStatus status {ResolveStatus::NoResponse}; - MachineInfo info; -}; +std::string normalize_machine_model(const std::string& model); -/// Parse nozzle_diameter / nozzle_volume_type from printer.objects.query extruder objects. -/// Dynamically discovers extruder / extruder1 / ... / extruderN keys in the response status. +/// Parse an extruder object map directly. The objects argument is the value of +/// data.status (printer.objects.query) or data.objects (gateway device snapshot). /// Returns true if at least one extruder with a valid nozzle_diameter was found. -bool parse_extruder_nozzle_info(const nlohmann::json &response, - std::vector &diameters, - std::vector &volume_types); +bool parse_extruder_objects(const nlohmann::json& objects, std::vector& diameters, std::vector& volume_types); /// sw_FinishFilamentMapping params.event — selects the action to run after the /// preprint webview closes. Extensible: add a new value here, then handle it in diff --git a/src/mqtt/CMakeLists.txt b/src/mqtt/CMakeLists.txt index 23b8bd19348f..61d52134d139 100644 --- a/src/mqtt/CMakeLists.txt +++ b/src/mqtt/CMakeLists.txt @@ -64,6 +64,9 @@ if(PAHO_WITH_MQTT_C) ## Build the Paho C library from the submodule set(PAHO_ENABLE_TESTING FALSE CACHE BOOL "No Paho C tests") set(PAHO_HIGH_PERFORMANCE TRUE CACHE BOOL "Paho C high performance") + if(WIN32) + set(PAHO_ENABLE_CPACK FALSE CACHE BOOL "No Paho C package documentation" FORCE) + endif() # TODO: Remove this when the next Paho C update is released. set(CMAKE_POLICY_DEFAULT_CMP0048 NEW) @@ -96,6 +99,14 @@ if(PAHO_WITH_MQTT_C) LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) + if(WIN32) + install(FILES + "${PROJECT_SOURCE_DIR}/externals/paho-mqtt-c/edl-v10" + "${PROJECT_SOURCE_DIR}/externals/paho-mqtt-c/epl-v20" + "${PROJECT_SOURCE_DIR}/externals/paho-mqtt-c/notice.html" + DESTINATION licenses/paho-mqtt-c + ) + endif() else() find_package(eclipse-paho-mqtt-c REQUIRED) endif() diff --git a/src/sentry_wrapper/SentryWrapper.cpp b/src/sentry_wrapper/SentryWrapper.cpp index a6a549b062fe..a00599e31077 100644 --- a/src/sentry_wrapper/SentryWrapper.cpp +++ b/src/sentry_wrapper/SentryWrapper.cpp @@ -306,10 +306,6 @@ void initSentryEx() sentry_set_tag("snapmaker_version", Snapmaker_VERSION); - std::string flutterVersion = common::get_flutter_version(); - if (!flutterVersion.empty()) - sentry_set_tag("flutter_version", flutterVersion.c_str()); - std::string machineID = common::getMachineId(); if (!machineID.empty()) sentry_set_tag("machine_id", machineID.c_str()); @@ -368,11 +364,6 @@ void sentryReportLogEx(SENTRY_LOG_LEVEL logLevel, sentry_value_t attr = sentry_value_new_attribute(sentry_value_new_string(Snapmaker_VERSION), NULL); sentry_value_set_by_key(tags, "snapmaker_version", attr); - std::string flutterVersion = common::get_flutter_version(); - if (!flutterVersion.empty()) { - sentry_value_t attr = sentry_value_new_attribute(sentry_value_new_string(flutterVersion.c_str()), NULL); - sentry_value_set_by_key(tags, "flutter_version", attr); - } // PC name is personal data; intentionally disabled for privacy. // std::string pcName = common::get_pc_name(); // if (!pcName.empty()) { diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 2058d40eee88..d2105db250b5 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -566,8 +566,6 @@ set(SLIC3R_GUI_SOURCES GUI/SMPhysicalPrinterDialog.cpp GUI/SSWCP.cpp GUI/SSWCP.hpp - GUI/WebSocketDebugServer.cpp - GUI/WebSocketDebugServer.hpp GUI/DownloadManager.cpp GUI/DownloadManager.hpp GUI/GenericDownloadDialog.cpp @@ -576,8 +574,6 @@ set(SLIC3R_GUI_SOURCES GUI/Timelapse/TimelapseDownloadPopup.hpp GUI/WebPresetDialog.hpp GUI/WebPresetDialog.cpp - GUI/WebSMUserLoginDialog.cpp - GUI/WebSMUserLoginDialog.hpp GUI/WebDeviceDialog.cpp GUI/WebDeviceDialog.hpp GUI/WebPreprintDialog.hpp @@ -599,6 +595,8 @@ set(SLIC3R_GUI_SOURCES Utils/CalibUtils.hpp Utils/ColorSpaceConvert.cpp Utils/ColorSpaceConvert.hpp + Utils/ConnectionProcessManager.cpp + Utils/ConnectionProcessManager.hpp Utils/CrealityPrint.cpp Utils/CrealityPrint.hpp Utils/Duet.cpp @@ -623,6 +621,14 @@ set(SLIC3R_GUI_SOURCES Utils/FontConfigHelp.hpp Utils/Frustum.cpp Utils/Frustum.hpp + Utils/GatewayProtocol.cpp + Utils/GatewayProtocol.hpp + Utils/GatewayDevice.cpp + Utils/GatewayDevice.hpp + Utils/GatewayAccount.cpp + Utils/GatewayAccount.hpp + Utils/GatewayService.cpp + Utils/GatewayService.hpp Utils/HexFile.cpp Utils/HexFile.hpp Utils/Http.cpp diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index 61a661e7be6d..5370b94c6509 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -27,7 +27,6 @@ #include "format.hpp" #include "WebUserLoginDialog.hpp" -#include "WebSMUserLoginDialog.hpp" #include "libslic3r/Print.hpp" @@ -528,12 +527,6 @@ void about() dlg.ShowModal(); } -void login() -{ - SMUserLogin dlg; - dlg.run(); -} - void desktop_open_datadir_folder() { // Execute command to open a file explorer, platform dependent. diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index 98a90be71d00..3452857c34f9 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -78,8 +78,6 @@ boost::filesystem::path into_path(const wxString &str); // Display an About dialog extern void about(); -// Display a Login dialog -extern void login(); // Ask the destop to open the datadir using the default file explorer. extern void desktop_open_datadir_folder(); // Ask the destop to open one folder diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index af8a7b710244..6f58fa851977 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -12,7 +12,6 @@ #include "Downloader.hpp" #include "slic3r/GUI/WebUrlDialog.hpp" -#include "slic3r/GUI/WebPresetDialog.hpp" #include "slic3r/GUI/SSWCP.hpp" #include "slic3r/GUI/DownloadManager.hpp" @@ -97,6 +96,8 @@ #include "../Utils/PrintHost.hpp" #include "../Utils/Process.hpp" #include "../Utils/MacDarkMode.hpp" +#include "../Utils/GatewayService.hpp" +#include "../Utils/GatewayDevice.hpp" #include "../Utils/Http.hpp" #include "../Utils/InstanceID.hpp" #include "../Utils/SnapLogClient.hpp" @@ -889,9 +890,6 @@ void GUI_App::log_version_info() BOOST_LOG_TRIVIAL(warning) << "[Version] Snapmaker Orca: " << Snapmaker_VERSION << ", Build: " << SLIC3R_VERSION; - std::string flutter_ver = common::get_flutter_version(); - BOOST_LOG_TRIVIAL(warning) << "[Version] Orca Web: " << (flutter_ver.empty() ? "N/A" : flutter_ver); - std::string profile_ver = common::get_profile_version(); BOOST_LOG_TRIVIAL(warning) << "[Version] Profile: " << (profile_ver.empty() ? "N/A" : profile_ver); @@ -1077,7 +1075,6 @@ void GUI_App::post_init() BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "Found glcontext not ready, postpone the init"; } //#endif - mainframe->Thaw(); // A URL open already in flight loads its own project and has already selected the 3D // view. Sending the user to the home page and starting a blank project would undo both. // On macOS the URL arrives through MacOpenURL after launch, so it is never visible in @@ -1085,6 +1082,9 @@ void GUI_App::post_init() // what switch_to_3d already does on platforms that receive the URL as a launch argument. if (m_url_open_pending) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", url open pending, staying on the 3D view and skipping the blank project"; + CallAfter([this] { + mainframe->Thaw(); + }); } else { // Defer the final tab selection to after pending events are // processed. During GL init, the PAGE_CHANGED handler posts @@ -1095,6 +1095,7 @@ void GUI_App::post_init() mainframe->select_tab(size_t(0)); else if (app_config->get("default_page") == "1") mainframe->select_tab(size_t(1)); + mainframe->Thaw(); }); plater_->trigger_restore_project(1); } @@ -1149,14 +1150,11 @@ void GUI_App::post_init() bool cw_showed = this->config_wizard_startup(); - SSWCP_MqttAgent_Instance::m_dialog = new WebPresetDialog(this); - std::string http_url = get_http_url(app_config->get_country_code()); std::string language = GUI::into_u8(current_language_code()); std::string network_ver = Slic3r::NetworkAgent::get_version(); bool sys_preset = app_config->get("sync_system_preset") == "true"; this->preset_updater->sync(http_url, language, network_ver, sys_preset ? preset_bundle : nullptr); - this->preset_updater->sync_web_async(true); this->request_version_from_config(false, false); }); @@ -1276,7 +1274,6 @@ GUI_App::GUI_App() m_page_http_server.set_request_handler(HttpServer::web_server_handle_request); m_page_http_server.start(); profiler.mark("m_page_http_server.start"); - BOOST_LOG_TRIVIAL(info) << "[Flutter] Version:" << common::get_flutter_version(); BOOST_LOG_TRIVIAL(info) << "[Profile] Version:" << common::get_profile_version(); flush_logs(); m_fltviews.set_app(this); @@ -1287,6 +1284,10 @@ void GUI_App::shutdown(bool isRecreate) { BOOST_LOG_TRIVIAL(info) << "GUI_App::shutdown enter"; + if (!m_is_recreating_gui) + m_is_closing = true; + stop_gateway_service(); + if (m_removable_drive_manager) { removable_drive_manager()->shutdown(); } @@ -1298,12 +1299,6 @@ void GUI_App::shutdown(bool isRecreate) login_dlg = nullptr; } - if (sm_login_dlg != nullptr) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": destroy SMlogin dialog"); - delete sm_login_dlg; - sm_login_dlg = nullptr; - } - if (web_device_dialog != nullptr) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": web device dialog"); delete web_device_dialog; @@ -1316,14 +1311,6 @@ void GUI_App::shutdown(bool isRecreate) web_preprint_dialog = nullptr; } - // Delete WebPresetDialog to ensure proper cleanup - if (SSWCP_MqttAgent_Instance::m_dialog != nullptr && !isRecreate) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": destroy WebPresetDialog"); - delete SSWCP_MqttAgent_Instance::m_dialog; - SSWCP_MqttAgent_Instance::m_dialog = nullptr; - } - - if (m_is_recreating_gui) return; m_is_closing = true; BOOST_LOG_TRIVIAL(info) << "GUI_App::shutdown exit"; @@ -2055,19 +2042,6 @@ GUI_App::~GUI_App() { GUI_App::m_app_alive.store(false); - if (m_token_check_timer) { - m_token_check_timer->Stop(); - m_token_check_timer.reset(); - } - if (m_silent_refresh_timeout_timer) { - m_silent_refresh_timeout_timer->Stop(); - m_silent_refresh_timeout_timer.reset(); - } - if (m_flutter_wcp_timeout_timer) { - m_flutter_wcp_timeout_timer->Stop(); - m_flutter_wcp_timeout_timer.reset(); - } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": enter"); if (app_config != nullptr) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": destroy app_config"); @@ -2279,95 +2253,6 @@ bool GUI_App::check_older_app_config(Semver current_version, bool backup) return false; } -void GUI_App::copy_web_resources() { - StartupProfiler profiler("GUI_App::copy_web_resources"); - - auto data_web_path = boost::filesystem::path(data_dir()) / "web"; - if (!boost::filesystem::exists(data_web_path / "flutter_web")) { - copy_bundled_flutter_web(false); - profiler.mark("copy flutter_web (missing target)"); - } else { - auto source_version_file = boost::filesystem::path(resources_dir()) / "web" / "flutter_web" / "version.json"; - auto target_version_file = data_web_path / "flutter_web" / "version.json"; - - try { - boost::property_tree::ptree source_config, target_config; - boost::property_tree::read_json(source_version_file.string(), source_config); - boost::property_tree::read_json(target_version_file.string(), target_config); - std::string source_build_number_str = source_config.get("build_number", "0"); - std::string target_build_number_str = target_config.get("build_number", "0"); - - if (source_build_number_str > target_build_number_str) { - copy_bundled_flutter_web(true); - profiler.mark("copy flutter_web (version upgrade)"); - } else { - profiler.note("flutter_web already up to date"); - } - } - catch (std::exception& e) { - profiler.note(std::string("version check failed: ") + e.what()); - } - } -} - -bool GUI_App::copy_bundled_flutter_web(bool upgrade) -{ - auto source_path = boost::filesystem::path(resources_dir()) / "web" / "flutter_web"; - auto target_path = boost::filesystem::path(data_dir()) / "web" / "flutter_web"; - if (copy_directory_recursively(source_path, target_path)) - return true; - - BOOST_LOG_TRIVIAL(error) << "Failed to copy bundled flutter_web to " << target_path.string(); - report_flutter_web_copy_failure(upgrade ? FlutterWebCopyStatus::UpgradeFailed : FlutterWebCopyStatus::InstallFailed); - return false; -} - -void GUI_App::report_flutter_web_copy_failure(FlutterWebCopyStatus status) -{ - if (status == FlutterWebCopyStatus::InstallFailed) - m_flutter_web_copy_status = FlutterWebCopyStatus::InstallFailed; - else if (status == FlutterWebCopyStatus::UpgradeFailed && - m_flutter_web_copy_status != FlutterWebCopyStatus::InstallFailed) - m_flutter_web_copy_status = FlutterWebCopyStatus::UpgradeFailed; - else - BOOST_LOG_TRIVIAL(error) << "FlutterWebCopyStatus not exit " << static_cast(status); -} - -void GUI_App::do_notify_flutter_web_copy_failure() -{ - if (m_flutter_web_copy_notified || m_flutter_web_copy_status == FlutterWebCopyStatus::Ok) - return; - - m_flutter_web_copy_notified = true; - - switch (m_flutter_web_copy_status) { - case FlutterWebCopyStatus::InstallFailed: - show_error(mainframe, - _L("Failed to install Web UI resources. Some features may not work correctly.\n" - "Please check disk space and file permissions, then restart the application.")); - break; - case FlutterWebCopyStatus::UpgradeFailed: - if (notification_manager()) { - notification_manager()->push_notification( - NotificationType::CustomNotification, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("Failed to update Web UI resources. The application will continue using the previous version.")); - } - break; - default: - BOOST_LOG_TRIVIAL(error) << "FlutterWebCopyStatus other status" << static_cast(m_flutter_web_copy_status); - break; - } -} - -void GUI_App::try_notify_flutter_web_copy_failure() -{ - if (wxThread::IsMain()) - do_notify_flutter_web_copy_failure(); - else - CallAfter([this]() { do_notify_flutter_web_copy_failure(); }); -} - void GUI_App::copy_older_config() { preset_bundle->copy_files(m_older_data_dir_path); @@ -2429,6 +2314,12 @@ void GUI_App::set_connect_host(const std::shared_ptr& input) { m_cnt_hst_mtx.unlock(); } +bool GUI_App::physical_printer_connected() const +{ + std::lock_guard lock(m_cnt_hst_mtx); + return m_connected_host != nullptr; +} + void GUI_App::on_start_subscribe_again(std::string dev_id) { auto start_subscribe_timer = new wxTimer(this, wxID_ANY); @@ -2489,6 +2380,7 @@ int GUI_App::OnExit() { ::Slic3r::SnapLog::v1::SnapLogClient::instance().shutdown(); + stop_gateway_service(); stop_sync_user_preset(); if (m_device_manager) { @@ -2681,6 +2573,9 @@ bool GUI_App::on_init_inner() // If load_language() fails, the application closes. load_language(wxString(), true); + if (!start_gateway_service()) + BOOST_LOG_TRIVIAL(warning) << "failed to start connection gateway during application startup"; + profiler.mark("gateway_service.start"); #ifdef _MSW_DARK_MODE #ifndef __WINDOWS__ @@ -2776,9 +2671,6 @@ bool GUI_App::on_init_inner() preset_bundle->setup_directories(); profiler.mark("preset_bundle->setup_directories"); - copy_web_resources(); - profiler.mark("copy_web_resources"); - if (m_init_app_config_from_older) copy_older_config(); profiler.mark("copy_older_config_if_needed"); @@ -2883,12 +2775,6 @@ bool GUI_App::on_init_inner() dlg.ShowModal(); }); - Bind(EVT_NO_WEB_RESOURCE_UPDATE, [this](const wxCommandEvent& evt) { - wxString msg = _L("This is the newest version."); - InfoDialog dlg(nullptr, _L("Info"), msg); - dlg.ShowModal(); - }); - Bind(EVT_REQUEST_SERVER_FAIL, [this](const wxCommandEvent& evt) { wxString msg = evt.GetString(); InfoDialog dlg(nullptr, _L("Error"), msg); @@ -3094,18 +2980,6 @@ bool GUI_App::on_init_inner() "configuration file.\nPlease note, application settings will be lost, but printer profiles will not be affected.")); } - do_notify_flutter_web_copy_failure(); - - // WebSocket debug server: only when Preferences → "Web Debug Mode" (websocket_debug) is on. - // When off, explicitly stop the debug server so port 8766 is not left listening. - const bool websocket_debug_pref = app_config->get_bool("websocket_debug"); - if (websocket_debug_pref) { - BOOST_LOG_TRIVIAL(debug) << "Web Debug Mode enabled in preferences, starting WebSocket debug server (port 8766)"; - Slic3r::GUI::SSWCP::enable_debug_mode(true); - } else { - Slic3r::GUI::SSWCP::enable_debug_mode(false); - } - namespace snap = ::Slic3r::SnapLog::v1; snap::SnapLogConfig snap_cfg; std::string snap_cc = app_config ? app_config->get_country_code() : ""; @@ -3165,64 +3039,6 @@ bool GUI_App::on_init_inner() return true; } -void GUI_App::machine_find() -{ - std::vector mdns_service_names; - - mdns_service_names.push_back("snapmaker"); - - Bonjour::TxtKeys txt_keys = {"sn", "version", "machine_type"}; - std::string unique_key = "sn"; - - m_machine_find_engine = Bonjour("snapmaker") - .set_txt_keys(std::move(txt_keys)) - .set_retries(3) - .set_timeout(10) - .on_reply([this](BonjourReply&& reply) { - if (!GUI_App::m_app_alive.load()) - return; - std::string hostname = reply.hostname; - size_t pos = hostname.find(".local"); - if (pos != std::string::npos) { - hostname = hostname.substr(0, pos); - } - std::string ip = reply.ip.to_string(); - - if (reply.txt_data.count("sn")) { - std::string sn = reply.txt_data["sn"]; - DeviceInfo info; - if (app_config->get_device_info(sn, info)) { - if (info.ip != ip && info.link_mode != "wan") { - info.ip = ip; - app_config->save_device_info(info); - - this->CallAfter([this]() { - auto devices = app_config->get_devices(); - json param; - param["command"] = "local_devices_arrived"; - param["sequece_id"] = "10001"; - param["data"] = devices; - std::string logout_cmd = param.dump(); - wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - GUI::wxGetApp().run_script(strJS); - - // wcp订阅 - json data = this->app_config->get_devices(); - wxGetApp().device_card_notify(data); - - }); - } - } - } - }) - .on_complete([this]() { - if (!GUI_App::m_app_alive.load()) - return; - reset_machine_find_engine(); - }) - .lookup(); -} - void GUI_App::copy_network_if_available() { if (app_config->get("update_network_plugin") != "true") @@ -3919,6 +3735,7 @@ void GUI_App::recreate_GUI(const wxString &msg_name) update_http_extra_header(); mainframe->shutdown(true); + start_gateway_service(true); ProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); dlg.Pulse(); @@ -3973,19 +3790,8 @@ void GUI_App::recreate_GUI(const wxString &msg_name) m_is_recreating_gui = false; - //reload home and device page + // Reload native views after the main frame has been recreated. sm_disconnect_current_machine(true); - auto devices = wxGetApp().app_config->get_devices(); - for (auto iter = devices.begin(); iter != devices.end();) { - if (iter->link_mode == "wan") { - iter = devices.erase(iter); - } else { - iter++; - } - } - - bool use_new_connection = wxGetApp().app_config->get("use_new_connect") == "true"; - const auto& edit_preset = preset_bundle->printers.get_edited_preset(); auto printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; auto printer_model_opt = printer_config.option("printer_model"); @@ -3997,10 +3803,8 @@ void GUI_App::recreate_GUI(const wxString &msg_name) if (!preset_bundle->is_bbl_vendor()) { if (is_snapmaker_u1) { - wxString url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(get_page_http_port()) + - "/web/flutter_web/index.html?path=2"); - auto real_url = wxGetApp().get_international_url(url); - mainframe->load_printer_url(real_url); + wxString url = wxGetApp().gateway_web_url("device_control"); + mainframe->load_printer_url(url); } else { std::string base_url = LOCALHOST_URL + std::to_string(wxGetApp().m_page_http_server.get_port()); auto url = wxString::Format("%s/web/orca/missing_connection.html", from_u8(base_url)); @@ -4008,8 +3812,6 @@ void GUI_App::recreate_GUI(const wxString &msg_name) } } - wxGetApp().device_card_notify(devices); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit"; } @@ -4029,6 +3831,7 @@ void GUI_App::keyboard_shortcuts() void GUI_App::ShowUserGuide() { // BBS:Show NewUser Guide try { + const std::string locale_before_wizard = gateway_locale(); bool res = false; GuideFrame GuideDlg(this); //if (GuideDlg.IsFirstUse()) @@ -4038,7 +3841,7 @@ if (res) { update_publish_status(); mainframe->refresh_plugin_tips(); - m_fltviews.reload_all(); + sync_gateway_region_after_wizard(locale_before_wizard); // BBS: remove SLA related message } } catch (std::exception &) { @@ -4099,8 +3902,6 @@ void GUI_App::ShowOnlyFilament() { } } - - // static method accepting a wxWindow object as first parameter bool GUI_App::catch_error(std::function cb, // wxMessageDialog* message_dialog, @@ -4293,245 +4094,44 @@ void GUI_App::import_presets() } } -void GUI_App::import_flutter_web() { - if (preset_updater) { - preset_updater->import_flutter_web(); - } else { - MessageDialog(nullptr, _L("import failed!")).ShowModal(); - } -} - -// SM -void GUI_App::sm_get_login_info() { - if (!m_login_userinfo.is_user_login() || m_login_userinfo.get_user_name().empty()) { - // default - json param; - param["command"] = "studio_useroffline"; - param["sequece_id"] = "10001"; - std::string logout_cmd = param.dump(); - wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - GUI::wxGetApp().run_script(strJS); - } else { - json param; - param["command"] = "studio_userlogin"; - param["sequece_id"] = "10001"; - json data; - data["avatar"] = m_login_userinfo.get_user_icon_url(); - data["name"] = m_login_userinfo.get_user_name(); - param["data"] = data; - std::string login_cmd = param.dump(); - wxString strJS = wxString::Format("window.postMessage(%s)", login_cmd); - GUI::wxGetApp().run_script(strJS); - } - mainframe->m_webview->SetLoginPanelVisibility(true); -} - -void GUI_App::sm_request_login(bool show_user_info) +// SM login state is owned by the connection gateway; mirror its account frames +// for native consumers (SnapLog identity, region-switch hint). +void GUI_App::apply_gateway_account(const Gateway::AccountSnapshot& snapshot) { - sm_ShowUserLogin(show_user_info); - - if (show_user_info) { - sm_get_login_info(); - } - -} - -void GUI_App::sm_ShowUserLogin(bool show) -{ - if (show) - sm_stop_silent_token_refresh(); - - // BBS: User Login Dialog - if (show) { - try { - if (!sm_login_dlg) - sm_login_dlg = new SMUserLogin(); - else { - delete sm_login_dlg; - sm_login_dlg = new SMUserLogin(); - } - m_sm_login_dialog_showing = true; - sm_login_dlg->ShowModal(); - m_sm_login_dialog_showing = false; - } catch (std::exception&) { - m_sm_login_dialog_showing = false; - ; - } - } else { - try { - if (!sm_login_dlg) - sm_login_dlg = new SMUserLogin(); - else { - delete sm_login_dlg; - sm_login_dlg = new SMUserLogin(); - } - } - catch (std::exception&) { - ; - } - } + const bool login_changed = snapshot.is_login != m_gateway_account.is_login; + m_gateway_account = snapshot; + BOOST_LOG_TRIVIAL(info) << "[gateway][account] " << (login_changed ? "login state changed: " : "account updated: ") + << (snapshot.is_login ? "online" : "offline") << ", userid=" << snapshot.userid + << ", nickname_present=" << std::boolalpha << !snapshot.nickname.empty(); + ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_user_token(snapshot.token); + ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_user_id(snapshot.userid); } -void GUI_App::sm_request_user_logout() +void GUI_App::refresh_gateway_account() { - sm_stop_silent_token_refresh(); - if (m_token_check_timer) - m_token_check_timer->Stop(); - - if (m_login_userinfo.is_user_login()) { - m_login_userinfo.set_user_login(false); - } - SNAP_LOG_BATCH(Info, "user logout", - {"eventName", "user_logout"}, {"source", "cpp"}); - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_user_token(""); - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_user_id(""); - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_connect_clientid(""); - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_print_sn(""); - try { - wxString region = wxString::FromUTF8(app_config->get_country_code()); - std::string url = ""; - if (region == "CN") { - url = "https://api.snapmaker.cn/api/oauth2/revoke"; - } else { - url = "https://id.snapmaker.com/api/oauth2/revoke"; - } - - Http http = Http::post(url); - http.form_add("token", m_login_userinfo.get_user_token()).perform(); - } catch (std::exception&) { - ; - } -} - -void GUI_App::start_flutter_wcp_timeout_watch() -{ - if (m_flutter_wcp_reported || m_flutter_wcp_timeout_timer) + if (!m_gateway_service) return; - m_flutter_wcp_timeout_timer = std::make_unique(this, wxID_ANY); - Bind(wxEVT_TIMER, &GUI_App::on_flutter_wcp_timeout, this, m_flutter_wcp_timeout_timer->GetId()); - m_flutter_wcp_timeout_timer->Start(FLUTTER_WCP_TIMEOUT_MS, wxTIMER_ONE_SHOT); -} - -void GUI_App::on_flutter_wcp_received() -{ - report_flutter_run_result_once(true); -} - -void GUI_App::on_flutter_wcp_timeout(wxTimerEvent &event) -{ - report_flutter_run_result_once(false); -} - -void GUI_App::report_flutter_run_result_once(bool success) -{ - if (m_flutter_wcp_reported) - return; - - if (m_flutter_wcp_timeout_timer) { - m_flutter_wcp_timeout_timer->Stop(); - m_flutter_wcp_timeout_timer.reset(); - } - - m_flutter_wcp_reported = true; - - if (success) { - SNAP_LOG_BATCH_FORCE(Info, "flutter run success", - {"eventName", "flutter_run_result"}, {"source", "cpp"}, {"success", "true"}); - } else { - SNAP_LOG_BATCH_FORCE(Error, "flutter run failed", - {"eventName", "flutter_run_result"}, {"source", "cpp"}, {"success", "false"}); - } -} - -void GUI_App::sm_maybe_refresh_login_token() -{ - if (!m_login_userinfo.is_user_login()) - return; - if (m_sm_login_dialog_showing || m_sm_silent_refresh_in_progress) - return; - - auto now = std::chrono::system_clock::now(); - if (now - m_token_last_refresh_success < std::chrono::hours(SM_TOKEN_REFRESH_INTERVAL_H)) - return; - if (now - m_token_last_refresh_attempt < std::chrono::minutes(SM_TOKEN_REFRESH_RETRY_MIN)) - return; - - m_token_last_refresh_attempt = now; - m_sm_silent_refresh_in_progress = true; - ++m_silent_refresh_generation; - BOOST_LOG_TRIVIAL(info) << "sm: start silent login-token refresh"; - - if (!m_silent_refresh_timeout_timer) { - m_silent_refresh_timeout_timer = std::make_unique(this, wxID_ANY); - Bind(wxEVT_TIMER, &GUI_App::on_silent_refresh_timeout, this, m_silent_refresh_timeout_timer->GetId()); - } - m_silent_refresh_timeout_timer->Start(std::chrono::seconds(SM_TOKEN_REFRESH_TIMEOUT_S).count() * 1000, wxTIMER_ONE_SHOT); - - auto refresh_generation = m_silent_refresh_generation; - CallAfter([refresh_generation]() { - if (refresh_generation == wxGetApp().sm_token_refresh_generation()) - wxGetApp().sm_ShowUserLogin(false); + const auto gateway = m_gateway_service; + const std::uint64_t generation = ++m_gateway_account_refresh_generation; + m_gateway_account_refresh_workers.emplace_back([this, gateway, generation]() { + const Gateway::GatewayService::ApiResult result = gateway->get_account(); + CallAfter([this, generation, result]() { + if (m_is_closing || generation != m_gateway_account_refresh_generation.load()) + return; + if (result.error) { + BOOST_LOG_TRIVIAL(warning) << "[gateway][account] GET /api/account failed: " << result.error.message; + return; + } + Gateway::AccountSnapshot snapshot; + if (Gateway::GatewayAccount::parse(result.value, snapshot)) + apply_gateway_account(snapshot); + else + BOOST_LOG_TRIVIAL(warning) << "[gateway][account] invalid /api/account response"; + }); }); } -void GUI_App::on_silent_refresh_timeout(wxTimerEvent &event) -{ - if (m_sm_silent_refresh_in_progress) { - m_sm_silent_refresh_in_progress = false; - BOOST_LOG_TRIVIAL(warning) << "sm: silent login-token refresh timed out, keep old token and retry later"; - } -} - -void GUI_App::sm_on_token_captured(std::size_t refresh_generation) -{ - if (refresh_generation != m_silent_refresh_generation) { - BOOST_LOG_TRIVIAL(warning) << "sm: ignore stale login-token capture"; - return; - } - - m_token_last_refresh_success = std::chrono::system_clock::now(); - if (m_sm_silent_refresh_in_progress) { - m_sm_silent_refresh_in_progress = false; - if (m_silent_refresh_timeout_timer) - m_silent_refresh_timeout_timer->Stop(); - BOOST_LOG_TRIVIAL(info) << "sm: silent login-token refresh succeeded"; - } - - if (!m_token_check_timer) { - m_token_check_timer = std::make_unique(this, wxID_ANY); - Bind(wxEVT_TIMER, &GUI_App::on_token_check_timer, this, m_token_check_timer->GetId()); - } - m_token_check_timer->Start(SM_TOKEN_CHECK_INTERVAL_MS); - - if (!m_sm_login_dialog_showing && sm_login_dlg) { - delete sm_login_dlg; - sm_login_dlg = nullptr; - } -} - -bool GUI_App::sm_is_token_refresh_current(std::size_t refresh_generation) const -{ return refresh_generation == m_silent_refresh_generation; } - -void GUI_App::on_token_check_timer(wxTimerEvent &event) -{ - sm_maybe_refresh_login_token(); -} - -void GUI_App::sm_stop_silent_token_refresh() -{ - ++m_silent_refresh_generation; - m_sm_silent_refresh_in_progress = false; - if (m_silent_refresh_timeout_timer) - m_silent_refresh_timeout_timer->Stop(); - - // Drop the hidden login dialog so a late redirect cannot re-login the user. - if (!m_sm_login_dialog_showing && sm_login_dlg) { - delete sm_login_dlg; - sm_login_dlg = nullptr; - } -} - //BBS void GUI_App::request_login(bool show_user_info) { @@ -4560,32 +4160,6 @@ void GUI_App::get_login_info() } } -wxString GUI_App::get_international_url(const wxString& origin_url) { - - wxString baseUrl = origin_url; - if (baseUrl.find("?locale=") != std::string::npos) { - baseUrl = baseUrl.substr(0, baseUrl.find("?locale=")); - } else if (baseUrl.find("&locale=") != std::string::npos) { - baseUrl = baseUrl.substr(0, baseUrl.find("&locale=")); - } - wxString lang = wxString::FromUTF8(app_config->get_language_code()); - wxString region = wxString::FromUTF8(app_config->get_country_code()); - if (region == "Others") { - region = "US"; - } - - string dark_mode = wxGetApp().app_config->get("dark_color_mode"); - - if (baseUrl.find("?") != std::string::npos) { - return baseUrl + wxString::FromUTF8("&locale=") + lang + wxString::FromUTF8("-") + region + - wxString::FromUTF8("&dark_mode=" + dark_mode); - } else { - return baseUrl + wxString::FromUTF8("?locale=") + lang + wxString::FromUTF8("-") + region + - wxString::FromUTF8("&dark_mode=" + dark_mode); - } - -} - bool GUI_App::is_user_login() { if (m_agent) { @@ -5268,12 +4842,6 @@ void maybe_attach_updater_signature(Http& http, const std::string& canonical_que } // namespace -void GUI_App::check_web_version() -{ - if (preset_updater != nullptr) - preset_updater->sync_web_async(); -} - void GUI_App::check_preset_version() { if (preset_updater != nullptr) @@ -5475,8 +5043,9 @@ void GUI_App::request_version_from_config(bool show_tips, bool by_user) allow_http_auth = true; #endif bool with_auth = false; - if (sm_get_userinfo()->is_user_login()) { - std::string auth_token = sm_get_userinfo()->get_user_token(); + const Gateway::AccountSnapshot& account = m_gateway_account; + if (account.is_login) { + const std::string& auth_token = account.token; if (!auth_token.empty()) { if (url.rfind("https://", 0) == 0 || allow_http_auth) { http.header("Authorization", auth_token); @@ -5684,7 +5253,6 @@ void GUI_App::no_new_version() } std::string GUI_App::version_display = ""; - std::string GUI_App::format_display_version() { if (!version_display.empty()) return version_display; @@ -6070,6 +5638,332 @@ void GUI_App::start_page_http_server() if (!m_page_http_server.is_started()) m_page_http_server.start(); } + +std::string GUI_App::gateway_locale() const +{ + std::string locale = app_config != nullptr ? app_config->get("language") : std::string{}; + if (locale.empty()) + locale = into_u8(current_language_code_safe()); + + const std::size_t separator = locale.find_first_of("-_"); + std::string language = separator == std::string::npos ? locale : locale.substr(0, separator); + std::transform(language.begin(), language.end(), language.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + if (language != "zh") + language = "en"; + + std::string region = app_config != nullptr ? app_config->get_country_code() : "US"; + if (region != "CN") + region = "US"; + + return language + "-" + region; +} + +bool GUI_App::start_gateway_service(bool restart) +{ + if (m_gateway_service && !restart) + return true; + + if (restart) + stop_gateway_service(); + + try { + Gateway::GatewayService::Dependencies dependencies; + const boost::filesystem::path connection_cli_path = + boost::filesystem::path{Slic3r::resources_dir()} / Gateway::ConnectionProcessManager::cli_executable_name(); + dependencies.process_manager = + std::make_shared(Gateway::ConnectionProcessManager::Config{connection_cli_path}); + dependencies.http = std::make_shared(); + dependencies.websocket = std::make_shared(); + dependencies.dispatcher = [this](std::function task) { + // A task may still be pending when shutdown starts; do not touch dying GUI windows. + CallAfter([this, task = std::move(task)]() mutable { + if (m_is_closing) + return; + task(); + }); + }; + + m_gateway_service = std::make_shared(Gateway::GatewayService::Config{}, std::move(dependencies)); + m_gateway_device = GatewayDeviceState{}; + if (preset_bundle != nullptr) + clear_gateway_machine_slots(); + m_gateway_loaded_base_url.clear(); + register_gateway_notifications(); + const std::string locale = gateway_locale(); + BOOST_LOG_TRIVIAL(info) << "starting connection gateway with locale " << locale; + if (!m_gateway_service->start(locale)) + return false; + BOOST_LOG_TRIVIAL(info) << "connection gateway startup requested"; + return true; + } catch (const std::exception& exception) { + BOOST_LOG_TRIVIAL(error) << "failed to start connection gateway: " << exception.what(); + m_gateway_service.reset(); + return false; + } +} + +void GUI_App::stop_gateway_service() +{ + ++m_gateway_account_refresh_generation; + for (std::thread& worker : m_gateway_account_refresh_workers) + if (worker.joinable()) + worker.join(); + m_gateway_account_refresh_workers.clear(); + + if (m_gateway_service) + m_gateway_service->stop(); + m_gateway_service.reset(); + clear_gateway_device(); +} + +bool GUI_App::gateway_device_connected() const { return m_gateway_device.connected && !m_gateway_device.serial_number.empty(); } + +void GUI_App::query_gateway_current_device() +{ + if (!m_gateway_service) + return; + + const std::uint64_t generation = m_gateway_device.generation; + m_gateway_service->request("query.device.current", nlohmann::json::object(), + [this, generation](Gateway::GatewayError error, const nlohmann::json& result) { + if (generation != m_gateway_device.generation) + return; + + if (error) { + BOOST_LOG_TRIVIAL(warning) + << "[gateway][device-status] failed to query current device: " << error.message; + m_gateway_device.connected = false; + if (mainframe != nullptr && mainframe->plater() != nullptr) + mainframe->plater()->sidebar().update_all_preset_comboboxes(false); + return; + } + + const auto current_device = Gateway::parse_current_device(result); + if (!current_device.has_value() || !current_device->valid) { + BOOST_LOG_TRIVIAL(warning) << "[gateway][device-status] current device response is invalid"; + m_gateway_device.connected = false; + return; + } + if (!current_device->connected || current_device->serial_number.empty()) { + clear_gateway_device(); + return; + } + + const bool device_changed = m_gateway_device.serial_number != current_device->serial_number; + if (device_changed) { + clear_gateway_device(); + m_gateway_device.generation = generation + 1; + } + m_gateway_device.serial_number = current_device->serial_number; + m_gateway_device.connected = true; + BOOST_LOG_TRIVIAL(info) + << "[gateway][device-status] current device applied, sn=" << m_gateway_device.serial_number; + + if (mainframe != nullptr && mainframe->plater() != nullptr) + mainframe->plater()->sidebar().update_all_preset_comboboxes(false); + query_gateway_device_objects(); + + if (m_gateway_device.watched_serial_number == current_device->serial_number) + return; + const std::string serial_number = current_device->serial_number; + m_gateway_device.watched_serial_number = serial_number; + m_gateway_service->watch_device(nlohmann::json{{"sn", serial_number}}, + [serial_number](Gateway::GatewayError watch_error, + const nlohmann::json& watch_result) { + if (watch_error) + BOOST_LOG_TRIVIAL(warning) + << "[gateway][device-status] failed to watch device " + << serial_number << ": " << watch_error.message; + else + BOOST_LOG_TRIVIAL(info) + << "[gateway][device-status] watching device " + << serial_number << ", result=" << watch_result.dump(); + }); + }); +} + +void GUI_App::query_gateway_device_objects() +{ + if (!m_gateway_service || !m_gateway_device.connected || m_gateway_device.serial_number.empty()) + return; + if (m_gateway_device.objects_query_active) { + m_gateway_device.objects_refresh_pending = true; + return; + } + + m_gateway_device.objects_query_active = true; + const std::uint64_t generation = m_gateway_device.generation; + const std::string serial_number = m_gateway_device.serial_number; + m_gateway_service->request("query.device.objects", nlohmann::json::object(), + [this, generation, serial_number](Gateway::GatewayError error, const nlohmann::json& result) { + if (generation != m_gateway_device.generation || serial_number != m_gateway_device.serial_number) + return; + + m_gateway_device.objects_query_active = false; + const bool refresh_again = m_gateway_device.objects_refresh_pending; + m_gateway_device.objects_refresh_pending = false; + + if (error) { + BOOST_LOG_TRIVIAL(warning) + << "[gateway][device-status] failed to query device objects: " << error.message; + if (refresh_again) + query_gateway_device_objects(); + return; + } + + std::vector slots; + if (!Gateway::GatewayDevice::parse_machine_slots(result, slots)) { + BOOST_LOG_TRIVIAL(warning) << "[gateway][device-status] device objects cannot build machine slots"; + if (refresh_again) + query_gateway_device_objects(); + return; + } + + PresetBundle* bundle = preset_bundle; + if (bundle == nullptr) { + BOOST_LOG_TRIVIAL(error) + << "[gateway][device-status] cannot apply machine slots before preset bundle is ready"; + return; + } + if (!Gateway::GatewayDevice::machine_slots_equal(bundle->m_connect_machine_info_list, slots)) { + apply_gateway_machine_slots(std::move(slots)); + refresh_gateway_machine_slots_ui(); + } + if (refresh_again) + query_gateway_device_objects(); + }); +} + +void GUI_App::clear_gateway_device() +{ + const bool had_slots = preset_bundle != nullptr && !preset_bundle->m_connect_machine_info_list.empty(); + const std::uint64_t generation = m_gateway_device.generation + 1; + m_gateway_device = GatewayDeviceState{}; + m_gateway_device.generation = generation; + if (preset_bundle != nullptr) + clear_gateway_machine_slots(); + if (had_slots) + refresh_gateway_machine_slots_ui(); +} + +void GUI_App::apply_gateway_machine_slots(std::vector slots) +{ + if (preset_bundle == nullptr) + return; + preset_bundle->m_connect_machine_info_list = std::move(slots); +} + +void GUI_App::clear_gateway_machine_slots() +{ + if (preset_bundle == nullptr) + return; + preset_bundle->m_connect_machine_info_list.clear(); +} + +void GUI_App::refresh_gateway_machine_slots_ui() +{ + if (m_is_closing) + return; + load_current_presets(); + if (mainframe != nullptr && mainframe->plater() != nullptr) + mainframe->plater()->sidebar().update_all_preset_comboboxes(false); +} + +void GUI_App::register_gateway_notifications() +{ + if (!m_gateway_service) + return; + + m_gateway_service->set_notification_handler("notify.account.changed", [this](const nlohmann::json& params) { + Gateway::AccountSnapshot snapshot; + if (!Gateway::GatewayAccount::parse(params, snapshot)) { + BOOST_LOG_TRIVIAL(warning) << "[gateway][account] ignored invalid notify.account.changed frame"; + return; + } + apply_gateway_account(snapshot); + if (snapshot.is_login && snapshot.token.empty()) + refresh_gateway_account(); + }); + + m_gateway_service->set_notification_handler("notify.device.connected", + [this](const nlohmann::json&) { query_gateway_current_device(); }); + m_gateway_service->set_notification_handler("notify.device.current_changed", + [this](const nlohmann::json&) { query_gateway_current_device(); }); + m_gateway_service->set_notification_handler("notify.device.object.changed", [this](const nlohmann::json& params) { + if (!m_gateway_device.connected || !Gateway::GatewayDevice::delta_affects_machine_slots(params)) + return; + query_gateway_device_objects(); + }); + m_gateway_service->set_notification_handler("notify.device.disconnected", [this](const nlohmann::json& params) { + const std::string serial_number = Gateway::parse_device_sn(params); + if (serial_number.empty() || m_gateway_device.serial_number == serial_number) + clear_gateway_device(); + }); + + m_gateway_service->set_state_callback([this](Gateway::ConnectionState state, const Gateway::GatewayError& error) { + if (state == Gateway::ConnectionState::Disconnected) { + BOOST_LOG_TRIVIAL(warning) << "connection gateway disconnected: " << error.message; + m_gateway_loaded_base_url.clear(); + clear_gateway_device(); + return; + } + if (state != Gateway::ConnectionState::Connected) + return; + + if (!m_gateway_service) + return; + + refresh_gateway_account(); + + const std::string base_url = m_gateway_service->base_url(); + const bool endpoint_changed = m_gateway_loaded_base_url != base_url; + m_gateway_loaded_base_url = base_url; + query_gateway_current_device(); + + if (endpoint_changed && mainframe != nullptr) { + mainframe->reload_gateway_pages(); + if (WebPreprintDialog* preprint_dialog = dynamic_cast(get_web_preprint_dialog())) + preprint_dialog->refresh_gateway_urls(); + } + }); +} + +wxString GUI_App::gateway_web_url(const wxString& page_key) const +{ + const std::string url = m_gateway_service ? m_gateway_service->web_url(into_u8(page_key)) : std::string{}; + if (!url.empty()) + return from_u8(url); + + return wxString::FromUTF8(LOCALHOST_URL + std::to_string(get_page_http_port()) + "/web/orca/missing_connection.html"); +} + +std::string GUI_App::gateway_localfile_url(const std::string& file_path) const +{ + return m_gateway_service ? m_gateway_service->localfile_url(file_path) : std::string{}; +} + +Gateway::PreprintStoreResult GUI_App::gateway_store_preprint_context(const std::string& id, const nlohmann::json& payload) const +{ + if (!m_gateway_service) + return {{Gateway::GatewayErrorCode::NotConnected, "connection gateway is not available"}}; + return m_gateway_service->store_preprint_context(id, payload); +} + +bool GUI_App::is_gateway_url(const wxString& url) const +{ + const std::string base_url = m_gateway_service ? m_gateway_service->base_url() : std::string{}; + if (base_url.empty()) + return false; + const std::string candidate = into_u8(url); + if (candidate.rfind(base_url, 0) != 0) + return false; + if (candidate.size() == base_url.size()) + return true; + switch (candidate[base_url.size()]) { + case '/': case '?': case '#': return true; + default: return false; + } +} void GUI_App::stop_page_http_server() { m_page_http_server.stop(); @@ -7351,6 +7245,7 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage { wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + const std::string locale_before_wizard = gateway_locale(); std::string strFinish = wxGetApp().app_config->get("firstguide", "finish"); long pStyle = wxCAPTION | wxCLOSE_BOX | wxSYSTEM_MENU; if (strFinish == "false" || strFinish.empty()) @@ -7378,7 +7273,7 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage wxGetApp().sidebar().update_nozzle_settings(); update_publish_status(); mainframe->refresh_plugin_tips(); - m_fltviews.reload_all(); + sync_gateway_region_after_wizard(locale_before_wizard); // BBS: remove SLA related message } auto isAgree = wxGetApp().app_config->get("app", PRIVACY_POLICY_FLAGS); @@ -7388,6 +7283,27 @@ bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage return res; } +void GUI_App::sync_gateway_region_after_wizard(const std::string& locale_before_wizard) +{ + const std::string locale_after_wizard = gateway_locale(); + bool gateway_refreshed = false; + if (locale_after_wizard != locale_before_wizard && m_gateway_service) { + const std::size_t separator = locale_after_wizard.find_last_of("-_"); + if (separator != std::string::npos && separator + 1 < locale_after_wizard.size()) { + const std::string region = locale_after_wizard.substr(separator + 1); + const auto result = m_gateway_service->set_region(region); + if (result.error) { + BOOST_LOG_TRIVIAL(warning) << "failed to update connection gateway region: " << result.error.message; + } else { + BOOST_LOG_TRIVIAL(info) << "connection gateway region updated to " << region << " with locale " << locale_after_wizard; + gateway_refreshed = true; + } + } + } + if (!gateway_refreshed) + m_fltviews.reload_all(); +} + void GUI_App::show_desktop_integration_dialog() { #ifdef __linux__ @@ -7542,17 +7458,6 @@ void GUI_App::recent_file_notify(const json& res) } } -void GUI_App::device_card_notify(const json& res) -{ - for (const auto& instance : m_device_card_subscribers) { - auto ptr = instance.second.lock(); - if (ptr) { - ptr->m_res_data = res; - ptr->send_to_js(); - } - } -} - void GUI_App::page_state_notify_webview(wxWebView* webview, const std::string& state) { if (!webview) return; @@ -7573,9 +7478,6 @@ void GUI_App::page_state_notify_webview(wxWebView* webview, const std::string& s void GUI_App::notify_foreground_change(const bool active) { - if (active) - sm_maybe_refresh_login_token(); - json data; data["state"] = active; @@ -7588,23 +7490,6 @@ void GUI_App::notify_foreground_change(const bool active) } } -void GUI_App::cache_notify(const std::string& key, const json& res) -{ - for (const auto& instance : m_cache_subscribers) { - auto ptr = instance.first.second.lock(); - if (ptr) { - std::string cache_key = instance.second; - if (cache_key == key) { - json object = json::object(); - object[key] = res; - - ptr->m_res_data = object; - ptr->send_to_js(); - } - } - } -} - void GUI_App::user_update_privacy_notify(const bool& res) { set_privacy_policy(res); @@ -7624,17 +7509,6 @@ void GUI_App::user_update_privacy_notify(const bool& res) } -void GUI_App::user_login_notify(const json& res) -{ - for (const auto& instance : m_user_login_subscribers) { - auto ptr = instance.second.lock(); - if (ptr) { - ptr->m_res_data = res; - ptr->send_to_js(); - } - } -} - bool GUI_App::config_wizard_startup() { auto isAgree = wxGetApp().app_config->get("app", PRIVACY_POLICY_FLAGS); @@ -7904,37 +7778,8 @@ bool GUI_App::sm_disconnect_current_machine(bool need_reload_printerview) if (true) { wxGetApp().CallAfter([this, need_reload_printerview](){ - wxGetApp().app_config->set("use_new_connect", "false"); - /*auto p_config = &(wxGetApp().preset_bundle->printers.get_edited_preset().config); - p_config->set("print_host", "");*/ - - auto devices = wxGetApp().app_config->get_devices(); - for (size_t i = 0; i < devices.size(); ++i) { - if (devices[i].connected) { - devices[i].connected = false; - wxGetApp().app_config->save_device_info(devices[i]); - break; - } - } - - //// 同步卡片 - //json param; - //param["command"] = "local_devices_arrived"; - //param["sequece_id"] = "10001"; - //param["data"] = devices; - //std::string logout_cmd = param.dump(); - //wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - //GUI::wxGetApp().run_script(strJS); - - // wcp订阅 - json data = this->app_config->get_devices(); - wxGetApp().device_card_notify(data); - - wxGetApp().mainframe->plater()->sidebar().update_all_preset_comboboxes(need_reload_printerview); wxGetApp().set_connect_host(nullptr); - - wxGetApp().preset_bundle->machine_filaments.clear(); - // wxGetApp().load_current_presets(); + wxGetApp().mainframe->plater()->sidebar().update_all_preset_comboboxes(need_reload_printerview); }); @@ -7962,22 +7807,6 @@ void GUI_App::start_download(std::string url) } -void GUI_App::SMUserInfo::notify() { - json data; - if (m_login) { - data["status"] = "online"; - data["nickname"] = m_login_user_name; - data["icon"] = m_login_user_icon_url; - data["token"] = m_login_user_token; - data["userid"] = m_login_user_id; - data["account"] = m_login_user_account; - } else { - data["status"] = "offline"; - } - - wxGetApp().user_login_notify(data); - -} //--------------------------------------------------------------------------- // Extensible incompatible support-material pair detection. // Each entry: {interface_material, incompatible_model_material}. @@ -8109,7 +7938,6 @@ bool check_pla_petg_support_pair(int extruder_id) return false; } - bool is_support_filament(int extruder_id) { auto &filament_presets = Slic3r::GUI::wxGetApp().preset_bundle->filament_presets; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 9258fbc5f9e2..24e40d3d877e 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -13,7 +13,7 @@ #include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/GUI/WebViewDialog.hpp" #include "slic3r/GUI/WebUserLoginDialog.hpp" -#include "slic3r/GUI/WebSMUserLoginDialog.hpp" +#include "slic3r/Utils/GatewayAccount.hpp" #include "slic3r/GUI/WebDeviceDialog.hpp" #include "slic3r/GUI/WebPreprintDialog.hpp" #include "slic3r/GUI/BindDialog.hpp" @@ -32,9 +32,12 @@ #include #include +#include #include #include +#include #include +#include //#define BBL_HAS_FIRST_PAGE 1 #define STUDIO_INACTIVE_TIMEOUT 15*60*1000 #define LOG_FILES_MAX_NUM 30 @@ -77,6 +80,12 @@ class DeviceManager; class NetworkAgent; class TaskManager; +namespace Gateway { +class GatewayService; +struct CurrentDevice; +struct PreprintStoreResult; +} + namespace GUI{ class RemovableDriveManager; @@ -98,7 +107,6 @@ class ModelMallDialog; class PingCodeBindDialog; class NetworkErrorDialog; - enum FileType { FT_STEP, @@ -259,8 +267,6 @@ class GUI_App : public wxApp bool m_app_conf_exists{ false }; EAppMode m_app_mode{ EAppMode::Editor }; bool m_is_recreating_gui{ false }; - /// Set only for the duration of `MsgUpdateConfig::ShowModal()` in load_flutter_web (atomic: safe vs updater threads + CallAfter). - std::atomic m_flutter_web_config_update_dlg_open{ false }; /// Set only for the duration of profile/preset `MsgUpdateConfig::ShowModal()` (atomic: safe vs updater threads + CallAfter). std::atomic m_profile_config_update_dlg_open{ false }; #ifdef __linux__ @@ -327,7 +333,6 @@ class GUI_App : public wxApp // login widget ZUserLogin* login_dlg { nullptr }; - SMUserLogin* sm_login_dlg{ nullptr }; public: @@ -352,19 +357,28 @@ class GUI_App : public wxApp public: HttpServer m_page_http_server; + mutable std::shared_ptr m_gateway_service; private: bool m_show_gcode_window{true}; boost::thread m_check_network_thread; std::shared_ptr m_connected_host = nullptr; - std::mutex m_cnt_hst_mtx; + mutable std::mutex m_cnt_hst_mtx; DynamicPrintConfig m_host_config; std::mutex m_host_cfg_mtx; - wxTimer* m_machine_find_timer = nullptr; - std::shared_ptr m_machine_find_engine = nullptr; - const int m_machine_find_id = 10086; + struct GatewayDeviceState + { + std::uint64_t generation{0}; + std::string serial_number; + bool connected{false}; + bool objects_query_active{false}; + bool objects_refresh_pending{false}; + std::string watched_serial_number; + }; + GatewayDeviceState m_gateway_device; + std::string m_gateway_loaded_base_url; public: DynamicPrintConfig* get_host_config() { @@ -378,10 +392,6 @@ class GUI_App : public wxApp void import_presets(); - void import_flutter_web(); - - void reset_machine_find_engine() { m_machine_find_engine = nullptr; } - void set_host_config(const DynamicPrintConfig& config) { m_host_cfg_mtx.lock(); @@ -390,6 +400,7 @@ class GUI_App : public wxApp } void get_connect_host(std::shared_ptr& output); void set_connect_host(const std::shared_ptr& intput); + bool physical_printer_connected() const; wxDialog* get_web_device_dialog() { return web_device_dialog; } void set_web_preprint_dialog(WebPreprintDialog* obj) { web_preprint_dialog = obj; } wxDialog* get_web_preprint_dialog() { return web_preprint_dialog; } @@ -409,7 +420,6 @@ class GUI_App : public wxApp //explicit GUI_App(EAppMode mode = EAppMode::Editor); ~GUI_App() override; - void machine_find(); void show_message_box(std::string msg) { wxMessageBox(msg); } EAppMode get_app_mode() const { return m_app_mode; } Slic3r::DeviceManager* getDeviceManager() { return m_device_manager; } @@ -419,14 +429,6 @@ class GUI_App : public wxApp bool is_editor() const { return m_app_mode == EAppMode::Editor; } bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; } bool is_recreating_gui() const { return m_is_recreating_gui; } - bool flutter_web_config_update_dlg_open() const - { - return m_flutter_web_config_update_dlg_open.load(std::memory_order_acquire); - } - void set_flutter_web_config_update_dlg_open(bool v) - { - m_flutter_web_config_update_dlg_open.store(v, std::memory_order_release); - } bool profile_config_update_dlg_open() const { return m_profile_config_update_dlg_open.load(std::memory_order_acquire); } void set_profile_config_update_dlg_open(bool v) { m_profile_config_update_dlg_open.store(v, std::memory_order_release); } std::string logo_name() const { return is_editor() ? "Snapmaker_Orca" : "Snapmaker_Orca-gcodeviewer"; } @@ -543,70 +545,12 @@ class GUI_App : public wxApp void get_login_info(); bool is_user_login(); - wxString get_international_url(const wxString& origin_url); - - // SM - struct SMUserInfo - { - public: - bool is_user_login() { return m_login; } - void set_user_login(bool login) - { - m_login = login; - notify(); - } - - std::string get_user_name() { return m_login_user_name; } - void set_user_name(const std::string& name) { m_login_user_name = name; } - - std::string get_user_token() { return m_login_user_token; } - void set_user_token(const std::string& token) { m_login_user_token = token; } - - std::string get_user_icon_url() { return m_login_user_icon_url; } - void set_user_icon_url(const std::string& url) { m_login_user_icon_url = url; } - - std::string get_user_id() { return m_login_user_id; } - void set_user_id(const std::string& id) { m_login_user_id = id; } - - std::string get_user_account() { return m_login_user_account; } - void set_user_account(const std::string& account) { m_login_user_account = account; } - - void clear() { - m_login_user_name = ""; - m_login_user_token = ""; - m_login_user_icon_url = ""; - m_login_user_id = ""; - m_login_user_account = ""; - m_login = false; - } - - void notify(); - private: - std::string m_login_user_name = ""; - std::string m_login_user_token = ""; - std::string m_login_user_icon_url = ""; - std::string m_login_user_id = ""; - std::string m_login_user_account = ""; - bool m_login = false; - }; - - SMUserInfo* sm_get_userinfo() { return &m_login_userinfo; } - void sm_get_login_info(); - void sm_request_login(bool show_user_info = false); - void sm_ShowUserLogin(bool show = true); - void sm_request_user_logout(); - void start_flutter_wcp_timeout_watch(); - void on_flutter_wcp_received(); - void report_flutter_run_result_once(bool success); - - // Silent login-token maintenance: the Snapmaker access token expires after - // ~24 h; a hidden login webview re-runs the cookie session and picks up a - // fresh token without user interaction. - void sm_maybe_refresh_login_token(); // due-check + guards; main thread - void sm_on_token_captured(std::size_t refresh_generation); // call on every token acquisition - void sm_stop_silent_token_refresh(); // drop an in-flight silent refresh - bool sm_is_token_refresh_current(std::size_t refresh_generation) const; - std::size_t sm_token_refresh_generation() const { return m_silent_refresh_generation; } + // SM login state is owned by the connection gateway. The app only mirrors + // GET /api/account / notify.account.changed frames for native consumers + // (currently SnapLog identity and the region-switch hint). + const Gateway::AccountSnapshot& gateway_account() const { return m_gateway_account; } + void apply_gateway_account(const Gateway::AccountSnapshot& snapshot); + void refresh_gateway_account(); void request_user_logout(); int request_user_unbind(std::string dev_id); @@ -628,7 +572,6 @@ class GUI_App : public wxApp bool m_studio_active = true; std::chrono::system_clock::time_point last_active_point; - void check_web_version(); void check_preset_version(); void check_new_version_sf(bool show_tips = false, bool by_user = false); // Gray release: POST /config/get (snapmaker-config) first, falls back to check_new_version_sf on failure @@ -655,11 +598,15 @@ class GUI_App : public wxApp /// Actual listen port (may differ from PAGE_HTTP_PORT if the default was in use). boost::asio::ip::port_type get_page_http_port() const { return m_page_http_server.get_port(); } - enum class FlutterWebCopyStatus { Ok, UpgradeFailed, InstallFailed, Other }; - /// Copy bundled flutter_web into the user data directory. On failure, records status for deferred user notification. - bool copy_bundled_flutter_web(bool upgrade); - void report_flutter_web_copy_failure(FlutterWebCopyStatus status); - void try_notify_flutter_web_copy_failure(); + bool start_gateway_service(bool restart = false); + void stop_gateway_service(); + wxString gateway_web_url(const wxString& page_key) const; + std::string gateway_localfile_url(const std::string& file_path) const; + Gateway::PreprintStoreResult gateway_store_preprint_context(const std::string& id, const nlohmann::json& payload) const; + bool is_gateway_url(const wxString& url) const; + std::shared_ptr gateway_service() const { return m_gateway_service; } + bool gateway_device_connected() const; + void switch_staff_pick(bool on); bool check_privacy_update(); @@ -672,7 +619,19 @@ class GUI_App : public wxApp bool switch_language(); bool load_language(wxString language, bool initial); + std::string gateway_locale() const; +private: + void sync_gateway_region_after_wizard(const std::string& locale_before_wizard); + void register_gateway_notifications(); + void query_gateway_current_device(); + void query_gateway_device_objects(); + void clear_gateway_device(); + void apply_gateway_machine_slots(std::vector slots); + void clear_gateway_machine_slots(); + void refresh_gateway_machine_slots_ui(); + +public: Tab* get_tab(Preset::Type type); Tab* get_plate_tab(); Tab* get_model_tab(bool part = false); @@ -860,8 +819,6 @@ class GUI_App : public wxApp void update_http_extra_header(); bool check_older_app_config(Semver current_version, bool backup); void copy_older_config(); - void copy_web_resources(); - void do_notify_flutter_web_copy_failure(); void window_pos_save(wxTopLevelWindow* window, const std::string &name); bool window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized = false); void window_pos_sanitize(wxTopLevelWindow* window); @@ -876,55 +833,21 @@ class GUI_App : public wxApp std::string m_older_data_dir_path; boost::optional m_last_config_version; bool m_config_corrupted { false }; - FlutterWebCopyStatus m_flutter_web_copy_status{ FlutterWebCopyStatus::Ok }; - bool m_flutter_web_copy_notified{ false }; - bool m_flutter_wcp_reported{false}; - std::unique_ptr m_flutter_wcp_timeout_timer; - static constexpr int FLUTTER_WCP_TIMEOUT_MS = 120 * 1000; - void on_flutter_wcp_timeout(wxTimerEvent &event); std::string m_open_method; - SMUserInfo m_login_userinfo; - - // --- Silent login-token refresh bookkeeping (see sm_maybe_refresh_login_token) --- - static constexpr int SM_TOKEN_REFRESH_INTERVAL_H = 12; // refresh cadence, well inside the 24 h token lifetime - static constexpr int SM_TOKEN_REFRESH_RETRY_MIN = 30; // min wait after a failed attempt - static constexpr int SM_TOKEN_REFRESH_TIMEOUT_S = 120; // give up on a single silent attempt - static constexpr int SM_TOKEN_CHECK_INTERVAL_MS = 5 * 60 * 1000; // periodic due-check tick - - std::chrono::system_clock::time_point m_token_last_refresh_success{}; - std::chrono::system_clock::time_point m_token_last_refresh_attempt{}; - std::size_t m_silent_refresh_generation = 0; - bool m_sm_silent_refresh_in_progress = false; - bool m_sm_login_dialog_showing = false; - std::unique_ptr m_token_check_timer; - std::unique_ptr m_silent_refresh_timeout_timer; - void on_token_check_timer(wxTimerEvent &event); - void on_silent_refresh_timeout(wxTimerEvent &event); + Gateway::AccountSnapshot m_gateway_account; + std::atomic m_gateway_account_refresh_generation{0}; + std::vector m_gateway_account_refresh_workers; public: std::unordered_map> m_recent_file_subscribers; - std::unordered_map> m_user_login_subscribers; - std::unordered_map> m_device_card_subscribers; std::unordered_map> m_page_state_subscribers; std::unordered_map> m_foreground_change_subscribers; std::unordered_map> m_user_update_privacy_subscribers; - struct CachePairCompare - { - bool operator()(const std::pair>& lhs, - const std::pair>& rhs) const - { - return lhs.first <= rhs.first; - } - }; - std::map>, std::string, CachePairCompare> m_cache_subscribers; - void recent_file_notify(const json& res); - void user_login_notify(const json& res); - void device_card_notify(const json& res); void page_state_notify_webview(wxWebView* webview, const std::string& state); + // Push foreground/background state change to all subscribed webview instances void notify_foreground_change(const bool active); - void cache_notify(const std::string& key, const json& res); void user_update_privacy_notify(const bool& res); public: @@ -981,20 +904,19 @@ class GUI_App : public wxApp void reload_all() { for (const auto& view : webviews) { auto ptr = view.first; - wxString new_url = app->get_international_url(view.second); - ptr->LoadURL(new_url); + ptr->LoadURL(view.second); } for (const auto& panel : webview_panels) { auto ptr = panel.first; - wxString new_url = app->get_international_url(panel.second); - ptr->load_url(new_url); + wxString url = panel.second; + ptr->load_url(url); } for (const auto& prview : printerviews) { auto ptr = prview.first; - wxString new_url = app->get_international_url(prview.second.first); - ptr->load_url(new_url, prview.second.second); + auto url = prview.second.first; + ptr->load_url(url, prview.second.second); } } }; diff --git a/src/slic3r/GUI/HintNotification.cpp b/src/slic3r/GUI/HintNotification.cpp index f62c94cf5b56..a582324e0e1c 100644 --- a/src/slic3r/GUI/HintNotification.cpp +++ b/src/slic3r/GUI/HintNotification.cpp @@ -317,12 +317,14 @@ void HintDatabase::init() } void HintDatabase::init_random_hint_id() { - if (m_loaded_hints.empty()) { - m_hint_id = 0; - return; - } - srand(time(NULL)); - m_hint_id = rand() % m_loaded_hints.size(); + if (m_loaded_hints.empty()) { + BOOST_LOG_TRIVIAL(error) << "There were no hints loaded from hints.ini file."; + m_hint_id = 0; + return; + } + + srand(time(NULL)); + m_hint_id = rand() % m_loaded_hints.size(); } void HintDatabase::load_hints_from_file(const boost::filesystem::path& path) { diff --git a/src/slic3r/GUI/HttpServer.cpp b/src/slic3r/GUI/HttpServer.cpp index 5c35b0528ca9..927b2be97bd8 100644 --- a/src/slic3r/GUI/HttpServer.cpp +++ b/src/slic3r/GUI/HttpServer.cpp @@ -433,7 +433,7 @@ void HttpServer::IOServer::stop_all() HttpServer::IOServer::IOServer(HttpServer& server) : server(server), acceptor(io_service) { try { - boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), server.port); + boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::address_v4::loopback(), server.port); acceptor.open(endpoint.protocol()); acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true)); acceptor.bind(endpoint); @@ -462,7 +462,7 @@ bool HttpServer::is_port_available(boost::asio::ip::port_type port) try { boost::asio::io_service io_service; boost::asio::ip::tcp::acceptor acceptor(io_service); - boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), port); + boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::address_v4::loopback(), port); acceptor.open(endpoint.protocol()); acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true)); @@ -1007,8 +1007,8 @@ std::string HttpServer::map_url_to_file_path(const std::string& url) trimmed_url = trimmed_url.substr(0, question_mark); } - if (trimmed_url == "/") { - trimmed_url = "/flutter_web/index.html"; // defualt home page + if (trimmed_url == "/" || trimmed_url.rfind("/web/flutter_web/", 0) == 0) { + return ""; } else if (trimmed_url.substr(0, 11) == "/localfile/") { auto real_path = trimmed_url.substr(11); @@ -1041,21 +1041,7 @@ std::string HttpServer::map_url_to_file_path(const std::string& url) return decoded; } - auto data_web_path = boost::filesystem::path(data_dir()) / "web"; - if (!boost::filesystem::exists(data_web_path / "flutter_web")) { - if (!GUI::wxGetApp().copy_bundled_flutter_web(false)) - GUI::wxGetApp().try_notify_flutter_web_copy_failure(); - } - - wxString res = ""; - if (trimmed_url.find("flutter_web") == std::string::npos) - { - res = wxString::FromUTF8(resources_dir()) + trimmed_url; - } - else - { - res = wxString::FromUTF8(data_dir()) + trimmed_url; - } + wxString res = wxString::FromUTF8(resources_dir()) + trimmed_url; auto strUTF8 = res.ToStdString(wxConvUTF8); @@ -1176,4 +1162,4 @@ void HttpServer::ResponseFile::write_response(std::stringstream& ssOut) ssOut << fileContent; // 响应体(长度必须与Content-Length一致) } -}} // namespace Slic3r::GUI \ No newline at end of file +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/HttpServer.hpp b/src/slic3r/GUI/HttpServer.hpp index b54a9e2dbb19..102a3c31b3db 100644 --- a/src/slic3r/GUI/HttpServer.hpp +++ b/src/slic3r/GUI/HttpServer.hpp @@ -15,8 +15,8 @@ #include #include -#define LOCALHOST_PORT 13618 -#define PAGE_HTTP_PORT 13619 +#define LOCALHOST_PORT 13628 +#define PAGE_HTTP_PORT 13629 #define LOCALHOST_URL "http://127.0.0.1:" #define WCP_DOWNLOAD_PREFIX "/wcp_download/" diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 43cc6483edbd..4ec4db7d541d 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1652,9 +1652,6 @@ bool MainFrame::can_send_gcode() const { if (m_plater && !m_plater->model().objects.empty()) { - auto devices = wxGetApp().app_config->get_devices(); - const auto& edit_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); - auto printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; auto printer_model_opt = printer_config.option("printer_model"); bool is_snapmaker_u1 = false; @@ -1668,7 +1665,7 @@ bool MainFrame::can_send_gcode() const return true; } - if (wxGetApp().app_config->get("use_new_connect") == "true") { + if (wxGetApp().physical_printer_connected()) { return true; } else { auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -2400,21 +2397,10 @@ static wxMenu* generate_help_menu() }, "", nullptr, []() { return true; }); - append_menu_item( - helpMenu, wxID_ANY, _L("Check for Web Resource Updates"), _L("Check for Web Resource Updates"), - [](wxCommandEvent&) { - wxGetApp().check_web_version(); - }, - "", nullptr, []() { return true; }); - append_menu_item(helpMenu, wxID_ANY, _L("Import Profile"), _L("Import Profile"), [](wxCommandEvent&) { wxGetApp().import_presets(); }); - append_menu_item(helpMenu, wxID_ANY, _L("Import Web Resource"), _L("Import Web Resource"), [](wxCommandEvent&) { - wxGetApp().import_flutter_web(); - }); - append_menu_item(helpMenu, wxID_ANY, _L("Open Network Test"), _L("Open Network Test"), [](wxCommandEvent&) { // Use shared_ptr to manage dialog lifetime auto dlg = std::make_shared(wxGetApp().mainframe); @@ -4053,6 +4039,17 @@ void MainFrame::load_printer_url() } } +void MainFrame::reload_gateway_pages() +{ + wxString home_url = wxGetApp().gateway_web_url("home_page"); + if (m_webview != nullptr && wxGetApp().is_gateway_url(home_url)) + m_webview->load_url(home_url); + + wxString device_url = wxGetApp().gateway_web_url("device_control"); + if (wxGetApp().is_gateway_url(device_url)) + load_printer_url(device_url); +} + bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelection() == TabPosition::tpMonitor; } diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index c6c001dc4354..5ddd75dcce81 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -358,6 +358,7 @@ class MainFrame : public DPIFrame void load_url(wxString url); void load_printer_url(wxString url, wxString apikey = ""); void load_printer_url(); + void reload_gateway_pages(); bool is_printer_view() const; void refresh_plugin_tips(); void RunScript(wxString js); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 05767e979e89..869b2a374ec6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -151,6 +151,8 @@ #include "../Utils/UndoRedo.hpp" #include "../Utils/PresetUpdater.hpp" #include "../Utils/Process.hpp" +#include "../Utils/GatewayProtocol.hpp" +#include "../Utils/GatewayDevice.hpp" #include "RemovableDriveManager.hpp" #include "InstanceCheck.hpp" #include "NotificationManager.hpp" @@ -1229,9 +1231,16 @@ class CustomNotebook : public wxControl #endif }; +struct NozzleSyncState +{ + std::atomic alive{true}; + Sidebar* sidebar{nullptr}; +}; + struct Sidebar::priv { Plater *plater; + std::shared_ptr nozzle_sync_state = std::make_shared(); wxPanel *scrolled; PlaterPresetComboBox *combo_print; @@ -2140,6 +2149,8 @@ static wxString nozzle_type_key_to_label(const std::string& key) Sidebar::Sidebar(Plater *parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(42 * wxGetApp().em_unit(), -1)), p(new priv(parent)) { + p->nozzle_sync_state->sidebar = this; + Choice::register_dynamic_list("support_filament", &dynamic_filament_list); Choice::register_dynamic_list("support_interface_filament", &dynamic_filament_list); Choice::register_dynamic_list("wall_filament", &dynamic_filament_list_1_based); @@ -2192,146 +2203,39 @@ Sidebar::Sidebar(Plater *parent) p->m_printerinfo_syncbtn->SetCursor(wxCURSOR_HAND); p->m_printerinfo_syncbtn->SetToolTip(_L("Synchronize nozzle information")); p->m_printerinfo_syncbtn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { - bool hasConnectDevice = false; - auto devices = wxGetApp().app_config->get_devices(); - for (const auto& device : devices) { - if (device.connected) - hasConnectDevice = true; - } - - if (!hasConnectDevice) - { - // showdialog tips no connect device - wxTheApp->CallAfter([this]() { - MessageDialog dlg(wxGetApp().mainframe, - _L("Printer not connected. Please go to the home page or the device page to connect the printer."), - _L("Note"), wxOK); - dlg.ShowModal(); - }); - return; - } - - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - SSWCPProtocol::ResolveResult resolve_result = SSWCP::resolve_machine_info(host); - MachineInfo& machine_info = resolve_result.info; - std::vector nozzle_diameters = machine_info.nozzle_diameters; - - const auto& sync_nozzle_slots = wxGetApp().preset_bundle->m_connect_machine_info_list; - if (!sync_nozzle_slots.empty()) { - std::vector> cached_slots; - for (const auto& slot : sync_nozzle_slots) - cached_slots.emplace_back(slot.nozzle_info, slot.nozzle_volume_type); - SSWCPProtocol::select_complete_cached_nozzle_info(cached_slots, nozzle_diameters, machine_info.nozzle_volume_types); - } - if (resolve_result.status != SSWCPProtocol::ResolveStatus::NoResponse && machine_info.model == "Snapmaker U1") - { - if (nozzle_diameters.size() <= 0) - { - wxTheApp->CallAfter([this]() { - MessageDialog dlgEx(wxGetApp().mainframe, - _L("No nozzle information detected. Please go to the printer settings to configure the nozzle."), - _L("Note"), wxOK); - dlgEx.ShowModal(); - }); - - return; - } - - bool res = false; - std::string headNozzleSize = nozzle_diameters[0]; - for (int i = 1; i < nozzle_diameters.size(); i++) - { - if (headNozzleSize != nozzle_diameters[i]) - { - res = true; - break; - } - } - - if (res) - { - std::vector diameters_raw = nozzle_diameters; - //std::vector diameters_raw = {"0.2", "0.8"}; - wxTheApp->CallAfter([this, diameters_raw, nozzle_volume_types = machine_info.nozzle_volume_types]() { - NozzleDiameterSelectDialog dlg( - wxGetApp().mainframe, - _L("Note: Inconsistent nozzle diameters. Current version does not support mixed diameter printing. Please select one nozzle for this print."), - _L("Set Nozzle Diameter"), - diameters_raw); - if (dlg.ShowModal() == wxID_OK) { - std::string sel = dlg.GetSelectedDiameter(); - if (!sel.empty()) { - auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, sel); - if (preset) { - preset->is_visible = true; - - auto diameter = sel; - auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, diameter); - if (preset == nullptr) { - BOOST_LOG_TRIVIAL(error) << "get the similar printer preset fail"; - } else { - preset->is_visible = true; // force visible - - for (size_t i = 0; i < p->m_nozzle_diameter_lists.size(); ++i) { - p->m_nozzle_diameter_lists[i]->SetValue(diameter + "mm"); - } - - wxGetApp().get_tab(Preset::TYPE_PRINTER)->select_preset(preset->name); - wxGetApp().plater()->sidebar().update_all_preset_comboboxes(true); - } - } - } - } - - if (!nozzle_volume_types.empty()) - GUI::FlowType::set_nozzle_volume_types(nozzle_volume_types); - - wxGetApp().plater()->sidebar().update_nozzle_settings(true); - }); - return; - } - else { - // All tool heads report the same diameter: apply it without opening the picker. - std::string diameter = headNozzleSize; - boost::algorithm::trim(diameter); - if (diameter.size() > 2 && boost::iends_with(diameter, "mm")) { - diameter.resize(diameter.size() - 2); - boost::algorithm::trim(diameter); - } - wxTheApp->CallAfter([this, diameter, nozzle_volume_types = machine_info.nozzle_volume_types]() { - auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, diameter); - if (preset == nullptr) { - BOOST_LOG_TRIVIAL(error) << "get the similar printer preset fail (uniform nozzle sync)"; - return; - } - preset->is_visible = true; - - for (size_t i = 0; i < p->m_nozzle_diameter_lists.size(); ++i) - p->m_nozzle_diameter_lists[i]->SetValue(diameter + "mm"); - - wxGetApp().get_tab(Preset::TYPE_PRINTER)->select_preset(preset->name); - wxGetApp().plater()->sidebar().update_all_preset_comboboxes(true); - - // Apply synchronized nozzle flow types BEFORE rebuilding the nozzle - // UI: update_nozzle_settings rebuilds the per-nozzle flow combos by - // reading nozzle_volume_type from config, so the write must land first - // or the combos show stale values. - if (!nozzle_volume_types.empty()) - GUI::FlowType::set_nozzle_volume_types(nozzle_volume_types); - - wxGetApp().plater()->sidebar().update_nozzle_settings(true); - - wxTheApp->CallAfter([this]() { - MessageDialog dlg_Ex(wxGetApp().mainframe, _L("Nozzle settings synchronized successfully"), - _L("Note"), wxOK); - dlg_Ex.ShowModal(); - }); - }); - } - } - - }); + // The gateway snapshot fetch is a blocking HTTP call (up to ~5s on a slow link), so run + // it on a worker thread and marshal the result back instead of freezing the main UI. + if (!p->m_printerinfo_syncbtn->IsEnabled()) + return; + p->m_printerinfo_syncbtn->Disable(); + + const auto gateway = wxGetApp().gateway_service(); + // wxWeakRef mutates wxTrackable state and must not be copied or destroyed on this worker. + const auto sync_state = p->nozzle_sync_state; + std::thread([sync_state, gateway]() { + std::string machine_type = ""; + std::vector nozzle_diameters; + std::vector nozzle_volume_types; + std::string device_name = ""; + // The gateway device snapshot doubles as the connected-guard: a successful + // GET /api/cache/all proves a live device channel (avoids probe + query serial waits). + const bool got_machine_info = Gateway::GatewayDevice::query_machine_info(gateway, machine_type, nozzle_diameters, nozzle_volume_types, + device_name); + + wxTheApp->CallAfter([sync_state, got_machine_info, machine_type = std::move(machine_type), + nozzle_diameters = std::move(nozzle_diameters), + nozzle_volume_types = std::move(nozzle_volume_types), + device_name = std::move(device_name)]() { + if (!sync_state->alive.load(std::memory_order_acquire)) + return; + Sidebar *self = sync_state->sidebar; + if (self->p->m_printerinfo_syncbtn) + self->p->m_printerinfo_syncbtn->Enable(); + self->apply_nozzle_sync_result(got_machine_info, std::move(machine_type), std::move(nozzle_diameters), + std::move(nozzle_volume_types), std::move(device_name)); + }); + }).detach(); + }); p->m_printer_setting = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "settings"); p->m_printer_setting->SetToolTip(_L("settings")); @@ -3496,7 +3400,7 @@ Sidebar::Sidebar(Plater *parent) SetSizer(sizer); } -Sidebar::~Sidebar() {} +Sidebar::~Sidebar() { p->nozzle_sync_state->alive.store(false, std::memory_order_release); } void Sidebar::create_printer_preset() { @@ -3621,9 +3525,7 @@ void Sidebar::update_all_preset_comboboxes(bool reload_printer_view) auto p_mainframe = wxGetApp().mainframe; auto cfg = preset_bundle.printers.get_edited_preset().config; - const auto& appconfig = wxGetApp().app_config; - - bool use_new_connection = appconfig->get("use_new_connect") == "true"; + const bool use_new_connection = wxGetApp().physical_printer_connected(); auto printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; auto printer_model_opt = printer_config.option("printer_model"); @@ -3647,7 +3549,7 @@ void Sidebar::update_all_preset_comboboxes(bool reload_printer_view) const bool showing_u1_device = p_mainframe->m_printer_view && p_mainframe->m_printer_view->is_u1_device_page(); // Non-U1: always leave path=2 for missing_connection / print_host. Keep skipping - // when use_new_connect already sits on the GIF (connect flow loads path=2 afterwards). + // when the physical connection already sits on the GIF (connect flow loads path=2 afterwards). const bool load_non_u1_page = !is_snapmaker_u1 && reload_printer_view && (!use_new_connection || showing_u1_device); if (load_non_u1_page) { @@ -3669,8 +3571,7 @@ void Sidebar::update_all_preset_comboboxes(bool reload_printer_view) MainFrame::PrintSelectType::eSendGcode; if (url.find("127.0.0.1") != std::string::npos) { - url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + - "/web/flutter_web/index.html?path=3"); + url = wxGetApp().gateway_web_url("device_control"); } } @@ -3683,26 +3584,14 @@ void Sidebar::update_all_preset_comboboxes(bool reload_printer_view) p_mainframe->set_print_button_to_default(print_btn_type); if (is_snapmaker_u1) { - - auto devices = wxGetApp().app_config->get_devices(); - bool hasOnlineMachine = false; - for (const auto& device : devices) { - if (device.connected) { - hasOnlineMachine = true; - break; - } - } - - if(hasOnlineMachine) + if (wxGetApp().gateway_device_connected()) p->combo_printer->set_show_machine_connecting_button(true); - wxString url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + - "/web/flutter_web/index.html?path=2"); - auto real_url = wxGetApp().get_international_url(url); + wxString url = wxGetApp().gateway_web_url("device_control"); if (reload_printer_view && !showing_u1_device) { - wxGetApp().mainframe->load_printer_url(real_url); - } + wxGetApp().mainframe->load_printer_url(url); + } } if (!p->combo_printer->get_show_machine_connecting_button() && !is_snapmaker_u1) { @@ -3827,7 +3716,7 @@ void Sidebar::update_all_preset_comboboxes(bool reload_printer_view) update_printer_thumbnail(); } - p_mainframe->show_device(preset_bundle.use_bbl_device_tab() && !use_new_connection); + p_mainframe->show_device(preset_bundle.use_bbl_device_tab() && !wxGetApp().physical_printer_connected()); p_mainframe->m_tabpanel->SetSelection(p_mainframe->m_tabpanel->GetSelection()); } @@ -9272,26 +9161,122 @@ void Sidebar::sync_ams_list() Layout(); } -void Sidebar::show_sync_filament_dialog() +void Sidebar::apply_nozzle_sync_result(bool got_machine_info, std::string machine_type, + std::vector nozzle_diameters, + std::vector nozzle_volume_types, + std::string device_name) { - if (!wxGetApp().plater()) + if (!got_machine_info) { + MessageDialog dlg(wxGetApp().mainframe, + _L("Printer not connected. Please go to the home page or the device page to connect the printer."), + _L("Note"), wxOK); + dlg.ShowModal(); return; + } - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - - MachineObject* device_machine = nullptr; - { - Slic3r::DeviceManager* dev = wxGetApp().getDeviceManager(); - if (dev) { - MachineObject* obj = dev->get_selected_machine(); - if (obj && obj->is_connected()) { - device_machine = obj; - } + MachineInfo machine_info; + machine_info.model = SSWCPProtocol::normalize_machine_model(machine_type); + machine_info.device_name = device_name; + machine_info.nozzle_diameters = nozzle_diameters; + machine_info.nozzle_volume_types = nozzle_volume_types; + const auto& sync_nozzle_slots = wxGetApp().preset_bundle->m_connect_machine_info_list; + if (!sync_nozzle_slots.empty()) { + std::vector> cached_slots; + for (const auto& slot : sync_nozzle_slots) + cached_slots.emplace_back(slot.nozzle_info, slot.nozzle_volume_type); + SSWCPProtocol::select_complete_cached_nozzle_info(cached_slots, nozzle_diameters, machine_info.nozzle_volume_types); + } + if (machine_info.model != "Snapmaker U1") + return; + + if (nozzle_diameters.empty()) { + MessageDialog dlg(wxGetApp().mainframe, + _L("No nozzle information detected. Please go to the printer settings to configure the nozzle."), + _L("Note"), wxOK); + dlg.ShowModal(); + return; + } + + auto apply_nozzle_preset = [this](const std::string& diameter) { + auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, diameter); + if (preset == nullptr) { + BOOST_LOG_TRIVIAL(error) << "get the similar printer preset fail"; + return false; } + preset->is_visible = true; + for (size_t i = 0; i < p->m_nozzle_diameter_lists.size(); ++i) + p->m_nozzle_diameter_lists[i]->SetValue(diameter + "mm"); + wxGetApp().get_tab(Preset::TYPE_PRINTER)->select_preset(preset->name); + wxGetApp().plater()->sidebar().update_all_preset_comboboxes(true); + return true; + }; + + const std::string& first_diameter = nozzle_diameters.front(); + const bool mixed_diameters = std::any_of(nozzle_diameters.begin() + 1, + nozzle_diameters.end(), + [&](const std::string& diameter) { return diameter != first_diameter; }); + if (mixed_diameters) { + NozzleDiameterSelectDialog dlg(wxGetApp().mainframe, + _L("Note: Inconsistent nozzle diameters. Current version does not support mixed diameter printing. Please select one nozzle for this print."), + _L("Set Nozzle Diameter"), + nozzle_diameters); + if (dlg.ShowModal() == wxID_OK) { + std::string selected_diameter = dlg.GetSelectedDiameter(); + if (!selected_diameter.empty()) + apply_nozzle_preset(selected_diameter); + } + } else { + std::string diameter = first_diameter; + boost::algorithm::trim(diameter); + if (diameter.size() > 2 && boost::iends_with(diameter, "mm")) { + diameter.resize(diameter.size() - 2); + boost::algorithm::trim(diameter); + } + if (!apply_nozzle_preset(diameter)) + return; } - if (!host && !device_machine) { + if (!machine_info.nozzle_volume_types.empty()) + GUI::FlowType::set_nozzle_volume_types(machine_info.nozzle_volume_types); + wxGetApp().plater()->sidebar().update_nozzle_settings(true); + if (!mixed_diameters) + wxTheApp->CallAfter([this]() { + MessageDialog dlg(wxGetApp().mainframe, _L("Nozzle settings synchronized successfully"), _L("Note"), wxOK); + dlg.ShowModal(); + }); +} +void Sidebar::show_sync_filament_dialog() +{ + if (!wxGetApp().plater()) + return; + + if (p->m_bpButton_sync_filament == nullptr || !p->m_bpButton_sync_filament->IsEnabled()) + return; + + p->m_bpButton_sync_filament->Disable(); + const auto gateway = wxGetApp().gateway_service(); + const auto sync_state = p->nozzle_sync_state; + ScalableButton* button = p->m_bpButton_sync_filament; + std::thread([gateway, sync_state, button]() { + std::string machine_type; + std::string device_name; + std::vector nozzle_diameters; + std::vector nozzle_volume_types; + const bool got_machine_info = Gateway::GatewayDevice::query_machine_info(gateway, machine_type, nozzle_diameters, nozzle_volume_types, device_name); + + wxTheApp->CallAfter([sync_state, button, got_machine_info, machine_type = std::move(machine_type)]() { + if (!sync_state->alive.load(std::memory_order_acquire)) + return; + if (button != nullptr) + button->Enable(); + sync_state->sidebar->continue_sync_filament_dialog(got_machine_info, machine_type); + }); + }).detach(); +} + +void Sidebar::continue_sync_filament_dialog(bool got_machine_info, const std::string& machine_type) +{ + if (!got_machine_info) { SyncRichConfirmDialog dlg(this, _L("No printer is connected. Please connect your U1 from the Device page before syncing."), wxYES_NO); @@ -9308,26 +9293,11 @@ void Sidebar::show_sync_filament_dialog() "Snapmaker U1" }; MachineInfo machine_info; - bool got_machine_info = false; - - if (host) { - SSWCPProtocol::ResolveResult resolve_result = SSWCP::resolve_machine_info(host); - if (resolve_result.status != SSWCPProtocol::ResolveStatus::NoResponse) { - machine_info = resolve_result.info; - got_machine_info = true; - } - } - - if (!got_machine_info || machine_info.model.empty()) { - if (device_machine) { - machine_info.model = SSWCPProtocol::normalize_machine_model(device_machine->printer_type); - got_machine_info = !machine_info.model.empty(); - } - } + machine_info.model = SSWCPProtocol::normalize_machine_model(machine_type); bool is_white_listed_type = white_list_machine_types.find(machine_info.model) != white_list_machine_types.end(); - if (got_machine_info && !machine_info.model.empty() && !is_white_listed_type) { + if (!machine_info.model.empty() && !is_white_listed_type) { SyncRichConfirmDialog dlg(this, _L("The connected printer is not U1. Unable to sync filament information. Please switch to U1 and try again."), wxYES_NO); @@ -15766,7 +15736,7 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) update_sidebar(); int old_sel = e.GetOldSelection(); if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->use_bbl_device_tab() && new_sel == MainFrame::tpMonitor && - wxGetApp().app_config->get("use_new_connect") != "true") { + !wxGetApp().physical_printer_connected()) { if (!wxGetApp().getAgent()) { e.Veto(); BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel; @@ -15794,9 +15764,7 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } const bool showing_u1 = main_frame->m_printer_view->is_u1_device_page(); if (is_snapmaker_u1 && !showing_u1) { - wxString u1_url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + - "/web/flutter_web/index.html?path=2"); - main_frame->load_printer_url(wxGetApp().get_international_url(u1_url)); + main_frame->load_printer_url(wxGetApp().gateway_web_url("device_control")); } else if (!is_snapmaker_u1 && showing_u1) { wxString non_u1 = url; if (non_u1.empty()) { @@ -21657,18 +21625,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us return output_file; }; - // 校验机型 - auto devices = wxGetApp().app_config->get_devices(); - std::string connect_preset = ""; - for (const auto device : devices) { - if (device.connected) { - connect_preset = device.preset_name; - } - } - auto current_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); - bool islegal = true; std::string c_preset = ""; if (current_preset.is_system) { c_preset = current_preset.name; @@ -21680,11 +21638,6 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us c_preset.erase(std::remove(c_preset.begin(), c_preset.end(), '('), c_preset.end()); c_preset.erase(std::remove(c_preset.begin(), c_preset.end(), ')'), c_preset.end()); - connect_preset.erase(std::remove(connect_preset.begin(), connect_preset.end(), '('), connect_preset.end()); - connect_preset.erase(std::remove(connect_preset.begin(), connect_preset.end(), ')'), connect_preset.end()); - - islegal = (c_preset == connect_preset); - DynamicPrintConfig* physical_printer_config = &Slic3r::GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; if (! physical_printer_config || p->model.objects.empty()) return; @@ -21701,7 +21654,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us is_snapmaker_u1 = boost::icontains(printer_model, "Snapmaker") && boost::icontains(printer_model, "U1"); } - if (wxGetApp().app_config->get("use_new_connect") == "true" || is_snapmaker_u1) { + if (wxGetApp().physical_printer_connected() || is_snapmaker_u1) { // firstly upload and open upload download dialog, // get default name // Obtain default output path @@ -21756,6 +21709,36 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us dialog->set_send_page(dlg.post_action() == PrintHostPostUploadAction::None); dialog->set_gcode_file_name(upload_job.upload_data.source_path.string()); dialog->set_display_file_name(upload_job.upload_data.upload_path.string()); + + const std::string source_path = upload_job.upload_data.source_path.string(); + const std::string display_name = upload_job.upload_data.upload_path.string(); + + nlohmann::json payload; + payload["file_path"] = source_path; + payload["file_name"] = display_name; + payload["active_file"] = SSWCP::build_active_file_json(source_path, display_name, false); + payload["filament_mapping"] = SSWCP::build_filament_mapping_json(source_path, display_name); + + std::string store_id = boost::uuids::to_string(boost::uuids::random_generator()()); + const auto store_result = wxGetApp().gateway_store_preprint_context(store_id, payload); + if (!store_result.ok) { + BOOST_LOG_TRIVIAL(error) << "preprint store failed: " << store_result.error.message; + MessageDialog confirm_dialog(this, _L("Failed to upload pre-print context. Open anyway?"), _L("Note"), wxYES_NO | wxICON_WARNING); + if (confirm_dialog.ShowModal() != wxID_YES) { + delete dialog; + return; + } + store_id.clear(); + } else if (!store_result.file_exists) { + BOOST_LOG_TRIVIAL(warning) << "preprint store reported that the G-code path does not exist"; + MessageDialog confirm_dialog(this, _L("The G-code path is invalid. Continue anyway?"), _L("Note"), wxYES_NO | wxICON_WARNING); + if (confirm_dialog.ShowModal() != wxID_YES) { + delete dialog; + return; + } + } + + dialog->set_store_id(store_id); bool res = dialog->run(); if (dialog->is_finish()) { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index f8e91d5cfba8..6e9cace946b2 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -230,6 +230,14 @@ class Sidebar : public wxPanel const std::vector& get_bed_type_combo_enum_values() const { return m_bed_type_combo_enum_values; } private: + void continue_sync_filament_dialog(bool got_machine_info, const std::string& machine_type); + + // Applies a nozzle sync query result on the UI thread (async sync-button path). + void apply_nozzle_sync_result(bool got_machine_info, std::string machine_type, + std::vector nozzle_diameters, + std::vector nozzle_volume_types, + std::string device_name); + struct priv; std::unique_ptr p; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 780c40e89378..b4fdbebe6878 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -342,36 +342,17 @@ wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxWin // snapmaker AppConfig* config = GUI::wxGetApp().app_config; combobox->SetSelection(region_index); - auto info = wxGetApp().sm_get_userinfo(); - if (info && info->is_user_login()) { - MessageDialog msg_wingow(this, _L("Changing the region will log out your account and restart.\n") + "\n" + _L("Do you want to continue?"), - L("Region selection"), wxICON_QUESTION | wxOK | wxCANCEL); - - if (msg_wingow.ShowModal() == wxID_CANCEL) { - combobox->SetSelection(current_region); - return; - } else { - wxGetApp().sm_request_user_logout(); - config->set("region", region.ToStdString()); - EndModal(wxID_CANCEL); - } - - Close(); - GetParent()->RemoveChild(this); - wxGetApp().recreate_GUI(_L("Change Region")); - } else { - MessageDialog msg_wingow(nullptr, - _L("Switching the region requires application restart.\n") + "\n" + _L("Do you want to continue?"), - L("Region selection"), wxICON_QUESTION | wxOK | wxCANCEL); - if (msg_wingow.ShowModal() == wxID_CANCEL) { - combobox->SetSelection(current_region); - return; - } - config->set("region", region.ToStdString()); - Close(); - GetParent()->RemoveChild(this); - wxGetApp().recreate_GUI(_L("Change Region")); + MessageDialog msg_wingow(nullptr, + _L("Switching the region requires application restart.\n") + "\n" + _L("Do you want to continue?"), + L("Region selection"), wxICON_QUESTION | wxOK | wxCANCEL); + if (msg_wingow.ShowModal() == wxID_CANCEL) { + combobox->SetSelection(current_region); + return; } + config->set("region", region.ToStdString()); + Close(); + GetParent()->RemoveChild(this); + wxGetApp().recreate_GUI(_L("Change Region")); wxGetApp().update_publish_status(); e.Skip(); @@ -1517,7 +1498,6 @@ wxWindow* PreferencesDialog::create_debug_page() page->SetBackgroundColour(*wxWHITE); m_internal_developer_mode_def = app_config->get("internal_developer_mode"); - m_websocket_debug_def = app_config->get("websocket_debug"); m_backup_interval_def = app_config->get("backup_interval"); m_iot_environment_def = app_config->get("iot_environment"); diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp index 6ac83f8aa2ef..165c97ce39bb 100644 --- a/src/slic3r/GUI/Preferences.hpp +++ b/src/slic3r/GUI/Preferences.hpp @@ -99,7 +99,6 @@ class PreferencesDialog : public DPIDialog wxString m_developer_mode_def; wxString m_internal_developer_mode_def; - wxString m_websocket_debug_def; wxString m_backup_interval_def; wxString m_iot_environment_def; diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index b49410cb5a11..73acbb08ff2b 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -327,7 +327,6 @@ int PresetComboBox::update_ams_color() if (m_filament_idx < 0) return -1; int idx = selected_ams_filament(); - auto& filaments = wxGetApp().preset_bundle->machine_filaments; const ConnectMachineInfo* machineInfo = nullptr; if (idx >= 0) { @@ -342,19 +341,6 @@ int PresetComboBox::update_ams_color() { real_idx = machineInfo->index; } - else if (idx >= 0) - { - int tmp = idx; - for (auto iter = filaments.begin(); iter != filaments.end(); ++iter) { - if (tmp == 0) { - real_idx = iter->first; - break; - } - - tmp--; - } - } - auto& filament_extruder_map = wxGetApp().app_config->get_filament_extruder_map_ref(); if (real_idx >= 0) { @@ -384,29 +370,13 @@ int PresetComboBox::update_ams_color() } else { - if (wxGetApp().preset_bundle->machine_filaments.size() > 0) - { - auto iter = wxGetApp().preset_bundle->machine_filaments.begin(); - for (size_t i = 0; iter != wxGetApp().preset_bundle->machine_filaments.end() && i < idx; ++i) { - ++iter; - } - if (iter == wxGetApp().preset_bundle->machine_filaments.end()) - { - return -1; - } - color = iter->second.second; - } - else - { - auto& ams_list = wxGetApp().preset_bundle->filament_ams_list; - auto iter = ams_list.find(idx); - if (iter == ams_list.end()) - { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": ams %1% out of range %2%") % idx % ams_list.size(); - return -1; - } - color = iter->second.opt_string("filament_colour", 0u); + auto& ams_list = wxGetApp().preset_bundle->filament_ams_list; + auto iter = ams_list.find(idx); + if (iter == ams_list.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": ams %1% out of range %2%") % idx % ams_list.size(); + return -1; } + color = iter->second.opt_string("filament_colour", 0u); } const std::string normalizedColor = FilamentColorUtils::NormalizeHexColor(color, "#26A69A"); if (machineInfo != nullptr && multiColors.empty() && !normalizedColor.empty()) @@ -1400,10 +1370,10 @@ void PlaterPresetComboBox::update() if (m_type == Preset::TYPE_FILAMENT && m_preset_bundle->is_bbl_vendor()) add_ams_filaments(into_u8(selected_user_preset), true); - if (m_type == Preset::TYPE_FILAMENT && wxGetApp().preset_bundle->machine_filaments.size() > 0) { + if (m_type == Preset::TYPE_FILAMENT && m_preset_bundle->m_connect_machine_info_list.size() > 0) { set_label_marker(Append(separator(L("Machine Filament")), wxNullBitmap)); auto& filaments = m_collection->get_presets(); - auto machine_nozzles_list = wxGetApp().preset_bundle->m_connect_machine_info_list; + auto& machine_nozzles_list = m_preset_bundle->m_connect_machine_info_list; m_first_ams_filament = GetCount(); std::string currentNozzleInfo; @@ -1420,7 +1390,7 @@ void PlaterPresetComboBox::update() std::string filament_name = machine_nozzles_list[i].filament_info; std::string machine_nozzles = machine_nozzles_list[i].nozzle_info; - // Filter by nozzle for display only; machine_filaments / m_connect_machine_info_list stay from sync (SSWCP). + // Filter by nozzle for display only; slot state stays owned by device sync. if (currentNozzleInfo != machine_nozzles) continue; diff --git a/src/slic3r/GUI/PrinterWebView.cpp b/src/slic3r/GUI/PrinterWebView.cpp index 85340fb41a27..4145ad01040d 100644 --- a/src/slic3r/GUI/PrinterWebView.cpp +++ b/src/slic3r/GUI/PrinterWebView.cpp @@ -28,7 +28,6 @@ PrinterWebView::PrinterWebView(wxWindow *parent) wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); - wxGetApp().start_flutter_wcp_timeout_watch(); // Load the real URL only in load_url(). Creating with Flutter here races the later // missing_connection / path=2 LoadURL and cancels the in-flight document (-999). m_browser = WebView::CreateWebView(this, "about:blank"); @@ -74,14 +73,14 @@ void PrinterWebView::load_url(wxString& url, wxString apikey) return; m_apikey = apikey; - if (url.find("path=2") != std::string::npos) { + if (wxGetApp().is_gateway_url(url)) { wxGetApp().fltviews().add_printer_view(this, url, apikey); } else { wxGetApp().fltviews().remove_printer_view(this); } m_browser->Show(); - m_browser->LoadURL(url); + WebView::LoadUrl(m_browser, url); UpdateState(); } @@ -96,7 +95,7 @@ bool PrinterWebView::isSnapmakerPage() if (m_browser == nullptr) return false; auto url = m_browser->GetCurrentURL(); - return (url.find("flutter_web") != std::string::npos); + return wxGetApp().is_gateway_url(url); } bool PrinterWebView::is_u1_device_page() @@ -104,7 +103,8 @@ bool PrinterWebView::is_u1_device_page() if (m_browser == nullptr) return false; auto url = m_browser->GetCurrentURL(); - return url.find("flutter_web") != std::string::npos && url.find("path=2") != std::string::npos; + return url.find("flutter_web") != std::string::npos && + (url.find("path=2") != std::string::npos || url.find("/device_control") != std::string::npos); } void PrinterWebView::sendMessage(const std::string& msg) { @@ -246,7 +246,6 @@ void PrinterWebView::OnScriptMessage(wxWebViewEvent& evt) { // wxLogMessage("Script message received; value = %s, handler = %s", evt.GetString(), evt.GetMessageHandler()); // test - wxGetApp().on_flutter_wcp_received(); SSWCP::handle_web_message(evt.GetString().ToUTF8().data(), m_browser); } diff --git a/src/slic3r/GUI/SMPhysicalPrinterDialog.cpp b/src/slic3r/GUI/SMPhysicalPrinterDialog.cpp index 3c06ee9e830c..4ae6604e4b64 100644 --- a/src/slic3r/GUI/SMPhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/SMPhysicalPrinterDialog.cpp @@ -36,7 +36,6 @@ #include "MsgDialog.hpp" #include "OAuthDialog.hpp" #include "SimplyPrint.hpp" -#include "slic3r/GUI/WebPresetDialog.hpp" namespace Slic3r { namespace GUI { @@ -698,8 +697,6 @@ void SMPhysicalPrinterDialog::OnOK(wxEvent& event) std::shared_ptr host(PrintHost::get_print_host(m_config)); wxString msg; if (host->test(msg)) { - wxGetApp().app_config->set("use_new_connect", "true"); - m_connected = true; auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -712,65 +709,10 @@ void SMPhysicalPrinterDialog::OnOK(wxEvent& event) if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) apikey = cfg.opt_string("printhost_apikey"); - wxGetApp().mainframe->load_printer_url(url, apikey); - - // 更新其他设备连接状态为断开 - bool is_history = false; - DeviceInfo history_info; - auto devices = wxGetApp().app_config->get_devices(); - for (size_t i = 0; i < devices.size(); ++i) { - if (devices[i].connected) { - devices[i].connected = false; - wxGetApp().app_config->save_device_info(devices[i]); - } - if (devices[i].dev_id == host->get_host()) { - is_history = true; - history_info = devices[i]; - } - } - - if (is_history) { - history_info.connected = true; - history_info.protocol = (int) (m_config->option>("host_type")->value); - history_info.api_key = cfg.opt_string("printhost_apikey"); - wxGetApp().app_config->save_device_info(history_info); - } else { - // 绑定预设 - DeviceInfo info; - info.ip = host->get_host(); - info.dev_id = host->get_sn() != "" ? host->get_sn() : info.ip; - info.dev_name = host->get_host(); - info.connected = true; - info.protocol = (int) (m_config->option>("host_type")->value); - info.api_key = cfg.opt_string("printhost_apikey"); - info.sn = "-1"; - - wxGetApp().app_config->save_device_info(info); - - MessageDialog msg_window(nullptr, - host->get_host() + _L(" The target machine model has not been detected. Please bind manually. "), - L("Machine Bind"), wxICON_QUESTION | wxOK); - msg_window.ShowModal(); - auto dialog = WebPresetDialog(&wxGetApp()); - dialog.m_device_id = host->get_host(); - dialog.run(); - } - - - // 更新卡片 - devices = wxGetApp().app_config->get_devices(); - - json param; - param["command"] = "local_devices_arrived"; - param["sequece_id"] = "10001"; - param["data"] = devices; - std::string logout_cmd = param.dump(); - wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - GUI::wxGetApp().run_script(strJS); + wxGetApp().set_connect_host(host); + wxGetApp().set_host_config(cfg); - // wcp订阅 - json data = devices; - wxGetApp().device_card_notify(data); + wxGetApp().mainframe->load_printer_url(url, apikey); MessageDialog msg_window_connected(nullptr, host->get_host() + _L(" connected sucessfully !\n"), L("Machine Connected"), wxICON_QUESTION | wxOK); @@ -779,9 +721,6 @@ void SMPhysicalPrinterDialog::OnOK(wxEvent& event) wxGetApp().mainframe->plater()->sidebar().update_all_preset_comboboxes(); wxGetApp().mainframe->m_print_enable = true; wxGetApp().mainframe->update_slice_print_status(MainFrame::eEventPlateUpdate); - - wxGetApp().set_connect_host(host); - wxGetApp().set_host_config(cfg); } else { m_connected = false; show_error(this, host->get_test_failed_msg(msg)); diff --git a/src/slic3r/GUI/SSWCP.cpp b/src/slic3r/GUI/SSWCP.cpp index 72280551f350..ae0d8067f806 100644 --- a/src/slic3r/GUI/SSWCP.cpp +++ b/src/slic3r/GUI/SSWCP.cpp @@ -12,9 +12,11 @@ #include "sentry_wrapper/SentryWrapper.hpp" #include "slic3r/Utils/SnapLogClient.hpp" #include +#include #include #include #include +#include #include #include #include @@ -41,13 +43,11 @@ #include "MoonRaker.hpp" -#include "slic3r/GUI/WebPresetDialog.hpp" #include "slic3r/GUI/HttpServer.hpp" #include #include "slic3r/GUI/SMPhysicalPrinterDialog.hpp" #include "slic3r/GUI/WebUrlDialog.hpp" -#include "slic3r/GUI/WebSMUserLoginDialog.hpp" #include "slic3r/GUI/FilamentGroupDialog.hpp" #include "slic3r/GUI/FlowTypeHelper.hpp" @@ -64,6 +64,47 @@ using namespace nlohmann; namespace Slic3r { namespace GUI { +namespace { + +long long preprint_color_to_int(const std::string& color) +{ + auto hex_digit = [](char digit) { + if (digit >= '0' && digit <= '9') + return digit - '0'; + if (digit >= 'a' && digit <= 'f') + return digit - 'a' + 10; + if (digit >= 'A' && digit <= 'F') + return digit - 'A' + 10; + return -1; + }; + + if ((color.size() != 7 && color.size() != 9) || color.front() != '#') + return 0; + + long long result = 0; + for (size_t index = 1; index < color.size(); ++index) { + const int digit = hex_digit(color[index]); + if (digit < 0) + return 0; + result = result * 16 + digit; + } + return result; +} + +bool parse_thumbnail_dimension(const std::string& value, double& result) +{ + if (value.empty()) + return false; + + errno = 0; + char* end = nullptr; + result = std::strtod(value.c_str(), &end); + const bool consumed_value = end == value.c_str() + value.size(); + return consumed_value && errno != ERANGE && std::isfinite(result) && result > 0.0; +} + +} // namespace + // WCP_Logger WCP_Logger::WCP_Logger() { @@ -469,12 +510,6 @@ void SSWCP_Instance::process() { async_test(); } else if (m_cmd == "sw_test_mqtt_moonraker") { test_mqtt_request(); - } else if (m_cmd == "sw_SetCache") { - sw_SetCache(); - } else if (m_cmd == "sw_GetCache") { - sw_GetCache(); - } else if (m_cmd == "sw_RemoveCache") { - sw_RemoveCache(); } else if (m_cmd == "sw_SwitchTab") { sw_SwitchTab(); } else if (m_cmd == "sw_Webview_Unsubscribe") { @@ -497,10 +532,6 @@ void SSWCP_Instance::process() { sw_Exit(); } else if(m_cmd == "sw_FileLog") { sw_FileLog(); - } else if (m_cmd == "sw_SubscribeCacheKey") { - sw_SubscribeCacheKey(); - } else if (m_cmd == "sw_UnsubscribeCacheKeys") { - sw_UnsubscribeCacheKeys(); } else if (m_cmd == "sw_UploadEvent") { sw_UploadEvent(); } else if (m_cmd == "sw_SnapLog") { @@ -736,6 +767,41 @@ void SSWCP_Instance::sw_Exit() { wxGetApp().Exit(); } +nlohmann::json SSWCP::build_active_file_metadata() +{ + nlohmann::json metadata_json = nlohmann::json::object(); + if (wxGetApp().model().model_info) { + auto& items = wxGetApp().model().model_info->metadata_items; + auto lookup = [&](const std::string& key) { + auto it = items.find(key); + if (it != items.end()) + metadata_json[key] = it->second; + }; + lookup("DesignModelId"); + lookup("DesignProfileId"); + lookup("DesignRegion"); + } + return metadata_json; +} + +nlohmann::json SSWCP::build_active_file_json(const std::string& file_path, const std::string& file_name, bool /*is_zip*/) +{ + std::string url_path = file_path; + std::replace(url_path.begin(), url_path.end(), '\\', '/'); + + nlohmann::json result; + result["metadata"] = build_active_file_metadata(); + result["file_name"] = file_name; + result["file_path"] = file_path; + result["url"] = wxGetApp().gateway_localfile_url(url_path); + + boost::system::error_code file_error; + const bool file_exists = boost::filesystem::is_regular_file(file_path, file_error); + result["origin_size"] = file_exists ? boost::filesystem::file_size(file_path) : 0; + result["checksum"] = file_exists ? calc_sha256_base64(file_path) : std::string{}; + return result; +} + void SSWCP_Instance::sw_GetActiveFile() { std::string file_path = SSWCP::get_active_filename(); @@ -783,7 +849,7 @@ void SSWCP_Instance::sw_GetActiveFile() self->m_res_data["origin_size"] = SSWCP::m_active_file_size; std::string url_zip_path = std::string(wxString(zipname).ToUTF8()); std::replace(url_zip_path.begin(), url_zip_path.end(), '\\', '/'); - self->m_res_data["url"] = LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + "/localfile/" + Http::url_encode(url_zip_path); + self->m_res_data["url"] = wxGetApp().gateway_localfile_url(url_zip_path); SSWCP::m_file_size_mutex.unlock(); // checksum: SHA-256 digest as standard Base64, for Flutter-side integrity verification @@ -814,7 +880,7 @@ void SSWCP_Instance::sw_GetActiveFile() // checksum: SHA-256 digest as standard Base64, for Flutter-side integrity verification m_res_data["checksum"] = calc_sha256_base64(file_path); - m_res_data["url"] = LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + "/localfile/" + Http::url_encode(url_path); + m_res_data["url"] = wxGetApp().gateway_localfile_url(url_path); send_to_js(); finish_job(); @@ -1060,9 +1126,6 @@ void SSWCP_Instance::send_to_js() WebView::RunScript(self->m_webview, str_res); } - // Flutter debug: copy message to Flutter debug interface via WebSocket - // This is independent of the original communication path above - SSWCP::send_message_to_flutter(json_str); } }); @@ -1089,9 +1152,6 @@ void SSWCP_Instance::async_test() { .perform(); } -// Synchronous test implementation -std::unordered_map SSWCP_Instance::m_wcp_cache; - void SSWCP_Instance::sync_test() { m_res_data = m_param_data; send_to_js(); @@ -1115,7 +1175,6 @@ void SSWCP_Instance::test_mqtt_request() { SSWCP_Instance::on_mqtt_msg_arrived(self, response); } }); - // host->async_get_printer_info([self](const json& response) { SSWCP_Instance::on_mqtt_msg_arrived(self, response); }); } catch (std::exception& e) { handle_general_fail(); } @@ -1126,8 +1185,10 @@ void SSWCP_Instance::sw_SwitchTab() { if (m_param_data.count("target")) { std::string target_tab = m_param_data["target"].get(); - if (SSWCP::m_tab_map.count(target_tab)) { - wxGetApp().mainframe->request_select_tab(MainFrame::TabPosition(SSWCP::m_tab_map[target_tab])); + auto tab_item = std::find_if(SSWCP::m_tab_map.begin(), SSWCP::m_tab_map.end(), + [&target_tab](const auto& tab) { return boost::iequals(tab.first, target_tab); }); + if (tab_item != SSWCP::m_tab_map.end()) { + wxGetApp().mainframe->request_select_tab(MainFrame::TabPosition(tab_item->second)); send_to_js(); finish_job(); return; @@ -1140,149 +1201,6 @@ void SSWCP_Instance::sw_SwitchTab() { } } -void SSWCP_Instance::sw_SetCache() { - try { - if (m_param_data.count("objects") && m_param_data["objects"].is_array() && - m_param_data["objects"].size() > 0) { - json objects = m_param_data["objects"]; - for (size_t i = 0; i < objects.size(); ++i) { - std::string key = objects[i]["key"].get(); - m_wcp_cache[key] = objects[i]["value"]; - wxGetApp().cache_notify(key, objects[i]["value"]); - } - - send_to_js(); - finish_job(); - } else { - handle_general_fail(); - } - } - catch (std::exception& e) { - handle_general_fail(); - } - -} - -void SSWCP_Instance::sw_GetCache() -{ - try { - if (m_param_data.count("keys") && m_param_data["keys"].is_array() && m_param_data["keys"].size() > 0) { - json res_data; - json keys = m_param_data["keys"]; - for (size_t i = 0; i < keys.size(); ++i) { - std::string key = keys[i].get(); - if (m_wcp_cache.count(key)) { - res_data.push_back(m_wcp_cache[key]); - } else { - res_data.push_back(json::object()); - } - } - m_res_data = res_data; - send_to_js(); - finish_job(); - } else { - handle_general_fail(); - } - } catch (std::exception& e) { - handle_general_fail(); - } -} - - -void SSWCP_Instance::sw_RemoveCache() -{ - try { - if (m_param_data.count("keys") && m_param_data["keys"].is_array()) { - json keys = m_param_data["keys"]; - if (keys.size() == 0) { - for (const auto& item : m_wcp_cache) { - wxGetApp().cache_notify(item.first, json::value_t::null); - } - m_wcp_cache.clear(); - } else { - for (size_t i = 0; i < keys.size(); ++i) { - std::string key = keys[i].get(); - if (m_wcp_cache.count(key)) { - wxGetApp().cache_notify(key, json::value_t::null); - m_wcp_cache.erase(key); - } - } - } - - send_to_js(); - finish_job(); - } else { - handle_general_fail(); - } - } catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_Instance::sw_SubscribeCacheKey() -{ - try { - if (!m_param_data.count("key") || !m_param_data["key"].is_string()) { - handle_general_fail(-1, "param [keys] required or wrong type"); - return; - } - - std::string key = m_param_data["key"].get(); - auto& cache_map = wxGetApp().m_cache_subscribers; - - for (auto iter = cache_map.begin(); iter != cache_map.end();) { - if (iter->first.first == m_webview && iter->second == key) { - // remove the old sub - iter = cache_map.erase(iter); - } else { - ++iter; - } - } - - std::weak_ptr weak_self = shared_from_this(); - cache_map[{m_webview, weak_self}] = key; - } - catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_Instance::sw_UnsubscribeCacheKeys() -{ - try { - if (!m_param_data.count("keys") || !m_param_data["keys"].is_array()) { - handle_general_fail(-1, "param [keys] required or wrong type!"); - return; - } - - json keys = m_param_data["keys"]; - auto& cache_map = wxGetApp().m_cache_subscribers; - if (keys.size() == 0) { - for (auto iter = cache_map.begin(); iter != cache_map.end();) { - if (iter->first.first == m_webview) { - iter = cache_map.erase(iter); - } else { - iter++; - } - } - } else { - for (size_t i = 0; i < keys.size(); ++i) { - std::string delete_key = keys[i].get(); - for (auto iter = cache_map.begin(); iter != cache_map.end();) { - if (iter->first.first == m_webview && iter->second == delete_key) { - iter = cache_map.erase(iter); - } else { - iter++; - } - } - } - } - } - catch (std::exception& e) { - handle_general_fail(); - } -} - void SSWCP_Instance::on_mqtt_status_msg_arrived(std::shared_ptr obj, const json& response) { if (!obj) { @@ -1328,10 +1246,7 @@ void SSWCP_Instance::on_mqtt_msg_arrived(std::shared_ptr obj, co } void SSWCP_Instance::sw_UnsubscribeAll() { - wxGetApp().m_device_card_subscribers.clear(); wxGetApp().m_recent_file_subscribers.clear(); - wxGetApp().m_user_login_subscribers.clear(); - wxGetApp().m_cache_subscribers.clear(); wxGetApp().m_user_update_privacy_subscribers.clear(); wxGetApp().m_foreground_change_subscribers.clear(); @@ -1340,24 +1255,6 @@ void SSWCP_Instance::sw_UnsubscribeAll() { } void SSWCP_Instance::sw_Webview_Unsubscribe() { - auto& device_map = wxGetApp().m_device_card_subscribers; - for (auto iter = device_map.begin(); iter != device_map.end();) { - if (iter->first == m_webview) { - iter = device_map.erase(iter); - } else { - iter++; - } - } - - auto& login_map = wxGetApp().m_user_login_subscribers; - for (auto iter = login_map.begin(); iter != login_map.end();) { - if (iter->first == m_webview) { - iter = login_map.erase(iter); - } else { - iter++; - } - } - auto& recent_file_map = wxGetApp().m_recent_file_subscribers; for (auto iter = recent_file_map.begin(); iter != recent_file_map.end();) { if (iter->first == m_webview) { @@ -1367,15 +1264,6 @@ void SSWCP_Instance::sw_Webview_Unsubscribe() { } } - auto& cache_map = wxGetApp().m_cache_subscribers; - for (auto iter = cache_map.begin(); iter != cache_map.end();) { - if (iter->first.first == m_webview) { - iter = cache_map.erase(iter); - } else { - iter++; - } - } - auto& privacy_map = wxGetApp().m_user_update_privacy_subscribers; for (auto iter = privacy_map.begin(); iter != privacy_map.end();) { if (iter->first == m_webview) { @@ -1408,99 +1296,9 @@ void SSWCP_Instance::sw_Unsubscribe_Filter() { return; } - auto& device_map = wxGetApp().m_device_card_subscribers; - auto& login_map = wxGetApp().m_user_login_subscribers; auto& privacy_map = wxGetApp().m_user_update_privacy_subscribers; auto& recent_file_map = wxGetApp().m_recent_file_subscribers; - auto& cache_map = wxGetApp().m_cache_subscribers; - - if (cmd == "") { - for (auto iter = device_map.begin(); iter != device_map.end();) { - if (iter->first == m_webview) { - auto ptr = iter->second.lock(); - if (ptr) { - if (ptr->m_event_id == event_id) { - iter = device_map.erase(iter); - } else { - iter++; - } - } else { - iter = device_map.erase(iter); - } - } else { - iter++; - } - } - - for (auto iter = login_map.begin(); iter != login_map.end();) { - if (iter->first == m_webview) { - auto ptr = iter->second.lock(); - if (ptr) { - if (ptr->m_event_id == event_id) { - iter = login_map.erase(iter); - } else { - iter++; - } - } else { - iter = login_map.erase(iter); - } - } else { - iter++; - } - } - - for (auto iter = privacy_map.begin(); iter != privacy_map.end();) { - if (iter->first == m_webview) { - auto ptr = iter->second.lock(); - if (ptr) { - if (ptr->m_event_id == event_id) { - iter = privacy_map.erase(iter); - } else { - iter++; - } - } else { - iter = privacy_map.erase(iter); - } - } else { - iter++; - } - } - - for (auto iter = recent_file_map.begin(); iter != recent_file_map.end();) { - if (iter->first == m_webview) { - auto ptr = iter->second.lock(); - if (ptr) { - if (ptr->m_event_id == event_id) { - iter = recent_file_map.erase(iter); - } else { - iter++; - } - } else { - iter = recent_file_map.erase(iter); - } - } else { - iter++; - } - } - - for (auto iter = cache_map.begin(); iter != cache_map.end();) { - if (iter->first.first == m_webview) { - auto ptr = iter->first.second.lock(); - if (ptr) { - if (ptr->m_event_id == event_id) { - iter = cache_map.erase(iter); - } else { - iter++; - } - } else { - iter = cache_map.erase(iter); - } - } else { - iter++; - } - } - - } else if (cmd == "sw_SubscribeRecentFiles") { + if (cmd == "sw_SubscribeRecentFiles") { for (auto iter = recent_file_map.begin(); iter != recent_file_map.end();) { if (iter->first == m_webview) { auto ptr = iter->second.lock(); @@ -1517,26 +1315,9 @@ void SSWCP_Instance::sw_Unsubscribe_Filter() { iter++; } } - } else if (cmd == "sw_SubscribeUserLoginState") { - for (auto iter = login_map.begin(); iter != login_map.end();) { - if (iter->first == m_webview) { - auto ptr = iter->second.lock(); - if (ptr) { - if (event_id == "" || (event_id != "" && event_id == ptr->m_event_id)) { - iter = login_map.erase(iter); - } else { - iter++; - } - } else { - iter = login_map.erase(iter); - } - } else { - iter++; - } - } } else if (cmd == UPDATE_PRIVACY_STATUS) { - for (auto iter = privacy_map.begin(); iter != privacy_map.end();) { + for (auto iter = privacy_map.begin(); iter != privacy_map.end();) { if (iter->first == m_webview) { auto ptr = iter->second.lock(); if (ptr) { @@ -1552,44 +1333,7 @@ void SSWCP_Instance::sw_Unsubscribe_Filter() { iter++; } } - - }else if (cmd == "sw_SubscribeLocalDevices") { - for (auto iter = device_map.begin(); iter != device_map.end();) { - if (iter->first == m_webview) { - auto ptr = iter->second.lock(); - if (ptr) { - if (event_id == "" || (event_id != "" && event_id == ptr->m_event_id)) { - iter = device_map.erase(iter); - } else { - iter++; - } - } else { - iter = device_map.erase(iter); - } - } else { - iter++; - } - } - } else if (cmd == "sw_SubscribeCacheKey") { - for (auto iter = cache_map.begin(); iter != cache_map.end();) { - if (iter->first.first == m_webview) { - auto ptr = iter->first.second.lock(); - if (ptr) { - if (event_id == "" || (event_id != "" && event_id == ptr->m_event_id)) { - iter = cache_map.erase(iter); - } else { - iter++; - } - } else { - iter = cache_map.erase(iter); - } - } else { - iter++; - } - } - } - else - { + } else { BOOST_LOG_TRIVIAL(warning) << "unknown command: " << cmd; } send_to_js(); @@ -1607,212 +1351,6 @@ void SSWCP_Instance::on_timeout() { finish_job(); } -void SSWCP_Instance::update_filament_info(const json& objects, bool send_message) -{ - if (objects.size() < 1) { - if (send_message) - handle_general_fail(-1, "objects is empty!"); - return; - } - - if (!objects[0].count("key") || !objects[0]["key"].is_string()) { - if (send_message) - handle_general_fail(-1, "param [key] required or wrong type!"); - return; - } - - if (!objects[0].count("value") || !objects[0]["value"].is_string()) { - if (send_message) - handle_general_fail(-1, "param [value] required or wrong type!"); - return; - } - - std::string sn = objects[0]["key"].get(); - std::string value = objects[0]["value"].get(); - - DeviceInfo info; - if (!wxGetApp().app_config->get_device_info(sn, info)) { - m_msg = "sn does not exist!"; - if (send_message) { - handle_general_fail(-1, m_msg); - } - return; - } else { - if (!info.connected) { - m_msg = "The machine is not connected!"; - if (send_message) { - handle_general_fail(-1, m_msg); - } - return; - } else { - json j_value; - try { - j_value = json::parse(value); - } catch (std::exception& e) { - if (send_message) - handle_general_fail(-1, "value parse failed"); - return; - } - - if (!j_value.count("nozzle_diameters") ||!j_value.count("filament_vendor") || !j_value["filament_vendor"].is_array() || - !j_value.count("filament_type") || - !j_value["filament_type"].is_array() || !j_value.count("filament_sub_type") || !j_value["filament_sub_type"].is_array() || - ((!j_value.count("filament_color") || !j_value["filament_color"].is_array()) && - (!j_value.count("filament_color_rgba") || !j_value["filament_color_rgba"].is_array())) || - !j_value.count("extruder_map_table") || !j_value["extruder_map_table"].is_array() || !j_value.count("filament_official") || - !j_value["filament_official"].is_array()) { - if (send_message) { - handle_general_fail(-1, "value parse failed"); - } - return; - } - - // Validate optional nozzle_volume_types - std::vector nozzle_volume_types; - const auto flow_status = SSWCPProtocol::parse_optional_flow_types( - j_value, "nozzle_volume_types", j_value["filament_official"].size(), nozzle_volume_types); - if (flow_status == SSWCPProtocol::OptionalFlowTypesStatus::Invalid) { - // Degrade: treat malformed flow data as absent rather than aborting the - // entire filament update, which would discard otherwise-valid entries. - nozzle_volume_types.clear(); - } - - - // save filament and update the gui filament - auto& filaments = wxGetApp().preset_bundle->machine_filaments; - auto& machine_nozzles = wxGetApp().preset_bundle->m_connect_machine_info_list; - static auto tmp_filaments = filaments; - - if (m_first_connected) { - tmp_filaments = filaments; - m_first_connected = false; - } - - machine_nozzles.clear(); - filaments.clear(); - - size_t count = 0; - for (size_t i = 0; i < j_value["filament_official"].size(); ++i) { - bool is_official = j_value["filament_official"][i].get(); - ConnectMachineInfo machineData; - if (/*is_official*/ true) { - std::string vendor = j_value["filament_vendor"][i].get(); - std::string type = j_value["filament_type"][i].get(); - std::string sub_type = j_value["filament_sub_type"][i].get(); - - machineData.filament_type = type; - - std::string name = ""; - - if (sub_type == "Support") { - name = vendor + " Support" + " For " + type; - } else { - name = vendor + " " + type + ((sub_type != "NONE" && sub_type != "") ? " " + sub_type : ""); - } - - int extruder = j_value["extruder_map_table"][i].get(); - machineData.index = static_cast(i); - machineData.filament_info = name; - - json::const_iterator multiColorIt = j_value.find("filament_color_multi"); - if (multiColorIt != j_value.end() && multiColorIt->is_array() && multiColorIt->size() > i && - (*multiColorIt)[i].is_object()) - { - const json& multiColor = (*multiColorIt)[i]; - json::const_iterator colorsIt = multiColor.find("colors"); - if (colorsIt != multiColor.end() && colorsIt->is_array()) - { - for (const json& colorJson : *colorsIt) - { - if (!colorJson.is_string()) - continue; - - const std::string colorText = colorJson.get(); - const std::string normalized = FilamentColorUtils::NormalizeHexColor(colorText); - if (!normalized.empty()) - machineData.multiColors.emplace_back(normalized); - } - } - - json::const_iterator modeIt = multiColor.find("mode"); - if (machineData.multiColors.size() > 1 && modeIt != multiColor.end() && modeIt->is_number_integer()) - machineData.colorMode = FilamentColorModeFromConfig(modeIt->get()); - } - - if (j_value.count("filament_color_rgba") && j_value["filament_color_rgba"].is_array() && - j_value["filament_color_rgba"].size() != 0) { - std::string str_color = "#" + j_value["filament_color_rgba"][i].get(); - const std::string normalizedColor = FilamentColorUtils::NormalizeHexColor(str_color, "#FFFFFF"); - str_color = normalizedColor.empty() ? str_color : normalizedColor; - filaments.insert({int(i), {name, str_color}}); - machineData.color_info = str_color; - } else { - if (j_value["filament_color"][i].is_number()) { - int color = j_value["filament_color"][i].get(); - std::ostringstream oss; - oss << "#" << std::uppercase << std::setfill('0') << std::setw(6) << std::hex - << (color & 0x00FFFFFF); // lower 24 - - std::string str_color = oss.str(); - filaments.insert({int(i), {name, str_color}}); - machineData.color_info = str_color; - } else { - std::string str_color = "#" + j_value["filament_color"][i].get(); - const std::string normalizedColor = FilamentColorUtils::NormalizeHexColor(str_color, "#FFFFFF"); - str_color = normalizedColor.empty() ? str_color : normalizedColor; - filaments.insert({int(i), {name, str_color}}); - machineData.color_info = str_color; - } - } - if (machineData.multiColors.empty() && !machineData.color_info.empty()) - machineData.multiColors.emplace_back(machineData.color_info); - if (machineData.multiColors.size() <= 1) - machineData.colorMode = FilamentColorMode::Segment; - if (j_value["nozzle_diameters"].is_array() && !j_value["nozzle_diameters"].empty()) - machineData.nozzle_info = j_value["nozzle_diameters"][i].get(); - if (flow_status == SSWCPProtocol::OptionalFlowTypesStatus::Valid && i < nozzle_volume_types.size()) - machineData.nozzle_volume_type = nozzle_volume_types[i]; - machine_nozzles.push_back(machineData); - } - } - - bool need_load_preset = false; - - if (filaments.size() == 0 && tmp_filaments.size() != 0) - need_load_preset = true; - - for (auto iter = filaments.begin(); iter != filaments.end(); iter++) { - if (tmp_filaments.count(iter->first)) { - auto pair = iter->second; - auto tmp_pair = tmp_filaments[iter->first]; - if (pair.first == tmp_pair.first && pair.second == tmp_pair.second) { - continue; - } else { - need_load_preset = true; - break; - } - } else { - auto pair = iter->second; - if (pair.first.find("NONE") != std::string::npos) { - continue; - } - need_load_preset = true; - break; - } - } - if (need_load_preset) { - tmp_filaments = filaments; - wxGetApp().load_current_presets(); - } - - if (send_message) { - send_to_js(); - finish_job(); - } - } - } -} - // SSWCP_MachineFind_Instance implementation // Process machine find commands @@ -2122,36 +1660,6 @@ void SSWCP_MachineFind_Instance::add_machine_to_list(const json& machine_info) m_res_data[key]["connected"] = false; - DeviceInfo info; - if (wxGetApp().app_config->get_device_info(sn, info)) { - if (info.connected) { - m_res_data[key]["connected"] = true; - } - std::string ip = value["ip"].get(); - if (0) { - info.ip = ip; - wxGetApp().app_config->save_device_info(info); - - - - wxGetApp().CallAfter([]() { - auto devices = wxGetApp().app_config->get_devices(); - json param; - param["command"] = "local_devices_arrived"; - param["sequece_id"] = "10001"; - param["data"] = devices; - std::string logout_cmd = param.dump(); - wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - GUI::wxGetApp().run_script(strJS); - - // wcp sub - json data = devices; - wxGetApp().device_card_notify(data); - }); - - } - } - if (need_send) { send_to_js(); } @@ -2185,22 +1693,6 @@ void SSWCP_MachineOption_Instance::process() sw_SendGCodes(); } else if (m_cmd == "sw_FileGetStatus") { sw_FileGetStatus(); - } else if (m_cmd == "sw_SystemGetDeviceInfo") { - sw_SystemGetDeviceInfo(); - } else if (m_cmd == "sw_GetMachineState") { - sw_GetMachineState(); - } else if (m_cmd == "sw_SubscribeMachineState") { - sw_SubscribeMachineState(); - } else if (m_cmd == "sw_GetMachineObjects") { - sw_GetMachineObjects(); - } else if (m_cmd == "sw_SetSubscribeFilter") { - sw_SetMachineSubscribeFilter(); - } else if (m_cmd == "sw_StopMachineStateSubscription") { - sw_UnSubscribeMachineState(); - } else if (m_cmd == "sw_GetPrinterInfo") { - sw_GetPrintInfo(); - } else if (m_cmd == "sw_GetMachineSystemInfo") { - sw_GetSystemInfo(); } else if (m_cmd == "sw_MachinePrintStart") { sw_MachinePrintStart(); } else if (m_cmd == "sw_MachinePrintPause") { @@ -2273,8 +1765,6 @@ void SSWCP_MachineOption_Instance::process() sw_exception_query(); } else if (m_cmd == "sw_GetFileListPage") { sw_GetFileListPage(); - } else if (m_cmd == "sw_UpdateMachineFilamentInfo") { - sw_UpdateMachineFilamentInfo(); } else if (m_cmd == "sw_UploadCameraTimelapse") { sw_UploadCameraTimelapse(); } else if (m_cmd == "sw_UploadAsyncTimelapseInstance") { @@ -2298,152 +1788,6 @@ void SSWCP_MachineOption_Instance::process() } } -void SSWCP_MachineOption_Instance::sw_UnSubscribeMachineState() { - try { - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - - if (!host) { - m_status = 1; - m_msg = "failure"; - send_to_js(); - finish_job(); - } - - auto weak_self = std::weak_ptr(shared_from_this()); - std::string key = m_event_id + std::to_string(int64_t(m_webview)); - host->async_unsubscribe_machine_info(key, [weak_self](const json& response) { - auto self = weak_self.lock(); - if (self) { - SSWCP_Instance::on_mqtt_msg_arrived(self, response); - } - }); - - SSWCP::stop_subscribe_machine(); - - - } catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MachineOption_Instance::sw_SubscribeMachineState() { - try { - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - - if (!host) { - m_status = 1; - m_msg = "failure"; - send_to_js(); - finish_job(); - return; - } - - auto weak_self = std::weak_ptr(shared_from_this()); - std::string key = m_event_id + std::to_string(int64_t(m_webview)); - host->async_subscribe_machine_info(key, [weak_self](const json& response) { - auto self = weak_self.lock(); - if (self) { - SSWCP_Instance::on_mqtt_status_msg_arrived(self, response); - } - }); - - } catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MachineOption_Instance::sw_GetPrintInfo() { - try { - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - - if (!host) { - handle_general_fail(); - return; - } - - auto weak_self = std::weak_ptr(shared_from_this()); - host->async_get_printer_info([weak_self](const json& response) { - auto self = weak_self.lock(); - if (self) { - SSWCP_Instance::on_mqtt_msg_arrived(self, response); - } - }); - } - catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MachineOption_Instance::sw_GetMachineState() { - try { - if (m_param_data.count("objects")) { - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - std::vector>> targets; - - json items = m_param_data["objects"]; - for (auto& [key, value] : items.items()) { - if (value.is_null()) { - targets.push_back({key, {}}); - } else { - std::vector items; - if (value.is_array()) { - for (size_t i = 0; i < value.size(); ++i) { - items.push_back(value[i].get()); - } - } else { - items.push_back(value.get()); - } - targets.push_back({key, items}); - } - } - - if (!host) { - handle_general_fail(); - return; - } - - auto weak_self = std::weak_ptr(shared_from_this()); - host->async_get_machine_info(targets, [weak_self](const json& response) { - auto self = weak_self.lock(); - if (self) { - SSWCP_Instance::on_mqtt_msg_arrived(self, response); - } - }); - } else { - handle_general_fail(); - } - - } catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MachineOption_Instance::sw_SystemGetDeviceInfo() { - try { - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - if (!host) { - handle_general_fail(); - return; - } - - auto weak_self = std::weak_ptr(shared_from_this()); - host->async_get_device_info([weak_self](const json& response) { - auto self = weak_self.lock(); - if (self) { - SSWCP_Instance::on_mqtt_msg_arrived(self, response); - } - }); - - } catch (const std::exception&) { - handle_general_fail(); - } -} - void SSWCP_MachineOption_Instance::sw_FileGetStatus() { try { @@ -2600,75 +1944,32 @@ void SSWCP_MachineOption_Instance::sw_MachinePrintCancel() } } -void SSWCP_MachineOption_Instance::sw_GetSystemInfo() +void SSWCP_MachineOption_Instance::sw_PullCloudFile() { try { std::shared_ptr host = nullptr; wxGetApp().get_connect_host(host); if (!host) { - handle_general_fail(); + handle_general_fail(-1, "Can't find the active machine"); return; } + json items = m_param_data; + auto weak_self = std::weak_ptr(shared_from_this()); - host->async_get_system_info([weak_self](const json& response) { + host->async_pull_cloud_file(items, [weak_self](const json& response) { auto self = weak_self.lock(); if (self) { SSWCP_Instance::on_mqtt_msg_arrived(self, response); } }); - } - catch(std::exception& e){ - handle_general_fail(); - } -} - -void SSWCP_MachineOption_Instance::sw_SetMachineSubscribeFilter() -{ - try { - if (m_param_data.count("objects")) { - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - std::vector>> targets; - - json items = m_param_data["objects"]; - for (auto& [key, value] : items.items()) { - if (value.is_null()) { - targets.push_back({key, {}}); - } else { - std::vector items; - if (value.is_array()) { - for (size_t i = 0; i < value.size(); ++i) { - items.push_back(value[i].get()); - } - } else { - items.push_back(value.get()); - } - targets.push_back({key, items}); - } - } - - if (!host) { - handle_general_fail(); - } else { - auto weak_self = std::weak_ptr(shared_from_this()); - host->async_set_machine_subscribe_filter(targets, [weak_self](const json& response) { - auto self = weak_self.lock(); - if (self) { - SSWCP_Instance::on_mqtt_msg_arrived(self, response); - } - }); - } - } else { - handle_general_fail(); - } - } catch (std::exception& e) { handle_general_fail(); } } -void SSWCP_MachineOption_Instance::sw_GetMachineObjects() + +void SSWCP_MachineOption_Instance::sw_CancelPullCloudFile() { try { std::shared_ptr host = nullptr; @@ -2680,19 +1981,18 @@ void SSWCP_MachineOption_Instance::sw_GetMachineObjects() } auto weak_self = std::weak_ptr(shared_from_this()); - host->async_get_machine_objects([weak_self](const json& response) { + host->async_cancel_pull_cloud_file([weak_self](const json& response) { auto self = weak_self.lock(); if (self) { SSWCP_Instance::on_mqtt_msg_arrived(self, response); } }); - } catch (std::exception& e) { handle_general_fail(); } } -void SSWCP_MachineOption_Instance::sw_PullCloudFile() +void SSWCP_MachineOption_Instance::sw_StartCloudPrint() { try { std::shared_ptr host = nullptr; @@ -2706,97 +2006,33 @@ void SSWCP_MachineOption_Instance::sw_PullCloudFile() json items = m_param_data; auto weak_self = std::weak_ptr(shared_from_this()); - host->async_pull_cloud_file(items, [weak_self](const json& response) { + host->async_start_cloud_print(items, [weak_self](const json& response) { auto self = weak_self.lock(); if (self) { SSWCP_Instance::on_mqtt_msg_arrived(self, response); } }); - } catch (std::exception& e) { + } + catch (std::exception& e) { handle_general_fail(); } } -void SSWCP_MachineOption_Instance::sw_UpdateMachineFilamentInfo() +void SSWCP_MachineOption_Instance::sw_StartLocalPrint() { - try { - if (!m_param_data.count("objects") || !m_param_data["objects"].is_array()) { - handle_general_fail(-1, "param [objects] required or wrong type!"); - return; - } + if (!m_param_data.count("type") || !m_param_data.count("path")) { + BOOST_LOG_TRIVIAL(error) << "[WCP] sw_StartLocalPrint: param [type] or [path] required!"; + handle_general_fail(-1, "param [type] or [path] required!"); + return; + } - update_filament_info(m_param_data["objects"], true); + std::shared_ptr host = nullptr; + wxGetApp().get_connect_host(host); - } - catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MachineOption_Instance::sw_CancelPullCloudFile() -{ - try { - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - - if (!host) { - handle_general_fail(-1, "Can't find the active machine"); - return; - } - - auto weak_self = std::weak_ptr(shared_from_this()); - host->async_cancel_pull_cloud_file([weak_self](const json& response) { - auto self = weak_self.lock(); - if (self) { - SSWCP_Instance::on_mqtt_msg_arrived(self, response); - } - }); - } catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MachineOption_Instance::sw_StartCloudPrint() -{ - try { - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - - if (!host) { - handle_general_fail(-1, "Can't find the active machine"); - return; - } - - json items = m_param_data; - - auto weak_self = std::weak_ptr(shared_from_this()); - host->async_start_cloud_print(items, [weak_self](const json& response) { - auto self = weak_self.lock(); - if (self) { - SSWCP_Instance::on_mqtt_msg_arrived(self, response); - } - }); - } - catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MachineOption_Instance::sw_StartLocalPrint() -{ - if (!m_param_data.count("type") || !m_param_data.count("path")) { - BOOST_LOG_TRIVIAL(error) << "[WCP] sw_StartLocalPrint: param [type] or [path] required!"; - handle_general_fail(-1, "param [type] or [path] required!"); - return; - } - - std::shared_ptr host = nullptr; - wxGetApp().get_connect_host(host); - - if (!host) { - BOOST_LOG_TRIVIAL(error) << "[WCP] sw_StartLocalPrint: Can't find the active machine"; - handle_general_fail(-1, "Can't find the active machine"); - return; + if (!host) { + BOOST_LOG_TRIVIAL(error) << "[WCP] sw_StartLocalPrint: Can't find the active machine"; + handle_general_fail(-1, "Can't find the active machine"); + return; } json items = m_param_data; @@ -3292,6 +2528,198 @@ void SSWCP_MachineOption_Instance::on_finish_filament_mapping_custom_flow_regrou wxGetApp().mainframe->start_slice(); }); } +nlohmann::json SSWCP::build_filament_mapping_json(const std::string& filename, const std::string& display_name) +{ + json response = json::object(); + try { + if (filename.empty() || !boost::filesystem::exists(filename) || !boost::filesystem::is_regular_file(filename)) + return response; + + auto* current_plate = wxGetApp().plater() ? wxGetApp().plater()->get_partplate_list().get_curr_plate() : nullptr; + if (current_plate == nullptr) + return response; + + auto* print = current_plate->fff_print(); + if (print == nullptr) + return response; + + auto& config = print->config(); + auto full_config = print->full_print_config(); + auto* slice_result = current_plate->get_slice_result(); + if (slice_result == nullptr) + return response; + + auto& result = *slice_result; + response["estimated_time"] = result.print_statistics.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].time; + + auto color_to_int = [](const std::string& original_color) -> long long { return preprint_color_to_int(original_color); }; + + if (config.has("filament_colour")) { + std::vector filament_color = config.option("filament_colour")->values; + const ConfigOptionStrings* filament_multi_colors = nullptr; + if (config.has("filament_multi_colors")) + filament_multi_colors = config.option("filament_multi_colors"); + const ConfigOptionInts* filament_colour_modes = nullptr; + if (config.has("filament_colour_mode")) + filament_colour_modes = config.option("filament_colour_mode"); + + std::vector number_result(filament_color.size(), 0); + std::vector string_result(filament_color.size()); + json multi_color_result = json::array(); + for (size_t i = 0; i < filament_color.size(); ++i) { + number_result[i] = color_to_int(filament_color[i]); + string_result[i] = filament_color[i]; + const bool has_multi_colors = filament_multi_colors != nullptr && filament_multi_colors->values.size() > i; + const bool has_mode = filament_colour_modes != nullptr && filament_colour_modes->values.size() > i; + const std::string multi_colors = has_multi_colors ? filament_multi_colors->values[i] : std::string{}; + FilamentColorMode color_mode = FilamentColorMode::Segment; + if (has_mode) + color_mode = FilamentColorModeFromConfig(filament_colour_modes->values[i]); + multi_color_result.push_back(FilamentColorUtils::BuildPreprintColorMultiItem(multi_colors, color_mode, filament_color[i])); + } + response["filament_color"] = number_result; + response["filament_color_rgba"] = string_result; + response["filament_color_multi"] = multi_color_result; + } + + if (const auto* filament_type_opt = full_config.option("filament_type"); + filament_type_opt != nullptr && !filament_type_opt->values.empty()) { + std::vector filament_types; + size_t filament_count = filament_type_opt->values.size(); + if (const auto* filament_colour_opt = full_config.option("filament_colour")) + filament_count = std::max(filament_count, filament_colour_opt->values.size()); + filament_types.reserve(filament_count); + for (size_t i = 0; i < filament_count; ++i) { + std::string filament_type = filament_type_opt->get_at(int(i)); + boost::trim(filament_type); + filament_types.emplace_back(std::move(filament_type)); + } + response["filament_type"] = filament_types; + } + + if (full_config.has("nozzle_diameter")) { + const auto* nozzle_diameters_opt = full_config.option("nozzle_diameter"); + if (nozzle_diameters_opt != nullptr) { + std::vector nozzle_diameters; + nozzle_diameters.reserve(nozzle_diameters_opt->values.size()); + for (double diameter : nozzle_diameters_opt->values) { + std::ostringstream stream; + stream << std::fixed << std::setprecision(1) << diameter; + nozzle_diameters.emplace_back(stream.str()); + } + response["nozzle_diameters"] = nozzle_diameters; + } + } + + if (config.has("filament_density")) { + auto filament_density = config.option("filament_density")->values; + std::vector filament_used_g(filament_density.size(), 0); + double total_weight = 0; + for (const auto& item : result.print_statistics.total_volumes_per_extruder) { + if (item.first >= filament_density.size()) + continue; + filament_used_g[item.first] = filament_density[item.first] * item.second * 0.001; + total_weight += filament_used_g[item.first]; + } + response["filament_weight"] = filament_used_g; + response["filament_weight_total"] = total_weight; + } + + if (config.has("filament_diameter")) { + const auto* filament_diameter_opt = config.option("filament_diameter"); + if (!filament_diameter_opt) + return response; + auto filament_diameter = filament_diameter_opt->values; + std::vector filament_used_mm(filament_diameter.size(), 0); + for (const auto& item : result.print_statistics.total_volumes_per_extruder) { + if (item.first >= filament_diameter.size()) + continue; + const double diameter = static_cast(filament_diameter[item.first]); + if (diameter > 0) + filament_used_mm[item.first] = item.second / (M_PI * diameter * 0.5 * diameter * 0.5); + } + response["filament_used_mm"] = filament_used_mm; + } + + auto& filament_extruder_map = wxGetApp().app_config->get_filament_extruder_map_ref(); + if (!filament_extruder_map.empty()) { + json object; + for (const auto& item : filament_extruder_map) + object[std::to_string(item.first)] = std::to_string(item.second); + response["filament_extruder_map"] = object; + } + + if (current_plate) { + auto* current_print = current_plate->fff_print(); + auto* nozzle_opt = current_print ? current_print->config().option("nozzle_diameter") : nullptr; + if (nozzle_opt) { + std::vector nozzle_list; + for (float diameter : nozzle_opt->values) { + nozzle_list.push_back(std::abs(diameter - 0.2f) < 1e-5f ? "0.2" : + std::abs(diameter - 0.4f) < 1e-5f ? "0.4" : + std::abs(diameter - 0.6f) < 1e-5f ? "0.6" : + std::abs(diameter - 0.8f) < 1e-5f ? "0.8" : std::to_string(diameter)); + } + response["nozzle_info"] = nozzle_list; + } + } + + auto current_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + std::string preset_name; + if (current_preset.is_system) { + preset_name = current_preset.name; + } else { + auto base_preset = wxGetApp().preset_bundle->printers.get_preset_base(current_preset); + preset_name = base_preset->name; + } + response["machine_model"] = preset_name; + + json thumbnails = json::array(); + if (config.has("thumbnails")) { + std::string thumbnail_description = config.option("thumbnails")->value; + std::vector> thumbnail_sizes; + do { + const size_t separator = thumbnail_description.find(", "); + std::string item; + if (separator != std::string::npos) { + item = thumbnail_description.substr(0, separator); + thumbnail_description = thumbnail_description.substr(separator + 2); + } else { + item = thumbnail_description; + thumbnail_description.clear(); + } + const size_t slash = item.find("/"); + if (slash == std::string::npos) + break; + item = item.substr(0, slash); + const size_t x = item.find("x"); + if (x == std::string::npos) + break; + double width = 0.0; + double height = 0.0; + if (!parse_thumbnail_dimension(item.substr(0, x), width) || !parse_thumbnail_dimension(item.substr(x + 1), height)) + break; + thumbnail_sizes.emplace_back(width, height); + } while (!thumbnail_description.empty()); + + auto thumbnail_list = load_thumbnails(filename, int(thumbnail_sizes.size())); + for (size_t i = 0; i < thumbnail_list.size(); ++i) { + json thumbnail; + thumbnail["url"] = "data:image/png;base64," + thumbnail_list[i]; + thumbnail["width"] = thumbnail_sizes[i].first; + thumbnail["height"] = thumbnail_sizes[i].second; + thumbnails.push_back(std::move(thumbnail)); + } + } + response["thumbnails"] = thumbnails; + response["filename"] = display_name; + response["filepath"] = filename; + } catch (...) { + return json::object(); + } + return response; +} + void SSWCP_MachineOption_Instance::sw_GetFileFilamentMapping() { try { @@ -3313,42 +2741,36 @@ void SSWCP_MachineOption_Instance::sw_GetFileFilamentMapping() return; } - auto* print = wxGetApp().plater()->get_partplate_list().get_curr_plate()->fff_print(); + auto* current_plate = wxGetApp().plater() ? wxGetApp().plater()->get_partplate_list().get_curr_plate() : nullptr; + if (current_plate == nullptr) { + handle_general_fail(); + return; + } + + auto* print = current_plate->fff_print(); + if (print == nullptr) { + handle_general_fail(); + return; + } + auto& config = print->config(); auto full_config = print->full_print_config(); - auto& result = *(wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_slice_result()); + auto* slice_result = current_plate->get_slice_result(); + if (slice_result == nullptr) { + handle_general_fail(); + return; + } + + auto& result = *slice_result; /*GCodeProcessor processor; processor.process_file(filename.data()); auto& result = processor.result(); auto& config = processor.current_dynamic_config();*/ - auto time = wxGetApp() - .mainframe->plater() - ->get_partplate_list() - .get_curr_plate() - ->get_slice_result() - ->print_statistics.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)] - .time; + auto time = result.print_statistics.modes[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].time; response["estimated_time"] = time; - auto color_to_int = [](const std::string& oriclr) -> long long { - - long long res = 0; - if ((oriclr.size() != 7 && oriclr.size() != 9) || oriclr[0] != '#') { - return 0; - } - - auto colorSize = oriclr.size();//7 or 9 - for (auto i = 1; i < colorSize; i++) - { - if (oriclr[colorSize - i] - '0' >= 0 && oriclr[colorSize - i] - '0' <= 9) { - res += std::pow(16, i - 1) * (oriclr[colorSize - i] - '0'); - } else { - res += std::pow(16, i - 1) * (oriclr[colorSize - i] - 'A' + 10); - } - } - return res; - }; + auto color_to_int = [](const std::string& oriclr) -> long long { return preprint_color_to_int(oriclr); }; // filament colour if (config.has("filament_colour")) { @@ -4279,8 +3701,6 @@ void SSWCP_MachineConnect_Instance::process() { sw_connect(); } else if (m_cmd == "sw_Disconnect") { sw_disconnect(); - } else if (m_cmd == "sw_GetConnectedMachine") { - sw_get_connect_machine(); } else if (m_cmd == "sw_ConnectOtherMachine"){ sw_connect_other_device(); } else if (m_cmd == "sw_GetPincode") { @@ -4474,56 +3894,17 @@ void SSWCP_MachineConnect_Instance::sw_connect() { } -void SSWCP_MachineConnect_Instance::sw_get_connect_machine() { - try { - auto devices = wxGetApp().app_config->get_devices(); - for (const auto& device : devices) { - if (device.connected) { - m_res_data = device; - break; - } - } - - send_to_js(); - finish_job(); - } - catch (std::exception& e) { - handle_general_fail(); - } -} - void SSWCP_MachineConnect_Instance::sw_disconnect() { bool need_reload = m_param_data.count("need_reload") ? m_param_data["need_reload"].get() : true; - std::string dev_id = m_param_data.count("dev_id") ? m_param_data["dev_id"] : ""; - auto weak_self = std::weak_ptr(shared_from_this()); if (m_work_thread.joinable()) m_work_thread.join(); - m_work_thread = std::thread([weak_self, need_reload, dev_id](){ + m_work_thread = std::thread([weak_self, need_reload](){ auto self = weak_self.lock(); - if (dev_id != "") { - DeviceInfo info; - if (wxGetApp().app_config->get_device_info(dev_id, info)) { - if (info.connected == false) { - if (self) { - self->handle_general_fail(-3, dev_id + " is not connected before disconnect!"); - return; - } - } - } else { - if (self) { - self->handle_general_fail(-1, dev_id + " is not exist!"); - return; - } - } - } - - - bool res = wxGetApp().sm_disconnect_current_machine(need_reload); m_first_connected = true; if (!res) { @@ -4537,7 +3918,6 @@ void SSWCP_MachineConnect_Instance::sw_disconnect() { wxGetApp().CallAfter([]() { wxGetApp().app_config->clear_filament_extruder_map(); - wxGetApp().preset_bundle->machine_filaments.clear(); wxGetApp().load_current_presets(); }); @@ -4700,18 +4080,7 @@ void SSWCP_UserLogin_Instance::process() m_header.clear(); m_header["event_id"] = m_event_id; } - if (m_cmd == "sw_UserLogin") { - sw_UserLogin(); - } else if (m_cmd == "sw_AskUserLogin") { - sw_AskUserLogin(); - } else if (m_cmd == "sw_UserLogout") { - sw_UserLogout(); - } else if (m_cmd == "sw_GetUserLoginState") { - sw_GetUserLoginState(); - } else if (m_cmd == "sw_SubscribeUserLoginState") { - sw_SubscribeUserLoginState(); - } - else if (m_cmd == UPDATE_PRIVACY_STATUS) { + if (m_cmd == UPDATE_PRIVACY_STATUS) { sw_SubUserUpdatePrivacy(); } else if (m_cmd == GET_PRIVACY_STATUS) { sw_GetUserUpdatePrivacy(); @@ -4738,166 +4107,44 @@ void SSWCP_UserLogin_Instance::process() handle_general_fail(); } } -void SSWCP_UserLogin_Instance::sw_UserLogin() +void SSWCP_UserLogin_Instance::sw_GetUserUpdatePrivacy() { - try { - send_to_js(); - finish_job(); - bool show = m_param_data.count("show") ? m_param_data["show"].get() : true; + json data; + auto isAgree = wxGetApp().app_config->get("app", PRIVACY_POLICY_FLAGS); + bool isUserAgree = false; + + if (isAgree == "true") + isUserAgree = true; + + data[PRIVACY_POLICY_FLAGS] = isUserAgree; + + m_res_data = data; + send_to_js(); + finish_job(); - wxGetApp().CallAfter([show]() { - wxGetApp().sm_request_login(show); - }); - } - catch (std::exception& e) { - handle_general_fail(); - } } -void SSWCP_UserLogin_Instance::sw_AskUserLogin() +void SSWCP_UserLogin_Instance::sw_DownloadFileAndOpen() { - auto weak_self = std::weak_ptr(shared_from_this()); - wxGetApp().CallAfter([weak_self]() { - auto self = weak_self.lock(); - if (!self) - return; + try { + std::string fileName = m_param_data.count("file_name") ? m_param_data["file_name"].get() : ""; + std::string fileUrl = m_param_data.count("file_url") ? m_param_data["file_url"].get() : ""; - if (s_ask_dialog_showing) { - auto already_queued = [&self](const std::weak_ptr& waiter) { - auto locked = waiter.lock(); - return locked && locked.get() == self.get(); - }; - if (!std::any_of(s_ask_waiters.begin(), s_ask_waiters.end(), already_queued)) - s_ask_waiters.push_back(self); + if (fileUrl.empty() || fileName.empty()) { + handle_general_fail(-1, wxString::FromUTF8("file_url and file_name are required")); return; } - struct AskLoginDialogStateGuard - { - bool active = true; - - ~AskLoginDialogStateGuard() - { - if (active) { - s_ask_dialog_showing = false; - s_ask_waiters.clear(); - } - } - }; - - bool login = false; - AskLoginDialogStateGuard state_guard; - s_ask_dialog_showing = true; - s_ask_waiters.push_back(self); - - SMAskUserLoginDialog dlg(wxGetApp().mainframe); - dlg.SetKeepAliveCallback([]() { - for (auto& weak : s_ask_waiters) - if (auto alive = weak.lock()) - SSWCP::renew_instance_timeout(alive.get()); - }); - login = (dlg.ShowModal() == wxID_OK); - - s_ask_dialog_showing = false; - json data; - data["result"] = login ? "login" : "cancel"; - for (auto& weak : s_ask_waiters) { - auto waiter = weak.lock(); - if (!waiter) - continue; - waiter->m_res_data = data; - waiter->send_to_js(); - waiter->finish_job(); - } - s_ask_waiters.clear(); - state_guard.active = false; - - if (login) - wxGetApp().sm_request_login(true); - }); -} - -bool SSWCP_UserLogin_Instance::s_ask_dialog_showing = false; -std::vector> SSWCP_UserLogin_Instance::s_ask_waiters; - -void SSWCP_UserLogin_Instance::sw_UserLogout() -{ - try { - send_to_js(); - wxGetApp().sm_request_user_logout(); - finish_job(); - } catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_UserLogin_Instance::sw_GetUserLoginState() -{ - try { - json data; - auto pInfo = wxGetApp().sm_get_userinfo(); - if (pInfo) { - bool islogin = pInfo->is_user_login(); - if (islogin) { - data["status"] = "online"; - data["nickname"] = pInfo->get_user_name(); - data["icon"] = pInfo->get_user_icon_url(); - data["token"] = pInfo->get_user_token(); - data["userid"] = pInfo->get_user_id(); - data["account"] = pInfo->get_user_account(); - } else { - data["status"] = "offline"; - } - - m_res_data = data; - send_to_js(); - finish_job(); - } else { - handle_general_fail(); - } - } - catch (std::exception& e) { - handle_general_fail(); - } -} -void SSWCP_UserLogin_Instance::sw_GetUserUpdatePrivacy() -{ - json data; - auto isAgree = wxGetApp().app_config->get("app", PRIVACY_POLICY_FLAGS); - bool isUserAgree = false; - - if (isAgree == "true") - isUserAgree = true; - - data[PRIVACY_POLICY_FLAGS] = isUserAgree; - - m_res_data = data; - send_to_js(); - finish_job(); - -} - -void SSWCP_UserLogin_Instance::sw_DownloadFileAndOpen() -{ - try { - std::string fileName = m_param_data.count("file_name") ? m_param_data["file_name"].get() : ""; - std::string fileUrl = m_param_data.count("file_url") ? m_param_data["file_url"].get() : ""; - - if (fileUrl.empty() || fileName.empty()) { - handle_general_fail(-1, wxString::FromUTF8("file_url and file_name are required")); - return; - } - - // Use Download Manager - DownloadManager* download_mgr = wxGetApp().download_manager(); - if (!download_mgr) { - handle_general_fail(-1, wxString::FromUTF8("Download Manager not available")); - return; - } + // Use Download Manager + DownloadManager* download_mgr = wxGetApp().download_manager(); + if (!download_mgr) { + handle_general_fail(-1, wxString::FromUTF8("Download Manager not available")); + return; + } // WebView script message runs inside the webview event handler; calling ShowModal() synchronously // (via GenericDownloadDialog in downloadOpenProject) causes re-entrancy / crashes on Windows. - // Defer to the next event-loop iteration — same pattern as sw_UserLogin(). + // Defer to the next event-loop iteration to avoid webview event re-entrancy. std::shared_ptr self = std::static_pointer_cast(shared_from_this()); wxGetApp().CallAfter([self, fileUrl, fileName]() { @@ -5973,7 +5220,6 @@ void SSWCP_UserLogin_Instance::push_timelapse_state(const std::string& sn, if (it != m_subscribe_map.end() && it->second && it->second->webview) { WebView::RunScript(it->second->webview, script); } - SSWCP::send_message_to_flutter(json_str); }); } } @@ -6240,73 +5486,6 @@ void SSWCP_UserLogin_Instance::sw_SubUserUpdatePrivacy() } -void SSWCP_UserLogin_Instance::sw_SubscribeUserLoginState() -{ - try { - std::weak_ptr weak_ptr = shared_from_this(); - wxGetApp().m_user_login_subscribers[m_webview] = weak_ptr; - } - catch (std::exception& e) { - handle_general_fail(); - } -} - -// SSWCP_MachineManage_Instance -void SSWCP_MachineManage_Instance::process() -{ - if (m_event_id != "") { - json header; - send_to_js(); - - m_header.clear(); - m_header["event_id"] = m_event_id; - } - - if (m_cmd == "sw_GetLocalDevices") { - sw_GetLocalDevices(); - } else if (m_cmd == "sw_AddDevice") { - sw_AddDevice(); - } else if (m_cmd == "sw_SubscribeLocalDevices") { - sw_SubscribeLocalDevices(); - } else if (m_cmd == "sw_RenameDevice") { - sw_RenameDevice(); - } else if (m_cmd == "sw_SwitchModel") { - sw_SwitchModel(); - } else if (m_cmd == "sw_DeleteDevices") { - sw_DeleteDevices(); - } else if (m_cmd == "sw_UpdateDeviceInfo") { - sw_UpdateDeviceInfo(); - } else { - handle_general_fail(); - } -} - -void SSWCP_MachineManage_Instance::sw_GetLocalDevices() -{ - try { - auto devices = wxGetApp().app_config->get_devices(); - m_res_data = devices; - - send_to_js(); - finish_job(); - } - catch (std::exception& e) - { - handle_general_fail(); - } -} - -void SSWCP_MachineManage_Instance::sw_SubscribeLocalDevices() -{ - try { - auto self = shared_from_this(); - wxGetApp().m_device_card_subscribers[m_webview] = self; - } catch (std::exception& e) { - handle_general_fail(); - } -} - -// SSWCP_PageStateChange_Instance void SSWCP_PageStateChange_Instance::process() { if (m_event_id != "") { @@ -6366,1522 +5545,108 @@ void SSWCP_PageStateChange_Instance::sw_UnsubscribePageStateChange() } } -void SSWCP_MachineManage_Instance::sw_AddDevice() -{ - try { - wxGetApp().CallAfter([] { - if (wxGetApp().web_device_dialog) - delete wxGetApp().web_device_dialog; - - wxGetApp().web_device_dialog = new WebDeviceDialog; - wxGetApp().web_device_dialog->run(); - }); - send_to_js(); - - finish_job(); - } catch (std::exception& e) { - handle_general_fail(); - } -} +TimeoutMap> SSWCP::m_instance_list; +constexpr std::chrono::milliseconds SSWCP::DEFAULT_INSTANCE_TIMEOUT; -void SSWCP_MachineManage_Instance::sw_RenameDevice() -{ - try { - if (m_param_data.count("dev_id") && m_param_data.count("dev_name")) { - std::string dev_id = m_param_data["dev_id"].get(); - std::string dev_name = m_param_data["dev_name"].get(); +std::string SSWCP::m_active_gcode_filename = ""; +std::string SSWCP::m_display_gcode_filename = ""; +long long SSWCP::m_active_file_size = 0; - DeviceInfo info; - wxGetApp().app_config->get_device_info(dev_id, info); - info.dev_name = dev_name; - wxGetApp().app_config->save_device_info(info); +std::unordered_map> + SSWCP_UserLogin_Instance::m_subscribe_map; +std::mutex SSWCP::m_file_size_mutex; - wxGetApp().CallAfter([] { +std::unordered_map SSWCP::m_tab_map = { + {"Home", MainFrame::TabPosition::tpHome}, + {"3DEditor", MainFrame::TabPosition::tp3DEditor}, + {"Preview", MainFrame::TabPosition::tpPreview}, + {"Monitor", MainFrame::TabPosition::tpMonitor}, + {"MultiDevice", MainFrame::TabPosition::tpMultiDevice}, + {"Project", MainFrame::TabPosition::tpProject}, + {"Calibration", MainFrame::TabPosition::tpCalibration}, + {"Auxiliary", MainFrame::TabPosition::tpAuxiliary}, + {"DebugTool", MainFrame::TabPosition::toDebugTool} +}; - // wcp订阅 - json data = wxGetApp().app_config->get_devices(); - wxGetApp().device_card_notify(data); - }); +std::unordered_set SSWCP::m_machine_find_cmd_list = { + "sw_GetMachineFindSupportInfo", + "sw_StartMachineFind", + "sw_StopMachineFind", + "sw_WakeupFind", +}; - send_to_js(); - finish_job(); - } else { - handle_general_fail(); - } - } catch (std::exception& e) { - handle_general_fail(); - } -} +std::unordered_set SSWCP::m_machine_option_cmd_list = { + "sw_SendGCodes", + "sw_FileGetStatus", + "sw_MachinePrintStart", + "sw_MachinePrintPause", + "sw_MachinePrintResume", + "sw_MachinePrintCancel", + "sw_MachineFilesRoots", + "sw_MachineFilesMetadata", + "sw_MachineFilesThumbnails", + "sw_MachineFilesGetDirectory", + "sw_CameraStartMonitor", + "sw_CameraStopMonitor", + "sw_DeleteMachineFile", + "sw_GetFileFilamentMapping", + "sw_SetFilamentMappingComplete", + "sw_FinishFilamentMapping", + "sw_DownloadMachineFile", + "sw_UploadFiletoMachine", + "sw_GetPrintLegal", + "sw_GetPrintZip", + "sw_FinishPreprint", + "sw_PullCloudFile", + "sw_CancelPullCloudFile", + "sw_StartCloudPrint", + "sw_StartLocalPrint", + "sw_MachineHeartbeat", + "sw_SetDeviceName", + "sw_ControlLed", + "sw_ControlPrintSpeed", + "sw_BedMesh_AbortProbeMesh", + "sw_ControlPurifier", + "sw_ControlMainFan", + "sw_ControlGenericFan", + "sw_ControlBedTemp", + "sw_ControlExtruderTemp", + "sw_FilesThumbnailsBase64", + "sw_exception_query", + "sw_GetFileListPage", + "sw_UploadCameraTimelapse", + "sw_UploadAsyncTimelapseInstance", + "sw_DeleteCameraTimelapse", + "sw_GetCameraTimelapseInstance", + "sw_ServerClientManagerSetUserinfo", + "sw_DefectDetactionConfig", + "sw_PrinterDefectDetection", + GET_DEVICEDATA_STORAGESPACE +}; -void SSWCP_MachineManage_Instance::sw_DeleteDevices() -{ - try { - if (m_param_data.count("dev_ids") && m_param_data["dev_ids"].is_array()) { - auto ids = m_param_data["dev_ids"]; - if (ids.size() == 0) { - auto devices = wxGetApp().app_config->get_devices(); - for (size_t i = 0; i < devices.size(); ++i) { - if (devices[i].connected) { - std::shared_ptr current_host; - wxGetApp().get_connect_host(current_host); - if (current_host && current_host->get_sn() == devices[i].sn) { - wxString msg = ""; - json param; - current_host->disconnect(msg, param); - } - } - } - wxGetApp().app_config->clear_device_info(); - } else { - for (size_t i = 0; i < ids.size(); ++i) { - std::string dev_id = ids[i].get(); - - DeviceInfo info; - if (wxGetApp().app_config->get_device_info(dev_id, info)) { - if (info.connected) { - std::shared_ptr current_host; - wxGetApp().get_connect_host(current_host); - wxString msg = ""; - json param; - current_host->disconnect(msg, param); - } - } - wxGetApp().app_config->remove_device_info(dev_id); +std::unordered_set SSWCP::m_machine_connect_cmd_list = { + "sw_Test_connect", + "sw_Connect", + "sw_Disconnect", + "sw_ConnectOtherMachine", + "sw_GetPincode", + "sw_SubscribeForegroundChange" +}; - } - } +std::unordered_set SSWCP::m_project_cmd_list = { + "sw_NewProject", "sw_OpenProject", "sw_GetRecentProjects", "sw_OpenRecentFile", "sw_DeleteRecentFiles", "sw_SubscribeRecentFiles", +}; - wxGetApp().CallAfter([] { - // wcp sub - json data = wxGetApp().app_config->get_devices(); - wxGetApp().device_card_notify(data); - }); +std::unordered_set SSWCP::m_login_cmd_list = {UPDATE_PRIVACY_STATUS, GET_PRIVACY_STATUS, + FILE_VIEW, OPEN_TIMELAPSE_FOLDER, CANCEL_DOWNLOAD, DOWNLOAD_FILE_AND_OPEN, DOWN_LOAD_FILE, SUBSCRIBE_DOWNLOAD_STATE, UNSUBSCRIBE_DOWNLOAD_STATE, NOTIFY_UPLOAD_TIMELASPE, GET_FILES_FROM_DIR}; - send_to_js(); - finish_job(); - } else { - handle_general_fail(); - } - } catch (std::exception& e) { - handle_general_fail(); - } -} +std::unordered_set SSWCP::m_page_state_cmd_list = { + "sw_SubscribePageStateChange", "sw_UnsubscribePageStateChange" +}; -void SSWCP_MachineManage_Instance::sw_SwitchModel() +std::shared_ptr SSWCP::create_sswcp_instance(std::string cmd, const json& header, const json& data, std::string event_id, wxWebView* webview) { - try { - if (m_param_data.count("dev_id")) { - std::string dev_id = m_param_data["dev_id"].get(); - send_to_js(); - wxGetApp().CallAfter([dev_id]() { - WebPresetDialog dialog(&wxGetApp()); - dialog.m_device_id = dev_id; - dialog.run(); - }); - finish_job(); - - } else { - handle_general_fail(); - } - } catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MachineManage_Instance::sw_UpdateDeviceInfo() -{ - // Expect "devices": [ { dev_id, ... }, ... ] — batch merge update - if (!m_param_data.count("devices") || !m_param_data["devices"].is_array()) { - handle_general_fail(-1, "param [devices] is required (array)"); - return; - } - - std::string new_preset_model; - std::string new_preset_nozzle; - bool any_changed = false; - - wxGetApp().CallAfter([devices_json = m_param_data["devices"], new_preset_model, new_preset_nozzle, any_changed]() mutable { - for (const auto& item : devices_json) { - if (!item.is_object()) - continue; - if (!item.count("dev_id") || !item["dev_id"].is_string()) - continue; - - std::string dev_id = item["dev_id"].get(); - - // Load existing or start fresh - DeviceInfo info; - bool exist = wxGetApp().app_config->get_device_info(dev_id, info); - if (!exist) { - info.dev_id = dev_id; - info.connected = false; - info.protocol = 0; - info.port = 0; - } - - // Snapshot before merge for diff - DeviceInfo before = info; - - // —— Merge: only overwrite fields that are present —— - - if (item.count("ip") && item["ip"].is_string()) - info.ip = item["ip"].get(); - - if (item.count("dev_name") && item["dev_name"].is_string()) - info.dev_name = item["dev_name"].get(); - - if (item.count("model_name") && item["model_name"].is_string()) { - info.model_name = item["model_name"].get(); - new_preset_model = info.model_name; - - // Machine cover image - size_t vendor_pos = info.model_name.find_first_of(" "); - if (vendor_pos != std::string::npos) { - std::string vendor = info.model_name.substr(0, vendor_pos); - info.img = LOCALHOST_URL + std::to_string(wxGetApp().m_page_http_server.get_port()) + "/profiles/" + vendor + "/" + - info.model_name + "_cover.png"; - } - } - - if (item.count("connected") && item["connected"].is_boolean()) - info.connected = item["connected"].get(); - - if (item.count("sn") && item["sn"].is_string()) - info.sn = item["sn"].get(); - - if (item.count("link_mode") && item["link_mode"].is_string()) - info.link_mode = item["link_mode"].get(); - - if (item.count("id") && item["id"].is_string()) - info.id = item["id"].get(); - - if (item.count("userid") && item["userid"].is_string()) - info.userid = item["userid"].get(); - - if (item.count("protocol") && item["protocol"].is_number()) - info.protocol = item["protocol"].get(); - - if (item.count("port") && item["port"].is_number()) - info.port = item["port"].get(); - - if (item.count("user") && item["user"].is_string()) - info.user = item["user"].get(); - - if (item.count("password") && item["password"].is_string()) - info.password = item["password"].get(); - - if (item.count("ca") && item["ca"].is_string()) - info.ca = item["ca"].get(); - - if (item.count("cert") && item["cert"].is_string()) - info.cert = item["cert"].get(); - - if (item.count("key") && item["key"].is_string()) - info.key = item["key"].get(); - - if (item.count("clientId") && item["clientId"].is_string()) - info.clientId = item["clientId"].get(); - - if (item.count("api_key") && item["api_key"].is_string()) - info.api_key = item["api_key"].get(); - - if (item.count("img") && item["img"].is_string()) - info.img = item["img"].get(); - - if (item.count("nozzle_sizes") && item["nozzle_sizes"].is_array()) { - info.nozzle_sizes.clear(); - for (const auto& n : item["nozzle_sizes"]) { - if (n.is_string()) { - std::string v = n.get(); - if (v == "0.2" || v == "0.4" || v == "0.6" || v == "0.8") - info.nozzle_sizes.push_back(v); - } else if (n.is_number()) { - double d = n.get(); - if (fabs(d - 0.2) < 1e-6) - info.nozzle_sizes.push_back("0.2"); - else if (fabs(d - 0.4) < 1e-6) - info.nozzle_sizes.push_back("0.4"); - else if (fabs(d - 0.6) < 1e-6) - info.nozzle_sizes.push_back("0.6"); - else if (fabs(d - 0.8) < 1e-6) - info.nozzle_sizes.push_back("0.8"); - } - } - if (!info.nozzle_sizes.empty()) - new_preset_nozzle = info.nozzle_sizes[0]; - } - - if (item.count("nozzle_volume_type") && item["nozzle_volume_type"].is_array()) { - info.nozzle_volume_type.clear(); - for (const auto& volume_type : item["nozzle_volume_type"]) { - if (!volume_type.is_string()) - continue; - - const std::string type = volume_type.get(); - if (type == "standard" || type == "high_flow") - info.nozzle_volume_type.push_back(type); - } - } - - if (item.count("preset_name") && item["preset_name"].is_string()) - info.preset_name = item["preset_name"].get(); - - // Build preset name if it wasn't explicitly set but we have model + nozzle - if (info.preset_name.empty() && !info.model_name.empty() && !info.nozzle_sizes.empty()) - info.preset_name = info.model_name + " (" + info.nozzle_sizes[0] + " nozzle)"; - - // —— Diff: skip save if nothing changed —— - if (exist && before.ip == info.ip && before.dev_name == info.dev_name && before.model_name == info.model_name && - before.preset_name == info.preset_name && before.connected == info.connected && before.img == info.img && - before.nozzle_sizes == info.nozzle_sizes && before.nozzle_volume_type == info.nozzle_volume_type && before.sn == info.sn && - before.protocol == info.protocol && before.api_key == info.api_key && before.user == info.user && - before.password == info.password && before.ca == info.ca && before.cert == info.cert && before.key == info.key && - before.clientId == info.clientId && before.port == info.port && before.link_mode == info.link_mode && - before.userid == info.userid && before.id == info.id) { - continue; - } - - wxGetApp().app_config->save_device_info(info); - any_changed = true; - } - - if (!any_changed) - return; - - // Update ProfileJson for any newly-seen model/nozzle combination - if (!new_preset_model.empty() && !new_preset_nozzle.empty()) { - std::lock_guard lock(m_ProfileJson_mutex); - int nModel = m_ProfileJson["model"].size(); - bool isFind = false; - for (int m = 0; m < nModel; m++) { - if (m_ProfileJson["model"][m]["model"].get() == new_preset_model) { - isFind = true; - std::string nozzle_selected = m_ProfileJson["model"][m]["nozzle_selected"].get(); - if (nozzle_selected.find(new_preset_nozzle) == std::string::npos) { - nozzle_selected += ";" + new_preset_nozzle; - m_ProfileJson["model"][m]["nozzle_selected"] = nozzle_selected; - } - break; - } - } - - if (!isFind) { - json new_item; - new_item["vendor"] = "Snapmaker"; - new_item["model"] = new_preset_model; - new_item["nozzle_selected"] = new_preset_nozzle; - new_item["nozzle_diameter"] = new_preset_nozzle; - m_ProfileJson["model"].push_back(new_item); - } - } - - // Update UI - auto devices = wxGetApp().app_config->get_devices(); - - json param; - param["command"] = "local_devices_arrived"; - param["sequece_id"] = "10001"; - param["data"] = devices; - std::string logout_cmd = param.dump(); - wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - GUI::wxGetApp().run_script(strJS); - - json data = devices; - wxGetApp().device_card_notify(data); - - wxGetApp().mainframe->plater()->sidebar().update_all_preset_comboboxes(false); - - if (SSWCP_MqttAgent_Instance::m_dialog) { - SSWCP_MqttAgent_Instance::m_dialog->SaveProfile(); - bool flag = false; - SSWCP_MqttAgent_Instance::m_dialog->apply_config(wxGetApp().app_config, wxGetApp().preset_bundle, wxGetApp().preset_updater, - flag); - wxGetApp().update_mode(); - } else { - BOOST_LOG_TRIVIAL(warning) << "sw_UpdateDeviceInfo: preset dialog not available, skip preset install"; - } - }); - - send_to_js(); - finish_job(); -} - -// SSWCP_MqttAgent_Instance - -namespace { -// Fresh UUID string for connectSessionId — the funnel-correlation key shared by -// every mqtt-agent event in one connection attempt (see SSWCP.hpp). -std::string make_connect_session_id() -{ - return boost::uuids::to_string(boost::uuids::random_generator()()); -} -} // namespace - -std::unordered_map>> SSWCP_MqttAgent_Instance::m_mqtt_engine_map; -std::mutex SSWCP_MqttAgent_Instance::m_engine_map_mtx; -std::map, std::string> SSWCP_MqttAgent_Instance::m_subscribe_map; -std::map, std::weak_ptr> SSWCP_MqttAgent_Instance::m_subscribe_instance_map; -WebPresetDialog* SSWCP_MqttAgent_Instance::m_dialog = nullptr; -std::unordered_map SSWCP_MqttAgent_Instance::m_connect_session_map; - -void SSWCP_MqttAgent_Instance::process() -{ - if (m_cmd == "sw_create_mqtt_client") { - sw_create_mqtt_client(); - } else if (m_cmd == "sw_mqtt_connect") { - sw_mqtt_connect(); - } else if (m_cmd == "sw_mqtt_disconnect") { - sw_mqtt_disconnect(); - } else if (m_cmd == "sw_mqtt_subscribe") { - sw_mqtt_subscribe(); - } else if (m_cmd == "sw_mqtt_unsubscribe") { - sw_mqtt_unsubscribe(); - } else if (m_cmd == "sw_mqtt_publish") { - sw_mqtt_publish(); - } else if (m_cmd == "sw_mqtt_set_engine") { - sw_mqtt_set_engine(); - } else { - handle_general_fail(); - } -} - -// check mqtt id -bool SSWCP_MqttAgent_Instance::validate_id(const std::string& id) -{ - bool flag = true; - - m_engine_map_mtx.lock(); - if (!m_mqtt_engine_map.count(m_webview)) { - flag = false; - } else { - flag = m_mqtt_engine_map[m_webview].first == id; - } - - m_engine_map_mtx.unlock(); - - return flag; -} - -// webview illegal call back -void SSWCP_MqttAgent_Instance::set_Instance_illegal() -{ - SSWCP_Instance::set_Instance_illegal(); - - clean_current_engine(); -} - -// detele mqtt instance -void SSWCP_MqttAgent_Instance::clean_current_engine() -{ - - for (auto iter = m_subscribe_map.begin(); iter != m_subscribe_map.end();) { - if (iter->first.second == m_webview) { - iter = m_subscribe_map.erase(iter); - } else { - iter++; - } - } - - for (auto iter = m_subscribe_instance_map.begin(); iter != m_subscribe_instance_map.end();) { - if (iter->first.second == m_webview) { - iter = m_subscribe_instance_map.erase(iter); - } else { - iter++; - } - } - - m_engine_map_mtx.lock(); - m_mqtt_engine_map.erase(m_webview); - m_engine_map_mtx.unlock(); -} - -// mqtt static msg callback -void SSWCP_MqttAgent_Instance::mqtt_msg_cb(const std::string& topic, const std::string& payload, void* client) -{ - auto& wcp_loger = GUI::WCP_Logger::getInstance(); - { - wxGetApp().CallAfter([topic, payload, client]() { - for (const auto& item : SSWCP_MqttAgent_Instance::m_subscribe_map) { - - std::string id_topic = item.second; - std::string target_topic = topic; - if (id_topic.find("+") != std::string::npos) { - id_topic = id_topic.substr(id_topic.find_first_of("/")); - target_topic = topic.substr(topic.find_first_of("/")); - - } - - if (id_topic == target_topic) { - if (SSWCP_MqttAgent_Instance::m_subscribe_instance_map.count(item.first)) { - auto& instance = SSWCP_MqttAgent_Instance::m_subscribe_instance_map[item.first]; - if (auto self = instance.lock()) { - auto mqtt_self = dynamic_pointer_cast(self); - - if (mqtt_self && (void*)(mqtt_self->get_current_engine().get()) == client) { - self->m_res_data["topic"] = topic; - self->m_res_data["data"] = payload; - self->send_to_js(); - } - } - - } else { - return; - } - } - } - }); - - } -} - -void SSWCP_MqttAgent_Instance::sw_create_mqtt_client() -{ - try { - // Assign the funnel id for this connection attempt; all subsequent - // mqtt-agent events (connect/subscribe/set_engine/disconnect) reuse it. - set_connect_session_id(make_connect_session_id()); - const std::string session_id = get_connect_session_id(); - - // A new connect attempt starts here. Reset the per-device SnapLog identity so - // this session's preamble events (create/connect/subscribe, which fire BEFORE - // sw_mqtt_set_engine refreshes them) don't inherit the PREVIOUS device's values. - // Without this, switching devices without a disconnect in between mis-tags the - // whole connect funnel with the prior printerSN/connect_clientid. - // print_sn stays empty here (sn is only known to sw_mqtt_set_engine); connect_clientid - // is re-filled just below, right after clientId is validated. - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_print_sn(""); - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_connect_clientid(""); - - // Parse connection parameters. - std::string server_address = ""; - std::string clientId = ""; - std::string ca = ""; - std::string cert = ""; - std::string key = ""; - std::string username = ""; - std::string password = ""; - bool clean_session = false; - - if (m_param_data.count("server_address") || !m_param_data["server_address"].is_string()) { - server_address = m_param_data["server_address"].get(); - if (server_address == "") { - SNAP_LOG_BATCH(Error, "mqtt client create failed", - {"eventName", "mqtt_client_create_failed"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"reason", "server_address illegal"}); - handle_general_fail(-1, "the value of param [server_address] is illegal"); - return; - } - } - else { - SNAP_LOG_BATCH(Error, "mqtt client create failed", - {"eventName", "mqtt_client_create_failed"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"reason", "server_address missing"}); - handle_general_fail(-1, "param [server_address] is required or wrong type"); - return; - } - - if (m_param_data.count("clientId") || !m_param_data["clientId"].is_string()) { - clientId = m_param_data["clientId"].get(); - if (clientId == "") { - SNAP_LOG_BATCH(Error, "mqtt client create failed", - {"eventName", "mqtt_client_create_failed"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"reason", "clientId illegal"}); - handle_general_fail(-1, "the value of param [clientId] is illegal"); - return; - } - } else { - SNAP_LOG_BATCH(Error, "mqtt client create failed", - {"eventName", "mqtt_client_create_failed"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"reason", "clientId missing"}); - handle_general_fail(-1, "param [clientId] is required or wroing type"); - return; - } - - // clientId is validated and non-empty here — mirror it into SnapLog now so this - // session's connect-stage events carry the correct connect_clientid (matches what - // sw_mqtt_set_engine sets later at SSWCP.cpp:5847), instead of the cleared value above. - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_connect_clientid(clientId); - - ca = m_param_data.count("ca") ? m_param_data["ca"].get() : ""; - cert = m_param_data.count("cert") ? m_param_data["cert"].get() : ""; - key = m_param_data.count("key") ? m_param_data["key"].get() : ""; - - clean_session = m_param_data.count("clean_session") ? m_param_data["clean_session"].get() : false; - - username = m_param_data.count("username") ? m_param_data["username"].get() : ""; - password = m_param_data.count("password") ? m_param_data["password"].get() : ""; - - - std::shared_ptr client = nullptr; - std::string type = "mqtt"; - if (ca != "" && cert != "" && key != "") { - type = "mqtts"; - client = MqttClient::create(server_address, clientId, ca, cert, key, username, password, clean_session); - }else{ - client = MqttClient::create(server_address, clientId, username, password, clean_session); - } - - if (client == nullptr) { - // Report client creation failure before returning. - SNAP_LOG_BATCH(Error, "mqtt client create failed", - {"eventName", "mqtt_client_create_failed"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"reason", "create instance failed"}); - handle_general_fail(-1, "create instance failed"); - return; - } - - // clear list for current sub machine - auto ptr = get_current_engine(); - if (!ptr) { - ptr.reset(); - } - clean_current_engine(); - - // - bool flag = set_current_engine({std::to_string(int64_t(client.get())), client}); - if (!flag) { - SNAP_LOG_BATCH(Error, "mqtt client create failed", - {"eventName", "mqtt_client_create_failed"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"reason", "set_current_engine failed"}); - handle_general_fail(-1, "create failed"); - return; - } - - client->SetMessageCallback(SSWCP_MqttAgent_Instance::mqtt_msg_cb); - - m_res_data["type"] = type; - m_res_data["id"] = std::to_string(int64_t(get_current_engine().get())); - - SNAP_LOG_BATCH(Info, "mqtt client created", - {"eventName", "mqtt_client_create"}, {"source", "cpp"}, - {"connectSessionId", session_id}, - {"transport", type}, {"useTls", type == "mqtts" ? "true" : "false"}, - {"server", server_address}); - - send_to_js(); - finish_job(); - } catch (std::exception& e) { - handle_general_fail(); - } -} - -// mqtt connect -void SSWCP_MqttAgent_Instance::sw_mqtt_connect() -{ - try { - SNAP_LOG_BATCH(Info, "mqtt connect attempt", - {"eventName", "mqtt_connect_attempt"}, {"source", "cpp"}, - {"connectSessionId", get_connect_session_id()}); - if (!m_param_data.count("id") || !m_param_data["id"].is_string()) { - handle_general_fail(-1, "param [id] is required or wrong type"); - - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_connect param [id] is required or wrong type"), DEVICE_CONNECT_ERR); - return; - } - - std::string id = m_param_data["id"].get(); - - if (!validate_id(id)) { - handle_general_fail(-1, "id is illegal"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_connect id is illegal"),DEVICE_CONNECT_ERR); - return; - } - - std::weak_ptr weak_ptr = shared_from_this(); - auto engine = get_current_engine(); - - if (m_work_thread.joinable()) - m_work_thread.join(); - - m_work_thread = std::thread([weak_ptr, engine]() { - if (!weak_ptr.lock()) { - return; - } - auto self = std::dynamic_pointer_cast(weak_ptr.lock()); - const std::string session_id = self ? self->get_connect_session_id() : std::string{}; - - // Capture the engine WEAKLY: this callback is stored inside the - // engine itself (MqttClient::connection_failure_callback_), so a - // shared_ptr capture would keep its refcount >= 1 forever and - // ~MqttClient — the only place the callback gets cleared — would - // never run, leaking the client and its Paho handles. - std::weak_ptr weak_engine = engine; - engine->SetConnectionFailureCallback([weak_engine, session_id]() { - auto engine = weak_engine.lock(); - if (!engine) { - return; - } - SNAP_LOG_BATCH(Error, "mqtt connection failure callback", - {"eventName", "mqtt_connect_failure"}, {"source", "cpp"}, - {"connectSessionId", session_id}); - std::string msg = ""; - engine->Disconnect(msg); - }); - - std::string msg; - bool flag = engine->Connect(msg); - - wxGetApp().CallAfter([weak_ptr, msg, flag, session_id]() { - auto self = weak_ptr.lock(); - if (self) { - if (flag) { - SNAP_LOG_BATCH(Info, "mqtt connect result", - {"eventName", "mqtt_connect_result"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"success", "true"}); - self->m_msg = msg; - self->send_to_js(); - self->finish_job(); - } else { - SNAP_LOG_BATCH(Error, "mqtt connect result", - {"eventName", "mqtt_connect_result"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"success", "false"}, - {"reason", msg}); - self->handle_general_fail(-1, msg); - } - } - }); - }); - - - } catch (std::exception& e) { - handle_general_fail(); - } -} - - -void SSWCP_MqttAgent_Instance::sw_mqtt_disconnect() -{ - try { - if (!m_param_data.count("id") || !m_param_data["id"].is_string()) { - handle_general_fail(-1, "param [id] is required or wrong type"); - return; - } - - std::string id = m_param_data["id"].get(); - - if (!validate_id(id)) { - handle_general_fail(-1, "id is illegal"); - return; - } - - std::weak_ptr weak_ptr = shared_from_this(); - auto engine = get_current_engine(); - - if (m_work_thread.joinable()) - m_work_thread.join(); - - m_work_thread = std::thread([weak_ptr, engine]() { - if (!weak_ptr.lock()) { - return; - } - auto self = std::dynamic_pointer_cast(weak_ptr.lock()); - const std::string session_id = self ? self->get_connect_session_id() : std::string{}; - - std::string msg = "success"; - bool flag = engine->Disconnect(msg); - if (flag && self) { - // Connection gone — drop the funnel id so later events don't reuse it. - self->clear_connect_session_id(); - } - - wxGetApp().CallAfter([weak_ptr, msg, flag, session_id]() { - auto self = weak_ptr.lock(); - if (self) { - if (flag) { - SNAP_LOG_BATCH(Info, "device disconnect", - {"eventName", "device_disconnect"}, {"source", "cpp"}, - {"connectSessionId", session_id}); - // Printer disconnected — clear Flutter MQTT identity in SnapLog. - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_connect_clientid(""); - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_print_sn(""); - self->m_msg = msg; - self->send_to_js(); - self->finish_job(); - } else { - self->handle_general_fail(-1, msg); - } - } - }); - }); - } - catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MqttAgent_Instance::sw_mqtt_subscribe() -{ - try { - if (!m_param_data.count("id") || !m_param_data["id"].is_string()) { - handle_general_fail(-1, "param [id] is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_subscribe param [id] is required or wrong type"), DEVICE_SUBSCRIBE_ERR); - return; - } - - std::string id = m_param_data["id"].get(); - - if (!validate_id(id)) { - handle_general_fail(-1, "id is illegal"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_subscribe id is illegal with:")+id,DEVICE_SUBSCRIBE_ERR); - return; - } - - if (m_event_id == "") { - handle_general_fail(-1, "event_id is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_subscribe event_id is required or wrong type with:") + id,DEVICE_SUBSCRIBE_ERR); - return; - } - - std::string event_id = m_event_id; - - if (!m_param_data.count("topic") || !m_param_data["topic"].is_string()) { - handle_general_fail(-1, "param [topic] is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_subscribe param [topic] is required or wrong type"),DEVICE_SUBSCRIBE_ERR); - return; - } - std::string topic = m_param_data["topic"].get(); - - - m_subscribe_map[{event_id, m_webview}] = topic; - m_subscribe_instance_map[{event_id, m_webview}] = shared_from_this(); - - if (!m_param_data.count("qos") || !m_param_data["qos"].is_number()) { - handle_general_fail(-1, "param [qos] is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_subscribe param [qos] is required or wrong type"), DEVICE_SUBSCRIBE_ERR); - return; - } - int qos = m_param_data["qos"].get(); - - std::weak_ptr weak_ptr = shared_from_this(); - auto engine = get_current_engine(); - - if (m_work_thread.joinable()) - m_work_thread.join(); - - m_work_thread = std::thread([weak_ptr, engine, topic, qos]() { - if (!weak_ptr.lock()) { - return; - } - auto self = std::dynamic_pointer_cast(weak_ptr.lock()); - const std::string session_id = self ? self->get_connect_session_id() : std::string{}; - - std::string msg = "success"; - bool flag = engine->Subscribe(topic, qos, msg); - - wxGetApp().CallAfter([weak_ptr, msg, flag, topic, qos, session_id]() { - auto self = weak_ptr.lock(); - if (self) { - if (flag) { - SNAP_LOG_BATCH(Info, "mqtt subscribe result", - {"eventName", "mqtt_subscribe_result"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"success", "true"}, - {"topic", topic}, {"qos", std::to_string(qos)}); - // response set event_id - if (self->m_event_id != "") { - self->m_msg = msg; - self->send_to_js(); - - json header; - self->m_header.clear(); - self->m_header["event_id"] = self->m_event_id; - } else { - self->handle_general_fail(-1, "event_id is null"); - } - - } else { - SNAP_LOG_BATCH(Warning, "mqtt subscribe result", - {"eventName", "mqtt_subscribe_result"}, {"source", "cpp"}, - {"connectSessionId", session_id}, {"success", "false"}, - {"topic", topic}, {"qos", std::to_string(qos)}, - {"reason", msg}); - self->handle_general_fail(-1, msg); - } - } - }); - }); - } - catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MqttAgent_Instance::sw_mqtt_unsubscribe() { - try { - if (!m_param_data.count("id") || !m_param_data["id"].is_string()) { - handle_general_fail(-1, "param [id] is required or wrong type"); - return; - } - - std::string id = m_param_data["id"].get(); - - if (!validate_id(id)) { - handle_general_fail(-1, "id is illegal"); - return; - } - - if (!m_param_data.count("topic") || !m_param_data["topic"].is_string()) { - handle_general_fail(-1, "param [topic] is required or wrong type"); - return; - } - std::string topic = m_param_data["topic"].get(); - - for (auto iter = m_subscribe_map.begin(); - iter != m_subscribe_map.end(); ) { - if (iter->second == topic) { - if (m_subscribe_instance_map.count(iter->first)) { - m_subscribe_instance_map.erase(iter->first); - } - iter = m_subscribe_map.erase(iter); - } else { - ++iter; - } - } - - std::weak_ptr weak_ptr = shared_from_this(); - auto engine = get_current_engine(); - - if (m_work_thread.joinable()) - m_work_thread.join(); - - m_work_thread = std::thread([weak_ptr, engine, topic]() { - if (!weak_ptr.lock()) { - return; - } - auto self = std::dynamic_pointer_cast(weak_ptr.lock()); - - std::string msg = "success"; - bool flag = engine->Unsubscribe(topic, msg); - - wxGetApp().CallAfter([weak_ptr, msg, flag]() { - auto self = weak_ptr.lock(); - if (self) { - if (flag) { - self->m_msg = msg; - self->send_to_js(); - self->finish_job(); - } else { - self->handle_general_fail(-1, msg); - } - } - }); - }); - } catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MqttAgent_Instance::sw_mqtt_set_engine() -{ - try { - if (!m_param_data.count("engine_id") || !m_param_data["engine_id"].is_string()) { - handle_general_fail(-1, "param [engine_id] is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_set_engine param [engine_id] is required or wrong type"),DEVICE_SET_ENGINE_ERR); - return; - } - - std::string engine_id = m_param_data["engine_id"].get(); - - if (!validate_id(engine_id)) { - handle_general_fail(-1, "id is illegal"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_set_engine id is illegal with:") + engine_id, DEVICE_SET_ENGINE_ERR); - return; - } - - if (!m_param_data.count("ip") || !m_param_data["ip"].is_string()) { - handle_general_fail(-1, "param [ip] is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_set_engine param [ip] is required or wrong type"),DEVICE_SET_ENGINE_ERR); - return; - } - std::string ip = m_param_data["ip"].get(); - - if (!m_param_data.count("port") || !m_param_data["port"].is_number()) { - handle_general_fail(-1, "param [port] is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_set_engine param [port] is required or wrong type"), DEVICE_SET_ENGINE_ERR); - return; - } - - bool reload_device_view = m_param_data.count("need_reload") ? m_param_data["need_reload"].get() : true; - - int port = m_param_data["port"].get(); - - auto config = wxGetApp().preset_bundle->printers.get_edited_preset().config; - - PrintHostType type = PrintHostType::htMoonRaker_mqtt; - // todo : add cin-type - - config.option>("host_type")->value = type; - - config.set("print_host", ip + (port == -1 ? "" : ":" + std::to_string(port))); - - std::shared_ptr tmp_host(PrintHost::get_print_host(&config)); - wxGetApp().set_connect_host(tmp_host); - wxGetApp().set_host_config(config); - - // Device switch: every in-progress timelapse download is now stale — - // stop them all and mark each unfinished file as "Download Failed". - cancel_all_active_timelapse(); - - std::shared_ptr host = dynamic_pointer_cast(tmp_host); - if (host) { - auto engine = get_current_engine(); - - if (engine == nullptr) { - handle_general_fail(-1, "invalid engine"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_set_engine invalid engine"),DEVICE_SET_ENGINE_ERR); - return; - } - - if (!engine->CheckConnected()) { - BOOST_LOG_TRIVIAL(error) << "[SSWCP_MqttAgent_Instance] connect error"; - handle_general_fail(-1, "engine connection lost"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_set_engine engine connection lost"), DEVICE_SET_ENGINE_ERR); - return; - } - - std::string msg = "success"; - bool flag = host->set_engine(engine, msg); - - if (flag) - { - if (m_param_data.count("ip")) { - std::string ip = m_param_data["ip"].get(); - - int port = -1; - if (m_param_data.count("port") && m_param_data["port"].is_number_integer()) { - port = m_param_data["port"].get(); - } - - // test - if (port == -1 || port == 1883) { - port = 1884; - } - - json connect_params; - if (m_param_data.count("sn") && m_param_data["sn"].is_string()) { - connect_params["sn"] = m_param_data["sn"].get(); - - host->m_sn_mtx.lock(); - host->m_sn = m_param_data["sn"].get(); - host->m_sn_mtx.unlock(); - // Mirror-push printer SN into SnapLog (emitted in ext as print_sn). - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_print_sn(m_param_data["sn"].get()); - } else { - handle_general_fail(-1, "param [sn] is required or wrong type"); - return; - } - - if (m_param_data.count("code")){ - connect_params["code"] = m_param_data["code"]; - } - - - if (m_param_data.count("ca")) { - connect_params["ca"] = m_param_data["ca"]; - host->m_ca = m_param_data["ca"].get(); - } - - - if (m_param_data.count("cert")) { - connect_params["cert"] = m_param_data["cert"]; - host->m_cert = m_param_data["cert"].get(); - } - - - if (m_param_data.count("key")) { - connect_params["key"] = m_param_data["key"]; - host->m_key = m_param_data["key"].get(); - } - - if (m_param_data.count("user")) { - connect_params["user"] = m_param_data["user"]; - host->m_user_name = m_param_data["user"].get(); - } - - if (m_param_data.count("password")) { - connect_params["password"] = m_param_data["password"]; - host->m_password = m_param_data["password"].get(); - } - - if (m_param_data.count("port")) { - connect_params["port"] = m_param_data["port"]; - host->m_port = m_param_data["port"].get(); - } - - if (m_param_data.count("clientId")) { - connect_params["clientId"] = m_param_data["clientId"]; - host->m_client_id = m_param_data["clientId"].get(); - // Mirror-push MQTT clientId into SnapLog (emitted in ext as connect_clientid). - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_connect_clientid(host->m_client_id); - } - - - std::string link_mode = m_param_data.count("link_mode") ? m_param_data["link_mode"] : "lan"; - connect_params["link_mode"] = link_mode; - - SNAP_LOG_BATCH(Info, "device engine set", - {"eventName", "device_engine_set"}, {"source", "cpp"}, - {"connectSessionId", get_connect_session_id()}, - {"printerSN", host->m_sn}, - {"clientId", host->m_client_id}, - {"linkMode", link_mode}); - - std::string id = m_param_data.count("id") ? m_param_data["id"].get() : ""; - std::string userid = m_param_data.count("userid") ? m_param_data["userid"].get() : ""; - - if (!host) { - handle_general_fail(-1, "host created failed"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_set_engine host created failed"), DEVICE_SET_ENGINE_ERR); - return; - } else { - auto weak_self = std::weak_ptr(shared_from_this()); - - if (m_work_thread.joinable()) - m_work_thread.join(); - - m_work_thread = std::thread([weak_self, host, connect_params, link_mode, id, userid, reload_device_view] { - auto self = weak_self.lock(); - wxString msg = ""; - json params; - host->set_connection_lost([]() { - wxGetApp().CallAfter([]() { - SSWCP_Instance::m_first_connected = true; - wxGetApp().app_config->clear_filament_extruder_map(); - wxGetApp().preset_bundle->machine_filaments.clear(); - wxGetApp().load_current_presets(); - }); - wxGetApp().CallAfter([]() { - wxGetApp().app_config->set("use_new_connect", "false"); - auto p_config = &(wxGetApp().preset_bundle->printers.get_edited_preset().config); - p_config->set("print_host", ""); - - std::shared_ptr ptr = nullptr; - wxGetApp().get_connect_host(ptr); - if (ptr) { - wxString disconn_msg = ""; - json disconn_param; - ptr->disconnect(disconn_msg, disconn_param); - } - - wxGetApp().set_connect_host(nullptr); - - auto devices = wxGetApp().app_config->get_devices(); - for (size_t i = 0; i < devices.size(); ++i) { - if (devices[i].connected) { - devices[i].connected = false; - wxGetApp().app_config->save_device_info(devices[i]); - break; - } - } - - // update card - json param; - param["command"] = "local_devices_arrived"; - param["sequece_id"] = "10001"; - param["data"] = devices; - std::string logout_cmd = param.dump(); - wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - GUI::wxGetApp().run_script(strJS); - - // wcp sub - json data = devices; - wxGetApp().device_card_notify(data); - - MessageDialog msg_window(nullptr, " " + _L("Connection has been disconnected and recovery attempt failed. Please reconnect.") + "\n", _L("Machine Disconnected"), - wxICON_QUESTION | wxOK); - msg_window.ShowModal(); - - wxGetApp().set_connect_host(nullptr); - - wxGetApp().mainframe->plater()->sidebar().update_all_preset_comboboxes(); - }); - }); - bool res = true; - - std::string ip_port = host->get_host(); - std::string ip; - { - int pos = ip_port.find(':'); - ip = (pos != std::string::npos) ? ip_port.substr(0, pos) : ip_port; - } - - // Defer all AppConfig writes to the UI thread to avoid data races - // with sw_UpdateDeviceInfo and other MachineManage handlers that - // also read/write DeviceInfo on the UI thread via CallAfter. - wxGetApp().CallAfter([weak_self, reload_device_view, ip, host, connect_params, link_mode, id, userid]() { - // Mark other devices as disconnected - auto devices = wxGetApp().app_config->get_devices(); - for (size_t i = 0; i < devices.size(); ++i) { - if (devices[i].connected) { - devices[i].connected = false; - wxGetApp().app_config->save_device_info(devices[i]); - break; - } - } - - // Save a minimal DeviceInfo from the connection params. - // Full device details (machine_type, nozzle_sizes, device_name) - // are pushed later by Flutter via sw_UpdateDeviceInfo. - std::string dev_id = connect_params.count("sn") ? connect_params["sn"].get() : ip; - { - DeviceInfo info; - bool exist = wxGetApp().app_config->get_device_info(dev_id, info); - if (!exist) { - info.dev_id = dev_id; - info.connected = false; - info.protocol = 0; - info.port = 0; - } - info.ip = ip; - info.connected = true; - info.link_mode = link_mode; - info.protocol = int(PrintHostType::htMoonRaker_mqtt); - info.id = id; - info.userid = userid; - info.dev_name = ip; // fallback device name - if (connect_params.count("sn") && connect_params["sn"].is_string()) { - info.sn = connect_params["sn"].get(); - info.dev_name = info.sn != "" ? info.sn : ip; - info.dev_id = info.sn != "" ? info.sn : info.dev_id; - } - // Carry over auth info from host - auto auth_info = host->get_auth_info(); - info.ca = ""; - info.cert = ""; - info.key = ""; - info.user = auth_info["user"]; - info.password = auth_info["password"]; - info.port = auth_info["port"]; - info.clientId = auth_info["clientId"]; - wxGetApp().app_config->save_device_info(info); - } - - devices = wxGetApp().app_config->get_devices(); - - json param; - param["command"] = "local_devices_arrived"; - param["sequece_id"] = "10001"; - param["data"] = devices; - std::string logout_cmd = param.dump(); - wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - GUI::wxGetApp().run_script(strJS); - - // wcp sub - json data = devices; - wxGetApp().device_card_notify(data); - - /*MessageDialog msg_window(nullptr, ip + " " + _L("connected sucessfully !") + "\n", _L("Machine - Connected"), wxICON_QUESTION | wxOK); msg_window.ShowModal();*/ - - auto dialog = wxGetApp().get_web_device_dialog(); - if (dialog) { - dialog->EndModal(1); - } - - wxGetApp().app_config->set("use_new_connect", "true"); - wxGetApp().mainframe->plater()->sidebar().update_all_preset_comboboxes(reload_device_view); - wxGetApp().mainframe->m_print_enable = true; - wxGetApp().mainframe->update_slice_print_status(MainFrame::eEventPlateUpdate); - - if (!wxGetApp().mainframe->m_printer_view->isSnapmakerPage()) { - wxString url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + - "/web/flutter_web/index.html?path=2"); - auto real_url = wxGetApp().get_international_url(url); - wxGetApp().mainframe->load_printer_url(real_url); - } else { - if (reload_device_view) { - wxString url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + - "/web/flutter_web/index.html?path=2"); - auto real_url = wxGetApp().get_international_url(url); - - wxGetApp().mainframe->load_printer_url(real_url); - } - - } - - auto self = weak_self.lock(); - if (!self) { - return; - } - - wxGetApp().app_config->clear_filament_extruder_map(); - - if (self->m_wcp_cache.count("deviceFilamentInfo")) { - try { - // Flutter writers encode this cache value inconsistently: - // one path jsonEncodes the payload once, another encodes it - // twice. Unwrap the extra string layer instead of assuming - // an object with a "value" key (operator[] would throw). - json value; - std::string value_str; - if (self->m_wcp_cache["deviceFilamentInfo"].is_string()) { - value_str = self->m_wcp_cache["deviceFilamentInfo"].get(); - value = json::parse(value_str); - if (value.is_string()) - value = json::parse(value.get()); - } else { - value = self->m_wcp_cache["deviceFilamentInfo"]; - } - - if (value.is_object() && value.contains("value") && value["value"].is_object()) { - json value_item = value["value"]; - auto machines = wxGetApp().app_config->get_devices(); - bool find = false; - for (auto& [key, value] : value_item.items()) { - if (find) { - break; - } - - for (const auto& machine : machines) { - if (machine.sn == key && machine.connected) { - find = true; - json target = json::array(); - json object = json::object(); - object["key"] = key; - object["value"] = value.dump(); - target.push_back(object); - self->update_filament_info(target, false); - break; - } - } - - } - } else { - BOOST_LOG_TRIVIAL(warning) << "[WCP] deviceFilamentInfo cache has unexpected format, skip. type=" << value.type_name() - << " raw=" << (value_str.empty() ? self->m_wcp_cache["deviceFilamentInfo"].dump() : value_str); - } - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "[WCP] deviceFilamentInfo cache parse failed: " << e.what(); - } - } - - auto mqtt_self = dynamic_pointer_cast(self); - mqtt_self->clean_current_engine(); - self->send_to_js(); - self->finish_job(); - }); - - }); - } - - } else { - handle_general_fail(-1, "param [ip] required"); - } - } else { - handle_general_fail(); - } - } else { - handle_general_fail(); - } - - } - catch (std::exception& e) { - handle_general_fail(); - } -} - -void SSWCP_MqttAgent_Instance::sw_mqtt_publish() -{ - try { - if (!m_param_data.count("id") || !m_param_data["id"].is_string()) { - handle_general_fail(-1, "param [id] is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_publish host created failed"), DEVICE_PBLISH_ERR); - return; - } - - std::string id = m_param_data["id"].get(); - - if (!validate_id(id)) { - handle_general_fail(-1, "id is illegal"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_publish id is illegal"), DEVICE_PBLISH_ERR); - return; - } - - if (!m_param_data.count("topic") || !m_param_data["topic"].is_string()) { - handle_general_fail(-1, "param [topic] is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_publish param [topic] is required or wrong type"), DEVICE_PBLISH_ERR); - return; - } - std::string topic = m_param_data["topic"].get(); - - if (!m_param_data.count("qos") || !m_param_data["qos"].is_number()) { - handle_general_fail(-1, "param [qos] is required or wrong type"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_publish param [qos] is required or wrong type"), DEVICE_PBLISH_ERR); - return; - } - int qos = m_param_data["qos"].get(); - - if (!m_param_data.count("payload") || !m_param_data["payload"].is_string()) { - handle_general_fail(-1, "param [payload] required"); - Slic3r::sentryReportLog(Slic3r::SENTRY_LOG_ERROR, std::string("device_publish param [payload] required"), DEVICE_PBLISH_ERR); - return; - } - std::string payload = m_param_data["payload"].get(); - - - std::weak_ptr weak_ptr = shared_from_this(); - auto engine = get_current_engine(); - - if (m_work_thread.joinable()) - m_work_thread.join(); - - m_work_thread = std::thread([weak_ptr, engine, topic, payload, qos]() { - if (!weak_ptr.lock()) { - return; - } - auto self = std::dynamic_pointer_cast(weak_ptr.lock()); - - std::string msg = "success"; - bool flag = engine->Publish(topic, payload, qos, msg); - - wxGetApp().CallAfter([weak_ptr, msg, flag]() { - auto self = weak_ptr.lock(); - if (self) { - if (flag) { - self->m_msg = msg; - self->send_to_js(); - self->finish_job(); - } else { - self->handle_general_fail(-1, msg); - } - } - }); - }); - } catch (std::exception& e) { - handle_general_fail(); - } -} - - - -// SSWCP -TimeoutMap> SSWCP::m_instance_list; -constexpr std::chrono::milliseconds SSWCP::DEFAULT_INSTANCE_TIMEOUT; - -std::string SSWCP::m_active_gcode_filename = ""; -std::string SSWCP::m_display_gcode_filename = ""; -long long SSWCP::m_active_file_size = 0; - -std::unordered_map> - SSWCP_UserLogin_Instance::m_subscribe_map; -std::mutex SSWCP::m_file_size_mutex; - -// WebSocket Debug Server static members -std::unique_ptr SSWCP::m_debug_server = nullptr; -std::mutex SSWCP::m_debug_server_mutex; -bool SSWCP::m_debug_mode_enabled = false; - -std::unordered_map SSWCP::m_tab_map = { - {"Home", MainFrame::TabPosition::tpHome}, - {"3DEditor", MainFrame::TabPosition::tp3DEditor}, - {"Preview", MainFrame::TabPosition::tpPreview}, - {"Monitor", MainFrame::TabPosition::tpMonitor}, - {"MultiDevice", MainFrame::TabPosition::tpMultiDevice}, - {"Project", MainFrame::TabPosition::tpProject}, - {"Calibration", MainFrame::TabPosition::tpCalibration}, - {"Auxiliary", MainFrame::TabPosition::tpAuxiliary}, - {"DebugTool", MainFrame::TabPosition::toDebugTool} -}; - -std::unordered_set SSWCP::m_machine_find_cmd_list = { - "sw_GetMachineFindSupportInfo", - "sw_StartMachineFind", - "sw_StopMachineFind", - "sw_WakeupFind", -}; - -std::unordered_set SSWCP::m_machine_option_cmd_list = { - "system.get_device_info", - "sw_SendGCodes", - "sw_FileGetStatus", - "sw_SystemGetDeviceInfo", - "sw_GetMachineState", - "sw_SubscribeMachineState", - "sw_GetMachineObjects", - "sw_SetSubscribeFilter", - "sw_StopMachineStateSubscription", - "sw_GetPrinterInfo", - "sw_MachinePrintStart", - "sw_MachinePrintPause", - "sw_MachinePrintResume", - "sw_MachinePrintCancel", - "sw_GetMachineSystemInfo", - "sw_MachineFilesRoots", - "sw_MachineFilesMetadata", - "sw_MachineFilesThumbnails", - "sw_MachineFilesGetDirectory", - "sw_CameraStartMonitor", - "sw_CameraStopMonitor", - "sw_DeleteMachineFile", - "sw_GetFileFilamentMapping", - "sw_SetFilamentMappingComplete", - "sw_FinishFilamentMapping", - "sw_DownloadMachineFile", - "sw_UploadFiletoMachine", - "sw_GetPrintLegal", - "sw_GetPrintZip", - "sw_FinishPreprint", - "sw_PullCloudFile", - "sw_CancelPullCloudFile", - "sw_StartCloudPrint", - "sw_StartLocalPrint", - "sw_MachineHeartbeat", - "sw_SetDeviceName", - "sw_ControlLed", - "sw_ControlPrintSpeed", - "sw_BedMesh_AbortProbeMesh", - "sw_ControlPurifier", - "sw_ControlMainFan", - "sw_ControlGenericFan", - "sw_ControlBedTemp", - "sw_ControlExtruderTemp", - "sw_FilesThumbnailsBase64", - "sw_exception_query", - "sw_GetFileListPage", - "sw_UpdateMachineFilamentInfo", - "sw_UploadCameraTimelapse", - "sw_UploadAsyncTimelapseInstance", - "sw_DeleteCameraTimelapse", - "sw_GetCameraTimelapseInstance", - "sw_ServerClientManagerSetUserinfo", - "sw_DefectDetactionConfig", - "sw_PrinterDefectDetection", - GET_DEVICEDATA_STORAGESPACE -}; - -std::unordered_set SSWCP::m_machine_connect_cmd_list = { - "sw_Test_connect", - "sw_Connect", - "sw_Disconnect", - "sw_GetConnectedMachine", - "sw_ConnectOtherMachine", - "sw_GetPincode", - "sw_SubscribeForegroundChange" -}; - -std::unordered_set SSWCP::m_project_cmd_list = { - "sw_NewProject", "sw_OpenProject", "sw_GetRecentProjects", "sw_OpenRecentFile", "sw_DeleteRecentFiles", "sw_SubscribeRecentFiles", -}; - -std::unordered_set SSWCP::m_login_cmd_list = {"sw_UserLogin", "sw_AskUserLogin", "sw_UserLogout", "sw_GetUserLoginState", "sw_SubscribeUserLoginState", - UPDATE_PRIVACY_STATUS, GET_PRIVACY_STATUS, - FILE_VIEW, OPEN_TIMELAPSE_FOLDER, CANCEL_DOWNLOAD, DOWNLOAD_FILE_AND_OPEN, DOWN_LOAD_FILE, SUBSCRIBE_DOWNLOAD_STATE, UNSUBSCRIBE_DOWNLOAD_STATE, NOTIFY_UPLOAD_TIMELASPE, GET_FILES_FROM_DIR}; - -std::unordered_set SSWCP::m_machine_manage_cmd_list = { - "sw_GetLocalDevices", "sw_AddDevice", "sw_SubscribeLocalDevices", "sw_RenameDevice", "sw_SwitchModel", "sw_DeleteDevices", - "sw_UpdateDeviceInfo" -}; - -std::unordered_set SSWCP::m_page_state_cmd_list = { - "sw_SubscribePageStateChange", "sw_UnsubscribePageStateChange" -}; - -std::unordered_set SSWCP::m_mqtt_agent_cmd_list = { - "sw_create_mqtt_client", "sw_mqtt_connect", "sw_mqtt_disconnect", "sw_mqtt_subscribe", "sw_mqtt_unpublish", "sw_mqtt_publish", "sw_mqtt_set_engine" -}; - -std::shared_ptr SSWCP::create_sswcp_instance(std::string cmd, const json& header, const json& data, std::string event_id, wxWebView* webview) -{ - std::shared_ptr instance; + std::shared_ptr instance; if (m_machine_find_cmd_list.find(cmd) != m_machine_find_cmd_list.end()) { instance = std::make_shared(cmd, header, data, event_id, webview); @@ -7893,12 +5658,8 @@ std::shared_ptr SSWCP::create_sswcp_instance(std::string cmd, co instance = std::make_shared(cmd, header, data, event_id, webview); } else if (m_login_cmd_list.find(cmd) != m_login_cmd_list.end()) { instance = std::make_shared(cmd, header, data, event_id, webview); - } else if (m_machine_manage_cmd_list.find(cmd) != m_machine_manage_cmd_list.end()) { - instance = std::make_shared(cmd, header, data, event_id, webview); } else if (m_page_state_cmd_list.find(cmd) != m_page_state_cmd_list.end()) { instance = std::make_shared(cmd, header, data, event_id, webview); - } else if(m_mqtt_agent_cmd_list.find(cmd) != m_mqtt_agent_cmd_list.end()) { - instance = std::make_shared(cmd, header, data, event_id, webview); } else { instance = std::make_shared(cmd, header, data, event_id, webview); @@ -7954,46 +5715,6 @@ void SSWCP::handle_web_message(std::string message, wxWebView* webview) { } } -// Handle incoming web messages for Flutter debug (no webview required) -void SSWCP::handle_webmsg_for_debug(std::string message) { - { - WCP_Logger::getInstance().add_log(message, false, "", "WCP", "info"); - - json j_message = json::parse(message); - - if (j_message.empty() || !j_message.count("header") || !j_message.count("payload") || !j_message["payload"].count("cmd")) { - return; - } - - json header = j_message["header"]; - json payload = j_message["payload"]; - - std::string cmd = ""; - std::string event_id = ""; - json params; - - if (payload.count("cmd")) { - cmd = payload["cmd"].get(); - } - if (payload.count("params")) { - params = payload["params"]; - } - - if (payload.count("event_id") && !payload["event_id"].is_null()) { - event_id = payload["event_id"].get(); - } - std::shared_ptr instance = create_sswcp_instance(cmd, header, params, event_id, nullptr); - if (instance) { - if (event_id != "") { - m_instance_list.add_infinite(instance.get(), instance); - } else { - m_instance_list.add(instance.get(), instance, DEFAULT_INSTANCE_TIMEOUT); - } - instance->process(); - } - } -} - // Delete instance from list void SSWCP::delete_target(SSWCP_Instance* target) { wxGetApp().CallAfter([target]() { @@ -8006,31 +5727,6 @@ void SSWCP::renew_instance_timeout(SSWCP_Instance* instance) { m_instance_list.update_timeout(instance, DEFAULT_INSTANCE_TIMEOUT); } -// Stop all machine subscriptions -void SSWCP::stop_subscribe_machine() -{ - wxGetApp().CallAfter([]() { - std::vector instances_to_stop; - - auto snapshot = m_instance_list.get_snapshot(); - - // Get all subscription instances to stop - for (const auto& instance : snapshot) { - if (instance.second->getType() == SSWCP_MachineFind_Instance::MACHINE_OPTION && instance.second->m_cmd == "sw_SubscribeMachineState") { - instances_to_stop.push_back(instance.first); - } - } - - // Stop each instance - for (auto* instance : instances_to_stop) { - auto instance_ptr = m_instance_list.get(instance); - if (instance_ptr) { - (*instance_ptr)->finish_job(); - } - } - }); -} - // Stop all machine discovery instances void SSWCP::stop_machine_find() { wxGetApp().CallAfter([]() { @@ -8077,24 +5773,6 @@ void SSWCP::on_webview_delete(wxWebView* view) } } - auto& device_map = wxGetApp().m_device_card_subscribers; - for (auto iter = device_map.begin(); iter != device_map.end();) { - if (iter->first == view) { - iter = device_map.erase(iter); - } else { - iter++; - } - } - - auto& login_map = wxGetApp().m_user_login_subscribers; - for (auto iter = login_map.begin(); iter != login_map.end();) { - if (iter->first == view) { - iter = login_map.erase(iter); - } else { - iter++; - } - } - auto& privacy_map = wxGetApp().m_user_update_privacy_subscribers; for (auto iter = privacy_map.begin(); iter != privacy_map.end();) { if (iter->first == view) { @@ -8113,15 +5791,6 @@ void SSWCP::on_webview_delete(wxWebView* view) } } - auto& cache_map = wxGetApp().m_cache_subscribers; - for (auto iter = cache_map.begin(); iter != cache_map.end();) { - if (iter->first.first == view) { - iter = cache_map.erase(iter); - } else { - iter++; - } - } - auto& page_state_map = wxGetApp().m_page_state_subscribers; for (auto iter = page_state_map.begin(); iter != page_state_map.end();) { if (iter->first == view) { @@ -8176,225 +5845,12 @@ void SSWCP::update_active_filename(const std::string& filename) m_active_gcode_filename = filename; } -// query the info of the machine -bool SSWCP::query_machine_info(std::shared_ptr& host, MachineInfo& out, int timeout_second) -{ - if (!host) return false; - - std::condition_variable cv; - std::shared_ptr mutex(new std::mutex); - std::weak_ptr cb_mutex = mutex; - bool received = false; - bool timeout = false; - json system_info; - - host->async_get_system_info( - [&, cb_mutex](const json& response) { - if (cb_mutex.expired()) { - return; - } - std::lock_guard lock(*mutex); - if (!response.is_null() && !response.count("error")) { - system_info = response; - } - received = true; - cv.notify_one(); - } - ); - - { - std::unique_lock lock(*mutex); - auto predicate = [&received]() { return received; }; - timeout = !cv.wait_for(lock, std::chrono::seconds(timeout_second), predicate); - } - - if (!timeout && !system_info.is_null()) - return SSWCPProtocol::parse_machine_info_response(system_info, out); - return false; -} - - -SSWCPProtocol::ResolveResult SSWCP::resolve_machine_info(std::shared_ptr& host, int timeout_second) -{ - SSWCPProtocol::ResolveResult result; - if (!host) return result; - - std::condition_variable cv; - std::shared_ptr mutex(new std::mutex); - std::weak_ptr cb_mutex = mutex; - std::shared_ptr done(new bool(false)); - int received_count = 0; - const int expected_count = 2; - json system_info; - json objects_query; - - auto on_system_info = [&, cb_mutex, done](const json& response) { - auto locked = cb_mutex.lock(); - if (!locked) return; - std::lock_guard lock(*locked); - if (*done) return; - if (!response.is_null() && !response.count("error")) - system_info = response; - if (++received_count >= expected_count) cv.notify_one(); - }; - auto on_objects_query = [&, cb_mutex, done](const json& response) { - auto locked = cb_mutex.lock(); - if (!locked) return; - std::lock_guard lock(*locked); - if (*done) return; - if (!response.is_null() && !response.count("error")) - objects_query = response; - if (++received_count >= expected_count) cv.notify_one(); - }; - - // Send both requests in parallel; seq_ids are distinct so they never collide. - // Query up to 8 extruder objects. Moonraker ignores keys that don't exist on the printer, - // so extra entries are harmless. The parser dynamically discovers whatever is returned. - std::vector>> extruder_targets; - extruder_targets.emplace_back("extruder", std::vector{}); - for (int i = 1; i <= 7; ++i) - extruder_targets.emplace_back("extruder" + std::to_string(i), std::vector{}); - host->async_get_system_info(on_system_info); - host->async_get_machine_info(extruder_targets, on_objects_query); - - { - std::unique_lock lock(*mutex); - cv.wait_for(lock, std::chrono::seconds(timeout_second), - [&received_count, expected_count]() { return received_count >= expected_count; }); - *done = true; - } - // --- Merge by field priority --- - MachineInfo& mi = result.info; - // model / device_name: system_info is authoritative, normalized for whitelist safety. - MachineInfo sys_mi; - bool got_system = !system_info.is_null() && SSWCPProtocol::parse_machine_info_response(system_info, sys_mi); - if (got_system) { - mi.model = SSWCPProtocol::normalize_machine_model(sys_mi.model); - mi.device_name = sys_mi.device_name; - // system_info nozzle data as fallback - mi.nozzle_diameters = sys_mi.nozzle_diameters; - mi.nozzle_volume_types = sys_mi.nozzle_volume_types; - } - // nozzle: objects.query is preferred (real-time), overrides system_info. - std::vector obj_diameters, obj_flows; - if (!objects_query.is_null() && SSWCPProtocol::parse_extruder_nozzle_info(objects_query, obj_diameters, obj_flows)) { - mi.nozzle_diameters = std::move(obj_diameters); - // Only overwrite flows when objects.query actually reported them; otherwise keep - // whatever system_info provided so we never silently clear the user's flow config. - if (!obj_flows.empty()) - mi.nozzle_volume_types = std::move(obj_flows); - } - - // If the machine reported nozzle diameters but no flow types (neither system_info - // nor objects.query carried them), default to standard -- one per nozzle. This - // matches the read-side semantics (FlowType::nozzle_volume_types() pads missing - // entries with standard) and lets sync always write a complete, usable vector - // instead of silently skipping the update. - if (!mi.nozzle_diameters.empty() && mi.nozzle_volume_types.empty()) - mi.nozzle_volume_types.assign(mi.nozzle_diameters.size(), FLOW_MODE_STANDARD); - - // Determine status - if (mi.model.empty()) - result.status = SSWCPProtocol::ResolveStatus::NoResponse; - else if (mi.nozzle_diameters.empty()) - result.status = SSWCPProtocol::ResolveStatus::GotIdentity; - else - result.status = SSWCPProtocol::ResolveStatus::Complete; - - return result; -} - - MachineIPType* MachineIPType::getInstance() { static MachineIPType mipt_instance; return &mipt_instance; } -// WebSocket Debug Server implementation -void SSWCP::enable_debug_mode(bool enable, unsigned short port) -{ - std::lock_guard lock(m_debug_server_mutex); - - if (enable && !m_debug_server) { - BOOST_LOG_TRIVIAL(info) << "Enabling WebSocket debug mode on port " << port; - - m_debug_server = std::make_unique(port); - - // Set message callback to handle messages from Flutter Web - m_debug_server->set_message_callback([](const std::string& message) { - BOOST_LOG_TRIVIAL(debug) << "Received message from Flutter Web via WebSocket"; - - // Handle the message using existing logic (no webview in debug/Flutter path) - wxGetApp().CallAfter([message]() { - SSWCP::handle_webmsg_for_debug(message); - }); - }); - - if (m_debug_server->start()) { - m_debug_mode_enabled = true; - BOOST_LOG_TRIVIAL(debug) << " WebSocket debug mode enabled successfully"; - BOOST_LOG_TRIVIAL(debug) << " Flutter Web can connect to: ws://localhost:" << port; - } else { - BOOST_LOG_TRIVIAL(error) << "Failed to start WebSocket debug server"; - m_debug_server.reset(); - m_debug_mode_enabled = false; - } - } else if (!enable && m_debug_server) { - BOOST_LOG_TRIVIAL(debug) << "Disabling WebSocket debug mode"; - m_debug_server->stop(); - m_debug_server.reset(); - m_debug_mode_enabled = false; - } -} - -void SSWCP::disable_debug_mode() -{ - enable_debug_mode(false); -} - -bool SSWCP::is_debug_mode_enabled() -{ - std::lock_guard lock(m_debug_server_mutex); - return m_debug_mode_enabled; -} - -void SSWCP::send_message_to_flutter(const std::string& message) -{ - std::lock_guard lock(m_debug_server_mutex); - - if (m_debug_server && m_debug_mode_enabled) { - m_debug_server->send_message(message); - } else { - BOOST_LOG_TRIVIAL(debug) << "Cannot send message: WebSocket debug mode not enabled"; - } -} - -void SSWCP::send_message_auto(const std::string& message, wxWebView* webview) -{ - // Original production path: send via WebView postMessage (unchanged, not affected by debug logic) - if (webview && webview->GetRefData()) { - BOOST_LOG_TRIVIAL(debug) << "Sending message to Flutter via WebView postMessage"; - std::string js_code = "window.postMessage(JSON.stringify(" + message + "), '*');"; - WebView::RunScript(webview, js_code); - } - - // Debug path: copy the message to Flutter debug interface via WebSocket (independent of original path) - { - std::lock_guard lock(m_debug_server_mutex); - if (m_debug_mode_enabled && m_debug_server && m_debug_server->has_client()) { - BOOST_LOG_TRIVIAL(debug) << "[DEBUG] Sending message to Flutter via WebSocket"; - m_debug_server->send_message(message); - } - } - - // Warning if no channel is available - if ((!webview || !webview->GetRefData()) && - (!m_debug_mode_enabled || !m_debug_server || !m_debug_server->has_client())) { - BOOST_LOG_TRIVIAL(warning) << "Cannot send message: no WebSocket client and no WebView available"; - } -} - }}; // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/SSWCP.hpp b/src/slic3r/GUI/SSWCP.hpp index 122c7e24014b..aa8d46145038 100644 --- a/src/slic3r/GUI/SSWCP.hpp +++ b/src/slic3r/GUI/SSWCP.hpp @@ -18,7 +18,6 @@ #include "slic3r/Utils/PrintHost.hpp" #include "slic3r/Utils/MQTT.hpp" #include "libslic3r/SSWCPProtocol.hpp" -#include "WebSocketDebugServer.hpp" using namespace nlohmann; @@ -149,8 +148,6 @@ class SSWCP_Instance : public std::enable_shared_from_this static void on_mqtt_status_msg_arrived(std::shared_ptr obj, const json& response); - static std::unordered_map m_wcp_cache; - private: // Test methods void sync_test(); @@ -158,14 +155,6 @@ class SSWCP_Instance : public std::enable_shared_from_this void test_mqtt_request(); - // Cache methods - void sw_SetCache(); - void sw_GetCache(); - void sw_RemoveCache(); - - void sw_SubscribeCacheKey(); - void sw_UnsubscribeCacheKeys(); - // select tab void sw_SwitchTab(); @@ -205,9 +194,6 @@ class SSWCP_Instance : public std::enable_shared_from_this void sw_OpenNetworkDialog(); -public: - void update_filament_info(const json& objects, bool send_message = false); - protected: std::thread m_work_thread; // Worker thread @@ -256,8 +242,6 @@ class SSWCP_MachineConnect_Instance : public SSWCP_Instance void sw_connect(); void sw_disconnect(); - void sw_get_connect_machine(); - void sw_connect_other_device(); void sw_get_pin_code(); @@ -273,98 +257,6 @@ class SSWCP_MachineConnect_Instance : public SSWCP_Instance }; -// mqtt-agent -class WebPresetDialog; -class SSWCP_MqttAgent_Instance : public SSWCP_Instance -{ -public: - SSWCP_MqttAgent_Instance(std::string cmd, const json& header, const json& data, std::string event_id, wxWebView* webview) - : SSWCP_Instance(cmd, header, data, event_id, webview) - { - m_type = MQTT_AGENT; - } - - ~SSWCP_MqttAgent_Instance() - { - if (m_work_thread.joinable()) - m_work_thread.detach(); - } - - void process() override; - -public: - static std::unordered_map>> m_mqtt_engine_map; // (id, client) - static std::mutex m_engine_map_mtx; - - static std::map, std::string> m_subscribe_map; // ((event_id, webview), topic) - static std::map, std::weak_ptr> m_subscribe_instance_map; // ((event_id, webview), instance) - - static WebPresetDialog* m_dialog; - - // Funnel-correlation id for the connection attempt on a given webview. Each - // sw_* call is a fresh instance object, so (like m_mqtt_engine_map) this id - // lives in static state keyed by the webview: assigned in sw_create_mqtt_client, - // read by connect/subscribe/set_engine/disconnect, cleared on disconnect. - static std::unordered_map m_connect_session_map; - -public: - bool validate_id(const std::string& id); - std::shared_ptr get_current_engine() { - m_engine_map_mtx.lock(); - std::shared_ptr ptr = nullptr; - if (m_mqtt_engine_map.count(m_webview)) { - ptr = m_mqtt_engine_map[m_webview].second; - } - - m_engine_map_mtx.unlock(); - - return ptr; - } - bool set_current_engine(const std::pair>& target) { - bool flag = true; - m_engine_map_mtx.lock(); - m_mqtt_engine_map[m_webview] = target; - - m_engine_map_mtx.unlock(); - return flag; - } - - // Accessors for the per-webview connectSessionId (guarded by m_engine_map_mtx, - // the same lock that protects m_mqtt_engine_map). Empty when no connection is - // in progress on this webview. - std::string get_connect_session_id() { - std::lock_guard lk(m_engine_map_mtx); - auto it = m_connect_session_map.find(m_webview); - return it != m_connect_session_map.end() ? it->second : std::string{}; - } - void set_connect_session_id(const std::string& id) { - std::lock_guard lk(m_engine_map_mtx); - m_connect_session_map[m_webview] = id; - } - void clear_connect_session_id() { - std::lock_guard lk(m_engine_map_mtx); - m_connect_session_map.erase(m_webview); - } - - void set_Instance_illegal() override; - -private: - - void sw_create_mqtt_client(); - void sw_mqtt_connect(); - void sw_mqtt_disconnect(); - void sw_mqtt_subscribe(); - void sw_mqtt_unsubscribe(); - void sw_mqtt_publish(); - void sw_mqtt_set_engine(); - -private: - void clean_current_engine(); - - static void mqtt_msg_cb(const std::string& topic, const std::string& payload, void* client); - -}; - // Instance class for handling machine discovery class SSWCP_MachineFind_Instance : public SSWCP_Instance { @@ -427,14 +319,6 @@ class SSWCP_MachineOption_Instance : public SSWCP_Instance // Machine option methods void sw_SendGCodes(); void sw_FileGetStatus(); - void sw_SystemGetDeviceInfo(); - void sw_GetMachineState(); - void sw_GetPrintInfo(); - void sw_SubscribeMachineState(); - void sw_UnSubscribeMachineState(); - void sw_GetMachineObjects(); - void sw_SetMachineSubscribeFilter(); - void sw_GetSystemInfo(); void sw_MachinePrintStart(); void sw_MachinePrintPause(); void sw_MachinePrintResume(); @@ -510,38 +394,6 @@ class SSWCP_MachineOption_Instance : public SSWCP_Instance // Request device heartbeat void sw_MachineHeartbeat(); - // update machine filament info - void sw_UpdateMachineFilamentInfo(); -}; - -// Instance class for Snapmaker machine manage -class SSWCP_MachineManage_Instance : public SSWCP_Instance -{ -public: - SSWCP_MachineManage_Instance(std::string cmd, const json& header, const json& data, std::string event_id, wxWebView* webview) - : SSWCP_Instance(cmd, header, data, event_id, webview) - { - m_type = MACHINE_MANAGE; - } - - ~SSWCP_MachineManage_Instance() {} - - void process() override; - -private: - void sw_GetLocalDevices(); - - void sw_AddDevice(); - - void sw_SubscribeLocalDevices(); - - void sw_RenameDevice(); - - void sw_SwitchModel(); - - void sw_DeleteDevices(); - - void sw_UpdateDeviceInfo(); }; // Instance class for page state change subscription @@ -579,16 +431,6 @@ class SSWCP_UserLogin_Instance : public SSWCP_Instance void process() override; private: - void sw_UserLogin(); - - void sw_AskUserLogin(); - - void sw_UserLogout(); - - void sw_GetUserLoginState(); - - void sw_SubscribeUserLoginState(); - void sw_GetUserUpdatePrivacy(); void sw_SubUserUpdatePrivacy(); @@ -608,10 +450,6 @@ class SSWCP_UserLogin_Instance : public SSWCP_Instance void sw_GetFilesFromDir(); void sw_NotifyUploadTimelaspe(); -public: - static bool s_ask_dialog_showing; - static std::vector> s_ask_waiters; - public: // Passive subscription entry — sw_SubscribeDownloadState only registers, // downloads are triggered by sw_DownLoadFile which pushes "download_complete" @@ -673,9 +511,6 @@ class SSWCP // Handle incoming web messages static void handle_web_message(std::string message, wxWebView* webview); - // Handle incoming web messages for Flutter debug (no webview required) - static void handle_webmsg_for_debug(std::string message); - // Create new SSWCP instance static std::shared_ptr create_sswcp_instance( std::string cmd, const json& header, const json& data, std::string event_id, wxWebView* webview); @@ -684,30 +519,16 @@ class SSWCP static void delete_target(SSWCP_Instance* target); // Extend a one-shot instance's timeout by the default timeout; used by - // long-running modal commands (e.g. sw_AskUserLogin) so their pending - // response is not dropped after the default 80 s. + // long-running modal commands so their pending response is not dropped + // after the default 80 s. static void renew_instance_timeout(SSWCP_Instance* instance); // Stop machine discovery static void stop_machine_find(); - // Stop machine subscription - static void stop_subscribe_machine(); - // Handle webview deletion static void on_webview_delete(wxWebView* webview); - // query the info of the machine - static bool query_machine_info(std::shared_ptr& host, MachineInfo& out, int timeout_second = 5); - - // Resolve machine info via parallel system_info + objects.query, then merge by field priority. - // model/device_name from system_info (real-time, authoritative), normalized. - // nozzle from objects.query (real-time, preferred) or system_info fallback. - // Status: NoResponse / GotIdentity / Complete. - // Must be called from a thread that is NOT the UI thread if timeout_second is large, - // or from UI thread if cache hit is expected to short-circuit quickly. - static SSWCPProtocol::ResolveResult resolve_machine_info(std::shared_ptr& host, int timeout_second = 8); - // update the active file name static void update_active_filename(const std::string& filename); @@ -719,27 +540,22 @@ class SSWCP static std::string get_display_filename(); + static nlohmann::json build_active_file_metadata(); + static nlohmann::json build_active_file_json(const std::string& file_path, const std::string& file_name, bool is_zip = false); + static nlohmann::json build_filament_mapping_json(const std::string& filename, const std::string& display_name); + static std::mutex m_file_size_mutex; static long long m_active_file_size; static std::unordered_map m_tab_map; // for switching tab - // WebSocket Debug Server methods - static void enable_debug_mode(bool enable = true, unsigned short port = 8766); - static void disable_debug_mode(); - static bool is_debug_mode_enabled(); - static void send_message_to_flutter(const std::string& message); - static void send_message_auto(const std::string& message, wxWebView* webview = nullptr); - private: static std::unordered_set m_machine_find_cmd_list; // Machine find commands static std::unordered_set m_machine_option_cmd_list; // Machine option commands static std::unordered_set m_machine_connect_cmd_list; // Machine connect commands static std::unordered_set m_project_cmd_list; // homepage project commands static std::unordered_set m_login_cmd_list; // homepage login commands - static std::unordered_set m_machine_manage_cmd_list; // homepage machine manage commands; - static std::unordered_set m_mqtt_agent_cmd_list; // mqtt-agent commands; static std::unordered_set m_page_state_cmd_list; // page state change commands; static TimeoutMap> m_instance_list; // Active instances @@ -748,10 +564,6 @@ class SSWCP static std::string m_active_gcode_filename; // name of the file which is pretend to be upload and print static std::string m_display_gcode_filename; // name for display - // WebSocket Debug Server - static std::unique_ptr m_debug_server; - static std::mutex m_debug_server_mutex; - static bool m_debug_mode_enabled; }; class MachineIPType diff --git a/src/slic3r/GUI/WebDeviceDialog.cpp b/src/slic3r/GUI/WebDeviceDialog.cpp index c55eb3768c08..39d06f38a866 100644 --- a/src/slic3r/GUI/WebDeviceDialog.cpp +++ b/src/slic3r/GUI/WebDeviceDialog.cpp @@ -16,12 +16,11 @@ END_EVENT_TABLE() WebDeviceDialog::WebDeviceDialog() : wxDialog((wxWindow*)(wxGetApp().mainframe), wxID_ANY, _L("Add Device")) { - m_device_url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + - "/web/flutter_web/index.html?path=discovery"); + m_device_url = wxGetApp().gateway_web_url("device_control"); SetBackgroundColour(*wxWHITE); - // Create blank; run() loads the localized URL once (same pattern as WebPreprintDialog / #679). + // Create blank; run() loads the gateway URL once (same pattern as WebPreprintDialog / #679). m_browser = WebView::CreateWebView(this, "about:blank"); if (m_browser == nullptr) { wxLogError("Could not init m_browser"); @@ -57,21 +56,19 @@ WebDeviceDialog::~WebDeviceDialog() void WebDeviceDialog::reload() { - wxString target_url = wxGetApp().get_international_url(m_device_url); - load_url(target_url); + load_url(m_device_url); } void WebDeviceDialog::load_url(wxString &url) { - m_browser->LoadURL(url); + WebView::LoadUrl(m_browser, url); m_browser->Show(); Layout(); } bool WebDeviceDialog::run() { - wxString target_url = wxGetApp().get_international_url(m_device_url); - this->load_url(target_url); + this->load_url(m_device_url); if (this->ShowModal() == wxID_OK) { return true; } diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index fa6d3f97dce6..1b77a004f03a 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -489,7 +489,6 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt) std::lock_guard lock(m_ProfileJson_mutex); oldregion = m_ProfileJson["region"]; } - bool bLogin = false; if (m_Region != oldregion) { AppConfig* config = GUI::wxGetApp().app_config; std::string country_code = config->get_country_code(); @@ -497,12 +496,9 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt) if (agent) { agent->set_country_code(country_code); if (wxGetApp().is_user_login()) { - bLogin = true; agent->user_logout(); } } - - wxGetApp().fltviews().reload_all(); } this->EndModal(wxID_OK); @@ -510,8 +506,6 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt) if (InstallNetplugin) GUI::wxGetApp().CallAfter([this] { GUI::wxGetApp().ShowDownNetPluginDlg(); }); - if (bLogin) - GUI::wxGetApp().CallAfter([this] { login(); }); } else if (strCmd == "user_guide_cancel") { this->EndModal(wxID_CANCEL); diff --git a/src/slic3r/GUI/WebPreprintDialog.cpp b/src/slic3r/GUI/WebPreprintDialog.cpp index f60c165e124c..0533927dd309 100644 --- a/src/slic3r/GUI/WebPreprintDialog.cpp +++ b/src/slic3r/GUI/WebPreprintDialog.cpp @@ -3,6 +3,7 @@ #include "GUI_App.hpp" #include "MainFrame.hpp" #include "SSWCP.hpp" +#include "slic3r/Utils/Http.hpp" #include #include #include "NotificationManager.hpp" @@ -10,6 +11,20 @@ namespace Slic3r { namespace GUI { +namespace { + +wxString append_store_id(const wxString& base_url, const std::string& store_id) +{ + if (store_id.empty()) + return base_url; + + wxString url = base_url; + url += (url.Contains("?") ? "&" : "?") + wxString::FromUTF8("id=" + Http::url_encode(store_id)); + return url; +} + +} // namespace + BEGIN_EVENT_TABLE(WebPreprintDialog, wxDialog) EVT_CLOSE(WebPreprintDialog::OnClose) END_EVENT_TABLE() @@ -17,11 +32,9 @@ END_EVENT_TABLE() WebPreprintDialog::WebPreprintDialog() : wxDialog((wxWindow*)(wxGetApp().mainframe), wxID_ANY, _L("Print preset")) { - m_prePrint_url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + - "/web/flutter_web/index.html?path=4"); + m_prePrint_url = wxGetApp().gateway_web_url("pre_paint_page"); - m_preSend_url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + - "/web/flutter_web/index.html?path=5"); + m_preSend_url = wxGetApp().gateway_web_url("pre_paint_upload_page"); SetBackgroundColour(*wxWHITE); // Create the webview with about:blank; the actual page will be loaded by run() @@ -112,10 +125,21 @@ void WebPreprintDialog::reload() load_url(m_prePrint_url); } +void WebPreprintDialog::refresh_gateway_urls() +{ + m_prePrint_url = wxGetApp().gateway_web_url("pre_paint_page"); + m_preSend_url = wxGetApp().gateway_web_url("pre_paint_upload_page"); + if (!wxGetApp().is_gateway_url(m_prePrint_url) || !wxGetApp().is_gateway_url(m_preSend_url) || m_browser == nullptr) + return; + + wxString real_url = build_web_url(); + load_url(real_url); +} + void WebPreprintDialog::load_url(wxString &url) { wxGetApp().fltviews().add_view(m_browser, url); - m_browser->LoadURL(url); + WebView::LoadUrl(m_browser, url); Layout(); } @@ -125,7 +149,7 @@ bool WebPreprintDialog::run() SSWCP::update_active_filename(m_gcode_file_name); SSWCP::update_display_filename(m_display_file_name); - auto real_url = m_send_page ? wxGetApp().get_international_url(m_preSend_url) : wxGetApp().get_international_url(m_prePrint_url); + auto real_url = build_web_url(); if(m_send_page){ this->SetTitle(_L("Pretreat the uploaded content")); }else{ @@ -147,6 +171,11 @@ bool WebPreprintDialog::run() return false; } +wxString WebPreprintDialog::build_web_url() const +{ + return append_store_id(m_send_page ? m_preSend_url : m_prePrint_url, m_store_id); +} + void WebPreprintDialog::RunScript(const wxString &javascript) { m_javascript = javascript; diff --git a/src/slic3r/GUI/WebPreprintDialog.hpp b/src/slic3r/GUI/WebPreprintDialog.hpp index 98ca1b7523d5..5668b5a1447e 100644 --- a/src/slic3r/GUI/WebPreprintDialog.hpp +++ b/src/slic3r/GUI/WebPreprintDialog.hpp @@ -15,6 +15,7 @@ class WebPreprintDialog : public wxDialog void load_url(wxString &url); bool run(); + void refresh_gateway_urls(); void RunScript(const wxString &javascript); void reload(); @@ -27,6 +28,8 @@ class WebPreprintDialog : public wxDialog void set_send_page(bool flag); + void set_store_id(const std::string& id) { m_store_id = id; } + bool need_switch_to_device() { return m_switch_to_device; } void set_swtich_to_device(bool flag); @@ -39,6 +42,8 @@ class WebPreprintDialog : public wxDialog void SafeEndModal(int returnCode); private: + wxString build_web_url() const; + void OnClose(wxCloseEvent& evt); void OnNavigationRequest(wxWebViewEvent &evt); void OnNavigationComplete(wxWebViewEvent &evt); @@ -54,6 +59,7 @@ class WebPreprintDialog : public wxDialog std::string m_display_file_name = ""; bool m_send_page = false; bool m_switch_to_device = false; + std::string m_store_id; bool m_finish = false; bool m_modal_ended = false; // BBS: Flag to prevent duplicate EndModal calls @@ -62,4 +68,4 @@ class WebPreprintDialog : public wxDialog }} // namespace Slic3r::GUI -#endif \ No newline at end of file +#endif diff --git a/src/slic3r/GUI/WebPresetDialog.cpp b/src/slic3r/GUI/WebPresetDialog.cpp index 3444ef08ea43..e8aa429cc1e1 100644 --- a/src/slic3r/GUI/WebPresetDialog.cpp +++ b/src/slic3r/GUI/WebPresetDialog.cpp @@ -473,38 +473,7 @@ void WebPresetDialog::OnScriptMessage(wxWebViewEvent& evt) } } - DeviceInfo info; - if (wxGetApp().app_config->get_device_info(m_device_id, info)) { - info.model_name = MSelected.begin().value()["model"].get(); - info.nozzle_sizes = {MSelected.begin().value()["nozzle_diameter"].get()}; - info.preset_name = info.model_name + " (" + info.nozzle_sizes[0] + " nozzle)"; - size_t vendor_pos = info.model_name.find_first_of(" "); - if (vendor_pos != std::string::npos) { - std::string vendor = info.model_name.substr(0, vendor_pos); - std::string machine_cover = LOCALHOST_URL + std::to_string(wxGetApp().m_page_http_server.get_port()) + "/profiles/" + vendor + "/" + - info.model_name + "_cover.png"; - - info.img = machine_cover; - } - - wxGetApp().app_config->save_device_info(info); - - //// 同步卡片 - //auto devices = wxGetApp().app_config->get_devices(); - //json param; - //param["command"] = "local_devices_arrived"; - //param["sequece_id"] = "10001"; - //param["data"] = devices; - //std::string logout_cmd = param.dump(); - //wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd); - //GUI::wxGetApp().run_script(strJS); - - // wcp订阅 - json data = wxGetApp().app_config->get_devices(); - wxGetApp().device_card_notify(data); - - wxGetApp().mainframe->plater()->sidebar().update_all_preset_comboboxes(); - } + wxGetApp().mainframe->plater()->sidebar().update_all_preset_comboboxes(); } else if (strCmd == "save_userguide_filaments") { { @@ -530,7 +499,6 @@ void WebPresetDialog::OnScriptMessage(wxWebViewEvent& evt) std::lock_guard lock(m_ProfileJson_mutex); oldregion = m_ProfileJson["region"]; } - bool bLogin = false; if (m_Region != oldregion) { AppConfig* config = GUI::wxGetApp().app_config; std::string country_code = config->get_country_code(); @@ -538,7 +506,6 @@ void WebPresetDialog::OnScriptMessage(wxWebViewEvent& evt) if (agent) { agent->set_country_code(country_code); if (wxGetApp().is_user_login()) { - bLogin = true; agent->user_logout(); } } @@ -549,8 +516,6 @@ void WebPresetDialog::OnScriptMessage(wxWebViewEvent& evt) if (InstallNetplugin) GUI::wxGetApp().CallAfter([this] { GUI::wxGetApp().ShowDownNetPluginDlg(); }); - if (bLogin) - GUI::wxGetApp().CallAfter([this] { login(); }); } else if (strCmd == "user_guide_cancel") { this->EndModal(wxID_CANCEL); this->Close(); @@ -609,21 +574,7 @@ void WebPresetDialog::SendUserGuideProfile() res_json = m_ProfileJson; } - std::string model_name = ""; - std::vector nozzle_sizes; - if (m_device_id != "") { - DeviceInfo info; - if (wxGetApp().app_config->get_device_info(m_device_id, info)) { - if (info.model_name != "") { - // test - if (info.model_name == "lava" || info.model_name == "Snapmaker test") { - info.model_name = "Snapmaker U1"; - } - model_name = info.model_name; - nozzle_sizes = info.nozzle_sizes; - } - } - } + const std::string model_name = ""; if (res_json.count("model")) { json& model = res_json["model"]; @@ -631,11 +582,7 @@ void WebPresetDialog::SendUserGuideProfile() json& item = model[i]; if (item.count("nozzle_selected")) { if (item["model"].get() == model_name) { - if (!nozzle_sizes.empty()) { - item["nozzle_selected"] = nozzle_sizes[0]; - } else { - item["nozzle_selected"] = ""; - } + item["nozzle_selected"] = ""; if (m_bind_nozzle) { json cp_item = item; diff --git a/src/slic3r/GUI/WebPresetDialog.hpp b/src/slic3r/GUI/WebPresetDialog.hpp index 80737ca212a8..10064ffecc0b 100644 --- a/src/slic3r/GUI/WebPresetDialog.hpp +++ b/src/slic3r/GUI/WebPresetDialog.hpp @@ -111,7 +111,6 @@ class WebPresetDialog : public DPIDialog bool network_plugin_ready{false}; public: - std::string m_device_id = ""; bool m_bind_nozzle = false; #if wxUSE_WEBVIEW_IE diff --git a/src/slic3r/GUI/WebSMUserLoginDialog.cpp b/src/slic3r/GUI/WebSMUserLoginDialog.cpp deleted file mode 100644 index 63ade3d592a8..000000000000 --- a/src/slic3r/GUI/WebSMUserLoginDialog.cpp +++ /dev/null @@ -1,598 +0,0 @@ -#include "WebSMUserLoginDialog.hpp" - -#include -#include "I18N.hpp" -#include "libslic3r/AppConfig.hpp" -#include "libslic3r/Utils.hpp" -#include "slic3r/GUI/wxExtensions.hpp" -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/Utils/SnapLogClient.hpp" -#include "common_func/common_func.hpp" -#include "slic3r/GUI/Widgets/StateColor.hpp" -#include "Widgets/Button.hpp" - -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include - -#include -#include "MainFrame.hpp" -#include - -#include -#include -#include "sentry_wrapper/SentryWrapper.hpp" -using namespace std; - -using namespace nlohmann; - -namespace Slic3r { namespace GUI { - -#define NETWORK_OFFLINE_TIMER_ID 10001 -#define CALLBACK_POLL_TIMER_ID 10002 - -BEGIN_EVENT_TABLE(SMUserLogin, wxDialog) -EVT_TIMER(NETWORK_OFFLINE_TIMER_ID, SMUserLogin::OnTimer) -EVT_TIMER(CALLBACK_POLL_TIMER_ID, SMUserLogin::OnCallbackPollTimer) -END_EVENT_TABLE() - -int SMUserLogin::web_sequence_id = 20000; - -SMUserLogin::SMUserLogin(bool isLogout) : wxDialog((wxWindow *) (wxGetApp().mainframe), wxID_ANY, "Snapmaker Orca") -{ - // url - auto region = wxGetApp().app_config->get_country_code(); - if (region.find("CN") == std::string::npos) { - TargetUrl = "https://id.snapmaker.com?from=orca"; - LogoutUrl = "https://id.snapmaker.com/logout?from=orca"; - m_hostUrl = "https://id.snapmaker.com"; - m_accountUrl = "https://id.snapmaker.com"; - m_userInfoUrl = "https://id.snapmaker.com/api/common/accounts/current"; - m_home_url = "https://www.snapmaker.com/"; - } else { - TargetUrl = "https://id.snapmaker.cn?from=orca"; - LogoutUrl = "https://id.snapmaker.cn/logout?from=orca"; - m_hostUrl = "https://id.snapmaker.cn"; - m_accountUrl = "https://api.snapmaker.cn"; - m_userInfoUrl = "https://api.snapmaker.cn/api/common/accounts/current"; - m_home_url = "https://www.snapmaker.cn/"; - } - - SetBackgroundColour(*wxWHITE); - - BOOST_LOG_TRIVIAL(info) << "login url = " << TargetUrl.ToStdString(); - - m_sm_user_agent = wxString::Format("SM-Slicer/v%s", SLIC3R_VERSION); - - // set the frame icon - - // Create the webview - m_browser = WebView::CreateWebView(this, isLogout ? LogoutUrl : TargetUrl); - if (m_browser == nullptr) { - wxLogError("Could not init m_browser"); - return; - } - m_browser->Hide(); - m_browser->SetSize(0, 0); - - // Log backend information - // wxLogMessage(wxWebView::GetBackendVersionInfo().ToString()); - // wxLogMessage("Backend: %s Version: %s", - // m_browser->GetClassInfo()->GetClassName(),wxWebView::GetBackendVersionInfo().ToString()); - // wxLogMessage("User Agent: %s", m_browser->GetUserAgent()); - - // Connect the webview events - Bind(wxEVT_WEBVIEW_NAVIGATING, &SMUserLogin::OnNavigationRequest, this, m_browser->GetId()); - Bind(wxEVT_WEBVIEW_NAVIGATED, &SMUserLogin::OnNavigationComplete, this, m_browser->GetId()); - Bind(wxEVT_WEBVIEW_LOADED, &SMUserLogin::OnDocumentLoaded, this, m_browser->GetId()); - Bind(wxEVT_WEBVIEW_ERROR, &SMUserLogin::OnError, this, m_browser->GetId()); - Bind(wxEVT_WEBVIEW_NEWWINDOW, &SMUserLogin::OnNewWindow, this, m_browser->GetId()); - Bind(wxEVT_WEBVIEW_TITLE_CHANGED, &SMUserLogin::OnTitleChanged, this, m_browser->GetId()); - Bind(wxEVT_WEBVIEW_FULLSCREEN_CHANGED, &SMUserLogin::OnFullScreenChanged, this, m_browser->GetId()); - //Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &SMUserLogin::OnScriptMessage, this, m_browser->GetId()); - - // Connect the idle events - // Bind(wxEVT_IDLE, &SMUserLogin::OnIdle, this); - // Bind(wxEVT_CLOSE_WINDOW, &SMUserLogin::OnClose, this); - - // The oauth callback page must be found by polling: WebView2 does not - // reliably deliver navigation events for that hop, and GUI_App shows this - // dialog via ShowModal() directly, run() is not used. - m_callback_timer = new wxTimer(this, CALLBACK_POLL_TIMER_ID); - m_callback_timer->Start(500); - - // UI - SetTitle(isLogout ? _L("Log out") : _L("Login")); - // Set a more sensible size for web browsing - wxSize pSize = FromDIP(wxSize(650, 840)); - SetSize(pSize); - - int screenheight = wxSystemSettings::GetMetric(wxSYS_SCREEN_Y, NULL); - int screenwidth = wxSystemSettings::GetMetric(wxSYS_SCREEN_X, NULL); - int MaxY = (screenheight - pSize.y) > 0 ? (screenheight - pSize.y) / 2 : 0; - wxPoint tmpPT((screenwidth - pSize.x) / 2, MaxY); - Move(tmpPT); - wxGetApp().UpdateDlgDarkUI(this); -} - -SMUserLogin::~SMUserLogin() { - if (m_callback_timer != NULL) { - m_callback_timer->Stop(); - delete m_callback_timer; - m_callback_timer = NULL; - } - if (m_timer != NULL) { - m_timer->Stop(); - delete m_timer; - m_timer = NULL; - } -} - -void SMUserLogin::OnCallbackPollTimer(wxTimerEvent &event) { - try_complete_oauth_callback(); -} - -// The third-party oauth callback endpoint may answer 200 with the raw token -// json as the page body instead of redirecting to a url containing "token=", -// the only format OnNavigationRequest understands. Detect that page, then -// have a fire-and-forget script smuggle the body out through document.title; -// wxWebView::RunScript is avoided because it busy-pumps the event loop on -// every backend and freezes the app when the web process is dead. -void SMUserLogin::try_complete_oauth_callback() { - if (m_callback_handled || m_browser == NULL) - return; - if (!m_browser->GetCurrentURL().Contains("/api/oauth2/callback/")) - return; - if (++m_callback_attempts > 60) { // ~30s at 500ms - m_callback_timer->Stop(); - return; - } - RunScript("document.title='SMOAUTH:'+(document.body?document.body.innerText:'')"); -} - -void SMUserLogin::OnTimer(wxTimerEvent &event) { - m_timer->Stop(); - - if (m_networkOk == false) - { - ShowErrorPage(); - } -} - -bool SMUserLogin::run() { - m_timer = new wxTimer(this, NETWORK_OFFLINE_TIMER_ID); - m_timer->Start(8000); - - if (this->ShowModal() == wxID_OK) { - return true; - } else { - return false; - } -} - - -void SMUserLogin::load_url(wxString &url) -{ - m_browser->LoadURL(url); - m_browser->SetFocus(); - UpdateState(); -} - - -/** - * Method that retrieves the current state from the web control and updates - * the GUI the reflect this current state. - */ -void SMUserLogin::UpdateState() -{ - // SetTitle(m_browser->GetCurrentTitle()); -} - -void SMUserLogin::OnIdle(wxIdleEvent &WXUNUSED(evt)) -{ - if (m_browser->IsBusy()) { - wxSetCursor(wxCURSOR_ARROWWAIT); - } else { - wxSetCursor(wxNullCursor); - } -} - -// void SMUserLogin::OnClose(wxCloseEvent& evt) -//{ -// this->Hide(); -//} - -/** - * Callback invoked when there is a request to load a new page (for instance - * when the user clicks a link) - */ -void SMUserLogin::OnNavigationRequest(wxWebViewEvent &evt) -{ - wxString tmpUrl = evt.GetURL(); - - size_t start = tmpUrl.find("token="); - if (start != std::string::npos) { - std::string token; - - start += std::string("token=").size(); // 跳过"token="的长度 - size_t end = tmpUrl.find_first_of("?&#", start); - if (end != std::string::npos) { - token = tmpUrl.substr(start, end - start).ToStdString(); - } else { - token = tmpUrl.substr(start).ToStdString(); - } - - if (this->IsModal()) - this->EndModal(wxID_OK); - - std::string info_url = m_userInfoUrl.ToStdString(); - std::size_t refresh_generation = wxGetApp().sm_token_refresh_generation(); - wxGetApp().CallAfter([token, info_url, refresh_generation]() { - if (!wxGetApp().sm_is_token_refresh_current(refresh_generation)) - return; - - std::string url = info_url; - auto http = Http::get(url); - http.header("Authorization",token); - http.on_complete([&](std::string body, unsigned status) { - if (!wxGetApp().sm_is_token_refresh_current(refresh_generation)) - return; - - if (status == 200) { - std::string user_id = ""; - json response = json::parse(body, nullptr, false); - if (response.is_discarded() || !response.is_object() || !response.contains("data")) { - BOOST_LOG_TRIVIAL(error) << "login userinfo response format is invalid" - << ", body_size=" << body.size(); - string parse_fail = BP_LOGIN_HTTP_CODE + string(":200 invalid response format"); - sentryReportLog(SENTRY_LOG_TRACE, parse_fail, BP_LOGIN); - return; - } - json data = response["data"]; - if (data.is_object()) { - if (data.contains("id") && data["id"].is_number_integer()) { - wxGetApp().sm_get_userinfo()->set_user_id(std::to_string(data["id"].get())); - user_id = std::to_string(data["id"].get()); - } - if (data.contains("nickname") && data["nickname"].is_string()) { - wxGetApp().sm_get_userinfo()->set_user_name(data["nickname"].get()); - } - if (data.contains("icon") && data["icon"].is_string()) { - wxGetApp().sm_get_userinfo()->set_user_icon_url(data["icon"].get()); - } - if (data.contains("account") && data["account"].is_string()) { - wxGetApp().sm_get_userinfo()->set_user_account(data["account"].get()); - } - } - string userInfo = BP_LOGIN_USER_ID + std::string(":") + user_id; - sentryReportLog(SENTRY_LOG_TRACE, userInfo, BP_LOGIN); - wxGetApp().sm_get_userinfo()->set_user_token(token); - wxGetApp().sm_get_userinfo()->set_user_login(true); - wxGetApp().sm_on_token_captured(refresh_generation); - // Mirror-push login identity into SnapLogClient (Task 12). - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_user_token(token); - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_user_id(user_id); - auto* ac = wxGetApp().app_config; - if (ac) { - bool consent = ac->get("app", PRIVACY_POLICY_FLAGS) == "true"; - ::Slic3r::SnapLog::v1::SnapLogClient::instance().set_consent(consent); - } - - SNAP_LOG_BATCH(Info, "user login success", - {"eventName", "user_login_result"}, {"source", "cpp"}, - {"success", "true"}, {"userId", user_id}); - } - }) - .on_error([&](std::string body, std::string, unsigned status) { - if (!wxGetApp().sm_is_token_refresh_current(refresh_generation)) - return; - - std::string http_code = BP_LOGIN_HTTP_CODE + string(":") + std::to_string(status) + - ", body_size=" + std::to_string(body.size()); - sentryReportLog(SENTRY_LOG_TRACE, http_code, BP_LOGIN); - SNAP_LOG_BATCH(Error, "user login failed", - {"eventName", "user_login_result"}, {"source", "cpp"}, - {"success", "false"}, {"httpStatus", std::to_string(status)}); - }) - .perform_sync(); - }); - } - UpdateState(); -} - -/** - * Callback invoked when a navigation request was accepted - */ -void SMUserLogin::OnNavigationComplete(wxWebViewEvent &evt) -{ - // wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'"); - m_browser->Show(); - Layout(); - UpdateState(); -} - -/** - * Callback invoked when a page is finished loading - */ -void SMUserLogin::OnDocumentLoaded(wxWebViewEvent &evt) -{ - // Only notify if the document is the main frame, not a subframe - wxString tmpUrl = evt.GetURL(); - std::string strHost = "https://id.snapmaker.com"; - - if ( tmpUrl.Contains(strHost) ) { - m_networkOk = true; - // wxLogMessage("%s", "Document loaded; url='" + evt.GetURL() + "'"); - } - - try_complete_oauth_callback(); - UpdateState(); -} - -/** - * On new window, we veto to stop extra windows appearing - */ -void SMUserLogin::OnNewWindow(wxWebViewEvent &evt) -{ - wxString flag = " (other)"; - - if (evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER) { flag = " (user)"; } - - // wxLogMessage("%s", "New window; url='" + evt.GetURL() + "'" + flag); - - // If we handle new window events then just load them in this window as we - // are a single window browser - m_browser->LoadURL(evt.GetURL()); - - UpdateState(); -} - -void SMUserLogin::OnTitleChanged(wxWebViewEvent &evt) -{ - // Body smuggled out by try_complete_oauth_callback(); feed the token to - // the existing "token=" capture path in OnNavigationRequest. - const wxString title = evt.GetString(); - if (!title.StartsWith("SMOAUTH:") || m_callback_handled || m_callback_timer == NULL) - return; - - // No-throw parsing: the title may carry an empty/partial body while the - // callback page is still rendering; a thrown parse_error would escape this - // wx event handler and terminate the app (seen crashing on macOS). - json response = json::parse(into_u8(title.Mid(wxString("SMOAUTH:").Length())), nullptr, false); - if (response.is_discarded() || !response.is_object() || !response.contains("data")) - return; - json data = response["data"]; - if (!data.contains("access_token") || !data["access_token"].is_string()) - return; - - m_callback_handled = true; - m_callback_timer->Stop(); - m_browser->LoadURL(m_hostUrl + "/?token=" + from_u8(data["access_token"].get())); -} - -void SMUserLogin::OnFullScreenChanged(wxWebViewEvent &evt) -{ - // wxLogMessage("Full screen changed; status = %d", evt.GetInt()); - ShowFullScreen(evt.GetInt() != 0); -} - -void SMUserLogin::OnScriptMessage(wxWebViewEvent &evt) -{ - wxString str_input = evt.GetString(); - try { - json j = json::parse(into_u8(str_input)); - - wxString strCmd = j["command"]; - - if (strCmd == "autotest_token") - { - m_AutotestToken = j["data"]["token"]; - } - if (strCmd == "user_login") { - j["data"]["autotest_token"] = m_AutotestToken; - Close(); - } - else if (strCmd == "get_localhost_url") { - BOOST_LOG_TRIVIAL(info) << "thirdparty_login: get_localhost_url"; - //wxGetApp().start_http_server(); - std::string sequence_id = j["sequence_id"].get(); - CallAfter([this, sequence_id] { - json ack_j; - ack_j["command"] = "get_localhost_url"; - ack_j["response"]["base_url"] = std::string(LOCALHOST_URL) + std::to_string(LOCALHOST_PORT); - ack_j["response"]["result"] = "success"; - ack_j["sequence_id"] = sequence_id; - wxString str_js = wxString::Format("window.postMessage(%s)", ack_j.dump()); - this->RunScript(str_js); - }); - } - else if (strCmd == "thirdparty_login") { - BOOST_LOG_TRIVIAL(info) << "thirdparty_login: thirdparty_login"; - if (j["data"].contains("url")) { - std::string jump_url = j["data"]["url"].get(); - CallAfter([this, jump_url] { - wxString url = wxString::FromUTF8(jump_url); - wxLaunchDefaultBrowser(url); - }); - } - } - else if (strCmd == "new_webpage") { - if (j["data"].contains("url")) { - std::string jump_url = j["data"]["url"].get(); - CallAfter([this, jump_url] { - wxString url = wxString::FromUTF8(jump_url); - wxLaunchDefaultBrowser(url); - }); - } - return; - } - } catch (std::exception &e) { - wxMessageBox(e.what(), "parse json failed", wxICON_WARNING); - Close(); - } -} - -void SMUserLogin::RunScript(const wxString &javascript) -{ - // Remember the script we run in any case, so the next time the user opens - // the "Run Script" dialog box, it is shown there for convenient updating. - m_javascript = javascript; - - if (!m_browser) return; - - WebView::RunScript(m_browser, javascript); -} -#if wxUSE_WEBVIEW_IE -void SMUserLogin::OnRunScriptObjectWithEmulationLevel(wxCommandEvent &WXUNUSED(evt)) -{ - wxWebViewIE::MSWSetModernEmulationLevel(); - RunScript("function f(){var person = new Object();person.name = 'Foo'; \ - person.lastName = 'Bar';return person;}f();"); - wxWebViewIE::MSWSetModernEmulationLevel(false); -} - -void SMUserLogin::OnRunScriptDateWithEmulationLevel(wxCommandEvent &WXUNUSED(evt)) -{ - wxWebViewIE::MSWSetModernEmulationLevel(); - RunScript("function f(){var d = new Date('10/08/2017 21:30:40'); \ - var tzoffset = d.getTimezoneOffset() * 60000; return \ - new Date(d.getTime() - tzoffset);}f();"); - wxWebViewIE::MSWSetModernEmulationLevel(false); -} - -void SMUserLogin::OnRunScriptArrayWithEmulationLevel(wxCommandEvent &WXUNUSED(evt)) -{ - wxWebViewIE::MSWSetModernEmulationLevel(); - RunScript("function f(){ return [\"foo\", \"bar\"]; }f();"); - wxWebViewIE::MSWSetModernEmulationLevel(false); -} -#endif - -/** - * Callback invoked when a loading error occurs - */ -void SMUserLogin::OnError(wxWebViewEvent &event) -{ - auto e = "unknown error"; - switch (event.GetInt()) { - case wxWEBVIEW_NAV_ERR_CONNECTION: e = "wxWEBVIEW_NAV_ERR_CONNECTION"; break; - case wxWEBVIEW_NAV_ERR_CERTIFICATE: e = "wxWEBVIEW_NAV_ERR_CERTIFICATE"; break; - case wxWEBVIEW_NAV_ERR_AUTH: e = "wxWEBVIEW_NAV_ERR_AUTH"; break; - case wxWEBVIEW_NAV_ERR_SECURITY: e = "wxWEBVIEW_NAV_ERR_SECURITY"; break; - case wxWEBVIEW_NAV_ERR_NOT_FOUND: e = "wxWEBVIEW_NAV_ERR_NOT_FOUND"; break; - case wxWEBVIEW_NAV_ERR_REQUEST: e = "wxWEBVIEW_NAV_ERR_REQUEST"; break; - case wxWEBVIEW_NAV_ERR_USER_CANCELLED: e = "wxWEBVIEW_NAV_ERR_USER_CANCELLED"; break; - case wxWEBVIEW_NAV_ERR_OTHER: e = "wxWEBVIEW_NAV_ERR_OTHER"; break; - } - BOOST_LOG_TRIVIAL(fatal) << __FUNCTION__<< boost::format(":SMUserLogin error loading page %1% %2% %3% %4%") % event.GetURL() % event.GetTarget() %e % event.GetString(); - -} - -void SMUserLogin::OnScriptResponseMessage(wxCommandEvent &WXUNUSED(evt)) -{ - // if (!m_response_js.empty()) - //{ - // RunScript(m_response_js); - //} - - // RunScript("This is a message to Web!"); - // RunScript("postMessage(\"AABBCCDD\");"); -} - -bool SMUserLogin::ShowErrorPage() -{ - wxString ErrorUrl = from_u8((boost::filesystem::path(resources_dir()) / "web\\login\\error.html").make_preferred().string()); - wxString strlang = wxGetApp().current_language_code_safe(); - if (strlang != "") - ErrorUrl = wxString::Format("file://%s/web/login/error.html?lang=%s", from_u8(resources_dir()), strlang); - load_url(ErrorUrl); - - return true; -} - -SMAskUserLoginDialog::SMAskUserLoginDialog(wxWindow* parent) - : DPIDialog(parent, wxID_ANY, _L("Log in"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) -{ - const auto background_colour = StateColor::darkModeColorFor(*wxWHITE); - SetBackgroundColour(background_colour); - std::string icon_path = (boost::format("%1%/images/Snapmaker_OrcaTitle.ico") % resources_dir()).str(); - SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO)); - - auto msg_text = new wxStaticText(this, wxID_ANY, _L("Are you sure you want to log in?")); - msg_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour(0x18, 0x18, 0x1b))); - msg_text->SetBackgroundColour(background_colour); - msg_text->SetFont(Label::Body_14); - - auto style_btn = [](Button *b) { - b->SetPaddingSize(b->FromDIP(wxSize(8, 3))); - b->SetMinSize(b->FromDIP(wxSize(96, 30))); - b->SetSize(b->FromDIP(wxSize(96, 30))); - }; - - auto login_btn = new Button(this, _L("Log in")); - login_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); - style_btn(login_btn); - - auto cancel_btn = new Button(this, _L("Cancel")); - cancel_btn->SetStyle(ButtonStyle::Regular, ButtonType::Choice); - style_btn(cancel_btn); - - wxBoxSizer *btn_sizer = new wxBoxSizer(wxHORIZONTAL); - btn_sizer->AddStretchSpacer(1); - btn_sizer->Add(login_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12)); - btn_sizer->Add(cancel_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 0); - - wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL); - main_sizer->AddSpacer(FromDIP(16)); - main_sizer->Add(msg_text, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30)); - main_sizer->AddSpacer(FromDIP(16)); - main_sizer->Add(btn_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(30)); - - login_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_OK); }); - cancel_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_CANCEL); }); - - Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent &e) { - if (e.GetKeyCode() == WXK_RETURN) - EndModal(wxID_OK); - else - e.Skip(); - }); - - SetSizer(main_sizer); - Layout(); - Fit(); - SetSize(wxSize(FromDIP(580), GetSize().y)); - SetMinSize(GetSize()); - Centre(); - wxGetApp().UpdateDlgDarkUI(this); - - m_keepalive_timer = std::make_unique(this, wxID_ANY); - Bind(wxEVT_TIMER, [this](wxTimerEvent &) { - if (m_keepalive_fn) m_keepalive_fn(); - }); - m_keepalive_timer->Start(30000); -} - -SMAskUserLoginDialog::~SMAskUserLoginDialog() -{ - if (m_keepalive_timer) - m_keepalive_timer->Stop(); -} - -void SMAskUserLoginDialog::SetKeepAliveCallback(std::function fn) -{ - m_keepalive_fn = std::move(fn); -} - -}} // namespace Slic3r::GUI - diff --git a/src/slic3r/GUI/WebSMUserLoginDialog.hpp b/src/slic3r/GUI/WebSMUserLoginDialog.hpp deleted file mode 100644 index c8c94b98aece..000000000000 --- a/src/slic3r/GUI/WebSMUserLoginDialog.hpp +++ /dev/null @@ -1,124 +0,0 @@ -#pragma once -#ifndef slic3r_SMWebUserLogin_HEAD_ -#define slic3r_SMWebUserLogin_HEAD_ - -#include "wx/artprov.h" -#include "wx/cmdline.h" -#include "wx/notifmsg.h" -#include "wx/settings.h" -#include "wx/webview.h" - -#if wxUSE_WEBVIEW_IE -#include "wx/msw/webview_ie.h" -#endif -#if wxUSE_WEBVIEW_EDGE -#include "wx/msw/webview_edge.h" -#endif - -#include "wx/webviewarchivehandler.h" -#include "wx/webviewfshandler.h" -#include "wx/numdlg.h" -#include "wx/infobar.h" -#include "wx/filesys.h" -#include "wx/fs_arc.h" -#include "wx/fs_mem.h" -#include "wx/stdpaths.h" -#include -#include "wx/timer.h" -#include -#include "wx/textctrl.h" - -#include -#include - -#include "GUI_Utils.hpp" - -namespace Slic3r { namespace GUI { - -class SMUserLogin : public wxDialog -{ -public: - SMUserLogin(bool isLogout = false); - virtual ~SMUserLogin(); - - void load_url(wxString &url); - - std::string w2s(wxString sSrc); - - void UpdateState(); - void OnIdle(wxIdleEvent &evt); - // void OnClose(wxCloseEvent &evt); - - void OnNavigationRequest(wxWebViewEvent &evt); - void OnNavigationComplete(wxWebViewEvent &evt); - void OnDocumentLoaded(wxWebViewEvent &evt); - void OnNewWindow(wxWebViewEvent &evt); - void OnError(wxWebViewEvent &evt); - void OnTitleChanged(wxWebViewEvent &evt); - void OnFullScreenChanged(wxWebViewEvent &evt); - void OnScriptMessage(wxWebViewEvent &evt); - - void OnScriptResponseMessage(wxCommandEvent &evt); - void RunScript(const wxString &javascript); - void try_complete_oauth_callback(); - - bool m_networkOk; - bool ShowErrorPage(); - - bool run(); - - static int web_sequence_id; -private: - wxTimer *m_timer { nullptr }; - void OnTimer(wxTimerEvent &event); - void OnCallbackPollTimer(wxTimerEvent &event); - - wxTimer *m_callback_timer { nullptr }; - bool m_callback_handled = false; - int m_callback_attempts = 0; - -private: - - wxString TargetUrl = "https://id.snapmaker.cn?from=orca"; - wxString LogoutUrl = "https://id.snapmaker.cn/logout?from=orca"; - wxString m_hostUrl = "https://id.snapmaker.cn"; - wxString m_accountUrl = "https://api.snapmaker.cn"; - wxString m_userInfoUrl = "https://api.snapmaker.cn/api/common/accounts/current"; - wxString m_home_url = "https://www.snapmaker.cn/"; - wxWebView *m_browser; - - std::string m_AutotestToken; - -#if wxUSE_WEBVIEW_IE - wxMenuItem *m_script_object_el; - wxMenuItem *m_script_date_el; - wxMenuItem *m_script_array_el; -#endif - // Last executed JavaScript snippet, for convenience. - wxString m_javascript; - wxString m_response_js; - - wxString m_sm_user_agent; - - DECLARE_EVENT_TABLE() -}; - -class SMAskUserLoginDialog : public DPIDialog -{ -public: - SMAskUserLoginDialog(wxWindow *parent); - ~SMAskUserLoginDialog(); - - void SetKeepAliveCallback(std::function fn); - -protected: - void on_dpi_changed(const wxRect &suggested_rect) override {} - -private: - std::unique_ptr m_keepalive_timer; - std::function m_keepalive_fn; -}; - -}} // namespace Slic3r::GUI - -#endif diff --git a/src/slic3r/GUI/WebSocketDebugServer.cpp b/src/slic3r/GUI/WebSocketDebugServer.cpp deleted file mode 100644 index 68be7934d996..000000000000 --- a/src/slic3r/GUI/WebSocketDebugServer.cpp +++ /dev/null @@ -1,253 +0,0 @@ -// WebSocket Debug Server implementation -#include "WebSocketDebugServer.hpp" -#include -#include - -namespace Slic3r { namespace GUI { - -WebSocketDebugServer::WebSocketDebugServer(unsigned short port) - : m_port(port) - , m_running(false) - , m_has_client(false) -{ - BOOST_LOG_TRIVIAL(info) << "WebSocketDebugServer created on port " << m_port; -} - -WebSocketDebugServer::~WebSocketDebugServer() -{ - stop(); -} - -bool WebSocketDebugServer::start() -{ - if (m_running.load()) { - BOOST_LOG_TRIVIAL(warning) << "WebSocket Debug Server already running"; - return true; - } - - m_io_context = std::make_unique(); - - tcp::endpoint endpoint(tcp::v4(), m_port); - m_acceptor = std::make_unique(*m_io_context, endpoint); - - m_running.store(true); - - // Start accept thread - m_accept_thread = std::thread(&WebSocketDebugServer::accept_loop, this); - - // Start send worker thread - m_send_thread = std::thread(&WebSocketDebugServer::send_worker, this); - - BOOST_LOG_TRIVIAL(info) << " WebSocket Debug Server started on ws://localhost:" << m_port; - BOOST_LOG_TRIVIAL(info) << " Waiting for Flutter Web client to connect..."; - return true; - -} - -void WebSocketDebugServer::stop() -{ - if (!m_running.load()) - return; - - BOOST_LOG_TRIVIAL(info) << "Stopping WebSocket Debug Server..."; - m_running.store(false); - m_send_cv.notify_all(); - - if (m_acceptor && m_acceptor->is_open()) - { - boost::system::error_code ec; - m_acceptor->close(ec); - - if (ec) - BOOST_LOG_TRIVIAL(warning) << "Error closing acceptor: " << ec.message(); - } - - // Close WebSocket connection - if (m_ws_stream) - { - boost::system::error_code ec; - m_ws_stream->close(websocket::close_code::normal, ec); - - if (ec) - BOOST_LOG_TRIVIAL(warning) << "Error closing WebSocket: " << ec.message(); - } - - // Stop io_context - if (m_io_context) - m_io_context->stop(); - - // Join threads - if (m_accept_thread.joinable()) - m_accept_thread.join(); - - if (m_send_thread.joinable()) - m_send_thread.join(); - - for (auto& t : m_session_threads) - if (t.joinable()) t.join(); - - m_session_threads.clear(); - m_has_client.store(false); - - BOOST_LOG_TRIVIAL(info) << "WebSocket Debug Server stopped"; -} - -void WebSocketDebugServer::accept_loop() -{ - while (m_running.load()) - { - tcp::socket socket(*m_io_context); - - // Accept connection (blocking) - boost::system::error_code ec; - m_acceptor->accept(socket, ec); - - if (ec) - { - - if (m_running.load()) - BOOST_LOG_TRIVIAL(error) << "Accept error: " << ec.message(); - - continue; - } - - BOOST_LOG_TRIVIAL(info) << "Flutter Web client connected from " - << socket.remote_endpoint().address().to_string(); - - // Spawn a thread per session so accept_loop stays unblocked - m_session_threads.emplace_back(&WebSocketDebugServer::session_loop, this, std::move(socket)); - } -} - -void WebSocketDebugServer::session_loop(tcp::socket socket) -{ - // Create WebSocket stream - auto ws = std::make_shared>(std::move(socket)); - - // Set WebSocket options - ws->set_option(websocket::stream_base::decorator( - [](websocket::response_type& res) { - res.set(beast::http::field::server, "OrcaSlicer-Debug-Server"); - } - )); - - // Accept WebSocket handshake - ws->accept(); - - // Swap in the new stream under the lock, then close old streams outside - // the lock so send_worker is never blocked waiting for TCP teardown. - std::vector>> to_close; - { - std::lock_guard lock(m_client_mutex); - to_close = std::move(m_old_streams); - if (m_ws_stream) { - to_close.push_back(m_ws_stream); - } - m_ws_stream = ws; - m_has_client.store(true); - } - - for (auto& old : to_close) { - if (old && old->is_open()) { - boost::system::error_code ec; - old->close(websocket::close_code::going_away, ec); - } - } - - BOOST_LOG_TRIVIAL(info) << " WebSocket handshake completed, client ready"; - - // Message receive loop - while (m_running.load()) { - beast::flat_buffer buffer; - - boost::system::error_code ec; - ws->read(buffer, ec); - - if (ec == websocket::error::closed) { - BOOST_LOG_TRIVIAL(info) << "Client closed connection"; - break; - } - - if (ec) { - BOOST_LOG_TRIVIAL(error) << "Read error: " << ec.message(); - break; - } - - std::string message = beast::buffers_to_string(buffer.data()); - - BOOST_LOG_TRIVIAL(debug) << "Received from Flutter: " << message.substr(0, 200) - << (message.length() > 200 ? "..." : ""); - - // Call message callback - if (m_message_callback) - m_message_callback(message); - } - - // Clean up - { - std::lock_guard lock(m_client_mutex); - if (m_ws_stream == ws) { - m_ws_stream.reset(); - m_has_client.store(false); - } - } - - BOOST_LOG_TRIVIAL(info) << " Flutter Web client disconnected"; -} - -void WebSocketDebugServer::send_worker() -{ - while (m_running.load()) { - std::string message; - - { - std::unique_lock lock(m_send_mutex); - m_send_cv.wait(lock, [this] { - return !m_send_queue.empty() || !m_running.load(); - }); - - if (!m_running.load()) break; - - message = m_send_queue.front(); - m_send_queue.pop(); - } - - if (!message.empty()) { - std::lock_guard lock(m_client_mutex); - if (m_ws_stream && m_has_client.load()) - { - - boost::system::error_code ec; - m_ws_stream->write(net::buffer(message), ec); - - if (ec) - BOOST_LOG_TRIVIAL(error) << "Send error: " << ec.message(); - else - BOOST_LOG_TRIVIAL(debug) << "Sent to Flutter: " << message.substr(0, 200) - << (message.length() > 200 ? "..." : ""); - - } - } - } -} - -void WebSocketDebugServer::send_message(const std::string& message) -{ - if (!m_running.load()) { - BOOST_LOG_TRIVIAL(warning) << "Cannot send message: server not running"; - return; - } - - { - std::lock_guard lock(m_send_mutex); - m_send_queue.push(message); - } - m_send_cv.notify_one(); -} - -void WebSocketDebugServer::set_message_callback(MessageCallback callback) -{ - m_message_callback = callback; -} - -}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/WebSocketDebugServer.hpp b/src/slic3r/GUI/WebSocketDebugServer.hpp deleted file mode 100644 index 78c9135f0a3c..000000000000 --- a/src/slic3r/GUI/WebSocketDebugServer.hpp +++ /dev/null @@ -1,76 +0,0 @@ -// WebSocket Debug Server for Flutter Web debugging -#ifndef WEBSOCKET_DEBUG_SERVER_HPP -#define WEBSOCKET_DEBUG_SERVER_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace beast = boost::beast; -namespace websocket = beast::websocket; -namespace net = boost::asio; -using tcp = boost::asio::ip::tcp; - -namespace Slic3r { namespace GUI { - -class WebSocketDebugServer { -public: - using MessageCallback = std::function; - - explicit WebSocketDebugServer(unsigned short port = 8766); - ~WebSocketDebugServer(); - - // Start the WebSocket server - bool start(); - - // Stop the WebSocket server - void stop(); - - // Send message to Flutter Web client - void send_message(const std::string& message); - - // Set callback for receiving messages - void set_message_callback(MessageCallback callback); - - // Check if a client is connected - bool has_client() const { return m_has_client.load(); } - - // Check if server is running - bool is_running() const { return m_running.load(); } - -private: - void accept_loop(); - void session_loop(tcp::socket socket); - void send_worker(); - - unsigned short m_port; - std::atomic m_running; - std::atomic m_has_client; - - std::unique_ptr m_io_context; - std::unique_ptr m_acceptor; - std::thread m_accept_thread; - std::thread m_send_thread; - std::vector m_session_threads; - - std::shared_ptr> m_ws_stream; - std::vector>> m_old_streams; - MessageCallback m_message_callback; - - std::mutex m_send_mutex; - std::condition_variable m_send_cv; - std::queue m_send_queue; - std::mutex m_client_mutex; -}; - -}} // namespace Slic3r::GUI - -#endif // WEBSOCKET_DEBUG_SERVER_HPP diff --git a/src/slic3r/GUI/WebViewDialog.cpp b/src/slic3r/GUI/WebViewDialog.cpp index f025ae5aa9c3..5f99b4e6d5e8 100644 --- a/src/slic3r/GUI/WebViewDialog.cpp +++ b/src/slic3r/GUI/WebViewDialog.cpp @@ -37,13 +37,9 @@ namespace GUI { WebViewPanel::WebViewPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize) { - wxString url = wxString::FromUTF8(LOCALHOST_URL + std::to_string(wxGetApp().get_page_http_port()) + - "/web/flutter_web/index.html?path=0"); + wxString url = wxGetApp().gateway_web_url("home_page"); // wxString url = wxString::Format("file://%s/web/homepage/index.html?path=homepage.html", from_u8(resources_dir())); // wxString url = wxString("http://127.0.0.1:") + wxString(std::to_string(PAGE_HTTP_PORT)) + wxString("/web/flutter_web/index.html?path=1"); - url = wxGetApp().get_international_url(url); - wxGetApp().start_flutter_wcp_timeout_watch(); - // test // url = "http://localhost:13619/web/flutter_web/1.html"; @@ -255,7 +251,7 @@ void WebViewPanel::reload() { void WebViewPanel::load_url(wxString& url) { - m_browser->LoadURL(url); + WebView::LoadUrl(m_browser, url); wxGetApp().fltviews().add_webview_panel(this, url); @@ -429,9 +425,6 @@ void WebViewPanel::OnClose(wxCloseEvent& evt) void WebViewPanel::OnFreshLoginStatus(wxTimerEvent &event) { - /*auto mainframe = Slic3r::GUI::wxGetApp().mainframe; - if (mainframe && mainframe->m_webview == this) - Slic3r::GUI::wxGetApp().sm_get_login_info();*/ } void WebViewPanel::SetLoginPanelVisibility(bool bshow) @@ -663,7 +656,6 @@ void WebViewPanel::OnScriptMessage(wxWebViewEvent& evt) // update login status if (m_LoginUpdateTimer == nullptr) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Create Timer"; - wxGetApp().on_flutter_wcp_received(); m_LoginUpdateTimer = new wxTimer(this, LOGIN_INFO_UPDATE_TIMER_ID); m_LoginUpdateTimer->Start(2000); } diff --git a/src/slic3r/GUI/Widgets/StateColor.cpp b/src/slic3r/GUI/Widgets/StateColor.cpp index ff48ffd3003d..3b312eaba7ee 100644 --- a/src/slic3r/GUI/Widgets/StateColor.cpp +++ b/src/slic3r/GUI/Widgets/StateColor.cpp @@ -51,7 +51,6 @@ static std::map gDarkColors{ {"#F0F0F0", "#3F3F46"}, // rgb(240, 240, 240) Card borders, dividers {"#F3F4F6", "#3A3A3F"}, // rgb(243, 244, 246) Internal card dividers {"#242424", "#E4E4E7"}, // rgb(36, 36, 36) Primary text (hex input, cancel btn) - {"#18181B", "#E4E4E7"}, // rgb(24, 24, 27) Login confirmation text {"#4A4A4A", "#A1A1AA"}, // rgb(74, 74, 74) Secondary text (segmented btns) {"#8F8F8F", "#8A8A95"}, // rgb(143, 143, 143) Label text (Hex:, Preview, percentages) {"#EBEBEB", "#45454B"}, // rgb(235, 235, 235) Slider track background diff --git a/src/slic3r/GUI/Widgets/WebView.cpp b/src/slic3r/GUI/Widgets/WebView.cpp index a821ac30eca3..852bb1989778 100644 --- a/src/slic3r/GUI/Widgets/WebView.cpp +++ b/src/slic3r/GUI/Widgets/WebView.cpp @@ -24,6 +24,7 @@ #endif #ifdef __WIN32__ +#include #include #include #include @@ -105,6 +106,17 @@ DWORD DownloadAndInstallWV2RT() { return returnCode; } +static wxString PreserveWebViewEngineUserAgent(const wxString &requestedUserAgent, const wxString ¤tUserAgent) +{ + const int requestedMarker = requestedUserAgent.Find(" Mozilla/5.0"); + if (!requestedUserAgent.StartsWith("SM-Slicer/") || requestedMarker == wxNOT_FOUND) + return requestedUserAgent; + + const int currentMarker = currentUserAgent.Find(" Mozilla/5.0"); + const wxString engineUserAgent = currentMarker == wxNOT_FOUND ? currentUserAgent : currentUserAgent.Mid(currentMarker + 1); + return requestedUserAgent.Left(requestedMarker) + " " + engineUserAgent; +} + class WebViewEdge : public wxWebViewEdge { public: @@ -121,10 +133,17 @@ class WebViewEdge : public wxWebViewEdge ICoreWebView2Settings2 *settings2; hr = settings->QueryInterface(&settings2); if (hr == S_OK) { - settings2->put_UserAgent(userAgent.wc_str()); + wchar_t *currentUserAgent = nullptr; + wxString userAgentToSet = userAgent; + if (settings2->get_UserAgent(¤tUserAgent) == S_OK && currentUserAgent) { + userAgentToSet = PreserveWebViewEngineUserAgent(userAgent, currentUserAgent); + CoTaskMemFree(currentUserAgent); + } + hr = settings2->put_UserAgent(userAgentToSet.wc_str()); settings2->Release(); - return true; } + settings->Release(); + return hr == S_OK; } settings->Release(); return false; @@ -371,7 +390,8 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url) url2.Replace("\\", "/"); #endif if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); } - BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8(); + if (!url2.empty()) + BOOST_LOG_TRIVIAL(info) << "webview create url=" << url2.ToUTF8().data(); #ifdef __WIN32__ wxWebView* webView = new WebViewEdge; @@ -485,7 +505,8 @@ void WebView::LoadUrl(wxWebView * webView, wxString const &url) url2.Replace("\\", "/"); #endif if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); } - BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << url2.ToUTF8(); + if (!url2.empty()) + BOOST_LOG_TRIVIAL(info) << "webview load url=" << url2.ToUTF8().data(); webView->LoadURL(url2); } diff --git a/src/slic3r/Utils/ConnectionProcessManager.cpp b/src/slic3r/Utils/ConnectionProcessManager.cpp new file mode 100644 index 000000000000..382cfc6c42e0 --- /dev/null +++ b/src/slic3r/Utils/ConnectionProcessManager.cpp @@ -0,0 +1,291 @@ +#include "ConnectionProcessManager.hpp" + +#undef pid_t + +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +#include +#include +#include +#include +#include +#include + +namespace Slic3r { namespace Gateway { +namespace { + +bool is_valid_locale(const std::string& locale) +{ + if (locale.empty() || locale.size() > 32 || locale.front() == '-') + return false; + + return std::all_of(locale.begin(), locale.end(), [](unsigned char ch) { return std::isalnum(ch) != 0 || ch == '-' || ch == '_'; }); +} + +bool starts_line_at(const std::string& data, std::size_t position) { return position == 0 || data[position - 1] == '\n'; } + +std::size_t find_next_line_port(const std::string& data, std::size_t from) +{ + std::size_t position = data.find("PORT:", from); + while (position != std::string::npos && !starts_line_at(data, position)) + position = data.find("PORT:", position + 1); + return position; +} + +std::string format_arguments(const std::vector& arguments) +{ + std::string formatted; + for (const std::string& argument : arguments) { + if (!formatted.empty()) + formatted += " "; + formatted += argument; + } + return formatted; +} + +} // namespace + +struct ConnectionProcessManager::ChildProcessTracker +{ + // Detached children survive the runner's local child object, so the manager keeps the + // handles here until the next terminate() call reaps them. + std::mutex mutex; + std::vector> children; + + void track(std::shared_ptr child) + { + std::lock_guard lock(mutex); + children.push_back(std::move(child)); + } + + void terminate_all() + { + std::lock_guard lock(mutex); + for (const auto& child : children) { + std::error_code terminate_error; + child->terminate(terminate_error); + std::error_code wait_error; + child->wait(wait_error); + } + children.clear(); + } +}; + +ConnectionProcessManager::ConnectionProcessManager(Config config, ProcessRunner runner) + : config_(std::move(config)), runner_(std::move(runner)), tracker_(std::make_shared()) +{ + if (!runner_) + runner_ = default_runner(config_, tracker_); +} + +void ConnectionProcessManager::terminate() { tracker_->terminate_all(); } + +std::vector ConnectionProcessManager::build_arguments(const std::string& locale) { return {"--locale=" + locale, "--orca"}; } + +std::string_view ConnectionProcessManager::cli_executable_name() +{ +#if defined(_WIN32) +#if defined(_M_X64) || defined(__x86_64__) + return "snapmaker_control_windows_x64.exe"; +#elif defined(_M_ARM64) || defined(__aarch64__) + return "snapmaker_control_windows_arm64.exe"; +#else +#error "unsupported Windows architecture for the snapmaker_control CLI" +#endif +#elif defined(__APPLE__) +#if defined(__x86_64__) + return "snapmaker_control_macos_x64"; +#elif defined(__arm64__) || defined(__aarch64__) + return "snapmaker_control_macos_arm64"; +#else +#error "unsupported macOS architecture for the snapmaker_control CLI" +#endif +#elif defined(__linux__) +#if defined(__x86_64__) + return "snapmaker_control_linux_x64"; +#elif defined(__aarch64__) || defined(__arm64__) + return "snapmaker_control_linux_arm64"; +#else +#error "unsupported Linux architecture for the snapmaker_control CLI" +#endif +#else +#error "unsupported platform for the snapmaker_control CLI" +#endif +} + +ProcessDiscoveryError ConnectionProcessManager::parse_port_frame(const std::string& output, std::uint16_t& port) +{ + constexpr std::string_view prefix{"PORT:"}; + const std::size_t frame_start = find_next_line_port(output, 0); + if (frame_start == std::string::npos) + return ProcessDiscoveryError::NoPortFrame; + + const std::size_t value_start = frame_start + prefix.size(); + const std::size_t terminator = output.find("\r\n\r\n", value_start); + if (terminator == std::string::npos) + return ProcessDiscoveryError::InvalidPortFrame; + + const std::string value = output.substr(value_start, terminator - value_start); + if (value.empty() || value.find_first_of("\r\n") != std::string::npos || + !std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isdigit(ch) != 0; })) + return ProcessDiscoveryError::InvalidPortFrame; + + unsigned long parsed = 0; + for (const char digit : value) { + parsed = parsed * 10 + static_cast(digit - '0'); + if (parsed > 65535) + return ProcessDiscoveryError::InvalidPortFrame; + } + if (parsed == 0) + return ProcessDiscoveryError::InvalidPortFrame; + port = static_cast(parsed); + + if (find_next_line_port(output, terminator + 4) != std::string::npos) + return ProcessDiscoveryError::MultiplePortFrames; + + return ProcessDiscoveryError::None; +} + +ConnectionProcessManager::DiscoveryResult ConnectionProcessManager::discover_port(const std::string& locale) +{ + DiscoveryResult result; + result.arguments = build_arguments(locale); + + if (config_.executable.empty()) { + result.error = ProcessDiscoveryError::InvalidExecutable; + result.message = "connection cli path is empty"; + return result; + } + if (!is_valid_locale(locale)) { + result.error = ProcessDiscoveryError::InvalidLocale; + result.message = "invalid locale: " + locale; + return result; + } + + ProcessRunResult process_result; + try { + process_result = runner_(result.arguments); + } catch (const std::exception& exception) { + result.error = ProcessDiscoveryError::LaunchFailed; + result.message = exception.what(); + return result; + } + if (process_result.launch_failed) { + result.error = ProcessDiscoveryError::LaunchFailed; + result.message = process_result.error.empty() ? "failed to launch " + config_.executable.filename().string() : process_result.error; + return result; + } + if (process_result.timed_out) { + result.error = ProcessDiscoveryError::Timeout; + result.message = "timed out waiting for PORT frame"; + return result; + } + if (!process_result.error.empty()) { + result.error = ProcessDiscoveryError::LaunchFailed; + result.message = process_result.error; + return result; + } + + const ProcessDiscoveryError parse_error = parse_port_frame(process_result.stdout_data, result.port); + result.error = parse_error; + switch (parse_error) { + case ProcessDiscoveryError::NoPortFrame: + result.message = "PORT frame was not found in " + config_.executable.filename().string() + " stdout"; + break; + case ProcessDiscoveryError::InvalidPortFrame: result.message = "PORT frame is malformed"; break; + case ProcessDiscoveryError::MultiplePortFrames: result.message = "multiple PORT frames were found"; break; + case ProcessDiscoveryError::None: break; + default: break; + } + return result; +} + +ConnectionProcessManager::ProcessRunner ConnectionProcessManager::default_runner(Config config, + const std::shared_ptr& tracker) +{ + return [config = std::move(config), tracker](const std::vector& arguments) -> ProcessRunResult { + ProcessRunResult result; + boost::asio::io_context io_context; + boost::process::async_pipe stdout_pipe(io_context); + boost::process::child child; + std::error_code launch_error; + + BOOST_LOG_TRIVIAL(info) << "connection cli launch: executable=" << config.executable + << ", arguments=" << format_arguments(arguments); + + try { +#ifdef _WIN32 + child = boost::process::child(config.executable, boost::process::args = arguments, boost::process::std_out > stdout_pipe, + boost::process::std_err > boost::process::null, boost::process::windows::create_no_window, + launch_error); +#else + child = boost::process::child(config.executable, boost::process::args = arguments, boost::process::std_out > stdout_pipe, + boost::process::std_err > boost::process::null, launch_error); +#endif + } catch (const std::exception& exception) { + result.launch_failed = true; + result.error = exception.what(); + return result; + } + + if (launch_error) { + result.launch_failed = true; + result.error = launch_error.message(); + boost::system::error_code pipe_error; + stdout_pipe.close(pipe_error); + return result; + } + + boost::asio::streambuf buffer; + boost::system::error_code read_error; + std::size_t bytes_read = 0; + bool delimiter_found = false; + boost::asio::async_read_until(stdout_pipe, buffer, "\r\n\r\n", [&](const boost::system::error_code& error, std::size_t bytes) { + read_error = error; + bytes_read = bytes; + delimiter_found = error == boost::system::errc::success; + }); + + const auto processed = io_context.run_for(config.timeout); + (void) processed; + if (!delimiter_found) { + result.timed_out = true; + std::error_code terminate_error; + child.terminate(terminate_error); + std::error_code wait_error; + child.wait(wait_error); + boost::system::error_code pipe_error; + stdout_pipe.close(pipe_error); + return result; + } + + if (bytes_read > config.max_stdout_bytes) { + result.launch_failed = true; + result.error = "PORT frame exceeds the stdout size limit"; + std::error_code terminate_error; + child.terminate(terminate_error); + std::error_code wait_error; + child.wait(wait_error); + return result; + } + + const auto* data = boost::asio::buffer_cast(buffer.data()); + result.stdout_data.assign(data, bytes_read); + auto tracked_child = std::make_shared(std::move(child)); + tracked_child->detach(); + if (tracker) + tracker->track(std::move(tracked_child)); + boost::system::error_code pipe_error; + stdout_pipe.close(pipe_error); + return result; + }; +} + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/ConnectionProcessManager.hpp b/src/slic3r/Utils/ConnectionProcessManager.hpp new file mode 100644 index 000000000000..be23ab9f56c2 --- /dev/null +++ b/src/slic3r/Utils/ConnectionProcessManager.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { namespace Gateway { + +enum class ProcessDiscoveryError { + None, + InvalidExecutable, + InvalidLocale, + LaunchFailed, + Timeout, + NoPortFrame, + InvalidPortFrame, + MultiplePortFrames, +}; + +class ConnectionProcessManager +{ +public: + struct Config + { + boost::filesystem::path executable; + std::chrono::milliseconds timeout{std::chrono::milliseconds{5000}}; + std::size_t max_stdout_bytes{64 * 1024}; + }; + + struct ProcessRunResult + { + bool launch_failed{false}; + bool timed_out{false}; + std::string stdout_data; + std::string error; + }; + + struct DiscoveryResult + { + ProcessDiscoveryError error{ProcessDiscoveryError::None}; + std::uint16_t port{0}; + std::string message; + std::vector arguments; + }; + + using ProcessRunner = std::function& arguments)>; + + ConnectionProcessManager(Config config, ProcessRunner runner = {}); + virtual ~ConnectionProcessManager() = default; + + DiscoveryResult discover_port(const std::string& locale); + + // Force-terminates the CLI process spawned by the most recent discover_port() call + // (TerminateProcess on Windows, SIGKILL elsewhere). No-op when the injected runner is + // used or no process has been spawned yet. + virtual void terminate(); + + const boost::filesystem::path& executable() const { return config_.executable; } + const Config& config() const { return config_; } + + static ProcessDiscoveryError parse_port_frame(const std::string& output, std::uint16_t& port); + static std::vector build_arguments(const std::string& locale); + // File name of the snapmaker_connection CLI binary expected next to the resources directory + // for the current platform and architecture. The binary itself is placed locally and not tracked in git. + static std::string_view cli_executable_name(); + +private: + struct ChildProcessTracker; + + static ProcessRunner default_runner(Config config, const std::shared_ptr& tracker); + + Config config_; + ProcessRunner runner_; + std::shared_ptr tracker_; +}; + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/GatewayAccount.cpp b/src/slic3r/Utils/GatewayAccount.cpp new file mode 100644 index 000000000000..0070f7c841e3 --- /dev/null +++ b/src/slic3r/Utils/GatewayAccount.cpp @@ -0,0 +1,97 @@ +#include "GatewayAccount.hpp" + +#include +#include + +namespace Slic3r { namespace Gateway { + +namespace { + +std::string get_string(const nlohmann::json& frame, std::initializer_list keys) +{ + for (const char* key : keys) { + const auto it = frame.find(key); + if (it != frame.end() && it->is_string() && !it->get().empty()) + return it->get(); + } + return std::string{}; +} + +std::optional get_bool(const nlohmann::json& frame, std::initializer_list keys) +{ + for (const char* key : keys) { + const auto it = frame.find(key); + if (it != frame.end() && it->is_boolean()) + return it->get(); + } + return std::nullopt; +} + +// The wrapped /api/account shape carries the cloud profile, where the user id is a JSON number. +std::string get_string_or_number(const nlohmann::json& frame, std::initializer_list keys) +{ + for (const char* key : keys) { + const auto it = frame.find(key); + if (it == frame.end()) + continue; + if (it->is_string() && !it->get().empty()) + return it->get(); + if (it->is_number_integer()) + return std::to_string(it->get()); + } + return std::string{}; +} + +std::optional wrapped_code_succeeded(const nlohmann::json& frame) +{ + const auto it = frame.find("code"); + if (it == frame.end() || !it->is_number_integer()) + return std::nullopt; + const std::int64_t code = it->get(); + return code == 0 || code == 200; +} + +bool status_is_online(const nlohmann::json& frame) +{ + for (const char* key : {"status", "stauts"}) { + const auto it = frame.find(key); + if (it != frame.end() && it->is_string()) + return it->get() == "online"; + } + return false; +} + +} // namespace + +bool GatewayAccount::parse(const nlohmann::json& frame, AccountSnapshot& out) +{ + if (!frame.is_object()) + return false; + + // Legacy CLI builds answer /api/account with {code, data:{...cloud profile}} + // instead of the documented flat frame. Unwrap it first. + const auto wrapped_data = frame.find("data"); + if (wrapped_data != frame.end() && wrapped_data->is_object()) { + AccountSnapshot snapshot; + snapshot.token = get_string(*wrapped_data, {"accessToken", "access_token", "token"}); + snapshot.userid = get_string_or_number(*wrapped_data, {"userid", "uid", "id"}); + snapshot.nickname = get_string(*wrapped_data, {"nickname"}); + snapshot.account = get_string(*wrapped_data, {"account"}); + snapshot.email = get_string(*wrapped_data, {"email"}); + snapshot.is_login = wrapped_code_succeeded(frame).value_or(true) && (!snapshot.token.empty() || !snapshot.userid.empty()); + out = std::move(snapshot); + return true; + } + + AccountSnapshot snapshot; + snapshot.is_login = get_bool(frame, {"is_login", "isLogin"}).value_or(status_is_online(frame)); + snapshot.userid = get_string(frame, {"userid", "id", "uid"}); + snapshot.token = get_string(frame, {"token", "access_token", "accessToken"}); + snapshot.nickname = get_string(frame, {"nickname"}); + snapshot.account = get_string(frame, {"account"}); + snapshot.email = get_string(frame, {"email"}); + out = std::move(snapshot); + return true; +} + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/GatewayAccount.hpp b/src/slic3r/Utils/GatewayAccount.hpp new file mode 100644 index 000000000000..3e93304afc09 --- /dev/null +++ b/src/slic3r/Utils/GatewayAccount.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +namespace Slic3r { namespace Gateway { + +// Login state as owned by the snapmaker_connection CLI. The CLI is the single +// source of truth; the C++ side only mirrors the frames below and never issues +// login or logout requests. +struct AccountSnapshot +{ + bool is_login{false}; + std::string userid; + std::string nickname; + std::string account; + std::string email; + std::string token; +}; + +// Account-facing parsing for the connection gateway. Both the cold-start +// GET /api/account response and the notify.account.changed params use the same +// flat frame with alias fields from different client generations. +class GatewayAccount +{ +public: + // frame: /api/account response body or notify.account.changed params. + // Accepts both the documented flat frame and the legacy {code, data} + // wrapper emitted by current CLI builds. Returns false when the frame is + // not a JSON object; individual fields fall back to their aliases and then + // to safe defaults. + static bool parse(const nlohmann::json& frame, AccountSnapshot& out); +}; + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/GatewayDevice.cpp b/src/slic3r/Utils/GatewayDevice.cpp new file mode 100644 index 000000000000..0937f78d6958 --- /dev/null +++ b/src/slic3r/Utils/GatewayDevice.cpp @@ -0,0 +1,323 @@ +#include "GatewayDevice.hpp" +#include "GatewayProtocol.hpp" +#include "GatewayService.hpp" + +#include "libslic3r/FilamentColorLibrary.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/SSWCPProtocol.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { namespace Gateway { + +namespace { + +const nlohmann::json* find_object(const nlohmann::json& parent, const char* key) +{ + const auto it = parent.find(key); + return it != parent.end() && it->is_object() ? &*it : nullptr; +} + +std::string get_string(const nlohmann::json& parent, const char* key) +{ + const auto it = parent.find(key); + return it != parent.end() && it->is_string() ? it->get() : std::string{}; +} + +// Normalize a nozzle diameter into the "0.2"/"0.4"/"0.6"/"0.8" preset labels used by the UI. +// Diameters outside the supported preset catalog are dropped on purpose — same mapping as the +// legacy SSWCP::query_machine_info behavior. +void append_nozzle(const nlohmann::json& value, std::vector& out_nozzle_diameters) +{ + double diameter = 0.0; + if (value.is_number()) + diameter = value.get(); + else if (value.is_string()) + diameter = std::atof(value.get().c_str()); + if (std::fabs(diameter - 0.2) < 1e-6) + out_nozzle_diameters.push_back("0.2"); + else if (std::fabs(diameter - 0.4) < 1e-6) + out_nozzle_diameters.push_back("0.4"); + else if (std::fabs(diameter - 0.6) < 1e-6) + out_nozzle_diameters.push_back("0.6"); + else if (std::fabs(diameter - 0.8) < 1e-6) + out_nozzle_diameters.push_back("0.8"); +} + +} // namespace + +bool GatewayDevice::query_machine_info(const std::shared_ptr& gateway, + std::string& out_model, + std::vector& out_nozzle_diameters, + std::vector& out_nozzle_volume_types, + std::string& device_name) +{ + if (gateway == nullptr) + return false; + + // GET /api/cache/all is the gateway's read-only full snapshot of the current device, so the + // firmware passthrough machine.system_info is no longer needed for these fields. + const GatewayService::ApiResult result = gateway->get_device(); + if (result.error) { + // NotConnected (gateway process not ready yet) is an expected answer; anything else is worth a log line. + if (result.error.code != GatewayErrorCode::NotConnected) + BOOST_LOG_TRIVIAL(warning) << "GatewayDevice::query_machine_info: GET /api/cache/all failed: " << result.error.message; + return false; + } + + const nlohmann::json* data = find_object(result.value, "data"); + const nlohmann::json* info = data == nullptr ? nullptr : find_object(*data, "info"); + const nlohmann::json* product = info == nullptr ? nullptr : find_object(*info, "product"); + const nlohmann::json* identity = data == nullptr ? nullptr : find_object(*data, "identity"); + + std::string model = product == nullptr ? std::string{} : get_string(*product, "machine_type"); + if (model.empty() && identity != nullptr) + model = get_string(*identity, "productName"); + std::vector nozzles; + std::string name = identity == nullptr ? std::string{} : get_string(*identity, "name"); + if (name.empty() && identity != nullptr) + name = get_string(*identity, "device_name"); + if (name.empty() && product != nullptr) + name = get_string(*product, "device_name"); + if (product != nullptr && product->contains("nozzle_diameter")) { + const nlohmann::json& nozzle_json = (*product)["nozzle_diameter"]; + if (nozzle_json.is_array()) { + for (const auto& nozzle : nozzle_json) + append_nozzle(nozzle, nozzles); + } else if (!nozzle_json.is_null()) { + append_nozzle(nozzle_json, nozzles); + } + } + + // nozzle_volume_type lives on the extruder objects in data.objects, not in data.info.product. + std::vector flows; + const nlohmann::json* objects = data == nullptr ? nullptr : find_object(*data, "objects"); + if (objects == nullptr || objects->empty()) { + // Preserve the legacy resolver's behavior when the snapshot has no runtime objects. + if (!nozzles.empty()) + flows.assign(nozzles.size(), FLOW_MODE_STANDARD); + } else { + std::vector object_diameters; + if (!SSWCPProtocol::parse_extruder_objects(*objects, object_diameters, flows) || object_diameters.size() != nozzles.size()) + flows.clear(); + } + + // An answer without a machine type is not usable: callers gate on it (e.g. the + // "Snapmaker U1" whitelist), so treat a malformed payload as a failed query. + if (model.empty()) { + BOOST_LOG_TRIVIAL(warning) << "GatewayDevice::query_machine_info: GET /api/cache/all returned no machine_type"; + return false; + } + + out_model = std::move(model); + out_nozzle_diameters = std::move(nozzles); + out_nozzle_volume_types = std::move(flows); + device_name = std::move(name); + + return true; +} + +namespace { + +const nlohmann::json* find_array(const nlohmann::json& object, const char* key) +{ + const auto item = object.find(key); + return item != object.end() && item->is_array() ? &*item : nullptr; +} + +std::string array_string(const nlohmann::json& array, size_t index, const std::string& fallback = {}) +{ return index < array.size() && array[index].is_string() ? array[index].get() : fallback; } + +std::string filament_color(const nlohmann::json& config, size_t index) +{ + if (const nlohmann::json* colors = find_array(config, "filament_color_rgba"); + colors != nullptr && index < colors->size() && (*colors)[index].is_string()) + return (*colors)[index].get(); + + const nlohmann::json* colors = find_array(config, "filament_color"); + if (colors == nullptr || index >= colors->size()) + return "#FFFFFF"; + + const nlohmann::json& value = (*colors)[index]; + if (value.is_string()) + return value.get(); + if (value.is_number_unsigned() || value.is_number_integer()) { + const std::uint64_t argb = (value.is_number_unsigned() ? value.get() : + static_cast(value.get())) & + 0xffffffffULL; + const std::uint64_t rgba = ((argb << 8) & 0xffffffffULL) | (argb >> 24); + std::ostringstream stream; + stream << std::uppercase << std::setfill('0') << std::setw(8) << std::hex << rgba; + return stream.str(); + } + return "#FFFFFF"; +} + +std::string machine_filament_display_name(const std::string& vendor, const std::string& type, const std::string& sub_type) +{ + if (sub_type == "Support") + return vendor + " Support For " + type; + return vendor + " " + type + ((sub_type != "NONE" && !sub_type.empty()) ? " " + sub_type : ""); +} + +std::string nozzle_value(const nlohmann::json& value) +{ + if (value.is_string()) + return value.get(); + if (value.is_number()) + return value.dump(); + return {}; +} + +std::string flow_value(const nlohmann::json& value) +{ + if (!value.is_string()) + return {}; + const std::string flow = value.get(); + return flow == FLOW_MODE_HIGH_FLOW ? FLOW_MODE_HIGH_FLOW : FLOW_MODE_STANDARD; +} + +void apply_config_nozzle_values(const nlohmann::json& config, + size_t count, + std::vector& diameters, + std::vector& flows) +{ + if (const nlohmann::json* values = find_array(config, "nozzle_diameters"); values != nullptr) + for (size_t index = 0; index < count && index < values->size(); ++index) { + diameters.resize(std::max(diameters.size(), index + 1)); + diameters[index] = nozzle_value((*values)[index]); + } + if (const nlohmann::json* values = find_array(config, "nozzle_volume_types"); values != nullptr) + for (size_t index = 0; index < count && index < values->size(); ++index) { + flows.resize(std::max(flows.size(), index + 1)); + flows[index] = flow_value((*values)[index]); + } +} + +} // namespace + +bool GatewayDevice::parse_machine_slots(const nlohmann::json& result, std::vector& slots) +{ + slots.clear(); + const auto objects_value = parse_device_object_query_result(result); + if (!objects_value.has_value() || !objects_value->is_object()) + return false; + + const nlohmann::json& objects = *objects_value; + const auto config_item = objects.find("print_task_config"); + if (config_item == objects.end() || !config_item->is_object()) + return false; + + const nlohmann::json& config = *config_item; + const nlohmann::json* vendors = find_array(config, "filament_vendor"); + const nlohmann::json* types = find_array(config, "filament_type"); + if (vendors == nullptr || types == nullptr) + return false; + + const size_t count = std::max(vendors->size(), types->size()); + if (count > static_cast(std::numeric_limits::max())) + return false; + std::vector nozzles; + std::vector flows; + SSWCPProtocol::parse_extruder_objects(objects, nozzles, flows); + apply_config_nozzle_values(config, count, nozzles, flows); + nozzles.resize(count); + flows.resize(count, FLOW_MODE_STANDARD); + + const nlohmann::json* sub_types = find_array(config, "filament_sub_type"); + const nlohmann::json* exists = find_array(config, "filament_exist"); + const nlohmann::json* multi_colors = find_array(config, "filament_color_multi"); + + std::vector parsed; + parsed.reserve(count); + for (size_t index = 0; index < count; ++index) { + std::string vendor = array_string(*vendors, index, "NONE"); + std::string type = array_string(*types, index, "NONE"); + const bool has_exist_state = exists != nullptr && index < exists->size(); + const bool filament_exists = !has_exist_state || ((*exists)[index].is_boolean() && (*exists)[index].get()); + if (!filament_exists) { + vendor = "NONE"; + type = "NONE"; + } + + ConnectMachineInfo slot; + slot.index = static_cast(index); + const std::string sub_type = array_string(sub_types ? *sub_types : nlohmann::json::array(), index, "NONE"); + slot.filament_info = filament_exists ? machine_filament_display_name(vendor, type, sub_type) : "NONE"; + slot.filament_type = type; + slot.nozzle_info = index < nozzles.size() ? nozzles[index] : std::string{}; + if (slot.nozzle_info.empty()) + return false; + slot.nozzle_volume_type = index < flows.size() ? flows[index] : std::string{FLOW_MODE_STANDARD}; + slot.color_info = NormalizeFilamentHexColor(filament_color(config, index), "#FFFFFF"); + + if (multi_colors != nullptr && index < multi_colors->size() && (*multi_colors)[index].is_object()) { + const auto& color_entry = (*multi_colors)[index]; + if (const auto colors = color_entry.find("colors"); colors != color_entry.end() && colors->is_array()) + for (const auto& color : *colors) + if (color.is_string()) { + const std::string normalized_color = NormalizeFilamentHexColor(color.get()); + if (!normalized_color.empty()) + slot.multiColors.emplace_back(normalized_color); + } + if (const auto mode = color_entry.find("mode"); mode != color_entry.end() && mode->is_number_integer() && + mode->get() >= 0 && + mode->get() <= std::numeric_limits::max()) + slot.colorMode = FilamentColorModeFromConfig(static_cast(mode->get())); + } + if (slot.multiColors.empty() && !slot.color_info.empty()) + slot.multiColors.emplace_back(slot.color_info); + if (slot.multiColors.size() <= 1) + slot.colorMode = FilamentColorMode::Segment; + + parsed.emplace_back(std::move(slot)); + } + + slots = std::move(parsed); + return true; +} + +bool GatewayDevice::delta_affects_machine_slots(const nlohmann::json& delta) +{ + if (!delta.is_object()) + return false; + if (delta.contains("print_task_config")) + return true; + for (auto item = delta.begin(); item != delta.end(); ++item) { + if (item.key() == "extruder") + return true; + if (item.key().size() > 8 && item.key().compare(0, 8, "extruder") == 0 && + std::all_of(item.key().begin() + 8, item.key().end(), [](unsigned char character) { return std::isdigit(character) != 0; })) + return true; + } + if (const auto objects = delta.find("objects"); objects != delta.end() && objects->is_object()) + return delta_affects_machine_slots(*objects); + return false; +} + +bool GatewayDevice::machine_slots_equal(const std::vector& left, const std::vector& right) +{ + if (left.size() != right.size()) + return false; + for (size_t index = 0; index < left.size(); ++index) { + const ConnectMachineInfo& lhs = left[index]; + const ConnectMachineInfo& rhs = right[index]; + if (lhs.index != rhs.index || lhs.filament_info != rhs.filament_info || lhs.filament_type != rhs.filament_type || + lhs.nozzle_info != rhs.nozzle_info || lhs.nozzle_volume_type != rhs.nozzle_volume_type || lhs.color_info != rhs.color_info || + lhs.multiColors != rhs.multiColors || lhs.colorMode != rhs.colorMode) + return false; + } + return true; +} + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/GatewayDevice.hpp b/src/slic3r/Utils/GatewayDevice.hpp new file mode 100644 index 000000000000..5f221f005efa --- /dev/null +++ b/src/slic3r/Utils/GatewayDevice.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include +#include +#include + +struct ConnectMachineInfo; + +namespace Slic3r { + +namespace Gateway { + +class GatewayService; + +// Device-facing queries over the connection gateway, written against the +// snapmaker_connection API contract (WS JSON-RPC / HTTP). One method per +// legacy SSWCP/MQTT interface as it is migrated; nothing here may call back +// into the SSWCP/PrintHost stack. +class GatewayDevice +{ +public: + // Replacement for SSWCP::query_machine_info. Fresh values via the gateway's + // read-only device snapshot GET /api/cache/all (data.info.product for the model, + // data.objects.extruder* for the per-extruder nozzle_volume_type). + // Returns false when the gateway is down or no device is connected. + static bool query_machine_info(const std::shared_ptr& gateway, + std::string& out_model, + std::vector& out_nozzle_diameters, + std::vector& out_nozzle_volume_types, + std::string& device_name); + + static bool parse_machine_slots(const nlohmann::json& result, std::vector& slots); + static bool delta_affects_machine_slots(const nlohmann::json& delta); + static bool machine_slots_equal(const std::vector& left, const std::vector& right); +}; + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/GatewayProtocol.cpp b/src/slic3r/Utils/GatewayProtocol.cpp new file mode 100644 index 000000000000..de22efd0389d --- /dev/null +++ b/src/slic3r/Utils/GatewayProtocol.cpp @@ -0,0 +1,215 @@ +#include "GatewayProtocol.hpp" + +#include + +namespace Slic3r { namespace Gateway { +namespace { + +bool get_string(const nlohmann::json& object, const char* key, std::string& value) +{ + const auto item = object.find(key); + if (item == object.end() || !item->is_string()) + return false; + value = item->get(); + return true; +} + +} // namespace + +nlohmann::json build_jsonrpc_request(std::int64_t id, std::string_view method, const nlohmann::json& params) +{ + nlohmann::json request{{"jsonrpc", "2.0"}, {"id", id}, {"method", std::string{method}}}; + request["params"] = params.is_null() ? nlohmann::json::object() : params; + return request; +} + +std::optional parse_json_object(const std::string& body) +{ + if (body.empty()) + return std::nullopt; + const nlohmann::json parsed = nlohmann::json::parse(body, nullptr, false); + if (parsed.is_discarded() || !parsed.is_object()) + return std::nullopt; + return std::optional{std::in_place, parsed}; +} + +GatewayError parse_health(const std::string& body, HealthInfo& health) +{ + const auto parsed = parse_json_object(body); + if (!parsed.has_value()) + return {GatewayErrorCode::InvalidResponse, "health response is not a JSON object"}; + + const nlohmann::json& root = *parsed; + const auto status = root.find("status"); + const auto ok = root.find("ok"); + const bool healthy = (status != root.end() && status->is_string() && status->get() == "ok") || + (ok != root.end() && ok->is_boolean() && ok->get()); + if (!healthy) + return {GatewayErrorCode::HealthNotReady, "health endpoint did not report ok"}; + + const auto components = root.find("components"); + if (components == root.end() || !components->is_object()) + return {GatewayErrorCode::InvalidResponse, "health components are missing"}; + for (const char* component_name : {"ipc_server", "web_server"}) { + const auto component = components->find(component_name); + if (component == components->end() || !component->is_string() || component->get() != "ok") + return {GatewayErrorCode::HealthNotReady, std::string{"component is not ready: "} + component_name}; + } + + const auto server_url = root.find("server_url"); + if (server_url == root.end() || !server_url->is_object()) + return {GatewayErrorCode::InvalidResponse, "health server_url is missing"}; + if (!get_string(*server_url, "base_url", health.base_url) || health.base_url.empty()) + return {GatewayErrorCode::InvalidResponse, "health base_url is missing"}; + + get_string(root, "cli_version", health.cli_version); + const auto device_connected = root.find("device_connected"); + const auto device_sn = root.find("sn"); + if (device_connected != root.end()) { + if (!device_connected->is_boolean()) + return {GatewayErrorCode::InvalidResponse, "health device_connected must be a boolean"}; + if (device_sn != root.end() && !device_sn->is_string()) + return {GatewayErrorCode::InvalidResponse, "health sn must be a string"}; + + health.has_device_state = true; + health.device_connected = device_connected->get(); + if (device_sn != root.end()) + health.device_sn = device_sn->get(); + } + health.pages.clear(); + for (const auto& item : server_url->items()) { + if (item.key() == "base_url") + continue; + if (item.value().is_string()) + health.pages.emplace(item.key(), item.value().get()); + } + return {}; +} + +std::string parse_device_sn(const nlohmann::json& params) +{ + if (!params.is_object()) + return {}; + + const nlohmann::json* source = ¶ms; + const auto device = params.find("device"); + if (device != params.end() && device->is_object()) + source = &*device; + + for (const char* key : {"sn", "device_sn", "serial_number"}) { + const auto value = source->find(key); + if (value != source->end() && value->is_string() && !value->get().empty()) + return value->get(); + } + return {}; +} + +std::optional parse_current_device(const nlohmann::json& result) +{ + if (!result.is_object()) + return std::nullopt; + + CurrentDevice current_device; + current_device.serial_number = parse_device_sn(result); + const auto connected = result.find("connected"); + current_device.connected = connected != result.end() && connected->is_boolean() && connected->get(); + if (current_device.serial_number.empty() && current_device.connected) + return std::nullopt; + current_device.valid = true; + return current_device; +} + +std::optional parse_device_object_query_result(const nlohmann::json& result) +{ + if (!result.is_object()) + return std::nullopt; + + if (const auto objects = result.find("objects"); objects != result.end() && objects->is_object()) + return std::optional{std::in_place, *objects}; + + // Older gateway builds returned the object map directly instead of wrapping it in "objects". + if (result.contains("print_task_config")) + return std::optional{std::in_place, result}; + return std::nullopt; +} + +RpcFrame classify_jsonrpc_message(const nlohmann::json& message) +{ + RpcFrame frame; + if (!message.is_object()) + return frame; + + const auto id = message.find("id"); + const auto method = message.find("method"); + const bool has_id = id != message.end() && (id->is_number_integer() || id->is_string()); + const bool has_result = message.contains("result"); + const bool has_error = message.contains("error"); + + if (has_id && has_result) { + frame.type = RpcFrameType::Result; + frame.id = id->is_number_integer() ? id->get() : 0; + frame.result = *message.find("result"); + return frame; + } + if (has_id && has_error) { + frame.type = RpcFrameType::Error; + frame.id = id->is_number_integer() ? id->get() : 0; + frame.error = *message.find("error"); + return frame; + } + if (method != message.end() && method->is_string()) { + const auto params = message.find("params"); + if (params != message.end() && !params->is_object() && !params->is_array()) + return frame; + + frame.type = RpcFrameType::Notification; + frame.method = method->get(); + frame.params = params != message.end() ? *params : nlohmann::json::object(); + } + return frame; +} + +namespace detail { + +ReconnectPolicy::ReconnectPolicy(Config config) : config_(std::move(config)) +{ + if (config_.initial_delay.count() <= 0) + config_.initial_delay = std::chrono::milliseconds{1}; + if (config_.max_delay < config_.initial_delay) + config_.max_delay = config_.initial_delay; + config_.jitter_fraction = std::clamp(config_.jitter_fraction, 0.0, 1.0); +} + +void ReconnectPolicy::record_connected(std::int64_t now_ms) { connected_since_ms_ = now_ms; } + +void ReconnectPolicy::record_failure(std::int64_t now_ms) +{ + if (connected_since_ms_.has_value() && now_ms - *connected_since_ms_ >= config_.stable_connection_time.count()) + attempts_ = 0; + ++attempts_; + connected_since_ms_.reset(); +} + +void ReconnectPolicy::reset() +{ + attempts_ = 0; + connected_since_ms_.reset(); +} + +bool ReconnectPolicy::should_retry() const { return config_.max_attempts == 0 || attempts_ < config_.max_attempts; } + +std::chrono::milliseconds ReconnectPolicy::next_delay(double jitter_ratio) const +{ + if (attempts_ == 0) + return config_.initial_delay; + + std::uint64_t shift = std::min(attempts_ - 1, 20); + const std::uint64_t base = static_cast(config_.initial_delay.count()) << shift; + const std::int64_t capped = std::min(base, static_cast(config_.max_delay.count())); + const double jitter = capped * config_.jitter_fraction * std::clamp(jitter_ratio, 0.0, 1.0); + return std::chrono::milliseconds{capped + static_cast(jitter)}; +} + +} // namespace detail + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/GatewayProtocol.hpp b/src/slic3r/Utils/GatewayProtocol.hpp new file mode 100644 index 000000000000..a9ac36b3593e --- /dev/null +++ b/src/slic3r/Utils/GatewayProtocol.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace Slic3r { namespace Gateway { + +enum class GatewayErrorCode { + None, + Cancelled, + NotConnected, + TransportError, + HttpError, + InvalidRequest, + InvalidResponse, + HealthNotReady, + RpcError, + ReconnectFailed, +}; + +struct GatewayError +{ + GatewayErrorCode code{GatewayErrorCode::None}; + std::string message; + int rpc_code{0}; + + explicit operator bool() const { return code != GatewayErrorCode::None; } +}; + +struct HealthInfo +{ + std::string cli_version; + std::string base_url; + std::map pages; + bool has_device_state{false}; + bool device_connected{false}; + std::string device_sn; +}; + +struct CurrentDevice +{ + bool valid{false}; + bool connected{false}; + std::string serial_number; +}; + +struct HttpResponse +{ + unsigned status{0}; + std::string body; + std::string error; +}; + +struct PreprintStoreResult +{ + GatewayError error; + bool ok{false}; + bool file_exists{true}; +}; + +enum class RpcFrameType { Unknown, Result, Error, Notification }; + +struct RpcFrame +{ + RpcFrameType type{RpcFrameType::Unknown}; + std::int64_t id{0}; + std::string method; + nlohmann::json result; + nlohmann::json error; + nlohmann::json params; +}; + +nlohmann::json build_jsonrpc_request(std::int64_t id, std::string_view method, const nlohmann::json& params); +GatewayError parse_health(const std::string& body, HealthInfo& health); +std::string parse_device_sn(const nlohmann::json& params); +std::optional parse_current_device(const nlohmann::json& result); +std::optional parse_device_object_query_result(const nlohmann::json& result); +RpcFrame classify_jsonrpc_message(const nlohmann::json& message); +std::optional parse_json_object(const std::string& body); + +namespace detail { + +class ReconnectPolicy +{ +public: + struct Config + { + std::chrono::milliseconds initial_delay{std::chrono::milliseconds{250}}; + std::chrono::milliseconds max_delay{std::chrono::milliseconds{5000}}; + std::chrono::milliseconds stable_connection_time{std::chrono::milliseconds{30000}}; + std::uint32_t max_attempts{0}; // Zero means unlimited. + double jitter_fraction{0.0}; + }; + + explicit ReconnectPolicy(Config config); + + void record_connected(std::int64_t now_ms); + void record_failure(std::int64_t now_ms); + void reset(); + bool should_retry() const; + std::chrono::milliseconds next_delay(double jitter_ratio = 0.0) const; + + std::uint32_t attempts() const { return attempts_; } + +private: + Config config_; + std::uint32_t attempts_{0}; + std::optional connected_since_ms_; +}; + +} // namespace detail + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/GatewayService.cpp b/src/slic3r/Utils/GatewayService.cpp new file mode 100644 index 000000000000..8769b6e4ff78 --- /dev/null +++ b/src/slic3r/Utils/GatewayService.cpp @@ -0,0 +1,932 @@ +#include "GatewayService.hpp" + +#include "Http.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace asio = boost::asio; +namespace beast = boost::beast; +namespace websocket = beast::websocket; + +namespace Slic3r { namespace Gateway { +namespace { + +std::int64_t wall_now_ms() +{ return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); } + +GatewayError transport_error(const std::string& message) { return {GatewayErrorCode::TransportError, message}; } + +std::string make_url(const std::string& host, std::uint16_t port, const std::string& path) +{ return "http://" + host + ":" + std::to_string(port) + path; } + +std::string join_url(const std::string& base_url, const std::string& path) +{ + if (base_url.empty() || path.empty()) + return {}; + if (base_url.back() == '/' && path.front() == '/') + return base_url.substr(0, base_url.size() - 1) + path; + if (base_url.back() != '/' && path.front() != '/') + return base_url + "/" + path; + return base_url + path; +} + +} // namespace + +LibcurlHttpTransport::LibcurlHttpTransport(long connect_timeout_seconds, long total_timeout_seconds) + : connect_timeout_seconds_(connect_timeout_seconds), total_timeout_seconds_(total_timeout_seconds) +{} + +HttpResponse LibcurlHttpTransport::get(const std::string& url) +{ + HttpResponse response; + bool fulfilled = false; + Http request = Http::get(url); + request.remove_header("Origin").timeout_connect(connect_timeout_seconds_).timeout_max(total_timeout_seconds_); + request.on_complete([&](std::string body, unsigned status) { + if (!fulfilled) { + fulfilled = true; + response.status = status; + response.body = std::move(body); + } + }); + request.on_error([&](std::string body, std::string error, unsigned status) { + if (!fulfilled) { + fulfilled = true; + response.status = status; + response.body = std::move(body); + response.error = std::move(error); + } + }); + request.perform_sync(); + return response; +} + +HttpResponse LibcurlHttpTransport::post_json(const std::string& url, const std::string& body) +{ + HttpResponse response; + bool fulfilled = false; + Http request = Http::post(url); + request.remove_header("Origin") + .header("Content-Type", "application/json") + .timeout_connect(connect_timeout_seconds_) + .timeout_max(total_timeout_seconds_) + .set_post_body(body); + request.on_complete([&](std::string response_body, unsigned status) { + if (!fulfilled) { + fulfilled = true; + response.status = status; + response.body = std::move(response_body); + } + }); + request.on_error([&](std::string response_body, std::string error, unsigned status) { + if (!fulfilled) { + fulfilled = true; + response.status = status; + response.body = std::move(response_body); + response.error = std::move(error); + } + }); + request.perform_sync(); + return response; +} + +struct GatewayWebSocketTransport::Session : public std::enable_shared_from_this +{ + Session(std::shared_ptr io_context, std::string host, std::string port, std::string path, Listener listener) + : io_context(std::move(io_context)) + , executor(this->io_context->get_executor()) + , resolver(executor) + , stream(executor) + , host(std::move(host)) + , port(std::move(port)) + , path(std::move(path)) + , listener(std::move(listener)) + {} + + void run() + { + post([self = shared_from_this()] { self->resolve(); }); + } + + void send_from_any_thread(std::string message) + { + post([self = shared_from_this(), message = std::move(message)]() mutable { self->queue_send(std::move(message)); }); + } + + void close_from_any_thread() + { + post([self = shared_from_this()] { self->close(); }); + } + +private: + template void post(Handler&& handler) + { + asio::post(executor, [self = shared_from_this(), handler = std::forward(handler)]() mutable { handler(); }); + } + + void resolve() + { + resolver.async_resolve(host, port, + [self = shared_from_this()](const boost::system::error_code& error, + asio::ip::tcp::resolver::results_type results) { + if (error) + self->fail(error.message()); + else + self->connect_socket(results); + }); + } + + void connect_socket(const asio::ip::tcp::resolver::results_type& results) + { + asio::async_connect(stream.next_layer(), results.begin(), results.end(), + [self = shared_from_this()](const boost::system::error_code& error, const asio::ip::tcp::resolver::iterator&) { + if (error) + self->fail(error.message()); + else + self->handshake(); + }); + } + + void handshake() + { + websocket::stream_base::timeout timeout = websocket::stream_base::timeout::suggested(beast::role_type::client); + timeout.handshake_timeout = std::chrono::seconds{ + 5}; // 5 secondes timeout for the handshake, otherwise the gateway may be busy and not respond + timeout.idle_timeout = std::chrono::seconds{ + 25}; // 25 seconds keep-alive timeout, otherwise the gateway may close the connection if no message is sent for a long time + timeout.keep_alive_pings = true; + stream.set_option(timeout); + stream.set_option(websocket::stream_base::decorator([](websocket::request_type& request) { + request.erase(beast::http::field::origin); + request.set(beast::http::field::user_agent, "Snapmaker_Orca"); + })); + stream.async_handshake(host, path, [self = shared_from_this()](const boost::system::error_code& error) { + if (error) + self->fail(error.message()); + else + self->on_handshake(); + }); + } + + void on_handshake() + { + connected.store(true, std::memory_order_release); + if (listener.opened) + listener.opened(); + if (!outbox.empty()) { + do_write(); + } + do_read(); + } + + void do_read() + { + stream.async_read(buffer, [self = shared_from_this()](const boost::system::error_code& error, std::size_t) { + if (error) { + self->fail(error.message()); + return; + } + if (self->listener.message) + self->listener.message(beast::buffers_to_string(self->buffer.data())); + self->buffer.clear(); + if (!self->closing) + self->do_read(); + }); + } + + void queue_send(std::string message) + { + if (closing) + return; + outbox.push_back(std::move(message)); + if (connected.load(std::memory_order_acquire) && outbox.size() == 1) + do_write(); + } + + void do_write() + { + if (outbox.empty() || writing) + return; + writing = true; + stream.async_write(asio::buffer(outbox.front()), [self = shared_from_this()](const boost::system::error_code& error, std::size_t) { + self->writing = false; + if (error) { + self->fail(error.message()); + return; + } + if (!self->outbox.empty()) + self->outbox.pop_front(); + if (!self->outbox.empty()) + self->do_write(); + }); + } + + void close() + { + if (closed.exchange(true)) + return; + closing = true; + boost::system::error_code error; + stream.close(websocket::close_code::normal, error); + if (listener.closed) + listener.closed(error ? error.message() : std::string{}); + io_context->stop(); + } + + void fail(const std::string& reason) + { + if (closed.exchange(true)) + return; + closing = true; + boost::system::error_code error; + stream.next_layer().close(error); + if (listener.closed) + listener.closed(reason); + io_context->stop(); + } + + std::shared_ptr io_context; + asio::any_io_executor executor; + asio::ip::tcp::resolver resolver; + websocket::stream stream; + std::string host; + std::string port; + std::string path; + Listener listener; + beast::flat_buffer buffer; + std::deque outbox; + std::atomic connected{false}; + bool writing{false}; + bool closing{false}; + std::atomic closed{false}; +}; + +GatewayWebSocketTransport::GatewayWebSocketTransport(std::string host) : host_(std::move(host)) {} + +GatewayWebSocketTransport::~GatewayWebSocketTransport() +{ + close(); + if (io_thread_.joinable()) + io_thread_.join(); +} + +void GatewayWebSocketTransport::set_listener(Listener listener) +{ + std::lock_guard lock(mutex_); + listener_ = std::move(listener); +} + +void GatewayWebSocketTransport::connect(std::uint16_t port, const std::string& path) +{ + std::lock_guard lock(mutex_); + close_locked(); + if (io_thread_.joinable()) + io_thread_.join(); + + auto io_context = std::make_shared(); + Listener listener = listener_; + auto session = std::make_shared(io_context, host_, std::to_string(port), path, std::move(listener)); + session_ = session; + session->run(); + io_thread_ = std::thread([io_context] { io_context->run(); }); +} + +bool GatewayWebSocketTransport::send(const std::string& message) +{ + std::shared_ptr session; + { + std::lock_guard lock(mutex_); + session = session_.lock(); + } + if (!session) + return false; + session->send_from_any_thread(message); + return true; +} + +void GatewayWebSocketTransport::close() +{ + std::lock_guard lock(mutex_); + close_locked(); +} + +void GatewayWebSocketTransport::close_locked() +{ + std::shared_ptr session = session_.lock(); + if (session) + session->close_from_any_thread(); + if (io_thread_.joinable()) + io_thread_.join(); + session_.reset(); +} + +GatewayService::GatewayService(Config config, Dependencies dependencies) + : config_(std::move(config)), dependencies_(std::move(dependencies)), reconnect_policy_(config_.reconnect) +{ + if (!dependencies_.process_manager || !dependencies_.http || !dependencies_.websocket) + throw std::invalid_argument("GatewayService dependencies must not be null"); + if (!dependencies_.dispatcher) + dependencies_.dispatcher = [](std::function task) { task(); }; + if (!dependencies_.now_ms) + dependencies_.now_ms = wall_now_ms; + dependencies_.websocket->set_listener({[this] { set_websocket_open(true); }, + [this](const std::string& message) { handle_websocket_message(message); }, + [this](const std::string& reason) { handle_websocket_closed(reason); }}); +} + +GatewayService::~GatewayService() +{ + stop(); + dependencies_.websocket->set_listener({}); +} + +bool GatewayService::start(const std::string& locale) +{ + std::lock_guard lifecycle_lock(lifecycle_mutex_); + if (worker_.joinable() && !stop_requested_.load(std::memory_order_relaxed)) + return false; + if (worker_.joinable()) + worker_.join(); + + { + std::lock_guard lock(state_mutex_); + stop_requested_.store(false, std::memory_order_relaxed); + state_ = ConnectionState::Connecting; + port_ = 0; + health_ = HealthInfo{}; + websocket_open_ = false; + websocket_error_.clear(); + } + worker_ = std::thread([this, locale] { run(locale); }); + return true; +} + +void GatewayService::stop() +{ + std::lock_guard lifecycle_lock(lifecycle_mutex_); + if (!worker_.joinable() && stop_requested_.load(std::memory_order_relaxed)) + return; + + { + std::lock_guard lock(state_mutex_); + stop_requested_.store(true, std::memory_order_relaxed); + state_ = ConnectionState::Disconnected; + port_ = 0; + health_ = HealthInfo{}; + websocket_open_ = false; + } + state_condition_.notify_all(); + if (dependencies_.websocket) + dependencies_.websocket->close(); + if (worker_.joinable()) + worker_.join(); + fail_pending({GatewayErrorCode::Cancelled, "GatewayService was stopped"}); + dispatch_state(ConnectionState::Disconnected, {GatewayErrorCode::Cancelled, "GatewayService was stopped"}); +} + +bool GatewayService::is_connected() const +{ + std::lock_guard lock(state_mutex_); + return state_ == ConnectionState::Connected; +} + +bool GatewayService::wait_for_connected(std::chrono::milliseconds timeout) +{ + std::unique_lock lock(state_mutex_); + return state_condition_.wait_for(lock, timeout, [this] { return stop_requested_ || state_ == ConnectionState::Connected; }) && + state_ == ConnectionState::Connected; +} + +ConnectionState GatewayService::state() const +{ + std::lock_guard lock(state_mutex_); + return state_; +} + +std::uint16_t GatewayService::port() const +{ + std::lock_guard lock(state_mutex_); + return port_; +} + +HealthInfo GatewayService::health() const +{ + std::lock_guard lock(state_mutex_); + return health_; +} + +std::string GatewayService::base_url() const +{ + std::lock_guard lock(state_mutex_); + if (state_ != ConnectionState::Connected) + return {}; + return health_.base_url; +} + +std::string GatewayService::web_url(const std::string& page_key) const +{ + HealthInfo copied_health; + { + std::lock_guard lock(state_mutex_); + // Health already validates the web server and carries the page URLs. The UI can + // therefore show gateway pages while the websocket control channel is still connecting. + if (stop_requested_ || state_ == ConnectionState::Disconnected) + return {}; + copied_health = health_; + } + + const auto page = copied_health.pages.find(page_key); + if (page == copied_health.pages.end() || copied_health.base_url.empty()) + return {}; + if (page->second.empty()) + return {}; + + return join_url(copied_health.base_url, page->second); +} + +std::string GatewayService::localfile_url(const std::string& file_path) const +{ + if (file_path.empty()) + return {}; + return join_url(base_url(), "/localfile/" + Http::url_encode(file_path)); +} + +void GatewayService::set_state_callback(StateCallback callback) +{ + std::lock_guard lock(callback_mutex_); + state_callback_ = std::move(callback); +} + +void GatewayService::set_notification_handler(const std::string& method, NotificationCallback callback) +{ + std::lock_guard lock(callback_mutex_); + notification_handlers_[method] = std::move(callback); +} + +void GatewayService::clear_notification_handler(const std::string& method) +{ + std::lock_guard lock(callback_mutex_); + notification_handlers_.erase(method); +} + +std::int64_t GatewayService::request(const std::string& method, const nlohmann::json& params, RpcCallback callback) +{ + { + std::lock_guard lock(state_mutex_); + if (stop_requested_ || state_ != ConnectionState::Connected || !websocket_open_) { + const GatewayError error{GatewayErrorCode::NotConnected, "gateway websocket is not connected"}; + if (callback) + dependencies_.dispatcher([callback = std::move(callback), error]() mutable { callback(error, nlohmann::json::object()); }); + return 0; + } + } + + return send_request(method, params, std::move(callback)); +} + +std::int64_t GatewayService::send_request(const std::string& method, const nlohmann::json& params, RpcCallback callback) +{ + const std::int64_t id = next_request_id(); + { + std::lock_guard lock(pending_mutex_); + pending_requests_.emplace(id, PendingRequest{std::move(callback)}); + } + if (!dependencies_.websocket->send(build_jsonrpc_request(id, method, params).dump())) { + complete_pending(id, {GatewayErrorCode::TransportError, "failed to queue websocket request"}, nlohmann::json::object()); + return id; + } + return id; +} + +GatewayService::ApiResult GatewayService::request_sync(const std::string& method, + const nlohmann::json& params, + std::chrono::milliseconds timeout) +{ + ApiResult result; + { + std::lock_guard lock(state_mutex_); + if (stop_requested_ || state_ != ConnectionState::Connected || !websocket_open_) { + result.error = {GatewayErrorCode::NotConnected, "gateway websocket is not connected"}; + return result; + } + } + + auto response = std::make_shared>(); + std::future future = response->get_future(); + const std::int64_t id = next_request_id(); + { + std::lock_guard lock(pending_mutex_); + pending_requests_.emplace(id, PendingRequest{[response](GatewayError error, const nlohmann::json& value) { + response->set_value(ApiResult{error, value}); + }, + true}); + } + if (!dependencies_.websocket->send(build_jsonrpc_request(id, method, params).dump())) { + std::lock_guard lock(pending_mutex_); + pending_requests_.erase(id); + result.error = {GatewayErrorCode::TransportError, "failed to queue websocket request"}; + return result; + } + if (future.wait_for(timeout) != std::future_status::ready) { + std::lock_guard lock(pending_mutex_); + pending_requests_.erase(id); + result.error = {GatewayErrorCode::TransportError, "request timed out"}; + return result; + } + return future.get(); +} + +std::int64_t GatewayService::watch_device(const nlohmann::json& params, RpcCallback callback) +{ + const nlohmann::json request_params = params.is_null() ? nlohmann::json::object() : params; + BOOST_LOG_TRIVIAL(info) << "[gateway][device-status] requesting action.device.watch, params=" << request_params.dump(); + return request("action.device.watch", request_params, std::move(callback)); +} + +GatewayService::ApiResult GatewayService::get_device(const std::optional& serial_number) +{ + if (!serial_number.has_value()) + return get_json(config_.device_path); + return get_json(config_.device_path + "/" + Http::url_encode(*serial_number)); +} + +GatewayService::ApiResult GatewayService::get_account() { return get_json(config_.account_path); } + +GatewayService::ApiResult GatewayService::set_region(const std::string& region) +{ + if (region.empty()) + return {{GatewayErrorCode::InvalidRequest, "region is empty"}, {}}; + + ApiResult result = post_json(config_.region_path, {{"region", region}}); + if (result.error) + return result; + + const auto code = result.value.find("code"); + if (code == result.value.end() || !code->is_number_integer() || code->get() != 200) { + result.error = {GatewayErrorCode::InvalidResponse, "region endpoint did not report code 200"}; + } + return result; +} + +PreprintStoreResult GatewayService::store_preprint_context(const std::string& id, const nlohmann::json& payload) +{ + PreprintStoreResult result; + if (id.empty() || !payload.is_object()) { + result.error = {GatewayErrorCode::InvalidRequest, "preprint store id or payload is invalid"}; + return result; + } + + const nlohmann::json request{{"id", id}, {"payload", payload}}; + const ApiResult api_result = post_json(config_.store_path, request); + if (api_result.error) { + result.error = api_result.error; + return result; + } + + const auto ok = api_result.value.find("ok"); + const auto legacy_code = api_result.value.find("code"); + const bool documented_ok = ok != api_result.value.end() && ok->is_boolean() && ok->get(); + const bool legacy_ok = legacy_code != api_result.value.end() && legacy_code->is_number_integer() && legacy_code->get() == 200; + if (!documented_ok && !legacy_ok) { + result.error = {GatewayErrorCode::InvalidResponse, "store endpoint did not report ok=true"}; + return result; + } + + result.ok = true; + for (const char* key : {"data", "meta"}) { + const auto container = api_result.value.find(key); + if (container == api_result.value.end() || !container->is_object()) + continue; + + const auto file_exists = container->find("file_exists"); + if (file_exists != container->end() && file_exists->is_boolean()) { + result.file_exists = file_exists->get(); + break; + } + } + return result; +} + +void GatewayService::run(const std::string locale) +{ + while (true) { + { + std::lock_guard lock(state_mutex_); + if (stop_requested_) + break; + state_ = ConnectionState::Connecting; + } + dispatch_state(ConnectionState::Connecting, {}); + + GatewayError error = connect_once(locale); + if (!error) { + { + std::lock_guard lock(state_mutex_); + if (stop_requested_) + break; + state_ = ConnectionState::Connected; + } + reconnect_policy_.record_connected(dependencies_.now_ms()); + dispatch_state(ConnectionState::Connected, {}); + + std::unique_lock lock(state_mutex_); + state_condition_.wait(lock, [this] { return stop_requested_ || !websocket_open_; }); + if (stop_requested_) + break; + error = transport_error("websocket connection was closed"); + } + + reconnect_policy_.record_failure(dependencies_.now_ms()); + { + std::lock_guard lock(state_mutex_); + state_ = ConnectionState::Disconnected; + port_ = 0; + health_ = HealthInfo{}; + websocket_open_ = false; + } + fail_pending({GatewayErrorCode::NotConnected, "gateway connection was lost"}); + dependencies_.websocket->close(); + dispatch_state(ConnectionState::Disconnected, error); + + if (stop_requested_ || !reconnect_policy_.should_retry()) + break; + + // discover_port() detaches the CLI child, so a wedged process from the failed attempt + // would otherwise linger forever while the next attempt spawns yet another instance. + // Force-kill the stale process before retrying (TerminateProcess / SIGKILL) so every + // reconnect starts from a clean single-instance state. + BOOST_LOG_TRIVIAL(warning) << "connection gateway reconnect attempt failed: " << error.message + << "; force-restarting the snapmaker_connection CLI"; + if (dependencies_.process_manager) + dependencies_.process_manager->terminate(); + + static thread_local std::mt19937 random_generator{std::random_device{}()}; + std::uniform_real_distribution distribution{0.0, 1.0}; + const auto delay = reconnect_policy_.next_delay(distribution(random_generator)); + std::unique_lock lock(state_mutex_); + state_condition_.wait_for(lock, delay, [this] { return stop_requested_.load(std::memory_order_relaxed); }); + } +} + +GatewayError GatewayService::connect_once(const std::string& locale) +{ + const ConnectionProcessManager::DiscoveryResult discovery = dependencies_.process_manager->discover_port(locale); + if (discovery.error != ProcessDiscoveryError::None) + return {GatewayErrorCode::ReconnectFailed, discovery.message}; + + { + std::lock_guard lock(state_mutex_); + port_ = discovery.port; + } + + HealthInfo health; + if (const GatewayError health_error = wait_for_health(discovery.port, health)) + return health_error; + { + std::lock_guard lock(state_mutex_); + health_ = std::move(health); + } + + if (const GatewayError websocket_error = connect_websocket(discovery.port)) + return websocket_error; + return {}; +} + +GatewayError GatewayService::wait_for_health(std::uint16_t port, HealthInfo& health) +{ + const auto deadline = std::chrono::steady_clock::now() + config_.health_timeout; + while (std::chrono::steady_clock::now() < deadline) { + { + std::lock_guard lock(state_mutex_); + if (stop_requested_) + return {GatewayErrorCode::Cancelled, "GatewayService was stopped"}; + } + + const HttpResponse response = dependencies_.http->get(make_url(config_.host, port, config_.health_path)); + if (response.error.empty() && response.status >= 200 && response.status < 300) { + if (const GatewayError health_error = parse_health(response.body, health)) + return health_error; + BOOST_LOG_TRIVIAL(info) << "connection gateway health response: status=" << response.status + << ", device_connected=" << health.device_connected << ", has_device_state=" << health.has_device_state; + return {}; + } + std::this_thread::sleep_for(config_.health_poll_interval); + } + return {GatewayErrorCode::HealthNotReady, "gateway health check timed out"}; +} + +GatewayError GatewayService::connect_websocket(std::uint16_t port) +{ + { + std::lock_guard lock(state_mutex_); + websocket_open_ = false; + websocket_error_.clear(); + } + dependencies_.websocket->connect(port, config_.websocket_path); + + std::unique_lock lock(state_mutex_); + if (!state_condition_.wait_for(lock, config_.health_timeout, + [this] { return stop_requested_ || websocket_open_ || !websocket_error_.empty(); })) + return transport_error("websocket connection timed out"); + if (stop_requested_) + return {GatewayErrorCode::Cancelled, "GatewayService was stopped"}; + if (!websocket_error_.empty()) + return transport_error(websocket_error_); + if (!websocket_open_) + return transport_error("websocket did not connect"); + return {}; +} + +void GatewayService::handle_websocket_message(const std::string& message) +{ + const auto parsed = parse_json_object(message); + if (!parsed.has_value()) + return; + + const RpcFrame frame = classify_jsonrpc_message(*parsed); + if (frame.type == RpcFrameType::Result) { + complete_pending(frame.id, {}, frame.result); + return; + } + if (frame.type == RpcFrameType::Error) { + std::string error_message = "JSON-RPC error"; + int error_code = 0; + if (frame.error.is_object()) { + if (const auto message = frame.error.find("message"); message != frame.error.end() && message->is_string()) + error_message = message->get(); + if (const auto code = frame.error.find("code"); code != frame.error.end() && code->is_number_integer()) { + if (code->is_number_unsigned()) { + const std::uint64_t value = code->get(); + error_code = value > static_cast(std::numeric_limits::max()) ? std::numeric_limits::max() : + static_cast(value); + } else { + const std::int64_t value = code->get(); + error_code = value < std::numeric_limits::min() ? std::numeric_limits::min() : + value > std::numeric_limits::max() ? std::numeric_limits::max() : + static_cast(value); + } + } + } + complete_pending(frame.id, {GatewayErrorCode::RpcError, error_message, error_code}, nlohmann::json::object()); + return; + } + if (frame.type != RpcFrameType::Notification) + return; + + BOOST_LOG_TRIVIAL(info) << "[gateway][device-status] notification received: " << frame.method; + + NotificationCallback callback; + { + std::lock_guard lock(callback_mutex_); + const auto handler = notification_handlers_.find(frame.method); + if (handler != notification_handlers_.end()) + callback = handler->second; + } + if (callback) + dependencies_.dispatcher([callback, params = frame.params]() mutable { callback(params); }); +} + +void GatewayService::handle_websocket_closed(const std::string& reason) +{ + { + std::lock_guard lock(state_mutex_); + websocket_open_ = false; + websocket_error_ = reason; + } + state_condition_.notify_all(); +} + +void GatewayService::dispatch_state(ConnectionState state, GatewayError error) +{ + StateCallback callback; + { + std::lock_guard lock(callback_mutex_); + callback = state_callback_; + } + if (callback) + dependencies_.dispatcher([callback, state, error]() mutable { callback(state, error); }); +} + +void GatewayService::complete_pending(std::int64_t id, GatewayError error, const nlohmann::json& result) +{ + PendingRequest request; + { + std::lock_guard lock(pending_mutex_); + const auto pending = pending_requests_.find(id); + if (pending == pending_requests_.end()) + return; + request = std::move(pending->second); + pending_requests_.erase(pending); + } + if (request.callback) { + if (request.direct_invoke) + request.callback(error, result); + else + dependencies_.dispatcher([callback = std::move(request.callback), error, result]() mutable { callback(error, result); }); + } +} + +void GatewayService::fail_pending(const GatewayError& error) +{ + std::map requests; + { + std::lock_guard lock(pending_mutex_); + requests.swap(pending_requests_); + } + for (auto& item : requests) { + if (!item.second.callback) + continue; + if (item.second.direct_invoke) + item.second.callback(error, nlohmann::json::object()); + else + dependencies_.dispatcher( + [callback = std::move(item.second.callback), error]() mutable { callback(error, nlohmann::json::object()); }); + } +} + +void GatewayService::set_websocket_open(bool opened) +{ + { + std::lock_guard lock(state_mutex_); + websocket_open_ = opened; + if (opened) + websocket_error_.clear(); + } + state_condition_.notify_all(); +} + +GatewayService::ApiResult GatewayService::get_json(const std::string& path) +{ + ApiResult result; + std::uint16_t port = 0; + { + std::lock_guard lock(state_mutex_); + if (state_ != ConnectionState::Connected) { + result.error = {GatewayErrorCode::NotConnected, "gateway is not connected"}; + return result; + } + port = port_; + } + const HttpResponse response = dependencies_.http->get(make_url(config_.host, port, path)); + if (!response.error.empty() || response.status < 200 || response.status >= 300) { + result.error = {GatewayErrorCode::HttpError, + response.error.empty() ? "request failed with HTTP status " + std::to_string(response.status) : response.error}; + return result; + } + const auto parsed = parse_json_object(response.body); + if (!parsed.has_value()) { + result.error = {GatewayErrorCode::InvalidResponse, "response is not a JSON object"}; + return result; + } + result.value = *parsed; + return result; +} + +GatewayService::ApiResult GatewayService::post_json(const std::string& path, const nlohmann::json& body) +{ + ApiResult result; + std::string base_url; + { + std::lock_guard lock(state_mutex_); + if (state_ != ConnectionState::Connected) { + result.error = {GatewayErrorCode::NotConnected, "gateway is not connected"}; + return result; + } + base_url = health_.base_url; + } + if (base_url.empty()) + return {{GatewayErrorCode::HealthNotReady, "gateway web base URL is not available"}, {}}; + + const HttpResponse response = dependencies_.http->post_json(join_url(base_url, path), body.dump()); + if (!response.error.empty() || response.status < 200 || response.status >= 300) { + result.error = {GatewayErrorCode::HttpError, + response.error.empty() ? "request failed with HTTP status " + std::to_string(response.status) : response.error}; + return result; + } + const auto parsed = parse_json_object(response.body); + if (!parsed.has_value()) { + result.error = {GatewayErrorCode::InvalidResponse, "response is not a JSON object"}; + return result; + } + result.value = *parsed; + return result; +} + +std::int64_t GatewayService::next_request_id() { return next_id_.fetch_add(1, std::memory_order_relaxed); } + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/GatewayService.hpp b/src/slic3r/Utils/GatewayService.hpp new file mode 100644 index 000000000000..0121617f47ed --- /dev/null +++ b/src/slic3r/Utils/GatewayService.hpp @@ -0,0 +1,199 @@ +#pragma once + +#include "ConnectionProcessManager.hpp" +#include "GatewayProtocol.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { namespace Gateway { + +enum class ConnectionState { Disconnected, Connecting, Connected }; + +class HttpTransport +{ +public: + virtual ~HttpTransport() = default; + virtual HttpResponse get(const std::string& url) = 0; + virtual HttpResponse post_json(const std::string& url, const std::string& body) = 0; +}; + +class WebSocketTransport +{ +public: + struct Listener + { + std::function opened; + std::function message; + std::function closed; + }; + + virtual ~WebSocketTransport() = default; + virtual void set_listener(Listener listener) = 0; + virtual void connect(std::uint16_t port, const std::string& path) = 0; + virtual bool send(const std::string& message) = 0; + virtual void close() = 0; +}; + +class LibcurlHttpTransport final : public HttpTransport +{ +public: + explicit LibcurlHttpTransport(long connect_timeout_seconds = 2, long total_timeout_seconds = 5); + HttpResponse get(const std::string& url) override; + HttpResponse post_json(const std::string& url, const std::string& body) override; + +private: + long connect_timeout_seconds_; + long total_timeout_seconds_; +}; + +class GatewayWebSocketTransport final : public WebSocketTransport +{ +public: + explicit GatewayWebSocketTransport(std::string host = "127.0.0.1"); + ~GatewayWebSocketTransport() override; + + void set_listener(Listener listener) override; + void connect(std::uint16_t port, const std::string& path) override; + bool send(const std::string& message) override; + void close() override; + +private: + struct Session; + + std::string host_; + mutable std::mutex mutex_; + Listener listener_; + std::weak_ptr session_; + std::thread io_thread_; + + void close_locked(); +}; + +class GatewayService +{ +public: + using NotificationCallback = std::function; + using RpcCallback = std::function; + using StateCallback = std::function; + using Dispatcher = std::function task)>; + + struct Config + { + std::string host{"127.0.0.1"}; + std::string health_path{"/api/health"}; + std::string websocket_path{"/ws"}; + std::string device_path{"/api/cache/all"}; + std::string account_path{"/api/account"}; + std::string store_path{"/api/store"}; + std::string region_path{"/api/region"}; + std::chrono::milliseconds health_poll_interval{std::chrono::milliseconds{100}}; + std::chrono::milliseconds health_timeout{std::chrono::milliseconds{5000}}; + detail::ReconnectPolicy::Config reconnect; + }; + + struct Dependencies + { + std::shared_ptr process_manager; + std::shared_ptr http; + std::shared_ptr websocket; + Dispatcher dispatcher; + std::function now_ms; + }; + + struct ApiResult + { + GatewayError error; + nlohmann::json value{nlohmann::json::object()}; + }; + + GatewayService(Config config, Dependencies dependencies); + ~GatewayService(); + + GatewayService(const GatewayService&) = delete; + GatewayService& operator=(const GatewayService&) = delete; + + bool start(const std::string& locale); + void stop(); + bool is_connected() const; + bool wait_for_connected(std::chrono::milliseconds timeout); + ConnectionState state() const; + std::uint16_t port() const; + HealthInfo health() const; + std::string base_url() const; + + void set_state_callback(StateCallback callback); + void set_notification_handler(const std::string& method, NotificationCallback callback); + void clear_notification_handler(const std::string& method); + + std::int64_t request(const std::string& method, const nlohmann::json& params, RpcCallback callback); + // Synchronous request. The callback is invoked directly on the websocket thread (bypassing + // the dispatcher), so this is safe from any thread except the gateway's own websocket I/O + // thread: waiting there would block the only thread that can deliver the response. + ApiResult request_sync(const std::string& method, const nlohmann::json& params, std::chrono::milliseconds timeout); + std::int64_t watch_device(const nlohmann::json& params, RpcCallback callback); + ApiResult get_device(const std::optional& serial_number = std::nullopt); + ApiResult get_account(); + PreprintStoreResult store_preprint_context(const std::string& id, const nlohmann::json& payload); + ApiResult set_region(const std::string& region); + std::string web_url(const std::string& page_key) const; + std::string localfile_url(const std::string& file_path) const; + +private: + struct PendingRequest + { + RpcCallback callback; + bool direct_invoke{false}; // complete on the websocket thread instead of the dispatcher + }; + + void run(const std::string locale); + GatewayError connect_once(const std::string& locale); + GatewayError wait_for_health(std::uint16_t port, HealthInfo& health); + GatewayError connect_websocket(std::uint16_t port); + void handle_websocket_message(const std::string& message); + void handle_websocket_closed(const std::string& reason); + void dispatch_state(ConnectionState state, GatewayError error); + void complete_pending(std::int64_t id, GatewayError error, const nlohmann::json& result); + void fail_pending(const GatewayError& error); + void set_websocket_open(bool opened); + ApiResult get_json(const std::string& path); + ApiResult post_json(const std::string& path, const nlohmann::json& body); + std::int64_t next_request_id(); + std::int64_t send_request(const std::string& method, const nlohmann::json& params, RpcCallback callback); + + Config config_; + Dependencies dependencies_; + detail::ReconnectPolicy reconnect_policy_; + + std::mutex lifecycle_mutex_; + mutable std::mutex state_mutex_; + std::condition_variable state_condition_; + ConnectionState state_{ConnectionState::Disconnected}; + std::uint16_t port_{0}; + HealthInfo health_; + bool websocket_open_{false}; + std::string websocket_error_; + std::atomic stop_requested_{false}; + std::thread worker_; + + mutable std::mutex callback_mutex_; + StateCallback state_callback_; + std::map notification_handlers_; + + std::mutex pending_mutex_; + std::map pending_requests_; + std::atomic next_id_{1}; +}; + +}} // namespace Slic3r::Gateway diff --git a/src/slic3r/Utils/MoonRaker.cpp b/src/slic3r/Utils/MoonRaker.cpp index c69d0f9792e4..15ad4a730ee3 100644 --- a/src/slic3r/Utils/MoonRaker.cpp +++ b/src/slic3r/Utils/MoonRaker.cpp @@ -311,50 +311,6 @@ bool Moonraker::test_with_resolved_ip(wxString& msg) const } #endif // WIN32 -bool Moonraker::get_machine_info(const std::vector>>& targets, json& response) { - auto& wcp_loger = GUI::WCP_Logger::getInstance(); - - bool res = true; - auto url = make_url("printer/objects/query"); - auto http = Http::post(std::move(url)); - - for (const auto pair : targets) { - std::string value = ""; - for (size_t i = 0; i < pair.second.size(); ++i) { - if (i != 0) { - value += ","; - } - value += pair.second[i]; - } - http.form_add(pair.first, value); - wcp_loger.add_log("adding query parameter: " + pair.first + " = " + value, false, "", "Moonraker_Mqtt", "info"); - } - - http.on_error([&](std::string body, std::string error, unsigned status) { - - BOOST_LOG_TRIVIAL(error) << "[Moonraker_Mqtt] failed to get machine info, error: " << error << ", HTTP status: " << status; - wcp_loger.add_log("failed to get machine info, error: " + error + ", HTTP status: " + std::to_string(status), false, "", "Moonraker_Mqtt", "error"); - res = false; - try{ - response = json::parse(body); - } catch (std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "[Moonraker_Mqtt] analysis error: " << e.what(); - } - }) - .on_complete([&](std::string body, unsigned) { - - wcp_loger.add_log("got machine info successfully", false, "", "Moonraker_Mqtt", "info"); - try { - response = json::parse(body); - } catch (std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "[Moonraker_Mqtt] analysis machine response: " << e.what(); - } - }) - .perform_sync(); - - return res; -} - bool Moonraker::send_gcodes(const std::vector& codes, std::string& extraInfo) { auto& wcp_loger = GUI::WCP_Logger::getInstance(); @@ -1651,29 +1607,6 @@ void Moonraker_Mqtt::async_cancel_print_job(std::function callback) { - auto& wcp_loger = GUI::WCP_Logger::getInstance(); - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] Starting get printer info"; - wcp_loger.add_log("Starting get printer info", false, "", "Moonraker_Mqtt", "info"); - std::string method = "printer.info"; - json params = json::object(); - - if (!send_to_request(method, params, true, callback, - [callback, &wcp_loger]() { - BOOST_LOG_TRIVIAL(warning) << "[Moonraker_Mqtt] get printer info timed out"; - wcp_loger.add_log("get printer info timed out", false, "", "Moonraker_Mqtt", "warning"); - json res; - res["error"] = "timeout"; - callback(res); - }) && - callback) { - BOOST_LOG_TRIVIAL(error) << "[Moonraker_Mqtt] failed to send get printer info request"; - wcp_loger.add_log("failed to send get printer info request", false, "", "Moonraker_Mqtt", "error"); - callback(json::value_t::null); - } -} - // Send G-code commands to printer void Moonraker_Mqtt::async_send_gcodes(const std::vector& scripts, std::function callback) { @@ -1758,49 +1691,6 @@ void Moonraker_Mqtt::async_unsubscribe_machine_info(const std::string& hash, std } -// Set filters for printer status subscription -void Moonraker_Mqtt::async_set_machine_subscribe_filter( - const std::vector>>& targets, - std::function callback) -{ - auto& wcp_loger = GUI::WCP_Logger::getInstance(); - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] Starting machine subscribe filter setup, target count: " << targets.size(); - wcp_loger.add_log("Starting machine subscribe filter setup, target count: " + std::to_string(targets.size()), false, "", "Moonraker_Mqtt", "info"); - std::string method = "printer.objects.subscribe"; - - json params; - params["objects"] = json::object(); - - for (size_t i = 0; i < targets.size(); ++i) { - if (targets[i].second.size() == 0) { - params["objects"][targets[i].first] = json::value_t::null; - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] added filter (all): " << targets[i].first; - wcp_loger.add_log("added filter (all): " + targets[i].first, false, "", "Moonraker_Mqtt", "info"); - } else { - params["objects"][targets[i].first] = json::array(); - - for (const auto& key : targets[i].second) { - params["objects"][targets[i].first].push_back(key); - } - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] added filter: " << targets[i].first - << ", field count: " << targets[i].second.size(); - wcp_loger.add_log("added filter: " + targets[i].first + ", field count: " + std::to_string(targets[i].second.size()), false, "", "Moonraker_Mqtt", "info"); - } - } - - if (!send_to_request(method, params, true, callback, [callback, &wcp_loger](){ - BOOST_LOG_TRIVIAL(warning) << "[Moonraker_Mqtt] set subscribe filter timed out"; - wcp_loger.add_log("set subscribe filter timed out", false, "", "Moonraker_Mqtt", "warning"); - json res; - res["error"] = "timeout"; - callback(res); - }) && callback) { - BOOST_LOG_TRIVIAL(error) << "[Moonraker_Mqtt] failed to send set subscribe filter request"; - wcp_loger.add_log("failed to send set subscribe filter request", false, "", "Moonraker_Mqtt", "error"); - callback(json::value_t::null); - } -} - void Moonraker_Mqtt::async_machine_files_roots(std::function callback) { auto& wcp_loger = GUI::WCP_Logger::getInstance(); BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] Starting get file system roots"; @@ -2617,75 +2507,6 @@ void Moonraker_Mqtt::async_cancel_pull_cloud_file(std::function callback) -{ - auto& wcp_loger = GUI::WCP_Logger::getInstance(); - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] Starting query firmware info"; - wcp_loger.add_log("Starting query firmware info", false, "", "Moonraker_Mqtt", "info"); - std::string method = "system.get_device_info"; - - json params; - - if (!send_to_request(method, params, true, callback, - [callback, &wcp_loger]() { - BOOST_LOG_TRIVIAL(warning) << "[Moonraker_Mqtt] query firmware info timed out"; - wcp_loger.add_log("query firmware info timed out", false, "", "Moonraker_Mqtt", "warning"); - json res; - res["error"] = "timeout"; - callback(res); - }) && - callback) { - BOOST_LOG_TRIVIAL(error) << "[Moonraker_Mqtt] failed to send query firmware info request"; - wcp_loger.add_log("failed to send query firmware info request", false, "", "Moonraker_Mqtt", "error"); - callback(json::value_t::null); - } -} - -// Query printer information -void Moonraker_Mqtt::async_get_machine_info( - const std::vector>>& targets, - std::function callback) -{ - auto& wcp_loger = GUI::WCP_Logger::getInstance(); - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] Starting query printer info, target count: " << targets.size(); - wcp_loger.add_log("Starting query printer info, target count: " + std::to_string(targets.size()), false, "", "Moonraker_Mqtt", "info"); - std::string method = "printer.objects.query"; - - json params; - params["objects"] = json::object(); - - for (size_t i = 0; i < targets.size(); ++i) { - if (targets[i].second.size() == 0) { - params["objects"][targets[i].first] = json::value_t::null; - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] query object (all properties): " << targets[i].first; - wcp_loger.add_log("query object (all properties): " + targets[i].first, false, "", "Moonraker_Mqtt", "info"); - } else { - params["objects"][targets[i].first] = json::array(); - - for (const auto& key : targets[i].second) { - params["objects"][targets[i].first].push_back(key); - } - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] query object: " << targets[i].first - << ", property count: " << targets[i].second.size(); - wcp_loger.add_log("query object: " + targets[i].first + ", property count: " + std::to_string(targets[i].second.size()), false, "", "Moonraker_Mqtt", "info"); - } - } - - if (!send_to_request(method, params, true, callback, [callback, &wcp_loger](){ - BOOST_LOG_TRIVIAL(warning) << "[Moonraker_Mqtt] query printer info timed out"; - wcp_loger.add_log("query printer info timed out", false, "", "Moonraker_Mqtt", "warning"); - json res; - res["error"] = "timeout"; - callback(res); - }) && callback) { - BOOST_LOG_TRIVIAL(error) << "[Moonraker_Mqtt] failed to send query printer info request"; - wcp_loger.add_log("failed to send query printer info request", false, "", "Moonraker_Mqtt", "error"); - callback(json::value_t::null); - } -} - // Get File state void Moonraker_Mqtt::async_server_files_get_status(std::function callback) { @@ -2710,50 +2531,6 @@ void Moonraker_Mqtt::async_server_files_get_status(std::function callback) -{ - auto& wcp_loger = GUI::WCP_Logger::getInstance(); - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] Starting get system info"; - wcp_loger.add_log("Starting get system info", false, "", "Moonraker_Mqtt", "info"); - std::string method = "machine.system_info"; - json params = json::object(); - - if (!send_to_request(method, params, true, callback, [callback, &wcp_loger](){ - BOOST_LOG_TRIVIAL(warning) << "[Moonraker_Mqtt] get system info timed out"; - wcp_loger.add_log("get system info timed out", false, "", "Moonraker_Mqtt", "warning"); - json res; - res["error"] = "timeout"; - callback(res); - }) && callback) { - BOOST_LOG_TRIVIAL(error) << "[Moonraker_Mqtt] failed to send get system info request"; - wcp_loger.add_log("failed to send get system info request", false, "", "Moonraker_Mqtt", "error"); - callback(json::value_t::null); - } -} - -// Get list of available printer objects -void Moonraker_Mqtt::async_get_machine_objects(std::function callback) -{ - auto& wcp_loger = GUI::WCP_Logger::getInstance(); - BOOST_LOG_TRIVIAL(info) << "[Moonraker_Mqtt] Starting get available printer object list"; - wcp_loger.add_log("Starting get available printer object list", false, "", "Moonraker_Mqtt", "info"); - std::string method = "printer.objects.list"; - json params = json::object(); - - if (!send_to_request(method, params, true, callback, [callback, &wcp_loger](){ - BOOST_LOG_TRIVIAL(warning) << "[Moonraker_Mqtt] get printer object list timed out"; - wcp_loger.add_log("get printer object list timed out", false, "", "Moonraker_Mqtt", "warning"); - json res; - res["error"] = "timeout"; - callback(res); - }) && callback) { - BOOST_LOG_TRIVIAL(error) << "[Moonraker_Mqtt] failed to send get printer object list request"; - wcp_loger.add_log("failed to send get printer object list request", false, "", "Moonraker_Mqtt", "error"); - callback(json::value_t::null); - } -} - // Send request to printer via MQTT bool Moonraker_Mqtt::send_to_request( const std::string& method, diff --git a/src/slic3r/Utils/MoonRaker.hpp b/src/slic3r/Utils/MoonRaker.hpp index a4083480c910..75fbe1669d89 100644 --- a/src/slic3r/Utils/MoonRaker.hpp +++ b/src/slic3r/Utils/MoonRaker.hpp @@ -43,9 +43,6 @@ class Moonraker : public PrintHost // Send G-code commands to printer bool send_gcodes(const std::vector& codes, std::string& extraInfo) override; - // Get printer information - bool get_machine_info(const std::vector>>& targets, nlohmann::json& response) override; - // Configuration getters bool has_auto_discovery() const override { return true; } bool can_test() const override { return true; } @@ -61,15 +58,8 @@ class Moonraker : public PrintHost virtual bool connect(wxString& msg, const nlohmann::json& params) override; virtual bool disconnect(wxString& msg, const nlohmann::json& params) override { return true; } - // Async printer information methods - virtual void async_get_system_info(std::function callback) override {} virtual void async_server_files_get_status(std::function callback) override {} - virtual void async_get_machine_info(const std::vector>>& targets, std::function) override {} - virtual void async_get_device_info(std::function) override {} virtual void async_subscribe_machine_info(const std::string& hash, std::function) override {} - virtual void async_get_machine_objects(std::function)override {} - virtual void async_set_machine_subscribe_filter(const std::vector>>& targets, - std::function callback) override {} virtual void async_unsubscribe_machine_info(const std::string& hash, std::function) override {} virtual void async_send_gcodes(const std::vector& scripts, std::function) override{} @@ -239,18 +229,10 @@ class Moonraker_Mqtt : public Moonraker virtual bool connect(wxString& msg, const nlohmann::json& params) override; virtual bool disconnect(wxString& msg, const nlohmann::json& params) override; - // Override async information methods - virtual void async_get_system_info(std::function callback) override; virtual void async_server_files_get_status(std::function callback) override; - virtual void async_get_machine_info(const std::vector>>& targets, std::function callback) override; - virtual void async_get_device_info(std::function callback) override; virtual void async_subscribe_machine_info(const std::string& hash, std::function) override; - virtual void async_get_machine_objects(std::function callback) override; - virtual void async_set_machine_subscribe_filter(const std::vector>>& targets, - std::function callback) override; virtual void async_unsubscribe_machine_info(const std::string& hash, std::function) override; virtual void async_send_gcodes(const std::vector& scripts, std::function) override; - virtual void async_get_printer_info(std::function callback) override; virtual void async_start_print_job(const std::string& filename, std::function cb) override; diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index c10a09895d47..d15d3619cbf9 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -57,7 +57,7 @@ using Slic3r::GUI::Config::SnapshotDB; namespace Slic3r { namespace { -// While profile `MsgUpdateConfig` is shown, Flutter `load_flutter_web` skips its own `MsgUpdateConfig` (no stacked modals). +// While profile `MsgUpdateConfig` is shown, nested profile checks are suppressed. struct ProfileConfigUpdateDlgScope { Slic3r::GUI::GUI_App *app; explicit ProfileConfigUpdateDlgScope(Slic3r::GUI::GUI_App *a) : app(a) @@ -74,23 +74,6 @@ struct ProfileConfigUpdateDlgScope { ProfileConfigUpdateDlgScope &operator=(const ProfileConfigUpdateDlgScope &) = delete; }; -// Covers `MsgUpdateConfig::ShowModal()` in `load_flutter_web`; profiles CallAfter checks this (atomic). -struct FlutterWebConfigUpdateDlgScope { - Slic3r::GUI::GUI_App *app; - explicit FlutterWebConfigUpdateDlgScope(Slic3r::GUI::GUI_App *p) : app(p) - { - if (app) - app->set_flutter_web_config_update_dlg_open(true); - } - ~FlutterWebConfigUpdateDlgScope() - { - if (app) - app->set_flutter_web_config_update_dlg_open(false); - } - FlutterWebConfigUpdateDlgScope(const FlutterWebConfigUpdateDlgScope &) = delete; - FlutterWebConfigUpdateDlgScope &operator=(const FlutterWebConfigUpdateDlgScope &) = delete; -}; - } // namespace static const char *INDEX_FILENAME = "index.idx"; @@ -167,13 +150,6 @@ struct Update { if (is_directory) { auto validate_staging = [this](const fs::path &staging) -> bool { - if (vendor == "flutter_web") { - if (!fs::exists(staging / "version.json")) { - BOOST_LOG_TRIVIAL(error) << "Web resource staging is missing version.json"; - return false; - } - return true; - } if (vendor.empty()) return true; @@ -245,7 +221,6 @@ struct Updates }; wxDEFINE_EVENT(EVT_REQUEST_SERVER_FAIL, wxCommandEvent); -wxDEFINE_EVENT(EVT_NO_WEB_RESOURCE_UPDATE, wxCommandEvent); wxDEFINE_EVENT(EVT_NO_PRESET_UPDATE, wxCommandEvent); wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); @@ -266,8 +241,6 @@ struct PresetUpdater::priv bool cancel; std::thread thread; - bool m_web_thread_cancel; - std::thread m_web_resource_thread; bool has_waiting_updates { false }; Updates waiting_updates; bool has_waiting_printer_updates { false }; @@ -294,8 +267,6 @@ struct PresetUpdater::priv void parse_version_string(const std::string& body) const; void sync_resources(std::string http_url, std::map &resources, bool check_patch = false, std::string current_version="", std::string changelog_file=""); void sync_config(bool isAuto_check = true); - void sync_update_flutter_resource(bool isAuto_check = true); - void download_flutter_resource_async(const std::string& url, const std::string& target_path, const std::string& version, bool isAuto_check); void download_profiles_resource_async(const std::string& url, const std::string& target_path, const std::string& version, bool isAuto_check); bool download_file(const std::string& url, const std::string& target_path, @@ -799,71 +770,6 @@ bool PresetUpdater::priv::download_file(const std::string& url, } -void PresetUpdater::priv::download_flutter_resource_async(const std::string& url, const std::string& target_path, const std::string& version, bool isAuto_check) -{ - bool res = false; - fs::path tmp_path = target_path + ".tmp"; - - Slic3r::Http::get(url) - .on_progress([this](Slic3r::Http::Progress progress, bool& cancel_http) { - if (m_web_thread_cancel) { - cancel_http = true; - } - }) - .on_error([this, target_path, isAuto_check](std::string body, std::string error, unsigned http_status) { - BOOST_LOG_TRIVIAL(error) << "[Flutter Updater] Download failed: " << target_path << ", HTTP status: " << http_status << ", error: " << error; - if (!isAuto_check) { - wxCommandEvent* evt = new wxCommandEvent(EVT_REQUEST_SERVER_FAIL); - wxString errorMsg = wxString::Format(_L("Flutter resource download failed: %s"), error); - evt->SetString(errorMsg); - GUI::wxGetApp().QueueEvent(evt); - } - }) - .on_complete([this, target_path, tmp_path, version, isAuto_check](std::string body, unsigned http_status) { - if (http_status != 200) { - BOOST_LOG_TRIVIAL(error) << "[Flutter Updater] Download failed with HTTP status: " << http_status; - return; - } - - // Save file - fs::path target(target_path); - if (!fs::exists(target.parent_path())) { - fs::create_directories(target.parent_path()); - } - - fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); - if (!file.is_open()) { - BOOST_LOG_TRIVIAL(error) << "[Flutter Updater] Failed to open file for writing: " << tmp_path; - return; - } - file.write(body.c_str(), body.size()); - file.close(); - - boost::system::error_code ec; - fs::rename(tmp_path, target_path, ec); - if (ec) { - BOOST_LOG_TRIVIAL(error) << "[Flutter Updater] Failed to rename temp file: " << ec.message(); - return; - } - - BOOST_LOG_TRIVIAL(info) << "[Flutter Updater] Download completed: " << target_path; - - // Extract zip on worker thread. Zip entries use "flutter_web/..." prefix, so extract to parent dir. - auto parent_path = fs::path(target_path).parent_path(); - auto flutter_dir = (parent_path / "flutter_web").string(); - if (!extract_file(target_path, parent_path.string())) { - BOOST_LOG_TRIVIAL(error) << "[Flutter Updater] Failed to extract file: " << target_path; - return; - } - - GUI::wxGetApp().CallAfter([flutter_dir]() { - GUI::wxGetApp().preset_updater->load_flutter_web(flutter_dir, true); - }); - }) - .timeout_max(60) - .perform_sync(); -} - void PresetUpdater::priv::download_profiles_resource_async(const std::string& url, const std::string& target_path, const std::string& version, bool isAuto_check) { bool res = false; @@ -947,10 +853,6 @@ void PresetUpdater::priv::download_profiles_resource_async(const std::string& ur GUI::wxGetApp().CallAfter([isAuto_check]() { Slic3r::GUI::GUI_App &app = GUI::wxGetApp(); - if (app.flutter_web_config_update_dlg_open()) { - app.check_config_updates_from_updater(false); - return; - } app.check_config_updates_from_updater(!isAuto_check); }); }) @@ -959,130 +861,7 @@ void PresetUpdater::priv::download_profiles_resource_async(const std::string& ur } -void PresetUpdater::priv::sync_update_flutter_resource(bool isAuto_check) -{ - auto cache_profile_path = cache_path; - - AppConfig* app_config = GUI::wxGetApp().app_config; - - auto preset_update_url = app_config->get_web_resource_upgrade_url(); - - Http::get(preset_update_url) - .on_error([cache_profile_path, isAuto_check](std::string body, std::string error, unsigned http_status) { - wxCommandEvent* evt = new wxCommandEvent(EVT_REQUEST_SERVER_FAIL); - wxString errorMsg = wxString::Format(_L("request to server update web resource fail with body:%s,error:%s,status:%d"), body, error, http_status); - evt->SetString(errorMsg); - if (!isAuto_check) - GUI::wxGetApp().QueueEvent(evt); - BOOST_LOG_TRIVIAL(info) << format("Error getting: `%1%`: HTTP %2%, %3%", "sync_update_flutter_resource", http_status, error); - }) - .timeout_connect(TIMEOUT_CONNECT) - .on_complete([this, cache_profile_path, isAuto_check](std::string body, unsigned http_status) { - // Http response OK - if (http_status != 200) - return; - - json jsonObj = json::parse(body); - auto errCode = jsonObj["code"]; - if (errCode != 200) - return; - - auto dataObj = jsonObj.value("data", json::object()); - auto buildNumer = dataObj.value("build_number", ""); - auto minSupportPcVersion = dataObj.value("min_support_pc_version", ""); - auto maxSupportPcVersion = dataObj.value("max_support_pc_version", ""); - - auto isForceUpgrade = dataObj.value("is_force_upgrade", false); - auto fileVersion = dataObj.value("file_version", ""); - auto fileSize = dataObj.value("file_size", 0); - auto fileMd5 = dataObj.value("file_md5", ""); - auto fileSha256 = dataObj.value("file_sha256", ""); - auto fileUrl = dataObj.value("file_url", ""); - auto description = dataObj.value("file_describe", ""); - auto reservedData = dataObj.value("reserved_1", ""); - auto reservedData2 = dataObj.value("reserved_2", ""); - - auto localProfilesjson = cache_path / "flutter_web/version.json"; - std::string json_path = data_dir() + "/web/flutter_web/version.json"; - // Use a unique filename per version to avoid deleting a zip that may still be in use (Windows file locks / concurrent UI import). - std::string fileName = (cache_profile_path / ("flutter_web_" + fileVersion + ".zip")).string(); - Semver currentPresetVersion = get_version_from_json(json_path); - Semver remoteVersion(fileVersion); - Semver minSpVersion(minSupportPcVersion); - Semver maxSpVersion(maxSupportPcVersion); - std::regex matcher("[0-9]+\\.[0-9]+(\\.[0-9]+)*(-[A-Za-z0-9]+)?(\\+[A-Za-z0-9]+)?"); - - auto get_version = [this](const std::string& str, const std::regex& regexp) - { - std::smatch match; - if (std::regex_match(str, match, regexp)) { - std::string version_cleaned = match[0]; - const boost::optional version = Semver::parse(version_cleaned); - if (version.has_value()) { - return *version; - } - } - return Semver::invalid(); - }; - - Semver currentSoftVersion = get_version(Snapmaker_VERSION, matcher); - - if (fileVersion.empty()) - { - if (!isAuto_check) { - wxCommandEvent* evt = new wxCommandEvent(EVT_NO_WEB_RESOURCE_UPDATE); - GUI::wxGetApp().QueueEvent(evt); - - BOOST_LOG_TRIVIAL(info) << format("use check the web update."); - } - return; - } - - bool maxRes = false; - bool minRes = false; - - if (!maxSupportPcVersion.empty()) - { - maxRes = currentSoftVersion > maxSpVersion; - } - - if (!minSupportPcVersion.empty()) - { - minRes = currentSoftVersion < minSpVersion; - } - - if (maxRes || minRes) { - if (!isAuto_check) { - wxCommandEvent* evt = new wxCommandEvent(EVT_NO_WEB_RESOURCE_UPDATE); - GUI::wxGetApp().QueueEvent(evt); - - BOOST_LOG_TRIVIAL(info) << format("use check the web update."); - } - return; - } - - if (currentPresetVersion < remoteVersion) { - fs::path tmpPath = fileName; - auto dirPath = tmpPath.parent_path() / "flutter_web"; - - if (fs::exists(dirPath)) - fs::remove_all(dirPath); - - // Download file asynchronously to avoid blocking UI - download_flutter_resource_async(fileUrl, fileName, fileVersion, isAuto_check); - } - else { - if (!isAuto_check) { - wxCommandEvent* evt = new wxCommandEvent(EVT_NO_WEB_RESOURCE_UPDATE); - GUI::wxGetApp().QueueEvent(evt); - - BOOST_LOG_TRIVIAL(info) << format("use check the web update local no profiles."); - } - } - }) - .perform_sync(); -} - // Orca: sync config update for currect App version +// Orca: sync config update for currect App version void PresetUpdater::priv::sync_config(bool isAuto_check) { auto cache_profile_path = cache_path; @@ -1908,11 +1687,6 @@ PresetUpdater::~PresetUpdater() p->thread.join(); } - if (p && p->m_web_resource_thread.joinable()) - { - p->m_web_thread_cancel = true; - p->m_web_resource_thread.join(); - } } //BBS: change directories by design @@ -2075,21 +1849,6 @@ bool PresetUpdater::install_bundles_rsrc(std::vector bundles, bool return p->install_bundles_rsrc(bundles, snapshot); } -void PresetUpdater::sync_web_async(bool isAutoUpdata) -{ - if (p->m_web_resource_thread.joinable()) { - p->m_web_thread_cancel = true; - p->m_web_resource_thread.join(); - } - - p->m_web_thread_cancel = false; - p->m_web_resource_thread = std::thread([this, isAutoUpdata]() { - BOOST_LOG_TRIVIAL(debug) << "[Orca Updater] sync_web_async started"; - this->p->sync_update_flutter_resource(isAutoUpdata); - // Note: load_flutter_web will be called automatically after download completes in download_flutter_resource_async - }); -} - void PresetUpdater::sync_config_async() { if (p->thread.joinable()) { @@ -2173,194 +1932,6 @@ bool PresetUpdater::version_check_enabled() const } -void PresetUpdater::load_flutter_web(const std::string& resource_path, bool serverUpdate) -{ - boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / "orca_temp_flutter_import"; - boost::filesystem::path flutter_root; - bool cleanup_temp = false; - { - if (serverUpdate && boost::filesystem::is_directory(resource_path)) { - // Server OTA flow: zip was already extracted in worker thread. - flutter_root = boost::filesystem::path(resource_path); - } else { - if (boost::filesystem::exists(temp_path)) - boost::filesystem::remove_all(temp_path); - boost::filesystem::create_directories(temp_path); - cleanup_temp = true; - - if (!p->extract_file(resource_path, temp_path.string())) { - if (!serverUpdate) - { - BOOST_LOG_TRIVIAL(error) << "extract_file Failed "; - GUI::MessageDialog(nullptr, _L("Import Failed")).ShowModal(); - } - return; - } - - flutter_root = temp_path / "flutter_web"; - } - - if (!boost::filesystem::exists(flutter_root / "version.json")) { - BOOST_LOG_TRIVIAL(error) << "Failed to import web resources: missing version.json in " << flutter_root.string(); - if (!serverUpdate) - GUI::MessageDialog(nullptr, _L("Import Failed")).ShowModal(); - return; - } - - std::vector outdated_presets; - Updates updates; - - auto app = dynamic_cast(wxTheApp); - if (!app) { - BOOST_LOG_TRIVIAL(error) << "update source on web fail "; - GUI::MessageDialog(nullptr, _L("Import Failed")).ShowModal(); - } - - std::string ori_version_str = "0"; - std::string ori_build_number_str = "0"; - - auto ori_version_file = boost::filesystem::path(data_dir()) / "web" / "flutter_web" / "version.json"; - boost::property_tree::ptree ori_config; - boost::property_tree::read_json(ori_version_file.string(), ori_config); - ori_version_str = ori_config.get("version", "0"); - ori_build_number_str = ori_config.get("build_number", "0"); - - { - const fs::path version_path = flutter_root / "version.json"; - boost::property_tree::ptree config; - boost::property_tree::read_json(version_path.string(), config); - std::string version_str = config.get("version", "0"); - std::string build_number_str = config.get("build_number", "0"); - - Semver online_version = version_str; - Semver current_version = ori_version_str; - - if (current_version < online_version) { - auto source_folder_path = flutter_root; - auto target_folder_path = (boost::filesystem::path(data_dir()) / "web" / "flutter_web"); - - Version version; - version.config_version = online_version; - - // changelog - std::string changelog = ""; - fs::path changelog_path = version_path; - std::string changelog_file = changelog_path.replace_extension(".changelog").string(); - boost::nowide::ifstream ifs(changelog_file); - if (ifs) { - std::ostringstream oss; - oss << ifs.rdbuf(); - changelog = oss.str(); - ifs.close(); - size_t pos = 0; - while ((pos = changelog.find("\\n", pos)) != std::string::npos) { - changelog.replace(pos, 2, "\n"); - pos += 1; - } - } - - Semver min_ver = get_min_version_from_json(version_path.string()); - Semver soft_ver = Semver(std::string(Snapmaker_VERSION)); - - bool legal = true; - legal = min_ver <= soft_ver; - if (!legal) { - changelog += ("\nSnapmaker Orca " + min_ver.to_string() + " " + _L("needed, but current version is ") + - soft_ver.to_string() + "\n") - .ToStdString(); - } - - updates.updates.emplace_back(std::move(source_folder_path), std::move(target_folder_path), version, "flutter_web", - changelog, "", false, true, legal); - - } else { - outdated_presets.push_back("flutter_web"); - } - - } - - bool need_restart = false; - if (!updates.updates.empty()) { - std::vector updates_msg; - for (const auto& update : updates.updates) { - // BBS: skip directory - if (!update.is_directory) - continue; - - if (update.can_install) { - need_restart = true; - } - std::string changelog = update.change_log; - updates_msg.emplace_back(update.vendor, update.version.config_version, update.descriptions, std::move(changelog)); - } - - if (app && app->profile_config_update_dlg_open()) { - BOOST_LOG_TRIVIAL(info) << "[Flutter Updater] Skipping web resource configuration dialog: profile configuration dialog is active."; - if (cleanup_temp) - boost::filesystem::remove_all(temp_path); - return; - } - - int res = wxID_CANCEL; - { - FlutterWebConfigUpdateDlgScope flutter_dlg_scope(app); - GUI::MsgUpdateConfig dlg(updates_msg); - res = dlg.ShowModal(); - } - - if (res == wxID_OK) { - if (!p->perform_updates(std::move(updates))) { - BOOST_LOG_TRIVIAL(error) << "[Flutter Updater] Failed to install web resources"; - GUI::MessageDialog(nullptr, _L("Import Failed")).ShowModal(); - return; - } - } else { - if (cleanup_temp) - boost::filesystem::remove_all(temp_path); - return; - } - } - - wxString message; - if (!outdated_presets.empty() && !serverUpdate) { - message = _L("This web resouce could not be imported due to outdated versions.") + "\n"; - for (const auto& preset : outdated_presets) { - message += "• " + preset + "\n"; - } - GUI::MessageDialog(nullptr, message).ShowModal(); - } - - if (need_restart) { - GUI::MessageDialog msg_wingow(nullptr, - _L("Updating the web resources requires application restart.") + "\n" + - _L("Do you want to continue?"), - L("Snapmaker Orca"), wxICON_QUESTION | wxOK | wxCANCEL); - if (msg_wingow.ShowModal() == wxID_CANCEL) { - return; - } - - app->schedule_recreate_gui_when_no_modal(_L("Update web resources")); - } - } - - if (cleanup_temp) - boost::filesystem::remove_all(temp_path); -} - - -void PresetUpdater::import_flutter_web() -{ - wxFileDialog dialog(nullptr, _L("Please choose a web resource package file:"), "", "", "resource packages (*.zip)|*.zip", - wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() != wxID_OK) - return; - - std::string zip_file = dialog.GetPath().ToUTF8().data(); - - load_flutter_web(zip_file); -} - void PresetUpdater::import_system_profile() { wxFileDialog dialog(nullptr, _L("Please choose a system profile package file:"), "", "", "Profile packages (*.zip)|*.zip", diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index a0432d7b8677..67385cd902df 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -63,21 +63,12 @@ class PresetUpdater void import_system_profile(); - void import_flutter_web(); - - // serverUpdate=true: pass extracted flutter_web directory. - // serverUpdate=false: pass zip file chosen by user. - void load_flutter_web(const std::string& resource_path, bool serverUpdate = false); - void sync_config_async(); - - void sync_web_async(bool isAutoUpdata = false); public: struct priv; std::unique_ptr p; }; wxDECLARE_EVENT(EVT_REQUEST_SERVER_FAIL, wxCommandEvent); -wxDECLARE_EVENT(EVT_NO_WEB_RESOURCE_UPDATE, wxCommandEvent); wxDECLARE_EVENT(EVT_NO_PRESET_UPDATE, wxCommandEvent); wxDECLARE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); wxDECLARE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); diff --git a/src/slic3r/Utils/PrintHost.hpp b/src/slic3r/Utils/PrintHost.hpp index 039b3ee7efac..b463d89f50ee 100644 --- a/src/slic3r/Utils/PrintHost.hpp +++ b/src/slic3r/Utils/PrintHost.hpp @@ -78,23 +78,8 @@ class PrintHost virtual bool send_gcodes(const std::vector& codes, std::string& extraInfo) { return false; } - virtual bool get_machine_info(const std::vector>>& targets, nlohmann::json& response) { return false; } - - virtual void async_get_system_info(std::function callback){} - virtual void async_server_files_get_status(std::function callback) {} - virtual void async_get_machine_info(const std::vector>>& targets, std::function) {} - - virtual void async_get_device_info(std::function) {} - - virtual void async_get_machine_objects(std::function) {} - - virtual void async_get_printer_info(std::function) {} - - virtual void async_set_machine_subscribe_filter(const std::vector>>& targets, - std::function callback) {} - virtual void async_unsubscribe_machine_info(const std::string& hash, std::function) {} virtual void async_subscribe_machine_info(const std::string& hash, std::function) {} diff --git a/tests/libslic3r/test_sswcp_protocol.cpp b/tests/libslic3r/test_sswcp_protocol.cpp index 047e83ab1979..abc139aac43f 100644 --- a/tests/libslic3r/test_sswcp_protocol.cpp +++ b/tests/libslic3r/test_sswcp_protocol.cpp @@ -12,16 +12,14 @@ using namespace Slic3r; TEST_CASE("SSWCP file flow mappings are aligned and normalized", "[SSWCPProtocol]") { - CHECK(SSWCPProtocol::build_filament_volume_types(nullptr, 3) == - std::vector{"standard", "standard", "standard"}); + CHECK(SSWCPProtocol::build_filament_volume_types(nullptr, 3) == std::vector{"standard", "standard", "standard"}); const std::vector values{fvtStandard, fvtHighFlow}; CHECK(SSWCPProtocol::build_filament_volume_types(&values, 4) == std::vector{"standard", "high_flow", "standard", "standard"}); const std::vector unknown{fvtHighFlow, 99}; - CHECK(SSWCPProtocol::build_filament_volume_types(&unknown, 2) == - std::vector{"high_flow", "standard"}); + CHECK(SSWCPProtocol::build_filament_volume_types(&unknown, 2) == std::vector{"high_flow", "standard"}); } TEST_CASE("SSWCP machine update flow types are optional but validated", "[SSWCPProtocol]") @@ -31,87 +29,36 @@ TEST_CASE("SSWCP machine update flow types are optional but validated", "[SSWCPP SSWCPProtocol::OptionalFlowTypesStatus::Missing); const json valid = {{"nozzle_volume_types", {"standard", "high_flow"}}}; - CHECK(SSWCPProtocol::parse_optional_flow_types(valid, "nozzle_volume_types", 2, flows) == - SSWCPProtocol::OptionalFlowTypesStatus::Valid); + CHECK(SSWCPProtocol::parse_optional_flow_types(valid, "nozzle_volume_types", 2, flows) == SSWCPProtocol::OptionalFlowTypesStatus::Valid); CHECK(flows == std::vector{"standard", "high_flow"}); - for (const json &invalid : std::vector{ - {{"nozzle_volume_types", "standard"}}, - {{"nozzle_volume_types", {"standard"}}}, - {{"nozzle_volume_types", {"standard", 1}}}, - {{"nozzle_volume_types", {"standard", "tpu_high_flow"}}}}) { + for (const json& invalid : std::vector{{{"nozzle_volume_types", "standard"}}, + {{"nozzle_volume_types", {"standard"}}}, + {{"nozzle_volume_types", {"standard", 1}}}, + {{"nozzle_volume_types", {"standard", "tpu_high_flow"}}}}) { CHECK(SSWCPProtocol::parse_optional_flow_types(invalid, "nozzle_volume_types", 2, flows) == SSWCPProtocol::OptionalFlowTypesStatus::Invalid); CHECK(flows.empty()); } } -TEST_CASE("SSWCP system info preserves supported scalar and array nozzle data", "[SSWCPProtocol]") -{ - const json response = {{"data", {{"system_info", {{"product_info", { - {"machine_type", "Snapmaker U1"}, - {"device_name", "Workshop"}, - {"nozzle_diameter", {0.2, 0.4, 0.6, 0.8}}, - {"nozzle_volume_type", {"standard", "high_flow", "standard", "high_flow"}} - }}}}}}}; - - MachineInfo info; - REQUIRE(SSWCPProtocol::parse_machine_info_response(response, info)); - CHECK(info.model == "Snapmaker U1"); - CHECK(info.device_name == "Workshop"); - CHECK(info.nozzle_diameters == std::vector{"0.2", "0.4", "0.6", "0.8"}); - CHECK(info.nozzle_volume_types == - std::vector{"standard", "high_flow", "standard", "high_flow"}); - - const json scalar = {{"system_info", {{"product_info", { - {"nozzle_diameter", 0.8}, {"nozzle_volume_type", "high_flow"} - }}}}}; - REQUIRE(SSWCPProtocol::parse_machine_info_response(scalar, info)); - CHECK(info.nozzle_diameters == std::vector{"0.8"}); - CHECK(info.nozzle_volume_types == std::vector{"high_flow"}); - - for (const auto &[diameter, expected] : std::vector>{ - {0.2, "0.2"}, {0.4, "0.4"}, {0.6, "0.6"}, {0.8, "0.8"}}) { - const json single = {{"system_info", {{"product_info", {{"nozzle_diameter", diameter}}}}}}; - REQUIRE(SSWCPProtocol::parse_machine_info_response(single, info)); - CHECK(info.nozzle_diameters == std::vector{expected}); - } -} - -TEST_CASE("SSWCP system info tolerates unavailable flow data", "[SSWCPProtocol]") -{ - MachineInfo info; - const json missing = {{"system_info", {{"product_info", {{"machine_type", "Snapmaker U1"}}}}}}; - REQUIRE(SSWCPProtocol::parse_machine_info_response(missing, info)); - CHECK(info.nozzle_volume_types.empty()); - - const json invalid = {{"system_info", {{"product_info", { - {"machine_type", "Snapmaker U1"}, {"nozzle_volume_type", {"standard", "tpu_high_flow"}} - }}}}}; - REQUIRE(SSWCPProtocol::parse_machine_info_response(invalid, info)); - CHECK(info.nozzle_volume_types.empty()); -} - TEST_CASE("Complete cached slots override direct nozzle data atomically", "[SSWCPProtocol]") { std::vector diameters{"0.8"}; std::vector flows{"high_flow"}; - REQUIRE(SSWCPProtocol::select_complete_cached_nozzle_info( - {{"0.4", "standard"}, {"0.4", "high_flow"}}, diameters, flows)); + REQUIRE(SSWCPProtocol::select_complete_cached_nozzle_info({{"0.4", "standard"}, {"0.4", "high_flow"}}, diameters, flows)); CHECK(diameters == std::vector{"0.4", "0.4"}); CHECK(flows == std::vector{"standard", "high_flow"}); diameters = {"0.8"}; flows = {"high_flow"}; - CHECK_FALSE(SSWCPProtocol::select_complete_cached_nozzle_info( - {{"0.4", "standard"}, {"", "high_flow"}}, diameters, flows)); + CHECK_FALSE(SSWCPProtocol::select_complete_cached_nozzle_info({{"0.4", "standard"}, {"", "high_flow"}}, diameters, flows)); CHECK(diameters == std::vector{"0.8"}); CHECK(flows == std::vector{"high_flow"}); diameters = {"0.8"}; flows = {"high_flow"}; - REQUIRE(SSWCPProtocol::select_complete_cached_nozzle_info( - {{"0.4", "standard"}, {"0.4", ""}}, diameters, flows)); + REQUIRE(SSWCPProtocol::select_complete_cached_nozzle_info({{"0.4", "standard"}, {"0.4", ""}}, diameters, flows)); CHECK(diameters == std::vector{"0.4", "0.4"}); // Incomplete cached flows must not wipe freshly resolved direct values (see SSWCPProtocol.hpp). CHECK(flows == std::vector{"high_flow"}); @@ -122,7 +69,7 @@ TEST_CASE("tpu_high_flow is never emitted in any protocol output", "[SSWCPProtoc // File mapping: any non-fvtHighFlow value (including future enum additions) maps to standard. const std::vector with_unknown{fvtHighFlow, 2, 99, fvtStandard}; const auto mapped = SSWCPProtocol::build_filament_volume_types(&with_unknown, 4); - for (const std::string &v : mapped) + for (const std::string& v : mapped) CHECK(v != "tpu_high_flow"); // Machine update: tpu_high_flow is explicitly rejected as Invalid. @@ -130,16 +77,6 @@ TEST_CASE("tpu_high_flow is never emitted in any protocol output", "[SSWCPProtoc const json bad_update = {{"nozzle_volume_types", {"tpu_high_flow", "standard"}}}; CHECK(SSWCPProtocol::parse_optional_flow_types(bad_update, "nozzle_volume_types", 2, flows) == SSWCPProtocol::OptionalFlowTypesStatus::Invalid); - - // System info: tpu_high_flow leaves flow unavailable, never stored. - MachineInfo info; - const json bad_system = {{"system_info", {{"product_info", { - {"machine_type", "X"}, {"nozzle_volume_type", {"tpu_high_flow", "high_flow"}} - }}}}}; - REQUIRE(SSWCPProtocol::parse_machine_info_response(bad_system, info)); - CHECK(info.nozzle_volume_types.empty()); - for (const std::string &v : info.nozzle_volume_types) - CHECK(v != "tpu_high_flow"); } TEST_CASE("Machine model normalization handles aliases and preserves empty", "[SSWCPProtocol]") @@ -159,97 +96,3 @@ TEST_CASE("Machine model normalization handles aliases and preserves empty", "[S // Unknown models pass through as-is. CHECK(SSWCPProtocol::normalize_machine_model("SomeFuturePrinter") == "SomeFuturePrinter"); } - -TEST_CASE("parse_extruder_nozzle_info extracts real-time nozzle data", "[SSWCPProtocol]") -{ - json response = R"({ - "data": { - "status": { - "extruder": { - "nozzle_diameter": "0.4", - "nozzle_volume_type": "standard" - }, - "extruder1": { - "nozzle_diameter": 0.6, - "nozzle_volume_type": "high_flow" - } - } - } - })"_json; - - std::vector diameters, flows; - CHECK(SSWCPProtocol::parse_extruder_nozzle_info(response, diameters, flows)); - REQUIRE(diameters.size() == 2); - CHECK(diameters[0] == "0.4"); - CHECK(diameters[1] == "0.6"); - REQUIRE(flows.size() == 2); - CHECK(flows[0] == "standard"); - CHECK(flows[1] == "high_flow"); - - // Missing status -> false - json empty = json::object(); - CHECK(!SSWCPProtocol::parse_extruder_nozzle_info(empty, diameters, flows)); - - // Invalid diameter value -> skipped, returns false when none valid - json bad = json::parse(R"({"data":{"status":{"extruder":{"nozzle_diameter":"0.99"}}}})"); - CHECK(!SSWCPProtocol::parse_extruder_nozzle_info(bad, diameters, flows)); - - // Missing flow type defaults to standard - json no_flow = json::parse(R"({"data":{"status":{"extruder":{"nozzle_diameter":"0.8"}}}})"); - CHECK(SSWCPProtocol::parse_extruder_nozzle_info(no_flow, diameters, flows)); - // Missing nozzle_volume_type means firmware doesn't report flow: leave flows empty - CHECK(flows.empty()); -} - -TEST_CASE("parse_extruder_nozzle_info dynamically discovers and orders extruders", "[SSWCPProtocol]") -{ - // Keys in non-sequential order: extruder1 before extruder. - // Also tests that non-extruder keys (gcode_move, heater_bed) are ignored. - json response = json::parse(R"({ - "data": { - "status": { - "gcode_move": {}, - "extruder1": {"nozzle_diameter": "0.6", "nozzle_volume_type": "high_flow"}, - "extruder": {"nozzle_diameter": "0.4", "nozzle_volume_type": "standard"}, - "heater_bed": {}, - "extruder2": {"nozzle_diameter": "0.8"} - } - } - })"); - - std::vector diameters, flows; - CHECK(SSWCPProtocol::parse_extruder_nozzle_info(response, diameters, flows)); - REQUIRE(diameters.size() == 3); - CHECK(diameters[0] == "0.4"); - CHECK(diameters[1] == "0.6"); - CHECK(diameters[2] == "0.8"); - // extruder2 is missing flow type -> all_flows_present is false -> flows cleared - CHECK(flows.empty()); -} - -TEST_CASE("parse_extruder_nozzle_info ignores pathological extruder key suffixes", "[SSWCPProtocol]") -{ - // A key with an absurdly long numeric suffix must be skipped, not crash stoul. - json response = json::parse(R"({ - "data": { - "status": { - "extruder99999999999999999": {"nozzle_diameter": "0.4", "nozzle_volume_type": "standard"} - } - } - })"); - - std::vector diameters, flows; - CHECK(!SSWCPProtocol::parse_extruder_nozzle_info(response, diameters, flows)); - CHECK(diameters.empty()); - - // Sanity: a reasonable multi-digit suffix still works. - json ok = json::parse(R"({ - "data": { - "status": { - "extruder10": {"nozzle_diameter": "0.8", "nozzle_volume_type": "high_flow"} - } - } - })"); - CHECK(SSWCPProtocol::parse_extruder_nozzle_info(ok, diameters, flows)); - CHECK(diameters == std::vector{"0.8"}); -} diff --git a/tests/packaging/test_stage_connect_bundle.py b/tests/packaging/test_stage_connect_bundle.py new file mode 100644 index 000000000000..2e9c69ed9494 --- /dev/null +++ b/tests/packaging/test_stage_connect_bundle.py @@ -0,0 +1,50 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_PATH = Path(__file__).parents[2] / "scripts" / "packaging" / "stage_connect_bundle.py" +SPEC = importlib.util.spec_from_file_location("stage_connect_bundle", SCRIPT_PATH) +assert SPEC is not None and SPEC.loader is not None +stage_connect_bundle = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(stage_connect_bundle) + + +class StageConnectBundleTest(unittest.TestCase): + def test_identical_web_fingerprints(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + first = Path(temporary) / "first" + second = Path(temporary) / "second" + for root in (first, second): + (root / "assets").mkdir(parents=True) + (root / "index.html").write_text("index", encoding="utf-8") + (root / "assets" / "main.js").write_bytes(b"main") + + self.assertEqual( + stage_connect_bundle.web_fingerprint(first), + stage_connect_bundle.web_fingerprint(second), + ) + + def test_changed_web_content_has_different_fingerprint(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + first = Path(temporary) / "first" + second = Path(temporary) / "second" + for root in (first, second): + root.mkdir() + (root / "index.html").write_text("index", encoding="utf-8") + (second / "index.html").write_text("changed", encoding="utf-8") + + self.assertNotEqual( + stage_connect_bundle.web_fingerprint(first), + stage_connect_bundle.web_fingerprint(second), + ) + + def test_missing_web_directory_is_rejected(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + with self.assertRaises(RuntimeError): + stage_connect_bundle.web_fingerprint(Path(temporary) / "missing") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 861ea88cf004..8eace3021867 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -36,6 +36,28 @@ if (MSVC) endif () set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") +add_executable(gateway_tests + gateway_tests_main.cpp + ConnectionProcessManager_test.cpp + GatewayProtocol_test.cpp + GatewayService_test.cpp + GatewayDevice_test.cpp + GatewayFlutterResources_test.cpp + GatewayAccount_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/Utils/ConnectionProcessManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/Utils/GatewayProtocol.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/Utils/GatewayService.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/Utils/GatewayDevice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/Utils/GatewayAccount.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/Utils/Http.cpp + ) +target_link_libraries(gateway_tests test_common libslic3r boost_libs libcurl OpenSSL::SSL OpenSSL::Crypto) +target_compile_definitions(gateway_tests PRIVATE GATEWAY_FLUTTER_BUNDLE="${CMAKE_CURRENT_SOURCE_DIR}/../../resources/web/flutter_web/main.dart.js") +if (MSVC) + target_compile_definitions(gateway_tests PRIVATE _WIN32_WINNT=0x0601 NOMINMAX) +endif () +set_property(TARGET gateway_tests PROPERTY FOLDER "tests") + if (WIN32) if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") Snapmaker_Orca_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug) @@ -47,6 +69,7 @@ if (WIN32) endif() catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: " EXTRA_ARGS ${CATCH_EXTRA_ARGS}) +catch_discover_tests(gateway_tests TEST_PREFIX "gateway: " EXTRA_ARGS ${CATCH_EXTRA_ARGS}) # add_test(${_TEST_NAME}_tests ${_TEST_NAME}_tests "${CATCH_EXTRA_ARGS} exclude:[NotWorking]") set(_FILAMENT_TEST_NAME filament_dropdown_mapping) diff --git a/tests/slic3rutils/ConnectionProcessManager_test.cpp b/tests/slic3rutils/ConnectionProcessManager_test.cpp new file mode 100644 index 000000000000..e10580218d21 --- /dev/null +++ b/tests/slic3rutils/ConnectionProcessManager_test.cpp @@ -0,0 +1,103 @@ +#include + +#include "slic3r/Utils/ConnectionProcessManager.hpp" + +#include + +#include +#include + +using namespace Slic3r::Gateway; + +TEST_CASE("ConnectionProcessManager builds the Orca launch arguments", "[gateway][process]") +{ + const std::vector arguments = ConnectionProcessManager::build_arguments("zh-CN"); + REQUIRE(arguments == std::vector{"--locale=zh-CN", "--orca"}); +} + +TEST_CASE("ConnectionProcessManager selects the platform-specific CLI executable", "[gateway][process]") +{ +#if defined(_WIN32) +#if defined(_M_X64) || defined(__x86_64__) + constexpr std::string_view expected = "snapmaker_control_windows_x64.exe"; +#elif defined(_M_ARM64) || defined(__aarch64__) + constexpr std::string_view expected = "snapmaker_control_windows_arm64.exe"; +#else +#error "unsupported Windows architecture for the snapmaker_control CLI" +#endif +#elif defined(__APPLE__) +#if defined(__x86_64__) + constexpr std::string_view expected = "snapmaker_control_macos_x64"; +#elif defined(__arm64__) || defined(__aarch64__) + constexpr std::string_view expected = "snapmaker_control_macos_arm64"; +#else +#error "unsupported macOS architecture for the snapmaker_control CLI" +#endif +#elif defined(__linux__) +#if defined(__x86_64__) + constexpr std::string_view expected = "snapmaker_control_linux_x64"; +#elif defined(__aarch64__) || defined(__arm64__) + constexpr std::string_view expected = "snapmaker_control_linux_arm64"; +#else +#error "unsupported Linux architecture for the snapmaker_control CLI" +#endif +#else +#error "unsupported platform for the snapmaker_control CLI" +#endif + + REQUIRE(ConnectionProcessManager::cli_executable_name() == expected); +} + +TEST_CASE("ConnectionProcessManager parses one PORT frame after noise", "[gateway][process]") +{ + std::uint16_t port = 0; + const std::string output = "starting dart cli\r\nloading locale\r\nPORT:8888\r\n\r\n"; + REQUIRE(ConnectionProcessManager::parse_port_frame(output, port) == ProcessDiscoveryError::None); + REQUIRE(port == 8888); +} + +TEST_CASE("ConnectionProcessManager rejects malformed and duplicate PORT frames", "[gateway][process]") +{ + std::uint16_t port = 0; + REQUIRE(ConnectionProcessManager::parse_port_frame("PORT:88\r\n", port) == ProcessDiscoveryError::InvalidPortFrame); + REQUIRE(ConnectionProcessManager::parse_port_frame("PORT:0\r\n\r\n", port) == ProcessDiscoveryError::InvalidPortFrame); + REQUIRE(ConnectionProcessManager::parse_port_frame("PORT:70000\r\n\r\n", port) == ProcessDiscoveryError::InvalidPortFrame); + REQUIRE(ConnectionProcessManager::parse_port_frame("PORT:8888\r\n\r\nPORT:8889\r\n\r\n", port) == + ProcessDiscoveryError::MultiplePortFrames); +} + +TEST_CASE("ConnectionProcessManager uses the injected process runner", "[gateway][process]") +{ + ConnectionProcessManager::Config config; + config.executable = boost::filesystem::path{"snapmaker_connection.exe"}; + + std::vector seen_arguments; + ConnectionProcessManager manager(config, [&](const std::vector& arguments) { + seen_arguments = arguments; + ConnectionProcessManager::ProcessRunResult result; + result.stdout_data = "PORT:8080\r\n\r\n"; + return result; + }); + + const auto result = manager.discover_port("en-US"); + REQUIRE(result.error == ProcessDiscoveryError::None); + REQUIRE(result.port == 8080); + REQUIRE(seen_arguments == std::vector{"--locale=en-US", "--orca"}); +} + +TEST_CASE("ConnectionProcessManager terminate is a no-op without a tracked child", "[gateway][process]") +{ + ConnectionProcessManager::Config config; + config.executable = boost::filesystem::path{"snapmaker_connection.exe"}; + + ConnectionProcessManager manager(config, [](const std::vector&) { + ConnectionProcessManager::ProcessRunResult result; + result.stdout_data = "PORT:8080\r\n\r\n"; + return result; + }); + + REQUIRE(manager.discover_port("en-US").error == ProcessDiscoveryError::None); + // Injected runners bypass the real child tracking, so terminate() must stay safe. + manager.terminate(); + manager.terminate(); +} diff --git a/tests/slic3rutils/GatewayAccount_test.cpp b/tests/slic3rutils/GatewayAccount_test.cpp new file mode 100644 index 000000000000..565d51077a2c --- /dev/null +++ b/tests/slic3rutils/GatewayAccount_test.cpp @@ -0,0 +1,139 @@ +#include + +#include "slic3r/Utils/GatewayAccount.hpp" + +#include + +#include + +using namespace Slic3r::Gateway; + +TEST_CASE("GatewayAccount parses an online frame with the primary aliases", "[gateway][account]") +{ + AccountSnapshot snapshot; + const nlohmann::json frame = { + {"is_login", true}, + {"status", "online"}, + {"userid", "1008611"}, + {"token", "token-primary"}, + {"nickname", "Tian"}, + {"account", "maker@example.com"}, + {"email", "maker@example.com"}, + }; + + REQUIRE(GatewayAccount::parse(frame, snapshot)); + REQUIRE(snapshot.is_login); + REQUIRE(snapshot.userid == "1008611"); + REQUIRE(snapshot.token == "token-primary"); + REQUIRE(snapshot.nickname == "Tian"); + REQUIRE(snapshot.account == "maker@example.com"); + REQUIRE(snapshot.email == "maker@example.com"); +} + +TEST_CASE("GatewayAccount falls back to the secondary alias fields", "[gateway][account]") +{ + AccountSnapshot snapshot; + const nlohmann::json frame = { + {"isLogin", true}, + {"stauts", "online"}, + {"id", "1008611"}, + {"accessToken", "token-tertiary"}, + }; + + REQUIRE(GatewayAccount::parse(frame, snapshot)); + REQUIRE(snapshot.is_login); + REQUIRE(snapshot.userid == "1008611"); + REQUIRE(snapshot.token == "token-tertiary"); + REQUIRE(snapshot.nickname.empty()); + REQUIRE(snapshot.account.empty()); + REQUIRE(snapshot.email.empty()); +} + +TEST_CASE("GatewayAccount parses an offline frame with null tokens", "[gateway][account]") +{ + AccountSnapshot snapshot; + snapshot.is_login = true; + snapshot.userid = "stale"; + snapshot.token = "stale"; + const nlohmann::json frame = { + {"is_login", false}, {"isLogin", false}, {"status", "offline"}, {"stauts", "offline"}, {"userid", ""}, + {"id", ""}, {"uid", nullptr}, {"token", nullptr}, {"access_token", nullptr}, {"accessToken", nullptr}, + {"nickname", ""}, {"account", ""}, {"email", ""}, + }; + + REQUIRE(GatewayAccount::parse(frame, snapshot)); + REQUIRE(!snapshot.is_login); + REQUIRE(snapshot.userid.empty()); + REQUIRE(snapshot.token.empty()); + REQUIRE(snapshot.nickname.empty()); + REQUIRE(snapshot.account.empty()); + REQUIRE(snapshot.email.empty()); +} + +TEST_CASE("GatewayAccount treats malformed fields as missing", "[gateway][account]") +{ + AccountSnapshot snapshot; + const nlohmann::json frame = { + {"is_login", "yes"}, {"isLogin", 1}, {"status", 7}, {"userid", 42}, {"token", false}, {"nickname", 3.5}, + }; + + REQUIRE(GatewayAccount::parse(frame, snapshot)); + REQUIRE(!snapshot.is_login); + REQUIRE(snapshot.userid.empty()); + REQUIRE(snapshot.token.empty()); + REQUIRE(snapshot.nickname.empty()); +} + +TEST_CASE("GatewayAccount rejects non-object frames", "[gateway][account]") +{ + AccountSnapshot snapshot; + REQUIRE(!GatewayAccount::parse(nlohmann::json::array({"is_login"}), snapshot)); + REQUIRE(!GatewayAccount::parse(nlohmann::json("online"), snapshot)); + REQUIRE(!GatewayAccount::parse(nlohmann::json(nullptr), snapshot)); +} + +TEST_CASE("GatewayAccount parses the wrapped offline account response", "[gateway][account]") +{ + AccountSnapshot snapshot; + const nlohmann::json frame = { + {"code", -1}, + {"data", + {{"id", nullptr}, + {"icon", nullptr}, + {"nickname", nullptr}, + {"account", nullptr}, + {"email", nullptr}, + {"status", nullptr}, + {"accessToken", nullptr}, + {"refreshToken", nullptr}}}, + }; + + REQUIRE(GatewayAccount::parse(frame, snapshot)); + REQUIRE(!snapshot.is_login); + REQUIRE(snapshot.userid.empty()); + REQUIRE(snapshot.token.empty()); + REQUIRE(snapshot.nickname.empty()); +} + +TEST_CASE("GatewayAccount parses the wrapped online account response", "[gateway][account]") +{ + AccountSnapshot snapshot; + const nlohmann::json frame = { + {"code", 0}, + {"data", + {{"id", 145608}, + {"nickname", "Tian"}, + {"account", "maker@example.com"}, + {"email", "maker@example.com"}, + {"status", "online"}, + {"accessToken", "eyJ-wrapped"}}}, + }; + + REQUIRE(GatewayAccount::parse(frame, snapshot)); + REQUIRE(snapshot.is_login); + REQUIRE(snapshot.userid == "145608"); + REQUIRE(snapshot.token == "eyJ-wrapped"); + REQUIRE(snapshot.nickname == "Tian"); + REQUIRE(snapshot.account == "maker@example.com"); + REQUIRE(snapshot.email == "maker@example.com"); +} diff --git a/tests/slic3rutils/GatewayDevice_test.cpp b/tests/slic3rutils/GatewayDevice_test.cpp new file mode 100644 index 000000000000..80dd6723ba7f --- /dev/null +++ b/tests/slic3rutils/GatewayDevice_test.cpp @@ -0,0 +1,418 @@ +#include + +#include "slic3r/Utils/GatewayDevice.hpp" +#include "slic3r/Utils/GatewayService.hpp" + +#include "libslic3r/PresetBundle.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +using namespace Slic3r::Gateway; + +namespace { + +struct DeviceFakeHttp final : public HttpTransport +{ + // Serialized body of GET /api/cache/all. Empty means the device endpoint answers 404. + std::string device_response; + + HttpResponse get(const std::string& url) override + { + if (url.find("/api/health") != std::string::npos) { + return {200, + R"({"status":"ok","cli_version":"1.0","components":{"ipc_server":"ok","web_server":"ok"},)" + R"("server_url":{"base_url":"http://127.0.0.1:8080/","home_page":"/index","device_control":""}})", + {}}; + } + if (url.find("/api/cache/all") != std::string::npos) { + if (device_response.empty()) + return {404, {}, "not found"}; + return {200, device_response, {}}; + } + return {404, {}, "not found"}; + } + + HttpResponse post_json(const std::string& url, const std::string& body) override { return {404, {}, "not found"}; } +}; + +struct DeviceFakeWebSocket final : public WebSocketTransport +{ + Listener listener; + + void set_listener(Listener value) override { listener = std::move(value); } + + void connect(std::uint16_t port, const std::string& path) override { listener.opened(); } + + bool send(const std::string& message) override { return true; } + + void close() override + { + if (listener.closed) + listener.closed("closed"); + } +}; + +bool wait_for_state(const GatewayService& service, ConnectionState expected) +{ + for (int i = 0; i < 2000; ++i) { + if (service.state() == expected) + return true; + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } + return service.state() == expected; +} + +struct ServiceFixture +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + std::shared_ptr service; + + ServiceFixture() + { + process = std::make_shared(ConnectionProcessManager::Config{boost::filesystem::path{ + "snapmaker_connection.exe"}}, + [](const std::vector&) { + ConnectionProcessManager::ProcessRunResult result; + result.stdout_data = "PORT:8888\r\n\r\n"; + return result; + }); + GatewayService::Dependencies dependencies; + dependencies.process_manager = process; + http = std::make_shared(); + dependencies.http = http; + websocket = std::make_shared(); + dependencies.websocket = websocket; + dependencies.dispatcher = [](std::function task) { task(); }; + service = std::make_shared(GatewayService::Config{}, std::move(dependencies)); + if (!service->start("zh-CN")) + return; + wait_for_state(*service, ConnectionState::Connected); + } + + ~ServiceFixture() { service->stop(); } +}; + +// Mirrors the GET /api/cache/all 200 contract from the snapmaker_connection API doc. +const nlohmann::json valid_device_snapshot{{"ok", true}, + {"data", + {{"schema", 1}, + {"sn", "SN1"}, + {"current", true}, + {"identity", + {{"sn", "SN1"}, + {"name", "qiepian-6"}, + {"device_name", "qiepian-6"}, + {"connected", true}, + {"productName", "Snapmaker U1"}, + {"deviceModel", "U1"}}}, + {"info", + {{"product", + {{"machine_type", "Snapmaker U1"}, + {"serial_number", "SN1"}, + {"device_name", "Data Registration"}, + {"nozzle_diameter", {0.4, 0.4, 0.25, "0.8", 0.6}}}}}}, + {"objects", + {{"extruder", {{"nozzle_diameter", 0.4}, {"nozzle_volume_type", "high_flow"}}}, + {"extruder1", {{"nozzle_diameter", 0.4}, {"nozzle_volume_type", "standard"}}}, + {"extruder2", {{"nozzle_diameter", 0.25}, {"nozzle_volume_type", "tpu_high_flow"}}}, + {"extruder3", {{"nozzle_diameter", "0.8"}, {"nozzle_volume_type", "high_flow"}}}, + {"extruder4", {{"nozzle_diameter", 0.6}, {"nozzle_volume_type", "standard"}}}}}}}}; + +} // namespace + +namespace { + +nlohmann::json machine_objects() +{ + return nlohmann::json{{"extruder", nlohmann::json{{"nozzle_diameter", 0.4}, {"nozzle_volume_type", "standard"}}}, + {"extruder1", nlohmann::json{{"nozzle_diameter", "0.6"}, {"nozzle_volume_type", "high_flow"}}}, + {"print_task_config", + nlohmann::json{{"filament_vendor", nlohmann::json::array({"Snapmaker", "Polymaker"})}, + {"filament_type", nlohmann::json::array({"PLA", "PETG"})}, + {"filament_sub_type", nlohmann::json::array({"Matte", "NONE"})}, + {"filament_official", nlohmann::json::array({true, false})}, + {"filament_exist", nlohmann::json::array({true, true})}, + {"filament_color", nlohmann::json::array({4281179737, 4294967295})}, + {"filament_color_rgba", nlohmann::json::array({"2D9E59FF", "FFFFFFFF"})}, + {"extruder_map_table", nlohmann::json::array({0, 1})}, + {"filament_color_multi", + nlohmann::json::array( + {nlohmann::json{{"mode", 2}, {"colors", nlohmann::json::array({"2D9E59", "FFFFFF"})}}, + nlohmann::json{{"mode", 0}, {"colors", nlohmann::json::array({"FFFFFF"})}}})}}}}; +} + +} // namespace + +TEST_CASE("GatewayDevice query_machine_info parses the device snapshot", "[gateway][device]") +{ + ServiceFixture fixture; + REQUIRE(fixture.service->is_connected()); + fixture.http->device_response = valid_device_snapshot.dump(); + + std::string model; + std::vector nozzles; + std::vector flows; + std::string name; + REQUIRE(GatewayDevice::query_machine_info(fixture.service, model, nozzles, flows, name)); + REQUIRE(model == "Snapmaker U1"); + REQUIRE(name == "qiepian-6"); + // 0.25 is not a supported preset label and is dropped by design; "0.8" as a string is accepted. + REQUIRE(nozzles == std::vector{"0.4", "0.4", "0.8", "0.6"}); + // Flow types follow the sorted extruder objects; the 0.25 extruder does not participate. + REQUIRE(flows == std::vector{"high_flow", "standard", "high_flow", "standard"}); +} + +TEST_CASE("GatewayDevice parse_machine_slots parses the complete object query", "[gateway][device][machine-slots]") +{ + std::vector slots; + REQUIRE(GatewayDevice::parse_machine_slots(nlohmann::json{{"objects", machine_objects()}}, slots)); + REQUIRE(slots.size() == 2); + REQUIRE(slots[0].index == 0); + REQUIRE(slots[0].filament_info == "Snapmaker PLA Matte"); + REQUIRE(slots[0].filament_type == "PLA"); + REQUIRE(slots[0].nozzle_info == "0.4"); + REQUIRE(slots[0].nozzle_volume_type == "standard"); + REQUIRE(slots[0].color_info == "#2D9E59"); + REQUIRE(slots[0].multiColors == std::vector{"#2D9E59", "#FFFFFF"}); + REQUIRE(slots[1].filament_info == "Polymaker PETG"); + REQUIRE(slots[1].nozzle_info == "0.6"); + REQUIRE(slots[1].nozzle_volume_type == "high_flow"); +} + +TEST_CASE("GatewayDevice parse_machine_slots supports legacy and missing optional fields", "[gateway][device][machine-slots]") +{ + nlohmann::json objects = machine_objects(); + objects["print_task_config"].erase("filament_exist"); + objects["print_task_config"].erase("filament_color_rgba"); + objects["print_task_config"].erase("filament_color_multi"); + objects["extruder1"].erase("nozzle_volume_type"); + + std::vector slots; + REQUIRE(GatewayDevice::parse_machine_slots(objects, slots)); + REQUIRE(slots.size() == 2); + REQUIRE(slots[0].color_info == "#2D9E59"); + REQUIRE(slots[0].multiColors == std::vector{"#2D9E59"}); + REQUIRE(slots[1].filament_info == "Polymaker PETG"); + REQUIRE(slots[1].nozzle_info == "0.6"); + REQUIRE(slots[1].nozzle_volume_type == "standard"); +} + +TEST_CASE("GatewayDevice parse_machine_slots renders empty slots as NONE", "[gateway][device][machine-slots]") +{ + nlohmann::json objects = machine_objects(); + objects["print_task_config"]["filament_sub_type"] = nlohmann::json::array({"Support", "NONE"}); + objects["print_task_config"]["filament_exist"] = nlohmann::json::array({false, true}); + + std::vector slots; + REQUIRE(GatewayDevice::parse_machine_slots(objects, slots)); + REQUIRE(slots.size() == 2); + REQUIRE(slots[0].filament_info == "NONE"); + REQUIRE(slots[0].filament_type == "NONE"); + REQUIRE(slots[1].filament_info == "Polymaker PETG"); +} + +TEST_CASE("GatewayDevice parse_machine_slots overrides only present config nozzles", "[gateway][device][machine-slots]") +{ + nlohmann::json objects = machine_objects(); + objects["print_task_config"]["nozzle_diameters"] = nlohmann::json::array({0.8}); + + std::vector slots; + REQUIRE(GatewayDevice::parse_machine_slots(objects, slots)); + REQUIRE(slots.size() == 2); + REQUIRE(slots[0].nozzle_info == "0.8"); + REQUIRE(slots[1].nozzle_info == "0.6"); +} + +TEST_CASE("GatewayDevice parse_machine_slots rejects malformed payloads", "[gateway][device][machine-slots]") +{ + std::vector slots; + REQUIRE_FALSE(GatewayDevice::parse_machine_slots(nlohmann::json::object(), slots)); + REQUIRE_FALSE(GatewayDevice::parse_machine_slots(nlohmann::json{{"objects", nlohmann::json::object()}}, slots)); + REQUIRE_FALSE( + GatewayDevice::parse_machine_slots(nlohmann::json{{"objects", nlohmann::json{{"print_task_config", nlohmann::json::object()}}}}, + slots)); + + nlohmann::json missing_nozzle = machine_objects(); + missing_nozzle.erase("extruder"); + missing_nozzle.erase("extruder1"); + REQUIRE_FALSE(GatewayDevice::parse_machine_slots(missing_nozzle, slots)); + REQUIRE(slots.empty()); +} + +TEST_CASE("GatewayDevice machine_slots_equal includes nozzle flow", "[gateway][device][machine-slots]") +{ + std::vector left; + std::vector right; + REQUIRE(GatewayDevice::parse_machine_slots(machine_objects(), left)); + right = left; + REQUIRE(GatewayDevice::machine_slots_equal(left, right)); + right[1].nozzle_volume_type = "standard"; + REQUIRE_FALSE(GatewayDevice::machine_slots_equal(left, right)); +} + +TEST_CASE("GatewayDevice delta_affects_machine_slots ignores unrelated objects", "[gateway][device][machine-slots]") +{ + REQUIRE(GatewayDevice::delta_affects_machine_slots(nlohmann::json{{"print_task_config", nlohmann::json::object()}})); + REQUIRE(GatewayDevice::delta_affects_machine_slots(nlohmann::json{{"extruder", nlohmann::json{{"temperature", 25}}}})); + REQUIRE(GatewayDevice::delta_affects_machine_slots(nlohmann::json{{"extruder1", nlohmann::json{{"nozzle_diameter", 0.2}}}})); + REQUIRE( + GatewayDevice::delta_affects_machine_slots(nlohmann::json{{"objects", nlohmann::json{{"extruder2", nlohmann::json::object()}}}})); + REQUIRE_FALSE(GatewayDevice::delta_affects_machine_slots(nlohmann::json{{"print_stats", nlohmann::json::object()}})); + REQUIRE_FALSE(GatewayDevice::delta_affects_machine_slots(nlohmann::json{{"extruder_sensor", nlohmann::json::object()}})); + REQUIRE_FALSE(GatewayDevice::delta_affects_machine_slots(nlohmann::json::array())); +} + +TEST_CASE("GatewayDevice query_machine_info falls back to identity fields", "[gateway][device]") +{ + ServiceFixture fixture; + REQUIRE(fixture.service->is_connected()); + nlohmann::json snapshot = valid_device_snapshot; + snapshot["data"]["info"]["product"]["machine_type"] = nullptr; + snapshot["data"]["info"]["product"]["device_name"] = ""; + fixture.http->device_response = snapshot.dump(); + + std::string model; + std::vector nozzles; + std::vector flows; + std::string name; + REQUIRE(GatewayDevice::query_machine_info(fixture.service, model, nozzles, flows, name)); + REQUIRE(model == "Snapmaker U1"); // from identity.productName + REQUIRE(name == "qiepian-6"); // from identity.device_name + REQUIRE(flows == std::vector{"high_flow", "standard", "high_flow", "standard"}); +} + +TEST_CASE("GatewayDevice query_machine_info falls back to the product device name", "[gateway][device]") +{ + ServiceFixture fixture; + REQUIRE(fixture.service->is_connected()); + nlohmann::json snapshot = valid_device_snapshot; + snapshot["data"]["identity"].erase("name"); + snapshot["data"]["identity"].erase("device_name"); + fixture.http->device_response = snapshot.dump(); + + std::string model; + std::vector nozzles; + std::vector flows; + std::string name; + REQUIRE(GatewayDevice::query_machine_info(fixture.service, model, nozzles, flows, name)); + REQUIRE(name == "Data Registration"); +} + +TEST_CASE("GatewayDevice query_machine_info rejects malformed payloads", "[gateway][device]") +{ + ServiceFixture fixture; + REQUIRE(fixture.service->is_connected()); + + SECTION("empty machine_type") + { + nlohmann::json snapshot = valid_device_snapshot; + snapshot["data"]["info"]["product"]["machine_type"] = ""; + snapshot["data"]["identity"].erase("productName"); + fixture.http->device_response = snapshot.dump(); + } + SECTION("missing product") + { + nlohmann::json snapshot = valid_device_snapshot; + snapshot["data"]["info"].erase("product"); + snapshot["data"]["identity"].erase("productName"); + fixture.http->device_response = snapshot.dump(); + } + SECTION("missing machine_type key") + { + nlohmann::json snapshot = valid_device_snapshot; + snapshot["data"]["info"]["product"].erase("machine_type"); + snapshot["data"]["identity"].erase("productName"); + fixture.http->device_response = snapshot.dump(); + } + + std::string model = "untouched"; + std::vector nozzles{"untouched"}; + std::vector flows{"untouched"}; + std::string name = "untouched"; + REQUIRE_FALSE(GatewayDevice::query_machine_info(fixture.service, model, nozzles, flows, name)); + // Outputs are not modified on failure. + REQUIRE(model == "untouched"); + REQUIRE(nozzles == std::vector{"untouched"}); + REQUIRE(flows == std::vector{"untouched"}); + REQUIRE(name == "untouched"); +} + +TEST_CASE("GatewayDevice query_machine_info reports http errors", "[gateway][device]") +{ + ServiceFixture fixture; + REQUIRE(fixture.service->is_connected()); + // Default DeviceFakeHttp answers 404 on /api/cache/all (no device snapshot available). + + std::string model; + std::vector nozzles; + std::vector flows; + std::string name; + REQUIRE_FALSE(GatewayDevice::query_machine_info(fixture.service, model, nozzles, flows, name)); +} + +TEST_CASE("GatewayDevice tolerates a null gateway", "[gateway][device]") +{ + std::string model; + std::vector nozzles; + std::vector flows; + std::string name; + REQUIRE_FALSE(GatewayDevice::query_machine_info(nullptr, model, nozzles, flows, name)); +} + +TEST_CASE("GatewayDevice query_machine_info nozzle_volume_type edge cases", "[gateway][device]") +{ + ServiceFixture fixture; + REQUIRE(fixture.service->is_connected()); + + std::string model; + std::vector nozzles; + std::vector flows; + std::string name; + + SECTION("unknown flow strings normalize to standard") + { + nlohmann::json snapshot = valid_device_snapshot; + snapshot["data"]["objects"]["extruder"]["nozzle_volume_type"] = "ultra_flow"; + fixture.http->device_response = snapshot.dump(); + + REQUIRE(GatewayDevice::query_machine_info(fixture.service, model, nozzles, flows, name)); + REQUIRE(flows == std::vector{"standard", "standard", "high_flow", "standard"}); + } + SECTION("a missing flow type leaves the vector empty for the caller fallback") + { + nlohmann::json snapshot = valid_device_snapshot; + snapshot["data"]["objects"]["extruder1"].erase("nozzle_volume_type"); + fixture.http->device_response = snapshot.dump(); + + REQUIRE(GatewayDevice::query_machine_info(fixture.service, model, nozzles, flows, name)); + REQUIRE(flows.empty()); + } + SECTION("no extruder objects fall back to standard per nozzle") + { + nlohmann::json snapshot = valid_device_snapshot; + snapshot["data"]["objects"] = nlohmann::json::object(); + fixture.http->device_response = snapshot.dump(); + + REQUIRE(GatewayDevice::query_machine_info(fixture.service, model, nozzles, flows, name)); + REQUIRE(flows == std::vector{"standard", "standard", "standard", "standard"}); + } + SECTION("a partial extruder report leaves flow types empty") + { + nlohmann::json snapshot = valid_device_snapshot; + snapshot["data"]["objects"].erase("extruder4"); + fixture.http->device_response = snapshot.dump(); + + REQUIRE(GatewayDevice::query_machine_info(fixture.service, model, nozzles, flows, name)); + REQUIRE(nozzles == std::vector{"0.4", "0.4", "0.8", "0.6"}); + REQUIRE(flows.empty()); + } +} diff --git a/tests/slic3rutils/GatewayFlutterResources_test.cpp b/tests/slic3rutils/GatewayFlutterResources_test.cpp new file mode 100644 index 000000000000..6090e185e95e --- /dev/null +++ b/tests/slic3rutils/GatewayFlutterResources_test.cpp @@ -0,0 +1,32 @@ +#include + +#include +#include +#include + +namespace { + +std::string read_flutter_bundle() +{ + std::ifstream bundle(GATEWAY_FLUTTER_BUNDLE, std::ios::binary); + REQUIRE(bundle.good()); + return std::string{std::istreambuf_iterator{bundle}, {}}; +} + +} // namespace + +TEST_CASE("shipped Flutter bundle does not use removed device bridge commands", "[gateway][flutter]") +{ + const std::string bundle = read_flutter_bundle(); + for (const char* command : { + "sw_SystemGetDeviceInfo", + "sw_GetMachineState", + "sw_SubscribeMachineState", + "sw_GetMachineObjects", + "sw_SetSubscribeFilter", + "sw_StopMachineStateSubscription", + "sw_GetPrinterInfo", + "sw_GetMachineSystemInfo", + }) + CHECK(bundle.find(command) == std::string::npos); +} diff --git a/tests/slic3rutils/GatewayProtocol_test.cpp b/tests/slic3rutils/GatewayProtocol_test.cpp new file mode 100644 index 000000000000..1630a51f0786 --- /dev/null +++ b/tests/slic3rutils/GatewayProtocol_test.cpp @@ -0,0 +1,175 @@ +#include + +#include "slic3r/Utils/GatewayProtocol.hpp" + +#include + +using namespace Slic3r::Gateway; +using nlohmann::json; + +TEST_CASE("parse_health accepts the documented health response", "[gateway][protocol]") +{ + const std::string body = R"({ + "status": "ok", + "cli_version": "1.0", + "components": {"ipc_server": "ok", "web_server": "ok"}, + "device_connected": true, + "sn": "U1-001", + "server_url": { + "base_url": "http://127.0.0.1:8080/", + "home_page": "/index.html?x=1", + "pre_paint_page": "/pre-paint" + } + })"; + HealthInfo health; + REQUIRE(!parse_health(body, health)); + REQUIRE(health.cli_version == "1.0"); + REQUIRE(health.base_url == "http://127.0.0.1:8080/"); + REQUIRE(health.device_connected); + REQUIRE(health.device_sn == "U1-001"); + REQUIRE(health.has_device_state); + REQUIRE(health.pages.at("home_page") == "/index.html?x=1"); + REQUIRE(health.pages.find("base_url") == health.pages.end()); +} + +TEST_CASE("parse_health rejects unhealthy components", "[gateway][protocol]") +{ + HealthInfo health; + const std::string body = R"({"status":"ok","components":{"ipc_server":"ok","web_server":"starting"}})"; + const GatewayError error = parse_health(body, health); + REQUIRE(error); + REQUIRE(error.code == GatewayErrorCode::HealthNotReady); +} + +TEST_CASE("parse_health accepts an explicit disconnected device without sn", "[gateway][protocol]") +{ + HealthInfo health; + const auto body = json{{"status", "ok"}, + {"components", json{{"ipc_server", "ok"}, {"web_server", "ok"}}}, + {"device_connected", false}, + {"server_url", {{"base_url", "http://127.0.0.1:8080/"}, {"home_page", "/index.html"}}}} + .dump(); + + REQUIRE(!parse_health(body, health)); + REQUIRE(health.has_device_state); + REQUIRE_FALSE(health.device_connected); + REQUIRE(health.device_sn.empty()); + + const auto invalid = json{{"status", "ok"}, + {"components", json{{"ipc_server", "ok"}, {"web_server", "ok"}}}, + {"device_connected", "false"}, + {"server_url", {{"base_url", "http://127.0.0.1:8080/"}, {"home_page", "/index.html"}}}} + .dump(); + const auto error = parse_health(invalid, health); + REQUIRE(error); + REQUIRE(error.code == GatewayErrorCode::InvalidResponse); +} + +TEST_CASE("parse_health ignores device state when connection state is absent", "[gateway][protocol]") +{ + HealthInfo health; + const auto body = json{{"status", "ok"}, + {"components", json{{"ipc_server", "ok"}, {"web_server", "ok"}}}, + {"sn", "U1-001"}, + {"server_url", {{"base_url", "http://127.0.0.1:8080/"}, {"home_page", "/index.html"}}}} + .dump(); + + REQUIRE(!parse_health(body, health)); + REQUIRE_FALSE(health.has_device_state); + REQUIRE(health.device_sn.empty()); +} + +TEST_CASE("parse_device_sn uses the active device contract", "[gateway][protocol]") +{ + for (const char* key : {"sn", "device_sn", "serial_number"}) { + REQUIRE(parse_device_sn(json{{key, "U1-001"}}) == "U1-001"); + REQUIRE(parse_device_sn(json{{"device", {{key, "U1-002"}}}}) == "U1-002"); + } + + REQUIRE(parse_device_sn(json{{"device", json{{"connected", true}}}}).empty()); + REQUIRE(parse_device_sn(json{{"sn", 1}}).empty()); + REQUIRE(parse_device_sn(json::array()).empty()); +} + +TEST_CASE("current device results use the compact device contract", "[gateway][protocol]") +{ + const auto connected = parse_current_device(json{{"sn", "U1-001"}, {"connected", true}}); + REQUIRE(connected.has_value()); + REQUIRE(connected->valid); + REQUIRE(connected->serial_number == "U1-001"); + REQUIRE(connected->connected); + + const auto disconnected = parse_current_device(json{{"sn", ""}, {"connected", false}}); + REQUIRE(disconnected.has_value()); + REQUIRE(disconnected->valid); + REQUIRE(disconnected->serial_number.empty()); + REQUIRE_FALSE(disconnected->connected); + + const auto missing_connected = parse_current_device(json{{"sn", "U1-001"}}); + REQUIRE(missing_connected.has_value()); + REQUIRE_FALSE(missing_connected->connected); + REQUIRE_FALSE(parse_current_device(json{{"sn", ""}, {"connected", true}}).has_value()); + REQUIRE_FALSE(parse_current_device(json::array()).has_value()); +} + +TEST_CASE("device object query results expose the complete object map", "[gateway][protocol]") +{ + const nlohmann::json objects{{"extruder", json{{"nozzle_diameter", 0.4}}}, + {"print_task_config", + json{{"filament_vendor", json::array({"Snapmaker"})}, {"filament_type", json::array({"PLA"})}}}}; + + REQUIRE(parse_device_object_query_result(json{{"objects", objects}}) == objects); + REQUIRE(parse_device_object_query_result(objects) == objects); + REQUIRE_FALSE(parse_device_object_query_result(json{{"objects", json::array()}}).has_value()); + REQUIRE_FALSE(parse_device_object_query_result(json::object()).has_value()); +} + +TEST_CASE("JSON-RPC frames are classified and requests are built", "[gateway][protocol]") +{ + const nlohmann::json request = build_jsonrpc_request(7, "action.device.watch", {{"sn", "A1"}}); + REQUIRE(request["jsonrpc"] == "2.0"); + REQUIRE(request["id"] == 7); + REQUIRE(request["method"] == "action.device.watch"); + REQUIRE(request["params"] == json{{"sn", "A1"}}); + + const RpcFrame result_frame = classify_jsonrpc_message(json{{"jsonrpc", "2.0"}, {"id", 7}, {"result", json{{"ok", true}}}}); + REQUIRE(result_frame.type == RpcFrameType::Result); + REQUIRE(result_frame.id == 7); + + const RpcFrame notification_frame = classify_jsonrpc_message( + json{{"jsonrpc", "2.0"}, {"method", "notify.account.changed"}, {"params", json{{"user", "u"}}}}); + REQUIRE(notification_frame.type == RpcFrameType::Notification); + REQUIRE(notification_frame.method == "notify.account.changed"); + REQUIRE(notification_frame.params == json{{"user", "u"}}); + + const RpcFrame notification_without_params = classify_jsonrpc_message(json{{"jsonrpc", "2.0"}, {"method", "notify.account.changed"}}); + REQUIRE(notification_without_params.type == RpcFrameType::Notification); + REQUIRE(notification_without_params.params == json::object()); + + const RpcFrame array_params_frame = classify_jsonrpc_message( + json{{"jsonrpc", "2.0"}, {"method", "notify.test"}, {"params", json::array({"A1"})}}); + REQUIRE(array_params_frame.type == RpcFrameType::Notification); + REQUIRE(array_params_frame.params == json::array({"A1"})); + + const RpcFrame malformed_params_frame = classify_jsonrpc_message(json{{"jsonrpc", "2.0"}, {"method", "notify.test"}, {"params", 7}}); + REQUIRE(malformed_params_frame.type == RpcFrameType::Unknown); +} + +TEST_CASE("ReconnectPolicy backs off and resets after a stable connection", "[gateway][protocol]") +{ + detail::ReconnectPolicy::Config config; + config.initial_delay = std::chrono::milliseconds{250}; + config.max_delay = std::chrono::milliseconds{5000}; + config.stable_connection_time = std::chrono::milliseconds{30000}; + detail::ReconnectPolicy policy(config); + + policy.record_failure(1000); + REQUIRE(policy.next_delay() == std::chrono::milliseconds{250}); + policy.record_failure(1100); + REQUIRE(policy.next_delay() == std::chrono::milliseconds{500}); + + policy.record_connected(2000); + policy.record_failure(40000); + REQUIRE(policy.next_delay() == std::chrono::milliseconds{250}); + REQUIRE(policy.attempts() == 1); +} diff --git a/tests/slic3rutils/GatewayService_test.cpp b/tests/slic3rutils/GatewayService_test.cpp new file mode 100644 index 000000000000..69201e976df9 --- /dev/null +++ b/tests/slic3rutils/GatewayService_test.cpp @@ -0,0 +1,565 @@ +#include + +#include "slic3r/Utils/GatewayService.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Slic3r::Gateway; + +namespace { + +struct FakeHttp final : public HttpTransport +{ + std::vector urls; + std::vector posts; + std::string store_response{R"({"ok":true,"data":{"file_exists":true}})"}; + std::atomic health_ok{true}; + + HttpResponse get(const std::string& url) override + { + urls.push_back(url); + if (url.find("/api/health") != std::string::npos) { + if (!health_ok.load()) + return {404, {}, "not ready"}; + return {200, + R"({"status":"ok","cli_version":"1.0","components":{"ipc_server":"ok","web_server":"ok"},)" + R"("server_url":{"base_url":"http://127.0.0.1:8080/","home_page":"/index","device_control":""}})", + {}}; + } + if (url.find("/api/cache/all") != std::string::npos) + return {200, R"({"devices":[{"sn":"A1"}]})", {}}; + if (url.find("/api/account") != std::string::npos) + return {200, R"({"user":"u"})", {}}; + return {404, {}, "not found"}; + } + + HttpResponse post_json(const std::string& url, const std::string& body) override + { + posts.push_back(url + "\n" + body); + if (url.find("/api/region") != std::string::npos) + return {200, R"({"code":200,"data":{"region":"US","locale":"en-US","reload":true}})", {}}; + return {200, store_response, {}}; + } +}; + +struct FakeWebSocket final : public WebSocketTransport +{ + Listener listener; + std::atomic connections{0}; + std::atomic fail_first{false}; + std::vector sent; + std::mutex mutex; + + void set_listener(Listener value) override { listener = std::move(value); } + + void connect(std::uint16_t port, const std::string& path) override + { + REQUIRE(port == 8888); + REQUIRE(path == "/ws"); + const int connection = connections.fetch_add(1) + 1; + if (connection == 1 && fail_first.load()) { + listener.closed("connection refused"); + return; + } + listener.opened(); + } + + bool send(const std::string& message) override + { + { + std::lock_guard lock(mutex); + sent.push_back(message); + } + const nlohmann::json request = nlohmann::json::parse(message); + if (request.value("method", "") == "action.device.watch") { + listener.message(nlohmann::json{{"jsonrpc", "2.0"}, {"id", request["id"]}, {"result", {{"watching", true}}}}.dump()); + } else if (request.value("method", "") == "sync.echo") { + listener.message(nlohmann::json{{"jsonrpc", "2.0"}, {"id", request["id"]}, {"result", {{"echoed", true}}}}.dump()); + } else if (request.value("method", "") == "sync.malformed_error") { + listener.message( + nlohmann::json{{"jsonrpc", "2.0"}, {"id", request["id"]}, {"error", nlohmann::json{{"code", "invalid"}, {"message", 7}}}} + .dump()); + } else if (request.value("method", "") == "sync.unsigned_error") { + listener.message(nlohmann::json{{"jsonrpc", "2.0"}, + {"id", request["id"]}, + {"error", nlohmann::json{{"code", 18446744073709551615ULL}, {"message", "too large"}}}} + .dump()); + } + // "sync.silent" intentionally gets no response (timeout / failure scenarios). + return true; + } + + void close() override + { + if (listener.closed) + listener.closed("closed"); + } + + void notify(const std::string& method, const nlohmann::json& params) + { listener.message(nlohmann::json{{"jsonrpc", "2.0"}, {"method", method}, {"params", params}}.dump()); } +}; + +struct BlockingFirstWebSocket final : public WebSocketTransport +{ + Listener listener; + std::promise release_first; + std::atomic connections{0}; + + void set_listener(Listener value) override { listener = std::move(value); } + + void connect(std::uint16_t port, const std::string& path) override + { + REQUIRE(port == 8888); + REQUIRE(path == "/ws"); + if (connections.fetch_add(1) == 0) + release_first.get_future().wait(); + listener.opened(); + } + + bool send(const std::string&) override { return true; } + + void close() override + { + if (listener.closed) + listener.closed("closed"); + } +}; + +bool wait_for_state(const GatewayService& service, ConnectionState expected) +{ + std::mutex mutex; + std::condition_variable condition; + bool ready = false; + std::thread waiter([&] { + while (service.state() != expected) + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + std::lock_guard lock(mutex); + ready = true; + condition.notify_all(); + }); + std::unique_lock lock(mutex); + const bool result = condition.wait_for(lock, std::chrono::seconds{2}, [&] { return ready; }); + lock.unlock(); + waiter.join(); + return result; +} + +GatewayService::Dependencies make_dependencies(std::shared_ptr& process, + std::shared_ptr& http, + std::shared_ptr& websocket) +{ + process = std::make_shared(ConnectionProcessManager::Config{boost::filesystem::path{ + "snapmaker_connection.exe"}}, + [](const std::vector&) { + ConnectionProcessManager::ProcessRunResult result; + result.stdout_data = "PORT:8888\r\n\r\n"; + return result; + }); + http = std::make_shared(); + websocket = std::make_shared(); + GatewayService::Dependencies dependencies; + dependencies.process_manager = process; + dependencies.http = http; + dependencies.websocket = websocket; + return dependencies; +} + +struct RecordingProcessManager final : public ConnectionProcessManager +{ + std::atomic terminate_calls{0}; + + using ConnectionProcessManager::ConnectionProcessManager; + + void terminate() override { terminate_calls.fetch_add(1); } +}; + +} // namespace + +TEST_CASE("GatewayService connects, handles device watch, and calls HTTP APIs", "[gateway][service]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService service(GatewayService::Config{}, make_dependencies(process, http, websocket)); + + std::atomic account_changes{0}; + service.set_notification_handler("notify.account.changed", [&](const nlohmann::json&) { account_changes += 1; }); + REQUIRE(service.start("zh-CN")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + REQUIRE(service.port() == 8888); + REQUIRE(service.health().cli_version == "1.0"); + REQUIRE(service.web_url("home_page") == "http://127.0.0.1:8080/index"); + REQUIRE(service.web_url("device_control").empty()); + REQUIRE(service.base_url() == "http://127.0.0.1:8080/"); + REQUIRE(service.localfile_url("folder/a file.gcode") == "http://127.0.0.1:8080/localfile/folder%2Fa%20file.gcode"); + + std::promise watch_promise; + auto watch_future = watch_promise.get_future(); + const std::int64_t request_id = service.watch_device({{"sn", "A1"}}, [&watch_promise](GatewayError error, const nlohmann::json& result) { + REQUIRE(!error); + watch_promise.set_value(result); + }); + REQUIRE(request_id > 0); + REQUIRE(watch_future.get().at("watching") == true); + + const auto device = service.get_device(); + REQUIRE(!device.error); + REQUIRE(device.value.at("devices").at(0).at("sn") == "A1"); + + const auto device_with_serial = service.get_device("SN 1/A"); + REQUIRE(!device_with_serial.error); + REQUIRE(http->urls.back() == "http://127.0.0.1:8888/api/cache/all/SN%201%2FA"); + + const auto account = service.get_account(); + REQUIRE(!account.error); + REQUIRE(account.value.at("user") == "u"); + + const auto store = service.store_preprint_context("store-id", {{"file_path", "C:/tmp/a.gcode"}, {"file_name", "a.gcode"}}); + REQUIRE(!store.error); + REQUIRE(store.ok); + REQUIRE(store.file_exists); + REQUIRE(http->posts.size() == 1); + const auto posted = nlohmann::json::parse(http->posts[0].substr(http->posts[0].find('\n') + 1)); + REQUIRE(http->posts[0].substr(0, http->posts[0].find('\n')) == "http://127.0.0.1:8080/api/store"); + REQUIRE(posted.at("id") == "store-id"); + REQUIRE_FALSE(posted.contains("ttl_seconds")); + REQUIRE(posted.at("payload").at("file_path") == "C:/tmp/a.gcode"); + REQUIRE(posted.at("payload").at("file_name") == "a.gcode"); + + http->store_response = R"({"ok":true,"data":{"file_exists":false}})"; + const auto missing_file = service.store_preprint_context("store-id", {{"file_path", "C:/tmp/missing.gcode"}}); + REQUIRE(missing_file.ok); + REQUIRE_FALSE(missing_file.file_exists); + + http->store_response = R"({"code":200,"data":{"file_path":"C:/tmp/missing.gcode"},"meta":{"file_exists":false}})"; + const auto legacy_missing_file = service.store_preprint_context("store-id", {{"file_path", "C:/tmp/missing.gcode"}}); + REQUIRE(legacy_missing_file.ok); + REQUIRE_FALSE(legacy_missing_file.file_exists); + + http->store_response = R"({"ok":false})"; + const auto failed_store = service.store_preprint_context("store-id", {{"file_path", "C:/tmp/a.gcode"}}); + REQUIRE_FALSE(failed_store.ok); + REQUIRE(failed_store.error.code == GatewayErrorCode::InvalidResponse); + + const auto invalid_request = service.store_preprint_context("", nlohmann::json::object()); + REQUIRE(invalid_request.error.code == GatewayErrorCode::InvalidRequest); + REQUIRE(http->posts.size() == 4); + + websocket->notify("notify.account.changed", {{"reason", "login"}}); + REQUIRE(account_changes.load() == 1); + + service.stop(); + REQUIRE(service.state() == ConnectionState::Disconnected); +} + +TEST_CASE("GatewayService updates the CLI region", "[gateway][service]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService service(GatewayService::Config{}, make_dependencies(process, http, websocket)); + + REQUIRE(service.start("zh-CN")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + + const auto result = service.set_region("US"); + REQUIRE(!result.error); + REQUIRE(http->posts.size() == 1); + const auto posted = nlohmann::json::parse(http->posts[0].substr(http->posts[0].find('\n') + 1)); + REQUIRE(http->posts[0].substr(0, http->posts[0].find('\n')) == "http://127.0.0.1:8080/api/region"); + REQUIRE(posted.at("region") == "US"); + REQUIRE(posted.size() == 1); + + const auto invalid_region = service.set_region(""); + REQUIRE(invalid_region.error.code == GatewayErrorCode::InvalidRequest); + REQUIRE(http->posts.size() == 1); + + service.stop(); +} + +TEST_CASE("GatewayService concurrent stops are idempotent", "[gateway][service][lifecycle]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService service(GatewayService::Config{}, make_dependencies(process, http, websocket)); + + REQUIRE(service.start("zh-CN")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + + std::vector stoppers; + stoppers.reserve(4); + for (int index = 0; index < 4; ++index) + stoppers.emplace_back([&] { service.stop(); }); + for (std::thread& stopper : stoppers) + stopper.join(); + + REQUIRE(service.state() == ConnectionState::Disconnected); +} + +TEST_CASE("GatewayService reconnects after the first websocket failure", "[gateway][service]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService::Config config; + config.reconnect.initial_delay = std::chrono::milliseconds{1}; + config.reconnect.max_delay = std::chrono::milliseconds{1}; + GatewayService service(config, make_dependencies(process, http, websocket)); + websocket->fail_first = true; + + REQUIRE(service.start("en-US")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + REQUIRE(service.wait_for_connected(std::chrono::milliseconds{100})); + REQUIRE(websocket->connections.load() >= 2); + service.stop(); +} + +TEST_CASE("GatewayService exposes health page URLs before the websocket connects", "[gateway][service]") +{ + auto manager = std::make_shared(ConnectionProcessManager::Config{boost::filesystem::path{ + "snapmaker_connection.exe"}}, + [](const std::vector&) { + ConnectionProcessManager::ProcessRunResult result; + result.stdout_data = "PORT:8888\r\n\r\n"; + return result; + }); + auto http = std::make_shared(); + auto websocket = std::make_shared(); + + GatewayService::Config config; + config.health_timeout = std::chrono::seconds{10}; + GatewayService::Dependencies dependencies; + dependencies.process_manager = manager; + dependencies.http = http; + dependencies.websocket = websocket; + GatewayService service(config, std::move(dependencies)); + + REQUIRE(service.start("zh-CN")); + bool health_ready = false; + for (int i = 0; i < 10000; ++i) { + if (service.port() == 8888 && !service.health().cli_version.empty()) { + health_ready = true; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } + REQUIRE(health_ready); + REQUIRE(service.state() == ConnectionState::Connecting); + REQUIRE(service.web_url("home_page") == "http://127.0.0.1:8080/index"); + + websocket->release_first.set_value(); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + service.stop(); +} + +TEST_CASE("GatewayService force-kills the stale CLI process when a reconnect attempt times out", "[gateway][service]") +{ + auto manager = std::make_shared(ConnectionProcessManager::Config{boost::filesystem::path{ + "snapmaker_connection.exe"}}, + [](const std::vector&) { + ConnectionProcessManager::ProcessRunResult result; + result.stdout_data = "PORT:8888\r\n\r\n"; + return result; + }); + auto http = std::make_shared(); + auto websocket = std::make_shared(); + http->health_ok = false; // every health check fails, so each connect attempt times out + + GatewayService::Config config; + config.health_timeout = std::chrono::milliseconds{50}; + config.reconnect.initial_delay = std::chrono::milliseconds{1}; + config.reconnect.max_delay = std::chrono::milliseconds{1}; + + GatewayService::Dependencies dependencies; + dependencies.process_manager = manager; + dependencies.http = http; + dependencies.websocket = websocket; + GatewayService service(config, std::move(dependencies)); + + REQUIRE(service.start("zh-CN")); + for (int i = 0; i < 1000 && manager->terminate_calls.load() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + REQUIRE(manager->terminate_calls.load() >= 1); + service.stop(); +} + +TEST_CASE("GatewayService clears cached endpoints after websocket disconnect", "[gateway][service]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService::Config config; + config.reconnect.initial_delay = std::chrono::milliseconds{1}; + config.reconnect.max_delay = std::chrono::milliseconds{1}; + config.reconnect.max_attempts = 1; + GatewayService service(config, make_dependencies(process, http, websocket)); + + REQUIRE(service.start("zh-CN")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + REQUIRE_FALSE(service.web_url("home_page").empty()); + + websocket->listener.closed("connection lost"); + REQUIRE(wait_for_state(service, ConnectionState::Disconnected)); + REQUIRE(service.port() == 0); + REQUIRE(service.base_url().empty()); + REQUIRE(service.web_url("home_page").empty()); + REQUIRE(service.localfile_url("a.gcode").empty()); +} + +TEST_CASE("GatewayService bounded wait fails when the gateway cannot start", "[gateway][service]") +{ + std::shared_ptr process = + std::make_shared(ConnectionProcessManager::Config{boost::filesystem::path{"snapmaker_connection.exe"}}, + [](const std::vector&) { + ConnectionProcessManager::ProcessRunResult result; + result.launch_failed = true; + return result; + }); + + GatewayService::Config config; + config.reconnect.initial_delay = std::chrono::milliseconds{1}; + config.reconnect.max_delay = std::chrono::milliseconds{1}; + GatewayService::Dependencies dependencies; + dependencies.process_manager = process; + dependencies.http = std::make_shared(); + dependencies.websocket = std::make_shared(); + GatewayService service(config, dependencies); + + REQUIRE(service.start("en-US")); + REQUIRE_FALSE(service.wait_for_connected(std::chrono::milliseconds{20})); + REQUIRE(service.localfile_url("a.gcode").empty()); + service.stop(); +} + +TEST_CASE("request_sync returns the rpc result", "[gateway][service][sync]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService service(GatewayService::Config{}, make_dependencies(process, http, websocket)); + + REQUIRE(service.start("zh-CN")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + + const auto result = service.request_sync("sync.echo", nlohmann::json::object(), std::chrono::milliseconds{1000}); + REQUIRE_FALSE(result.error); + REQUIRE(result.value.at("echoed") == true); + + service.stop(); +} + +TEST_CASE("request_sync tolerates malformed rpc error fields", "[gateway][service][sync]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService service(GatewayService::Config{}, make_dependencies(process, http, websocket)); + + REQUIRE(service.start("zh-CN")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + + const auto result = service.request_sync("sync.malformed_error", nlohmann::json::object(), std::chrono::milliseconds{1000}); + REQUIRE(result.error); + REQUIRE(result.error.code == GatewayErrorCode::RpcError); + REQUIRE(result.error.message == "JSON-RPC error"); + REQUIRE(result.error.rpc_code == 0); + + const auto unsigned_error = service.request_sync("sync.unsigned_error", nlohmann::json::object(), std::chrono::milliseconds{1000}); + REQUIRE(unsigned_error.error); + REQUIRE(unsigned_error.error.code == GatewayErrorCode::RpcError); + REQUIRE(unsigned_error.error.rpc_code == 2147483647); + + service.stop(); +} + +TEST_CASE("request_sync times out when nothing answers", "[gateway][service][sync]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService service(GatewayService::Config{}, make_dependencies(process, http, websocket)); + + REQUIRE(service.start("zh-CN")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + + const auto start = std::chrono::steady_clock::now(); + const auto result = service.request_sync("sync.silent", nlohmann::json::object(), std::chrono::milliseconds{200}); + const auto elapsed = std::chrono::steady_clock::now() - start; + REQUIRE(result.error); + REQUIRE(result.error.code == GatewayErrorCode::TransportError); + REQUIRE(result.error.message.find("timed out") != std::string::npos); + REQUIRE(elapsed < std::chrono::seconds{2}); + + service.stop(); +} + +TEST_CASE("request_sync fails fast when the websocket drops while waiting", "[gateway][service][sync]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService service(GatewayService::Config{}, make_dependencies(process, http, websocket)); + + REQUIRE(service.start("zh-CN")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + + std::atomic error_code{-1}; + std::atomic elapsed_ms{0}; + std::thread caller([&] { + const auto start = std::chrono::steady_clock::now(); + const auto result = service.request_sync("sync.silent", nlohmann::json::object(), std::chrono::milliseconds{10000}); + const auto end = std::chrono::steady_clock::now(); + elapsed_ms = std::chrono::duration_cast(end - start).count(); + if (result.error) + error_code = static_cast(result.error.code); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds{100}); + websocket->close(); // fail_pending must complete the sync request directly, not via the dispatcher + + caller.join(); + REQUIRE(error_code.load() == static_cast(GatewayErrorCode::NotConnected)); + REQUIRE(elapsed_ms.load() < 3000); // nowhere near the 10 s timeout + + service.stop(); +} + +TEST_CASE("request_sync fails fast when the service stops while waiting", "[gateway][service][sync]") +{ + std::shared_ptr process; + std::shared_ptr http; + std::shared_ptr websocket; + GatewayService service(GatewayService::Config{}, make_dependencies(process, http, websocket)); + + REQUIRE(service.start("zh-CN")); + REQUIRE(wait_for_state(service, ConnectionState::Connected)); + + std::atomic error_code{-1}; + std::atomic elapsed_ms{0}; + std::thread caller([&] { + const auto start = std::chrono::steady_clock::now(); + const auto result = service.request_sync("sync.silent", nlohmann::json::object(), std::chrono::milliseconds{10000}); + const auto end = std::chrono::steady_clock::now(); + elapsed_ms = std::chrono::duration_cast(end - start).count(); + if (result.error) + error_code = static_cast(result.error.code); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds{100}); + service.stop(); // fail_pending must complete the sync request directly, not via the dispatcher + + caller.join(); + const int code = error_code.load(); + REQUIRE((code == static_cast(GatewayErrorCode::NotConnected) || code == static_cast(GatewayErrorCode::Cancelled))); + REQUIRE(elapsed_ms.load() < 3000); // nowhere near the 10 s timeout +} diff --git a/tests/slic3rutils/gateway_tests_main.cpp b/tests/slic3rutils/gateway_tests_main.cpp new file mode 100644 index 000000000000..b2aa80259de3 --- /dev/null +++ b/tests/slic3rutils/gateway_tests_main.cpp @@ -0,0 +1 @@ +#include