diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index e1501e9c4d..51c0d6629c 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1272,6 +1272,9 @@ wxBoxSizer *PreferencesDialog::create_item_network_plugin_version(wxString title if (ver.is_latest) { label += " " + _L("(Latest)"); + } else if (ver.is_discovered && ver.suffix.empty()) { + // A same-series build found on disk (usually installed by the OTA update). + label += " " + _L("(installed)"); } m_network_version_combo->Append(label); if (current_version == ver.version) { diff --git a/src/slic3r/Utils/BBLNetworkPlugin.cpp b/src/slic3r/Utils/BBLNetworkPlugin.cpp index 3d44dbcdb4..cf8c00ccdb 100644 --- a/src/slic3r/Utils/BBLNetworkPlugin.cpp +++ b/src/slic3r/Utils/BBLNetworkPlugin.cpp @@ -833,6 +833,58 @@ std::vector get_all_available_versions() NetworkLibraryVersionInfo::from_discovered(full, base, sfx)); } + // Also surface discovered versions WITHOUT a suffix (typically installed by the + // OTA plug-in update) when they belong to a whitelisted release series: the plugin + // ABI is stable within an AA.BB.CC series (the OTA sync only ever accepts same-series + // updates), so these load with the same struct layout as the static entry they follow. + // The legacy entry is excluded as an anchor - is_legacy_version() matches exactly, so + // a different 01.x build would be loaded with the modern layout and break. + std::vector> series_versions; // {anchor whitelist version, discovered version} + for (const auto& version : discovered) { + if (all_known_versions.count(version) > 0) + continue; + if (!extract_suffix(version).empty()) + continue; + if (version.size() < 8) + continue; + + for (const auto& base : known_base_versions) { + if (BBLNetworkPlugin::is_legacy_version(base)) + continue; + if (base.size() >= 8 && base.compare(0, 8, version, 0, 8) == 0) { + series_versions.emplace_back(base, version); + all_known_versions.insert(version); + break; + } + } + } + + // Ascending sort + insertion right behind the anchor leaves the newest build + // closest to its whitelist entry. + std::sort(series_versions.begin(), series_versions.end(), + [](const auto& a, const auto& b) { + if (a.first != b.first) return a.first > b.first; + return a.second < b.second; + }); + + for (const auto& [anchor, full] : series_versions) { + size_t insert_pos = result.size(); + for (size_t i = 0; i < result.size(); ++i) { + if (result[i].version == anchor) { + // Skip past the whole base-version block (the entry itself plus any + // suffixed dev builds shown under it) so the tree glyphs stay adjacent. + insert_pos = i + 1; + while (insert_pos < result.size() && + result[insert_pos].base_version == anchor) { + ++insert_pos; + } + break; + } + } + result.insert(result.begin() + insert_pos, + NetworkLibraryVersionInfo::from_discovered(full, full, "")); + } + return result; } diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 0342450109..d0fe0a8147 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -2,6 +2,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp test_dev_mapping.cpp + test_network_versions.cpp ) if (MSVC) diff --git a/tests/slic3rutils/test_network_versions.cpp b/tests/slic3rutils/test_network_versions.cpp new file mode 100644 index 0000000000..7df2a98864 --- /dev/null +++ b/tests/slic3rutils/test_network_versions.cpp @@ -0,0 +1,112 @@ +#include + +#include +#include + +#include "libslic3r/Utils.hpp" +#include "slic3r/Utils/bambu_networking.hpp" + +using namespace Slic3r; +namespace fs = boost::filesystem; + +namespace { + +// Platform naming used by BBLNetworkPlugin::scan_plugin_versions(). +#if defined(_MSC_VER) || defined(_WIN32) +static const char* PLUGIN_PREFIX = "bambu_networking_"; +static const char* PLUGIN_EXT = ".dll"; +#elif defined(__WXMAC__) || defined(__APPLE__) +static const char* PLUGIN_PREFIX = "libbambu_networking_"; +static const char* PLUGIN_EXT = ".dylib"; +#else +static const char* PLUGIN_PREFIX = "libbambu_networking_"; +static const char* PLUGIN_EXT = ".so"; +#endif + +struct PluginFolderFixture +{ + fs::path root; + std::string previous_data_dir; + + PluginFolderFixture() + { + previous_data_dir = data_dir(); + root = fs::temp_directory_path() / fs::unique_path("orca-netver-%%%%%%%%"); + fs::create_directories(root / "plugins"); + set_data_dir(root.string()); + } + + ~PluginFolderFixture() + { + set_data_dir(previous_data_dir); + boost::system::error_code ec; + fs::remove_all(root, ec); + } + + void add_plugin(const std::string& version) + { + boost::nowide::ofstream f((root / "plugins" / (PLUGIN_PREFIX + version + PLUGIN_EXT)).string()); + f << "stub"; + } +}; + +int count_version(const std::vector& versions, const std::string& v) +{ + int n = 0; + for (const auto& info : versions) + if (info.version == v) + ++n; + return n; +} + +} // namespace + +TEST_CASE_METHOD(PluginFolderFixture, "Same-series OTA plugin versions are surfaced", "[NetworkVersions]") +{ + add_plugin("02.08.01.55"); // same series as the whitelisted latest -> listed + add_plugin("02.09.00.10"); // unknown series -> not listed + add_plugin("02.08.01.52-custom"); // suffixed build of a whitelisted base -> listed (existing behavior) + add_plugin("02.03.00.62"); // exactly a whitelisted version -> no duplicate + + auto versions = get_all_available_versions(); + + REQUIRE(count_version(versions, "02.08.01.55") == 1); + REQUIRE(count_version(versions, "02.09.00.10") == 0); + REQUIRE(count_version(versions, "02.08.01.52-custom") == 1); + REQUIRE(count_version(versions, "02.03.00.62") == 1); + + size_t latest_pos = versions.size(), ota_pos = versions.size(); + for (size_t i = 0; i < versions.size(); ++i) { + if (versions[i].version == "02.08.01.52") latest_pos = i; + if (versions[i].version == "02.08.01.55") ota_pos = i; + } + REQUIRE(latest_pos < versions.size()); + // The discovered build lands right after the whole 02.08.01.52 block + // (the whitelist entry plus its suffixed dev build). + REQUIRE(ota_pos == latest_pos + 2); + REQUIRE(versions[ota_pos - 1].base_version == "02.08.01.52"); + REQUIRE(versions[ota_pos - 1].version == "02.08.01.52-custom"); + + const auto& ota = versions[ota_pos]; + REQUIRE(ota.is_discovered); + REQUIRE(ota.suffix.empty()); + REQUIRE_FALSE(ota.is_latest); + + // The static whitelist entry keeps its "latest" role. + REQUIRE(versions[latest_pos].is_latest); + REQUIRE(std::string(get_latest_network_version()) == "02.08.01.52"); +} + +TEST_CASE_METHOD(PluginFolderFixture, "Legacy series never adopts discovered builds", "[NetworkVersions]") +{ + // A different build of the legacy series must not be surfaced: is_legacy_version() + // matches exactly, so it would be loaded with the modern struct layout. + std::string legacy = BAMBU_NETWORK_AGENT_VERSION_LEGACY; + std::string legacy_sibling = legacy.substr(0, 9) + (legacy.substr(9) == "99" ? "98" : "99"); + add_plugin(legacy_sibling); + + auto versions = get_all_available_versions(); + + REQUIRE(count_version(versions, legacy_sibling) == 0); + REQUIRE(count_version(versions, legacy) == 1); +}