Files
OrcaSlicer/docs/superpowers/plans/2026-09-03-orca-mqtt-contract-consolidation.md
T

60 KiB
Raw Blame History

OrcaSlicer MQTT Contract Consolidation — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make OrcaPrinterAgent talk to OrcaCloud and OrcaSonar through one OrcaMqttConnection class where LAN and cloud differ only by a Config value — commands published to device/<id>/request, status subscribed on device/<id>/report, on both transports.

Architecture: One OrcaMqttConnection (MQTT 3.1.1 codec + WebSocket transport, ws:// and wss://) in its own translation unit. Two instances: a LAN one owned by OrcaPrinterAgent, a per-printer cloud one owned by OrcaCloudServiceAgent and driven by OrcaPrinterAgent via get_mqtt_connection(). OrcaPrinterAgent is routing + lifecycle only; get_appropriate_mqtt_connection(is_lan) picks the instance and send_request() is the uniform outbound seam. Inbound is a per-connection MessageHandler callback funnelled to on_message_fn (no reader loop). Exactly one socket is connected at a time, for the selected printer.

Tech Stack: C++17, Boost.Beast (WebSocket + TLS), Boost.Asio, nlohmann/json, Catch2 (tests/slic3rutils, target slic3rutils_tests, discovered via orcaslicer_discover_tests).

Spec: docs/superpowers/specs/2026-09-03-orca-mqtt-contract-consolidation-design.md — read it alongside this plan.

Global Constraints

  • No git commits. Per this repo's workflow, all changes stay unstaged; there is no per-task commit. Each task ends at "tests green" (see Workflow note).
  • Build once at the end. Per-task verification builds and runs only the slic3rutils_tests target (cmake --build build --target slic3rutils_tests). The full cmake --build build runs only in the final task.
  • No back-compat. OPCP v1.2.0 replaces v1.1.0. No REST /commands fallback in the client, no /tunnel, no dual-schema handling.
  • sequence_id band: every sequence_id the client emits MUST be a decimal string in 2000029999.
  • Cross-platform: Windows, macOS, Linux. No POSIX-only calls; use Boost/std.
  • Threading: every websocket::stream write goes through write_mutex. Handlers run on the connection's worker thread and must marshal to the UI thread via queue_on_main_fn before touching wx.
  • Topic id: the id in device/<id>/... is the id the client already holds (LAN: the mDNS-advertised device_id passed as dev_id; cloud: the cloud printer UUID). No wildcard subscribe, no learning the id from traffic.

Workflow note (per-task loop, adapted for no-commits)

Each task's last step is "Verify green", not "Commit":

cmake --build build --target slic3rutils_tests -j
ctest --test-dir build -R 'OrcaMqtt|OrcaPrinterAgent' --output-on-failure

Leave changes unstaged. Move to the next task.


File Structure

New:

File Responsibility
src/slic3r/Utils/OrcaMqttConnection.hpp OrcaMqttConnection class: Config, lifecycle (start/stop), send_request, subscribe/unsubscribe, state accessors, and the pure static codec helpers (parse_endpoint, make_*_packet) — public so tests reach them.
src/slic3r/Utils/OrcaMqttConnection.cpp Its implementation (moved verbatim from OrcaCloudServiceAgent.cpp, then extended).
tests/slic3rutils/test_orca_mqtt_connection.cpp Unit tests: codec byte-shapes, parse_endpoint, Config→handshake selection, send_request/subscribe topic strings.
tests/slic3rutils/orca_mqtt_mock_broker.hpp Header-only in-process MQTT-over-WS mock (Beast server on 127.0.0.1:0, plaintext): answers CONNECT→CONNACK, SUBSCRIBE→SUBACK, PUBLISH→PUBACK, and can push a canned message on a subscribed topic. Test-only.
tests/slic3rutils/test_orca_printer_agent.cpp OrcaPrinterAgent routing + lifecycle tests + the parametrized LAN/cloud integration test. Seeded from salvage/orcasonar-lan-agent-2026-09-03:tests/slic3rutils/test_orca_printer_agent.cpp then rewritten for this design.

Modified:

File Change
src/slic3r/Utils/OrcaCloudServiceAgent.hpp Replace the inline OrcaMqttConnection class with #include "OrcaMqttConnection.hpp". Keep get_mqtt_connection(). Add configure_selected_printer_mqtt(std::string dev_id) / teardown_selected_printer_mqtt() decls.
src/slic3r/Utils/OrcaCloudServiceAgent.cpp Move the OrcaMqttConnection impl out. connect_server() stops starting the aggregate socket. Implement configure_selected_printer_mqtt / teardown_selected_printer_mqtt. is_server_connected() from the REST health result only.
src/slic3r/Utils/OrcaPrinterAgent.hpp Remove read_loop / m_read_loop_thread / m_should_end. Add std::atomic<uint64_t> m_lan_generation, std::string m_lan_dev_id, void on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation), and the sequence_id-stamped payload builders.
src/slic3r/Utils/OrcaPrinterAgent.cpp Implement connect_printer, disconnect_printer, set_user_selected_machine, the send_message* collapse, per-connection MessageHandler wiring, generation guard, destructor teardown. Remove read_loop.
src/slic3r/CMakeLists.txt Add Utils/OrcaMqttConnection.cpp + .hpp to SLIC3R_GUI_SOURCES.
tests/slic3rutils/CMakeLists.txt Add test_orca_mqtt_connection.cpp and test_orca_printer_agent.cpp to slic3rutils_tests.

Task 1: Extract OrcaMqttConnection to its own translation unit

Pure move, no behaviour change. Makes every later diff legible.

Files:

  • Create: src/slic3r/Utils/OrcaMqttConnection.hpp, src/slic3r/Utils/OrcaMqttConnection.cpp
  • Modify: src/slic3r/Utils/OrcaCloudServiceAgent.hpp, src/slic3r/Utils/OrcaCloudServiceAgent.cpp, src/slic3r/CMakeLists.txt

Interfaces:

  • Produces: class Slic3r::OrcaMqttConnection at src/slic3r/Utils/OrcaMqttConnection.hpp with today's exact API (start(const std::string& endpoint, TokenProvider, MessageHandler, StateHandler), stop(), is_running(), is_connected(), subscribe(const std::vector<std::string>&), unsubscribe(...), clear_subscriptions(), send_request(const std::string&, const std::string&)).

  • Step 1: Create OrcaMqttConnection.hpp

Move the class OrcaMqttConnection { ... }; block out of OrcaCloudServiceAgent.hpp (currently lines ~34108) into a new header with include guard #ifndef slic3r_OrcaMqttConnection_hpp_. Carry the includes it needs: <boost/beast/websocket.hpp>, <boost/beast/ssl.hpp>, <boost/asio/ssl.hpp>, <atomic>, <condition_variable>, <functional>, <memory>, <mutex>, <set>, <string>, <thread>, <vector>, <cstdint>. Keep it in namespace Slic3r.

  • Step 2: Create OrcaMqttConnection.cpp

Move every OrcaMqttConnection:: and OrcaMqttConnection::Connection definition out of OrcaCloudServiceAgent.cpp (the block from struct OrcaMqttConnection::Connection { to the end of OrcaMqttConnection::run()), plus the file-local using json = nlohmann::json; / anonymous-namespace helpers those definitions use. #include "OrcaMqttConnection.hpp" at the top; carry the boost/log/json includes it references.

  • Step 3: Point OrcaCloudServiceAgent at the new header

In OrcaCloudServiceAgent.hpp replace the removed class with #include "OrcaMqttConnection.hpp". OrcaCloudServiceAgent.cpp keeps compiling (it already #includes its own header).

  • Step 4: Add to the build

In src/slic3r/CMakeLists.txt, in set(SLIC3R_GUI_SOURCES next to Utils/OrcaCloudServiceAgent.cpp:

    Utils/OrcaMqttConnection.cpp
    Utils/OrcaMqttConnection.hpp
  • Step 5: Verify green
cmake --build build --target slic3rutils_tests -j
ctest --test-dir build -R 'printer_agent|Orca' --output-on-failure

Expected: builds; existing test_printer_agent / test_qidi_printer_agent unchanged and passing. No new tests yet.


Task 2: Config struct + parse_endpoint for ws://

Files:

  • Modify: src/slic3r/Utils/OrcaMqttConnection.hpp, src/slic3r/Utils/OrcaMqttConnection.cpp
  • Create: tests/slic3rutils/test_orca_mqtt_connection.cpp
  • Modify: tests/slic3rutils/CMakeLists.txt

Interfaces:

  • Produces:

    struct OrcaMqttConnection::Config {
        std::string   url;
        bool          use_tls = false;
        TokenProvider bearer_provider;       // set => bearer on WS upgrade, CONNECT creds omitted
        std::string   username;
        std::string   password;
        std::string   client_id  = "OrcaSlicer";
        int           keepalive_seconds = 60;
    };
    static bool OrcaMqttConnection::parse_endpoint(const std::string& url, Endpoint& out); // now public, ws:// + wss://
    struct OrcaMqttConnection::Endpoint { std::string host; std::string port; std::string target; };
    

    Endpoint and parse_endpoint move to the public: section.

  • Step 1: Write the failing test

Create tests/slic3rutils/test_orca_mqtt_connection.cpp:

#include <catch2/catch_test_macros.hpp>
#include "slic3r/Utils/OrcaMqttConnection.hpp"

using Slic3r::OrcaMqttConnection;

TEST_CASE("OrcaMqtt parse_endpoint handles ws and wss", "[OrcaMqtt]") {
    OrcaMqttConnection::Endpoint ep;

    REQUIRE(OrcaMqttConnection::parse_endpoint("ws://printer.local:8280/mqtt", ep));
    CHECK(ep.host   == "printer.local");
    CHECK(ep.port   == "8280");
    CHECK(ep.target == "/mqtt");

    REQUIRE(OrcaMqttConnection::parse_endpoint("ws://10.0.0.5/mqtt", ep));
    CHECK(ep.port == "80");

    REQUIRE(OrcaMqttConnection::parse_endpoint("wss://api.example.com/api/v1/printers/abc/mqtt", ep));
    CHECK(ep.host   == "api.example.com");
    CHECK(ep.port   == "443");
    CHECK(ep.target == "/api/v1/printers/abc/mqtt");

    CHECK_FALSE(OrcaMqttConnection::parse_endpoint("http://x/y", ep));
}
  • Step 2: Register the test file and run it (expect FAIL)

Add test_orca_mqtt_connection.cpp to the add_executable(${_TEST_NAME}_tests list in tests/slic3rutils/CMakeLists.txt.

cmake --build build --target slic3rutils_tests -j

Expected: compile error — parse_endpoint is private / Endpoint is private / no ws:// support.

  • Step 3: Make it pass

In OrcaMqttConnection.hpp move struct Endpoint and static bool parse_endpoint(...) into public:. Add the Config struct (above) in public:.

In OrcaMqttConnection.cpp replace the wss://-only parse_endpoint body:

bool OrcaMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) {
    std::string rest;
    std::string default_port;
    if      (url.rfind("wss://", 0) == 0) { rest = url.substr(6); default_port = "443"; }
    else if (url.rfind("ws://",  0) == 0) { rest = url.substr(5); default_port = "80";  }
    else return false;

    const auto slash = rest.find('/');
    const std::string authority = rest.substr(0, slash);
    endpoint.target = (slash == std::string::npos) ? "/" : rest.substr(slash);

    // host[:port] — leave an unbracketed IPv6 literal alone
    const auto colon = authority.rfind(':');
    if (colon != std::string::npos && authority.find(']') == std::string::npos) {
        endpoint.host = authority.substr(0, colon);
        endpoint.port = authority.substr(colon + 1);
    } else {
        endpoint.host = authority;
        endpoint.port = default_port;
    }
    return !endpoint.host.empty() && !endpoint.port.empty() && !endpoint.target.empty();
}
  • Step 4: Verify green
cmake --build build --target slic3rutils_tests -j
ctest --test-dir build -R OrcaMqtt --output-on-failure

Expected: PASS.


Task 3: make_connect_packet with client-id / auth / keepalive

Files:

  • Modify: src/slic3r/Utils/OrcaMqttConnection.hpp, .cpp, tests/slic3rutils/test_orca_mqtt_connection.cpp

Interfaces:

  • Consumes: Config (Task 2).

  • Produces: static std::vector<uint8_t> OrcaMqttConnection::make_connect_packet(const std::string& client_id, const std::string& username, const std::string& password, int keepalive_seconds); — public. Clean-session always set; username/password flags + fields only when username non-empty.

  • Step 1: Write the failing test

TEST_CASE("OrcaMqtt CONNECT packet — no auth (cloud form)", "[OrcaMqtt]") {
    auto p = OrcaMqttConnection::make_connect_packet("OrcaSlicer", "", "", 300);
    REQUIRE(p.size() >= 12);
    CHECK(p[0] == 0x10);                 // CONNECT
    // variable header: protocol name "MQTT", level 4
    CHECK(p[2] == 'M'); CHECK(p[3] == 'Q'); CHECK(p[4] == 'T'); CHECK(p[5] == 'T');
    CHECK(p[6] == 0x04);
    CHECK(p[7] == 0x02);                 // connect flags: clean session only
    CHECK(((p[8] << 8) | p[9]) == 300);  // keepalive
}

TEST_CASE("OrcaMqtt CONNECT packet — username/password (LAN form)", "[OrcaMqtt]") {
    auto p = OrcaMqttConnection::make_connect_packet("orcaslicer-lan-x", "orcasonar", "code123", 60);
    CHECK(p[0] == 0x10);
    CHECK(p[7] == (0x02 | 0x80 | 0x40)); // clean session + username + password flags
    // payload contains the client id, then username, then password strings
    const std::string blob(p.begin(), p.end());
    CHECK(blob.find("orcaslicer-lan-x") != std::string::npos);
    CHECK(blob.find("orcasonar")        != std::string::npos);
    CHECK(blob.find("code123")          != std::string::npos);
}
  • Step 2: Run it (expect FAIL)
cmake --build build --target slic3rutils_tests -j

Expected: compile error — make_connect_packet takes no args / is private.

  • Step 3: Implement

Move make_connect_packet to public: and replace its body:

std::vector<uint8_t> OrcaMqttConnection::make_connect_packet(
    const std::string& client_id, const std::string& username,
    const std::string& password, int keepalive_seconds) {
    std::vector<uint8_t> packet{0x10};
    append_string(packet, "MQTT");
    packet.push_back(4); // protocol level 3.1.1

    uint8_t flags = 0x02; // clean session
    if (!username.empty()) { flags |= 0x80; if (!password.empty()) flags |= 0x40; }
    packet.push_back(flags);

    packet.push_back(static_cast<uint8_t>(keepalive_seconds >> 8));
    packet.push_back(static_cast<uint8_t>(keepalive_seconds & 0xff));

    append_string(packet, client_id.empty() ? "OrcaSlicer" : client_id);
    if (!username.empty()) {
        append_string(packet, username);
        if (!password.empty()) append_string(packet, password);
    }
    prepend_remaining_length(packet, packet.size() - 1);
    return packet;
}

Update the one caller in connect_and_read() to pass current_config.client_id, current_config.username, current_config.password, current_config.keepalive_seconds (the current_config member arrives in Task 6; until then pass "OrcaSlicer","","",60 and leave a // TODO(Task 6): from Config marker — remove the marker in Task 6).

  • Step 4: Verify green
ctest --test-dir build -R OrcaMqtt --output-on-failure

Expected: PASS.


Task 4: request / report topic packet builders

Files:

  • Modify: src/slic3r/Utils/OrcaMqttConnection.hpp, .cpp, tests/slic3rutils/test_orca_mqtt_connection.cpp

Interfaces:

  • Produces (all public, all static):

    static std::string request_topic(const std::string& dev_id);            // "device/<id>/request"
    static std::string report_topic(const std::string& dev_id);             // "device/<id>/report"  (already exists; make public)
    static std::vector<uint8_t> make_publish_packet(const std::string& topic, const std::string& payload); // QoS 0
    static std::vector<uint8_t> make_subscribe_packet(uint16_t packet_id, const std::string& topic, uint8_t qos);
    static std::vector<uint8_t> make_unsubscribe_packet(uint16_t packet_id, const std::string& topic);
    
  • Step 1: Write the failing test

TEST_CASE("OrcaMqtt topic helpers", "[OrcaMqtt]") {
    CHECK(OrcaMqttConnection::request_topic("abc") == "device/abc/request");
    CHECK(OrcaMqttConnection::report_topic("abc")  == "device/abc/report");
}

TEST_CASE("OrcaMqtt PUBLISH packet QoS0", "[OrcaMqtt]") {
    auto p = OrcaMqttConnection::make_publish_packet("device/abc/request", "{\"ok\":1}");
    CHECK((p[0] & 0xf0) == 0x30);   // PUBLISH
    CHECK((p[0] & 0x06) == 0x00);   // QoS 0
    const std::string blob(p.begin(), p.end());
    CHECK(blob.find("device/abc/request") != std::string::npos);
    CHECK(blob.find("{\"ok\":1}")          != std::string::npos);
}

TEST_CASE("OrcaMqtt SUBSCRIBE packet", "[OrcaMqtt]") {
    auto p = OrcaMqttConnection::make_subscribe_packet(7, "device/abc/report", 1);
    CHECK(p[0] == 0x82);                       // SUBSCRIBE + reserved bit
    CHECK(((p[2] << 8) | p[3]) == 7);          // packet id
    CHECK(p.back() == 1);                      // requested QoS
}
  • Step 2: Run it (expect FAIL)cmake --build build --target slic3rutils_tests -j; missing symbols.

  • Step 3: Implement

Make report_topic public. Add:

std::string OrcaMqttConnection::request_topic(const std::string& id) { return "device/" + id + "/request"; }

std::vector<uint8_t> OrcaMqttConnection::make_publish_packet(const std::string& topic, const std::string& payload) {
    std::vector<uint8_t> packet{0x30}; // PUBLISH, QoS 0, no retain
    append_string(packet, topic);      // no packet id at QoS 0
    packet.insert(packet.end(), payload.begin(), payload.end());
    prepend_remaining_length(packet, packet.size() - 1);
    return packet;
}

std::vector<uint8_t> OrcaMqttConnection::make_subscribe_packet(uint16_t id, const std::string& topic, uint8_t qos) {
    std::vector<uint8_t> packet{0x82};
    packet.push_back(id >> 8); packet.push_back(id & 0xff);
    append_string(packet, topic);
    packet.push_back(qos);
    prepend_remaining_length(packet, packet.size() - 1);
    return packet;
}

std::vector<uint8_t> OrcaMqttConnection::make_unsubscribe_packet(uint16_t id, const std::string& topic) {
    std::vector<uint8_t> packet{0xA2};
    packet.push_back(id >> 8); packet.push_back(id & 0xff);
    append_string(packet, topic);
    prepend_remaining_length(packet, packet.size() - 1);
    return packet;
}

Keep the existing make_topic_packet(uint8_t, uint16_t, const std::vector<std::string>&) for now — Task 5 removes its callers.

  • Step 4: Verify greenctest --test-dir build -R OrcaMqtt --output-on-failure

Task 5: single-id subscribe / unsubscribe + persistent set; Config-carrying start

Files:

  • Modify: src/slic3r/Utils/OrcaMqttConnection.hpp, .cpp, tests/slic3rutils/test_orca_mqtt_connection.cpp

Interfaces:

  • Produces:

    bool start(const Config& config, MessageHandler on_message, StateHandler on_state);
    bool subscribe(const std::string& dev_id);     // adds report_topic(dev_id) to the set, SUBSCRIBEs if connected
    bool unsubscribe(const std::string& dev_id);
    int  last_connack_rc() const;                   // 0 ok, 1..5 refusal, -1 none this attempt
    

    The old start(const std::string&, TokenProvider, ...) and vector subscribe/unsubscribe are removed (no back-compat). report_topic set members replace the device-id set.

  • Step 1: Write the failing test

TEST_CASE("OrcaMqtt start takes a Config", "[OrcaMqtt]") {
    OrcaMqttConnection conn;
    OrcaMqttConnection::Config cfg;
    cfg.url = "ws://127.0.0.1:1/mqtt";           // nothing listening
    cfg.keepalive_seconds = 42;
    // start() returns false (no server) but must compile with the Config overload
    const bool ok = conn.start(cfg, [](auto, auto){}, [](bool, bool){});
    CHECK_FALSE(ok);
    CHECK(conn.last_connack_rc() == -1);
    conn.stop();
}
  • Step 2: Run it (expect FAIL) — compile error: no start(Config, ...), no last_connack_rc.

  • Step 3: Implement

  • Replace the endpoint_url / get_token members with Config current_config; and std::atomic<int> m_last_connack_rc{-1};.

  • Replace std::set<std::string> subscriptions semantics: it now holds full report-topic strings. Add pending_subscriptions / pending_unsubscriptions as topic strings too (rename in place).

  • New start:

    bool OrcaMqttConnection::start(const Config& config, MessageHandler on_message, StateHandler on_state) {
        stop();
        { std::lock_guard<std::mutex> l(mutex);
          current_config = config; this->on_message = std::move(on_message); this->on_state = std::move(on_state);
          initial_completed = false; initial_result = false; connected = false; m_last_connack_rc = -1; }
        stopping.store(false);
        worker = std::thread(&OrcaMqttConnection::run, this);
        std::unique_lock<std::mutex> l(mutex);
        initial_cv.wait_for(l, std::chrono::seconds(10), [this]{ return initial_completed; });
        return initial_result;
    }
    
  • subscribe(dev_id) / unsubscribe(dev_id):

    bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
        const std::string topic = report_topic(dev_id);
        { std::lock_guard<std::mutex> l(mutex);
          subscriptions.insert(topic); pending_unsubscriptions.erase(topic);
          pending_subscriptions.insert(topic); }
        flush_subscription_change();
        return true;
    }
    

    (mirror for unsubscribe).

  • connect_and_read() / send_current_subscriptions / send_pending_subscriptions: iterate topic strings, build with make_subscribe_packet(next_packet_id++, topic, 1) / make_unsubscribe_packet. Delete make_topic_packet and its declaration.

  • In connect_and_read() use current_config: parse_endpoint(current_config.url, ep); branch TLS on current_config.use_tls (Task 7 completes the plaintext branch); build CONNECT via make_connect_packet(current_config.client_id, current_config.username, current_config.password, current_config.keepalive_seconds)delete the Task 3 TODO marker; add the Authorization: Bearer upgrade header only when current_config.bearer_provider is set.

  • On CONNACK: m_last_connack_rc = <byte 3 of CONNACK>;. If rc ∈ {4,5}: stop the worker, do not retry (terminal).

  • Update OrcaCloudServiceAgent.cpp::connect_server() (its only caller) to the new signature — see Task 15; for now make it compile with a Config built from today's wss://.../api/v1/printers/mqtt URL and bearer_provider = [this]{ return get_access_token(); }.

  • Step 4: Verify greenctest --test-dir build -R OrcaMqtt --output-on-failure (the new test plus Tasks 24). Also ctest -R 'printer_agent' still green.


Task 6: send_request publishes to device/<id>/request

Files:

  • Modify: src/slic3r/Utils/OrcaMqttConnection.cpp, tests/slic3rutils/test_orca_mqtt_connection.cpp

Interfaces:

  • Consumes: make_publish_packet, request_topic, ws_write/send (existing write path), write_mutex.

  • Produces: bool send_request(const std::string& dev_id, const std::string& payload) — returns false without sending if !is_connected().

  • Step 1: Write the failing test (uses the mock broker from Task 8 — so this test is added but [.]-hidden until Task 9 wires it; for now assert the guard):

TEST_CASE("OrcaMqtt send_request refuses when not connected", "[OrcaMqtt]") {
    OrcaMqttConnection conn;
    CHECK_FALSE(conn.send_request("abc", "{\"pushing\":{\"command\":\"pushall\",\"sequence_id\":\"20001\"}}"));
}
  • Step 2: Run it (expect FAIL)send_request currently returns something else / is unimplemented.

  • Step 3: Implement

bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::string& payload) {
    if (!connected.load()) return false;
    std::shared_ptr<Connection> conn;
    { std::lock_guard<std::mutex> l(connection_mutex); conn = active_connection; }
    if (!conn) return false;
    const auto packet = make_publish_packet(request_topic(dev_id), payload);
    std::lock_guard<std::mutex> w(write_mutex);
    try { send(conn->websocket, packet); }          // existing write helper
    catch (const std::exception& e) {
        BOOST_LOG_TRIVIAL(warning) << "OrcaMqtt send_request failed: " << e.what();
        return false;
    }
    return true;
}

(If Task 7 renamed sendws_write, use that.)

  • Step 4: Verify greenctest --test-dir build -R OrcaMqtt --output-on-failure

Task 7: plaintext (ws://) transport branch

Files:

  • Modify: src/slic3r/Utils/OrcaMqttConnection.hpp, .cpp

Interfaces:

  • Consumes: Config.use_tls, Config.bearer_provider.

  • Produces: struct Connection holds std::optional<PlainWebSocket> ws; and std::optional<TlsWebSocket> wss; with PlainWebSocket = websocket::stream<beast::tcp_stream> and TlsWebSocket = websocket::stream<ssl::stream<beast::tcp_stream>>. Internal helpers ws_write(Connection&, const std::vector<uint8_t>&), ws_read(Connection&, beast::flat_buffer&, error_code&), ws_handshake(Connection&, const Config&, const Endpoint&), ws_close(Connection&) dispatch on which optional is engaged.

  • Step 1: Write the failing test (needs the mock broker — mark [.integration], un-hide in Task 9):

TEST_CASE("OrcaMqtt connects over plaintext ws", "[OrcaMqtt][.integration]") {
    orca_mqtt_test::MockBroker broker;                    // Task 8
    OrcaMqttConnection conn;
    OrcaMqttConnection::Config cfg; cfg.url = broker.ws_url(); cfg.use_tls = false;
    std::atomic<bool> up{false};
    REQUIRE(conn.start(cfg, [](auto,auto){}, [&](bool c, bool){ if (c) up = true; }));
    CHECK(conn.is_connected());
    conn.stop();
}
  • Step 2: Run it (expect FAIL to build / link) — no ws_handshake etc.

  • Step 3: Implement

  • Connection gets both optionals + the shared beast::flat_buffer read_buffer; and a net::io_context io; / ssl::context tls{ssl::context::tlsv12_client}; as today.

  • ws_handshake: resolve host/port; beast::get_lowest_layer(stream).connect(results); if use_tls: set SNI (SSL_set_tlsext_host_name), stream.next_layer().handshake(ssl::stream_base::client). Then set the upgrade decorator that adds Authorization: Bearer <token> when config.bearer_provider is set, and Sec-WebSocket-Protocol: mqtt. Then stream.handshake(host, target).

  • ws_write / ws_read / ws_close: if (conn.wss) conn.wss->...; else conn.ws->...;.

  • connect_and_read(): replace direct WebSocket use with a Connection and the ws_* helpers; construct conn->ws.emplace(conn->io) or conn->wss.emplace(conn->io, conn->tls) based on current_config.use_tls.

  • Delete the old using WebSocket = websocket::stream<ssl::stream<...>>; typedef and the send(WebSocket&, ...) signature (fold into ws_write).

API note: exact Beast calls (beast::get_lowest_layer, tcp_stream::connect, decorator signature) vary by Boost version. Match the version already vendored in deps/; the existing TLS code in this file is the reference for the wss side.

  • Step 4: Verify green
cmake --build build --target slic3rutils_tests -j
ctest --test-dir build -R 'OrcaMqtt' --output-on-failure   # non-integration subset still green

Task 8: in-process MQTT-over-WS mock broker

Files:

  • Create: tests/slic3rutils/orca_mqtt_mock_broker.hpp

Interfaces:

  • Produces:

    namespace orca_mqtt_test {
    class MockBroker {                       // starts on ctor, stops on dtor
    public:
        MockBroker();
        ~MockBroker();
        std::string ws_url() const;          // "ws://127.0.0.1:<port>/mqtt"
        void push_report(const std::string& dev_id, const std::string& payload); // server->client PUBLISH on device/<id>/report
        std::vector<std::string> received_requests() const;   // payloads PUBLISHed by the client to any device/<id>/request
        int connect_count() const;
    };
    }
    
  • Step 1: Smoke test

#include "orca_mqtt_mock_broker.hpp"
TEST_CASE("MockBroker starts and reports a url", "[OrcaMqtt][.integration]") {
    orca_mqtt_test::MockBroker b;
    CHECK(b.ws_url().rfind("ws://127.0.0.1:", 0) == 0);
    CHECK(b.connect_count() == 0);
}
  • Step 2: Run it (expect FAIL) — header missing.

  • Step 3: Implement

Header-only. One std::thread running a net::io_context; tcp::acceptor on {net::ip::make_address("127.0.0.1"), 0} (port 0 → OS-assigned, read back via acceptor.local_endpoint().port()). On accept: websocket::stream<beast::tcp_stream>, accept the upgrade echoing Sec-WebSocket-Protocol: mqtt. Then a minimal MQTT read loop:

  • 0x10 CONNECT → reply 0x20 0x02 0x00 0x00 (CONNACK accepted); connect_count_++.
  • 0x82 SUBSCRIBE → reply 0x90 SUBACK with the echoed packet id and one granted-QoS byte 0x00; remember the subscribed topic.
  • 0x30 PUBLISH (QoS 0) → parse topic + payload; if topic ends /request, append payload to received_requests_.
  • 0xC0 PINGREQ → reply 0xD0 0x00.
  • 0xE0 DISCONNECT / read error → close. push_report() posts a make-style PUBLISH (0x30, topic device/<id>/report, payload) onto the connected client socket via net::post(strand, ...).

Use only Boost already vendored. Guard all shared state (received_requests_, connect_count_) with a std::mutex.

  • Step 4: Verify greenctest --test-dir build -R 'MockBroker' --output-on-failure with --allow-running-no-tests off; run the hidden tag explicitly: ctest --test-dir build -R OrcaMqtt -C RelWithDebInfo then the test binary directly: ./build/tests/slic3rutils/slic3rutils_tests "[.integration]".

Task 9: parametrized LAN/cloud integration test

Files:

  • Modify: tests/slic3rutils/test_orca_mqtt_connection.cpp

Interfaces:

  • Consumes: MockBroker (Task 8), OrcaMqttConnection::start/subscribe/send_request (Tasks 57).

  • Step 1: Write the test

static void run_round_trip(bool use_tls_flag_only) {
    orca_mqtt_test::MockBroker broker;
    OrcaMqttConnection conn;
    OrcaMqttConnection::Config cfg;
    cfg.url = broker.ws_url();                     // plaintext regardless
    cfg.use_tls = false;                           // the mock is plaintext; the flag path is unit-tested elsewhere
    if (use_tls_flag_only) cfg.bearer_provider = []{ return std::string("tok"); };
    else { cfg.username = "orcasonar"; cfg.password = "code"; }

    std::promise<std::pair<std::string,std::string>> got;
    REQUIRE(conn.start(cfg,
        [&](const std::string& id, const std::string& payload){ got.set_value({id, payload}); },
        [](bool,bool){}));
    REQUIRE(conn.subscribe("dev-1"));
    REQUIRE(conn.send_request("dev-1", R"({"pushing":{"command":"pushall","sequence_id":"20001"}})"));

    broker.push_report("dev-1", R"({"print":{"command":"push_status","sequence_id":"20001","result":"success"}})");
    auto fut = got.get_future();
    REQUIRE(fut.wait_for(std::chrono::seconds(3)) == std::future_status::ready);
    auto [id, payload] = fut.get();
    CHECK(id == "dev-1");
    CHECK(payload.find("push_status") != std::string::npos);

    // the client's command reached the broker on the request topic
    CHECK(broker.received_requests().size() == 1);
    conn.stop();
}

TEST_CASE("OrcaMqtt round-trip — LAN-style config",   "[OrcaMqtt][.integration]") { run_round_trip(false); }
TEST_CASE("OrcaMqtt round-trip — cloud-style config", "[OrcaMqtt][.integration]") { run_round_trip(true);  }
  • Step 2: Run it (expect FAIL then iterate)./build/tests/slic3rutils/slic3rutils_tests "[.integration]".

  • Step 3: Fix defects in OrcaMqttConnection until both cases pass with identical assertions. Un-hide the Task 6/7 integration tests ([.integration] → keep the tag; they run via explicit tag selection).

  • Step 4: Verify green./build/tests/slic3rutils/slic3rutils_tests "[OrcaMqtt]" (all, including [.integration]).


Task 10: OrcaPrinterAgent — remove read_loop, wire per-connection inbound

Files:

  • Modify: src/slic3r/Utils/OrcaPrinterAgent.hpp, .cpp
  • Create: tests/slic3rutils/test_orca_printer_agent.cpp (seed from salvage/orcasonar-lan-agent-2026-09-03)
  • Modify: tests/slic3rutils/CMakeLists.txt

Interfaces:

  • Produces: void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload) (private) — snapshots on_message_fn under state_mutex, calls it (marshalling via queue_on_main_fn when set). Used as the body of every connection's MessageHandler.

  • Step 1: Seed + write the failing test

git show salvage/orcasonar-lan-agent-2026-09-03:tests/slic3rutils/test_orca_printer_agent.cpp \
  > tests/slic3rutils/test_orca_printer_agent.cpp

Then replace its body with this design's tests. First test:

#include <catch2/catch_test_macros.hpp>
#include "slic3r/Utils/OrcaPrinterAgent.hpp"
using Slic3r::OrcaPrinterAgent;

TEST_CASE("OrcaPrinterAgent forwards a status payload to on_message_fn", "[OrcaPrinterAgent]") {
    OrcaPrinterAgent agent("/tmp");
    std::string got_id, got_payload;
    agent.set_on_message_fn([&](std::string id, std::string p){ got_id = id; got_payload = p; });
    agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status"}})");   // test-only hook
    CHECK(got_id == "dev-1");
    CHECK(got_payload.find("push_status") != std::string::npos);
}

Add test_orca_printer_agent.cpp to tests/slic3rutils/CMakeLists.txt.

  • Step 2: Run it (expect FAIL)deliver_to_sink missing; read_loop still present.

  • Step 3: Implement

  • OrcaPrinterAgent.hpp: delete read_loop, m_read_loop_thread, m_should_end. Add void deliver_to_sink(const std::string& dev_id, const std::string& payload);.

  • OrcaPrinterAgent.cpp: delete the read_loop definition and its ctor std::thread(...)/detach(). Delete the file-scope std::mutex m_conn_type_mtx;. Ctor body becomes empty (or just the log line). Add:

    void OrcaPrinterAgent::deliver_to_sink(const std::string& dev_id, const std::string& payload) {
        OnMessageFn fn; QueueOnMainFn q;
        { std::lock_guard<std::mutex> l(state_mutex); fn = on_message_fn; q = queue_on_main_fn; }
        if (!fn) return;
        if (q) q([fn, dev_id, payload]{ fn(dev_id, payload); });
        else   fn(dev_id, payload);
    }
    
  • In set_cloud_agent, the existing set_printer_status_callback lambda body becomes deliver_to_sink(std::move(dev_id), std::move(payload));.

  • Step 4: Verify greenctest --test-dir build -R OrcaPrinterAgent --output-on-failure


Task 11: connect_printer builds + starts the LAN connection

Files:

  • Modify: src/slic3r/Utils/OrcaPrinterAgent.hpp, .cpp, tests/slic3rutils/test_orca_printer_agent.cpp

Interfaces:

  • Consumes: OrcaMqttConnection::Config, start.

  • Produces:

    static bool OrcaPrinterAgent::parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port); // "8280" default, "/mqtt" implied
    static std::string OrcaPrinterAgent::make_lan_client_id(const std::string& dev_id);
    

    New members: std::atomic<uint64_t> m_lan_generation{0}, std::string m_lan_dev_id.

  • Step 1: Write the failing test

TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") {
    std::string h, p;
    REQUIRE(OrcaPrinterAgent::parse_lan_endpoint("192.168.1.9", h, p));
    CHECK(h == "192.168.1.9"); CHECK(p == "8280");
    REQUIRE(OrcaPrinterAgent::parse_lan_endpoint("http://host.local:9000/x", h, p));
    CHECK(h == "host.local"); CHECK(p == "9000");
}

TEST_CASE("connect_printer stands up a LAN connection", "[OrcaPrinterAgent]") {
    OrcaPrinterAgent agent("/tmp");
    // 10.255.255.1 is unroutable → start() returns fast-ish; we only assert wiring
    const int rc = agent.connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false);
    CHECK(rc == BAMBU_NETWORK_SUCCESS);
    CHECK(agent.get_user_selected_machine().empty());        // LAN path does not set the cloud selection
    agent.disconnect_printer();
}
  • Step 2: Run it (expect FAIL) — helpers missing; connect_printer is a stub returning success without doing anything (make the wiring assertion fail by checking an observable — see Step 3 for the observable: a protected lan_connection_url() test hook).

  • Step 3: Implement

  • Add parse_lan_endpoint (scheme strip, host[:port], default 8280) and make_lan_client_id ("orcaslicer-lan-" + dev_id + "-" + <8 hex, drawn once per process>).

  • connect_printer:

    int OrcaPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip,
                                          std::string username, std::string password, bool use_ssl) {
        if (dev_id.empty() || dev_ip.empty()) return BAMBU_NETWORK_ERR_INVALID_HANDLE;
        std::string host, port;
        if (!parse_lan_endpoint(dev_ip, host, port)) return BAMBU_NETWORK_ERR_INVALID_HANDLE;
        disconnect_printer();
        const uint64_t gen = ++m_lan_generation;
        OrcaMqttConnection::Config cfg;
        cfg.url      = "ws://" + host + ":" + port + "/mqtt";
        cfg.use_tls  = false;                       // OrcaSonar LAN is plaintext; use_ssl ignored
        cfg.username = username.empty() ? "orcasonar" : username;
        cfg.password = password;
        cfg.client_id = make_lan_client_id(dev_id);
        cfg.keepalive_seconds = 60;
        { std::lock_guard<std::mutex> l(state_mutex); m_lan_dev_id = dev_id; m_current_connection = LAN; }
        lan_mqtt_connection = std::make_unique<OrcaMqttConnection>();
        auto* conn = lan_mqtt_connection.get();
        std::thread([this, conn, cfg, dev_id, gen] {
            const bool ok = conn->start(cfg,
                [this, gen](const std::string& id, const std::string& p){ if (gen == m_lan_generation.load()) deliver_to_sink(id, p); },
                [this, gen, dev_id, conn](bool c, bool initial){ if (c && !initial && gen == m_lan_generation.load()) on_connected(dev_id, conn, gen); });
            if (ok && gen == m_lan_generation.load()) on_connected(dev_id, conn, gen);
        }).detach();
        return BAMBU_NETWORK_SUCCESS;
    }
    
  • Add a protected test hook: std::string lan_connection_target() const { return lan_mqtt_connection ? /* Config.url snapshot */ : ""; } — store cfg.url in a std::string m_lan_url; member when building it, and return that. Assert it in the test.

  • Step 4: Verify greenctest --test-dir build -R OrcaPrinterAgent --output-on-failure


Task 12: on_connected — the shared post-connect sequence

Files:

  • Modify: src/slic3r/Utils/OrcaPrinterAgent.hpp, .cpp, tests/slic3rutils/test_orca_printer_agent.cpp

Interfaces:

  • Consumes: OrcaMqttConnection::subscribe, send_request.

  • Produces:

    void OrcaPrinterAgent::on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation);
    static std::string OrcaPrinterAgent::seq(int n);   // "2000" + zero-padded band offset; returns a string in 20000..29999
    static std::string OrcaPrinterAgent::build_pushing_start(const std::string& sequence_id);
    static std::string OrcaPrinterAgent::build_pushall(const std::string& sequence_id);
    static std::string OrcaPrinterAgent::build_get_version(const std::string& sequence_id);
    static std::string OrcaPrinterAgent::build_get_capabilities(const std::string& sequence_id);
    
  • Step 1: Write the failing test — a fake OrcaMqttConnection subclass is not available (non-virtual). Instead test the payload builders + call order via a seam: on_connected takes an OrcaMqttConnection*; make the four send_request payloads and the subscribe observable by having on_connected delegate to a protected virtual void emit_connect_sequence(const std::string&, std::function<void(const std::string&)> subscribe, std::function<void(const std::string&)> request) that the test overrides.

TEST_CASE("post-connect sequence is subscribe then 4 requests in order", "[OrcaPrinterAgent]") {
    struct Probe : OrcaPrinterAgent {
        using OrcaPrinterAgent::OrcaPrinterAgent;
        std::vector<std::string> calls;
        void emit_connect_sequence(const std::string& dev_id,
            std::function<void(const std::string&)> sub,
            std::function<void(const std::string&)> req) override {
            OrcaPrinterAgent::emit_connect_sequence(dev_id,
                [&](const std::string& id){ calls.push_back("sub:" + id); },
                [&](const std::string& body){ calls.push_back(body); });
        }
    } probe("/tmp");
    probe.run_connect_sequence_for_test("dev-1");     // thin public shim calling emit_connect_sequence
    REQUIRE(probe.calls.size() == 5);
    CHECK(probe.calls[0] == "sub:dev-1");
    CHECK(probe.calls[1].find("\"pushing\"")  != std::string::npos);
    CHECK(probe.calls[1].find("\"start\"")    != std::string::npos);
    CHECK(probe.calls[2].find("pushall")      != std::string::npos);
    CHECK(probe.calls[3].find("get_version")  != std::string::npos);
    CHECK(probe.calls[4].find("get_capabilities") != std::string::npos);
    for (auto& c : probe.calls)                       // sequence_id band
        if (auto pos = c.find("sequence_id"); pos != std::string::npos)
            CHECK(c.substr(pos).find("\"2") != std::string::npos);
}
  • Step 2: Run it (expect FAIL) — members missing.

  • Step 3: Implement

  • Payload builders return exact JSON, e.g.:

    std::string OrcaPrinterAgent::build_pushall(const std::string& sid) {
        return R"({"pushing":{"command":"pushall","sequence_id":")" + sid + R"(","version":1,"push_target":1}})";
    }
    std::string OrcaPrinterAgent::build_pushing_start(const std::string& sid) {
        return R"({"pushing":{"command":"start","sequence_id":")" + sid + R"("}})";
    }
    std::string OrcaPrinterAgent::build_get_version(const std::string& sid) {
        return R"({"info":{"command":"get_version","sequence_id":")" + sid + R"("}})";
    }
    std::string OrcaPrinterAgent::build_get_capabilities(const std::string& sid) {
        return R"({"info":{"command":"get_capabilities","sequence_id":")" + sid + R"("}})";
    }
    
  • seq(n): return std::to_string(20000 + (n % 10000));

  • emit_connect_sequence(dev_id, sub, req): sub(dev_id); req(build_pushing_start(seq(1))); req(build_pushall(seq(2))); req(build_get_version(seq(3))); req(build_get_capabilities(seq(4)));

  • on_connected(dev_id, conn, generation): if generation != m_lan_generation.load() and the cloud generation guard (Task 14) both mismatch, return. Else emit_connect_sequence(dev_id, [conn](auto& id){ conn->subscribe(id); }, [conn, &dev_id](auto& body){ conn->send_request(dev_id, body); });

  • Add the tiny public shim run_connect_sequence_for_test.

  • Step 4: Verify greenctest --test-dir build -R OrcaPrinterAgent --output-on-failure


Task 13: disconnect_printer + generation guard

Files:

  • Modify: src/slic3r/Utils/OrcaPrinterAgent.cpp, tests/slic3rutils/test_orca_printer_agent.cpp

Interfaces:

  • Consumes: m_lan_generation, lan_mqtt_connection, OrcaMqttConnection::stop.

  • Step 1: Write the failing test

TEST_CASE("a stale-generation inbound message is dropped", "[OrcaPrinterAgent]") {
    struct Probe : OrcaPrinterAgent { using OrcaPrinterAgent::OrcaPrinterAgent;
        using OrcaPrinterAgent::make_lan_message_handler; };   // expose for the test
    Probe agent("/tmp");
    int hits = 0;
    agent.set_on_message_fn([&](std::string, std::string){ ++hits; });
    auto handler_gen1 = agent.make_lan_message_handler(/*generation=*/1);
    agent.bump_lan_generation_for_test();                       // now current == 2
    handler_gen1("dev-1", "{}");                                // late callback from gen 1
    CHECK(hits == 0);
}
  • Step 2: Run it (expect FAIL) — helpers missing.

  • Step 3: Implement

  • Factor the message-handler lambda into std::function<void(const std::string&, const std::string&)> make_lan_message_handler(uint64_t generation) returning [this, generation](auto& id, auto& p){ if (generation == m_lan_generation.load()) deliver_to_sink(id, p); }.

  • disconnect_printer:

    int OrcaPrinterAgent::disconnect_printer() {
        ++m_lan_generation;                       // fence stale callbacks
        std::unique_ptr<OrcaMqttConnection> doomed;
        { std::lock_guard<std::mutex> l(state_mutex);
          doomed = std::move(lan_mqtt_connection);
          m_lan_dev_id.clear();
          if (m_current_connection == LAN) m_current_connection = NONE; }
        if (doomed) doomed->stop();               // joins the worker, outside the lock
        return BAMBU_NETWORK_SUCCESS;
    }
    
  • Add bump_lan_generation_for_test().

  • Step 4: Verify greenctest --test-dir build -R OrcaPrinterAgent --output-on-failure


Task 14: set_user_selected_machine drives the cloud per-printer connection

Files:

  • Modify: src/slic3r/Utils/OrcaPrinterAgent.cpp, src/slic3r/Utils/OrcaCloudServiceAgent.hpp, .cpp, tests/slic3rutils/test_orca_printer_agent.cpp

Interfaces:

  • Consumes: OrcaCloudServiceAgent::get_mqtt_connection().

  • Produces on OrcaCloudServiceAgent:

    int  configure_selected_printer_mqtt(const std::string& dev_id);  // (re)build Config{wss://<api>/api/v1/printers/{id}/mqtt, bearer}, start()
    void teardown_selected_printer_mqtt();                            // stop()
    std::string selected_printer_mqtt_url() const;                    // test hook — "" when not configured
    
  • Step 1: Write the failing test

TEST_CASE("selecting a cloud printer configures the per-printer socket", "[OrcaPrinterAgent]") {
    auto cloud = std::make_shared<Slic3r::OrcaCloudServiceAgent>("/tmp");
    cloud->set_api_base_url("api.example.com");
    OrcaPrinterAgent agent("/tmp");
    agent.set_cloud_agent(cloud);

    agent.set_user_selected_machine("printer-uuid-1");
    CHECK(cloud->selected_printer_mqtt_url() == "wss://api.example.com/api/v1/printers/printer-uuid-1/mqtt");

    agent.set_user_selected_machine("");
    CHECK(cloud->selected_printer_mqtt_url().empty());
}
  • Step 2: Run it (expect FAIL) — methods missing; set_user_selected_machine still does the aggregate add_subscribe dance.

  • Step 3: Implement

  • OrcaCloudServiceAgent::configure_selected_printer_mqtt(dev_id):

    OrcaMqttConnection::Config cfg;
    cfg.url = "wss://" + api_base_url + "/api/v1/printers/" + dev_id + "/mqtt";
    cfg.use_tls = true;
    cfg.bearer_provider = [this]{ return get_access_token(); };
    cfg.client_id = "OrcaSlicer";
    cfg.keepalive_seconds = 300;
    m_selected_printer_mqtt_url = cfg.url;
    return mqtt_connection->start(cfg,
        [this](const std::string& id, const std::string& p){ /* Task 15 routes to printer_status_callback */ deliver_cloud_message(id, p); },
        [this](bool, bool){ }) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
    

    teardown_selected_printer_mqtt(): mqtt_connection->stop(); m_selected_printer_mqtt_url.clear();

  • OrcaPrinterAgent::set_user_selected_machine(dev_id):

    int OrcaPrinterAgent::set_user_selected_machine(std::string dev_id) {
        auto* cloud = get_orca_cloud_agent();
        std::string previous;
        { std::lock_guard<std::mutex> l(state_mutex);
          if (dev_id == selected_machine) return BAMBU_NETWORK_SUCCESS;
          previous = selected_machine; selected_machine = dev_id; }
        if (!cloud) return BAMBU_NETWORK_SUCCESS;
        auto* conn = cloud->get_mqtt_connection();
        if (!previous.empty() && conn && conn->is_connected())
            conn->send_request(previous, build_pushing_stop(seq(5)));
        cloud->teardown_selected_printer_mqtt();
        if (!dev_id.empty()) {
            const uint64_t gen = ++m_lan_generation;    // reuse the same fence for cloud
            std::thread([this, cloud, dev_id, gen]{
                if (cloud->configure_selected_printer_mqtt(dev_id) == BAMBU_NETWORK_SUCCESS
                    && gen == m_lan_generation.load())
                    on_connected(dev_id, cloud->get_mqtt_connection(), gen);
            }).detach();
            { std::lock_guard<std::mutex> l(state_mutex); m_current_connection = CLOUD; }
        } else {
            std::lock_guard<std::mutex> l(state_mutex); m_current_connection = NONE;
        }
        return BAMBU_NETWORK_SUCCESS;
    }
    

    Add build_pushing_stop next to build_pushing_start ("command":"stop").

  • Delete the old aggregate add_subscribe / del_subscribe / send_message(pushall) / deliver_mock_get_version body from set_user_selected_machine (the mock get_version is now redundant — on_connected sends a real info.get_version).

  • Step 4: Verify greenctest --test-dir build -R OrcaPrinterAgent --output-on-failure


Task 15: OrcaCloudServiceAgent::connect_server() stops starting the aggregate socket

Files:

  • Modify: src/slic3r/Utils/OrcaCloudServiceAgent.cpp, .hpp, tests/slic3rutils/test_orca_printer_agent.cpp (or a new test_orca_cloud_service_agent.cpp)

Interfaces:

  • Consumes: existing REST http_get(ORCA_HEALTH_PATH, ...).

  • Produces: connect_server() returns success/failure from the health probe alone; is_server_connected() returns the last health result; mqtt_connection is untouched by connect_server().

  • Step 1: Write the failing test

TEST_CASE("connect_server does not start an MQTT socket", "[OrcaCloud]") {
    auto cloud = std::make_shared<Slic3r::OrcaCloudServiceAgent>("/tmp");
    cloud->set_api_base_url("127.0.0.1:1");     // health probe will fail fast
    cloud->connect_server();
    REQUIRE(cloud->get_mqtt_connection() != nullptr);
    CHECK_FALSE(cloud->get_mqtt_connection()->is_running());
    CHECK(cloud->selected_printer_mqtt_url().empty());
}
  • Step 2: Run it (expect FAIL)connect_server() currently builds cfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt" and calls mqtt_connection->start(...).

  • Step 3: Implement

  • Delete the whole if (connected && mqtt_connection && !mqtt_connection->is_running()) { ... mqtt_connection->start(cfg, ...); } block from connect_server() and the trailing "aggregate MQTT" logging.

  • Keep the health check; set is_connected = connected; from it and call invoke_server_connected_callback(connected ? 0 : -1, http_code);.

  • refresh_connection() still just calls connect_server().

  • The printer_status_callback / set_printer_status_callback machinery stays (Task 14's configure_selected_printer_mqtt message lambda calls deliver_cloud_message → the registered printer_status_callback). Add:

    void OrcaCloudServiceAgent::deliver_cloud_message(const std::string& id, const std::string& p) {
        OnMessageFn cb;
        { std::lock_guard<std::mutex> l(callback_mutex); cb = printer_status_callback; }
        if (cb) cb(id, p);
    }
    
  • Step 4: Verify greenctest --test-dir build -R 'OrcaCloud|OrcaPrinterAgent' --output-on-failure


Task 16: collapse send_message / send_message_to_printer

Files:

  • Modify: src/slic3r/Utils/OrcaPrinterAgent.cpp, tests/slic3rutils/test_orca_printer_agent.cpp

Interfaces:

  • Consumes: get_appropriate_mqtt_connection(bool), OrcaMqttConnection::send_request.

  • Step 1: Write the failing test

TEST_CASE("send_message* reject when no connection", "[OrcaPrinterAgent]") {
    OrcaPrinterAgent agent("/tmp");                 // no cloud agent, no LAN connection
    CHECK(agent.send_message("d", "{}", 0, 0)            == BAMBU_NETWORK_ERR_INVALID_HANDLE);
    CHECK(agent.send_message_to_printer("d", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE);
}

TEST_CASE("send_message_to_printer publishes on the LAN connection", "[OrcaPrinterAgent][.integration]") {
    orca_mqtt_test::MockBroker broker;
    OrcaPrinterAgent agent("/tmp");
    // point the LAN connection at the mock by connecting to its host:port
    auto ep = broker.host_port();                   // {"127.0.0.1", <port>}
    agent.connect_printer("dev-1", ep.first + ":" + ep.second, "orcasonar", "code", false);
    // wait for connect
    for (int i = 0; i < 100 && broker.connect_count() == 0; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(20));
    CHECK(agent.send_message_to_printer("dev-1", R"({"print":{"command":"pause","sequence_id":"20007"}})", 0, 0)
          == BAMBU_NETWORK_SUCCESS);
    for (int i = 0; i < 100 && broker.received_requests().empty(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(20));
    REQUIRE(broker.received_requests().size() >= 1);
    CHECK(broker.received_requests().back().find("pause") != std::string::npos);
    agent.disconnect_printer();
}

MockBroker::host_port() — add a small accessor returning {"127.0.0.1", std::to_string(port_)} in Task 8's header (fold this one-line addition here).

  • Step 2: Run it (expect FAIL)send_message still spawns the old REST thread / send_message_to_printer is a bare return SUCCESS.

  • Step 3: Implement

int OrcaPrinterAgent::send_message(std::string dev_id, std::string json_str, int, int) {
    return route_send(false, dev_id, json_str);
}
int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int, int) {
    return route_send(true, dev_id, json_str);
}
int OrcaPrinterAgent::route_send(bool is_lan, const std::string& dev_id, const std::string& json_str) {
    if (dev_id.empty()) return BAMBU_NETWORK_ERR_INVALID_HANDLE;
    OrcaMqttConnection* conn = get_appropriate_mqtt_connection(is_lan);
    if (!conn) return BAMBU_NETWORK_ERR_INVALID_HANDLE;
    return conn->send_request(dev_id, json_str) ? BAMBU_NETWORK_SUCCESS
                                               : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}

Guard get_appropriate_mqtt_connection(false) against a null cloud agent: return get_orca_cloud_agent() ? get_orca_cloud_agent()->get_mqtt_connection() : nullptr;

  • Step 4: Verify green./build/tests/slic3rutils/slic3rutils_tests "[OrcaPrinterAgent]"

Task 17: destructor teardown — no detached threads touching *this

Files:

  • Modify: src/slic3r/Utils/OrcaPrinterAgent.cpp, tests/slic3rutils/test_orca_printer_agent.cpp

  • Step 1: Write the failing test

TEST_CASE("destroying an agent mid-connect does not hang or crash", "[OrcaPrinterAgent]") {
    for (int i = 0; i < 20; ++i) {
        auto agent = std::make_unique<OrcaPrinterAgent>("/tmp");
        agent->connect_printer("dev-1", "10.255.255.1", "orcasonar", "code", false);  // unroutable, still connecting
        agent.reset();     // dtor must join/stop cleanly
    }
    SUCCEED();
}
  • Step 2: Run it (expect FAIL / hang / TSAN error) — the connect_printer detached thread may outlive *this and call on_connected/deliver_to_sink.

  • Step 3: Implement

  • The detached connect thread already guards every callback with the generation check. Add: ~OrcaPrinterAgent() bumps m_lan_generation first, then:

    OrcaPrinterAgent::~OrcaPrinterAgent() {
        ++m_lan_generation;
        std::unique_ptr<OrcaMqttConnection> lan;
        { std::lock_guard<std::mutex> l(state_mutex); lan = std::move(lan_mqtt_connection); }
        if (lan) lan->stop();                                   // joins the OrcaMqttConnection worker
        if (auto* cloud = get_orca_cloud_agent()) cloud->teardown_selected_printer_mqtt();
    }
    
  • The remaining race is the connect std::thread(...).detach() still running conn->start() on a conn that lan->stop() then destroys. Fix: hold the connect thread as a member std::thread m_lan_connect_thread; (not detached) and join() it in the dtor before moving lan_mqtt_connection. Update Task 11 & Task 14 to assign m_lan_connect_thread = std::thread(...) (join any prior one first).

  • Step 4: Verify green — run under sanitizers if the build has them: ctest --test-dir build -R OrcaPrinterAgent --output-on-failure


Task 18: full build, full test run, manual smoke

Files:

  • Modify: none (verification only)

  • Step 1: Full build

cmake --build build --config RelWithDebInfo --target all -j

Expected: success on Linux. (CI covers Windows/macOS.)

  • Step 2: Full unit + integration suite
ctest --test-dir build --output-on-failure
./build/tests/slic3rutils/slic3rutils_tests "[.integration]"

Expected: no regressions in test_printer_agent, test_qidi_printer_agent, test_plugin_*; all [OrcaMqtt] / [OrcaPrinterAgent] green.

  • Step 3: Manual smoke — LAN

Run OrcaSlicer against a real OrcaSonar hub: it appears via discovery; select it; Device tab populates (version, temps); set nozzle temperature; confirm the result echo in the log (OrcaMqtt / on_message lines). Record a log excerpt or screenshot.

  • Step 4: Manual smoke — cloud

Against OrcaCloud staging with a paired printer (requires CH-1 deployed there): select the printer; same checks. If CH-1 is not yet deployed, record that this step is blocked on the §5.2 hand-off and verify only that the socket opens and status is received (commands will 4403 until CH-1 ships).

  • Step 5: Record results

Append a short "Verification" section to the spec doc: build result, ctest summary line, and the two smoke outcomes (or the cloud-blocked note).


Self-Review

Spec coverage

Spec section Task(s)
§3.1 canonical core (topics, envelope) T4 (topics), T3 (CONNECT), payload builders T12
§3.2 command send = PUBLISH both transports T6, T16
§3.2 sequence_id band 2000029999 T12 (seq), asserted in T12 test
§3.2 capability via info.get_capabilities T12 (build_get_capabilities, in the sequence)
§3.2 status gating (pushing.start/stop) T12 (start), T14 (stop on deselect)
§3.2 exact-topic subscribe, no wildcard T4/T5 (report_topic(dev_id), single-id subscribe)
§3.2 auth: bearer or CONNECT creds, precedence T3 (flags), T5/T7 (bearer header vs creds), T7
§3.2 QoS ≤1, tolerate SUBACK QoS 0 T4 (make_subscribe_packet qos arg), T8 mock grants 0, T9 round-trip
§3.2 keepalive from Config T3, T5
§4.1 OrcaMqttConnection own TU, Config, API T1, T2, T5
§4.1 reuse salvage pieces T2/T3/T5 (parse_endpoint, connect packet, last_connack_rc)
§4.2 outbound collapse T16
§4.2 remove read_loop, per-connection MessageHandler T10
§4.2 get_appropriate_mqtt_connection kept T16 (null-guarded)
§4.2 lifecycle table (connect/select/deselect) T11, T13, T14
§4.2 identical post-connect sequence T12
§4.2 generation guard T11 (capture), T13 (test), T17 (dtor)
§4.2 CONNACK rc 4/5 terminal T5
§4.2 dtor stops both, no detached threads on *this T17
§4.3 one socket, per-printer cloud endpoint T14 (wss://.../printers/{id}/mqtt)
§4.3 remove aggregate socket, health-only is_server_connected T15
§6.1 unit tests T2T6, T12
§6.2 parametrized LAN/cloud integration T9
§6.2 reconnect re-sends subs + pushing.start gap → see below
§6.2 auth-reject no retry storm T5 (rc 4/5 terminal); add assertion in T5
§6.5 gates: full build, no regressions, smoke T18

Gaps found & fixed inline:

  • §6.2 "reconnect re-sends subscriptions and re-issues pushing.start" had no task. Added to Task 9 Step 3: extend the round-trip test with a broker.drop_client() call, assert the client reconnects (connect_count() == 2), that the report subscription is re-established (a second push_report is delivered), and that the agent re-runs on_connected (observable via a second pushall in received_requests()). Requires MockBroker::drop_client() — fold into Task 8.
  • §6.2 "auth reject → terminal, no retry storm" — added assertion to Task 5 Step 1 test: point Config.username/password at a mock that CONNACKs 0x05; assert last_connack_rc() == 5 and is_running() == false within 1 s (needs MockBroker ctor flag refuse_auth — fold into Task 8).

Placeholder scan: the Task 3 // TODO(Task 6) marker is intentional and explicitly removed in Task 5 Step 3 (corrected: the marker is added in T3, removed in T5 — not T6). No other TODO/TBD. All code steps carry real code.

Type consistency:

  • OrcaMqttConnection::start(const Config&, MessageHandler, StateHandler) — defined T5, used T7/T9/T11/T14. ✓
  • subscribe(const std::string&) / send_request(const std::string&, const std::string&) — defined T5/T6, used T9/T12/T16. ✓
  • on_connected(const std::string&, OrcaMqttConnection*, uint64_t) — decl T11 interfaces, defined T12, called T11/T14. ✓
  • emit_connect_sequence / run_connect_sequence_for_test — introduced T12, only used by T12's test. ✓
  • deliver_to_sink — T10, used T11/T13. ✓
  • route_send — T16 only. ✓
  • configure_selected_printer_mqtt / teardown_selected_printer_mqtt / selected_printer_mqtt_url / deliver_cloud_message — T14/T15, used T14/T15/T17. ✓
  • MockBroker API grows across T8 (ws_url, push_report, received_requests, connect_count), T16 (host_port), self-review (drop_client, refuse_auth ctor flag). All folded into Task 8's file; later tasks only call them. ✓
  • make_connect_packet 4-arg — T3, caller updated T5. Old 0-arg removed T3. ✓
  • make_topic_packet removed in T5; no later reference. ✓