60 KiB
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_teststarget (cmake --build build --target slic3rutils_tests). The fullcmake --build buildruns only in the final task. - No back-compat. OPCP v1.2.0 replaces v1.1.0. No REST
/commandsfallback in the client, no/tunnel, no dual-schema handling. sequence_idband: everysequence_idthe client emits MUST be a decimal string in20000–29999.- Cross-platform: Windows, macOS, Linux. No POSIX-only calls; use Boost/std.
- Threading: every
websocket::streamwrite goes throughwrite_mutex. Handlers run on the connection's worker thread and must marshal to the UI thread viaqueue_on_main_fnbefore touching wx. - Topic id: the id in
device/<id>/...is the id the client already holds (LAN: the mDNS-advertiseddevice_idpassed asdev_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::OrcaMqttConnectionatsrc/slic3r/Utils/OrcaMqttConnection.hppwith 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 ~34–108) 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
OrcaCloudServiceAgentat 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; };Endpointandparse_endpointmove to thepublic: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 whenusernamenon-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 green —
ctest --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 attemptThe old
start(const std::string&, TokenProvider, ...)and vectorsubscribe/unsubscribeare removed (no back-compat).report_topicset 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, ...), nolast_connack_rc. -
Step 3: Implement
-
Replace the
endpoint_url/get_tokenmembers withConfig current_config;andstd::atomic<int> m_last_connack_rc{-1};. -
Replace
std::set<std::string> subscriptionssemantics: it now holds full report-topic strings. Addpending_subscriptions/pending_unsubscriptionsas 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 withmake_subscribe_packet(next_packet_id++, topic, 1)/make_unsubscribe_packet. Deletemake_topic_packetand its declaration. -
In
connect_and_read()usecurrent_config:parse_endpoint(current_config.url, ep); branch TLS oncurrent_config.use_tls(Task 7 completes the plaintext branch); build CONNECT viamake_connect_packet(current_config.client_id, current_config.username, current_config.password, current_config.keepalive_seconds)— delete the Task 3 TODO marker; add theAuthorization: Bearerupgrade header only whencurrent_config.bearer_provideris 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 aConfigbuilt from today'swss://.../api/v1/printers/mqttURL andbearer_provider = [this]{ return get_access_token(); }. -
Step 4: Verify green —
ctest --test-dir build -R OrcaMqtt --output-on-failure(the new test plus Tasks 2–4). Alsoctest -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)— returnsfalsewithout 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_requestcurrently 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 send→ws_write, use that.)
- Step 4: Verify green —
ctest --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 Connectionholdsstd::optional<PlainWebSocket> ws;andstd::optional<TlsWebSocket> wss;withPlainWebSocket = websocket::stream<beast::tcp_stream>andTlsWebSocket = websocket::stream<ssl::stream<beast::tcp_stream>>. Internal helpersws_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_handshakeetc. -
Step 3: Implement
-
Connectiongets both optionals + the sharedbeast::flat_buffer read_buffer;and anet::io_context io;/ssl::context tls{ssl::context::tlsv12_client};as today. -
ws_handshake: resolve host/port;beast::get_lowest_layer(stream).connect(results); ifuse_tls: set SNI (SSL_set_tlsext_host_name),stream.next_layer().handshake(ssl::stream_base::client). Then set the upgrade decorator that addsAuthorization: Bearer <token>whenconfig.bearer_provideris set, andSec-WebSocket-Protocol: mqtt. Thenstream.handshake(host, target). -
ws_write/ws_read/ws_close:if (conn.wss) conn.wss->...; else conn.ws->...;. -
connect_and_read(): replace directWebSocketuse with aConnectionand thews_*helpers; constructconn->ws.emplace(conn->io)orconn->wss.emplace(conn->io, conn->tls)based oncurrent_config.use_tls. -
Delete the old
using WebSocket = websocket::stream<ssl::stream<...>>;typedef and thesend(WebSocket&, ...)signature (fold intows_write).
API note: exact Beast calls (
beast::get_lowest_layer,tcp_stream::connect, decorator signature) vary by Boost version. Match the version already vendored indeps/; the existing TLS code in this file is the reference for thewssside.
- 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:
0x10CONNECT → reply0x20 0x02 0x00 0x00(CONNACK accepted);connect_count_++.0x82SUBSCRIBE → reply0x90SUBACK with the echoed packet id and one granted-QoS byte0x00; remember the subscribed topic.0x30PUBLISH (QoS 0) → parse topic + payload; if topic ends/request, append payload toreceived_requests_.0xC0PINGREQ → reply0xD0 0x00.0xE0DISCONNECT / read error → close.push_report()posts amake-style PUBLISH (0x30, topicdevice/<id>/report, payload) onto the connected client socket vianet::post(strand, ...).
Use only Boost already vendored. Guard all shared state (received_requests_, connect_count_) with a std::mutex.
- Step 4: Verify green —
ctest --test-dir build -R 'MockBroker' --output-on-failurewith--allow-running-no-testsoff; run the hidden tag explicitly:ctest --test-dir build -R OrcaMqtt -C RelWithDebInfothen 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 5–7). -
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
OrcaMqttConnectionuntil 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 fromsalvage/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) — snapshotson_message_fnunderstate_mutex, calls it (marshalling viaqueue_on_main_fnwhen set). Used as the body of every connection'sMessageHandler. -
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_sinkmissing;read_loopstill present. -
Step 3: Implement
-
OrcaPrinterAgent.hpp: deleteread_loop,m_read_loop_thread,m_should_end. Addvoid deliver_to_sink(const std::string& dev_id, const std::string& payload);. -
OrcaPrinterAgent.cpp: delete theread_loopdefinition and its ctorstd::thread(...)/detach(). Delete the file-scopestd::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 existingset_printer_status_callbacklambda body becomesdeliver_to_sink(std::move(dev_id), std::move(payload));. -
Step 4: Verify green —
ctest --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_printeris a stub returning success without doing anything (make the wiring assertion fail by checking an observable — see Step 3 for the observable: a protectedlan_connection_url()test hook). -
Step 3: Implement
-
Add
parse_lan_endpoint(scheme strip,host[:port], default8280) andmake_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 */ : ""; }— storecfg.urlin astd::string m_lan_url;member when building it, and return that. Assert it in the test. -
Step 4: Verify green —
ctest --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
OrcaMqttConnectionsubclass is not available (non-virtual). Instead test the payload builders + call order via a seam:on_connectedtakes anOrcaMqttConnection*; make the foursend_requestpayloads and thesubscribeobservable by havingon_connecteddelegate to aprotected 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): ifgeneration != m_lan_generation.load()and the cloud generation guard (Task 14) both mismatch, return. Elseemit_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 green —
ctest --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 green —
ctest --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_machinestill does the aggregateadd_subscribedance. -
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_stopnext tobuild_pushing_start("command":"stop"). -
Delete the old aggregate
add_subscribe/del_subscribe/send_message(pushall)/deliver_mock_get_versionbody fromset_user_selected_machine(the mock get_version is now redundant —on_connectedsends a realinfo.get_version). -
Step 4: Verify green —
ctest --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 newtest_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_connectionis untouched byconnect_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 buildscfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt"and callsmqtt_connection->start(...). -
Step 3: Implement
-
Delete the whole
if (connected && mqtt_connection && !mqtt_connection->is_running()) { ... mqtt_connection->start(cfg, ...); }block fromconnect_server()and the trailing "aggregate MQTT" logging. -
Keep the health check; set
is_connected = connected;from it and callinvoke_server_connected_callback(connected ? 0 : -1, http_code);. -
refresh_connection()still just callsconnect_server(). -
The
printer_status_callback/set_printer_status_callbackmachinery stays (Task 14'sconfigure_selected_printer_mqttmessage lambda callsdeliver_cloud_message→ the registeredprinter_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 green —
ctest --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_messagestill spawns the old REST thread /send_message_to_printeris a barereturn 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_printerdetached thread may outlive*thisand callon_connected/deliver_to_sink. -
Step 3: Implement
-
The detached connect thread already guards every callback with the generation check. Add:
~OrcaPrinterAgent()bumpsm_lan_generationfirst, 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 runningconn->start()on aconnthatlan->stop()then destroys. Fix: hold the connect thread as a memberstd::thread m_lan_connect_thread;(not detached) andjoin()it in the dtor before movinglan_mqtt_connection. Update Task 11 & Task 14 to assignm_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 20000–29999 |
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 | T2–T6, 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 abroker.drop_client()call, assert the client reconnects (connect_count() == 2), that the report subscription is re-established (a secondpush_reportis delivered), and that the agent re-runson_connected(observable via a secondpushallinreceived_requests()). RequiresMockBroker::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/passwordat a mock that CONNACKs0x05; assertlast_connack_rc() == 5andis_running() == falsewithin 1 s (needsMockBrokerctor flagrefuse_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. ✓MockBrokerAPI grows across T8 (ws_url,push_report,received_requests,connect_count), T16 (host_port), self-review (drop_client,refuse_authctor flag). All folded into Task 8's file; later tasks only call them. ✓make_connect_packet4-arg — T3, caller updated T5. Old 0-arg removed T3. ✓make_topic_packetremoved in T5; no later reference. ✓