Compare commits

...
Author SHA1 Message Date
Ian Chua 87eb8de001 fix: serialize MQTT ws I/O and dewcouple keepalive 2026-09-24 20:28:13 +08:00
Ian Chua 220508534a fix: cancel inflight cloud signaling requests 2026-09-24 19:03:58 +08:00
Ian Chua 5a0810365b fix: clarify OrcaCloud signaling endpoint contract 2026-09-24 18:18:48 +08:00
Ian Chua fa98ec5afe feat: add ssl and ca_cert for OrcaPrinterAgent 2026-09-24 17:53:46 +08:00
Ian Chua 3c1d8ac8b4 fix: allow input ip address dialog to validate if is valid endpoint 2026-09-24 17:53:12 +08:00
Ian Chua 936b0bbc17 feat: add https compatibility for moonraker based printer agents 2026-09-24 14:53:32 +08:00
Ian Chua 5222a37eff fix: command_ams_refresh_rfid function signature in test_printer_agent.cpp 2026-09-24 12:35:03 +08:00
peachismomo 6da938b660 fix: compilation after merge 2026-09-24 01:15:55 +08:00
peachismomo 2dcdf49185 Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl 2026-09-24 01:02:59 +08:00
peachismomo 52e845a821 fix: add include for non BBL_RELEASE_TO_PUBLIC path in BBLPrinterAgent 2026-09-24 00:23:47 +08:00
Ian Chua ffb4813510 fix: preserve moonraker device names 2026-09-23 20:17:06 +08:00
Ian Chua 7436f68e09 fix: make moonraker gcode commands asynchronous 2026-09-23 20:15:52 +08:00
Ian Chua ad956591ce fix: use correct device id for Moonraker connections 2026-09-23 20:14:21 +08:00
Ian Chua 8da8e9bee1 fix: persist input printer host and port 2026-09-23 20:12:18 +08:00
Ian Chua 0acc8608b1 refactor: make printer connection SSL agent-specific 2026-09-23 20:11:47 +08:00
Ian Chua a2a9b2ce94 refactor: collapse command_ams_refresh_rfid and command_ams_refresh_rfid2 2026-09-23 18:30:05 +08:00
Ian Chua 07bee6c7a4 Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl 2026-09-23 15:35:22 +08:00
Ian Chua 1615390182 Merge branch 'main' into feat/printer-agent-infra 2026-09-23 14:54:38 +08:00
Ian Chua cf20994b58 refactor: connect_printer api and dialog (#15827)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Changing the IP address input dialog and `connect_printer` API in
preparation for configurable Moonraker/Orca printer agent connections.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
<img width="460" height="699" alt="image"
src="https://github.com/user-attachments/assets/35e3ce17-7594-4401-b140-100dea082221"
/>


## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-23 14:54:26 +08:00
Ian Chua 309bb3c6e2 Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl 2026-09-23 14:53:49 +08:00
Ian Chua 79f20c3996 Merge branch 'feat/printer-agent-infra' into refactor/connect-printer-api 2026-09-23 14:48:33 +08:00
Ian Chua 33179c1902 fix: make integration tests headless 2026-09-23 14:29:57 +08:00
peachismomo ccaafb3003 tests: cover WebMediaController lifecycle with wxWebView stub 2026-09-23 03:50:35 +08:00
peachismomo 7d4d8276a2 tests: add device manager integration coverage 2026-09-23 03:25:08 +08:00
peachismomo c492436228 refactor: centralize printer compatibility checks 2026-09-23 03:06:15 +08:00
peachismomo 99a1cd1b74 chore: reduce diagnostic logging level to trace 2026-09-23 02:58:30 +08:00
peachismomo 4b41bf7f4f fix: clear webview document on stop 2026-09-23 02:57:56 +08:00
peachismomo d5962052ca fix: handle malformed printer progress values safely 2026-09-23 02:53:10 +08:00
peachismomo f1410c8cac feat: extend optional printer model warnings to calibration & ams workflows 2026-09-23 02:49:27 +08:00
peachismomo 7283a05040 fix: validate windows FFmpeg avformat library 2026-09-23 02:33:47 +08:00
peachismomo c32c87863b test: cover printer agent default command dispatch 2026-09-23 01:54:23 +08:00
peachismomo 06f7016d6a fix: preserve printer agent defaults in PrinterAgentPluginCapabilityTrampoline 2026-09-23 01:53:56 +08:00
peachismomo be9ae6ffbf fix(tests): make omitted printer agent operations answer like a missing agent 2026-09-23 00:35:33 +08:00
Ian Chua cda25c3715 refactor: connect_printer api and dialog 2026-09-22 21:54:10 +08:00
Ian Chua 2dcdfe2d32 fix: refresh token before configuring printer MQTT 2026-09-22 21:46:09 +08:00
Ian Chua 9e34ca536b fix: port BBL implementations from #15711 2026-09-22 21:11:03 +08:00
Ian Chua 201617a6c0 fix: guard DeviceManager command dispatch when no printer agent is bound 2026-09-22 21:09:58 +08:00
Ian Chua 2dcbee3d76 Merge branch 'main' into feat/printer-agent-infra 2026-09-22 20:55:29 +08:00
Ian Chua 6c353d9596 feat: add ssl compatibility with moonrakerprinteragent 2026-09-22 18:30:30 +08:00
Ian Chua ce0c759ded fix: snapmakerprinteragent use moonraker's lifecycle-owned command worker 2026-09-22 16:06:18 +08:00
Ian Chua 9a7e925790 fix: re-implement missing commands on BBLPrinterAgent 2026-09-22 15:49:22 +08:00
Ian Chua f27b0a1f9d Merge branch main into feat/printer-agent-infra 2026-09-22 12:11:40 +08:00
Ian Chua cf1c164546 feat: add printer-agent.md doc to HLSD 2026-09-21 18:40:24 +08:00
Ian Chua d9f1f8b207 fix: camera signaling API 2026-09-21 16:19:31 +08:00
Ian Chua a096e1ab8c Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl 2026-09-21 16:02:11 +08:00
Ian Chua bca8dd00dd Merge branch 'feat/printer-agent-infra' of https://github.com/OrcaSlicer/OrcaSlicer into feat/printer-agent-infra 2026-09-21 15:45:49 +08:00
Ian Chua 8b7f8cac82 fix: follow external-packages for flatpak libdatachannel deps & add flatpak path to use source_dir 2026-09-21 15:45:39 +08:00
Ian Chua aae03257ed Merge branch 'main' into feat/printer-agent-infra 2026-09-21 11:54:23 +08:00
Ian Chua 3fba237c9a fix: shift camera signaling channel to network agent 2026-09-21 11:48:16 +08:00
Ian Chua cfb6349695 fix: update printer agent plugin API 2026-09-21 11:46:36 +08:00
Ian Chua f53ad6993a fix: update windows ffmpeg prebuild 2026-09-21 11:02:20 +08:00
peachismomo 91c6d1d3d5 Merge branch 'feat/printer-agent-impl' of https://github.com/OrcaSlicer/OrcaSlicer into feat/printer-agent-impl 2026-09-18 22:31:37 +08:00
peachismomo b91f48141d fix: load windows roots for cloud MQTT and camera signaling 2026-09-18 22:31:25 +08:00
Ian Chua e168d900f2 fix: stop flatpak DataChannel build from re-cloning over the sandboxed network 2026-09-18 22:10:30 +08:00
Ian Chua 467e249b3d Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl 2026-09-18 20:46:18 +08:00
Ian Chua bc03e3be2f fix: bump libdatachannel ver & update flatpak to use tar instead 2026-09-18 20:45:29 +08:00
Ian Chua 5287fd0160 fix: swtich start print command from http to mqtt command 2026-09-18 15:59:57 +08:00
Ian Chua c441fd2fa4 Merge branch feat/printer-agent-infra into feat/printer-agent-impl 2026-09-18 15:59:25 +08:00
Ian Chua f224b8e4a6 refactor: media controller playback routing and ownership 2026-09-18 13:36:46 +08:00
Ian Chua 8b479c9af3 fix: default impl for vendor agnostic gcode commansd 2026-09-17 18:46:56 +08:00
Ian Chua d8299c388e fix: regression bug, connecting to bambu needs bblp username 2026-09-17 18:44:54 +08:00
Ian Chua 771d455d12 Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl 2026-09-17 16:33:38 +08:00
Ian Chua a7ab01b815 fix: remove unimplemented pure virtual functions 2026-09-17 16:33:11 +08:00
Ian Chua 4d85b6627e fix: wrap command_* with small wrapper 2026-09-17 12:51:17 +08:00
Ian Chua 38e35dbff4 Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl 2026-09-16 22:59:22 +08:00
Ian Chua dcddb9283c fix: updated stale docs 2026-09-16 22:54:54 +08:00
Ian Chua ba9cf6e897 Revert "fix: latent ams in use bug"
This reverts commit 910dbcd2e6.
2026-09-16 22:54:54 +08:00
Ian Chua d1f2ecb903 feat: restore printer agent implementations 2026-09-16 22:54:46 +08:00
Ian Chua ee2b4ccf70 Merge branch 'main' into feat/printer-agent-infra 2026-09-16 22:32:32 +08:00
Ian Chua 753c793ea0 revert: filament sync work 2026-09-16 22:20:35 +08:00
Ian Chua 9c5494b19f fix: disable unused DataChannel media support 2026-09-16 22:08:53 +08:00
Ian Chua 93a119427a Merge branch main into feat/printer-agent-infra 2026-09-16 22:06:09 +08:00
Ian Chua d264fcaef4 fix: always build bundled DataChannel dep 2026-09-16 22:04:27 +08:00
Ian Chua b4ab6dedfd fix: move printer agent plugin tests into test_plugin_lifecycle.cpp 2026-09-16 21:33:04 +08:00
Ian Chua ed59f93456 fix: scope get_my_machine_list to printers listed under the current printer agent 2026-09-16 19:48:59 +08:00
Ian Chua cf52c81dcd fix: stop the correct media controller 2026-09-16 18:44:06 +08:00
Ian Chua 803a2a3239 fix: dedupe compatible printer type check 2026-09-16 17:18:59 +08:00
Ian Chua 985092bb46 fix: move non-mandatory printer agent function stubs to IPrinterAgent 2026-09-16 16:56:43 +08:00
Ian Chua cb16a2e547 feat: enable https camera stream mode 2026-09-16 16:24:29 +08:00
Ian Chua 2c2e91b629 fix: remove hardcoded ICE servers 2026-09-16 14:36:49 +08:00
Ian Chua c3faf903ca fix: use ORCA_CLOUD_PROVIDER instead of hardcoded string 2026-09-16 14:24:40 +08:00
Ian Chua 5f3c957597 fix: remove stale comment 2026-09-16 14:23:00 +08:00
Ian Chua 8557285389 fix: revert moonraker specific behavior 2026-09-16 14:21:09 +08:00
Ian Chua a9edab78a8 fix: inject provider, agent id and generation to get_user_print_info to ensure correct metadata 2026-09-16 14:15:24 +08:00
Ian Chua 79dede7d59 fix: change rtc log level 2026-09-16 14:14:21 +08:00
Ian Chua 2ac45b2f78 fix: invoke js clearInterval on WebMediaController::stop 2026-09-16 14:13:37 +08:00
Ian Chua b4f33e71e9 fix: add internal_developer_mode chekc back to MediaPlayCtrl::load() 2026-09-16 14:12:48 +08:00
Ian Chua 8c91cab971 fix(ci): add libdatachannel to flatpak manifest 2026-09-15 20:01:48 +08:00
Ian Chua 79077518e9 fix: re-include apply header guarded by ifdef __APPLE__ 2026-09-15 19:45:14 +08:00
Ian Chua 56aa348c89 fix(ci): set depends openssl 2026-09-15 18:16:50 +08:00
Lam Wei Lun ae1752fbf2 Resolve printer agent first before getting cloud printer agent 2026-09-15 18:02:02 +08:00
Ian Chua cf3f36a1c6 Merge branch 'feat/printer-agent-infra' of https://github.com/OrcaSlicer/OrcaSlicer into feat/printer-agent-infra 2026-09-15 17:37:57 +08:00
Ian Chua cca7d8adb1 fix(ci): deps build order for datachannel 2026-09-15 17:35:25 +08:00
Ian Chua 396d9ff51a Merge branch 'main' into feat/printer-agent-infra 2026-09-15 16:47:15 +08:00
Ian Chua 81540c81e0 Merge branch 'feat/printer-agent-infra' of https://github.com/OrcaSlicer/OrcaSlicer into feat/printer-agent-infra 2026-09-15 16:46:38 +08:00
Ian Chua e7ace6cc99 fix: unit tests & unused variables 2026-09-15 16:46:28 +08:00
Lam Wei Lun 79c20a1fad Guard libdatachannel. Remove unused code 2026-09-15 16:33:12 +08:00
Ian Chua e48f9f47d2 Revert "fix: parameterize orcaslicer_copy_test_dlls() for printer_agent_plugin_tests"
This reverts commit 2f566e3779.
2026-09-15 16:12:33 +08:00
Ian Chua 2f566e3779 fix: parameterize orcaslicer_copy_test_dlls() for printer_agent_plugin_tests 2026-09-15 16:08:10 +08:00
Ian Chua 37d1edf69a fix: printer agent virutal optional functions 2026-09-15 15:54:55 +08:00
Lam Wei Lun f1cf9c69b1 Log first before std::move 2026-09-15 15:49:15 +08:00
Lam Wei Lun 1df362888e Fixes nullptr deref 2026-09-15 15:47:48 +08:00
Ian Chua 4b2466b179 fix: uninitialized ams state blocking print 2026-09-15 14:40:04 +08:00
Ian Chua 3adcb3e953 fix: split infra from impl 2026-09-15 13:27:17 +08:00
Ian Chua 104dbb2140 fix: camera auto-play on startup 2026-09-14 16:17:17 +08:00
Ian Chua e482cbbdd7 fix: warnings 2026-09-14 14:56:34 +08:00
Ian Chua 2a5ddd9edb fix: warnings 2026-09-14 14:16:02 +08:00
Ian Chua c10f83cf61 Merge branch 'main' into feat/printer-agent-impl 2026-09-14 13:01:03 +08:00
Ian Chua 8eeec6935e Merge branch 'main' of https://github.com/OrcaSlicer/OrcaSlicer_priv into feat/printer-agent-impl 2026-09-14 12:53:04 +08:00
Ian Chua 131a2abf99 Merge pull request #138 from OrcaSlicer/feat/orca-printer-agent
feat: orca printer agent
2026-09-14 12:14:53 +08:00
peachismomo 9daecc59b4 temp: doc for intended change 2026-09-12 02:32:01 +08:00
peachismomo 58e000d842 feat: cloud download via HTTP 2026-09-12 02:31:27 +08:00
Ian Chua af6be5858e fix: cloud printers were using the wrong MQTT endpoint 2026-09-11 17:38:56 +08:00
Ian Chua 3fc7fd99d6 fix: shim layer for any compatibiliity changes 2026-09-10 21:55:31 +08:00
Ian Chua 3fe043651b fix: moonraker printer agent hang on printer power cut 2026-09-09 18:40:01 +08:00
Ian Chua b0b78c296c feat: check printer storage status before sending 2026-09-09 18:35:38 +08:00
Ian Chua 62791fabfa fix: revert sdcard check 2026-09-09 14:10:12 +08:00
Ian Chua b0ada2dee5 fix: ffmpeg http camera stream jittering due to incomplete frames 2026-09-09 13:40:51 +08:00
Ian Chua 8f4df1aa8c fix: model_id resolution method for non bambu printers 2026-09-09 13:40:24 +08:00
Ian Chua 5fec9b8d19 Merge pull request #139 from OrcaSlicer/feat/webrtc-impl
feat: webrtc implementation
2026-09-08 19:37:31 +08:00
Ian Chua 75ed5afbac Merge branch 'feat/orca-printer-agent' into feat/webrtc-impl 2026-09-08 19:31:14 +08:00
Ian Chua 01c553bad9 feat: LAN impl for Orca Printer Agent 2026-09-08 19:13:48 +08:00
Ian Chua a4376c77c3 Merge branch 'feat/orca-printer-agent' of https://github.com/OrcaSlicer/OrcaSlicer_priv into feat/orca-printer-agent 2026-09-08 12:06:25 +08:00
peachismomo c459994290 fix: connect via ip dialog 2026-09-08 04:40:12 +08:00
Ian Chua 3a0fda7d18 fix: make model_id/dev_type optional instead of blocking 2026-09-07 17:28:40 +08:00
Ian Chua eb8733f0f4 Merge branch 'feat/orca-printer-agent' into feat/webrtc-impl 2026-09-04 18:28:48 +08:00
Ian Chua 55acb940a8 Merge branch 'feat/printer-agent-impl' into feat/orca-printer-agent 2026-09-04 18:28:25 +08:00
Ian Chua 0d691403de Merge branch 'main' into feat/printer-agent-impl 2026-09-04 18:26:58 +08:00
Ian Chua 9b2b75a51d Merge branch 'feat/orca-printer-agent' into feat/webrtc-impl 2026-09-04 18:23:26 +08:00
Ian Chua 25da273ea0 Merge pull request #142 from OrcaSlicer/fix/orca-printer-agent-refactor
fix: orcaprinteragent refactor
2026-09-04 18:05:27 +08:00
Ian Chua dbcb82075f feat: use ffmpeg to render http camera stream 2026-09-04 16:42:36 +08:00
Ian Chua 2f82cfe40f fix: LAN paths and camera stream 2026-09-04 16:18:43 +08:00
Ian Chua 972031cf06 fix: orcaprinteragent refactor 2026-09-03 19:42:25 +08:00
Ian Chua 2a4792e762 feat: remove frame assembler and change config to set protocol 2026-09-02 15:59:13 +08:00
Ian Chua dd15ac6146 fix: cmake 2026-09-02 11:51:40 +08:00
Ian Chua b0469254bc Merge branch 'feat/orca-printer-agent' into feat/webrtc-impl 2026-09-01 19:24:04 +08:00
Ian Chua 2228589e16 Merge branch 'feat/printer-agent-impl' into feat/orca-printer-agent 2026-09-01 19:23:47 +08:00
Ian Chua 4d8480e1a1 fix: build 2026-09-01 19:21:23 +08:00
Ian Chua 4320cc78d9 feat: camera via webrtc 2026-09-01 18:59:20 +08:00
Ian Chua d9d9678a7e Merge branch 'feat/orca-printer-agent' into feat/webrtc-impl 2026-09-01 18:38:01 +08:00
Ian Chua d55a0bfec6 fix: build errors 2026-09-01 18:37:04 +08:00
Ian Chua 3b1017df51 Merge branch 'feat/orca-printer-agent' of https://github.com/OrcaSlicer/OrcaSlicer_priv into feat/orca-printer-agent 2026-09-01 18:17:23 +08:00
Ian Chua f5b81d2ffc Merge branch 'feat/webrtc-impl' of https://github.com/OrcaSlicer/OrcaSlicer_priv into feat/webrtc-impl 2026-09-01 18:16:20 +08:00
Ian Chua eb9cfe0ecb feat: connect to cloud printer and monitor 2026-09-01 18:15:53 +08:00
Ian Chua 1e8805d43d feat: connect to cloud printer and monitor 2026-09-01 18:15:53 +08:00
Ian Chua e8f089dfa4 fix: build & access code UI 2026-09-01 18:13:11 +08:00
Ian Chua 84e929ea24 feat: generic camera stream support for http snapshot and rtsp 2026-09-01 18:13:11 +08:00
Ian Chua abec603e73 fix: build & access code UI 2026-09-01 18:04:37 +08:00
Ian Chua 0ed19d4826 feat: generic camera stream support for http snapshot and rtsp 2026-09-01 18:02:53 +08:00
Ian Chua b747c13ef4 Merge branch 'feat/orca-printer-agent' of https://github.com/OrcaSlicer/OrcaSlicer_priv into feat/orca-printer-agent 2026-09-01 17:40:07 +08:00
Ian Chua 6a12aca495 feat: connect to cloud printer and monitor 2026-09-01 17:38:52 +08:00
Ian Chua d486db6459 Merge branch 'main' into feat/printer-agent-impl 2026-09-01 16:51:34 +08:00
Ian Chua fe777b8801 feat: printer agent impl 2026-09-01 16:49:04 +08:00
Ian Chua 1b3e206f34 feat: connect to cloud printer and monitor 2026-08-28 18:10:31 +08:00
Ian Chua 4df1607ec4 fix: clear up some unrelated changes 2026-08-28 16:45:01 +08:00
Ian Chua a0ec8bec8a fix: merge artifact 2026-08-27 15:08:06 +08:00
Ian Chua 36c362cbd2 Merge branch 'main' into feat/printer-agent-impl 2026-08-27 14:54:22 +08:00
Ian Chua 84fde32616 Merge branch 'main' into refactor/printer-agent-interface 2026-08-27 14:52:50 +08:00
Ian Chua 34e349e323 fix: snapmaker U1 SelectMachineDialog blocking print 2026-08-27 14:47:09 +08:00
Ian Chua 44941e571f fix: remove unused variable 2026-08-26 18:40:48 +08:00
Ian Chua 876d6e2499 fix: printer agent switching on preset change 2026-08-26 18:40:47 +08:00
Ian Chua 858b3024ff fix: tests 2026-08-26 12:04:02 +08:00
Ian Chua 6595557c34 fix: access codes regression 2026-08-25 18:32:02 +08:00
Ian Chua c729849843 cleanup moonraker and snapmaker printer agents 2026-08-25 16:39:08 +08:00
Ian Chua c0563be36e Merge branch 'feat/printer-agent-impl' of https://github.com/OrcaSlicer/OrcaSlicer into feat/printer-agent-impl 2026-08-25 14:47:12 +08:00
Ian Chua 462f8dce30 fix: remote do_fetch_filament_info from tests 2026-08-25 14:46:11 +08:00
Ian Chua b2a0485139 fix: defer filesystem and camera abstractions 2026-08-25 14:34:11 +08:00
Ian Chua f16071f083 fix: remove heavy includes from IPrinterAgent 2026-08-25 13:54:40 +08:00
Ian Chua a1505e9bed Merge branch 'main' into refactor/printer-agent-interface 2026-08-25 13:50:25 +08:00
Ian Chua 8e48312b1f Merge branch 'main' into feat/printer-agent-impl 2026-08-25 13:47:11 +08:00
Ian Chua ba7fdb8ceb Merge branch 'main' into feat/printer-agent-impl 2026-08-21 14:17:56 +08:00
Ian Chua 8be9800567 remove irrelevant docs 2026-08-21 14:16:49 +08:00
Ian Chua c4eabc3c52 fix: extend access code requirements t 0, 8 or more characters. 2026-08-21 14:14:23 +08:00
Ian Chua 0d0d281d0c feat: parse nozzle information for qidi and moonraker printer agents 2026-08-20 20:11:31 +08:00
Ian Chua c4bbcd322b fix: skip filament sync dialog if filamentSyncMode is none 2026-08-20 19:28:02 +08:00
Ian Chua 7aea1b1235 Merge branch 'main' into feat/printer-agent-impl 2026-08-20 16:39:54 +08:00
Ian Chua 41c16436bf Merge branch 'main' into refactor/printer-agent-interface 2026-08-20 16:09:10 +08:00
Ian Chua 2bbb229c91 Merge branch 'main' into refactor/printer-agent-interface 2026-08-19 23:30:22 +08:00
Ian Chua c2a43157d2 Merge branch 'main' into feat/printer-agent-impl 2026-08-19 18:29:36 +08:00
Ian Chua 738b30a743 fix: ams sync info and periodic ams sync via subscription workflow 2026-08-19 18:29:19 +08:00
Ian Chua 6e6ffe13f8 Merge branch 'refactor/printer-agent-interface' of https://github.com/OrcaSlicer/OrcaSlicer into refactor/printer-agent-interface 2026-08-19 17:11:14 +08:00
Ian Chua eb96d127b0 fix: resolve stubgen byte header conflict 2026-08-19 17:11:10 +08:00
SoftFever 60421c33f4 Merge branch 'main' into refactor/printer-agent-interface 2026-08-19 14:32:42 +08:00
SoftFever 300c4b8afb Merge branch 'main' into refactor/printer-agent-interface 2026-08-18 20:44:56 +08:00
Ian Chua 96092ef2d3 feat: update qidi to use subscription based filament sync mode 2026-08-18 19:05:17 +08:00
Ian Chua d9002ad87d fix: ams filament mapping workflow 2026-08-18 18:07:22 +08:00
Ian Chua 4c1ea0a602 Merge branch 'main' into 'feat/printer-agent-impl' 2026-08-17 14:51:04 +08:00
Ian Chua 9415812d85 Merge branch 'main' into refactor/printer-agent-interface 2026-08-17 14:24:23 +08:00
Ian Chua d3c728557e Merge 'main' into 'refactor/printer-agent-interface' 2026-08-17 14:24:05 +08:00
Ian Chua 6558c52849 revert file transfer abstraction 2026-08-17 14:19:41 +08:00
Ian Chua a34d056de2 specify api for getting file transfer url 2026-08-14 15:31:42 +08:00
Ian Chua 7ba5718be6 fix: remove redundant cache 2026-08-12 18:41:51 +08:00
Ian Chua c1163ce7e5 Merge branch 'refactor/printer-agent-interface' of https://github.com/OrcaSlicer/OrcaSlicer into refactor/printer-agent-interface 2026-08-12 15:29:56 +08:00
Ian Chua da187eaaf9 fix callback error 2026-08-12 15:29:50 +08:00
Ian Chua de0268ce86 Merge branch 'main' into refactor/printer-agent-interface 2026-08-12 15:28:53 +08:00
Ian Chua 108923bdaa fix: default impl 2026-08-12 14:02:04 +08:00
Ian Chua 44793f7a21 remove unused 2026-08-12 13:51:54 +08:00
Ian Chua 7cb1805272 refactor: push bbl workflows to bbl printer agent 2026-08-12 13:50:47 +08:00
Ian Chua d5c528b7c7 Merge branch 'refactor/printer-agent-interface' of https://github.com/OrcaSlicer/OrcaSlicer into refactor/printer-agent-interface 2026-08-11 15:00:42 +08:00
Ian Chua 742cb712d8 refactor: abstract bambu specific protocol to printer agent 2026-08-11 15:00:34 +08:00
Ian Chua fd1c5d826c Merge branch 'main' into refactor/printer-agent-interface 2026-08-07 18:37:56 +08:00
Ian Chua 58be7f4861 feat: abstract remaining gcode commands in devicemanager 2026-08-07 18:36:38 +08:00
Ian Chua 4031b00915 Merge branch 'main' into refactor/printer-agent-interface 2026-08-06 16:27:57 +08:00
Ian Chua 07c62112b8 Reconcile implementation split with PR tip 2026-08-04 18:12:51 +08:00
Ian Chua f23e4963bf fix: merge access codes into one 2026-08-04 18:12:43 +08:00
Andrew 95279f7084 docs: document the printer-agent subsystem 2026-08-04 18:12:43 +08:00
Andrew 1014558c91 Keep printer-agent progress in sync
Keep the shared task progress aligned with agent
reports that lack Bambu cloud task identity.

Release the lazily allocated task during reset to
avoid leaks when machine objects reconnect.
2026-08-04 18:12:42 +08:00
Andrew 6515062a3a fix: stop Qidi slot parse throwing on null 2026-08-04 18:12:42 +08:00
Andrew 8153e26b6d fix: start stream when camera URL changes 2026-08-04 18:12:42 +08:00
Andrew 79fd33d49a Show Snapmaker U1 camera in Device tab
The U1 exposes no /server/webcams/list entry;
its camera only captures after an explicit
camera.start_monitor RPC, which the Moonraker
websocket executes unauthenticated but only
answers over MQTT - so the call is fire and
forget.

Start the camera when the camera view is
shown and renew every 300 s: the printer
retires the capture task at ~362 s and
stop_monitor is accepted but ineffective,
so teardown is simply to stop renewing.

Frames land in monitor.jpg as still JPEGs
(~2 fps at interval 0), so the webview loads
a local HTML wrapper that repolls with a
cache buster.
2026-08-04 18:12:41 +08:00
Andrew cdb662d4b2 Move Moonraker commands off the UI thread
Pause/resume/stop, g-code sends, temps, and
light ran synchronous HTTP on the UI thread,
freezing the app up to 10s per click on slow
or unreachable printers.

Run them on a single agent-owned FIFO worker
so g-code ordering is preserved, while command
translation stays synchronous so unsupported-
command dialogs still work.

Add a pending-disabled state to the pause,
resume, and abort buttons for Moonraker-family
printers: the icon only flips once the
WebSocket reports the real state, which also
rules out double-click races.
2026-08-04 18:12:41 +08:00
Andrew a27fa7b5df Fix multi-color filament logic
Reuse color decoding across functions to improve
code readability and maintain consistency in
multi-color filament handling.
2026-08-04 18:12:40 +08:00
Andrew cdeed107c5 Keep Bambu AMS dialect out of the agent waist
M620 is Bambu firmware dialect, not a
neutral command. Composing it in
MachineObject let non-Bambu agents
(Moonraker/Klipper) forward it and
report success on firmware that
cannot run it.

Agents now own the dialect: the
default refusal on IPrinterAgent
returns not-supported so the UI
can say so; BBLPrinterAgent keeps
the byte-identical composition.
2026-08-04 18:12:40 +08:00
Andrew be92f7e78c Surface Moonraker webcams and gate unrunnable controls 2026-08-04 18:12:39 +08:00
Andrew 76f9de4cfb fix: make Klipper macro lamp control reliable 2026-08-04 18:12:39 +08:00
Andrew cd6d2cbf5e Stop blocking print on unreported nozzle data 2026-08-04 18:12:39 +08:00
Andrew deaca0189f Bring Moonraker device panel to feature parity
The monitor panel showed wrong or missing
data for Moonraker printers, and its
controls did nothing.

Push payload now carries layer number and
total layers. Remaining time replaces the
wrong total_duration - print_duration
formula. The chamber light toggle maps to
Klipper SET_PIN / SET_LED, and pause,
resume and stop post to
/printer/print/{action}. Task thumbnails
resolve via /server/files/thumbnails onto
a new MachineObject thumbnail url.

Filament sync switches to pull mode so the
agent is queried on demand.

Not compiled or run.
2026-08-04 18:12:38 +08:00
Andrew 7d54ee286d fix: pin HTTP to prevent connection refusal
Set `use_ssl` to false to ensure Moonraker
connectivity, as the service uses HTTP rather
than HTTPS, preventing connection issues. Initialize
device info early for reliable name resolution.
2026-08-04 18:12:38 +08:00
Andrew dddfc7a8ad Add printer-agent and plugin status tests
Ports the agent lifecycle, duplicate
agent-id, built-in-id clash and
status-resolution tests. The loader
runs on a detached worker thread, so
the lifecycle tests live in their own
executable. Tests install the
production unload-side registry
wiring themselves (no GUI in the
test binary) and register agents
manually so concurrent loads stay
deterministic.
2026-08-04 18:12:37 +08:00
Andrew 673bfb0dee Track BBLPrinterAgentPlugin.py 2026-08-04 18:12:37 +08:00
Andrew b9e8c5a38c Gate agent mode behind use_printer_agents toggle
Replace per-printer auto-activation
(is_current_printer_agent_plugin)
with a global experimental AppConfig
toggle, default off: legacy
print-host behavior is unchanged
until the user opts in. The toggle
drives device-tab routing, print
button defaults, connect-button
visibility and sidebar layout, and
dedups machine-select dialog opens.
2026-08-04 18:12:36 +08:00
Andrew a2a4ca20e4 Reset device selection on agent swap or unload (#124)
set_live_printer_agent centralizes
the swap: deselect the machine,
clear stale sidebar state and the
previous agent's Other Devices, then
install the new agent (or null when
its provider vanished). Plugin
load/unload callbacks refresh the
dropdown and re-run agent selection.
load_last_machine no longer falls
back to the first available machine.
2026-08-04 18:12:36 +08:00
Andrew 12075459d2 Replace fake-enum printer agent dropdown (#121)
A dedicated PrinterAgentChoice field
reads rows straight from the live
agent registry and stores the agent
id string, replacing the fake-coEnum
index mapping. The field moves to
TabPrinter and registers with the
searcher so UnsavedChanges renders
it; the PhysicalPrinterDialog copy
and its update hook are removed
(#125). switch_printer_agent now
resolves ids via
resolve_printer_agent_id.
2026-08-04 18:12:36 +08:00
Andrew f3770f3106 fix: checkbox should depend on plugin is_loaded status 2026-08-04 18:12:35 +08:00
Andrew 90fbf1770f Resolve duplicate agent ID conflicts
Reject a printer-agent capability
whose agent ID is already owned by
another capability or built-in:
flag the plugin error, disable the
capability, and warn the user
instead of silently ignoring it.
2026-08-04 18:12:35 +08:00
Andrew bde94ab37f Add support for runtime error status in plugins
Distinguish a loaded plugin whose
capability errored (RuntimeError,
warn-styled, stays checked) from a
load-time Error. Status now derives
via resolve_plugin_status(); enum
ordinal keeps dialog sort priority.
Unloading clears stale errors.
2026-08-04 18:12:34 +08:00
Ian Chua 6d25e1777e Working Moonraker and Qidi printer agent transport (#104)
Folds the Qidi AMS box-mapping print
overrides (apply_box_mapping +
start_* wrappers) that the transport
fix builds on.
2026-08-04 18:12:34 +08:00
Andrew 7ce26ca8e5 Harden send flow and separate upload failure recovery (#111)
* fix(send): harden FT send path + IP pre-flight UX

* Remove early returns
2026-08-04 18:12:33 +08:00
Andrew 367fcce634 Prevent loss of user access code on LAN reselect
Keep user access code intact to maintain access rights even if
device slot is unpopulated, ensuring continuous connection
and status message reception.
2026-08-04 18:12:33 +08:00
Andrew 9b3a44b339 Parse user print info on the UI thread to prevent heap corruption (#119)
get_user_print_info()'s HTTP fetch can run on a worker thread (e.g. BindJob),
but parse_user_print_info() mutates userMachineList (insert/erase/delete
MachineObject). on_machine_alive (SSDP) mutates the same maps on the UI thread
without locking, so parsing off-thread races the map and frees MachineObjects
out from under it -> heap corruption.

Keep all device-list mutation on the UI thread: parse inline when already on
the main thread, otherwise marshal via CallAfter so it stays serialized with
on_machine_alive.
2026-08-04 18:12:33 +08:00
Ian Chua 6384191102 Add developer flag for printer agents 2026-08-04 18:12:32 +08:00
106 changed files with 10821 additions and 1514 deletions
+42 -1
View File
@@ -826,6 +826,37 @@ find_package(OpenSSL REQUIRED)
find_package(CURL REQUIRED) find_package(CURL REQUIRED)
find_package(Freetype REQUIRED) find_package(Freetype REQUIRED)
if (SLIC3R_GUI)
# LibDataChannel's installed export references its bundled dependencies,
# but does not install their CMake targets. Recreate those targets from
# the same dependency prefix before loading the LibDataChannel config.
if (NOT TARGET Usrsctp::usrsctp)
find_library(_ORCA_USRSCTP_LIBRARY NAMES usrsctp
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
if (_ORCA_USRSCTP_LIBRARY)
add_library(Usrsctp::usrsctp UNKNOWN IMPORTED GLOBAL)
set_target_properties(Usrsctp::usrsctp PROPERTIES
IMPORTED_LOCATION "${_ORCA_USRSCTP_LIBRARY}"
IMPORTED_LINK_INTERFACE_LANGUAGES C
INTERFACE_LINK_LIBRARIES "Threads::Threads")
endif()
endif()
if (NOT TARGET LibJuice::LibJuice)
find_library(_ORCA_LIBJUICE_LIBRARY NAMES juice
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
if (_ORCA_LIBJUICE_LIBRARY)
add_library(LibJuice::LibJuice UNKNOWN IMPORTED GLOBAL)
set_target_properties(LibJuice::LibJuice PROPERTIES
IMPORTED_LOCATION "${_ORCA_LIBJUICE_LIBRARY}"
IMPORTED_LINK_INTERFACE_LANGUAGES C
INTERFACE_LINK_LIBRARIES "Threads::Threads")
endif()
endif()
find_package(LibDataChannel CONFIG REQUIRED)
endif()
add_library(libcurl INTERFACE) add_library(libcurl INTERFACE)
target_link_libraries(libcurl INTERFACE CURL::libcurl) target_link_libraries(libcurl INTERFACE CURL::libcurl)
@@ -1106,6 +1137,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
endif () endif ()
file(COPY ${_occt_dlls} file(COPY ${_occt_dlls}
${CMAKE_PREFIX_PATH}/bin/freetype.dll ${CMAKE_PREFIX_PATH}/bin/freetype.dll
${CMAKE_PREFIX_PATH}/bin/avformat-61.dll
${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll ${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll
${CMAKE_PREFIX_PATH}/bin/swresample-5.dll ${CMAKE_PREFIX_PATH}/bin/swresample-5.dll
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll ${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
@@ -1118,6 +1150,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${_out_dir}/WebView2Loader.dll ${_out_dir}/WebView2Loader.dll
${_out_dir}/freetype.dll ${_out_dir}/freetype.dll
${_out_dir}/avformat-61.dll
${_out_dir}/avcodec-61.dll ${_out_dir}/avcodec-61.dll
${_out_dir}/swresample-5.dll ${_out_dir}/swresample-5.dll
${_out_dir}/swscale-8.dll ${_out_dir}/swscale-8.dll
@@ -1141,7 +1174,10 @@ function(orcaslicer_copy_sos target config postfix output_sos)
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}") set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
endif () endif ()
file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so file(COPY ${CMAKE_PREFIX_PATH}/lib/libavformat.so
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61.1.100
${CMAKE_PREFIX_PATH}/lib/libavcodec.so
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61 ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100 ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100
${CMAKE_PREFIX_PATH}/lib/libavutil.so ${CMAKE_PREFIX_PATH}/lib/libavutil.so
@@ -1156,6 +1192,9 @@ function(orcaslicer_copy_sos target config postfix output_sos)
DESTINATION ${_out_dir}) DESTINATION ${_out_dir})
set(${output_sos} set(${output_sos}
${_out_dir}/libavformat.so
${_out_dir}/libavformat.so.61
${_out_dir}/libavformat.so.61.1.100
${_out_dir}/libavcodec.so ${_out_dir}/libavcodec.so
${_out_dir}/libavcodec.so.61 ${_out_dir}/libavcodec.so.61
${_out_dir}/libavcodec.so.61.3.100 ${_out_dir}/libavcodec.so.61.3.100
@@ -1285,6 +1324,8 @@ endif ()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux") if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LIBRARY_FILES set(LIBRARY_FILES
${LIBDIR_BIN}/libavformat.so.61
${LIBDIR_BIN}/libavformat.so.61.1.100
${LIBDIR_BIN}/libavcodec.so.61 ${LIBDIR_BIN}/libavcodec.so.61
${LIBDIR_BIN}/libavcodec.so.61.3.100 ${LIBDIR_BIN}/libavcodec.so.61.3.100
${LIBDIR_BIN}/libavutil.so.59 ${LIBDIR_BIN}/libavutil.so.59
+23 -11
View File
@@ -156,7 +156,7 @@ if (NOT _is_multi AND NOT CMAKE_BUILD_TYPE)
endif () endif ()
function(orcaslicer_add_cmake_project projectname) function(orcaslicer_add_cmake_project projectname)
cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN}) cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND;SOURCE_DIR" "CMAKE_ARGS" ${ARGN})
# MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the # MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the
# generator. A non-Visual-Studio superbuild passes its own generator down, and with # generator. A non-Visual-Studio superbuild passes its own generator down, and with
@@ -202,12 +202,18 @@ function(orcaslicer_add_cmake_project projectname)
set(_build_j "-j${NPROC}") set(_build_j "-j${NPROC}")
endif () endif ()
set(_source_dir_arg "")
if (P_ARGS_SOURCE_DIR)
set(_source_dir_arg SOURCE_DIR ${P_ARGS_SOURCE_DIR})
endif ()
if (NOT IS_CROSS_COMPILE OR NOT APPLE) if (NOT IS_CROSS_COMPILE OR NOT APPLE)
ExternalProject_Add( ExternalProject_Add(
dep_${projectname} dep_${projectname}
EXCLUDE_FROM_ALL ON EXCLUDE_FROM_ALL ON
INSTALL_DIR ${DESTDIR} INSTALL_DIR ${DESTDIR}
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname} DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
${_source_dir_arg}
${_gen} ${_gen}
CMAKE_ARGS CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_POLICY_VERSION_MINIMUM=3.5
@@ -240,12 +246,14 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
# note for future devs: shared libs may actually create a size reduction # note for future devs: shared libs may actually create a size reduction
# but orcaslicer_deps tends to get really funny regarding linking after that (notably boost) # but orcaslicer_deps tends to get really funny regarding linking after that (notably boost)
# so, as much as I would like to use that, it's not happening # so, as much as I would like to use that, it's not happening
ExternalProject_Add_Step(dep_${projectname} free_download_space if (NOT P_ARGS_SOURCE_DIR)
DEPENDEES download # do after download ExternalProject_Add_Step(dep_${projectname} free_download_space
COMMENT "Freeing Space: Removing source archive" DEPENDEES download # do after download
WORKING_DIRECTORY ${DEP_DOWNLOAD_DIR} COMMENT "Freeing Space: Removing source archive"
COMMAND ${CMAKE_COMMAND} -E rm -r ${projectname} WORKING_DIRECTORY ${DEP_DOWNLOAD_DIR}
) COMMAND ${CMAKE_COMMAND} -E rm -rf ${projectname}
)
endif ()
ExternalProject_Add_Step(dep_${projectname} free_build_space ExternalProject_Add_Step(dep_${projectname} free_build_space
DEPENDEES install # do after install DEPENDEES install # do after install
COMMENT "Freeing Space: Removing source and build files" COMMENT "Freeing Space: Removing source and build files"
@@ -259,6 +267,7 @@ else()
EXCLUDE_FROM_ALL ON EXCLUDE_FROM_ALL ON
INSTALL_DIR ${DESTDIR} INSTALL_DIR ${DESTDIR}
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname} DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
${_source_dir_arg}
${_gen} ${_gen}
CMAKE_ARGS CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DCMAKE_POLICY_VERSION_MINIMUM=3.5
@@ -386,10 +395,6 @@ include(libnoise/libnoise.cmake)
include(Draco/Draco.cmake) include(Draco/Draco.cmake)
include(FFMPEG/FFMPEG.cmake)
include(Assimp/Assimp.cmake)
# I *think* 1.1 is used for *just* md5 hashing? # I *think* 1.1 is used for *just* md5 hashing?
# 3.1 has everything in the right place, but the md5 funcs used are deprecated # 3.1 has everything in the right place, but the md5 funcs used are deprecated
# a grep across the repo shows it is used for other things # a grep across the repo shows it is used for other things
@@ -400,6 +405,12 @@ if(NOT OPENSSL_FOUND)
set(OPENSSL_PKG dep_OpenSSL) set(OPENSSL_PKG dep_OpenSSL)
endif() endif()
include(FFMPEG/FFMPEG.cmake)
include(Assimp/Assimp.cmake)
include(DataChannel/DataChannel.cmake)
set(DATACHANNEL_PKG dep_DataChannel)
# we don't want to load a "wrong" openssl when loading curl # we don't want to load a "wrong" openssl when loading curl
# so, just don't even bother # so, just don't even bother
# ...i think this is how it works? change if wrong # ...i think this is how it works? change if wrong
@@ -473,6 +484,7 @@ set(_dep_list
dep_wxInspector dep_wxInspector
dep_FFMPEG dep_FFMPEG
dep_Assimp dep_Assimp
${DATACHANNEL_PKG}
) )
if (MSVC) if (MSVC)
+37
View File
@@ -0,0 +1,37 @@
# libdatachannel is the native ICE/DTLS/SCTP implementation used by the
# GUI WebRTC camera controller. Keep the source revision fixed: the signaling
# protocol is evolving independently of this transport dependency.
#
# It vendors plog, usrsctp and libjuice as git submodules, which a plain
# GitHub tag tarball does not include. The flatpak sandbox has no network
# access during the build, so there the manifest itself clones the repo
# (submodules and all) into the dependency download directory before the
# sandbox closes. ExternalProject_Add is pointed at that existing checkout
# instead of being given its own network-dependent download method.
if (FLATPAK)
set(_datachannel_source
SOURCE_DIR ${DEP_DOWNLOAD_DIR}/DataChannel
)
else()
set(_datachannel_source
GIT_REPOSITORY https://github.com/paullouisageneau/libdatachannel.git
GIT_TAG v0.24.5
GIT_SHALLOW ON
GIT_SUBMODULES_RECURSE ON
)
endif()
orcaslicer_add_cmake_project(DataChannel
DEPENDS ${OPENSSL_PKG}
CMAKE_ARGS
-DNO_EXAMPLES=ON
-DNO_TESTS=ON
-DNO_WEBSOCKET=ON
-DNO_MEDIA=ON
-DUSE_NICE=OFF
-DUSE_SYSTEM_JUICE=OFF
-DUSE_SYSTEM_USRSCTP=OFF
-DOPENSSL_ROOT_DIR:PATH=${DESTDIR}
-DOPENSSL_USE_STATIC_LIBS=ON
${_datachannel_source}
)
+23 -7
View File
@@ -1,14 +1,26 @@
set(_conf_cmd ./configure) set(_conf_cmd ./configure)
set(_ffmpeg_depends)
set(_ffmpeg_configure_command ${_conf_cmd})
if (TARGET dep_OpenSSL)
set(_ffmpeg_depends DEPENDS dep_OpenSSL)
set(_ffmpeg_configure_command
${CMAKE_COMMAND} -E env
"PKG_CONFIG_PATH=${DESTDIR}/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}"
${_conf_cmd}
)
endif()
if (MSVC) if (MSVC)
set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG") set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG")
set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-winarm64-orca-shared-7.0.zip") set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-winarm64-orca-shared-7.0.zip")
set(PREBUILD_HASH_arm64 "12f4140279f2f8469885e1b5b2e8be9d788882914c21523cacd56989f3548054") set(PREBUILD_HASH_arm64 "da480cbb39680056de824c57ec4dc3bd577b479ebbc310ff1f9dc55cf014b4c1")
set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-win64-orca-shared-7.0.zip") set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-win64-orca-shared-7.0.zip")
set(PREBUILD_HASH_x64 "e65916020ddb9ef84b2666dfbcbfc9b1d67f69d15b4a66db53754637bf2d498c") set(PREBUILD_HASH_x64 "85da19daf198f5548259d8aabb349db84997a3f6e6886d8d7764114add9c6dae")
ExternalProject_Add(dep_FFMPEG ExternalProject_Add(dep_FFMPEG
${_ffmpeg_depends}
URL ${PREBUILD_URL_${DEPS_ARCH}} URL ${PREBUILD_URL_${DEPS_ARCH}}
URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}} URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}}
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
@@ -21,6 +33,8 @@ if (MSVC)
) )
else () else ()
set(_openssl_cmd --enable-openssl)
if (APPLE) if (APPLE)
set(_minos_cmd set(_minos_cmd
"--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}" "--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
@@ -52,10 +66,11 @@ else ()
endif() endif()
ExternalProject_Add(dep_FFMPEG ExternalProject_Add(dep_FFMPEG
${_ffmpeg_depends}
URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
CONFIGURE_COMMAND ${_conf_cmd} CONFIGURE_COMMAND ${_ffmpeg_configure_command}
${_cross_cmd} ${_cross_cmd}
${_pic_cmd} ${_pic_cmd}
${_arch_cmd} ${_arch_cmd}
@@ -63,20 +78,21 @@ else ()
"--prefix=${DESTDIR}" "--prefix=${DESTDIR}"
${_link_cmd} ${_link_cmd}
${_minos_cmd} ${_minos_cmd}
${_openssl_cmd}
--disable-doc --disable-doc
--enable-small --enable-small
--disable-outdevs --disable-outdevs
--disable-filters --disable-filters
--enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test* --enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test*
--disable-protocols --disable-protocols
--enable-protocol=file,fd,pipe,rtp,udp --enable-protocol=file,fd,pipe,http,https,rtp,tcp,udp
--disable-muxers --disable-muxers
--enable-muxer=rtp --enable-muxer=rtp
--disable-encoders --disable-encoders
--disable-decoders --disable-decoders
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv* --enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
--disable-demuxers --disable-demuxers
--enable-demuxer=h264,mp3,mov --enable-demuxer=h264,mp3,mov,mpjpeg,rtsp,sdp
--disable-zlib --disable-zlib
--disable-avdevice --disable-avdevice
BUILD_IN_SOURCE ON BUILD_IN_SOURCE ON
+240
View File
@@ -0,0 +1,240 @@
# Printer agents
Printer agents isolate printer-specific communication from the rest of OrcaSlicer. The GUI and
`DeviceManager` operate on a shared set of printer operations and device state; a selected printer
agent implements those operations for a particular printer ecosystem. The agent boundary allows
Bambu, Moonraker-based printers, built-in integrations, and Python-provided integrations to use the
same application workflow without making the GUI understand every printer protocol.
The current boundary is an adapter boundary around the existing application contract. In particular,
some request fields and message payloads still use the Bambu-shaped representation that existing
`MachineObject` and `DeviceManager` code consumes. The printer agent is responsible for translating
that representation into the protocol spoken by its printer. This is an intentional compatibility
constraint of the current design; the interface is not yet a neutral printer protocol.
The v1 dialect migration path is deliberately narrow. `DeviceManager` currently speaks the Bambu JSON
dialect because that is the payload shape already used throughout the command and state workflow. The
v1 `OrcaPrinterAgent` also accepts that Bambu dialect. Its transport path places the small translation
needed for the target printer at `deliver_to_sink`, keeping the compatibility code at the edge rather
than spreading it through `DeviceManager` or the agent interface.
The eventual direction is for `DeviceManager` to produce an Orca JSON dialect. The Bambu agent will then
own the translation from Orca JSON to Bambu's protocol, while `OrcaPrinterAgent` can forward the Orca
payload directly to its sink. The v1 translation at `deliver_to_sink` can then be removed without
changing `DeviceManager`, the command callers, or the rest of the agent workflow.
## Components
The system has four relevant layers:
```text
GUI / DeviceManager / MachineObject
|
NetworkAgent
/ \
IPrinterAgent ICloudServiceAgent
| |
printer protocol authentication and cloud services
```
### `DeviceManager` and `MachineObject`
`DeviceManager` owns the application-facing printer workflow. It maintains `MachineObject` instances,
updates their state, filters devices for the active printer agent, and initiates operations such as
homing, temperature changes, printing, subscriptions, and camera playback.
`MachineObject` remains the shared state model used by the GUI. It does not contain the implementation
of a printer protocol. When a device is discovered or returned by a cloud query, the device is tagged
with the active `printer_agent_id`. Device lists and selected-machine operations use that tag to avoid
sending an operation through an agent that does not own the device.
### `NetworkAgent`
`NetworkAgent` is the façade used by the GUI and `DeviceManager`. It owns:
- the currently selected `IPrinterAgent`;
- the registered cloud-service instances, indexed by provider;
- callbacks shared by the active printer agent and the application;
- the forwarding methods for printer commands and cloud operations.
There is one active printer agent for the currently selected printer preset. Switching the preset
increments the machine-list generation, disconnects the old printer agent, removes its callbacks, and
installs the newly selected agent. The façade then forwards printer operations to that agent.
Cloud operations are selected separately using a provider key. `NetworkAgent` forwards a cloud request
to the matching `ICloudServiceAgent`, and forwards cloud camera operations with a device ID. The
printer agent receives a cloud-agent pointer through `set_cloud_agent()` when it is created, allowing
printer communication to obtain cloud tokens without depending on a concrete cloud implementation.
### `IPrinterAgent`
`IPrinterAgent` is the printer-facing contract. It covers:
- cloud-relay and direct-LAN message delivery;
- LAN connection, discovery, binding, and certificates;
- printer subscriptions and callbacks;
- print operations;
- filament synchronization;
- camera capability and local camera URL reporting;
- printer command methods.
Concrete built-in implementations include the Bambu wrapper, the native Orca/Moonraker path, and
other printer-agent implementations registered by the application. A printer agent may use either
the cloud agent, a direct LAN connection, or both.
### `ICloudServiceAgent`
`ICloudServiceAgent` owns authentication and services provided by a cloud backend. It covers login
state, tokens, user and printer lists, settings synchronization, model services, cloud messages, and
cloud camera operations.
Cloud camera operations are device-scoped:
- `get_camera_url(dev_id, callback)` obtains a stream URL for one device;
- `create_camera_signaling_channel(dev_id)` creates signaling for one device where the provider
supports it.
This is separate from the local camera URL exposed by `IPrinterAgent`, which is currently scoped to
the active printer agent because a normal LAN agent represents one physical printer connection.
## Agent registration and selection
`NetworkAgentFactory` maintains the printer-agent registry. Each registry entry contains an agent ID,
a display name, and a factory function. Built-in agents register during application initialization.
Python printer-agent capabilities register dynamically and contribute an agent ID and factory entry.
The selected printer preset contains the printer-agent choice. If no explicit choice is stored, the
application preserves the existing default behavior: Bambu presets select the Bambu agent and other
presets select the native Orca agent. When a preset is changed, `GUI_App` resolves the effective agent
ID, obtains the corresponding cloud agent, creates the printer agent through the registry, and installs
it in `NetworkAgent`.
The registry rejects conflicting agent IDs. This matters for Python plugins because an agent ID is the
stable identity used by presets and device ownership; two enabled plugin capabilities must not claim
the same ID.
## Message and command flow
There are two low-level message paths:
- `send_message()` publishes a command through the printer's cloud relay;
- `send_message_to_printer()` sends a command directly to the printer over the LAN path.
Both paths accept a JSON string, quality-of-service and flag values, and return the existing network
status code domain. The agent owns the conversion from that JSON contract to its native transport.
The typed `command_*` methods are the application-facing convenience layer. The five generic defaults
currently implemented by `IPrinterAgent` construct the existing JSON dialect and route through the
same message path:
| Method | Default operation |
| --- | --- |
| `command_xyz_abs()` | Send `G90` for absolute positioning |
| `command_auto_leveling()` | Send `G29` for bed leveling |
| `command_go_home()` | Use the supported homing operation or send `G28` |
| `command_set_bed()` | Use the supported bed control or send `M140` |
| `command_set_nozzle()` | Send `M104` for nozzle temperature |
These are compatibility defaults for common printer workflows, not a guarantee that every firmware
implements every command identically. An agent can override a method when its protocol needs another
operation. For example, a Klipper configuration may use `BED_MESH_CALIBRATE` instead of `G29`.
The remaining common command methods default to `ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED` because their
existing behavior is vendor-specific or has no portable implementation:
- AMS RFID refresh;
- AMS calibration;
- AMS tray selection;
- camera start;
- axis control.
The methods remain on the common interface so an agent that supports them can override them explicitly.
`sequence_id` remains part of the command contract because `DeviceManager` creates and tracks it as
the command ID.
## Device ownership and stale responses
Printer-agent ownership is represented by `printer_agent_id` on device records and `MachineObject`
instances. The active agent ID is attached when a device is discovered, returned by a cloud list, or
reused after a preset switch. Local-machine configuration also persists the agent ID so a saved LAN
device is not silently reused by an unrelated agent.
Cloud printer-list responses carry three pieces of request context added by `NetworkAgent`:
```text
provider cloud provider used for the request
agent_id active printer agent when the request was made
generation machine-list generation when the request was made
```
`DeviceManager` accepts the response only when those values still match the current provider, active
agent, and generation. This prevents a slow response from the previous preset or provider from
repopulating the current device list.
The provider mapping is currently selected by `GUI_App`: the Bambu agent maps to the Bambu cloud
provider and other agents map to the Orca cloud provider. The generation check protects that existing
selection from races; it does not make cloud-provider ownership intrinsic to an agent. Cloud-printer
ownership and the broader Orca cloud services are therefore still separate architectural concerns.
## Python printer agents
`PrinterAgentPluginCapability` implements `IPrinterAgent` directly. The live capability object is
registered with `NetworkAgentFactory` and handed out as the printer agent when its agent ID is selected.
The plugin receives the selected `ICloudServiceAgent` through `set_cloud_agent()` just like a built-in
printer agent.
Python plugins must implement the core communication and lifecycle methods required by the interface,
including agent metadata, printer connection, discovery callbacks, and the two message-send methods.
Methods that are meaningful only to a particular printer are optional overrides where the C++ base
class provides a default.
All ten `command_*` methods are available in the Python binding and in the trampoline. Their override
status is intentionally optional:
- the five generic commands use the C++ default when Python does not override them;
- the five vendor-specific commands return `NOT_SUPPORTED` unless Python supplies an implementation;
- a Python implementation can replace either behavior for its own protocol.
The Python camera binding exposes HTTP, HTTPS, RTSP, and HTTP-snapshot modes. WebRTC remains a
built-in C++ camera mode, but is not exposed as a Python mode because the current Python capability
does not provide the corresponding cloud signaling-channel contract.
## Camera playback boundary
The camera stream mode describes how a stream is obtained; it does not by itself define ownership of
the wxWidgets view that renders it. `MediaPlayCtrl` selects and tears down the active backend, while
the wx parent owns the child window or renderer. This is important because a web view, native media
control, and frame-based/WebRTC renderer have different wx window-lifetime requirements.
Cloud URL and signaling requests are routed through `NetworkAgent` to the cloud provider selected for
the device. Local URL requests are routed to the active printer agent. The distinction keeps cloud
account services device-scoped while preserving the current one-LAN-agent/one-printer model.
## Compatibility constraints
The printer-agent boundary intentionally preserves several existing application contracts:
- Bambu-shaped JSON is still the shared command representation;
- existing network status codes are reused, with Orca-specific unsupported/capability errors added
in the Orca-reserved range;
- `MachineObject` remains the shared device-state model;
- preset and local-machine data retain compatibility with the existing agent-selection behavior;
- Python plugins use the existing capability and pybind11 registration system.
The agent abstraction is therefore responsible for containing vendor differences, not for pretending
that all vendor protocols are identical. The planned Orca JSON dialect is the protocol-neutral command
model for the `DeviceManager`/agent boundary. Once it is introduced, Bambu-specific translation remains
inside the Bambu agent and the Orca agent's v1 sink adapter can be removed as a self-contained cleanup.
## Main implementation locations
- [`IPrinterAgent`](../../src/slic3r/Utils/IPrinterAgent.hpp) — printer-agent contract and generic command defaults
- [`ICloudServiceAgent`](../../src/slic3r/Utils/ICloudServiceAgent.hpp) — cloud service and per-device
cloud camera contract
- [`NetworkAgent`](../../src/slic3r/Utils/NetworkAgent.hpp) — façade and dispatch between active agents
- [`NetworkAgentFactory`](../../src/slic3r/Utils/NetworkAgentFactory.hpp) — built-in and Python agent registry
- [`DeviceManager`](../../src/slic3r/GUI/DeviceCore/DevManager.cpp) — device ownership, filtering, and
stale-response checks
- [`PrinterAgentPluginCapability`](../../src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp)
— Python bindings
- [`MediaPlayCtrl`](../../src/slic3r/GUI/MediaPlayCtrl.cpp) — camera backend selection and playback lifecycle
+3 -1
View File
@@ -575,7 +575,7 @@ function CapabilityCanRun(plugin, capability) {
} }
function IsPluginChecked(plugin) { function IsPluginChecked(plugin) {
return GetStatus(plugin) === "Activated"; return plugin.is_loaded;
} }
function HasMixedCapabilityState(plugin) { function HasMixedCapabilityState(plugin) {
@@ -1347,6 +1347,8 @@ function StatusDescription(plugin) {
return "This plugin is still loading."; return "This plugin is still loading.";
case "Error": case "Error":
return "This plugin is blocked until its error is fixed."; return "This plugin is blocked until its error is fixed.";
case "RuntimeError":
return "This plugin is loaded but a capability reported an error.";
case "Inactive": case "Inactive":
default: default:
return "This plugin is inactive. Activate it to install or load it."; return "This plugin is inactive. Activate it to install or load it.";
@@ -424,6 +424,11 @@ body.pane-resizing {
font-weight: 600; font-weight: 600;
} }
.status-cell.status-runtimeerror {
color: var(--plugin-status-warn);
font-weight: 600;
}
.status-cell.status-loading { .status-cell.status-loading {
color: var(--plugin-status-warn); color: var(--plugin-status-warn);
font-weight: 600; font-weight: 600;
@@ -680,6 +685,11 @@ body.pane-resizing {
color: var(--plugin-status-danger); color: var(--plugin-status-danger);
} }
.detail-status-chip.status-runtimeerror {
background: var(--plugin-status-warn-bg);
color: var(--plugin-status-warn);
}
.detail-status-chip.status-loading { .detail-status-chip.status-loading {
background: var(--plugin-status-warn-bg); background: var(--plugin-status-warn-bg);
color: var(--plugin-status-warn); color: var(--plugin-status-warn);
@@ -331,6 +331,15 @@ modules:
sha256: deedcabe339165214a3637df4c86a507aef0d793cf8774ff68735f4737e8ddbc sha256: deedcabe339165214a3637df4c86a507aef0d793cf8774ff68735f4737e8ddbc
dest: external-packages/FFMPEG dest: external-packages/FFMPEG
# libdatachannel v0.24.5
# The Git checkout includes the submodules that are missing from the
# GitHub source archive. DataChannel.cmake consumes it as SOURCE_DIR.
- type: git
url: https://github.com/paullouisageneau/libdatachannel.git
tag: v0.24.5
commit: 443f6934d9007eb7076ab7825ba330f355fcbead
dest: external-packages/DataChannel
# --------------------------------------------------------------- # ---------------------------------------------------------------
# Fallback archives for deps normally provided by the GNOME SDK. # Fallback archives for deps normally provided by the GNOME SDK.
# These are only used if find_package() fails to locate them. # These are only used if find_package() fails to locate them.
-1
View File
@@ -3824,7 +3824,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type"); ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map"); ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map"); ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
// Snapshot and temporarily strip mixed filament slots so AMS sync operates on physical // Snapshot and temporarily strip mixed filament slots so AMS sync operates on physical
// filaments only. A mixed slot is virtual and has no tray to sync against; leaving it in // filaments only. A mixed slot is virtual and has no tray to sync against; leaving it in
// would let AMS mapping overwrite it and would break the physical-first slot ordering the // would let AMS mapping overwrite it and would break the physical-first slot ordering the
+22 -9
View File
@@ -236,6 +236,7 @@ set(SLIC3R_GUI_SOURCES
GUI/GLToolbar.hpp GUI/GLToolbar.hpp
GUI/ImageDPIFrame.cpp GUI/ImageDPIFrame.cpp
GUI/ImageDPIFrame.hpp GUI/ImageDPIFrame.hpp
GUI/IMediaController.hpp
GUI/GUI_App.cpp GUI/GUI_App.cpp
GUI/GUI_App.hpp GUI/GUI_App.hpp
GUI/GUI_AuxiliaryList.cpp GUI/GUI_AuxiliaryList.cpp
@@ -359,6 +360,8 @@ set(SLIC3R_GUI_SOURCES
GUI/MediaFilePanel.h GUI/MediaFilePanel.h
GUI/MediaPlayCtrl.cpp GUI/MediaPlayCtrl.cpp
GUI/MediaPlayCtrl.h GUI/MediaPlayCtrl.h
GUI/WebRtcMediaController.cpp
GUI/WebRtcMediaController.hpp
GUI/MeshUtils.cpp GUI/MeshUtils.cpp
GUI/MeshUtils.hpp GUI/MeshUtils.hpp
GUI/ModelMall.cpp GUI/ModelMall.cpp
@@ -552,6 +555,8 @@ set(SLIC3R_GUI_SOURCES
GUI/WebUserLoginDialog.hpp GUI/WebUserLoginDialog.hpp
GUI/WebViewDialog.cpp GUI/WebViewDialog.cpp
GUI/WebViewDialog.hpp GUI/WebViewDialog.hpp
GUI/WebMediaController.hpp
GUI/WebMediaController.cpp
GUI/Widgets/AMSControl.cpp GUI/Widgets/AMSControl.cpp
GUI/Widgets/AMSControl.hpp GUI/Widgets/AMSControl.hpp
GUI/Widgets/AMSItem.cpp GUI/Widgets/AMSItem.cpp
@@ -746,10 +751,15 @@ set(SLIC3R_GUI_SOURCES
Utils/NetworkAgentFactory.cpp Utils/NetworkAgentFactory.cpp
Utils/ICloudServiceAgent.hpp Utils/ICloudServiceAgent.hpp
Utils/IPrinterAgent.hpp Utils/IPrinterAgent.hpp
Utils/ICameraSignalingChannel.hpp
Utils/OrcaCloudServiceAgent.cpp Utils/OrcaCloudServiceAgent.cpp
Utils/OrcaCloudServiceAgent.hpp Utils/OrcaCloudServiceAgent.hpp
Utils/OrcaMqttConnection.cpp
Utils/OrcaMqttConnection.hpp
Utils/OrcaPrinterAgent.cpp Utils/OrcaPrinterAgent.cpp
Utils/OrcaPrinterAgent.hpp Utils/OrcaPrinterAgent.hpp
Utils/OrcaCloudSignalingChannel.cpp
Utils/OrcaCloudSignalingChannel.hpp
Utils/QidiPrinterAgent.cpp Utils/QidiPrinterAgent.cpp
Utils/QidiPrinterAgent.hpp Utils/QidiPrinterAgent.hpp
Utils/SnapmakerPrinterAgent.cpp Utils/SnapmakerPrinterAgent.cpp
@@ -890,7 +900,7 @@ target_include_directories(libslic3r_gui PRIVATE Utils ${CMAKE_CURRENT_BINARY_DI
if (WIN32) if (WIN32)
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../deps/WebView2/include)
target_link_libraries(libslic3r_gui Advapi32) target_link_libraries(libslic3r_gui Advapi32 Crypt32)
endif() endif()
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES}) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SLIC3R_GUI_SOURCES})
@@ -916,7 +926,7 @@ else()
set(_opengl_link_lib OpenGL::GL) set(_opengl_link_lib OpenGL::GL)
endif() endif()
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise pybind11::embed) target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto LibDataChannel::LibDataChannel noise::noise pybind11::embed)
if (CMAKE_SYSTEM_NAME STREQUAL "Linux") if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
# Linux finds wxWidgets in module mode, whose include dirs and definitions # Linux finds wxWidgets in module mode, whose include dirs and definitions
@@ -973,29 +983,32 @@ endif ()
if (APPLE) if (APPLE)
# Static FFmpeg from the deps install: nothing to bundle into the .app, # Static FFmpeg from the deps install: nothing to bundle into the .app,
# no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil. # no rpath/install_name handling. Order matters: avformat -> avcodec -> swscale -> avutil.
find_library(LIBAVFORMAT_LIBRARY NAMES libavformat.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) if (NOT LIBAVFORMAT_LIBRARY OR NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "Static FFmpeg (libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.") message(FATAL_ERROR "Static FFmpeg (libavformat.a/libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.")
endif () endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) target_link_libraries(libslic3r_gui ${LIBAVFORMAT_LIBRARY} ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
elseif (WIN32) elseif (WIN32)
# Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config, # Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config,
# so resolve the import libraries out of the deps prefix directly; the DLLs # so resolve the import libraries out of the deps prefix directly; the DLLs
# are copied next to the executable by the top level CMakeLists. # are copied next to the executable by the top level CMakeLists.
find_library(LIBAVFORMAT_LIBRARY NAMES avformat PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) if (NOT LIBAVFORMAT_LIBRARY OR NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "FFmpeg (avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.") message(FATAL_ERROR "FFmpeg (avformat/avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.")
endif () endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) target_link_libraries(libslic3r_gui ${LIBAVFORMAT_LIBRARY} ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
else () else ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavformat
libavcodec libavcodec
libswscale libswscale
libavutil libavutil
+27
View File
@@ -45,6 +45,25 @@ int AVVideoDecoder::open(Bambu_StreamInfo const &info)
return 0; return 0;
} }
int AVVideoDecoder::open(AVCodecParameters const &parameters)
{
if (avcodec_parameters_to_context(codec_ctx_, &parameters) < 0)
return -1;
auto codec = avcodec_find_decoder(codec_ctx_->codec_id);
if (codec == nullptr) {
fprintf(stderr, "AVVideoDecoder: unsupported codec!\n");
return -1;
}
if (avcodec_open2(codec_ctx_, codec, nullptr) < 0) {
fprintf(stderr, "AVVideoDecoder: could not open codec\n");
return -1;
}
frame_ = av_frame_alloc();
return frame_ == nullptr ? -1 : 0;
}
int AVVideoDecoder::decode(const Bambu_Sample &sample) int AVVideoDecoder::decode(const Bambu_Sample &sample)
{ {
int ret = -1; int ret = -1;
@@ -71,6 +90,14 @@ int AVVideoDecoder::decode(const Bambu_Sample &sample)
return ret; return ret;
} }
int AVVideoDecoder::decode(const AVPacket &packet)
{
int ret = avcodec_send_packet(codec_ctx_, &packet);
if (ret == 0)
got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0;
return ret;
}
int AVVideoDecoder::flush() int AVVideoDecoder::flush()
{ {
int ret = avcodec_send_packet(codec_ctx_, nullptr); int ret = avcodec_send_packet(codec_ctx_, nullptr);
+10
View File
@@ -23,8 +23,10 @@ public:
public: public:
int open(Bambu_StreamInfo const &info); int open(Bambu_StreamInfo const &info);
int open(AVCodecParameters const &parameters);
int decode(Bambu_Sample const &sample); int decode(Bambu_Sample const &sample);
int decode(AVPacket const &packet);
int flush(); int flush();
@@ -34,6 +36,14 @@ public:
bool toWxBitmap(wxBitmap &bitmap, wxSize const & size); bool toWxBitmap(wxBitmap &bitmap, wxSize const & size);
// Native size of the most recently decoded frame, or an unspecified size if
// nothing has decoded yet. Lets a caller learn the video dimensions when the
// container/probe could not report them up front.
wxSize decoded_frame_size() const
{
return got_frame_ && frame_ ? wxSize{frame_->width, frame_->height} : wxSize{};
}
private: private:
AVCodecContext *codec_ctx_ = nullptr; AVCodecContext *codec_ctx_ = nullptr;
AVFrame * frame_ = nullptr; AVFrame * frame_ = nullptr;
+1 -1
View File
@@ -328,7 +328,7 @@ void SelectMObjectPopup::update_user_devices()
} }
m_bind_machine_list.clear(); m_bind_machine_list.clear();
m_bind_machine_list = dev->get_my_machine_list(); m_bind_machine_list = dev->get_my_machine_list(dev->get_current_printer_agent_id());
//sort list //sort list
std::vector<std::pair<std::string, MachineObject*>> user_machine_list; std::vector<std::pair<std::string, MachineObject*>> user_machine_list;
+16 -29
View File
@@ -6,8 +6,10 @@
#include "Widgets/Label.hpp" #include "Widgets/Label.hpp"
#include "MsgDialog.hpp" #include "MsgDialog.hpp"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "PrePrintChecker.hpp"
#include "DeviceCore/DevConfig.h" #include "DeviceCore/DevConfig.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevExtruderSystem.h"
#include "DeviceCore/DevFilaBlackList.h" #include "DeviceCore/DevFilaBlackList.h"
#include "DeviceCore/DevFilaSystem.h" #include "DeviceCore/DevFilaSystem.h"
@@ -1638,29 +1640,6 @@ void CalibrationPresetPage::update_combobox_filaments(MachineObject* obj)
select_default_compatible_filament(); select_default_compatible_filament();
} }
bool CalibrationPresetPage::is_blocking_printing()
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
MachineObject* obj_ = dev->get_selected_machine();
if (obj_ == nullptr) return true;
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
auto target_model = obj_->printer_type;
if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
if (it == compatible_machine.end()) {
return true;
}
}
return false;
}
bool CalibrationPresetPage::is_nozzle_info_synced() const bool CalibrationPresetPage::is_nozzle_info_synced() const
{ {
if (!curr_obj || !curr_obj->is_info_ready()) if (!curr_obj || !curr_obj->is_info_ready())
@@ -1742,11 +1721,13 @@ void CalibrationPresetPage::update_show_status()
} }
} }
//if (is_blocking_printing()) { bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type);
// show_status(CaliPresetPageStatus::CaliPresetStatusUnsupportedPrinter); if (PresetBundle *preset_bundle = wxGetApp().preset_bundle) {
// return; has_optional_printer_model = has_optional_printer_model ||
//} DevPrinterConfigUtil::is_optional_printer_model_id(
//else preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle));
}
if (obj_->is_connecting() || !obj_->is_connected()) { if (obj_->is_connecting() || !obj_->is_connected()) {
show_status(CaliPresetPageStatus::CaliPresetStatusInConnecting); show_status(CaliPresetPageStatus::CaliPresetStatusInConnecting);
return; return;
@@ -1798,7 +1779,9 @@ void CalibrationPresetPage::update_show_status()
return; return;
} }
show_status(CaliPresetPageStatus::CaliPresetStatusNormal); show_status(has_optional_printer_model ?
CaliPresetPageStatus::CaliPresetStatusOptionalPrinterModel :
CaliPresetPageStatus::CaliPresetStatusNormal);
} }
@@ -1852,6 +1835,10 @@ void CalibrationPresetPage::show_status(CaliPresetPageStatus status)
Layout(); Layout();
Fit(); Fit();
} }
else if (status == CaliPresetPageStatus::CaliPresetStatusOptionalPrinterModel) {
update_print_status_msg(PrePrintChecker::get_pre_state_msg(PrintDialogStatus::PrintStatusOptionalPrinterModel), true);
Enable_Send_Button(true);
}
else if (status == CaliPresetPageStatus::CaliPresetStatusNoUserLogin) { else if (status == CaliPresetPageStatus::CaliPresetStatusNoUserLogin) {
wxString msg_text = _L("No login account, only printers in LAN mode are displayed."); wxString msg_text = _L("No login account, only printers in LAN mode are displayed.");
update_print_status_msg(msg_text, false); update_print_status_msg(msg_text, false);
@@ -152,7 +152,8 @@ enum CaliPresetPageStatus
CaliPresetStatusInConnecting, CaliPresetStatusInConnecting,
CaliPresetStatusFilamentIncompatible, CaliPresetStatusFilamentIncompatible,
CaliPresetStatusLanModeSDcardNotAvailable, CaliPresetStatusLanModeSDcardNotAvailable,
CaliPresetStatusDifferentNozzleDiameters CaliPresetStatusDifferentNozzleDiameters,
CaliPresetStatusOptionalPrinterModel
}; };
class CalibrationPresetPage : public CalibrationWizardPage class CalibrationPresetPage : public CalibrationWizardPage
@@ -268,7 +269,6 @@ protected:
bool is_nozzle_info_synced() const; bool is_nozzle_info_synced() const;
void show_status(CaliPresetPageStatus status); void show_status(CaliPresetPageStatus status);
void Enable_Send_Button(bool enable); void Enable_Send_Button(bool enable);
bool is_blocking_printing();
bool need_check_sdcard(MachineObject* obj); bool need_check_sdcard(MachineObject* obj);
CaliPresetPageStatus get_status() { return m_page_status; } CaliPresetPageStatus get_status() { return m_page_status; }
-60
View File
@@ -28,7 +28,6 @@ wxEND_EVENT_TABLE()
wxDEFINE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDEFINE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent);
wxDEFINE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); wxDEFINE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent);
wxDEFINE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent);
#define CAMERAPOPUP_CLICK_INTERVAL 20 #define CAMERAPOPUP_CLICK_INTERVAL 20
@@ -102,34 +101,6 @@ CameraPopup::CameraPopup(wxWindow *parent)
top_sizer->Add(0, 0, wxALL, 0); top_sizer->Add(0, 0, wxALL, 0);
} }
// Orca: custom IP camera source — lets the user point Live Video at any camera URL (Orca feature; not in the reference)
m_custom_camera_input_confirm = new Button(m_panel, _L("Enable"));
m_custom_camera_input_confirm->SetBackgroundColor(wxColour(38, 166, 154));
m_custom_camera_input_confirm->SetBorderColor(wxColour(38, 166, 154));
m_custom_camera_input_confirm->SetTextColor(wxColour(0xFFFFFE));
m_custom_camera_input_confirm->SetFont(Label::Body_14);
m_custom_camera_input_confirm->SetMinSize(wxSize(FromDIP(90), FromDIP(30)));
m_custom_camera_input_confirm->SetPosition(wxDefaultPosition);
m_custom_camera_input_confirm->SetCornerRadius(FromDIP(12));
m_custom_camera_input = new TextInput(m_panel, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, wxDefaultSize);
m_custom_camera_input->GetTextCtrl()->SetHint(_L("Hostname or IP"));
m_custom_camera_input->GetTextCtrl()->SetFont(Label::Body_14);
m_custom_camera_hint = new wxStaticText(m_panel, wxID_ANY, _L("Custom camera source"));
m_custom_camera_hint->Wrap(-1);
m_custom_camera_hint->SetFont(Label::Head_14);
m_custom_camera_hint->SetForegroundColour(TEXT_COL);
m_custom_camera_input_confirm->Bind(wxEVT_BUTTON, &CameraPopup::on_camera_source_changed, this);
if (!wxGetApp().app_config->get("camera", "custom_source").empty()) {
m_custom_camera_input->GetTextCtrl()->SetValue(wxGetApp().app_config->get("camera", "custom_source"));
set_custom_cam_button_state(wxGetApp().app_config->get("camera", "enable_custom_source") == "true");
}
top_sizer->Add(m_custom_camera_hint, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5));
top_sizer->Add(0, 0, wxALL, 0);
top_sizer->Add(m_custom_camera_input, 2, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxALL, FromDIP(5));
top_sizer->Add(m_custom_camera_input_confirm, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5));
main_sizer->Add(top_sizer, 0, wxALL, FromDIP(10)); main_sizer->Add(top_sizer, 0, wxALL, FromDIP(10));
auto url = wxString(L"https://www.orcaslicer.com/wiki/"); // Orca: neutral wiki link (vendor URL removed) auto url = wxString(L"https://www.orcaslicer.com/wiki/"); // Orca: neutral wiki link (vendor URL removed)
@@ -184,37 +155,6 @@ void CameraPopup::sdcard_absent_hint()
GetEventHandler()->ProcessEvent(evt); GetEventHandler()->ProcessEvent(evt);
} }
void CameraPopup::on_camera_source_changed(wxCommandEvent &event)
{
if (m_obj && !m_custom_camera_input->GetTextCtrl()->IsEmpty()) {
handle_camera_source_change();
}
}
void CameraPopup::handle_camera_source_change()
{
m_custom_camera_enabled = !m_custom_camera_enabled;
set_custom_cam_button_state(m_custom_camera_enabled);
wxGetApp().app_config->set("camera", "custom_source", m_custom_camera_input->GetTextCtrl()->GetValue().ToStdString());
wxGetApp().app_config->set("camera", "enable_custom_source", m_custom_camera_enabled);
wxCommandEvent evt(EVT_CAM_SOURCE_CHANGE);
evt.SetEventObject(this);
GetEventHandler()->ProcessEvent(evt);
}
void CameraPopup::set_custom_cam_button_state(bool state)
{
m_custom_camera_enabled = state;
auto stateColour = state ? wxColour(170, 0, 0) : wxColour(38, 166, 154);
auto stateText = state ? "Disable" : "Enable";
m_custom_camera_input_confirm->SetBackgroundColor(stateColour);
m_custom_camera_input_confirm->SetBorderColor(stateColour);
m_custom_camera_input_confirm->SetLabel(_L(stateText));
}
void CameraPopup::on_switch_recording(wxCommandEvent& event) void CameraPopup::on_switch_recording(wxCommandEvent& event)
{ {
if (!m_obj) return; if (!m_obj) return;
+4 -9
View File
@@ -15,14 +15,12 @@
#include "Widgets/SwitchButton.hpp" #include "Widgets/SwitchButton.hpp"
#include "Widgets/RadioBox.hpp" #include "Widgets/RadioBox.hpp"
#include "Widgets/PopupWindow.hpp" #include "Widgets/PopupWindow.hpp"
#include "Widgets/TextInput.hpp"
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent);
wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent);
wxDECLARE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent);
class CameraPopup : public PopupWindow class CameraPopup : public PopupWindow
{ {
@@ -53,9 +51,6 @@ protected:
void on_switch_recording(wxCommandEvent& event); void on_switch_recording(wxCommandEvent& event);
void on_set_resolution(); void on_set_resolution();
void sdcard_absent_hint(); void sdcard_absent_hint();
void on_camera_source_changed(wxCommandEvent& event);
void handle_camera_source_change();
void set_custom_cam_button_state(bool state);
wxWindow * create_item_radiobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left); wxWindow * create_item_radiobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left);
void select_curr_radiobox(int btn_idx); void select_curr_radiobox(int btn_idx);
@@ -76,10 +71,10 @@ private:
wxStaticText* m_text_liveview_retry; wxStaticText* m_text_liveview_retry;
SwitchButton* m_switch_liveview_retry; SwitchButton* m_switch_liveview_retry;
#endif //BBL_RELEASE_TO_PUBLIC #endif //BBL_RELEASE_TO_PUBLIC
wxStaticText* m_custom_camera_hint; // wxStaticText* m_custom_camera_hint;
TextInput* m_custom_camera_input; // TextInput* m_custom_camera_input;
Button* m_custom_camera_input_confirm; // Button* m_custom_camera_input_confirm;
bool m_custom_camera_enabled{ false }; // bool m_custom_camera_enabled{ false };
wxStaticText* m_text_resolution; wxStaticText* m_text_resolution;
wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM]; wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM];
wxScrolledWindow *m_panel; wxScrolledWindow *m_panel;
+3 -1
View File
@@ -35,7 +35,9 @@ ConnectPrinterDialog::ConnectPrinterDialog(wxWindow *parent, wxWindowID id, cons
sizer_connect = new wxBoxSizer(wxHORIZONTAL); sizer_connect = new wxBoxSizer(wxHORIZONTAL);
m_textCtrl_code = new TextInput(this, wxEmptyString); m_textCtrl_code = new TextInput(this, wxEmptyString);
m_textCtrl_code->GetTextCtrl()->SetMaxLength(10); // OrcaSonar uses a 12-character base32 access code. Keep this field long
// enough for it while retaining the existing validation for LAN codes.
m_textCtrl_code->GetTextCtrl()->SetMaxLength(12);
m_textCtrl_code->SetFont(Label::Body_14); m_textCtrl_code->SetFont(Label::Body_14);
m_textCtrl_code->SetCornerRadius(FromDIP(5)); m_textCtrl_code->SetCornerRadius(FromDIP(5));
m_textCtrl_code->SetSize(wxSize(FromDIP(330), FromDIP(40))); m_textCtrl_code->SetSize(wxSize(FromDIP(330), FromDIP(40)));
+16 -1
View File
@@ -1,5 +1,7 @@
#include "DevConfigUtil.h" #include "DevConfigUtil.h"
#include "slic3r/GUI/DeviceManager.hpp"
#include <wx/dir.h> #include <wx/dir.h>
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include "../I18N.hpp" #include "../I18N.hpp"
@@ -41,6 +43,19 @@ static void _toolhead_translation_markers()
std::string DevPrinterConfigUtil::m_resource_file_path = ""; std::string DevPrinterConfigUtil::m_resource_file_path = "";
bool DevPrinterConfigUtil::is_printer_model_compatible(const std::string& source_model, MachineObject& machine)
{
const std::string& target_model = machine.printer_type;
if (is_optional_printer_model_id(source_model) || is_optional_printer_model_id(target_model))
return true;
if (source_model == target_model)
return true;
const auto compatible_machine = machine.get_compatible_machine();
return std::find(compatible_machine.begin(), compatible_machine.end(), source_model) != compatible_machine.end();
}
std::map<std::string, std::string> DevPrinterConfigUtil::get_all_model_id_with_name() std::map<std::string, std::string> DevPrinterConfigUtil::get_all_model_id_with_name()
{ {
@@ -405,4 +420,4 @@ std::string DevPrinterConfigUtil::get_toolhead_display_name(
return result; return result;
} }
}; };
+7 -1
View File
@@ -25,6 +25,8 @@
namespace Slic3r namespace Slic3r
{ {
class MachineObject;
/// Toolhead component type (extruder / nozzle / hotend) /// Toolhead component type (extruder / nozzle / hotend)
enum class ToolHeadComponent { enum class ToolHeadComponent {
Extruder, Extruder,
@@ -59,6 +61,10 @@ public:
/*printer*/ /*printer*/
// info // info
static std::map<std::string, std::string> get_all_model_id_with_name(); static std::map<std::string, std::string> get_all_model_id_with_name();
// A printer agent may not know the physical model. Keep that case optional so
// model compatibility checks do not turn missing identity into a hard error.
static bool is_printer_model_compatible(const std::string& source_model, MachineObject& machine);
static bool is_optional_printer_model_id(const std::string& model_id) { return model_id.empty(); }
static std::string get_printer_type(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "printer_type"); } static std::string get_printer_type(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "printer_type"); }
static std::string get_printer_display_name(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "display_name"); } static std::string get_printer_display_name(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "display_name"); }
static std::string get_printer_series_str(std::string type_str) { return get_value_from_config<std::string>(type_str, "printer_series"); } static std::string get_printer_series_str(std::string type_str) { return get_value_from_config<std::string>(type_str, "printer_series"); }
@@ -227,4 +233,4 @@ static std::string _parse_printer_type(const std::string &type_str)
return type_str; return type_str;
} }
};// namespace Slic3r };// namespace Slic3r
+4 -4
View File
@@ -762,9 +762,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
{ {
curr_tray->remain = -1; curr_tray->remain = -1;
} }
if (tray_it->contains("tray_slot_placeholder")) { // The tray objects are reused across status updates. Reset this
curr_tray->is_slot_placeholder = true; // state when a previously empty slot receives a filament again.
} curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder");
int ams_id_int = 0; int ams_id_int = 0;
int tray_id_int = 0; int tray_id_int = 0;
try try
@@ -999,4 +999,4 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
} }
} }
} }
+133 -52
View File
@@ -1,8 +1,10 @@
#include "DevManager.h"
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <exception> #include <exception>
#include "DevManager.h" #include <libslic3r/AppConfig.hpp>
#include "CloudProvider.hpp"
#include "DevUtil.h" #include "DevUtil.h"
// TODO: remove this include // TODO: remove this include
@@ -15,6 +17,8 @@
#include "libslic3r/Time.hpp" #include "libslic3r/Time.hpp"
#include "IPrinterAgent.hpp"
using namespace nlohmann; using namespace nlohmann;
namespace { namespace {
@@ -46,10 +50,12 @@ namespace {
namespace Slic3r namespace Slic3r
{ {
DeviceManager::DeviceManager(NetworkAgent* agent) DeviceManager::DeviceManager(NetworkAgent* agent, bool enable_refresher, AppConfig* app_config)
{ {
m_agent = agent; m_agent = agent;
m_refresher = new DeviceManagerRefresher(this); m_app_config = app_config;
if (enable_refresher)
m_refresher = new DeviceManagerRefresher(this);
DevPrinterConfigUtil::InitFilePath(resources_dir()); DevPrinterConfigUtil::InitFilePath(resources_dir());
@@ -60,9 +66,14 @@ namespace Slic3r
} }
} }
AppConfig* DeviceManager::get_app_config() const
{
return m_app_config ? m_app_config : GUI::wxGetApp().app_config;
}
void DeviceManager::load_local_machines_from_config() void DeviceManager::load_local_machines_from_config()
{ {
AppConfig* config = GUI::wxGetApp().app_config; AppConfig* config = get_app_config();
if (!config) if (!config)
return; return;
const auto local_machines = config->get_local_machines(); const auto local_machines = config->get_local_machines();
@@ -88,9 +99,8 @@ namespace Slic3r
} }
} }
void DeviceManager::update_local_machine(const MachineObject& m) void DeviceManager::update_local_machine(const MachineObject& m, AppConfig* config)
{ {
AppConfig* config = GUI::wxGetApp().app_config;
if (config) { if (config) {
if (m.is_lan_mode_printer()) { if (m.is_lan_mode_printer()) {
if (m.has_access_right()) { if (m.has_access_right()) {
@@ -111,7 +121,8 @@ namespace Slic3r
DeviceManager::~DeviceManager() DeviceManager::~DeviceManager()
{ {
delete m_refresher; if (m_refresher)
delete m_refresher;
for (auto it = localMachineList.begin(); it != localMachineList.end(); it++) for (auto it = localMachineList.begin(); it != localMachineList.end(); it++)
{ {
@@ -171,14 +182,22 @@ namespace Slic3r
return printer_agent ? printer_agent->get_agent_info().id : ""; return printer_agent ? printer_agent->get_agent_info().id : "";
} }
std::string DeviceManager::get_current_cloud_provider() const
{
const std::string agent_id = get_current_printer_agent_id();
if (!agent_id.empty())
return agent_id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER;
return GUI::wxGetApp().get_printer_cloud_provider();
}
void DeviceManager::EnableMultiMachine(bool enable) void DeviceManager::EnableMultiMachine(bool enable)
{ {
m_agent->enable_multi_machine(enable); m_agent->enable_multi_machine(enable);
m_enable_mutil_machine = enable; m_enable_mutil_machine = enable;
} }
void DeviceManager::start_refresher() { m_refresher->Start(); } void DeviceManager::start_refresher() { if (m_refresher) m_refresher->Start(); }
void DeviceManager::stop_refresher() { m_refresher->Stop(); } void DeviceManager::stop_refresher() { if (m_refresher) m_refresher->Stop(); }
void DeviceManager::keep_alive() void DeviceManager::keep_alive()
@@ -265,6 +284,10 @@ namespace Slic3r
/* update userMachineList info */ /* update userMachineList info */
auto it = userMachineList.find(dev_id); auto it = userMachineList.find(dev_id);
if (it != userMachineList.end()) { if (it != userMachineList.end()) {
// A reused entry may have been created while another printer agent was active.
// The response was obtained through the current agent, so move ownership with
// the entry; otherwise agent-scoped lists hide it after a preset switch.
it->second->printer_agent_id = get_current_printer_agent_id();
if (it->second->get_dev_ip() != dev_ip || if (it->second->get_dev_ip() != dev_ip ||
it->second->bind_state != bind_state || it->second->bind_state != bind_state ||
it->second->bind_sec_link != sec_link || it->second->bind_sec_link != sec_link ||
@@ -291,10 +314,15 @@ namespace Slic3r
/* update localMachineList */ /* update localMachineList */
it = localMachineList.find(dev_id); it = localMachineList.find(dev_id);
AppConfig* config = get_app_config();
if (it != localMachineList.end()) { if (it != localMachineList.end()) {
// update properties // update properties
/* ip changed */ /* ip changed */
obj = it->second; obj = it->second;
// A reused LAN entry may have been discovered while another printer agent was
// active. The current discovery message establishes ownership for this agent.
obj->printer_agent_id = get_current_printer_agent_id();
if (obj->get_dev_ip().compare(dev_ip) != 0) { if (obj->get_dev_ip().compare(dev_ip) != 0) {
if ( connection_name.empty() ) { if ( connection_name.empty() ) {
@@ -384,7 +412,6 @@ namespace Slic3r
obj->m_is_online = true; obj->m_is_online = true;
//load access code //load access code
AppConfig* config = Slic3r::GUI::wxGetApp().app_config;
if (config) { if (config) {
obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false); obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false);
} }
@@ -407,7 +434,7 @@ namespace Slic3r
fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx); fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx);
} }
} }
update_local_machine(*obj); update_local_machine(*obj, config);
} }
catch (...) { catch (...) {
; ;
@@ -422,6 +449,9 @@ namespace Slic3r
auto it = localMachineList.find(machine.dev_id); auto it = localMachineList.find(machine.dev_id);
if (it != localMachineList.end()) { if (it != localMachineList.end()) {
obj = it->second; obj = it->second;
// insert_local_device is called by the active agent, so a reused entry must follow
// that agent as well; otherwise the agent-scoped printer list hides it.
obj->printer_agent_id = get_current_printer_agent_id();
} else { } else {
obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip); obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip);
obj->printer_agent_id = get_current_printer_agent_id(); obj->printer_agent_id = get_current_printer_agent_id();
@@ -439,11 +469,16 @@ namespace Slic3r
obj->last_alive = Slic3r::Utils::get_current_time_utc(); obj->last_alive = Slic3r::Utils::get_current_time_utc();
obj->set_access_code(access_code, false); obj->set_access_code(access_code, false);
update_local_machine(*obj); update_local_machine(*obj, get_app_config());
return obj; return obj;
} }
void DeviceManager::update_local_machine(const MachineObject& m)
{
update_local_machine(m, GUI::wxGetApp().app_config);
}
int DeviceManager::query_bind_status(std::string& msg, const std::string& provider) int DeviceManager::query_bind_status(std::string& msg, const std::string& provider)
{ {
if (!m_agent) if (!m_agent)
@@ -514,7 +549,7 @@ namespace Slic3r
MachineObject* DeviceManager::get_my_machine(std::string dev_id) MachineObject* DeviceManager::get_my_machine(std::string dev_id)
{ {
auto list = get_my_machine_list(); auto list = get_my_machine_list(get_current_printer_agent_id());
auto it = list.find(dev_id); auto it = list.find(dev_id);
if (it != list.end()) if (it != list.end())
{ {
@@ -551,25 +586,15 @@ namespace Slic3r
OnSelectedMachineChanged(previous_selected_machine, selected_machine); OnSelectedMachineChanged(previous_selected_machine, selected_machine);
} }
void DeviceManager::clear_other_devices(const std::string& target_agent_id) void DeviceManager::clear_other_devices()
{ {
// why: on agent swap, keep "My Devices" but drop the transient "Other Devices" // Device entries are now scoped by printer_agent_id when they are presented. Keep
// Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own. // agent-owned discoveries across a switch so agents without automatic discovery (and
// // plugins whose devices have not received an access code yet) do not lose their list.
// Also drop "My Devices" stamped by a different agent than the one we're swapping to // Entries without an owner are legacy/unscoped and cannot safely be shown.
// (target_agent_id, passed by the caller since the live agent hasn't been repointed yet
// at this point): otherwise a device first discovered under agent A survives every swap
// with a stale printer_agent_id, stays hidden from every agent's filtered list, and only
// gets re-tagged if something happens to delete and re-create it (e.g. account logout).
// Dropping it here instead lets the new agent's start_discovery re-insert and re-stamp it
// like any other fresh device.
const auto my = get_my_machine_list();
for (auto it = localMachineList.begin(); it != localMachineList.end();) for (auto it = localMachineList.begin(); it != localMachineList.end();)
{ {
const bool is_my_device = my.find(it->first) != my.end(); if (!it->second || it->second->printer_agent_id.empty())
const bool agent_mismatch = !target_agent_id.empty() && it->second &&
it->second->printer_agent_id != target_agent_id;
if (!is_my_device || agent_mismatch)
{ {
delete it->second; delete it->second;
it = localMachineList.erase(it); it = localMachineList.erase(it);
@@ -585,8 +610,22 @@ namespace Slic3r
{ {
BOOST_LOG_TRIVIAL(info) << "set_selected_machine=" << dev_id BOOST_LOG_TRIVIAL(info) << "set_selected_machine=" << dev_id
<< " cur_selected=" << selected_machine; << " cur_selected=" << selected_machine;
auto my_machine_list = get_my_machine_list(); auto my_machine_list = get_my_machine_list(get_current_printer_agent_id());
auto it = my_machine_list.find(dev_id); auto it = my_machine_list.find(dev_id);
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: set_selected_machine lookup dev_id=" << dev_id
<< " found=" << (it != my_machine_list.end())
<< " my_machine_count=" << my_machine_list.size()
<< " current_agent=" << get_current_printer_agent_id()
<< " provider=" << GUI::wxGetApp().get_printer_cloud_provider();
if (it != my_machine_list.end() && it->second) {
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: target machine dev_id=" << it->second->get_dev_id()
<< " printer_agent_id=" << it->second->printer_agent_id
<< " connection_type=" << it->second->connection_type()
<< " dev_connection_type=" << it->second->dev_connection_type;
} else if (!dev_id.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: target machine was not found in the current agent's machine list";
return false;
}
// disconnect last if dev_id difference from previous one // disconnect last if dev_id difference from previous one
auto last_selected = my_machine_list.find(selected_machine); auto last_selected = my_machine_list.find(selected_machine);
@@ -597,7 +636,9 @@ namespace Slic3r
m_agent->disconnect_printer(); m_agent->disconnect_printer();
} }
else if (last_selected->second->connection_type() == "cloud") { else if (last_selected->second->connection_type() == "cloud") {
m_agent->set_user_selected_machine(""); const int result = m_agent->set_user_selected_machine("");
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: cleared previous cloud selection dev_id="
<< selected_machine << " result=" << result;
} }
} }
@@ -632,11 +673,7 @@ namespace Slic3r
m_agent->disconnect_printer(); m_agent->disconnect_printer();
it->second->reset(); it->second->reset();
#if !BBL_RELEASE_TO_PUBLIC it->second->connect();
it->second->connect(Slic3r::GUI::wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false);
#else
it->second->connect(it->second->local_use_ssl);
#endif
it->second->set_lan_mode_connection_state(true); it->second->set_lan_mode_connection_state(true);
} }
} }
@@ -649,18 +686,16 @@ namespace Slic3r
{ {
// diff dev_id, cloud => set_user_selected_machine(new) // diff dev_id, cloud => set_user_selected_machine(new)
BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new cloud machine, dev_id =" << dev_id; BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new cloud machine, dev_id =" << dev_id;
m_agent->set_user_selected_machine(dev_id); const int result = m_agent->set_user_selected_machine(dev_id);
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: set new cloud selection dev_id="
<< dev_id << " result=" << result;
it->second->reset(); it->second->reset();
} }
else else
{ {
BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new lan machine, dev_id =" << dev_id; BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new lan machine, dev_id =" << dev_id;
it->second->reset(); it->second->reset();
#if !BBL_RELEASE_TO_PUBLIC it->second->connect();
it->second->connect(Slic3r::GUI::wxGetApp().app_config->get("enable_ssl_for_mqtt") == "true" ? true : false);
#else
it->second->connect(it->second->local_use_ssl);
#endif
it->second->set_lan_mode_connection_state(true); it->second->set_lan_mode_connection_state(true);
} }
} }
@@ -677,6 +712,8 @@ namespace Slic3r
selected_machine = dev_id; selected_machine = dev_id;
record_user_last_machine(selected_machine); record_user_last_machine(selected_machine);
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: DeviceManager selection complete selected_machine="
<< selected_machine;
return true; return true;
} }
@@ -707,7 +744,9 @@ namespace Slic3r
dev_list.push_back(it->first); dev_list.push_back(it->first);
BOOST_LOG_TRIVIAL(trace) << "add_user_subscribe: " << it->first; BOOST_LOG_TRIVIAL(trace) << "add_user_subscribe: " << it->first;
} }
m_agent->add_subscribe(dev_list); const int result = m_agent->add_subscribe(dev_list);
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: add_user_subscribe count=" << dev_list.size()
<< " result=" << result;
} }
@@ -720,7 +759,9 @@ namespace Slic3r
dev_list.push_back(it->first); dev_list.push_back(it->first);
BOOST_LOG_TRIVIAL(trace) << "del_user_subscribe: " << it->first; BOOST_LOG_TRIVIAL(trace) << "del_user_subscribe: " << it->first;
} }
m_agent->del_subscribe(dev_list); const int result = m_agent->del_subscribe(dev_list);
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: del_user_subscribe count=" << dev_list.size()
<< " result=" << result;
} }
void DeviceManager::subscribe_device_list(std::vector<std::string> dev_list) void DeviceManager::subscribe_device_list(std::vector<std::string> dev_list)
@@ -839,7 +880,24 @@ namespace Slic3r
try try
{ {
json j = json::parse(body); json j = json::parse(body);
const std::string provider = GUI::wxGetApp().get_printer_cloud_provider();
const bool has_request_context = j.contains("provider") && j.contains("agent_id") && j.contains("generation");
const std::string current_provider = get_current_cloud_provider();
const std::string provider = j.contains("provider") ? j["provider"].get<std::string>()
: current_provider;
const std::string agent_id = j.contains("agent_id") ? j["agent_id"].get<std::string>()
: get_current_printer_agent_id();
const std::uint64_t generation = j.value("generation", std::uint64_t(0));
if (has_request_context &&
(provider != current_provider ||
agent_id != get_current_printer_agent_id() ||
generation != (m_agent ? m_agent->get_user_machine_list_generation() : 0))) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": ignoring stale response provider="
<< provider << " agent_id=" << agent_id
<< " generation=" << generation;
return;
}
#if !BBL_RELEASE_TO_PUBLIC #if !BBL_RELEASE_TO_PUBLIC
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << j; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << j;
@@ -862,11 +920,14 @@ namespace Slic3r
/* update field */ /* update field */
obj = iter->second; obj = iter->second;
obj->set_dev_id(dev_id); obj->set_dev_id(dev_id);
// A device can be rediscovered by a different agent after a preset
// switch while retaining the same MachineObject instance.
obj->printer_agent_id = agent_id;
} }
else else
{ {
obj = new MachineObject(this, m_agent, "", "", ""); obj = new MachineObject(this, m_agent, "", "", "");
obj->printer_agent_id = get_current_printer_agent_id(); obj->printer_agent_id = agent_id;
if (m_agent) if (m_agent)
{ {
obj->set_bind_status(m_agent->get_user_name(provider)); obj->set_bind_status(m_agent->get_user_name(provider));
@@ -874,13 +935,20 @@ namespace Slic3r
if (obj->get_dev_ip().empty()) if (obj->get_dev_ip().empty())
{ {
obj->get_dev_ip() = Slic3r::GUI::wxGetApp().app_config->get("ip_address", dev_id); if (AppConfig* config = get_app_config())
obj->get_dev_ip() = config->get("ip_address", dev_id);
} }
userMachineList.insert(std::make_pair(dev_id, obj)); userMachineList.insert(std::make_pair(dev_id, obj));
} }
if (!obj) continue; if (!obj) continue;
// Orca cloud printers are only ever delivered through this REST
// account list; tag them so DeviceManager's cloud/lan branches
// (subscribe + deselect in set_selected_machine) treat them right.
if (provider == ORCA_CLOUD_PROVIDER)
obj->dev_connection_type = "cloud";
if (!elem["dev_id"].is_null()) if (!elem["dev_id"].is_null())
obj->set_dev_id(elem["dev_id"].get<std::string>()); obj->set_dev_id(elem["dev_id"].get<std::string>());
if (!elem["dev_name"].is_null()) if (!elem["dev_name"].is_null())
@@ -912,6 +980,12 @@ namespace Slic3r
acc_code.erase(std::remove(acc_code.begin(), acc_code.end(), '\n'), acc_code.end()); acc_code.erase(std::remove(acc_code.begin(), acc_code.end(), '\n'), acc_code.end());
obj->set_access_code(acc_code); obj->set_access_code(acc_code);
} }
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: parsed cloud machine dev_id=" << dev_id
<< " name=" << obj->get_dev_name()
<< " agent_id=" << obj->printer_agent_id
<< " connection_type=" << obj->connection_type()
<< " online=" << obj->m_is_online;
} }
//remove MachineObject from userMachineList //remove MachineObject from userMachineList
@@ -927,6 +1001,9 @@ namespace Slic3r
iterat++; iterat++;
} }
} }
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: parse_user_print_info complete provider=" << provider
<< " parsed_count=" << new_list.size()
<< " stored_count=" << userMachineList.size();
} }
} }
catch (std::exception& e) catch (std::exception& e)
@@ -943,25 +1020,29 @@ namespace Slic3r
unsigned int http_code; unsigned int http_code;
std::string body; std::string body;
int result = m_agent->get_user_print_info(&http_code, &body, provider); int result = m_agent->get_user_print_info(&http_code, &body, provider);
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: get_user_print_info provider=" << provider
<< " result=" << result << " http_code=" << http_code
<< " body_bytes=" << body.size();
if (result == 0) if (result == 0)
{ {
// parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map. // parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map.
// on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info. // on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info.
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: queueing parse_user_print_info on UI thread";
Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); }); Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); });
} }
} }
void DeviceManager::record_user_last_machine(const std::string& dev_id) void DeviceManager::record_user_last_machine(const std::string& dev_id)
{ {
if (Slic3r::GUI::wxGetApp().app_config) { if (AppConfig* config = get_app_config()) {
Slic3r::GUI::wxGetApp().app_config->set("user_last_selected_machine", dev_id); config->set("user_last_selected_machine", dev_id);
} }
} }
std::string DeviceManager::get_user_last_machine() const std::string DeviceManager::get_user_last_machine() const
{ {
if (Slic3r::GUI::wxGetApp().app_config) { if (AppConfig* config = get_app_config()) {
const auto& user_last_machine = Slic3r::GUI::wxGetApp().app_config->get("user_last_selected_machine"); const auto& user_last_machine = config->get("user_last_selected_machine");
if (!user_last_machine.empty()) { if (!user_last_machine.empty()) {
return user_last_machine; return user_last_machine;
} else if (m_agent) { } else if (m_agent) {
+14 -9
View File
@@ -12,6 +12,7 @@ namespace Slic3r
struct BBLocalMachine; struct BBLocalMachine;
class MachineObject; class MachineObject;
class NetworkAgent; class NetworkAgent;
class AppConfig;
namespace GUI { namespace GUI {
class GUI_App; class GUI_App;
@@ -24,6 +25,7 @@ class DeviceManager
friend class DeviceManagerRefresher; friend class DeviceManagerRefresher;
private: private:
NetworkAgent* m_agent{ nullptr }; NetworkAgent* m_agent{ nullptr };
AppConfig* m_app_config{ nullptr };
DeviceManagerRefresher* m_refresher{ nullptr }; DeviceManagerRefresher* m_refresher{ nullptr };
bool m_enable_mutil_machine = false; bool m_enable_mutil_machine = false;
@@ -35,11 +37,13 @@ private:
std::map<std::string, MachineObject*> userMachineList; /* dev_id -> MachineObject* cloudMachine of User */ std::map<std::string, MachineObject*> userMachineList; /* dev_id -> MachineObject* cloudMachine of User */
public: public:
DeviceManager(NetworkAgent* agent = nullptr); DeviceManager(NetworkAgent* agent = nullptr, bool enable_refresher = true,
AppConfig* app_config = nullptr);
~DeviceManager(); ~DeviceManager();
public: public:
NetworkAgent* get_agent() const { return m_agent; } NetworkAgent* get_agent() const { return m_agent; }
AppConfig* get_app_config() const;
void set_agent(NetworkAgent* agent); void set_agent(NetworkAgent* agent);
void start_refresher(); void start_refresher();
@@ -74,10 +78,9 @@ public:
void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); } void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); }
void clean_user_info(bool keep_local_selection = false); void clean_user_info(bool keep_local_selection = false);
// target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check, // Retain agent-owned LAN discoveries across a switch; the active-agent list filter keeps
// just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the // entries from other agents hidden while allowing them to reappear when switched back.
// live one - this runs before the live agent is repointed. void clear_other_devices();
void clear_other_devices(const std::string& target_agent_id = "");
void load_last_machine(); void load_last_machine();
void update_user_machine_list_info(const std::string& provider); void update_user_machine_list_info(const std::string& provider);
@@ -122,6 +125,7 @@ private:
void keep_alive(); void keep_alive();
void check_pushing(); void check_pushing();
std::string get_current_cloud_provider() const;
void OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state); void OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state);
void OnSelectedMachineChanged(const std::string& pre_dev_id, const std::string& new_dev_id); void OnSelectedMachineChanged(const std::string& pre_dev_id, const std::string& new_dev_id);
@@ -134,14 +138,15 @@ public:
std::string connection_type, std::string bind_state, std::string version, std::string connection_type, std::string bind_state, std::string version,
std::string access_code); std::string access_code);
static void update_local_machine(const MachineObject& m); static void update_local_machine(const MachineObject& m);
static void update_local_machine(const MachineObject& m, AppConfig* config);
}; };
class DeviceManagerRefresher : public wxObject class DeviceManagerRefresher : public wxObject
{ {
wxTimer* m_timer{ nullptr }; wxTimer* m_timer{nullptr};
int m_timer_interval_msec = 5000; int m_timer_interval_msec = 5000;
DeviceManager* m_manager{ nullptr }; DeviceManager* m_manager{nullptr};
public: public:
DeviceManagerRefresher(DeviceManager* manger); DeviceManagerRefresher(DeviceManager* manger);
@@ -154,4 +159,4 @@ public:
protected: protected:
virtual void on_timer(wxTimerEvent& event); virtual void on_timer(wxTimerEvent& event);
}; };
}; };
+213 -96
View File
@@ -4,6 +4,7 @@
#include "I18N.hpp" #include "I18N.hpp"
#include "libslic3r/Time.hpp" #include "libslic3r/Time.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "slic3r/Utils/Http.hpp"
#include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/plugin/PluginManager.hpp" #include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/NetworkAgentFactory.hpp"
@@ -17,6 +18,7 @@
#include "ReleaseNote.hpp" #include "ReleaseNote.hpp"
#include <thread> #include <thread>
#include <mutex> #include <mutex>
#include <charconv>
#include <codecvt> #include <codecvt>
#include <boost/foreach.hpp> #include <boost/foreach.hpp>
#include <boost/typeof/typeof.hpp> #include <boost/typeof/typeof.hpp>
@@ -54,6 +56,7 @@
#include "DeviceCore/DevStatus.h" #include "DeviceCore/DevStatus.h"
#include "DeviceCore/DevUpgrade.h" #include "DeviceCore/DevUpgrade.h"
#include "IPrinterAgent.hpp"
#define CALI_DEBUG #define CALI_DEBUG
#define MINUTE_30 1800000 //ms #define MINUTE_30 1800000 //ms
@@ -374,8 +377,22 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str)
wxString MachineObject::get_printer_type_display_str() const wxString MachineObject::get_printer_type_display_str() const
{ {
std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type);
// Bambu printers use m_resource_file_path + "/printers/" + type_str + ".json", which is a semantic that only works for their profiles.
// For any other profile, we can simply consult preset bundle if the model_id exists.
if (display_name.empty()) {
for (const auto& [vendor_id, vendor] : GUI::wxGetApp().preset_bundle->vendors) {
for (const auto& model : vendor.models) {
if (printer_type == model.model_id)
display_name = model.name;
}
}
}
if (!display_name.empty()) if (!display_name.empty())
return display_name; return display_name;
else if (printer_type == "orcasonar")
return "OrcaSonar Printer";
else else
return _L("Unknown"); return _L("Unknown");
} }
@@ -462,7 +479,7 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
{ {
this->access_code = code; this->access_code = code;
if (only_refresh) { if (only_refresh) {
AppConfig* config = GUI::wxGetApp().app_config; AppConfig* config = m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config;
if (config) { if (config) {
if (is_lan_mode_printer()) { if (is_lan_mode_printer()) {
// why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and // why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and
@@ -475,7 +492,7 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
// fresh from the cloud API's current response, so there's no cross-agent leakage // fresh from the cloud API's current response, so there's no cross-agent leakage
// risk to guard against there. // risk to guard against there.
if (!code.empty()) { if (!code.empty()) {
DeviceManager::update_local_machine(*this); DeviceManager::update_local_machine(*this, config);
} else { } else {
// Only patch an existing record's code - don't persist a brand-new // Only patch an existing record's code - don't persist a brand-new
// never-bound entry just because set_access_code("") was called on it. // never-bound entry just because set_access_code("") was called on it.
@@ -1342,7 +1359,6 @@ int MachineObject::command_get_access_code() {
return this->publish_json(j); return this->publish_json(j);
} }
int MachineObject::command_request_push_all(bool request_now) int MachineObject::command_request_push_all(bool request_now)
{ {
auto curr_time = std::chrono::system_clock::now(); auto curr_time = std::chrono::system_clock::now();
@@ -1474,26 +1490,20 @@ int MachineObject::command_upgrade_module(std::string url, std::string module_ty
int MachineObject::command_xyz_abs() int MachineObject::command_xyz_abs()
{ {
return this->publish_gcode("G90 \n"); if (!m_agent) return -1;
return command_with_dialog(m_agent->command_xyz_abs(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer()));
} }
int MachineObject::command_auto_leveling() int MachineObject::command_auto_leveling()
{ {
return this->publish_gcode("G29 \n"); if (!m_agent) return -1;
return command_with_dialog(m_agent->command_auto_leveling(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer()));
} }
int MachineObject::command_go_home() int MachineObject::command_go_home()
{ {
if (m_support_mqtt_homing) if (!m_agent) return -1;
{ return command_with_dialog(m_agent->command_go_home(get_dev_id(), this->is_in_printing(), m_support_mqtt_homing, MachineObject::m_sequence_id++, is_lan_mode_printer()));
json j;
j["print"]["command"] = "back_to_center";
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
return this->publish_json(j);
}
// gcode command
return this->is_in_printing() ? this->publish_gcode("G28 X\n") : this->publish_gcode("G28 \n");
} }
int MachineObject::command_task_partskip(std::vector<int> part_ids) int MachineObject::command_task_partskip(std::vector<int> part_ids)
@@ -1615,23 +1625,14 @@ int MachineObject::command_stop_buzzer()
int MachineObject::command_set_bed(int temp) int MachineObject::command_set_bed(int temp)
{ {
if (m_support_mqtt_bet_ctrl) if (!m_agent) return -1;
{ return command_with_dialog(m_agent->command_set_bed(get_dev_id(), temp, m_support_mqtt_bet_ctrl, MachineObject::m_sequence_id++, is_lan_mode_printer()));
json j;
j["print"]["command"] = "set_bed_temp";
j["print"]["temp"] = temp;
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
return this->publish_json(j);
}
std::string gcode_str = (boost::format("M140 S%1%\n") % temp).str();
return this->publish_gcode(gcode_str);
} }
int MachineObject::command_set_nozzle(int temp) int MachineObject::command_set_nozzle(int temp)
{ {
std::string gcode_str = (boost::format("M104 S%1%\n") % temp).str(); if (!m_agent) return -1;
return this->publish_gcode(gcode_str); return command_with_dialog(m_agent->command_set_nozzle(get_dev_id(), temp, MachineObject::m_sequence_id++, is_lan_mode_printer()));
} }
int MachineObject::command_set_nozzle_new(int nozzle_id, int temp) int MachineObject::command_set_nozzle_new(int nozzle_id, int temp)
@@ -1736,9 +1737,8 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
int MachineObject::command_ams_calibrate(int ams_id) int MachineObject::command_ams_calibrate(int ams_id)
{ {
std::string gcode_cmd = (boost::format("M620 C%1% \n") % ams_id).str(); if (!m_agent) return -1;
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
return this->publish_gcode(gcode_cmd);
} }
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max) int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
@@ -1774,29 +1774,23 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
return this->publish_json(j); return this->publish_json(j);
} }
int MachineObject::command_ams_refresh_rfid(std::string tray_id) int MachineObject::command_ams_refresh_rfid(int ams_id, int slot_id)
{ {
std::string gcode_cmd = (boost::format("M620 R%1% \n") % tray_id).str(); if (!m_agent) return -1;
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; return command_with_dialog(m_agent->command_ams_refresh_rfid(get_dev_id(), ams_id, slot_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
return this->publish_gcode(gcode_cmd);
} }
int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id) int MachineObject::command_start_camera()
{ {
json j; if (!m_agent) return -1;
j["print"]["command"] = "ams_get_rfid"; return m_agent->command_start_camera(get_dev_id());
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
j["print"]["ams_id"] = ams_id;
j["print"]["slot_id"] = slot_id;
return this->publish_json(j);
} }
int MachineObject::command_ams_select_tray(std::string tray_id) int MachineObject::command_ams_select_tray(std::string tray_id)
{ {
std::string gcode_cmd = (boost::format("M620 P%1% \n") % tray_id).str(); if (!m_agent) return -1;
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; return command_with_dialog(m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
return this->publish_gcode(gcode_cmd);
} }
int MachineObject::command_ams_control(std::string action) int MachineObject::command_ams_control(std::string action)
@@ -1955,47 +1949,10 @@ int MachineObject::command_ams_air_print_detect(bool air_print_detect)
int MachineObject::command_axis_control(std::string axis, double unit, double input_val, int speed) int MachineObject::command_axis_control(std::string axis, double unit, double input_val, int speed)
{ {
if (m_support_mqtt_axis_control) if (!m_agent) return -1;
{ return command_with_dialog(m_agent->command_axis_control(get_dev_id(), axis, unit, input_val, speed, is_core_xy(),
int dir = input_val > 0 ? 1 : -1; m_support_mqtt_axis_control, MachineObject::m_sequence_id++,
// i3-arch printers move the bed for Y/Z, so the on-screen direction is is_lan_mode_printer()));
// reversed — same negation the g-code fallback below applies.
if (!is_core_xy() && (axis.compare("Y") == 0 || axis.compare("Z") == 0)) {
dir = -dir;
}
json j;
j["print"]["command"] = "xyz_ctrl";
j["print"]["axis"] = axis;
j["print"]["dir"] = dir;
j["print"]["mode"] = (std::abs(input_val) >= 10) ? 1 : 0;
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
return this->publish_json(j);
}
double value = input_val;
if (!is_core_xy()) {
if ( axis.compare("Y") == 0
|| axis.compare("Z") == 0) {
value = -1.0 * input_val;
}
}
char cmd[256];
if (axis.compare("X") == 0
|| axis.compare("Y") == 0
|| axis.compare("Z") == 0) {
sprintf(cmd, "M211 S \nM211 X1 Y1 Z1\nM1002 push_ref_mode\nG91 \nG1 %s%0.1f F%d\nM1002 pop_ref_mode\nM211 R\n", axis.c_str(), value * unit, speed);
}
else if (axis.compare("E") == 0) {
sprintf(cmd, "M83 \nG0 %s%0.1f F%d\n", axis.c_str(), value * unit, speed);
}
else {
return -1;
}
return this->publish_gcode(cmd);
} }
int MachineObject::command_extruder_control(int nozzle_id, double val) int MachineObject::command_extruder_control(int nozzle_id, double val)
@@ -2620,7 +2577,12 @@ void MachineObject::reset()
vt_slot.erase(vt_slot.begin() + 1); vt_slot.erase(vt_slot.begin() + 1);
} }
} }
subtask_ = nullptr; // why: reset reuses MachineObject, so release its lazy subtask
// before dropping the pointer to prevent reconnect leaks.
if (subtask_) {
delete subtask_;
subtask_ = nullptr;
}
has_extra_flow_type = false; has_extra_flow_type = false;
m_partskip_ids.clear(); m_partskip_ids.clear();
} }
@@ -2638,15 +2600,63 @@ void MachineObject::set_print_state(std::string status)
} }
} }
int MachineObject::connect(bool use_openssl) // why: printer agents can report progress without BBL cloud task identity.
void MachineObject::update_print_progress(const json& value)
{
if (value.is_string()) {
const std::string progress = value.get<std::string>();
int parsed_progress;
const auto result = std::from_chars(progress.data(), progress.data() + progress.size(), parsed_progress);
if (result.ec != std::errc{} || result.ptr != progress.data() + progress.size())
return;
mc_print_percent = parsed_progress;
} else if (value.is_number_integer())
mc_print_percent = value.get<int>();
else
return;
if (BBLSubTask* curr_task = get_subtask())
curr_task->task_progress = mc_print_percent;
}
int MachineObject::connect()
{ {
if (get_dev_ip().empty()) return -1; if (get_dev_ip().empty()) return -1;
std::string username = "bblp"; std::string username = m_agent ? m_agent->default_lan_username() : std::string();
std::string password = get_access_code(); std::string password = get_access_code();
std::string port;
std::string input = get_dev_ip();
const bool use_ssl = input.rfind("https", 0) == 0;
// This strips out the http/https prefix
std::string host = Http::get_host_from_url(input, &port);
std::string ca_file;
if (GUI::wxGetApp().preset_bundle) {
const auto& config = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config;
if (port.empty())
port = config.opt_string("printhost_port");
ca_file = config.opt_string("printhost_cafile");
}
if (host.empty())
host = get_dev_ip();
if (m_agent) { if (m_agent) {
try { try {
return m_agent->connect_printer(get_dev_id(), get_dev_ip(), username, password, use_openssl); PrinterConnectionParams params{
get_dev_id(),
host,
port,
username,
password,
use_ssl,
ca_file
};
return m_agent->connect_printer(params);
} catch (...) { } catch (...) {
; ;
} }
@@ -2762,6 +2772,14 @@ int MachineObject::publish_json(const json& json_item, int qos, int flag)
BOOST_LOG_TRIVIAL(error) << "publish_json: " << json_item.dump() << " code: " << rtn; BOOST_LOG_TRIVIAL(error) << "publish_json: " << json_item.dump() << " code: " << rtn;
} }
// why: the agent is the only thing that knows what it can translate, so it reports
// not-supported in its return value and this - the single funnel every command_* builder
// passes through - is the one place that turns it into something the user sees. No list of
// unsupported commands is needed anywhere: an agent that has no case for a command says so.
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) {
show_unsupported_dlg(rtn);
}
return rtn; return rtn;
} }
@@ -3080,6 +3098,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
} }
} catch (...) {} } catch (...) {}
try {
if (j.contains("info"))
parse_new_info2(j["info"]);
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "parse_json: failed to parse OrcaSonar capability info";
}
try { try {
if (auto ptr = m_fila_system->GetAmsFirmwareSwitch().lock()) { if (auto ptr = m_fila_system->GetAmsFirmwareSwitch().lock()) {
ptr->ParseFirmwareSwitch(j); ptr->ParseFirmwareSwitch(j);
@@ -3332,10 +3357,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
print_type = jj["print_type"].get<std::string>(); print_type = jj["print_type"].get<std::string>();
} }
if (jj.contains("mc_percent")) { if (jj.contains("mc_percent")) {
if (jj["mc_percent"].is_string()) update_print_progress(jj["mc_percent"]);
mc_print_percent = stoi(j["print"]["mc_percent"].get<std::string>());
else if (jj["mc_percent"].is_number_integer())
mc_print_percent = j["print"]["mc_percent"].get<int>();
} }
if (jj.contains("mc_print_sub_stage")) { if (jj.contains("mc_print_sub_stage")) {
if (jj["mc_print_sub_stage"].is_number_integer()) if (jj["mc_print_sub_stage"].is_number_integer())
@@ -3505,6 +3527,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
this->task_id_ = jj["task_id"].get<std::string>(); this->task_id_ = jj["task_id"].get<std::string>();
} }
if (jj.contains("thumbnail_url") && jj["thumbnail_url"].is_string())
m_agent_thumbnail_url = jj["thumbnail_url"].get<std::string>();
if (jj.contains("job_attr")) { if (jj.contains("job_attr")) {
int jobAttr = jj["job_attr"].get<int>(); int jobAttr = jj["job_attr"].get<int>();
jobState_ = get_flag_bits(jobAttr, 4, 4); jobState_ = get_flag_bits(jobAttr, 4, 4);
@@ -3550,7 +3575,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
update_slice_info(jj["project_id"].get<std::string>(), jj["profile_id"].get<std::string>(), jj["subtask_id"].get<std::string>(), plate_index); update_slice_info(jj["project_id"].get<std::string>(), jj["profile_id"].get<std::string>(), jj["subtask_id"].get<std::string>(), plate_index);
BBLSubTask* curr_task = get_subtask(); BBLSubTask* curr_task = get_subtask();
if (curr_task) { if (curr_task) {
curr_task->task_progress = mc_print_percent;
curr_task->printing_status = print_status; curr_task->printing_status = print_status;
curr_task->task_id = jj["subtask_id"].get<std::string>(); curr_task->task_id = jj["subtask_id"].get<std::string>();
} }
@@ -3853,6 +3877,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
has_ipcam = true; has_ipcam = true;
} else { } else {
has_ipcam = false; has_ipcam = false;
webcam_stream_url.clear();
} }
} }
if (ipcam.contains("resolution")) { if (ipcam.contains("resolution")) {
@@ -3887,6 +3912,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
liveview_local = local_rtsp_url.empty() ? LVL_None : local_rtsp_url == "disable" liveview_local = local_rtsp_url.empty() ? LVL_None : local_rtsp_url == "disable"
? LVL_Disable : boost::algorithm::starts_with(local_rtsp_url, "rtsps") ? LVL_Rtsps : LVL_Rtsp; ? LVL_Disable : boost::algorithm::starts_with(local_rtsp_url, "rtsps") ? LVL_Rtsps : LVL_Rtsp;
} }
if (ipcam.contains("stream_url") && ipcam["stream_url"].is_string()) {
webcam_stream_url = ipcam["stream_url"].get<std::string>();
}
if (ipcam.contains("tutk_server")) { if (ipcam.contains("tutk_server")) {
tutk_state = ipcam["tutk_server"].get<std::string>(); tutk_state = ipcam["tutk_server"].get<std::string>();
} }
@@ -4645,7 +4673,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
if (diff.count() > 10.0f) { if (diff.count() > 10.0f) {
BOOST_LOG_TRIVIAL(trace) << "parse_json timeout = " << diff.count(); BOOST_LOG_TRIVIAL(trace) << "parse_json timeout = " << diff.count();
} }
DeviceManager::update_local_machine(*this); DeviceManager::update_local_machine(*this, m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config);
return 0; return 0;
} }
@@ -5453,6 +5481,86 @@ void MachineObject::parse_new_info(json print)
} }
} }
void MachineObject::parse_new_info2(const json& info)
{
if (!info.is_object() || info.value("command", "") != "get_capabilities")
return;
const auto capabilities_it = info.find("capabilities");
if (capabilities_it == info.end() || !capabilities_it->is_object())
return;
const auto flags_it = capabilities_it->find("flags");
if (flags_it == capabilities_it->end() || !flags_it->is_object())
return;
const json& flags = *flags_it;
BOOST_LOG_TRIVIAL(info) << "parse_new_info2: OrcaSonar capability flags=" << flags.dump();
auto parse_bool = [&flags](const char* name, bool& target) {
const auto it = flags.find(name);
if (it != flags.end() && it->is_boolean())
target = it->get<bool>();
};
parse_bool("support_send_to_sd", is_support_send_to_sdcard);
parse_bool("support_filament_backup", is_support_filament_backup);
parse_bool("support_update_remain", is_support_update_remain);
parse_bool("support_auto_recovery_step_loss", is_support_auto_recovery_step_loss);
parse_bool("support_ams_humidity", is_support_ams_humidity);
parse_bool("support_prompt_sound", is_support_prompt_sound);
parse_bool("support_filament_tangle_detect", is_support_filament_tangle_detect);
parse_bool("support_1080dpi", is_support_1080dpi);
parse_bool("support_cloud_print_only", is_support_cloud_print_only);
parse_bool("support_command_ams_switch", is_support_command_ams_switch);
parse_bool("support_mqtt_alive", is_support_mqtt_alive);
parse_bool("support_motor_noise_cali", is_support_motor_noise_cali);
parse_bool("support_timelapse", is_support_timelapse);
parse_bool("support_user_preset", is_support_user_preset);
parse_bool("support_refresh_nozzle", is_support_refresh_nozzle);
parse_bool("support_flow_calibration", is_support_flow_calibration);
parse_bool("support_build_plate_marker_detect", is_support_build_plate_marker_detect);
parse_bool("support_nozzle_blob_detect", is_support_nozzle_blob_detection);
if (!m_manager->IsMultiMachineEnabled() && !is_support_agora)
parse_bool("support_tunnel_mqtt", is_support_tunnel_mqtt);
const auto bed_leveling_it = flags.find("support_bed_leveling");
if (bed_leveling_it != flags.end() && bed_leveling_it->is_number_integer())
is_support_bed_leveling = bed_leveling_it->get<int>();
auto copy_bool = [&flags](json& target, const char* name) {
const auto it = flags.find(name);
if (it != flags.end() && it->is_boolean())
target[name] = *it;
};
// The capability manifest uses an object for this range, while the legacy
// DeviceCore parser consumes a boolean plus a two-element range array.
json device_config;
copy_bool(device_config, "support_chamber");
copy_bool(device_config, "support_first_layer_inspect");
copy_bool(device_config, "support_ai_monitoring");
copy_bool(device_config, "support_lidar_calibration");
const auto chamber_edit_it = flags.find("support_chamber_temp_edit");
if (chamber_edit_it != flags.end() && chamber_edit_it->is_boolean()) {
device_config["support_chamber_temp_edit"] = *chamber_edit_it;
} else if (chamber_edit_it != flags.end() && chamber_edit_it->is_object()) {
const auto min_it = chamber_edit_it->find("min");
const auto max_it = chamber_edit_it->find("max");
if (min_it != chamber_edit_it->end() && max_it != chamber_edit_it->end() && min_it->is_number() && max_it->is_number()) {
device_config["support_chamber_temp_edit"] = true;
device_config["support_chamber_temp_edit_range"] = {*min_it, *max_it};
}
}
json fan_config;
copy_bool(fan_config, "support_aux_fan");
copy_bool(fan_config, "support_chamber_fan");
m_config->ParseConfig(device_config);
m_fan->ParseV2_0(fan_config);
}
static bool is_hex_digit(char c) { static bool is_hex_digit(char c) {
return std::isxdigit(static_cast<unsigned char>(c)) != 0; return std::isxdigit(static_cast<unsigned char>(c)) != 0;
} }
@@ -5958,6 +6066,15 @@ bool MachineObject::HasAms() const
return m_fila_system->HasAms(); return m_fila_system->HasAms();
} }
int MachineObject::command_with_dialog(int cmd_result)
{
if (!m_agent)
return -1;
if (cmd_result == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || cmd_result == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(cmd_result);
return cmd_result;
}
void change_the_opacity(wxColour& colour) void change_the_opacity(wxColour& colour)
{ {
if (colour.Alpha() == 255) { if (colour.Alpha() == 255) {
+14 -28
View File
@@ -18,6 +18,7 @@
#include "boost/bimap/bimap.hpp" #include "boost/bimap/bimap.hpp"
#include "libslic3r/calib.hpp" #include "libslic3r/calib.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "slic3r/Utils/PrinterNetworkTypes.hpp"
#include "DeviceCore/DevDefs.h" #include "DeviceCore/DevDefs.h"
#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevConfigUtil.h"
@@ -100,7 +101,6 @@ struct DevPrintTaskRatingInfo;
// given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware. // given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware.
bool is_stringing_prone_filament(const std::string& filament_id, float nozzle_diameter); bool is_stringing_prone_filament(const std::string& filament_id, float nozzle_diameter);
class MachineObject class MachineObject
{ {
private: private:
@@ -547,30 +547,12 @@ public:
bool xcam_first_layer_inspector { false }; bool xcam_first_layer_inspector { false };
time_t xcam_first_layer_hold_start = 0; time_t xcam_first_layer_hold_start = 0;
std::string local_rtsp_url; std::string local_rtsp_url;
std::string webcam_stream_url;
std::string tutk_state; std::string tutk_state;
enum LiveviewLocal { LiveviewLocal liveview_local{ LiveviewLocal::LVL_None };
LVL_None, LiveviewRemote liveview_remote{ LiveviewRemote::LVR_None};
LVL_Disable, FileLocal file_local{ FileLocal::FL_None };
LVL_Local, FileRemote file_remote{ FileRemote::FR_None };
LVL_Rtsps,
LVL_Rtsp
} liveview_local{ LVL_None };
enum LiveviewRemote {
LVR_None,
LVR_Tutk,
LVR_Agora,
LVR_TutkAgora
} liveview_remote{ LVR_None };
enum FileLocal {
FL_None,
FL_Local
} file_local{ FL_None };
enum FileRemote {
FR_None,
FR_Tutk,
FR_Agora,
FR_TutkAgora
} file_remote{ FR_None };
enum PlateMakerDectect : int enum PlateMakerDectect : int
{ {
@@ -696,7 +678,7 @@ public:
/* machine mqtt apis */ /* machine mqtt apis */
int connect(bool use_openssl = true); int connect();
int disconnect(); int disconnect();
json_diff print_json; json_diff print_json;
@@ -708,6 +690,8 @@ public:
std::string subtask_id_; std::string subtask_id_;
std::string job_id_; std::string job_id_;
std::string last_subtask_id_; std::string last_subtask_id_;
// note: printer-agent-supplied thumbnail url, empty when the agent supplies none.
std::string m_agent_thumbnail_url;
BBLSliceInfo* slice_info {nullptr}; BBLSliceInfo* slice_info {nullptr};
boost::thread* get_slice_info_thread { nullptr }; boost::thread* get_slice_info_thread { nullptr };
boost::thread* get_model_task_thread { nullptr }; boost::thread* get_model_task_thread { nullptr };
@@ -765,6 +749,7 @@ public:
int command_set_printer_nozzle(std::string nozzle_type, float diameter); int command_set_printer_nozzle(std::string nozzle_type, float diameter);
int command_set_printer_nozzle2(int id, std::string nozzle_type, float diameter); int command_set_printer_nozzle2(int id, std::string nozzle_type, float diameter);
int command_get_access_code(); int command_get_access_code();
int command_start_camera();
int command_ack_proceed(json& proceed); int command_ack_proceed(json& proceed);
int command_purification_disable(); int command_purification_disable();
int command_dont_remind_next_time(json& mqtt_guard_json); int command_dont_remind_next_time(json& mqtt_guard_json);
@@ -811,8 +796,7 @@ public:
int command_ams_calibrate(int ams_id); int command_ams_calibrate(int ams_id);
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max); int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max);
int command_ams_select_tray(std::string tray_id); int command_ams_select_tray(std::string tray_id);
int command_ams_refresh_rfid(std::string tray_id); int command_ams_refresh_rfid(int ams_id, int slot_id);
int command_ams_refresh_rfid2(int ams_id, int slot_id);
int command_ams_control(std::string action); int command_ams_control(std::string action);
int command_ams_drying_stop(); int command_ams_drying_stop();
int command_start_extrusion_cali(int tray_index, int nozzle_temp, int bed_temp, float max_volumetric_speed, std::string setting_id = ""); int command_start_extrusion_cali(int tray_index, int nozzle_temp, int bed_temp, float max_volumetric_speed, std::string setting_id = "");
@@ -894,6 +878,7 @@ public:
static bool is_in_printing_status(std::string status); static bool is_in_printing_status(std::string status);
void set_print_state(std::string status); void set_print_state(std::string status);
void update_print_progress(const json& value);
bool is_connected(); bool is_connected();
bool is_connecting(); bool is_connecting();
@@ -962,6 +947,7 @@ public:
/*for parse new info*/ /*for parse new info*/
bool check_enable_np(const json& print) const; bool check_enable_np(const json& print) const;
void parse_new_info(json print); void parse_new_info(json print);
void parse_new_info2(const json& info);
int get_flag_bits(std::string str, int start, int count = 1) const; int get_flag_bits(std::string str, int start, int count = 1) const;
uint32_t get_flag_bits_no_border(std::string str, int start_idx, int count = 1) const; uint32_t get_flag_bits_no_border(std::string str, int start_idx, int count = 1) const;
int get_flag_bits(int num, int start, int count = 1, int base = 10) const; int get_flag_bits(int num, int start, int count = 1, int base = 10) const;
@@ -990,7 +976,7 @@ public:
void command_set_save_remote_print_file_to_storage(bool save); void command_set_save_remote_print_file_to_storage(bool save);
private: private:
int command_with_dialog(int cmd_result);
/* xcam door open check*/ /* xcam door open check*/
bool is_support_door_open_check = false; bool is_support_door_open_check = false;
DoorOpenCheckState xcam_door_open_check = DoorOpenCheckState::DOOR_OPEN_CHECK_DISABLE; DoorOpenCheckState xcam_door_open_check = DoorOpenCheckState::DOOR_OPEN_CHECK_DISABLE;
+34 -47
View File
@@ -4,6 +4,7 @@
#include "libslic3r/Platform.hpp" #include "libslic3r/Platform.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "Shortcuts.hpp" #include "Shortcuts.hpp"
#include "DeviceCore/DevConfigUtil.h"
#include "BindDialog.hpp" #include "BindDialog.hpp"
#include "DeviceManager.hpp" #include "DeviceManager.hpp"
#include "HMS.hpp" #include "HMS.hpp"
@@ -24,7 +25,6 @@
#include <boost/locale/encoding_utf.hpp> #include <boost/locale/encoding_utf.hpp>
#include <boost/log/detail/native_typeof.hpp> #include <boost/log/detail/native_typeof.hpp>
#include <libslic3r/Config.hpp> #include <libslic3r/Config.hpp>
#include <mutex>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/event.h> #include <wx/event.h>
@@ -41,9 +41,11 @@
#include <iterator> #include <iterator>
#include <exception> #include <exception>
#include <cstdlib> #include <cstdlib>
#include <mutex>
#include <regex> #include <regex>
#include <thread> #include <thread>
#include <string_view> #include <string_view>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
#include <boost/format.hpp> #include <boost/format.hpp>
@@ -88,7 +90,6 @@
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "libslic3r/miniz_extension.hpp" #include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "libslic3r/Color.hpp"
#include "slic3r/plugin/PluginManager.hpp" #include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/host/PluginHostUi.hpp" #include "slic3r/plugin/host/PluginHostUi.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp" #include "slic3r/plugin/PythonInterpreter.hpp"
@@ -108,19 +109,18 @@
#include "../Utils/PrintHost.hpp" #include "../Utils/PrintHost.hpp"
#include "../Utils/Process.hpp" #include "../Utils/Process.hpp"
#include "../Utils/wxInspectorPlugins/Registration.hpp" #include "../Utils/wxInspectorPlugins/Registration.hpp"
#include "../Utils/MacDarkMode.hpp"
#include "../Utils/Http.hpp" #include "../Utils/Http.hpp"
#include "../Utils/InstanceID.hpp" #include "../Utils/InstanceID.hpp"
#include "../Utils/UndoRedo.hpp" #include "../Utils/UndoRedo.hpp"
#include "slic3r/Config/Snapshot.hpp" #include "slic3r/Config/Snapshot.hpp"
#include "Preferences.hpp" #include "Preferences.hpp"
#include "Tab.hpp" #include "Tab.hpp"
#include "SysInfoDialog.hpp"
#include "UpdateDialogs.hpp" #include "UpdateDialogs.hpp"
#include "Mouse3DController.hpp" #include "Mouse3DController.hpp"
#include "RemovableDriveManager.hpp" #include "RemovableDriveManager.hpp"
#include "InstanceCheck.hpp" #include "InstanceCheck.hpp"
#ifdef __APPLE__ #ifdef __APPLE__
#include "../Utils/MacDarkMode.hpp"
#include "DeepLinkHandlerMac.h" #include "DeepLinkHandlerMac.h"
#endif #endif
#include "NotificationManager.hpp" #include "NotificationManager.hpp"
@@ -129,8 +129,6 @@
#include "PrintHostDialogs.hpp" #include "PrintHostDialogs.hpp"
#include "NetworkPluginDialog.hpp" #include "NetworkPluginDialog.hpp"
#include "DesktopIntegrationDialog.hpp" #include "DesktopIntegrationDialog.hpp"
#include "SendSystemInfoDialog.hpp"
#include "ParamsDialog.hpp"
#include "KBShortcutsDialog.hpp" #include "KBShortcutsDialog.hpp"
#include "DownloadProgressDialog.hpp" #include "DownloadProgressDialog.hpp"
#include "TroubleshootDialog.hpp" #include "TroubleshootDialog.hpp"
@@ -141,7 +139,6 @@
#include "Widgets/ProgressDialog.hpp" #include "Widgets/ProgressDialog.hpp"
//BBS: DailyTip and UserGuide Dialog //BBS: DailyTip and UserGuide Dialog
#include "WebDownPluginDlg.hpp"
#include "WebGuideDialog.hpp" #include "WebGuideDialog.hpp"
#include "ReleaseNote.hpp" #include "ReleaseNote.hpp"
#include "PrivacyUpdateDialog.hpp" #include "PrivacyUpdateDialog.hpp"
@@ -2394,34 +2391,23 @@ GUI_App::~GUI_App()
bool GUI_App::is_blocking_printing(MachineObject *obj_) bool GUI_App::is_blocking_printing(MachineObject *obj_)
{ {
DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
std::string target_model;
if (obj_ == nullptr) {
obj_ = dev->get_selected_machine();
if (obj_) {
target_model = obj_->printer_type;
}
} else {
target_model = obj_->printer_type;
}
if (!obj_)
{
return false;
}
PresetBundle *preset_bundle = wxGetApp().preset_bundle; PresetBundle *preset_bundle = wxGetApp().preset_bundle;
std::string source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); const std::string source_model = preset_bundle
? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle)
: std::string();
return is_blocking_printing(obj_, source_model);
}
if (source_model != target_model) { bool GUI_App::is_blocking_printing(MachineObject *obj_, const std::string& source_model)
std::vector<std::string> compatible_machine = obj_->get_compatible_machine(); {
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); DeviceManager *dev = getDeviceManager();
if (it == compatible_machine.end()) { if (!dev) return true;
return true; if (obj_ == nullptr)
} obj_ = dev->get_selected_machine();
} if (!obj_)
return false; return false;
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
} }
// If formatted for github, plaintext with OpenGL extensions enclosed into <details>. // If formatted for github, plaintext with OpenGL extensions enclosed into <details>.
@@ -3976,20 +3962,16 @@ void GUI_App::set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent)
m_agent->set_user_selected_machine(""); m_agent->set_user_selected_machine("");
// note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer) // note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer)
dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS
// why: drop stale LAN discoveries; keep My Devices, but only those belonging to the // why: retain agent-owned LAN discoveries so agents without automatic discovery (for
// agent we're about to swap to, so a device stamped by the outgoing agent doesn't // example the Moonraker-based Qidi/Snapmaker agents) can reuse them after a switch.
// linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh. dev->clear_other_devices();
// agent is null when clearing the live agent entirely (e.g. plugin unload); there's no
// target to filter against then, so fall back to the original "keep all My Devices"
// behavior rather than guessing.
dev->clear_other_devices(agent ? agent->get_agent_info().id : std::string());
} }
m_agent->set_printer_agent(agent); m_agent->set_printer_agent(agent);
sidebar().update_all_preset_comboboxes(); sidebar().update_all_preset_comboboxes();
} }
std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id) const
{ {
if (!stored_id.empty()) if (!stored_id.empty())
return stored_id; return stored_id;
@@ -4025,6 +4007,7 @@ void GUI_App::switch_printer_agent()
std::string log_dir = data_dir(); std::string log_dir = data_dir();
std::string cloud_agent_id = agent_info.id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER; std::string cloud_agent_id = agent_info.id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << agent_info.id;
std::shared_ptr<ICloudServiceAgent> cloud_agent = m_agent->get_cloud_agent(cloud_agent_id); std::shared_ptr<ICloudServiceAgent> cloud_agent = m_agent->get_cloud_agent(cloud_agent_id);
// Create new printer agent via registry // Create new printer agent via registry
@@ -4038,8 +4021,10 @@ void GUI_App::switch_printer_agent()
return; return;
} }
// The factory caches agents per ID, so an identical pointer means the agent type is unchanged. // Compare the registered IDs, not only the implementation pointer. Different registry IDs
if (m_agent->get_printer_agent() == new_printer_agent) { // may intentionally be backed by the same implementation object (especially for plugins).
const auto current_printer_agent = m_agent->get_printer_agent();
if (current_printer_agent && current_printer_agent->get_agent_info().id == effective_agent_id) {
// Orca: the agent type is unchanged (e.g. switching between two Moonraker/Klipper // Orca: the agent type is unchanged (e.g. switching between two Moonraker/Klipper
// printer presets), so the selected machine and the agent's cached device_info still // printer presets), so the selected machine and the agent's cached device_info still
// point at the previously active printer preset. Re-select the machine when the new // point at the previously active printer preset. Re-select the machine when the new
@@ -5020,14 +5005,16 @@ bool GUI_App::is_user_login(const std::string& provider/* = ORCA_CLOUD_PROVIDER*
return false; return false;
} }
const std::string& GUI_App::get_printer_cloud_provider() const std::string GUI_App::get_printer_cloud_provider() const
{ {
// Orca todo: this need to be revisted. currently it is mainly used for device manager and related clausses and only bambu machines use them. const std::string agent_id = resolve_printer_agent_id(
// preset_bundle ? preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent")
return BBL_CLOUD_PROVIDER; : std::string());
return agent_id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER;
} }
bool GUI_App::check_login(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/) bool GUI_App::check_login(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/)
{ {
bool result = false; bool result = false;
+3 -5
View File
@@ -23,9 +23,6 @@
#include <wx/snglinst.h> #include <wx/snglinst.h>
#include <wx/msgdlg.h> #include <wx/msgdlg.h>
#include <mutex>
#include <stack>
//#define BBL_HAS_FIRST_PAGE 1 //#define BBL_HAS_FIRST_PAGE 1
#define STUDIO_INACTIVE_TIMEOUT 15*60*1000 #define STUDIO_INACTIVE_TIMEOUT 15*60*1000
#define LOG_FILES_MAX_NUM 30 #define LOG_FILES_MAX_NUM 30
@@ -371,6 +368,7 @@ public:
EAppMode get_app_mode() const { return m_app_mode; } EAppMode get_app_mode() const { return m_app_mode; }
Slic3r::DeviceManager* getDeviceManager() { return m_device_manager; } Slic3r::DeviceManager* getDeviceManager() { return m_device_manager; }
bool is_blocking_printing(MachineObject *obj_ = nullptr); bool is_blocking_printing(MachineObject *obj_ = nullptr);
bool is_blocking_printing(MachineObject *obj_, const std::string& source_model);
Slic3r::TaskManager* getTaskManager() { return m_task_manager; } Slic3r::TaskManager* getTaskManager() { return m_task_manager; }
HMSQuery* get_hms_query() { return hms_query; } HMSQuery* get_hms_query() { return hms_query; }
NetworkAgent* getAgent() { return m_agent; } NetworkAgent* getAgent() { return m_agent; }
@@ -378,7 +376,7 @@ public:
// Reconcile the live printer agent with the stored preset selection. // Reconcile the live printer agent with the stored preset selection.
void switch_printer_agent(); void switch_printer_agent();
std::string resolve_printer_agent_id(const std::string& stored_id); std::string resolve_printer_agent_id(const std::string& stored_id) const;
// ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id // ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id
// then, all resolve and canonical would just be ORCA<->"" // then, all resolve and canonical would just be ORCA<->""
std::string canonical_printer_agent_id(const std::string& picked_id); std::string canonical_printer_agent_id(const std::string& picked_id);
@@ -504,7 +502,7 @@ public:
bool check_login(const std::string& provider = ORCA_CLOUD_PROVIDER); bool check_login(const std::string& provider = ORCA_CLOUD_PROVIDER);
void get_login_info(const std::string& provider = ORCA_CLOUD_PROVIDER); void get_login_info(const std::string& provider = ORCA_CLOUD_PROVIDER);
bool is_user_login(const std::string& provider = ORCA_CLOUD_PROVIDER); bool is_user_login(const std::string& provider = ORCA_CLOUD_PROVIDER);
const std::string& get_printer_cloud_provider() const; std::string get_printer_cloud_provider() const;
void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER); void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER); void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <wx/mediactrl.h>
#include <wx/uri.h>
namespace Slic3r { namespace GUI {
class IMediaController
{
public:
virtual ~IMediaController() = default;
virtual void Load(wxURI url) = 0;
virtual void Play() = 0;
virtual void Stop() = 0;
virtual wxMediaState GetState() { return wxMediaState{}; }
virtual int GetLastError() const { return {}; };
virtual wxSize GetVideoSize() const { return {}; };
};
}} // namespace Slic3r::GUI
+2 -1
View File
@@ -15,6 +15,7 @@
#include "slic3r/Utils/FileTransferUtils.hpp" #include "slic3r/Utils/FileTransferUtils.hpp"
#include "slic3r/Utils/BBLNetworkPlugin.hpp" #include "slic3r/Utils/BBLNetworkPlugin.hpp"
#include "NetworkAgent.hpp"
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
@@ -216,7 +217,7 @@ void PrintJob::process(Ctl &ctl)
params.dev_ip = m_dev_ip; params.dev_ip = m_dev_ip;
params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; params.use_ssl_for_ftp = m_local_use_ssl_for_ftp;
params.use_ssl_for_mqtt = m_local_use_ssl; params.use_ssl_for_mqtt = m_local_use_ssl;
params.username = "bblp"; params.username = m_agent->default_lan_username();
params.password = m_access_code; params.password = m_access_code;
// check access code and ip address // check access code and ip address
+2 -2
View File
@@ -127,7 +127,7 @@ void SendJob::process(Ctl &ctl)
if (m_is_check_mode) { if (m_is_check_mode) {
PrintParams verify_params; PrintParams verify_params;
verify_params.dev_ip = m_dev_ip; verify_params.dev_ip = m_dev_ip;
verify_params.username = "bblp"; verify_params.username = agent->default_lan_username();
verify_params.password = m_access_code; verify_params.password = m_access_code;
verify_params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; verify_params.use_ssl_for_ftp = m_local_use_ssl_for_ftp;
verify_params.use_ssl_for_mqtt = m_local_use_ssl; verify_params.use_ssl_for_mqtt = m_local_use_ssl;
@@ -219,7 +219,7 @@ void SendJob::process(Ctl &ctl)
// local print access // local print access
params.dev_ip = m_dev_ip; params.dev_ip = m_dev_ip;
params.username = "bblp"; params.username = agent->default_lan_username();
params.password = m_access_code; params.password = m_access_code;
params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; params.use_ssl_for_ftp = m_local_use_ssl_for_ftp;
params.use_ssl_for_mqtt = m_local_use_ssl; params.use_ssl_for_mqtt = m_local_use_ssl;
+1 -1
View File
@@ -4385,7 +4385,7 @@ void MainFrame::load_printer_url()
if (auto *device_manager = wxGetApp().getDeviceManager()) { if (auto *device_manager = wxGetApp().getDeviceManager()) {
auto *machine = device_manager->get_selected_machine(); auto *machine = device_manager->get_selected_machine();
if (!machine) { if (!machine) {
auto machines = device_manager->get_my_machine_list(); auto machines = device_manager->get_my_machine_list(device_manager->get_current_printer_agent_id());
if (machines.size() == 1) if (machines.size() == 1)
machine = machines.begin()->second; machine = machines.begin()->second;
} }
+328 -80
View File
@@ -1,4 +1,6 @@
#include "MediaPlayCtrl.h" #include "MediaPlayCtrl.h"
#include "WebMediaController.hpp"
#include "IPrinterAgent.hpp"
#include "Widgets/Button.hpp" #include "Widgets/Button.hpp"
#include "Widgets/CheckBox.hpp" #include "Widgets/CheckBox.hpp"
#include "Widgets/Label.hpp" #include "Widgets/Label.hpp"
@@ -7,6 +9,7 @@
#include "DeviceManager.hpp" #include "DeviceManager.hpp"
#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevConfigUtil.h"
#include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include "I18N.hpp" #include "I18N.hpp"
@@ -15,11 +18,15 @@
#include "slic3r/Utils/BBLNetworkPlugin.hpp" #include "slic3r/Utils/BBLNetworkPlugin.hpp"
#include <algorithm>
#include <boost/lexical_cast.hpp> #include <boost/lexical_cast.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/nowide/cstdio.hpp> #include <boost/nowide/cstdio.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include <boost/nowide/utf8_codecvt.hpp> #include <boost/nowide/utf8_codecvt.hpp>
#include <slic3r/GUI/DeviceManager.hpp>
#include <wx/mediactrl.h>
#undef pid_t #undef pid_t
#include <boost/process.hpp> #include <boost/process.hpp>
#ifdef __WIN32__ #ifdef __WIN32__
@@ -48,6 +55,7 @@ namespace GUI {
MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const wxPoint &pos, const wxSize &size) MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const wxPoint &pos, const wxSize &size)
: wxPanel(parent, wxID_ANY, pos, size) : wxPanel(parent, wxID_ANY, pos, size)
, m_media_ctrl(media_ctrl) , m_media_ctrl(media_ctrl)
, m_active_media_controller(media_ctrl)
{ {
SetLabel("MediaPlayCtrl"); SetLabel("MediaPlayCtrl");
SetBackgroundColour(*wxWHITE); SetBackgroundColour(*wxWHITE);
@@ -94,7 +102,10 @@ MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const w
}); });
m_button_play->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &e) { TogglePlay(); }); m_button_play->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &e) { TogglePlay(); });
m_button_play->Bind(wxEVT_RIGHT_UP, [this](auto & e) { m_media_ctrl->Play(); }); m_button_play->Bind(wxEVT_RIGHT_UP, [this](auto & e) {
if (m_active_media_controller)
m_active_media_controller->Play();
});
// Orca: live-view FAQ link binding removed (vendor URL) // Orca: live-view FAQ link binding removed (vendor URL)
Bind(wxEVT_RIGHT_UP, [this](auto & e) { Bind(wxEVT_RIGHT_UP, [this](auto & e) {
@@ -139,9 +150,12 @@ MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const w
MediaPlayCtrl::~MediaPlayCtrl() MediaPlayCtrl::~MediaPlayCtrl()
{ {
if (m_webrtc_ctrl)
m_webrtc_ctrl->Stop();
m_media_ctrl->EndExternalStream();
{ {
boost::unique_lock lock(m_mutex); boost::unique_lock lock(m_mutex);
m_tasks.push_back("<exit>"); m_tasks.push_back({"<exit>", nullptr});
m_cond.notify_all(); m_cond.notify_all();
} }
while (!m_thread.try_join_for(boost::chrono::milliseconds(10))) { while (!m_thread.try_join_for(boost::chrono::milliseconds(10))) {
@@ -151,59 +165,144 @@ MediaPlayCtrl::~MediaPlayCtrl()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << this; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << this;
} }
void MediaPlayCtrl::SetWebMediaController(IMediaController *ctrl)
{
m_web_ctrl = ctrl;
set_active_media_controller(current_mode());
}
void MediaPlayCtrl::set_active_media_controller(CameraStreamMode mode)
{
switch (mode) {
case CameraStreamMode::http_snapshot:
if (auto *web_ctrl = dynamic_cast<WebMediaController *>(m_web_ctrl))
web_ctrl->set_mode(mode);
m_active_media_controller = m_web_ctrl;
break;
case CameraStreamMode::webrtc:
if (!m_webrtc_ctrl) {
m_webrtc_ctrl = std::make_unique<WebRtcMediaController>(
[this](const wxImage& image, wxSize size) { m_media_ctrl->SetExternalFrame(image, size); },
[this, token = std::weak_ptr<int>(m_token)](WebRtcMediaController::Status status) {
if (token.expired())
return;
CallAfter([this, status] { on_webrtc_status(status); });
});
}
m_active_media_controller = m_webrtc_ctrl.get();
break;
default:
m_active_media_controller = m_media_ctrl;
break;
}
}
CameraStreamMode MediaPlayCtrl::current_mode() const
{
auto agent = wxGetApp().getAgent();
return agent ? agent->get_camera_stream_mode() : CameraStreamMode::none;
}
void MediaPlayCtrl::SetMachineObject(MachineObject* obj) void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
{ {
std::string machine = obj ? obj->get_dev_id() : ""; const CameraStreamMode mode = current_mode();
if (obj) { if (mode != m_last_mode) {
m_camera_exists = obj->has_ipcam; if (m_last_state != MEDIASTATE_IDLE) {
m_dev_ver = obj->get_ota_version(); m_failed_code = 0; // a mode switch is not a stream failure - don't arm back-off
m_lan_mode = obj->is_lan_mode_printer(); Stop(" ");
m_lan_proto = obj->liveview_local;
m_remote_proto = obj->get_liveview_remote();
m_lan_ip = obj->get_dev_ip();
m_lan_passwd = obj->get_access_code();
m_device_busy = obj->is_camera_busy_off();
m_tutk_state = obj->tutk_state;
if (DevPrinterConfigUtil::get_printer_series_str(obj->printer_type) == "series_o" && BBLNetworkPlugin::instance().use_legacy_network()) {
// Legacy plugin cannot support remote play for H2D, force using local mode
m_remote_proto = MachineObject::LVR_None;
} }
} else { m_last_mode = mode;
m_camera_exists = false;
m_lan_mode = false;
m_lan_proto = MachineObject::LVL_None;
m_lan_ip.clear();
m_lan_passwd.clear();
m_dev_ver.clear();
m_tutk_state.clear();
m_remote_proto = 0;
m_device_busy = false;
} }
Enable(obj && obj->is_info_ready() && obj->m_push_count > 0); set_active_media_controller(mode);
if (machine == m_machine) {
if (m_last_state == MEDIASTATE_IDLE && IsEnabled()) const bool uses_local_camera_url = mode == CameraStreamMode::http || mode == CameraStreamMode::https ||
Play(); mode == CameraStreamMode::http_snapshot || mode == CameraStreamMode::rtsp;
const bool uses_webrtc = mode == CameraStreamMode::webrtc;
const std::string machine = obj ? obj->get_dev_id() : "";
bool changed = false;
if (uses_local_camera_url) {
auto agent = wxGetApp().getAgent();
std::string url = agent ? agent->get_local_camera_stream_url() : "";
m_camera_exists = !url.empty();
Enable(obj && m_camera_exists);
changed = machine != m_machine || url != m_agent_camera_url;
m_agent_camera_url = url;
m_url = from_u8(url);
} else if (uses_webrtc) {
m_camera_exists = obj != nullptr;
Enable(obj != nullptr);
changed = machine != m_machine;
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::SetMachineObject webrtc: changed=" << changed
<< " last_state=" << m_last_state << " web_user_stopped=" << m_web_user_stopped;
m_url.clear();
m_agent_camera_url.clear();
} else {
if (obj) {
m_camera_exists = obj->has_ipcam;
m_dev_ver = obj->get_ota_version();
m_lan_mode = obj->is_lan_mode_printer();
m_lan_proto = obj->liveview_local;
m_remote_proto = obj->get_liveview_remote();
m_lan_ip = obj->get_dev_ip();
m_lan_passwd = obj->get_access_code();
m_device_busy = obj->is_camera_busy_off();
m_tutk_state = obj->tutk_state;
if (DevPrinterConfigUtil::get_printer_series_str(obj->printer_type) == "series_o" && BBLNetworkPlugin::instance().use_legacy_network()) {
// Legacy plugin cannot support remote play for H2D, force using local mode
m_remote_proto = LiveviewRemote::LVR_None;
}
} else {
m_camera_exists = false;
m_lan_mode = false;
m_lan_proto = LiveviewLocal::LVL_None;
m_lan_ip.clear();
m_lan_passwd.clear();
m_dev_ver.clear();
m_tutk_state.clear();
m_remote_proto = 0;
m_device_busy = false;
}
Enable(obj && obj->is_info_ready() && obj->m_push_count > 0);
if (machine == m_machine)
return;
m_machine = machine;
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl switch machine: " << m_machine;
m_disable_lan = false;
m_failed_retry = 0;
m_last_failed_codes.clear();
m_last_user_play = wxDateTime::Now();
std::string stream_url;
if (get_stream_url(&stream_url)) {
m_streaming = boost::algorithm::contains(stream_url, "device=" + m_machine);
} else {
m_streaming = false;
}
if (m_last_state != MEDIASTATE_IDLE)
Stop(" ");
if (m_next_retry.IsValid()) // Try open 2 seconds later, to avoid state conflict
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(2);
else
SetStatus("", false);
return; return;
} }
m_machine = machine; m_machine = machine;
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl switch machine: " << m_machine; if (!changed)
m_disable_lan = false; return;
m_failed_retry = 0;
m_last_failed_codes.clear(); // A genuine target switch is not a stream failure and should clear the
m_last_user_play = wxDateTime::Now(); // manual-stop state before the new target is allowed to play.
std::string stream_url; m_web_user_stopped = false;
if (get_stream_url(&stream_url)) { if (uses_local_camera_url) {
m_streaming = boost::algorithm::contains(stream_url, "device=" + m_machine); m_failed_code = 0;
} else { m_failed_retry = 0;
m_streaming = false; m_next_retry = wxDateTime();
} }
if (m_last_state != MEDIASTATE_IDLE) if (m_last_state != MEDIASTATE_IDLE)
Stop(" "); Stop(" ");
if (m_next_retry.IsValid()) // Try open 2 seconds later, to avoid state conflict
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(2);
else
SetStatus("", false);
} }
wxString hide_id_middle_string(wxString const &str, size_t offset = 0, size_t length = -1) wxString hide_id_middle_string(wxString const &str, size_t offset = 0, size_t length = -1)
@@ -254,13 +353,71 @@ void refresh_agora_url(char const* device, char const* dev_ver, char const* chan
void MediaPlayCtrl::Play() void MediaPlayCtrl::Play()
{ {
if (!m_next_retry.IsValid() || wxDateTime::Now() < m_next_retry) if ((m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry) || !IsShownOnScreen() || m_last_state != MEDIASTATE_IDLE)
return; return;
if (!IsShownOnScreen())
return; const CameraStreamMode mode = current_mode();
if (m_last_state != MEDIASTATE_IDLE) { set_active_media_controller(mode);
m_last_mode = mode;
auto agent = wxGetApp().getAgent();
auto printer_agent = agent ? agent->get_printer_agent() : nullptr;
const bool is_bbl = (printer_agent ? printer_agent->get_agent_info().id : "") == BBL_PRINTER_AGENT_ID;
if (!is_bbl) {
const bool is_webrtc = mode == CameraStreamMode::webrtc;
const bool is_snapshot = mode == CameraStreamMode::http_snapshot;
const bool is_http_stream = mode == CameraStreamMode::http || mode == CameraStreamMode::https || mode == CameraStreamMode::rtsp;
if (!is_webrtc && !is_snapshot && !is_http_stream)
return;
auto *webrtc_ctrl = is_webrtc ? dynamic_cast<WebRtcMediaController *>(m_active_media_controller) : nullptr;
if (is_webrtc && (!webrtc_ctrl || !m_media_ctrl)) {
Stop(_L("Please confirm if the printer is connected."));
return;
}
if (webrtc_ctrl && webrtc_ctrl->is_active())
return;
m_failed_code = 0;
if (!m_active_media_controller || m_machine.empty() || !IsEnabled() || !m_camera_exists ||
(!is_webrtc && m_url.IsEmpty())) {
Stop(_L("Please confirm if the printer is connected."));
return;
}
if (is_webrtc) {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play webrtc: last_state=" << m_last_state << " failed_retry=" << m_failed_retry
<< " shown=" << IsShownOnScreen();
auto channel = agent ? agent->create_camera_signaling_channel(m_machine, wxGetApp().get_printer_cloud_provider()) : nullptr;
if (!channel) {
Stop(_L("Sign in to OrcaCloud to view the camera."));
return;
}
webrtc_ctrl->set_signalling_channel(std::move(channel));
m_media_ctrl->BeginExternalStream();
m_last_state = MEDIASTATE_INITIALIZING;
SetStatus(_L("Initializing..."), false);
} else if (is_snapshot) {
m_last_state = wxMEDIASTATE_PLAYING;
SetStatus(_L("Playing..."), false);
}
m_button_play->SetIcon("media_stop");
if (m_active_media_controller == m_media_ctrl) {
// wxMediaCtrl3 reports when it has a decoded frame; load() waits for
// that event before queuing Play so the stream is not marked stopped.
load();
} else {
m_active_media_controller->Load(wxURI(m_url));
m_active_media_controller->Play();
}
if (webrtc_ctrl) {
m_webrtc_epoch = webrtc_ctrl->epoch();
}
return; return;
} }
m_failed_code = 0; m_failed_code = 0;
if (m_machine.empty()) { if (m_machine.empty()) {
Stop(_L("Please confirm if the printer is connected.")); Stop(_L("Please confirm if the printer is connected."));
@@ -281,16 +438,15 @@ void MediaPlayCtrl::Play()
} }
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play: " << m_lan_proto << m_remote_proto << m_disable_lan; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play: " << m_lan_proto << m_remote_proto << m_disable_lan;
NetworkAgent *agent = wxGetApp().getAgent();
std::string agent_version = agent ? agent->get_version() : ""; std::string agent_version = agent ? agent->get_version() : "";
if (m_lan_proto > MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) { if (m_lan_proto > LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) {
m_disable_lan = m_remote_proto && !m_lan_mode; // try remote next time m_disable_lan = m_remote_proto && !m_lan_mode; // try remote next time
std::string url; std::string url;
if (m_lan_proto == MachineObject::LVL_Local) if (m_lan_proto == LiveviewLocal::LVL_Local)
url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd; url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd;
else if (m_lan_proto == MachineObject::LVL_Rtsps) else if (m_lan_proto == LiveviewLocal::LVL_Rtsps)
url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps"; url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps";
else if (m_lan_proto == MachineObject::LVL_Rtsp) else if (m_lan_proto == LiveviewLocal::LVL_Rtsp)
url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp"; url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp";
url += "&device=" + m_machine; url += "&device=" + m_machine;
url += "&net_ver=" + agent_version; url += "&net_ver=" + agent_version;
@@ -312,8 +468,8 @@ void MediaPlayCtrl::Play()
// !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_Disable (*) // !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_Disable (*)
// !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_None (x) // !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_None (x)
if (m_lan_proto <= MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto)) { if (m_lan_proto <= LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto)) {
Stop(m_lan_proto == MachineObject::LVL_None Stop(m_lan_proto == LiveviewLocal::LVL_None
? _L("A problem occurred. Please update the printer firmware and try again.") ? _L("A problem occurred. Please update the printer firmware and try again.")
: _L("LAN Only Liveview is off. Please turn on the liveview on printer screen.")); : _L("LAN Only Liveview is off. Please turn on the liveview on printer screen."));
return; return;
@@ -381,15 +537,78 @@ void MediaPlayCtrl::Play()
void start_ping_test(); void start_ping_test();
void MediaPlayCtrl::StopWebStream()
{
if (m_last_state == MEDIASTATE_IDLE)
return;
if (m_active_media_controller && m_active_media_controller == m_web_ctrl)
m_active_media_controller->Stop();
m_button_play->SetIcon("media_play");
m_last_state = MEDIASTATE_IDLE;
SetStatus(_L("Video Stopped."), false);
}
void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2) void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
{ {
const bool webrtc_active = m_last_mode == CameraStreamMode::webrtc &&
dynamic_cast<WebRtcMediaController *>(m_active_media_controller) != nullptr;
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Stop: last_state=" << m_last_state
<< " webrtc_active=" << webrtc_active << " failed_code=" << m_failed_code
<< " msg='" << msg.ToUTF8().data() << "'";
if (webrtc_active) {
m_active_media_controller->Stop();
m_media_ctrl->EndExternalStream();
}
switch (m_last_mode) {
case CameraStreamMode::http:
case CameraStreamMode::https:
case CameraStreamMode::http_snapshot: {
const bool snapshot = m_last_mode == CameraStreamMode::http_snapshot;
if (m_last_state != MEDIASTATE_IDLE) {
if (snapshot) {
if (m_active_media_controller)
m_active_media_controller->Stop();
} else {
// http/https mode plays through the ffmpeg backend (m_media_ctrl), not
// the webview - tear its read thread down too, otherwise it keeps
// pulling and painting frames after the UI says "Video Stopped".
boost::unique_lock lock(m_mutex);
m_tasks.push_back({"<stop>", m_active_media_controller});
m_cond.notify_all();
}
m_button_play->SetIcon("media_play");
m_last_state = MEDIASTATE_IDLE;
if (!msg.IsEmpty())
SetStatus(msg);
else
SetStatus(_L("Video Stopped."), false);
// Keep retries bounded for an explicit or retry-driven playback attempt.
// m_failed_retry is cleared on success (onStateChanged) and on a deliberate
// machine switch (SetMachineObject); manual playback via TogglePlay resets it.
if (m_failed_code != 0) {
const bool auto_retry = wxGetApp().app_config->get("liveview", "auto_retry") != "false";
++m_failed_retry;
m_next_retry = auto_retry
? wxDateTime::Now() + wxTimeSpan::Seconds(std::min(5 * m_failed_retry, 30))
: wxDateTime::Now() + wxTimeSpan::Days(1); // "off": wait for a manual retry
}
} else if (!msg.IsEmpty()) {
SetStatus(msg, false);
}
return;
}
default:
break;
}
int last_state = m_last_state; int last_state = m_last_state;
if (m_last_state != MEDIASTATE_IDLE) { if (m_last_state != MEDIASTATE_IDLE) {
m_media_ctrl->InvalidateBestSize(); m_media_ctrl->InvalidateBestSize();
m_button_play->SetIcon("media_play"); m_button_play->SetIcon("media_play");
boost::unique_lock lock(m_mutex); boost::unique_lock lock(m_mutex);
m_tasks.push_back("<stop>"); if (!webrtc_active)
m_tasks.push_back({"<stop>", m_active_media_controller});
m_cond.notify_all(); m_cond.notify_all();
if (!msg.IsEmpty()) if (!msg.IsEmpty())
SetStatus(msg); SetStatus(msg);
@@ -454,15 +673,38 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(5 * m_failed_retry); m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(5 * m_failed_retry);
} }
void MediaPlayCtrl::on_webrtc_status(WebRtcMediaController::Status status)
{
// Drop CallAfter-queued events from a superseded Play attempt.
if (status.epoch != m_webrtc_epoch)
return;
if (status.kind == WebRtcMediaController::Status::Connecting) {
m_last_state = MEDIASTATE_INITIALIZING;
SetStatus(_L("Initializing..."), false);
} else if (status.kind == WebRtcMediaController::Status::Playing) {
m_last_state = wxMEDIASTATE_PLAYING;
m_failed_code = 0;
m_failed_retry = 0;
SetStatus(_L("Playing..."), false);
} else if (status.kind == WebRtcMediaController::Status::Failed) {
m_failed_code = static_cast<int>(status.code) + 1;
Stop();
}
// Status::Stopped needs no action: a genuine failure arrives as Failed, and
// a stop we initiated is already handled by Stop() itself.
}
void MediaPlayCtrl::TogglePlay() void MediaPlayCtrl::TogglePlay()
{ {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::TogglePlay"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::TogglePlay";
if (m_last_state != MEDIASTATE_IDLE) { if (m_last_state != MEDIASTATE_IDLE) {
m_next_retry = wxDateTime(); m_next_retry = wxDateTime();
m_web_user_stopped = true;
Stop(); Stop();
} else { } else {
m_failed_retry = 0; m_failed_retry = 0;
m_user_triggered = true; m_user_triggered = true;
m_web_user_stopped = false;
if (m_last_user_play + wxTimeSpan::Minutes(5) < wxDateTime::Now()) { if (m_last_user_play + wxTimeSpan::Minutes(5) < wxDateTime::Now()) {
m_last_failed_codes.clear(); m_last_failed_codes.clear();
m_last_user_play = wxDateTime::Now(); m_last_user_play = wxDateTime::Now();
@@ -528,13 +770,13 @@ void MediaPlayCtrl::ToggleStream()
wxGetApp().app_config->set("not_show_vcamera_stop_prev", "1"); wxGetApp().app_config->set("not_show_vcamera_stop_prev", "1");
if (res == wxID_CANCEL) return; if (res == wxID_CANCEL) return;
} }
if (m_lan_proto > MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) { if (m_lan_proto > LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) {
std::string url; std::string url;
if (m_lan_proto == MachineObject::LVL_Local) if (m_lan_proto == LiveviewLocal::LVL_Local)
url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd; url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd;
else if (m_lan_proto == MachineObject::LVL_Rtsps) else if (m_lan_proto == LiveviewLocal::LVL_Rtsps)
url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps"; url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps";
else if (m_lan_proto == MachineObject::LVL_Rtsp) else if (m_lan_proto == LiveviewLocal::LVL_Rtsp)
url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp"; url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp";
url += "&device=" + into_u8(m_machine); url += "&device=" + into_u8(m_machine);
url += "&dev_ver=" + m_dev_ver; url += "&dev_ver=" + m_dev_ver;
@@ -594,7 +836,9 @@ void MediaPlayCtrl::jump_to_play()
void MediaPlayCtrl::onStateChanged(wxMediaEvent &event) void MediaPlayCtrl::onStateChanged(wxMediaEvent &event)
{ {
auto last_state = m_last_state; auto last_state = m_last_state;
auto state = m_media_ctrl->GetState(); if (m_active_media_controller != m_media_ctrl)
return;
auto state = m_active_media_controller->GetState();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::onStateChanged: " << state << ", last_state: " << last_state; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::onStateChanged: " << state << ", last_state: " << last_state;
if ((int) state < 0) return; if ((int) state < 0) return;
{ {
@@ -606,14 +850,14 @@ void MediaPlayCtrl::onStateChanged(wxMediaEvent &event)
} }
if ((last_state == MEDIASTATE_IDLE || last_state == MEDIASTATE_INITIALIZING) && state == wxMEDIASTATE_STOPPED) { return; } if ((last_state == MEDIASTATE_IDLE || last_state == MEDIASTATE_INITIALIZING) && state == wxMEDIASTATE_STOPPED) { return; }
if ((last_state == wxMEDIASTATE_PAUSED || last_state == wxMEDIASTATE_PLAYING) && state == wxMEDIASTATE_STOPPED) { if ((last_state == wxMEDIASTATE_PAUSED || last_state == wxMEDIASTATE_PLAYING) && state == wxMEDIASTATE_STOPPED) {
m_failed_code = m_media_ctrl->GetLastError(); m_failed_code = m_active_media_controller->GetLastError();
Stop(); Stop();
return; return;
} }
if (last_state == MEDIASTATE_LOADING && (state == wxMEDIASTATE_STOPPED || state == wxMEDIASTATE_PAUSED)) { if (last_state == MEDIASTATE_LOADING && (state == wxMEDIASTATE_STOPPED || state == wxMEDIASTATE_PAUSED)) {
wxSize size = m_media_ctrl->GetVideoSize(); wxSize size = m_active_media_controller->GetVideoSize();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::onStateChanged: size: " << size.x << "x" << size.y; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::onStateChanged: size: " << size.x << "x" << size.y;
m_failed_code = m_media_ctrl->GetLastError(); m_failed_code = m_active_media_controller->GetLastError();
if (size.GetWidth() >= 320) { if (size.GetWidth() >= 320) {
m_last_state = state; m_last_state = state;
m_failed_code = 0; m_failed_code = 0;
@@ -623,7 +867,7 @@ void MediaPlayCtrl::onStateChanged(wxMediaEvent &event)
m_failed_retry = 0; m_failed_retry = 0;
m_disable_lan = false; m_disable_lan = false;
boost::unique_lock lock(m_mutex); boost::unique_lock lock(m_mutex);
m_tasks.push_back("<play>"); m_tasks.push_back({"<play>", m_active_media_controller});
m_cond.notify_all(); m_cond.notify_all();
} else if (event.GetId()) { } else if (event.GetId()) {
if (m_failed_code == 0) if (m_failed_code == 0)
@@ -666,7 +910,8 @@ void MediaPlayCtrl::load()
{ {
m_last_state = MEDIASTATE_LOADING; m_last_state = MEDIASTATE_LOADING;
SetStatus(_L("Loading...")); SetStatus(_L("Loading..."));
if (wxGetApp().app_config->get("internal_developer_mode") == "true") { const auto mode = current_mode();
if (mode == CameraStreamMode::none && wxGetApp().app_config->get("internal_developer_mode") == "true") {
std::string file_h264 = data_dir() + "/video.h264"; std::string file_h264 = data_dir() + "/video.h264";
std::string file_info = data_dir() + "/video.info"; std::string file_info = data_dir() + "/video.info";
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl dump video to " << file_h264; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl dump video to " << file_h264;
@@ -677,7 +922,7 @@ void MediaPlayCtrl::load()
m_url = m_url + "&dump_info=" + boost::lexical_cast<std::string>(dump_info_file); m_url = m_url + "&dump_info=" + boost::lexical_cast<std::string>(dump_info_file);
} }
boost::unique_lock lock(m_mutex); boost::unique_lock lock(m_mutex);
m_tasks.push_back(m_url); m_tasks.push_back({m_url, m_active_media_controller});
m_cond.notify_all(); m_cond.notify_all();
} }
@@ -686,9 +931,8 @@ void MediaPlayCtrl::on_show_hide(wxShowEvent &evt)
evt.Skip(); evt.Skip();
if (m_isBeingDeleted) return; if (m_isBeingDeleted) return;
m_failed_retry = 0; m_failed_retry = 0;
if (m_next_retry.IsValid()) // Try open 2 seconds later, to avoid quick play/stop if (!IsShownOnScreen())
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(2); Stop();
IsShownOnScreen() ? Play() : Stop();
} }
void MediaPlayCtrl::media_proc() void MediaPlayCtrl::media_proc()
@@ -698,30 +942,34 @@ void MediaPlayCtrl::media_proc()
while (m_tasks.empty()) { while (m_tasks.empty()) {
m_cond.wait(lock); m_cond.wait(lock);
} }
wxString url = m_tasks.front(); MediaTask task = m_tasks.front();
if (m_tasks.size() >= 2 && !url.IsEmpty() && url[0] != '<' && m_tasks[1] == "<stop>") { if (m_tasks.size() >= 2 && !task.command.IsEmpty() && task.command[0] != '<' &&
BOOST_LOG_TRIVIAL(trace) << "MediaPlayCtrl: busy skip url: " << url; m_tasks[1].command == "<stop>" && task.controller == m_tasks[1].controller) {
BOOST_LOG_TRIVIAL(trace) << "MediaPlayCtrl: busy skip url: " << task.command;
m_tasks.pop_front(); m_tasks.pop_front();
m_tasks.pop_front(); m_tasks.pop_front();
continue; continue;
} }
lock.unlock(); lock.unlock();
if (url == "<stop>") { if (task.command == "<stop>") {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start stop"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start stop";
m_media_ctrl->Stop(); if (task.controller)
task.controller->Stop();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end stop"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end stop";
} }
else if (url == "<exit>") { else if (task.command == "<exit>") {
break; break;
} }
else if (url == "<play>") { else if (task.command == "<play>") {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start play"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start play";
m_media_ctrl->Play(); if (task.controller)
task.controller->Play();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end play"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end play";
} }
else { else {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start load"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start load";
m_media_ctrl->Load(wxURI(url)); if (task.controller)
task.controller->Load(wxURI(task.command));
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end load"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end load";
} }
lock.lock(); lock.lock();
+23 -1
View File
@@ -9,6 +9,9 @@
#define MediaPlayCtrl_h #define MediaPlayCtrl_h
#include "wxMediaCtrl3.h" #include "wxMediaCtrl3.h"
#include "IMediaController.hpp"
#include "WebRtcMediaController.hpp"
#include "slic3r/Utils/IPrinterAgent.hpp"
#include <wx/panel.h> #include <wx/panel.h>
@@ -36,6 +39,10 @@ public:
void SetMachineObject(MachineObject * obj); void SetMachineObject(MachineObject * obj);
void SetWebMediaController(IMediaController *ctrl);
void StopWebStream();
bool IsStreaming() const; bool IsStreaming() const;
void ToggleStream(); void ToggleStream();
@@ -54,6 +61,7 @@ protected:
void TogglePlay(); void TogglePlay();
void SetStatus(wxString const &msg, bool hyperlink = true); void SetStatus(wxString const &msg, bool hyperlink = true);
void on_webrtc_status(WebRtcMediaController::Status status);
private: private:
void load(); void load();
@@ -66,6 +74,9 @@ private:
static bool get_stream_url(std::string *url = nullptr); static bool get_stream_url(std::string *url = nullptr);
CameraStreamMode current_mode() const;
void set_active_media_controller(CameraStreamMode mode);
private: private:
static inline const wxMediaState MEDIASTATE_IDLE = static_cast<wxMediaState>(3); static inline const wxMediaState MEDIASTATE_IDLE = static_cast<wxMediaState>(3);
static inline const wxMediaState MEDIASTATE_INITIALIZING = static_cast<wxMediaState>(4); static inline const wxMediaState MEDIASTATE_INITIALIZING = static_cast<wxMediaState>(4);
@@ -76,6 +87,13 @@ private:
std::shared_ptr<int> m_token = std::make_shared<int>(0); std::shared_ptr<int> m_token = std::make_shared<int>(0);
wxMediaCtrl3 * m_media_ctrl; wxMediaCtrl3 * m_media_ctrl;
IMediaController * m_active_media_controller = nullptr;
IMediaController * m_web_ctrl = nullptr;
std::unique_ptr<WebRtcMediaController> m_webrtc_ctrl;
CameraStreamMode m_last_mode = CameraStreamMode::none;
std::uint64_t m_webrtc_epoch = 0;
std::string m_agent_camera_url;
bool m_web_user_stopped = false;
wxMediaState m_last_state = MEDIASTATE_IDLE; wxMediaState m_last_state = MEDIASTATE_IDLE;
std::string m_machine; std::string m_machine;
int m_lan_proto = 0; int m_lan_proto = 0;
@@ -91,7 +109,11 @@ private:
bool m_disable_lan = false; bool m_disable_lan = false;
wxString m_url; wxString m_url;
std::deque<wxString> m_tasks; struct MediaTask {
wxString command;
IMediaController *controller = nullptr;
};
std::deque<MediaTask> m_tasks;
boost::mutex m_mutex; boost::mutex m_mutex;
boost::condition_variable m_cond; boost::condition_variable m_cond;
boost::thread m_thread; boost::thread m_thread;
+11 -1
View File
@@ -34,6 +34,8 @@
#include "DeviceCore/DevManager.h" #include "DeviceCore/DevManager.h"
#include <boost/log/trivial.hpp>
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
@@ -259,6 +261,7 @@ void MonitorPanel::msw_rescale()
void MonitorPanel::select_machine(std::string machine_sn) void MonitorPanel::select_machine(std::string machine_sn)
{ {
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: MonitorPanel::select_machine queueing machine_sn=" << machine_sn;
wxCommandEvent *event = new wxCommandEvent(wxEVT_COMMAND_CHOICE_SELECTED); wxCommandEvent *event = new wxCommandEvent(wxEVT_COMMAND_CHOICE_SELECTED);
event->SetString(machine_sn); event->SetString(machine_sn);
wxQueueEvent(this, event); wxQueueEvent(this, event);
@@ -276,13 +279,20 @@ void MonitorPanel::on_timer(wxTimerEvent& event)
void MonitorPanel::on_select_printer(wxCommandEvent& event) void MonitorPanel::on_select_printer(wxCommandEvent& event)
{ {
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
const std::string requested_dev_id = event.GetString().ToStdString();
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: MonitorPanel::on_select_printer requested_dev_id="
<< requested_dev_id << " device_manager=" << (dev ? "set" : "null");
if (!dev) return; if (!dev) return;
if ( dev->get_selected_machine() && (dev->get_selected_machine()->get_dev_id() != event.GetString().ToStdString()) && m_hms_panel) { if ( dev->get_selected_machine() && (dev->get_selected_machine()->get_dev_id() != event.GetString().ToStdString()) && m_hms_panel) {
m_hms_panel->clear_hms_tag(); m_hms_panel->clear_hms_tag();
} }
if (!dev->set_selected_machine(event.GetString().ToStdString())) const bool selected = dev->set_selected_machine(requested_dev_id);
BOOST_LOG_TRIVIAL(trace) << "Orca diagnostic: MonitorPanel::on_select_printer set_selected_machine result="
<< selected << " selected_dev_id="
<< (dev->get_selected_machine() ? dev->get_selected_machine()->get_dev_id() : "<null>");
if (!selected)
return; return;
set_default(); set_default();
+1 -22
View File
@@ -51,7 +51,7 @@ void DeviceItem::sync_state()
state_printable = 6; state_printable = 6;
} }
if (is_blocking_printing(obj_)) { if (wxGetApp().is_blocking_printing(obj_)) {
state_printable = 5; state_printable = 5;
} }
@@ -104,27 +104,6 @@ void DeviceItem::unselected()
} }
} }
bool DeviceItem::is_blocking_printing(MachineObject* obj_)
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
auto target_model = obj_->printer_type;
std::string source_model = "";
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
if (it == compatible_machine.end()) {
return true;
}
}
return false;
}
void DeviceItem::update_item(const DeviceItem* item) void DeviceItem::update_item(const DeviceItem* item)
{ {
// Except for the selected status, everything else is updated // Except for the selected status, everything else is updated
-1
View File
@@ -59,7 +59,6 @@ public:
void selected(); void selected();
void unselected(); void unselected();
bool is_blocking_printing(MachineObject* obj_);
void update_item(const DeviceItem* item); void update_item(const DeviceItem* item);
}; };
+1 -1
View File
@@ -159,4 +159,4 @@ private:
void OnApplyDialog(wxCommandEvent &event); void OnApplyDialog(wxCommandEvent &event);
}; };
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
+89 -24
View File
@@ -2027,12 +2027,14 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
std::string machine_print_name = obj->get_show_printer_type(); std::string machine_print_name = obj->get_show_printer_type();
PresetBundle *preset_bundle = wxGetApp().preset_bundle; PresetBundle *preset_bundle = wxGetApp().preset_bundle;
std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle);
Preset* machine_preset = get_printer_preset(obj); const bool optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type);
if (!machine_preset) { const bool optional_target_model = DevPrinterConfigUtil::is_optional_printer_model_id(target_model_id);
Preset* machine_preset = optional_printer_model ? nullptr : get_printer_preset(obj);
if (!optional_printer_model && !optional_target_model && !machine_preset) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty";
return false; return false;
} }
if (machine_print_name != target_model_id) { if (!optional_printer_model && !optional_target_model && machine_print_name != target_model_id) {
MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n" MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n"
"Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); "Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO);
if (dlg.ShowModal() == wxID_NO) { if (dlg.ShowModal() == wxID_NO) {
@@ -2224,6 +2226,11 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
return; return;
} }
if (DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type)) {
clear_all_sync_status();
return;
}
bool printer_synced = false; bool printer_synced = false;
// 1. update printer status // 1. update printer status
const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset();
@@ -5824,11 +5831,30 @@ void Sidebar::load_ams_list(MachineObject* obj)
filament_ams_list = build_filament_ams_list(obj); filament_ams_list = build_filament_ams_list(obj);
} }
bool device_change = false;
const std::string& device = obj ? obj->get_dev_id() : ""; const std::string& device = obj ? obj->get_dev_id() : "";
if (p->ams_list_device != device) { const bool same_device = p->ams_list_device == device;
// Keep sync metadata out of the device payload, but preserve it across a
// subscription refresh when the physical filament in a slot is unchanged.
// Otherwise the refreshed configs differ only by the missing
// filament_changed key, causing combo boxes to rebuild and lose their
// transient post-sync badges.
auto &previous_filament_ams_list = wxGetApp().preset_bundle->filament_ams_list;
for (auto &entry : filament_ams_list) {
auto previous = previous_filament_ams_list.find(entry.first);
const auto *previous_changed = previous == previous_filament_ams_list.end() ? nullptr :
dynamic_cast<const ConfigOptionBool *>(previous->second.option("filament_changed"));
if (!same_device || previous_changed == nullptr ||
previous->second.opt_string("filament_id", 0u) != entry.second.opt_string("filament_id", 0u)) {
continue;
}
entry.second.set_key_value("filament_changed",
new ConfigOptionBool{previous_changed->value});
}
bool device_change = !same_device;
if (device_change) {
p->ams_list_device = device; p->ams_list_device = device;
device_change = true;
} }
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size();
if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change)
@@ -5838,9 +5864,27 @@ void Sidebar::load_ams_list(MachineObject* obj)
wxGetApp().preset_bundle->filament_ams_list = filament_ams_list; wxGetApp().preset_bundle->filament_ams_list = filament_ams_list;
for (auto c : p->combos_filament){ for (auto c : p->combos_filament){
c->set_sync_badge(false);
c->update(); c->update();
if (device_change) { }
c->ShowBadge(false);//change printer,then clear badge
if (!device_change) {
size_t combo_index = 0;
for (const auto &entry : filament_ams_list) {
const auto &tray = entry.second;
const bool has_filament = !tray.opt_string("filament_id", 0u).empty();
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
tray.opt_bool("filament_slot_placeholder", 0u);
if (!has_filament && !is_placeholder) {
continue;
}
if (combo_index >= p->combos_filament.size()) {
break;
}
const auto *filament_changed = dynamic_cast<const ConfigOptionBool *>(tray.option("filament_changed"));
p->combos_filament[combo_index]->set_sync_badge(
has_filament && !is_placeholder && filament_changed != nullptr && filament_changed->value);
++combo_index;
} }
} }
@@ -6014,18 +6058,32 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") : auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") :
_L("Filament type and color information have been synchronized, but slot information is not included."); _L("Filament type and color information have been synchronized, but slot information is not included.");
c->SetToolTip(tip); c->SetToolTip(tip);
c->ShowBadge(true); c->set_sync_badge(true);
}; };
{ // badge ams filament { // badge ams filament
clear_combos_filament_badge(); clear_combos_filament_badge();
if (sync_result.direct_sync) { if (sync_result.direct_sync) {
// Orca: PresetBundle::sync_ams_list rebuilds combos_filament // A placeholder contributes a preserved project filament to the
// 1:1 from the AMS trays that produce a combo (loaded trays + placeholders; non-placeholder // overwrite result, but it is not AMS-sourced and must not get a
// empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The // sync badge. Non-placeholder empty trays are omitted entirely.
// previous per-tray index walked the full filament_ams_list (including the skipped empties), size_t combo_index = 0;
// so an empty slot before a loaded one dropped the badge for the trailing filaments. for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) {
for (auto &c : p->combos_filament) { const auto &tray = entry.second;
badge_combox_filament(c); const bool has_filament = !tray.opt_string("filament_id", 0u).empty();
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
tray.opt_bool("filament_slot_placeholder", 0u);
if (!has_filament && !is_placeholder) {
continue;
}
if (combo_index >= p->combos_filament.size()) {
break;
}
if (is_placeholder) {
p->combos_filament[combo_index]->set_sync_badge(false);
} else {
badge_combox_filament(p->combos_filament[combo_index]);
}
++combo_index;
} }
} }
} }
@@ -6293,6 +6351,11 @@ template<typename T> void setup_dialog_position(T& info)
void Sidebar::pop_sync_nozzle_and_ams_dialog() { void Sidebar::pop_sync_nozzle_and_ams_dialog() {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog";
auto agent = wxGetApp().getAgent();
if (!agent || agent->get_filament_sync_mode() == FilamentSyncMode::none) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " filament synchronization is not supported; skipping dialog";
return;
}
wxTheApp->CallAfter([this]() { wxTheApp->CallAfter([this]() {
SyncNozzleAndAmsDialog::InputInfo temp_na_info; SyncNozzleAndAmsDialog::InputInfo temp_na_info;
wxPoint big_btn_pt; wxPoint big_btn_pt;
@@ -6424,17 +6487,14 @@ void Sidebar::clear_combos_filament_badge()
{ {
auto &combos_filament = p->combos_filament; auto &combos_filament = p->combos_filament;
for (auto &c : combos_filament) { // clear flag for (auto &c : combos_filament) { // clear flag
c->ShowBadge(false); c->set_sync_badge(false);
} }
} }
void Sidebar::udpate_combos_filament_badge() { void Sidebar::udpate_combos_filament_badge() {
auto &combos_filament = p->combos_filament; auto &combos_filament = p->combos_filament;
for (auto &c : combos_filament) { for (auto &c : combos_filament) {
auto selection = c->GetSelection(); c->update_badge_according_flag();
auto select_flag = c->GetFlag(selection);
auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS;
c->ShowBadge(ok);
} }
} }
@@ -12418,7 +12478,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
sidebar->auto_calc_flushing_volumes(idx); sidebar->auto_calc_flushing_volumes(idx);
} }
auto select_flag = combo->GetFlag(selection); auto select_flag = combo->GetFlag(selection);
combo->ShowBadge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); combo->set_sync_badge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS);
q->on_filament_change(idx); q->on_filament_change(idx);
} }
bool select_preset = !combo->selection_is_changed_according_to_physical_printers(); bool select_preset = !combo->selection_is_changed_according_to_physical_printers();
@@ -21102,9 +21162,14 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning)
} }
if (!check_printer_initialized(obj, true, popup_warning)) if (!check_printer_initialized(obj, true, popup_warning))
return false; return false;
Preset * machine_preset = get_printer_preset(obj); const std::string machine_model = obj->printer_type;
if (!machine_preset) PresetBundle *preset_bundle = wxGetApp().preset_bundle;
const std::string selected_model = preset_bundle ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) : std::string();
if (!DevPrinterConfigUtil::is_optional_printer_model_id(machine_model) &&
!DevPrinterConfigUtil::is_optional_printer_model_id(selected_model) &&
!get_printer_preset(obj)) {
return false; return false;
}
if (wxGetApp().is_blocking_printing()) { if (wxGetApp().is_blocking_printing()) {
if (popup_warning) { if (popup_warning) {
+18
View File
@@ -11,6 +11,7 @@ namespace Slic3r
// IMPORTANT: ordinal order is the Plugins dialog Status sort priority. // IMPORTANT: ordinal order is the Plugins dialog Status sort priority.
Activated, Activated,
Error, Error,
RuntimeError,
Inactive, Inactive,
Loading Loading
}; };
@@ -21,11 +22,28 @@ namespace Slic3r
{ {
case PluginStatus::Activated: return "Activated"; case PluginStatus::Activated: return "Activated";
case PluginStatus::Error: return "Error"; case PluginStatus::Error: return "Error";
case PluginStatus::RuntimeError: return "RuntimeError";
case PluginStatus::Inactive: return "Inactive"; case PluginStatus::Inactive: return "Inactive";
case PluginStatus::Loading: return "Loading"; case PluginStatus::Loading: return "Loading";
} }
return "Inactive"; return "Inactive";
} }
// why: a plugin whose module is live but whose catalog carries an error is a
// RUNTIME fault (e.g. a capability rejected at register time) - it stays
// loaded/checked and is only flagged, distinct from a load-time Error where
// the module never came up. Loading wins over both so an in-flight reload
// never flashes an error.
inline PluginStatus resolve_plugin_status(bool loading, bool has_error, bool is_loaded)
{
if (loading)
return PluginStatus::Loading;
if (has_error)
return is_loaded ? PluginStatus::RuntimeError : PluginStatus::Error;
if (is_loaded)
return PluginStatus::Activated;
return PluginStatus::Inactive;
}
} }
} // namespace Slic3r::GUI } // namespace Slic3r::GUI
+5 -8
View File
@@ -193,6 +193,7 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
payload_item["label"] = dialog_item.display_name; payload_item["label"] = dialog_item.display_name;
payload_item["source"] = to_string(dialog_item.source); payload_item["source"] = to_string(dialog_item.source);
payload_item["status"] = to_string(dialog_item.status); payload_item["status"] = to_string(dialog_item.status);
payload_item["is_loaded"] = dialog_item.is_loaded;
payload_item["error"] = dialog_item.error_text; payload_item["error"] = dialog_item.error_text;
payload_item["update_status"] = to_string(dialog_item.update_status); payload_item["update_status"] = to_string(dialog_item.update_status);
payload_item["unauthorized"] = dialog_item.unauthorized; payload_item["unauthorized"] = dialog_item.unauthorized;
@@ -337,14 +338,7 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
item.sharing_token = descriptor.sharing_token; item.sharing_token = descriptor.sharing_token;
item.thumbnail_url = descriptor.thumbnail_url; item.thumbnail_url = descriptor.thumbnail_url;
if (item.loading) item.status = resolve_plugin_status(item.loading, item.has_error, item.is_loaded);
item.status = PluginStatus::Loading;
else if (item.has_error)
item.status = PluginStatus::Error;
else if (item.is_loaded)
item.status = PluginStatus::Activated;
else
item.status = PluginStatus::Inactive;
item.available_actions = evaluate_action_policy(item); item.available_actions = evaluate_action_policy(item);
const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(), const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(),
@@ -790,6 +784,9 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
} }
BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key; BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key;
// A user-disabled plugin has no meaningful error state.
if (!manager.clear_plugin_error(plugin_key))
BOOST_LOG_TRIVIAL(warning) << "Failed to clear plugin error for " << plugin_key << " (failed to find)";
// A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later. // A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later.
if (m_activating_plugin_key == plugin_key) if (m_activating_plugin_key == plugin_key)
m_activating_plugin_key.clear(); m_activating_plugin_key.clear();
+2 -1
View File
@@ -63,6 +63,7 @@ std::string PrePrintChecker::get_print_status_info(PrintDialogStatus status)
case PrintStatusRackReading: return "PrintStatusRackReading"; case PrintStatusRackReading: return "PrintStatusRackReading";
case PrintStatusRackNozzleNumUnmeetWarning: return "PrintStatusRackNozzleNumUnmeetWarning"; case PrintStatusRackNozzleNumUnmeetWarning: return "PrintStatusRackNozzleNumUnmeetWarning";
case PrintStatusHasUnreliableNozzleWarning: return "PrintStatusHasUnreliableNozzleWarning"; case PrintStatusHasUnreliableNozzleWarning: return "PrintStatusHasUnreliableNozzleWarning";
case PrintStatusOptionalPrinterModel: return "PrintStatusOptionalPrinterModel";
case PrintStatusWarningExtFilamentNotMatch: return "PrintStatusWarningExtFilamentNotMatch"; case PrintStatusWarningExtFilamentNotMatch: return "PrintStatusWarningExtFilamentNotMatch";
case PrintStatusFilamentWarningNozzleHRC: return "PrintStatusFilamentWarningNozzleHRC"; case PrintStatusFilamentWarningNozzleHRC: return "PrintStatusFilamentWarningNozzleHRC";
case PrintStatusTPUUnsupportCaliOn: return "PrintStatusTPUUnsupportCaliOn"; case PrintStatusTPUUnsupportCaliOn: return "PrintStatusTPUUnsupportCaliOn";
@@ -104,6 +105,7 @@ wxString PrePrintChecker::get_pre_state_msg(PrintDialogStatus status)
case PrintStatusNeedConsistencyUpgrading: return _L("Cannot send the print job to a printer whose firmware must be updated."); case PrintStatusNeedConsistencyUpgrading: return _L("Cannot send the print job to a printer whose firmware must be updated.");
case PrintStatusBlankPlate: return _L("Cannot send a print job for an empty plate."); case PrintStatusBlankPlate: return _L("Cannot send a print job for an empty plate.");
case PrintStatusTimelapseNoSdcard: return _L("Storage needs to be inserted to record timelapse."); case PrintStatusTimelapseNoSdcard: return _L("Storage needs to be inserted to record timelapse.");
case PrintStatusOptionalPrinterModel: return _L("The selected printer model could not be identified, so compatibility with the print file configuration cannot be verified. Please verify the printer preset before sending.");
case PrintStatusMixAmsAndVtSlotWarning: return _L("You have selected both external and AMS filaments for an extruder. You will need to manually switch the external filament during printing."); case PrintStatusMixAmsAndVtSlotWarning: return _L("You have selected both external and AMS filaments for an extruder. You will need to manually switch the external filament during printing.");
case PrintStatusTPUUnsupportAutoCali: return _L("TPU 90A/TPU 85A is too soft and does not support automatic Flow Dynamics calibration."); case PrintStatusTPUUnsupportAutoCali: return _L("TPU 90A/TPU 85A is too soft and does not support automatic Flow Dynamics calibration.");
case PrintStatusWarningKvalueNotUsed: return _L("Set dynamic flow calibration to 'OFF' to enable custom dynamic flow value."); case PrintStatusWarningKvalueNotUsed: return _L("Set dynamic flow calibration to 'OFF' to enable custom dynamic flow value.");
@@ -379,4 +381,3 @@ bool PrinterMsgPanel::UpdateInfos(const std::vector<prePrintInfo>& infos)
} }
}; };
+2 -1
View File
@@ -112,6 +112,7 @@ enum PrintDialogStatus : unsigned int {
// Orca: a nozzle diameter that differs from the one the printer remembers is a warning, // Orca: a nozzle diameter that differs from the one the printer remembers is a warning,
// not an error, so non-standard nozzles can still be printed with. // not an error, so non-standard nozzles can still be printed with.
PrintStatusNozzleDiameterMismatch, PrintStatusNozzleDiameterMismatch,
PrintStatusOptionalPrinterModel,
PrintStatusPrinterWarningEnd, PrintStatusPrinterWarningEnd,
// Warnings for filament // Warnings for filament
@@ -170,7 +171,7 @@ public:
void add_with_checkbox(PrintDialogStatus state, wxString msg, wxString checkbox_label, bool checked, std::function<void(bool)> checkbox_callback); void add_with_checkbox(PrintDialogStatus state, wxString msg, wxString checkbox_label, bool checked, std::function<void(bool)> checkbox_callback);
static ::std::string get_print_status_info(PrintDialogStatus status); static ::std::string get_print_status_info(PrintDialogStatus status);
wxString get_pre_state_msg(PrintDialogStatus status); static wxString get_pre_state_msg(PrintDialogStatus status);
static bool is_error(PrintDialogStatus status) { return (PrintStatusErrorBegin < status) && (PrintStatusErrorEnd > status); }; static bool is_error(PrintDialogStatus status) { return (PrintStatusErrorBegin < status) && (PrintStatusErrorEnd > status); };
static bool is_error_printer(PrintDialogStatus status) { return (PrintStatusPrinterErrorBegin < status) && (PrintStatusPrinterErrorEnd > status); }; static bool is_error_printer(PrintDialogStatus status) { return (PrintStatusPrinterErrorBegin < status) && (PrintStatusPrinterErrorEnd > status); };
static bool is_error_filament(PrintDialogStatus status) { return (PrintStatusFilamentErrorBegin < status) && (PrintStatusFilamentErrorEnd > status); }; static bool is_error_filament(PrintDialogStatus status) { return (PrintStatusFilamentErrorBegin < status) && (PrintStatusFilamentErrorEnd > status); };
+11 -3
View File
@@ -361,7 +361,8 @@ wxString PresetComboBox::get_preset_item_name(unsigned int index)
return GetString(index); return GetString(index);
} }
std::map<std::string, MachineObject *> machine_list = dev->get_my_machine_list(); std::map<std::string, MachineObject *> machine_list =
dev->get_my_machine_list(dev->get_current_printer_agent_id());
if (machine_list.empty()) { if (machine_list.empty()) {
assert(false); assert(false);
m_selected_dev_id.clear(); m_selected_dev_id.clear();
@@ -479,7 +480,8 @@ void PresetComboBox::add_connected_printers(std::string selected, bool alias_nam
if (!dev) if (!dev)
return; return;
std::map<std::string, MachineObject *> machine_list = dev->get_my_machine_list(); std::map<std::string, MachineObject *> machine_list =
dev->get_my_machine_list(dev->get_current_printer_agent_id());
if (machine_list.empty()) if (machine_list.empty())
return; return;
@@ -999,7 +1001,13 @@ void PlaterPresetComboBox::update_badge_according_flag() {
auto selection = GetSelection(); auto selection = GetSelection();
auto select_flag = GetFlag(selection); auto select_flag = GetFlag(selection);
auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS;
ShowBadge(ok); ShowBadge(m_sync_badge || ok);
}
void PlaterPresetComboBox::set_sync_badge(bool show)
{
m_sync_badge = show;
ShowBadge(show);
} }
bool PlaterPresetComboBox::switch_to_tab() bool PlaterPresetComboBox::switch_to_tab()
+2
View File
@@ -205,6 +205,7 @@ public:
void msw_rescale() override; void msw_rescale() override;
void OnSelect(wxCommandEvent& evt) override; void OnSelect(wxCommandEvent& evt) override;
void update_badge_according_flag(); void update_badge_according_flag();
void set_sync_badge(bool show);
FilamentColor get_cur_color_info(); FilamentColor get_cur_color_info();
void show_default_color_picker(); void show_default_color_picker();
@@ -214,6 +215,7 @@ public:
private: private:
// BBS // BBS
wxColor m_color; wxColor m_color;
bool m_sync_badge{false};
}; };
+84 -16
View File
@@ -20,6 +20,7 @@
#include <wx/progdlg.h> #include <wx/progdlg.h>
#include <wx/clipbrd.h> #include <wx/clipbrd.h>
#include <wx/dcgraph.h> #include <wx/dcgraph.h>
#include <wx/filedlg.h>
#include <miniz.h> #include <miniz.h>
#include <algorithm> #include <algorithm>
#include "Plater.hpp" #include "Plater.hpp"
@@ -29,6 +30,7 @@
#include "DeviceCore/DevManager.h" #include "DeviceCore/DevManager.h"
#include "DeviceCore/DevStorage.h" #include "DeviceCore/DevStorage.h"
#include "md4c/src/md4c-html.h" #include "md4c/src/md4c-html.h"
#include "../Utils/Http.hpp"
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
@@ -1461,6 +1463,45 @@ InputIpAddressDialog::InputIpAddressDialog(wxWindow *parent)
m_input_top_sizer->Add(0, 0, 0, wxTOP, FromDIP(4)); m_input_top_sizer->Add(0, 0, 0, wxTOP, FromDIP(4));
m_input_top_sizer->Add(m_input_area, 0, wxRIGHT | wxEXPAND, FromDIP(18)); m_input_top_sizer->Add(m_input_area, 0, wxRIGHT | wxEXPAND, FromDIP(18));
m_tips_cafile = new Label(ip_input_top_panel, _L("HTTPS CA File"));
m_input_cafile = new wxTextCtrl(ip_input_top_panel, wxID_ANY);
m_input_cafile->SetMinSize(wxSize(FromDIP(260), FromDIP(28)));
m_input_cafile->SetMaxSize(wxSize(FromDIP(260), FromDIP(28)));
m_button_cafile = new Button(ip_input_top_panel, _L("Browse") + " " + dots);
m_button_cafile->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
m_button_cafile->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
static const auto filemasks = _L("Certificate files (*.crt, *.pem)|*.crt;*.pem|All files|*.*");
wxFileDialog openFileDialog(this, _L("Open CA certificate file"), "", "", filemasks,
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (openFileDialog.ShowModal() != wxID_CANCEL)
m_input_cafile->SetValue(openFileDialog.GetPath());
});
auto cafile_input_sizer = new wxBoxSizer(wxHORIZONTAL);
cafile_input_sizer->Add(m_input_cafile, 1, wxALIGN_CENTER_VERTICAL);
cafile_input_sizer->Add(m_button_cafile, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10));
m_cafile_hint = new Label(ip_input_top_panel, _L("HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate."));
m_cafile_hint->Wrap(FromDIP(352));
m_input_top_sizer->Add(m_tips_cafile, 0, wxTOP | wxEXPAND, FromDIP(10));
m_input_top_sizer->Add(cafile_input_sizer, 0, wxTOP | wxEXPAND, FromDIP(4));
m_input_top_sizer->Add(m_cafile_hint, 0, wxTOP | wxEXPAND, FromDIP(4));
if (!Http::ca_file_supported()) {
m_input_cafile->Disable();
m_button_cafile->Disable();
m_cafile_hint->SetLabel(_L("This system uses HTTPS certificates from the system Certificate Store or Keychain. To use a custom CA file, import it there."));
m_cafile_hint->Wrap(FromDIP(352));
}
if (wxGetApp().preset_bundle) {
const auto& config = wxGetApp().preset_bundle->printers.get_edited_preset().config;
if (config.has("printhost_cafile"))
m_input_cafile->SetValue(from_u8(config.opt_string("printhost_cafile")));
}
ip_input_top_panel->SetSizer(m_input_top_sizer); ip_input_top_panel->SetSizer(m_input_top_sizer);
ip_input_top_panel->Layout(); ip_input_top_panel->Layout();
ip_input_top_panel->Fit(); ip_input_top_panel->Fit();
@@ -1736,7 +1777,8 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj)
auto str_ip = m_input_ip->GetTextCtrl()->GetValue(); auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
// ORCA enabling / disabling buttons with conditions enough to change its style // ORCA enabling / disabling buttons with conditions enough to change its style
m_button_ok->Enable(isIp(str_ip.ToStdString()) && str_access_code.Length() == 8); m_button_ok->Enable(isValidEndpoint(str_ip.ToStdString()) &&
(str_access_code.IsEmpty() || str_access_code.Length() >= 8));
Layout(); Layout();
Fit(); Fit();
@@ -1773,19 +1815,29 @@ void InputIpAddressDialog::update_test_msg(wxString msg,bool connected)
Fit(); Fit();
} }
bool InputIpAddressDialog::isIp(std::string ipstr) bool InputIpAddressDialog::isValidEndpoint(std::string endpoint)
{ {
istringstream ipstream(ipstr); if (endpoint.empty() || std::any_of(endpoint.begin(), endpoint.end(), [](unsigned char c) {
int num[4]; return std::isspace(c) != 0;
char point[3]; }))
string end; return false;
ipstream >> num[0] >> point[0] >> num[1] >> point[1] >> num[2] >> point[2] >> num[3] >> end;
for (int i = 0; i < 3; ++i) { const bool has_http_scheme = endpoint.rfind("http://", 0) == 0;
if (num[i] < 0 || num[i]>255) return false; const bool has_https_scheme = endpoint.rfind("https://", 0) == 0;
if (point[i] != '.') return false; const auto scheme_pos = endpoint.find("://");
} if (scheme_pos != std::string::npos && !has_http_scheme && !has_https_scheme)
if (num[3] < 0 || num[3]>255) return false; return false;
if (!end.empty()) return false;
std::string port;
const std::string host = Http::get_host_from_url(endpoint, &port);
if (host.empty())
return false;
// get_host_from_url returns its input when libcurl cannot parse it. For a
// URL with a scheme, that means a failed parse still needs to be rejected.
if (scheme_pos != std::string::npos && host == endpoint)
return false;
return true; return true;
} }
@@ -1801,6 +1853,8 @@ void InputIpAddressDialog::on_ok(wxMouseEvent& evt)
m_trouble_shoot->Hide(); m_trouble_shoot->Hide();
std::string str_ip = m_input_ip->GetTextCtrl()->GetValue().ToStdString(); std::string str_ip = m_input_ip->GetTextCtrl()->GetValue().ToStdString();
std::string str_access_code = m_input_access_code->GetTextCtrl()->GetValue().ToStdString(); std::string str_access_code = m_input_access_code->GetTextCtrl()->GetValue().ToStdString();
if (str_access_code.empty())
str_access_code = "88888888";
std::string str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both).ToStdString(); std::string str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both).ToStdString();
// Serial number should not contain lower case letters, and bambu_network plugin crashes // Serial number should not contain lower case letters, and bambu_network plugin crashes
// if user entered the wrong serial number, so we call `Upper()` here. // if user entered the wrong serial number, so we call `Upper()` here.
@@ -1820,6 +1874,17 @@ void InputIpAddressDialog::on_ok(wxMouseEvent& evt)
Layout(); Layout();
Fit(); Fit();
if (wxGetApp().preset_bundle) {
auto& config = wxGetApp().preset_bundle->printers.get_edited_preset().config;
std::string port;
Http::get_host_from_url(str_ip, &port);
config.opt_string("print_host") = str_ip;
if (!port.empty())
config.opt_string("printhost_port") = port;
if (Http::ca_file_supported())
config.opt_string("printhost_cafile") = m_input_cafile->GetValue().ToStdString();
}
token_.reset(this, nop_deleter); token_.reset(this, nop_deleter);
m_thread = new boost::thread(boost::bind(&InputIpAddressDialog::workerThreadFunc, this, str_ip, str_access_code, str_sn, str_model_id, str_name)); m_thread = new boost::thread(boost::bind(&InputIpAddressDialog::workerThreadFunc, this, str_ip, str_access_code, str_sn, str_model_id, str_name));
} }
@@ -1835,6 +1900,8 @@ void InputIpAddressDialog::on_send_retry()
Fit(); Fit();
wxString ip = m_input_ip->GetTextCtrl()->GetValue(); wxString ip = m_input_ip->GetTextCtrl()->GetValue();
wxString str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); wxString str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
if (str_access_code.IsEmpty())
str_access_code = "88888888";
// check support function // check support function
if (!m_obj) return; if (!m_obj) return;
@@ -2056,7 +2123,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
auto str_ip = m_input_ip->GetTextCtrl()->GetValue(); auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
if (str_access_code.empty()) { if (str_access_code.IsEmpty()) {
str_access_code = "88888888"; str_access_code = "88888888";
} }
@@ -2072,7 +2139,8 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
} }
// ORCA enabling / disabling buttons with conditions enough to change its style // ORCA enabling / disabling buttons with conditions enough to change its style
bool enable_btns = isIp(str_ip.ToStdString()) && str_access_code.Length() == 8 && invalid_access_code; bool valid_access_code_length = str_access_code.IsEmpty() || str_access_code.Length() >= 8;
bool enable_btns = isValidEndpoint(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
m_button_manual_setup->Enable(enable_btns); m_button_manual_setup->Enable(enable_btns);
m_button_ok->Enable(enable_btns); m_button_ok->Enable(enable_btns);
@@ -2087,7 +2155,7 @@ InputIpAddressDialog::~InputIpAddressDialog()
void InputIpAddressDialog::on_dpi_changed(const wxRect& suggested_rect) void InputIpAddressDialog::on_dpi_changed(const wxRect& suggested_rect)
{ {
m_button_cafile->Rescale();
} }
+5 -1
View File
@@ -315,12 +315,16 @@ public:
Button* m_button_manual_setup{ nullptr }; Button* m_button_manual_setup{ nullptr };
Label* m_tips_ip{ nullptr }; Label* m_tips_ip{ nullptr };
Label* m_tips_access_code{ nullptr }; Label* m_tips_access_code{ nullptr };
Label* m_tips_cafile{ nullptr };
Label* m_cafile_hint{ nullptr };
Label* m_tips_sn{nullptr}; Label* m_tips_sn{nullptr};
Label* m_tips_modelID{nullptr}; Label* m_tips_modelID{nullptr};
Label* m_test_right_msg{ nullptr }; Label* m_test_right_msg{ nullptr };
Label* m_test_wrong_msg{ nullptr }; Label* m_test_wrong_msg{ nullptr };
TextInput* m_input_ip{ nullptr }; TextInput* m_input_ip{ nullptr };
TextInput* m_input_access_code{ nullptr }; TextInput* m_input_access_code{ nullptr };
wxTextCtrl* m_input_cafile{ nullptr };
Button* m_button_cafile{ nullptr };
TextInput* m_input_printer_name{ nullptr }; TextInput* m_input_printer_name{ nullptr };
TextInput* m_input_sn{ nullptr }; TextInput* m_input_sn{ nullptr };
ComboBox* m_input_modelID{ nullptr }; ComboBox* m_input_modelID{ nullptr };
@@ -344,7 +348,7 @@ public:
void update_title(wxString title); void update_title(wxString title);
void set_machine_obj(MachineObject* obj); void set_machine_obj(MachineObject* obj);
void update_test_msg(wxString msg, bool connected); void update_test_msg(wxString msg, bool connected);
bool isIp(std::string ipstr); bool isValidEndpoint(std::string endpoint);
void check_ip_address_failed(int result); void check_ip_address_failed(int result);
void on_check_ip_address_failed(wxCommandEvent& evt); void on_check_ip_address_failed(wxCommandEvent& evt);
void on_ok(wxMouseEvent& evt); void on_ok(wxMouseEvent& evt);
+80 -41
View File
@@ -21,6 +21,7 @@
#include "Jobs/PlaterWorker.hpp" #include "Jobs/PlaterWorker.hpp"
#include "DeviceCore/DevConfig.h" #include "DeviceCore/DevConfig.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevNozzleSystem.h" #include "DeviceCore/DevNozzleSystem.h"
#include "DeviceCore/DevNozzleRack.h" #include "DeviceCore/DevNozzleRack.h"
#include "DeviceCore/DevExtensionTool.h" #include "DeviceCore/DevExtensionTool.h"
@@ -1125,7 +1126,10 @@ bool SelectMachineDialog::do_ams_mapping(MachineObject *obj_,bool use_ams)
int filament_result = 0; int filament_result = 0;
std::vector<bool> map_opt; //four values: use_left_ams, use_right_ams, use_left_ext, use_right_ext std::vector<bool> map_opt; //four values: use_left_ams, use_right_ams, use_left_ext, use_right_ext
if (nozzle_nums > 1){ // Orca: only do the per-physical-extruder left/right split when the device actually reports
// 2+ extruders. A non-BBL multi-nozzle printer (e.g. Snapmaker U1) reports a single extruder
// with one filament pool, so it maps as a single surface via the else branch below.
if (nozzle_nums > 1 && obj_->GetExtderSystem()->GetTotalExtderCount() > 1){
//get nozzle property, the extders are same? //get nozzle property, the extders are same?
if (true/*!can_hybrid_mapping(obj_get_extder_data())*/){ if (true/*!can_hybrid_mapping(obj_get_extder_data())*/){
std::vector<FilamentInfo> m_ams_mapping_result_left, m_ams_mapping_result_right; std::vector<FilamentInfo> m_ams_mapping_result_left, m_ams_mapping_result_right;
@@ -2312,8 +2316,10 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
} else if (status == PrintDialogStatus::PrintStatusNoSdcard) { } else if (status == PrintDialogStatus::PrintStatusNoSdcard) {
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
Enable_Send_Button(false); Enable_Send_Button(false);
}else if (status == PrintDialogStatus::PrintStatusUnsupportedPrinter) { }else if (status == PrintDialogStatus::PrintStatusUnsupportedPrinter ||
status == PrintDialogStatus::PrintStatusOptionalPrinterModel) {
wxString msg_text; wxString msg_text;
const bool block_send = status == PrintDialogStatus::PrintStatusUnsupportedPrinter;
try try
{ {
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
@@ -2339,17 +2345,21 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
auto target_print_name = wxString(DevPrinterConfigUtil::get_printer_display_name(target_model_id)); auto target_print_name = wxString(DevPrinterConfigUtil::get_printer_display_name(target_model_id));
target_print_name.Replace(wxT("Bambu Lab "), wxEmptyString); target_print_name.Replace(wxT("Bambu Lab "), wxEmptyString);
msg_text = wxString::Format(_L("The selected printer (%s) is incompatible with the print file configuration (%s). Please adjust the printer preset in the prepare page or choose a compatible printer on this page."), sourcet_print_name, target_print_name); if (block_send) {
msg_text = wxString::Format(_L("The selected printer (%s) is incompatible with the print file configuration (%s). Please adjust the printer preset in the prepare page or choose a compatible printer on this page."), sourcet_print_name, target_print_name);
} else {
msg_text = wxString::Format(_L("The selected printer (%s) has an unknown model, so compatibility with the print file configuration (%s) cannot be verified. Please verify the printer preset before sending."), sourcet_print_name, target_print_name);
}
msg = msg_text; msg = msg_text;
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
Enable_Send_Button(false); Enable_Send_Button(!block_send);
} }
catch (...) catch (...)
{ {
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
Enable_Send_Button(false); Enable_Send_Button(!block_send);
} }
@@ -2510,31 +2520,13 @@ void SelectMachineDialog::on_cancel(wxCloseEvent &event)
bool SelectMachineDialog::is_blocking_printing(MachineObject* obj_) bool SelectMachineDialog::is_blocking_printing(MachineObject* obj_)
{ {
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (m_print_type == PrintFromType::FROM_NORMAL)
if (!dev) return true; return wxGetApp().is_blocking_printing(obj_);
auto target_model = obj_->printer_type;
std::string source_model = "";
if (m_print_type == PrintFromType::FROM_NORMAL) { std::string source_model;
PresetBundle* preset_bundle = wxGetApp().preset_bundle; if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty())
source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id;
return wxGetApp().is_blocking_printing(obj_, source_model);
}else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW) {
if (m_required_data_plate_data_list.size() > 0) {
source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id;
}
}
if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
if (it == compatible_machine.end()) {
return true;
}
}
return false;
} }
static std::unordered_set<int> _get_used_nozzle_idxes() static std::unordered_set<int> _get_used_nozzle_idxes()
@@ -2622,6 +2614,10 @@ bool SelectMachineDialog::is_same_printer_model()
if(preset_bundle == nullptr) return result; if(preset_bundle == nullptr) return result;
const auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); const auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
const auto target_model = obj_->printer_type; const auto target_model = obj_->printer_type;
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) {
return true;
}
// Orca: ignore P1P -> P1S // Orca: ignore P1P -> P1S
if (source_model != target_model) { if (source_model != target_model) {
if ((source_model == "C12" && target_model == "C11") || (source_model == "C11" && target_model == "C12") || if ((source_model == "C12" && target_model == "C11") || (source_model == "C11" && target_model == "C12") ||
@@ -3696,10 +3692,32 @@ void SelectMachineDialog::on_send_print()
m_print_job->on_success([this]() { finish_mode(); }); m_print_job->on_success([this]() { finish_mode(); });
m_print_job->on_check_ip_address_fail([this]() { m_print_job->on_check_ip_address_fail([this]() {
wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS); // Invoked from the PrintJob worker thread when the LAN pre-flight (file upload
wxQueueEvent(this, evt); // verification) fails. Marshal device/UI access to the main thread.
wxGetApp().show_ip_address_enter_dialog(); CallAfter([this]()
}); {
// Reset the dialog out of sending mode so the user can retry.
wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS);
wxQueueEvent(this, evt);
DeviceManager* dev = wxGetApp().getDeviceManager();
MachineObject* obj = dev ? dev->get_selected_machine() : nullptr;
if (obj && obj->is_connected())
{
// Connected: failed on file upload
MessageDialog dlg(this,
_L("Failed to upload the file to the printer's storage. Please try again."),
_L("Send Failed"), wxOK | wxICON_ERROR);
dlg.ShowModal();
}
else
{
// Not connected: reenter ip and access code
wxGetApp().show_ip_address_enter_dialog();
}
});
});
// update ota version // update ota version
NetworkAgent* agent = wxGetApp().getAgent(); NetworkAgent* agent = wxGetApp().getAgent();
@@ -4533,11 +4551,13 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
// check nozzle data valid // check nozzle data valid
{ {
if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine || // Commented out the following as ntUndefine and 0.0f are default values
installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) { // (signifying that the value is not given) that should PASS, not fail
show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid); // if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine ||
return false; // installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) {
} // show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid);
// return false;
// }
if (obj_->is_nozzle_flow_type_supported() && if (obj_->is_nozzle_flow_type_supported() &&
installed_ext_nozzle.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) { installed_ext_nozzle.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) {
@@ -4566,7 +4586,10 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
// check nozzle diameter // check nozzle diameter
{ {
if (slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) { // 0.0f is default when there is no nozzle diameter is given.
// In nozzle_diameter == 0.0f case, it passes and does not require a comparison
if (installed_ext_nozzle.GetNozzleDiameter() > 0.0f &&
slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) {
std::vector<wxString> msg_params; std::vector<wxString> msg_params;
if (ext_sys->GetTotalExtderCount() == 2) { if (ext_sys->GetTotalExtderCount() == 2) {
const wxString& mismatch_nozzle_str = _get_nozzle_name(ext_sys->GetTotalExtderCount(), slicing_ext_idx); const wxString& mismatch_nozzle_str = _get_nozzle_name(ext_sys->GetTotalExtderCount(), slicing_ext_idx);
@@ -4760,6 +4783,22 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_)
return; return;
} }
bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type);
if (m_print_type == PrintFromType::FROM_NORMAL) {
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
has_optional_printer_model = has_optional_printer_model ||
(preset_bundle && DevPrinterConfigUtil::is_optional_printer_model_id(
preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle)));
} else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty()) {
has_optional_printer_model = has_optional_printer_model ||
DevPrinterConfigUtil::is_optional_printer_model_id(
m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id);
}
if (has_optional_printer_model) {
show_status(PrintDialogStatus::PrintStatusOptionalPrinterModel);
}
if (is_blocking_printing(obj_)) { if (is_blocking_printing(obj_)) {
show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter); show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter);
return; return;
@@ -5482,8 +5521,8 @@ void SelectMachineDialog::reset_and_sync_ams_list()
item = new MaterialItem(m_filament_left_panel, colour_rgb, _L(display_materials[extruder])); item = new MaterialItem(m_filament_left_panel, colour_rgb, _L(display_materials[extruder]));
m_sizer_ams_mapping_left->Add(item, 0, wxALL, FromDIP(5)); m_sizer_ams_mapping_left->Add(item, 0, wxALL, FromDIP(5));
} }
else if (m_filaments_map[extruder] == 2) else // map == 2, or (non-BBL multi-nozzle) 3+; update_material_item_pos() collapses
{ { // these into the single panel when the device reports < 2 extruders.
item = new MaterialItem(m_filament_right_panel, colour_rgb, _L(display_materials[extruder])); item = new MaterialItem(m_filament_right_panel, colour_rgb, _L(display_materials[extruder]));
m_sizer_ams_mapping_right->Add(item, 0, wxALL, FromDIP(5)); m_sizer_ams_mapping_right->Add(item, 0, wxALL, FromDIP(5));
} }
+1 -1
View File
@@ -738,7 +738,7 @@ void SendMultiMachinePage::on_send(wxCommandEvent& event)
if (obj && obj->is_online() && !obj->can_abort() && !obj->is_in_upgrading() && it->second->get_state_selected() == 1 && it->second->state_printable <= 2) { if (obj && obj->is_online() && !obj->can_abort() && !obj->is_in_upgrading() && it->second->get_state_selected() == 1 && it->second->state_printable <= 2) {
if (!it->second->is_blocking_printing(obj)) { if (!wxGetApp().is_blocking_printing(obj)) {
PrintParams params = request_params(obj); PrintParams params = request_params(obj);
print_params.push_back(params); print_params.push_back(params);
} }
+108 -64
View File
@@ -24,6 +24,7 @@
#include "BitmapCache.hpp" #include "BitmapCache.hpp"
#include "DeviceCore/DevManager.h" #include "DeviceCore/DevManager.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevStorage.h" #include "DeviceCore/DevStorage.h"
#include "slic3r/Utils/FileTransferUtils.hpp" #include "slic3r/Utils/FileTransferUtils.hpp"
@@ -300,7 +301,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater)
m_storage_panel->Layout(); m_storage_panel->Layout();
// try to connect // try to connect
m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(400), -1), wxALIGN_CENTER_HORIZONTAL);
m_statictext_printer_msg->SetFont(::Label::Body_13); m_statictext_printer_msg->SetFont(::Label::Body_13);
m_statictext_printer_msg->SetForegroundColour(*wxBLACK); m_statictext_printer_msg->SetForegroundColour(*wxBLACK);
m_statictext_printer_msg->Hide(); m_statictext_printer_msg->Hide();
@@ -760,9 +761,25 @@ void SendToPrinterDialog::update_priner_status_msg(wxString msg, bool is_warning
if (str_new != str_old) { if (str_new != str_old) {
if (m_statictext_printer_msg->GetLabel() != msg) { if (m_statictext_printer_msg->GetLabel() != msg) {
m_statictext_printer_msg->SetLabel(msg); m_statictext_printer_msg->SetLabel(msg);
m_statictext_printer_msg->SetMinSize(wxSize(FromDIP(400), -1)); const int wrap_width = FromDIP(400);
m_statictext_printer_msg->SetMaxSize(wxSize(FromDIP(400), -1)); m_statictext_printer_msg->Wrap(wrap_width);
m_statictext_printer_msg->Wrap(FromDIP(400)); int line_count = 1;
const wxString wrapped_label = m_statictext_printer_msg->GetLabel();
for (size_t i = 0; i < wrapped_label.length(); ++i) {
if (wrapped_label[i] == '\n')
++line_count;
}
wxCoord text_width = 0;
wxCoord text_height = 0;
m_statictext_printer_msg->GetTextExtent(msg, &text_width, &text_height);
const int extent_line_count = text_width > 0 ?
std::max(1, (static_cast<int>(text_width) + wrap_width - 1) / wrap_width) : 1;
line_count = std::max(line_count, extent_line_count);
const int line_height = std::max(m_statictext_printer_msg->GetCharHeight(), static_cast<int>(text_height));
const int min_height = std::max(m_statictext_printer_msg->GetBestSize().GetHeight(),
line_count * line_height + FromDIP(2));
m_statictext_printer_msg->SetMinSize(wxSize(wrap_width, min_height));
m_statictext_printer_msg->SetMaxSize(wxDefaultSize);
m_statictext_printer_msg->Show(); m_statictext_printer_msg->Show();
Layout(); Layout();
Fit(); Fit();
@@ -1102,7 +1119,7 @@ void SendToPrinterDialog::update_user_printer()
wxArrayString machine_list_name; wxArrayString machine_list_name;
std::map<std::string, MachineObject*> option_list; std::map<std::string, MachineObject*> option_list;
option_list = dev->get_my_machine_list(); option_list = dev->get_my_machine_list(dev->get_current_printer_agent_id());
// same machine only appear once // same machine only appear once
for (auto it = option_list.begin(); it != option_list.end(); it++) { for (auto it = option_list.begin(); it != option_list.end(); it++) {
@@ -1260,7 +1277,7 @@ void SendToPrinterDialog::update_show_status()
reset_timeout(); reset_timeout();
// reading done // reading done
if (is_blocking_printing(obj_)) { if (wxGetApp().is_blocking_printing(obj_)) {
show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter); show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter);
return; return;
} }
@@ -1325,26 +1342,6 @@ void SendToPrinterDialog::update_show_status()
} }
} }
bool SendToPrinterDialog::is_blocking_printing(MachineObject* obj_)
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
auto target_model = obj_->printer_type;
if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
if (it == compatible_machine.end()) {
return true;
}
}
return false;
}
void SendToPrinterDialog::Enable_Refresh_Button(bool en) void SendToPrinterDialog::Enable_Refresh_Button(bool en)
{ {
if (!en) { if (!en) {
@@ -1486,6 +1483,9 @@ void SendToPrinterDialog::show_status(PrintDialogStatus status, std::vector<wxSt
Enable_Send_Button(false); Enable_Send_Button(false);
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
} else if (status == PrintDialogStatus::PrintStatusPublicInitFailed) { } else if (status == PrintDialogStatus::PrintStatusPublicInitFailed) {
wxString msg_text = _L(
"Failed to initialize the printer file transfer. Please check the connection and try again.");
update_print_status_msg(msg_text, true, true);
Enable_Send_Button(false); Enable_Send_Button(false);
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
} else if (status == PrintDialogStatus::PrintStatusPublicUploadFiled) { } else if (status == PrintDialogStatus::PrintStatusPublicUploadFiled) {
@@ -1663,30 +1663,18 @@ extern void refresh_agora_url(char const *device, char const *dev_ver, char
void SendToPrinterDialog::GetConnection() void SendToPrinterDialog::GetConnection()
{ {
DeviceManager *dm = GUI::wxGetApp().getDeviceManager(); DeviceManager *dm = GUI::wxGetApp().getDeviceManager();
MachineObject *obj = dm ? dm->get_selected_machine() : nullptr;
MachineObject *obj = dm->get_selected_machine(); if (!obj)
if (obj == nullptr) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : obj is empty"; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : obj is empty";
m_connection_status = ConnectionStatus::NOT_START; if (obj && !obj->get_file_remote())
}
int remote_proto = obj->get_file_remote();
if (!remote_proto) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : remote_proto is not support"; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : remote_proto is not support";
m_connection_status = ConnectionStatus::NOT_START; if (obj && obj->is_camera_busy_off())
} BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy";
if (obj->is_camera_busy_off()) { NetworkAgent* agent = wxGetApp().getAgent();
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy";
m_connection_status = ConnectionStatus::NOT_START;
}
NetworkAgent *agent = wxGetApp().getAgent(); if (m_url_timer && m_url_timer->IsRunning())
std::string agent_version = agent ? agent->get_version() : "";
std::string dev_ver = obj->get_ota_version();
std::string dev_id = obj->get_dev_id();
if (m_url_timer && m_url_timer->IsRunning())
{ {
m_url_timer->Stop(); m_url_timer->Stop();
} }
@@ -1709,19 +1697,40 @@ void SendToPrinterDialog::GetConnection()
m_url_timer->GetId()); m_url_timer->GetId());
m_url_timer->StartOnce(8000); m_url_timer->StartOnce(8000);
if (agent) { if (obj && agent)
{
std::string dev_ver = obj->get_ota_version();
std::string dev_id = obj->get_dev_id();
if (m_tcp_try_connect) { if (m_tcp_try_connect) {
std::string devIP = obj->get_dev_ip(); std::string devIP = obj->get_dev_ip();
std::string accessCode = obj->get_access_code(); std::string accessCode = obj->get_access_code();
std::string url = "bambu:///local/" + devIP + "?port=6000&user=" + "bblp" + "&passwd=" + accessCode; std::string url = "bambu:///local/" + devIP + "?port=6000&user=" + "bblp" + "&passwd=" + accessCode;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp, dev_id=" << dev_id
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url); << ", dev_ip=" << devIP << ", access_code_len=" << accessCode.size();
m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg) {
CallAfter([this, is_success, err_code, error_msg]() { try
OnConnection(is_success, err_code, error_msg); {
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg)
{
CallAfter([this, is_success, err_code, error_msg]()
{
OnConnection(is_success, err_code, error_msg);
});
}); });
}); m_filetransfer_tunnel->start_connect();
m_filetransfer_tunnel->start_connect(); }
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": tcp FileTransferTunnel unavailable for dev_id=" <<
dev_id
<< " dev_ip=" << devIP << ": " << e.what();
if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop();
m_filetransfer_tunnel.reset();
m_connection_status = ConnectionStatus::CONNECTION_FAILED;
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
}
} }
else if (m_tutk_try_connect) else if (m_tutk_try_connect)
{ {
@@ -1749,11 +1758,28 @@ void SendToPrinterDialog::GetConnection()
if (boost::algorithm::starts_with(url, "bambu:///")) if (boost::algorithm::starts_with(url, "bambu:///"))
{ {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tutk"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tutk";
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url); try
m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg) { {
CallAfter([this, is_success, err_code, error_msg]() { OnConnection(is_success, err_code, error_msg); }); m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
}); m_filetransfer_tunnel->on_connection(
m_filetransfer_tunnel->start_connect(); [this](bool is_success, int err_code, std::string error_msg)
{
CallAfter([this, is_success, err_code, error_msg]()
{
OnConnection(is_success, err_code, error_msg);
});
});
m_filetransfer_tunnel->start_connect();
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": tutk FileTransferTunnel unavailable: " << e.
what();
if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop();
m_filetransfer_tunnel.reset();
m_connection_status = ConnectionStatus::CONNECTION_FAILED;
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
}
} }
else else
{ {
@@ -1852,8 +1878,17 @@ void SendToPrinterDialog::ResetTunnelAndJob()
void SendToPrinterDialog::CreateMediaAbilityJob() void SendToPrinterDialog::CreateMediaAbilityJob()
{ {
nlohmann::json media_ability = {{"cmd_type", 7}}; nlohmann::json media_ability = {{"cmd_type", 7}};
m_filetransfer_mediability_job = std::make_unique<FileTransferJob>(module(), std::string(media_ability.dump())); try
{
m_filetransfer_mediability_job = std::make_unique<FileTransferJob>(module(), std::string(media_ability.dump()));
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what();
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
return;
}
m_filetransfer_mediability_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) { m_filetransfer_mediability_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) {
//this pl //this pl
CallAfter([this, res, resp_ec, json_res] { CallAfter([this, res, resp_ec, json_res] {
@@ -1908,11 +1943,20 @@ void SendToPrinterDialog::CreateUploadFileJob(const std::string &path, const std
{"cmd_type", 5}, {"cmd_type", 5},
}; };
upload_params["dest_storage"] = m_selected_storage; upload_params["dest_storage"] = m_selected_storage;
upload_params["dest_name"] = name; // filenme no path upload_params["dest_name"] = name; // filenme no path
upload_params["file_path"] = path; upload_params["file_path"] = path;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob";
m_filetransfer_uploadfile_job = std::make_unique<FileTransferJob>(module(), std::string(upload_params.dump())); try
{
m_filetransfer_uploadfile_job = std::make_unique<FileTransferJob>(module(), std::string(upload_params.dump()));
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what();
show_status(PrintDialogStatus::PrintStatusPublicUploadFiled);
return;
}
m_filetransfer_uploadfile_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) { // m_filetransfer_uploadfile_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) { //
CallAfter([this, res, resp_ec, json_res, bin_res] { CallAfter([this, res, resp_ec, json_res, bin_res] {
UploadFileRessultCallback(res, resp_ec,json_res, bin_res); UploadFileRessultCallback(res, resp_ec,json_res, bin_res);
-1
View File
@@ -181,7 +181,6 @@ public:
void reset_timeout(); void reset_timeout();
void update_user_printer(); void update_user_printer();
void update_show_status(); void update_show_status();
bool is_blocking_printing(MachineObject* obj_);
void prepare(int print_plate_idx); void prepare(int print_plate_idx);
void check_focus(wxWindow* window); void check_focus(wxWindow* window);
void check_fcous_state(wxWindow* window); void check_fcous_state(wxWindow* window);
+106 -105
View File
@@ -22,6 +22,7 @@
#include <wx/mstream.h> #include <wx/mstream.h>
#include <wx/sstream.h> #include <wx/sstream.h>
#include <wx/zstream.h> #include <wx/zstream.h>
#include <chrono>
#include "DeviceCore/DevBed.h" #include "DeviceCore/DevBed.h"
#include "DeviceCore/DevCtrl.h" #include "DeviceCore/DevCtrl.h"
@@ -1493,27 +1494,26 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
m_setting_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); m_setting_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24)));
m_setting_button->SetBackgroundColour(STATUS_TITLE_BG); m_setting_button->SetBackgroundColour(STATUS_TITLE_BG);
m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0); // m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0);
m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); // m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24)));
m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG); // m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG);
m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp()); // m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp());
m_camera_switch_button->Bind(wxEVT_LEFT_DOWN, &StatusBasePanel::on_camera_switch_toggled, this); // m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) {
m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) { // const std::string js_request_pip = R"(
const std::string js_request_pip = R"( // document.querySelector('video').requestPictureInPicture();
document.querySelector('video').requestPictureInPicture(); // )";
)"; // m_custom_camera_view->RunScript(js_request_pip);
m_custom_camera_view->RunScript(js_request_pip); // });
}); // m_camera_switch_button->Hide();
m_camera_switch_button->Hide();
m_bitmap_sdcard_img->SetToolTip(_L("Storage")); m_bitmap_sdcard_img->SetToolTip(_L("Storage"));
m_bitmap_timelapse_img->SetToolTip(_L("Timelapse")); m_bitmap_timelapse_img->SetToolTip(_L("Timelapse"));
m_bitmap_recording_img->SetToolTip(_L("Video")); m_bitmap_recording_img->SetToolTip(_L("Video"));
m_bitmap_vcamera_img->SetToolTip(_L("Go Live")); m_bitmap_vcamera_img->SetToolTip(_L("Go Live"));
m_setting_button->SetToolTip(_L("Camera Setting")); m_setting_button->SetToolTip(_L("Camera Setting"));
m_camera_switch_button->SetToolTip(_L("Switch Camera View")); // m_camera_switch_button->SetToolTip(_L("Switch Camera View"));
bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); // bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
bSizer_monitoring_title->Add(m_bitmap_sdcard_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_sdcard_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
bSizer_monitoring_title->Add(m_bitmap_timelapse_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_timelapse_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
bSizer_monitoring_title->Add(m_bitmap_recording_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_recording_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
@@ -1536,19 +1536,18 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString); m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString);
m_custom_camera_view->EnableContextMenu(false); m_custom_camera_view->EnableContextMenu(false);
Bind(wxEVT_WEBVIEW_NAVIGATING, &StatusBasePanel::on_webview_navigating, this, m_custom_camera_view->GetId()); Bind(wxEVT_WEBVIEW_NAVIGATING, &StatusBasePanel::on_webview_navigating, this, m_custom_camera_view->GetId());
m_web_media_controller = std::make_unique<WebMediaController>(m_custom_camera_view);
m_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40))); m_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40)));
m_media_play_ctrl->SetWebMediaController(m_web_media_controller.get());
m_custom_camera_view->Hide(); m_custom_camera_view->Hide();
m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) { // m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) {
if (evt.GetString() == "leavepictureinpicture") { // if (evt.GetString() == "leavepictureinpicture") {
// When leaving PiP, video gets paused in some cases and toggling play // // When leaving PiP, video gets paused in some cases and toggling play
// programmatically does not work. // // programmatically does not work.
m_custom_camera_view->Reload(); // m_custom_camera_view->Reload();
} // }
else if (evt.GetString() == "enterpictureinpicture") { // });
toggle_builtin_camera();
}
});
sizer->Add(m_media_ctrl, 1, wxEXPAND | wxALL, 0); sizer->Add(m_media_ctrl, 1, wxEXPAND | wxALL, 0);
sizer->Add(m_custom_camera_view, 1, wxEXPAND | wxALL, 0); sizer->Add(m_custom_camera_view, 1, wxEXPAND | wxALL, 0);
@@ -1558,10 +1557,6 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
// //
// sizer->Add(media_ctrl_panel, 1, wxEXPAND | wxALL, 1); // sizer->Add(media_ctrl_panel, 1, wxEXPAND | wxALL, 1);
if (wxGetApp().app_config->get("camera", "enable_custom_source") == "true") {
handle_camera_source_change();
}
return sizer; return sizer;
} }
@@ -2311,6 +2306,27 @@ void StatusPanel::update_camera_state(MachineObject* obj)
{ {
if (!obj) return; if (!obj) return;
auto agent = wxGetApp().getAgent();
const auto camera_mode = agent ? agent->get_camera_stream_mode() : CameraStreamMode::none;
const bool use_webview = camera_mode == CameraStreamMode::http_snapshot;
if (use_webview) {
//m_camera_switch_button->Hide();
if (!m_custom_camera_view->IsShown()) {
// why: do not reload the WebView URL per tick, or redirects can cause a reload loop.
// MediaPlayCtrl (via its WebMediaController) owns loading/playing the stream itself.
m_custom_camera_view->Show();
m_media_ctrl->Hide();
}
} else {
if (m_custom_camera_view->IsShown()) {
m_custom_camera_view->Hide();
// Stop the snapshot WebView before switching to native playback
// or leaving the camera mode.
m_media_play_ctrl->StopWebStream();
}
m_media_ctrl->Show();
}
//sdcard //sdcard
auto sdcard_state = obj->GetStorage()->get_sdcard_state(); auto sdcard_state = obj->GetStorage()->get_sdcard_state();
if (m_last_sdcard != sdcard_state) { if (m_last_sdcard != sdcard_state) {
@@ -2342,7 +2358,12 @@ void StatusPanel::update_camera_state(MachineObject* obj)
m_last_recording = obj->is_recording() ? 1 : 0; m_last_recording = obj->is_recording() ? 1 : 0;
} }
if (!m_bitmap_recording_img->IsShown()) { if (use_webview) {
if (m_bitmap_recording_img->IsShown()) {
m_bitmap_recording_img->Hide();
m_panel_monitoring_title->Layout();
}
} else if (!m_bitmap_recording_img->IsShown()) {
m_bitmap_recording_img->Show(); m_bitmap_recording_img->Show();
m_panel_monitoring_title->Layout(); m_panel_monitoring_title->Layout();
} }
@@ -2399,6 +2420,8 @@ void StatusPanel::update_camera_state(MachineObject* obj)
bool show_vcamera = m_media_play_ctrl->IsStreaming(); bool show_vcamera = m_media_play_ctrl->IsStreaming();
m_camera_popup->update(show_vcamera); m_camera_popup->update(show_vcamera);
} }
m_setting_button->Show(!use_webview);
} }
StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name) StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name)
@@ -3920,39 +3943,60 @@ void StatusPanel::update_cloud_subtask(MachineObject *obj)
update_calib_bitmap(); update_calib_bitmap();
if (obj->slice_info) { if (obj->slice_info) {
m_request_url = wxString(obj->slice_info->thumbnail_url); m_request_url = wxString(obj->slice_info->thumbnail_url);
if (!m_request_url.IsEmpty()) { load_thumbnail_from_url(m_request_url, obj);
wxImage img;
std::map<wxString, wxImage>::iterator it = img_list.find(m_request_url);
if (it != img_list.end()) {
if (m_current_print_mode != PrintingTaskType::CALIBRATION ||(m_calib_mode == CalibMode::Calib_Flow_Rate && m_calib_method == CalibrationMethod::CALI_METHOD_MANUAL)) {
img = it->second;
wxImage resize_img = img.Scale(m_project_task_panel->get_bitmap_thumbnail()->GetSize().x, m_project_task_panel->get_bitmap_thumbnail()->GetSize().y);
m_project_task_panel->set_thumbnail_img(resize_img, "");
m_project_task_panel->set_brightness_value(get_brightness_value(resize_img));
}
if (this->obj) {
m_project_task_panel->set_plate_index(obj->m_plate_index);
} else {
m_project_task_panel->set_plate_index(-1);
}
task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL;
BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image";
} else {
web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url);
BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url;
web_request.Start();
m_start_loading_thumbnail = false;
}
}
} }
} }
} }
bool StatusPanel::load_thumbnail_from_url(const wxString &url, MachineObject *obj)
{
if (url.IsEmpty())
return false;
wxImage img;
std::map<wxString, wxImage>::iterator it = img_list.find(url);
if (it != img_list.end()) {
if (m_current_print_mode != PrintingTaskType::CALIBRATION ||(m_calib_mode == CalibMode::Calib_Flow_Rate && m_calib_method == CalibrationMethod::CALI_METHOD_MANUAL)) {
img = it->second;
wxImage resize_img = img.Scale(m_project_task_panel->get_bitmap_thumbnail()->GetSize().x, m_project_task_panel->get_bitmap_thumbnail()->GetSize().y);
m_project_task_panel->set_thumbnail_img(resize_img, "");
m_project_task_panel->set_brightness_value(get_brightness_value(resize_img));
}
if (this->obj) {
m_project_task_panel->set_plate_index(obj->m_plate_index);
} else {
m_project_task_panel->set_plate_index(-1);
}
task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL;
BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image";
} else {
m_request_url = url;
web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url);
BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url;
web_request.Start();
m_start_loading_thumbnail = false;
}
return true;
}
void StatusPanel::update_sdcard_subtask(MachineObject *obj) void StatusPanel::update_sdcard_subtask(MachineObject *obj)
{ {
if (!obj) return; if (!obj) return;
if (!m_load_sdcard_thumbnail) { const wxString thumbnail_url = wxString(obj->m_agent_thumbnail_url);
if (!thumbnail_url.IsEmpty()) {
if (m_request_url != thumbnail_url || !m_load_sdcard_thumbnail) {
if (web_request.IsOk() && web_request.GetState() == wxWebRequest::State_Active)
web_request.Cancel();
update_calib_bitmap();
m_request_url = thumbnail_url;
load_thumbnail_from_url(thumbnail_url, obj);
m_load_sdcard_thumbnail = true;
}
return;
}
if (!m_load_sdcard_thumbnail || !m_request_url.IsEmpty()) {
update_calib_bitmap(); update_calib_bitmap();
if (m_current_print_mode != PrintingTaskType::CALIBRATION) { if (m_current_print_mode != PrintingTaskType::CALIBRATION) {
m_project_task_panel->get_bitmap_thumbnail()->SetBitmap(m_thumbnail_sdcard.bmp()); m_project_task_panel->get_bitmap_thumbnail()->SetBitmap(m_thumbnail_sdcard.bmp());
@@ -3960,6 +4004,7 @@ void StatusPanel::update_sdcard_subtask(MachineObject *obj)
} }
task_thumbnail_state = ThumbnailState::SDCARD_THUMBNAIL; task_thumbnail_state = ThumbnailState::SDCARD_THUMBNAIL;
m_load_sdcard_thumbnail = true; m_load_sdcard_thumbnail = true;
m_request_url.clear();
} }
} }
@@ -4689,11 +4734,11 @@ void StatusPanel::on_ams_refresh_rfid(wxCommandEvent &event)
try { try {
if (!use_new_command) { if (!use_new_command) {
int tray_index = atoi(curr_ams_id.c_str()) * 4 + atoi(slot_it->second->id.c_str()); int tray_index = atoi(curr_ams_id.c_str()) * 4 + atoi(slot_it->second->id.c_str());
obj->command_ams_refresh_rfid(std::to_string(tray_index)); obj->command_ams_refresh_rfid(-1, tray_index);
} }
if (use_new_command) { if (use_new_command) {
obj->command_ams_refresh_rfid2(stoi(curr_ams_id), stoi(curr_can_id)); obj->command_ams_refresh_rfid(stoi(curr_ams_id), stoi(curr_can_id));
} }
} catch (...) { } catch (...) {
@@ -4959,7 +5004,6 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event)
} }
sdcard_hint_dlg->on_show(); sdcard_hint_dlg->on_show();
}); });
m_camera_popup->Bind(EVT_CAM_SOURCE_CHANGE, &StatusPanel::on_camera_source_change, this);
wxWindow* ctrl = (wxWindow*)event.GetEventObject(); wxWindow* ctrl = (wxWindow*)event.GetEventObject();
wxPoint pos = ctrl->ClientToScreen(wxPoint(0, 0)); wxPoint pos = ctrl->ClientToScreen(wxPoint(0, 0));
wxSize sz = ctrl->GetSize(); wxSize sz = ctrl->GetSize();
@@ -4971,54 +5015,6 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event)
} }
} }
void StatusBasePanel::on_camera_source_change(wxCommandEvent& event)
{
handle_camera_source_change();
}
void StatusBasePanel::handle_camera_source_change()
{
const auto new_cam_url = wxGetApp().app_config->get("camera", "custom_source");
const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
if (enabled && !new_cam_url.empty()) {
m_custom_camera_view->LoadURL(new_cam_url);
toggle_custom_camera();
m_camera_switch_button->Show();
} else {
toggle_builtin_camera();
m_camera_switch_button->Hide();
}
}
void StatusBasePanel::toggle_builtin_camera()
{
m_custom_camera_view->Hide();
m_media_ctrl->Show();
m_media_play_ctrl->Show();
}
void StatusBasePanel::toggle_custom_camera()
{
const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
if (enabled) {
m_custom_camera_view->Show();
m_media_ctrl->Hide();
m_media_play_ctrl->Hide();
}
}
void StatusBasePanel::on_camera_switch_toggled(wxMouseEvent& event)
{
const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
if (enabled && m_media_ctrl->IsShown()) {
toggle_custom_camera();
} else {
toggle_builtin_camera();
}
}
void StatusBasePanel::remove_controls() void StatusBasePanel::remove_controls()
{ {
const std::string js_cleanup_video_element = R"( const std::string js_cleanup_video_element = R"(
@@ -5171,6 +5167,11 @@ bool StatusPanel::is_stage_list_info_changed(MachineObject *obj)
void StatusPanel::set_default() void StatusPanel::set_default()
{ {
BOOST_LOG_TRIVIAL(trace) << "status_panel: set_default"; BOOST_LOG_TRIVIAL(trace) << "status_panel: set_default";
if (m_custom_camera_view->IsShown()) {
m_custom_camera_view->Hide();
m_media_ctrl->Show();
m_media_play_ctrl->StopWebStream();
}
obj = nullptr; obj = nullptr;
last_subtask = nullptr; last_subtask = nullptr;
last_tray_exist_bits = -1; last_tray_exist_bits = -1;
+6 -6
View File
@@ -14,7 +14,9 @@
#include <wx/sizer.h> #include <wx/sizer.h>
#include <wx/gbsizer.h> #include <wx/gbsizer.h>
#include <wx/webrequest.h> #include <wx/webrequest.h>
#include <memory>
#include "MediaPlayCtrl.h" #include "MediaPlayCtrl.h"
#include "WebMediaController.hpp"
#include "AMSSetting.hpp" #include "AMSSetting.hpp"
#include "Calibration.hpp" #include "Calibration.hpp"
#include "CalibrationWizardPage.hpp" #include "CalibrationWizardPage.hpp"
@@ -439,7 +441,7 @@ protected:
wxStaticBitmap *m_bitmap_sdcard_img; wxStaticBitmap *m_bitmap_sdcard_img;
wxStaticBitmap *m_bitmap_static_use_time; wxStaticBitmap *m_bitmap_static_use_time;
wxStaticBitmap *m_bitmap_static_use_weight; wxStaticBitmap *m_bitmap_static_use_weight;
wxStaticBitmap* m_camera_switch_button; // wxStaticBitmap* m_camera_switch_button;
wxMediaCtrl3 * m_media_ctrl; wxMediaCtrl3 * m_media_ctrl;
@@ -461,6 +463,8 @@ protected:
ScalableButton *m_button_abort; ScalableButton *m_button_abort;
Button * m_button_clean; Button * m_button_clean;
wxWebView * m_custom_camera_view{nullptr}; wxWebView * m_custom_camera_view{nullptr};
std::unique_ptr<WebMediaController> m_web_media_controller;
wxSimplebook* m_extruder_book; wxSimplebook* m_extruder_book;
std::vector<ExtruderImage *> m_extruderImage; std::vector<ExtruderImage *> m_extruderImage;
@@ -576,13 +580,8 @@ protected:
virtual void on_axis_ctrl_e_up_10(wxCommandEvent &event) { event.Skip(); } virtual void on_axis_ctrl_e_up_10(wxCommandEvent &event) { event.Skip(); }
virtual void on_axis_ctrl_e_down_10(wxCommandEvent &event) { event.Skip(); } virtual void on_axis_ctrl_e_down_10(wxCommandEvent &event) { event.Skip(); }
virtual void on_nozzle_selected(wxCommandEvent &event) { event.Skip(); } virtual void on_nozzle_selected(wxCommandEvent &event) { event.Skip(); }
void on_camera_source_change(wxCommandEvent& event);
void handle_camera_source_change();
void remove_controls(); void remove_controls();
void on_webview_navigating(wxWebViewEvent& evt); void on_webview_navigating(wxWebViewEvent& evt);
void on_camera_switch_toggled(wxMouseEvent& event);
void toggle_custom_camera();
void toggle_builtin_camera();
public: public:
StatusBasePanel(wxWindow * parent, StatusBasePanel(wxWindow * parent,
@@ -629,6 +628,7 @@ class StatusPanel : public StatusBasePanel
{ {
private: private:
friend class MonitorPanel; friend class MonitorPanel;
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
protected: protected:
std::shared_ptr<SliceInfoPopup> m_slice_info_popup; std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
+32 -21
View File
@@ -1742,6 +1742,8 @@ void SyncAmsInfoDialog::show_status(PrintDialogStatus status, std::vector<wxStri
update_print_status_msg(msg_text, true, true); update_print_status_msg(msg_text, true, true);
} else if (status == PrintDialogStatus::PrintStatusAmsMappingSuccess) { } else if (status == PrintDialogStatus::PrintStatusAmsMappingSuccess) {
update_print_status_msg(wxEmptyString, false, false); update_print_status_msg(wxEmptyString, false, false);
} else if (status == PrintDialogStatus::PrintStatusOptionalPrinterModel) {
update_print_status_msg(PrePrintChecker::get_pre_state_msg(PrintDialogStatus::PrintStatusOptionalPrinterModel), true, true);
} else if (status == PrintDialogStatus::PrintStatusAmsMappingInvalid) { } else if (status == PrintDialogStatus::PrintStatusAmsMappingInvalid) {
update_print_status_msg(wxEmptyString, true, false); update_print_status_msg(wxEmptyString, true, false);
} else if (status == PrintDialogStatus::PrintStatusAmsMappingMixInvalid) { } else if (status == PrintDialogStatus::PrintStatusAmsMappingMixInvalid) {
@@ -1861,26 +1863,13 @@ void SyncAmsInfoDialog::on_cancel(wxCloseEvent &event)
bool SyncAmsInfoDialog::is_blocking_printing(MachineObject *obj_) bool SyncAmsInfoDialog::is_blocking_printing(MachineObject *obj_)
{ {
DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (m_print_type == PrintFromType::FROM_NORMAL)
if (!dev) return true; return wxGetApp().is_blocking_printing(obj_);
auto target_model = obj_->printer_type;
std::string source_model = "";
if (m_print_type == PrintFromType::FROM_NORMAL) { std::string source_model;
PresetBundle *preset_bundle = wxGetApp().preset_bundle; if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty())
source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id;
return wxGetApp().is_blocking_printing(obj_, source_model);
} else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW) {
if (m_required_data_plate_data_list.size() > 0) { source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id; }
}
if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
if (it == compatible_machine.end()) { return true; }
}
return false;
} }
bool SyncAmsInfoDialog::is_same_nozzle_type(std::string &filament_type, NozzleType &tag_nozzle_type) bool SyncAmsInfoDialog::is_same_nozzle_type(std::string &filament_type, NozzleType &tag_nozzle_type)
@@ -1930,7 +1919,13 @@ bool SyncAmsInfoDialog::is_same_printer_model()
if (obj_ == nullptr) { return result; } if (obj_ == nullptr) { return result; }
PresetBundle *preset_bundle = wxGetApp().preset_bundle; PresetBundle *preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) != obj_->printer_type) { const std::string source_model = preset_bundle ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) : std::string();
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type)) {
return true;
}
if (preset_bundle && source_model != obj_->printer_type) {
if ((obj_->is_support_upgrade_kit && obj_->installed_upgrade_kit) && (preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == "C12")) { if ((obj_->is_support_upgrade_kit && obj_->installed_upgrade_kit) && (preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == "C12")) {
return true; return true;
} }
@@ -2131,7 +2126,7 @@ void SyncAmsInfoDialog::update_user_printer()
std::map<std::string, MachineObject *> option_list; std::map<std::string, MachineObject *> option_list;
// user machine list // user machine list
option_list = dev->get_my_machine_list(); option_list = dev->get_my_machine_list(dev->get_current_printer_agent_id());
// same machine only appear once // same machine only appear once
for (auto it = option_list.begin(); it != option_list.end(); it++) { for (auto it = option_list.begin(); it != option_list.end(); it++) {
@@ -2264,6 +2259,18 @@ void SyncAmsInfoDialog::update_show_status()
reset_timeout(); reset_timeout();
bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type);
if (m_print_type == PrintFromType::FROM_NORMAL) {
if (PresetBundle *preset_bundle = wxGetApp().preset_bundle) {
has_optional_printer_model = has_optional_printer_model ||
DevPrinterConfigUtil::is_optional_printer_model_id(
preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle));
}
} else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty()) {
has_optional_printer_model = has_optional_printer_model ||
DevPrinterConfigUtil::is_optional_printer_model_id(m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id);
}
if (!obj_->GetConfig()->SupportPrintAllPlates() && m_print_plate_idx == PLATE_ALL_IDX) { if (!obj_->GetConfig()->SupportPrintAllPlates() && m_print_plate_idx == PLATE_ALL_IDX) {
show_status(PrintDialogStatus::PrintStatusNotSupportedPrintAll); show_status(PrintDialogStatus::PrintStatusNotSupportedPrintAll);
return; return;
@@ -2357,6 +2364,9 @@ void SyncAmsInfoDialog::update_show_status()
} }
} }
} }
if (has_optional_printer_model)
show_status(PrintDialogStatus::PrintStatusOptionalPrinterModel);
} }
bool SyncAmsInfoDialog::has_timelapse_warning() bool SyncAmsInfoDialog::has_timelapse_warning()
@@ -3206,6 +3216,7 @@ SyncAmsInfoDialog::~SyncAmsInfoDialog() {
void SyncAmsInfoDialog::set_info(SyncInfo &info) void SyncAmsInfoDialog::set_info(SyncInfo &info)
{ {
m_input_info = info; m_input_info = info;
reinit_dialog();
} }
void SyncAmsInfoDialog::update_lan_machine_list() void SyncAmsInfoDialog::update_lan_machine_list()
+90
View File
@@ -0,0 +1,90 @@
#include "WebMediaController.hpp"
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
namespace {
void initialize_webview(wxWebView* webview)
{
if (!webview)
return;
webview->SetBackgroundColour(*wxBLACK);
webview->SetPage("<html><head><style>html,body{margin:0;height:100%;background:#000;}</style></head><body></body></html>", "");
}
} // namespace
WebMediaController::WebMediaController(wxWebView* webview)
: m_webview(webview)
{
initialize_webview(m_webview);
}
void WebMediaController::Load(wxURI url)
{
m_url = url.BuildURI().ToStdString();
}
void WebMediaController::set_mode(CameraStreamMode mode)
{
m_stream_mode = mode;
}
void WebMediaController::Play()
{
if (!m_webview)
return;
wxString url = wxString::FromUTF8(m_url);
wxString html = "<html><head><style>"
"html,body{margin:0;height:100%;background:#000;overflow:hidden;}"
"img{width:100%;height:100%;object-fit:contain;display:block;}"
"</style></head><body><img id=\"camera-frame\"";
if (m_stream_mode == CameraStreamMode::http_snapshot) {
html += " data-camera-url=\"" + url +
"\"><script>"
"const cameraFrame=document.getElementById('camera-frame');"
"const cameraUrl=cameraFrame.dataset.cameraUrl;"
"let cameraFrameLoading=false;"
"function refreshCameraFrame(){"
"if(cameraFrameLoading)return;"
"cameraFrameLoading=true;"
"const nextFrame=new Image();"
"nextFrame.onload=function(){cameraFrame.src=nextFrame.src;cameraFrameLoading=false;};"
"nextFrame.onerror=function(){cameraFrameLoading=false;};"
"nextFrame.src=cameraUrl+(cameraUrl.indexOf('?')>=0?'&':'?')+'_orca_frame='+Date.now();"
"}"
"let cameraRefreshInterval=null;"
"function stopCameraRefresh(){"
"if(cameraRefreshInterval!==null){"
"clearInterval(cameraRefreshInterval);"
"cameraRefreshInterval=null;"
"}"
"}"
"refreshCameraFrame();"
"cameraRefreshInterval = setInterval(refreshCameraFrame,200);"
"</script></body></html>";
m_webview->SetPage(html, url);
} else {
// Load MJPEG streams as the top-level document. Some embedded WebView
// backends buffer a multipart stream when it is used as an <img> resource,
// which introduces noticeable live-view latency.
m_webview->LoadURL(url);
}
}
void WebMediaController::Stop()
{
if (m_webview) {
m_webview->RunScript("if(typeof stopCameraRefresh==='function') stopCameraRefresh();");
m_webview->Stop();
m_webview->SetPage("", "about:blank");
m_webview->ClearHistory();
}
m_url.clear();
}
}} // namespace Slic3r::GUI
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <slic3r/GUI/IMediaController.hpp>
#include <slic3r/Utils/IPrinterAgent.hpp>
#include <string>
class wxWebView;
namespace Slic3r { namespace GUI {
class WebMediaController : public IMediaController
{
public:
explicit WebMediaController(wxWebView* webview);
void Load(wxURI url) override;
void set_mode(CameraStreamMode mode);
void Play() override;
void Stop() override;
private:
wxWebView* m_webview = nullptr;
std::string m_url;
CameraStreamMode m_stream_mode = CameraStreamMode::http;
};
}} // namespace Slic3r::GUI
+401
View File
@@ -0,0 +1,401 @@
#include "WebRtcMediaController.hpp"
#include <rtc/common.hpp>
#include <rtc/rtc.hpp>
#include <mutex>
#include <wx/mstream.h>
#include <boost/log/trivial.hpp>
namespace {
void init_rtc_logger_once()
{
static std::once_flag flag;
std::call_once(flag, [] {
rtc::InitLogger(rtc::LogLevel::Warning, [](rtc::LogLevel level, std::string message) {
BOOST_LOG_TRIVIAL(trace) << "[rtc:" << static_cast<int>(level) << "] " << message;
});
});
}
} // namespace
namespace Slic3r { namespace GUI {
WebRtcMediaController::WebRtcMediaController(std::function<void(const wxImage&, wxSize)> frame_sink,
std::function<void(Status)> on_status)
: m_frame_sink(std::move(frame_sink))
, m_on_status(std::move(on_status))
{
}
WebRtcMediaController::~WebRtcMediaController()
{
Stop();
}
void WebRtcMediaController::report(Status status)
{
status.epoch = m_epoch.load();
BOOST_LOG_TRIVIAL(info) << "WebRTC: report kind=" << static_cast<int>(status.kind)
<< " code=" << static_cast<int>(status.code) << " epoch=" << status.epoch;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (status.kind == Status::Connecting)
m_state = static_cast<wxMediaState>(4);
else if (status.kind == Status::Playing)
m_state = wxMEDIASTATE_PLAYING;
else
m_state = static_cast<wxMediaState>(3);
}
if (m_on_status)
m_on_status(status);
}
void WebRtcMediaController::set_signalling_channel(std::unique_ptr<ICameraSignalingChannel> channel)
{
m_pending_signaling = std::move(channel);
}
void WebRtcMediaController::Play()
{
std::unique_ptr<ICameraSignalingChannel> channel = std::move(m_pending_signaling);
// Tear down any previous attempt WITHOUT notifying: the Stopped that would
// otherwise be delivered (async, via CallAfter) races the new attempt's
// Connecting and makes the consumer cancel a session that is mid-connect.
teardown(false);
if (!channel)
return;
m_epoch.fetch_add(1);
m_alive.store(true);
{
std::lock_guard<std::mutex> lock(m_mutex);
m_signaling = std::move(channel);
m_jpeg_queue.clear();
m_pending_candidates.clear();
m_remote_description_set = false;
m_video_size = wxDefaultSize;
m_has_frame = false;
m_last_frame_time = {};
}
ICameraSignalingChannel* signaling = nullptr;
{
std::lock_guard<std::mutex> lock(m_mutex);
signaling = m_signaling.get();
}
signaling->on_ready = [this](std::vector<CameraIceServer> servers) {
if (m_alive.load())
on_ready(std::move(servers));
};
signaling->on_answer = [this](std::string sdp) {
if (m_alive.load())
on_answer(std::move(sdp));
};
signaling->on_ice = [this](std::string candidate, std::string mid) {
if (m_alive.load())
on_ice(std::move(candidate), std::move(mid));
};
signaling->on_unavailable = [this](CameraUnavailableReason reason, std::string detail) {
if (m_alive.load())
on_unavailable(reason, std::move(detail));
};
m_decode_thread = std::thread([this] { decode_loop(); });
report({Status::Connecting});
signaling->open();
}
void WebRtcMediaController::Stop()
{
teardown(true);
}
void WebRtcMediaController::teardown(bool notify)
{
const bool was_alive = m_alive.exchange(false);
if (!was_alive && !m_decode_thread.joinable())
return;
m_cond.notify_all();
std::unique_ptr<ICameraSignalingChannel> signaling;
std::shared_ptr<rtc::PeerConnection> peer_connection;
{
std::lock_guard<std::mutex> lock(m_mutex);
signaling = std::move(m_signaling);
peer_connection = std::move(m_peer_connection);
m_data_channel.reset();
}
if (signaling)
signaling->close();
if (peer_connection)
peer_connection->close();
if (m_decode_thread.joinable())
m_decode_thread.join();
{
std::lock_guard<std::mutex> lock(m_mutex);
m_jpeg_queue.clear();
}
if (was_alive && notify)
report({Status::Stopped});
}
wxMediaState WebRtcMediaController::GetState()
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_state;
}
wxSize WebRtcMediaController::GetVideoSize() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_video_size;
}
void WebRtcMediaController::bind_data_channel(const std::shared_ptr<rtc::DataChannel>& dc)
{
const std::string label = dc->label();
dc->onOpen([label] { BOOST_LOG_TRIVIAL(info) << "WebRTC: data channel '" << label << "' open"; });
dc->onClosed([label] { BOOST_LOG_TRIVIAL(info) << "WebRTC: data channel '" << label << "' closed"; });
dc->onError([label](std::string e) {
BOOST_LOG_TRIVIAL(warning) << "WebRTC: data channel '" << label << "' error: " << e;
});
dc->onMessage(
[this](rtc::binary data) {
if (m_alive.load())
enqueue_jpeg(std::vector<std::byte>(data.begin(), data.end()));
},
[](rtc::string) {});
}
void WebRtcMediaController::on_ready(std::vector<CameraIceServer> servers)
{
init_rtc_logger_once();
rtc::Configuration configuration;
// Allow complete-JPEG DataChannel messages up to 1 MiB. This value is
// advertised in SDP and becomes the upper bound for frames OrcaSonar can
// send to OrcaSlicer.
configuration.maxMessageSize = 1024 * 1024;
for (const CameraIceServer& server : servers) {
try {
rtc::IceServer ice_server(server.urls);
ice_server.username = server.username;
ice_server.password = server.credential;
configuration.iceServers.emplace_back(std::move(ice_server));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "WebRTC: invalid ICE server: " << e.what();
}
}
BOOST_LOG_TRIVIAL(info) << "WebRTC: creating peer connection with " << configuration.iceServers.size() << " ice servers";
auto peer_connection = std::make_shared<rtc::PeerConnection>(std::move(configuration));
peer_connection->onLocalDescription([this](rtc::Description description) {
if (!m_alive.load())
return;
const std::string sdp(description);
BOOST_LOG_TRIVIAL(info) << "WebRTC: local description ready (" << description.typeString()
<< "), OFFER SDP:\n" << sdp;
std::lock_guard<std::mutex> lock(m_mutex);
if (m_signaling)
m_signaling->send_offer(sdp);
});
peer_connection->onLocalCandidate([this](rtc::Candidate candidate) {
if (!m_alive.load())
return;
std::lock_guard<std::mutex> lock(m_mutex);
if (m_signaling)
m_signaling->send_ice(std::string(candidate), candidate.mid());
});
peer_connection->onStateChange([this](rtc::PeerConnection::State state) {
BOOST_LOG_TRIVIAL(info) << "WebRTC: peer state -> " << static_cast<int>(state);
if (!m_alive.load())
return;
if (state == rtc::PeerConnection::State::Failed || state == rtc::PeerConnection::State::Disconnected)
report({Status::Failed, Status::ICE_FAILED});
});
peer_connection->onGatheringStateChange([](rtc::PeerConnection::GatheringState state) {
BOOST_LOG_TRIVIAL(info) << "WebRTC: gathering state -> " << static_cast<int>(state);
});
// Accept a DataChannel opened by the remote peer (OrcaSonar may create the
// "camera" channel from its side rather than answering the one we offer).
peer_connection->onDataChannel([this](std::shared_ptr<rtc::DataChannel> dc) {
BOOST_LOG_TRIVIAL(info) << "WebRTC: remote opened data channel '" << dc->label() << "'";
bind_data_channel(dc);
std::lock_guard<std::mutex> lock(m_mutex);
m_data_channel = std::move(dc);
});
rtc::DataChannelInit init;
init.reliability.unordered = true;
// init.reliability.maxPacketLifeTime = std::chrono::milliseconds(350);
// Request one complete JPEG frame per DataChannel message. OrcaSonar
// keeps the legacy chunked protocol for clients that omit this property.
init.protocol = "orca-jpeg";
auto data_channel = peer_connection->createDataChannel("camera", init);
if (data_channel)
bind_data_channel(data_channel);
// Camera media is carried as one complete JPEG per DataChannel message;
// no RTP video track or application-level framing is required.
std::shared_ptr<rtc::PeerConnection> peer_for_description;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_alive.load())
return;
m_peer_connection = std::move(peer_connection);
peer_for_description = m_peer_connection;
m_data_channel = std::move(data_channel);
}
if (peer_for_description)
peer_for_description->setLocalDescription();
}
void WebRtcMediaController::on_answer(std::string sdp)
{
std::shared_ptr<rtc::PeerConnection> peer_connection;
{
std::lock_guard<std::mutex> lock(m_mutex);
peer_connection = m_peer_connection;
}
BOOST_LOG_TRIVIAL(info) << "WebRTC: applying remote answer (" << sdp.size() << " bytes), ANSWER SDP:\n" << sdp;
if (!peer_connection)
return;
try {
peer_connection->setRemoteDescription(rtc::Description(sdp, "answer"));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "WebRTC: setRemoteDescription failed: " << e.what();
report({Status::Failed, Status::ICE_FAILED});
return;
}
// Flush any remote candidates that arrived before the answer.
std::vector<std::pair<std::string, std::string>> pending;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_remote_description_set = true;
pending.swap(m_pending_candidates);
}
BOOST_LOG_TRIVIAL(info) << "WebRTC: remote description set, flushing " << pending.size()
<< " buffered candidate(s)";
for (const auto& c : pending) {
try {
peer_connection->addRemoteCandidate(rtc::Candidate(c.first, c.second));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "WebRTC: addRemoteCandidate (buffered) failed: " << e.what();
}
}
}
void WebRtcMediaController::on_ice(std::string candidate, std::string mid)
{
std::shared_ptr<rtc::PeerConnection> peer_connection;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_remote_description_set) {
m_pending_candidates.emplace_back(std::move(candidate), std::move(mid));
return;
}
peer_connection = m_peer_connection;
}
if (!peer_connection)
return;
try {
peer_connection->addRemoteCandidate(rtc::Candidate(candidate, mid));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "WebRTC: addRemoteCandidate failed: " << e.what();
}
}
void WebRtcMediaController::on_unavailable(CameraUnavailableReason reason, std::string detail)
{
BOOST_LOG_TRIVIAL(warning) << "WebRTC camera unavailable: " << detail;
Status::Code code = Status::UNAVAILABLE_ERROR;
if (reason == CameraUnavailableReason::Busy)
code = Status::UNAVAILABLE_BUSY;
else if (reason == CameraUnavailableReason::Disabled)
code = Status::UNAVAILABLE_DISABLED;
else if (reason == CameraUnavailableReason::Closed)
code = Status::SIGNALING_CLOSED;
report({Status::Failed, code});
}
void WebRtcMediaController::enqueue_jpeg(std::vector<std::byte> jpeg)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_jpeg_queue.size() >= 4)
m_jpeg_queue.pop_front();
m_jpeg_queue.emplace_back(std::move(jpeg));
m_cond.notify_one();
}
void WebRtcMediaController::deliver_jpeg(std::vector<std::byte> jpeg)
{
const auto now = std::chrono::steady_clock::now();
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_last_frame_time != std::chrono::steady_clock::time_point{} &&
now - m_last_frame_time < std::chrono::milliseconds(33))
return;
m_last_frame_time = now;
}
wxMemoryInputStream stream(jpeg.data(), jpeg.size());
wxImage image;
if (!image.LoadFile(stream, wxBITMAP_TYPE_JPEG)) {
report({Status::Failed, Status::DECODE_ERROR});
return;
}
bool first_frame = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_video_size = image.GetSize();
first_frame = !m_has_frame;
m_has_frame = true;
}
if (m_frame_sink)
m_frame_sink(image, image.GetSize());
if (first_frame)
report({Status::Playing});
}
void WebRtcMediaController::decode_loop()
{
int stall_polls = 0;
std::unique_lock<std::mutex> lock(m_mutex);
while (m_alive.load()) {
const bool woke = m_cond.wait_for(lock, std::chrono::seconds(2), [this] {
return !m_alive.load() || !m_jpeg_queue.empty();
});
if (!m_alive.load())
break;
if (!woke && !m_has_frame) {
const int pc_state = m_peer_connection ? static_cast<int>(m_peer_connection->state()) : -1;
std::string dc = "none";
if (m_data_channel)
dc = "label='" + m_data_channel->label() + "' open=" +
(m_data_channel->isOpen() ? "1" : "0");
lock.unlock();
BOOST_LOG_TRIVIAL(info) << "WebRTC: waiting for frames; peer_state=" << pc_state
<< " data_channel=" << dc;
if (++stall_polls >= 8) { // ~16s connected with no frame -> give up so the UI can retry
report({Status::Failed, Status::TIMEOUT});
lock.lock();
break;
}
lock.lock();
continue;
}
stall_polls = 0;
if (!m_jpeg_queue.empty()) {
auto jpeg = std::move(m_jpeg_queue.front());
m_jpeg_queue.pop_front();
lock.unlock();
deliver_jpeg(std::move(jpeg));
lock.lock();
}
}
}
}} // namespace Slic3r::GUI
+93
View File
@@ -0,0 +1,93 @@
#pragma once
#include "IMediaController.hpp"
#include <slic3r/Utils/ICameraSignalingChannel.hpp>
#include <wx/image.h>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
namespace rtc {
class DataChannel;
class PeerConnection;
}
namespace Slic3r { namespace GUI {
class WebRtcMediaController : public IMediaController {
public:
struct Status {
enum Kind { Connecting, Playing, Stopped, Failed } kind = Stopped;
enum Code {
ICE_FAILED,
SIGNALING_CLOSED,
UNAVAILABLE_BUSY,
UNAVAILABLE_ERROR,
UNAVAILABLE_DISABLED,
DECODE_ERROR,
TIMEOUT,
} code = ICE_FAILED;
// Identifies the Play attempt this status belongs to, so the
// consumer can drop CallAfter-queued events from a superseded attempt.
std::uint64_t epoch = 0;
};
WebRtcMediaController(std::function<void(const wxImage&, wxSize)> frame_sink,
std::function<void(Status)> on_status);
~WebRtcMediaController() override;
void set_signalling_channel(std::unique_ptr<ICameraSignalingChannel> channel);
std::uint64_t epoch() const { return m_epoch.load(); }
bool is_active() const { return m_alive.load(); }
void Load(wxURI) override {}
void Play() override;
void Stop() override;
wxMediaState GetState() override;
wxSize GetVideoSize() const override;
private:
void teardown(bool notify);
void report(Status status);
void bind_data_channel(const std::shared_ptr<rtc::DataChannel>& dc);
void on_ready(std::vector<CameraIceServer> servers);
void on_answer(std::string sdp);
void on_ice(std::string candidate, std::string mid);
void on_unavailable(CameraUnavailableReason reason, std::string detail);
void decode_loop();
void enqueue_jpeg(std::vector<std::byte> jpeg);
void deliver_jpeg(std::vector<std::byte> jpeg);
mutable std::mutex m_mutex;
std::condition_variable m_cond;
std::deque<std::vector<std::byte>> m_jpeg_queue;
// Remote candidates can arrive before the answer; libdatachannel rejects
// addRemoteCandidate until a remote description is set, so buffer them.
std::vector<std::pair<std::string, std::string>> m_pending_candidates;
bool m_remote_description_set = false;
std::unique_ptr<ICameraSignalingChannel> m_pending_signaling;
std::unique_ptr<ICameraSignalingChannel> m_signaling;
std::shared_ptr<rtc::PeerConnection> m_peer_connection;
std::shared_ptr<rtc::DataChannel> m_data_channel;
std::thread m_decode_thread;
std::atomic<bool> m_alive{false};
std::atomic<std::uint64_t> m_epoch{0};
wxMediaState m_state = static_cast<wxMediaState>(3);
wxSize m_video_size = wxDefaultSize;
std::function<void(const wxImage&, wxSize)> m_frame_sink;
std::function<void(Status)> m_on_status;
bool m_has_frame = false;
std::chrono::steady_clock::time_point m_last_frame_time{};
};
}} // namespace Slic3r::GUI
+366 -84
View File
@@ -4,6 +4,14 @@
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <wx/dcclient.h> #include <wx/dcclient.h>
#include <cstdarg>
#include <cstdlib>
#include <cstring>
#include <mutex>
extern "C" {
#include <libavformat/avformat.h>
#include <libavutil/log.h>
}
#ifdef __WIN32__ #ifdef __WIN32__
#include <versionhelpers.h> #include <versionhelpers.h>
#include <wx/msw/registry.h> #include <wx/msw/registry.h>
@@ -43,9 +51,13 @@ wxMediaCtrl3::~wxMediaCtrl3()
m_thread.join(); m_thread.join();
} }
static void adjust_frame_size(wxSize& frame, wxSize const& video, wxSize const& window);
void wxMediaCtrl3::Load(wxURI url) void wxMediaCtrl3::Load(wxURI url)
{ {
std::unique_lock<std::mutex> lk(m_mutex); std::unique_lock<std::mutex> lk(m_mutex);
if (m_external)
return;
m_video_size = wxDefaultSize; m_video_size = wxDefaultSize;
m_error = 0; m_error = 0;
m_url.reset(new wxURI(url)); m_url.reset(new wxURI(url));
@@ -55,6 +67,8 @@ void wxMediaCtrl3::Load(wxURI url)
void wxMediaCtrl3::Play() void wxMediaCtrl3::Play()
{ {
std::unique_lock<std::mutex> lk(m_mutex); std::unique_lock<std::mutex> lk(m_mutex);
if (m_external)
return;
if (m_state != wxMEDIASTATE_PLAYING) { if (m_state != wxMEDIASTATE_PLAYING) {
m_state = wxMEDIASTATE_PLAYING; m_state = wxMEDIASTATE_PLAYING;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
@@ -74,6 +88,62 @@ void wxMediaCtrl3::Stop()
Refresh(); Refresh();
} }
void wxMediaCtrl3::SetExternalFrame(const wxImage& frame, wxSize videoSize)
{
if (!frame.IsOk())
return;
{
std::unique_lock<std::mutex> lk(m_mutex);
if (!m_external)
return;
m_frame = frame;
m_video_size = videoSize.IsFullySpecified() ? videoSize : frame.GetSize();
adjust_frame_size(m_frame_size, m_video_size, GetSize());
}
CallAfter([this] { Refresh(); });
}
#ifdef _WIN32
void wxMediaCtrl3::SetExternalFrame(const wxBitmap& frame, wxSize videoSize)
{
if (!frame.IsOk())
return;
{
std::unique_lock<std::mutex> lk(m_mutex);
if (!m_external)
return;
m_frame = frame;
m_video_size = videoSize.IsFullySpecified() ? videoSize : frame.GetSize();
adjust_frame_size(m_frame_size, m_video_size, GetSize());
}
CallAfter([this] { Refresh(); });
}
#endif
void wxMediaCtrl3::BeginExternalStream()
{
std::unique_lock<std::mutex> lk(m_mutex);
m_external = true;
m_url.reset();
m_active_url.reset();
m_video_size = wxDefaultSize;
m_frame = wxImage(m_idle_image);
m_cond.notify_all();
Refresh();
}
void wxMediaCtrl3::EndExternalStream()
{
std::unique_lock<std::mutex> lk(m_mutex);
m_external = false;
m_url.reset();
m_active_url.reset();
m_video_size = wxDefaultSize;
m_frame = wxImage(m_idle_image);
m_cond.notify_all();
Refresh();
}
void wxMediaCtrl3::SetIdleImage(wxString const &image) void wxMediaCtrl3::SetIdleImage(wxString const &image)
{ {
if (m_idle_image == image) if (m_idle_image == image)
@@ -93,13 +163,13 @@ wxMediaState wxMediaCtrl3::GetState()
return m_state; return m_state;
} }
int wxMediaCtrl3::GetLastError() int wxMediaCtrl3::GetLastError() const
{ {
std::unique_lock<std::mutex> lk(m_mutex); std::unique_lock<std::mutex> lk(m_mutex);
return m_error; return m_error;
} }
wxSize wxMediaCtrl3::GetVideoSize() wxSize wxMediaCtrl3::GetVideoSize() const
{ {
std::unique_lock<std::mutex> lk(m_mutex); std::unique_lock<std::mutex> lk(m_mutex);
return m_video_size; return m_video_size;
@@ -185,6 +255,210 @@ void wxMediaCtrl3::bambu_log(void *ctx, int level, tchar const *msg2)
BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data(); BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data();
} }
// FFmpeg's own diagnostics (HTTP status, "Invalid data found", demuxer choice,
// missing stream dimensions, ...) are otherwise swallowed: a failed camera open
// only surfaces as wxMediaCtrl3's generic error code, which MediaPlayCtrl maps to
// the misleading "Player is malfunctioning" string. Forward them to the Orca log
// instead. Verbosity defaults to AV_LOG_VERBOSE and can be raised at runtime with
// ORCA_FFMPEG_LOG_LEVEL=debug|trace|... (or lowered to warning/error/quiet).
static int ffmpeg_log_level_from_env()
{
const char *env = std::getenv("ORCA_FFMPEG_LOG_LEVEL");
if (env == nullptr || *env == '\0')
return AV_LOG_VERBOSE;
const wxString v = wxString(env).Lower();
if (v == "quiet") return AV_LOG_QUIET;
if (v == "panic") return AV_LOG_PANIC;
if (v == "fatal") return AV_LOG_FATAL;
if (v == "error") return AV_LOG_ERROR;
if (v == "warning") return AV_LOG_WARNING;
if (v == "info") return AV_LOG_INFO;
if (v == "verbose") return AV_LOG_VERBOSE;
if (v == "debug") return AV_LOG_DEBUG;
if (v == "trace") return AV_LOG_TRACE;
return AV_LOG_VERBOSE;
}
static void ffmpeg_log_callback(void *avcl, int level, const char *fmt, va_list vl)
{
if (level > av_log_get_level())
return;
thread_local int print_prefix = 1;
char line[1024];
av_log_format_line2(avcl, level, fmt, vl, line, (int) sizeof(line), &print_prefix);
size_t len = std::strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r' || line[len - 1] == ' '))
line[--len] = '\0';
if (len == 0)
return;
if (level <= AV_LOG_ERROR)
BOOST_LOG_TRIVIAL(error) << "ffmpeg: " << line;
else if (level <= AV_LOG_WARNING)
BOOST_LOG_TRIVIAL(warning) << "ffmpeg: " << line;
else if (level <= AV_LOG_INFO)
BOOST_LOG_TRIVIAL(info) << "ffmpeg: " << line;
else
BOOST_LOG_TRIVIAL(debug) << "ffmpeg: " << line;
}
static void install_ffmpeg_logger()
{
av_log_set_level(ffmpeg_log_level_from_env());
av_log_set_callback(&ffmpeg_log_callback);
}
int wxMediaCtrl3::ffmpeg_interrupt_callback(void *opaque)
{
auto *ctrl = static_cast<wxMediaCtrl3 *>(opaque);
std::lock_guard<std::mutex> lock(ctrl->m_mutex);
return ctrl->m_url != ctrl->m_active_url;
}
int wxMediaCtrl3::PlayFfmpeg(std::shared_ptr<wxURI> const &url, std::unique_lock<std::mutex> &lock)
{
static std::once_flag logger_once;
std::call_once(logger_once, install_ffmpeg_logger);
if (avformat_network_init() < 0)
return 2;
AVFormatContext *format_context = avformat_alloc_context();
if (!format_context) {
avformat_network_deinit();
return 2;
}
format_context->interrupt_callback = {&wxMediaCtrl3::ffmpeg_interrupt_callback, this};
format_context->flags |= AVFMT_FLAG_NOBUFFER;
format_context->max_delay = 0;
m_active_url = url;
auto finish = [&](int error) {
lock.unlock();
avformat_close_input(&format_context);
avformat_network_deinit();
lock.lock();
m_active_url.reset();
return error;
};
const std::string uri = url->BuildURI().ToUTF8().data();
const wxString scheme = url->GetScheme();
const bool http_stream = scheme.CmpNoCase("http") == 0 || scheme.CmpNoCase("https") == 0;
AVDictionary *options = nullptr;
if (http_stream) {
// Live multipart MJPEG. fflags=nobuffer / AVFMT_FLAG_NOBUFFER / max_delay=0
// (set above) are the low-latency levers - they disable the demuxer
// read-ahead queue. probesize / analyzeduration only bound the one-off
// avformat_find_stream_info() at open; a 32-byte budget returned before a
// whole JPEG frame was seen, so width/height came back unset and the open
// was rejected. Give it room to identify one frame (a startup cost only).
// rw_timeout / timeout bound a wedged connect or read so a stale stream
// fails fast and is retried, instead of the reader thread hanging.
// avioflags=direct is deliberately NOT set: unbuffered reads make the
// mpjpeg demuxer emit "Packet corrupt" and bail on any short read across
// a multipart boundary.
av_dict_set(&options, "fflags", "nobuffer", 0);
av_dict_set(&options, "probesize", "5000000", 0);
av_dict_set(&options, "analyzeduration", "1000000", 0);
av_dict_set(&options, "rw_timeout", "5000000", 0);
av_dict_set(&options, "timeout", "5000000", 0);
} else {
av_dict_set(&options, "rtsp_transport", "tcp", 0);
}
lock.unlock();
int error = avformat_open_input(&format_context, uri.c_str(), nullptr, &options);
av_dict_free(&options);
lock.lock();
if (error < 0)
return finish(2);
lock.unlock();
error = avformat_find_stream_info(format_context, nullptr);
lock.lock();
if (error < 0)
return finish(2);
const int video_stream = av_find_best_stream(format_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (video_stream < 0)
return finish(2);
AVVideoDecoder decoder;
if (decoder.open(*format_context->streams[video_stream]->codecpar) < 0)
return finish(2);
// Prefer the dimensions the container reported. A small probe budget, or a
// camera that doesn't announce a size up front, can leave these unset - in
// that case fill them in from the first frame that decodes (below) rather
// than failing the open outright.
auto apply_video_size = [&](wxSize size) {
if (!size.IsFullySpecified() || size.x <= 0 || size.y <= 0)
return false;
m_video_size = size;
adjust_frame_size(m_frame_size, m_video_size, GetSize());
NotifyStopped();
return true;
};
bool have_size = apply_video_size({format_context->streams[video_stream]->codecpar->width,
format_context->streams[video_stream]->codecpar->height});
int size_probe_frames = 0; // frames spent still waiting for a usable size
AVPacket *packet = av_packet_alloc();
if (!packet)
return finish(2);
while (m_url == url) {
lock.unlock();
error = av_read_frame(format_context, packet);
lock.lock();
if (m_url != url)
break;
if (error < 0)
break;
if (packet->stream_index == video_stream) {
const int decode_error = decoder.decode(*packet);
if (decode_error == 0) {
if (!have_size) {
have_size = apply_video_size(decoder.decoded_frame_size());
if (!have_size) {
av_packet_unref(packet);
// MJPEG yields a sized frame on the first full packet; if
// several seconds of frames never do, treat it as a bad
// stream instead of sitting in "Loading..." forever.
if (++size_probe_frames > 120)
break;
continue;
}
}
auto frame_size = m_frame_size;
lock.unlock();
#ifdef _WIN32
wxBitmap frame;
decoder.toWxBitmap(frame, frame_size);
#else
wxImage frame;
decoder.toWxImage(frame, frame_size);
#endif
lock.lock();
if (m_url != url)
break;
if (frame.IsOk())
m_frame = frame;
if (!m_refresh_pending.exchange(true)) {
CallAfter([this] {
m_refresh_pending.store(false);
Refresh();
});
}
}
}
av_packet_unref(packet);
}
av_packet_free(&packet);
return finish(m_url == url ? 2 : 1);
}
void wxMediaCtrl3::PlayThread() void wxMediaCtrl3::PlayThread()
{ {
using namespace std::chrono_literals; using namespace std::chrono_literals;
@@ -197,42 +471,22 @@ void wxMediaCtrl3::PlayThread()
continue; continue;
if (!url->HasScheme()) if (!url->HasScheme())
break; break;
lk.unlock(); const wxString scheme = url->GetScheme();
Bambu_Tunnel tunnel = nullptr; const bool generic_ffmpeg = scheme.CmpNoCase("http") == 0 || scheme.CmpNoCase("https") == 0 ||
int error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8()); scheme.CmpNoCase("rtsp") == 0 || scheme.CmpNoCase("rtsps") == 0;
if (error == 0) { int error = 0;
Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this); if (generic_ffmpeg) {
error = Bambu_Open(tunnel); error = PlayFfmpeg(url, lk);
if (error == 0) } else {
error = Bambu_would_block; lk.unlock();
} Bambu_Tunnel tunnel = nullptr;
lk.lock(); error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8());
while (error == int(Bambu_would_block)) { if (error == 0) {
m_cond.wait_for(lk, 100ms); Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this);
if (m_url != url) { error = Bambu_Open(tunnel);
error = 1; if (error == 0)
break; error = Bambu_would_block;
} }
lk.unlock();
error = Bambu_StartStream(tunnel, true);
lk.lock();
}
Bambu_StreamInfo info;
if (error == 0)
error = Bambu_GetStreamInfo(tunnel, 0, &info);
AVVideoDecoder decoder;
int minFrameDuration = 0;
if (error == 0) {
decoder.open(info);
m_video_size = { info.format.video.width, info.format.video.height };
adjust_frame_size(m_frame_size, m_video_size, GetSize());
minFrameDuration = 800 / info.format.video.frame_rate; // 80%
NotifyStopped();
}
Bambu_Sample sample;
while (error == 0) {
lk.unlock();
error = Bambu_ReadSample(tunnel, &sample);
lk.lock(); lk.lock();
while (error == int(Bambu_would_block)) { while (error == int(Bambu_would_block)) {
m_cond.wait_for(lk, 100ms); m_cond.wait_for(lk, 100ms);
@@ -240,59 +494,87 @@ void wxMediaCtrl3::PlayThread()
error = 1; error = 1;
break; break;
} }
lk.unlock();
error = Bambu_StartStream(tunnel, true);
lk.lock();
}
Bambu_StreamInfo info;
if (error == 0)
error = Bambu_GetStreamInfo(tunnel, 0, &info);
AVVideoDecoder decoder;
int minFrameDuration = 0;
if (error == 0) {
decoder.open(info);
m_video_size = { info.format.video.width, info.format.video.height };
adjust_frame_size(m_frame_size, m_video_size, GetSize());
minFrameDuration = 800 / info.format.video.frame_rate; // 80%
NotifyStopped();
}
Bambu_Sample sample;
while (error == 0) {
lk.unlock(); lk.unlock();
error = Bambu_ReadSample(tunnel, &sample); error = Bambu_ReadSample(tunnel, &sample);
lk.lock(); lk.lock();
} while (error == int(Bambu_would_block)) {
if (error == 0) { m_cond.wait_for(lk, 100ms);
auto frame_size = m_frame_size; if (m_url != url) {
lk.unlock(); error = 1;
decoder.decode(sample); break;
#ifdef _WIN32
wxBitmap bm;
decoder.toWxBitmap(bm, frame_size);
#else
wxImage bm;
decoder.toWxImage(bm, frame_size);
#endif
lk.lock();
if (m_url != url) {
error = 1;
break;
}
if (bm.IsOk()) {
auto now = std::chrono::system_clock::now();
if (m_last_PTS && (sample.decode_time - m_last_PTS) < 30000000ULL) { // 3s
auto next_PTS_expected = m_last_PTS_expected + std::chrono::milliseconds((sample.decode_time - m_last_PTS) / 10000ULL);
// The frame is late, catch up a little
auto next_PTS_practical = m_last_PTS_practical + std::chrono::milliseconds(minFrameDuration);
auto next_PTS = std::max(next_PTS_expected, next_PTS_practical);
if(now < next_PTS)
std::this_thread::sleep_until(next_PTS);
else
next_PTS = now;
//auto text = wxString::Format(L"wxMediaCtrl3 pts diff %ld\n", std::chrono::duration_cast<std::chrono::milliseconds>(next_PTS - next_PTS_expected).count());
//OutputDebugString(text);
m_last_PTS = sample.decode_time;
m_last_PTS_expected = next_PTS_expected;
m_last_PTS_practical = next_PTS;
} else {
// Resync
m_last_PTS = sample.decode_time;
m_last_PTS_expected = now;
m_last_PTS_practical = now;
} }
m_frame = bm; lk.unlock();
error = Bambu_ReadSample(tunnel, &sample);
lk.lock();
}
if (error == 0) {
auto frame_size = m_frame_size;
lk.unlock();
decoder.decode(sample);
#ifdef _WIN32
wxBitmap bm;
decoder.toWxBitmap(bm, frame_size);
#else
wxImage bm;
decoder.toWxImage(bm, frame_size);
#endif
lk.lock();
if (m_url != url) {
error = 1;
break;
}
if (bm.IsOk()) {
auto now = std::chrono::system_clock::now();
if (m_last_PTS && (sample.decode_time - m_last_PTS) < 30000000ULL) { // 3s
auto next_PTS_expected = m_last_PTS_expected + std::chrono::milliseconds((sample.decode_time - m_last_PTS) / 10000ULL);
// The frame is late, catch up a little
auto next_PTS_practical = m_last_PTS_practical + std::chrono::milliseconds(minFrameDuration);
auto next_PTS = std::max(next_PTS_expected, next_PTS_practical);
if(now < next_PTS)
std::this_thread::sleep_until(next_PTS);
else
next_PTS = now;
//auto text = wxString::Format(L"wxMediaCtrl3 pts diff %ld\n", std::chrono::duration_cast<std::chrono::milliseconds>(next_PTS - next_PTS_expected).count());
//OutputDebugString(text);
m_last_PTS = sample.decode_time;
m_last_PTS_expected = next_PTS_expected;
m_last_PTS_practical = next_PTS;
} else {
// Resync
m_last_PTS = sample.decode_time;
m_last_PTS_expected = now;
m_last_PTS_practical = now;
}
m_frame = bm;
}
CallAfter([this] { Refresh(); });
} }
CallAfter([this] { Refresh(); });
} }
} if (tunnel) {
if (tunnel) { lk.unlock();
lk.unlock(); Bambu_Close(tunnel);
Bambu_Close(tunnel); Bambu_Destroy(tunnel);
Bambu_Destroy(tunnel); tunnel = nullptr;
tunnel = nullptr; lk.lock();
lk.lock(); }
} }
if (m_url == url) if (m_url == url)
m_error = error; m_error = error;
+26 -11
View File
@@ -13,41 +13,51 @@
#include "wx/bitmap.h" #include "wx/bitmap.h"
#include "wx/uri.h" #include "wx/uri.h"
#include "wx/mediactrl.h" #include "wx/mediactrl.h"
#include "IMediaController.hpp"
wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent); wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height); void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
#define BAMBU_DYNAMIC #define BAMBU_DYNAMIC
#include <atomic>
#include <condition_variable> #include <condition_variable>
#include <thread> #include <thread>
#ifndef _WIN32
#include <wx/image.h> #include <wx/image.h>
#endif
#include "Printer/BambuTunnel.h" #include "Printer/BambuTunnel.h"
class AVVideoDecoder; class AVVideoDecoder;
class wxMediaCtrl3 : public wxWindow, BambuLib class wxMediaCtrl3 : public wxWindow, public Slic3r::GUI::IMediaController, BambuLib
{ {
public: public:
wxMediaCtrl3(wxWindow *parent); wxMediaCtrl3(wxWindow *parent);
~wxMediaCtrl3(); ~wxMediaCtrl3() override;
void Load(wxURI url); void Load(wxURI url) override;
void Play(); void Play() override;
void Stop(); void Stop() override;
// Render frames supplied by a controller which owns its own transport.
// The frame is copied while m_mutex is held; callers may release it after
// this method returns.
void SetExternalFrame(const wxImage& frame, wxSize videoSize);
#ifdef _WIN32
void SetExternalFrame(const wxBitmap& frame, wxSize videoSize);
#endif
void BeginExternalStream();
void EndExternalStream();
void SetIdleImage(wxString const & image); void SetIdleImage(wxString const & image);
wxMediaState GetState(); wxMediaState GetState() override;
int GetLastError(); int GetLastError() const override;
wxSize GetVideoSize(); wxSize GetVideoSize() const override;
protected: protected:
DECLARE_EVENT_TABLE() DECLARE_EVENT_TABLE()
@@ -59,8 +69,10 @@ protected:
void DoSetSize(int x, int y, int width, int height, int sizeFlags) override; void DoSetSize(int x, int y, int width, int height, int sizeFlags) override;
static void bambu_log(void *ctx, int level, tchar const *msg); static void bambu_log(void *ctx, int level, tchar const *msg);
static int ffmpeg_interrupt_callback(void *opaque);
void PlayThread(); void PlayThread();
int PlayFfmpeg(std::shared_ptr<wxURI> const &url, std::unique_lock<std::mutex> &lock);
void NotifyStopped(); void NotifyStopped();
@@ -77,12 +89,15 @@ private:
#endif #endif
std::shared_ptr<wxURI> m_url; std::shared_ptr<wxURI> m_url;
std::shared_ptr<wxURI> m_active_url;
bool m_external = false;
std::uint64_t m_last_PTS{0}; std::uint64_t m_last_PTS{0};
std::chrono::system_clock::time_point m_last_PTS_expected; std::chrono::system_clock::time_point m_last_PTS_expected;
std::chrono::system_clock::time_point m_last_PTS_practical; std::chrono::system_clock::time_point m_last_PTS_practical;
std::mutex m_mutex; mutable std::mutex m_mutex;
std::condition_variable m_cond; std::condition_variable m_cond;
std::thread m_thread; std::thread m_thread;
std::atomic_bool m_refresh_pending{false};
}; };
#endif /* wxMediaCtrl3_h */ #endif /* wxMediaCtrl3_h */
+121 -4
View File
@@ -1,8 +1,13 @@
#include "BBLPrinterAgent.hpp" #include "BBLPrinterAgent.hpp"
#include "BBLNetworkPlugin.hpp" #include "BBLNetworkPlugin.hpp"
#include "IPrinterAgent.hpp"
#include "NetworkAgentFactory.hpp" #include "NetworkAgentFactory.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "NetworkAgent.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/DeviceCore/DevManager.h"
#include <boost/format.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@@ -10,6 +15,10 @@ using json = nlohmann::json;
#include <type_traits> #include <type_traits>
#include <unordered_map> #include <unordered_map>
#include <memory>
#include <nlohmann/json.hpp>
#include <cmath>
#include <slic3r/GUI/DeviceManager.hpp>
namespace Slic3r { namespace Slic3r {
@@ -133,7 +142,7 @@ BBLPrinterAgent::~BBLPrinterAgent() = default;
void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
{ {
m_cloud_agent = cloud; (void) cloud;
// BBL DLL manages tokens internally, so this is just for interface compliance // BBL DLL manages tokens internally, so this is just for interface compliance
} }
@@ -141,6 +150,97 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
// Communication // Communication
// ============================================================================ // ============================================================================
int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
if (ams_id == -1) {
const std::string gcode = (boost::format("M620 R%1% \n") % slot_id).str();
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
j["print"]["command"] = "ams_get_rfid";
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
j["print"]["ams_id"] = ams_id;
j["print"]["slot_id"] = slot_id;
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode)
{
const std::string gcode = (boost::format("M620 C%1% \n") % ams_id).str();
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
const std::string gcode = (boost::format("M620 P%1% \n") % tray_id).str();
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_axis_control) {
int dir = input_val > 0 ? 1 : -1;
// i3-arch printers move the bed for Y/Z, so the on-screen direction is
// reversed -- same negation the g-code fallback below applies.
if (!is_core_xy && (axis == "Y" || axis == "Z")) {
dir = -dir;
}
j["print"]["command"] = "xyz_ctrl";
j["print"]["axis"] = axis;
j["print"]["dir"] = dir;
j["print"]["mode"] = (std::abs(input_val) >= 10) ? 1 : 0;
return publish(dev_id, j, lan_mode);
}
double value = input_val;
if (!is_core_xy && (axis == "Y" || axis == "Z")) {
value = -1.0 * input_val;
}
std::string value_str = (boost::format("%.1f") % (value * unit)).str();
std::string gcode;
if (axis == "X" || axis == "Y" || axis == "Z") {
gcode = (boost::format("M211 S \nM211 X1 Y1 Z1\nM1002 push_ref_mode\nG91 \nG1 %1%%2% F%3%\nM1002 pop_ref_mode\nM211 R\n")
% axis % value_str % speed).str();
} else if (axis == "E") {
gcode = (boost::format("M83 \nG0 %1%%2% F%3%\n") % axis % value_str % speed).str();
} else {
return -1;
}
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode)
{
const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0);
if (rtn == 0) {
BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn;
} else {
BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn;
}
return rtn;
}
int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag)
{ {
json_str = from_orca_payload(std::move(json_str)); json_str = from_orca_payload(std::move(json_str));
@@ -165,13 +265,23 @@ int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int
return -1; return -1;
} }
int BBLPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) int BBLPrinterAgent::connect_printer(const PrinterConnectionParams& params)
{ {
auto& plugin = BBLNetworkPlugin::instance(); auto& plugin = BBLNetworkPlugin::instance();
auto agent = plugin.get_agent(); auto agent = plugin.get_agent();
auto func = plugin.get_connect_printer(); auto func = plugin.get_connect_printer();
#if !BBL_RELEASE_TO_PUBLIC
const bool use_ssl_for_mqtt = GUI::wxGetApp().app_config &&
GUI::wxGetApp().app_config->get_bool("enable_ssl_for_mqtt");
#else
bool use_ssl_for_mqtt = true;
if (auto* dev_manager = GUI::wxGetApp().getDeviceManager()) {
if (auto* machine = dev_manager->get_my_machine(params.dev_id))
use_ssl_for_mqtt = machine->local_use_ssl;
}
#endif
if (func && agent) { if (func && agent) {
return func(agent, dev_id, dev_ip, username, password, use_ssl); return func(agent, params.dev_id, params.host, params.username, params.password, use_ssl_for_mqtt);
} }
return -1; return -1;
} }
@@ -473,8 +583,15 @@ int BBLPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateS
int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
{ {
return dispatch_start<func_start_send_gcode_to_sdcard_legacy, func_start_send_gcode_to_sdcard_0203>( int result = dispatch_start<func_start_send_gcode_to_sdcard_legacy, func_start_send_gcode_to_sdcard_0203>(
BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn); BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn);
if (result != 0) {
BOOST_LOG_TRIVIAL(error) << "start_send_gcode_to_sdcard failed: result=" << result
<< ", try_emmc_print=" << params.try_emmc_print
<< ", legacy_mode=" << BBLNetworkPlugin::instance().use_legacy_network()
<< ", dev_ip=" << params.dev_ip << ", dev_id=" << params.dev_id;
}
return result;
} }
int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
+10 -2
View File
@@ -5,6 +5,7 @@
#include "ICloudServiceAgent.hpp" #include "ICloudServiceAgent.hpp"
#include <string> #include <string>
#include <memory> #include <memory>
#include <nlohmann/json.hpp>
namespace Slic3r { namespace Slic3r {
@@ -28,7 +29,13 @@ public:
// Communication // Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; int command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode) override;
std::string default_lan_username() const override { return "bblp"; }
int connect_printer(const PrinterConnectionParams& params) override;
int disconnect_printer() override; int disconnect_printer() override;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
@@ -99,7 +106,8 @@ public:
static std::string from_orca_payload(std::string json_text); static std::string from_orca_payload(std::string json_text);
private: private:
std::shared_ptr<ICloudServiceAgent> m_cloud_agent; // why: the lan/cloud DECISION stays machine-side; keep this mechanical branch in sync with publish_json.
int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode);
}; };
} // namespace Slic3r } // namespace Slic3r
+4 -1
View File
@@ -282,8 +282,11 @@ bool CrealityPrintAgent::parse_cfs_response(const std::string& response,
return true; return true;
} }
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id) bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
if (sync_mode != get_filament_sync_mode())
return false;
if (device_info.dev_ip.empty()) { if (device_info.dev_ip.empty()) {
BOOST_LOG_TRIVIAL(warning) BOOST_LOG_TRIVIAL(warning)
<< "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent"; << "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent";
+2 -1
View File
@@ -1,6 +1,7 @@
#ifndef __CREALITY_PRINT_AGENT_HPP__ #ifndef __CREALITY_PRINT_AGENT_HPP__
#define __CREALITY_PRINT_AGENT_HPP__ #define __CREALITY_PRINT_AGENT_HPP__
#include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include <string> #include <string>
@@ -41,7 +42,7 @@ public:
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
bool fetch_filament_info(std::string dev_id) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
// Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into // Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into
// a flat list of loaded slots, plus the count of CFS boxes the printer reports. // a flat list of loaded slots, plus the count of CFS boxes the printer reports.
+1 -1
View File
@@ -224,4 +224,4 @@ void FileTransferJob::solve_result(ft_job_result result)
res_json_.assign(result.json ? result.json : ""); res_json_.assign(result.json ? result.json : "");
} }
} // namespace Slic3r } // namespace Slic3r
+1 -1
View File
@@ -252,4 +252,4 @@ inline FileTransferModule &module()
return *detail::g_mod; return *detail::g_mod;
} }
} // namespace Slic3r } // namespace Slic3r
+57 -6
View File
@@ -1,5 +1,6 @@
#include "Http.hpp" #include "Http.hpp"
#include <atomic>
#include <cstdlib> #include <cstdlib>
#include <functional> #include <functional>
#include <thread> #include <thread>
@@ -14,8 +15,19 @@
#include <curl/curl.h> #include <curl/curl.h>
#ifdef OPENSSL_CERT_OVERRIDE #include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/x509.h> #include <openssl/x509.h>
#include <openssl/x509err.h>
#ifdef _WIN32
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <windows.h>
# include <wincrypt.h>
// wincrypt.h uses this token for a certificate-name property identifier.
# undef X509_NAME
#endif #endif
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
@@ -122,7 +134,7 @@ struct Http::priv
std::string error_buffer; // Used for CURLOPT_ERRORBUFFER std::string error_buffer; // Used for CURLOPT_ERRORBUFFER
std::string headers; std::string headers;
size_t limit; size_t limit;
bool cancel; std::atomic_bool cancel;
std::unique_ptr<form_file> putFile; std::unique_ptr<form_file> putFile;
std::thread io_thread; std::thread io_thread;
@@ -260,9 +272,9 @@ int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_o
self->progressfn(progress, cb_cancel); self->progressfn(progress, cb_cancel);
} }
if (cb_cancel) { self->cancel = true; } if (cb_cancel) { self->cancel.store(true); }
return self->cancel; return self->cancel.load();
} }
int Http::priv::xfercb_legacy(void *userp, double dltotal, double dlnow, double ultotal, double ulnow) int Http::priv::xfercb_legacy(void *userp, double dltotal, double dlnow, double ultotal, double ulnow)
@@ -473,7 +485,7 @@ void Http::priv::http_perform()
if (res != CURLE_OK) { if (res != CURLE_OK) {
if (res == CURLE_ABORTED_BY_CALLBACK) { if (res == CURLE_ABORTED_BY_CALLBACK) {
if (cancel) { if (cancel.load()) {
// The abort comes from the request being cancelled programatically // The abort comes from the request being cancelled programatically
Progress dummyprogress(0, 0, 0, 0, std::string()); Progress dummyprogress(0, 0, 0, 0, std::string());
bool cancel = true; bool cancel = true;
@@ -784,7 +796,7 @@ void Http::perform_sync()
void Http::cancel() void Http::cancel()
{ {
if (p) { p->cancel = true; } if (p) { p->cancel.store(true); }
} }
void Http::print() const void Http::print() const
@@ -939,6 +951,45 @@ std::string Http::tls_system_cert_store()
return ret; return ret;
} }
void Http::add_platform_root_certificates(SSL_CTX* ssl_context)
{
#ifdef _WIN32
X509_STORE* openssl_store = SSL_CTX_get_cert_store(ssl_context);
if (!openssl_store)
throw std::runtime_error("unable to get OpenSSL certificate store");
const auto load_store = [&](DWORD location) {
HCERTSTORE windows_store = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0,
location | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG,
L"ROOT");
if (!windows_store)
return;
PCCERT_CONTEXT windows_certificate = nullptr;
while ((windows_certificate = CertEnumCertificatesInStore(windows_store, windows_certificate)) != nullptr) {
const unsigned char* encoded = windows_certificate->pbCertEncoded;
X509* certificate = d2i_X509(nullptr, &encoded, static_cast<long>(windows_certificate->cbCertEncoded));
if (!certificate) {
ERR_clear_error();
continue;
}
ERR_clear_error();
if (X509_STORE_add_cert(openssl_store, certificate) != 1)
ERR_clear_error();
X509_free(certificate);
}
CertCloseStore(windows_store, 0);
};
load_store(CERT_SYSTEM_STORE_CURRENT_USER);
load_store(CERT_SYSTEM_STORE_LOCAL_MACHINE);
#else
(void)ssl_context;
#endif
}
std::string Http::url_encode(const std::string &str) std::string Http::url_encode(const std::string &str)
{ {
::CURL *curl = ::curl_easy_init(); ::CURL *curl = ::curl_easy_init();
+6
View File
@@ -11,6 +11,8 @@
#include "libslic3r/Exception.hpp" #include "libslic3r/Exception.hpp"
#include "libslic3r_version.h" #include "libslic3r_version.h"
typedef struct ssl_ctx_st SSL_CTX;
#define MAX_SIZE_TO_FILE 3*1024 #define MAX_SIZE_TO_FILE 3*1024
namespace Slic3r { namespace Slic3r {
@@ -198,6 +200,10 @@ public:
// Return empty string on success or error message on fail. // Return empty string on success or error message on fail.
static std::string tls_global_init(); static std::string tls_global_init();
static std::string tls_system_cert_store(); static std::string tls_system_cert_store();
// Add platform root certificates to a standalone OpenSSL context. This
// supplements set_default_verify_paths() on platforms where OpenSSL does
// not use the native certificate store.
static void add_platform_root_certificates(SSL_CTX* ssl_context);
// converts the given string to an url_encoded_string // converts the given string to an url_encoded_string
static std::string url_encode(const std::string &str); static std::string url_encode(const std::string &str);
@@ -0,0 +1,39 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
namespace Slic3r {
struct CameraIceServer {
std::string urls;
std::string username;
std::string credential;
};
enum class CameraUnavailableReason {
Busy,
Error,
Disabled,
Closed,
};
class ICameraSignalingChannel {
public:
virtual ~ICameraSignalingChannel() = default;
virtual void open() = 0;
virtual void close() = 0;
virtual void send_offer(std::string sdp) = 0;
virtual void send_ice(std::string candidate, std::string mid) = 0;
// These callbacks are invoked by the channel's worker thread. Consumers
// must marshal UI work to the GUI thread themselves.
std::function<void(std::vector<CameraIceServer>)> on_ready;
std::function<void(std::string)> on_answer;
std::function<void(std::string, std::string)> on_ice;
std::function<void(CameraUnavailableReason, std::string)> on_unavailable;
};
} // namespace Slic3r
+12 -1
View File
@@ -1,11 +1,11 @@
#ifndef __I_CLOUD_SERVICE_AGENT_HPP__ #ifndef __I_CLOUD_SERVICE_AGENT_HPP__
#define __I_CLOUD_SERVICE_AGENT_HPP__ #define __I_CLOUD_SERVICE_AGENT_HPP__
#include "ICameraSignalingChannel.hpp"
#include "bambu_networking.hpp" #include "bambu_networking.hpp"
#include "CloudProvider.hpp" #include "CloudProvider.hpp"
#include "../../libslic3r/ProjectTask.hpp" #include "../../libslic3r/ProjectTask.hpp"
#include <string> #include <string>
#include <string_view>
#include <map> #include <map>
#include <vector> #include <vector>
#include <functional> #include <functional>
@@ -328,6 +328,17 @@ public:
*/ */
virtual int get_camera_url(std::string dev_id, std::function<void(std::string)> callback) = 0; virtual int get_camera_url(std::string dev_id, std::function<void(std::string)> callback) = 0;
/**
* Create a camera signaling channel object for P2P camera streaming over WebRTC.
*
*/
virtual std::unique_ptr<ICameraSignalingChannel>
create_camera_signaling_channel(const std::string& dev_id)
{
(void) dev_id;
return nullptr;
}
/** /**
* Fetch staff-picked designs from model mall. * Fetch staff-picked designs from model mall.
*/ */
+186 -10
View File
@@ -11,6 +11,11 @@
#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability #define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability
#include <string> #include <string>
#include <memory> #include <memory>
#include <vector>
#include <functional>
#include <cmath>
#include <nlohmann/json.hpp>
#include <boost/format.hpp>
namespace Slic3r { namespace Slic3r {
@@ -29,6 +34,17 @@ struct AgentInfo {
std::string description; ///< Brief description of the agent's capabilities, e.g. "Orca printer agent" std::string description; ///< Brief description of the agent's capabilities, e.g. "Orca printer agent"
}; };
struct PrinterConnectionParams
{
std::string dev_id;
std::string host; // host address, usually the IP address without the http/https protocol
std::string port; // optional
std::string username;
std::string password;
bool use_ssl = false; // indicates if http or https
std::string ca_file;
};
/** /**
* FilamentSyncMode - Modes for filament data synchronization. * FilamentSyncMode - Modes for filament data synchronization.
* *
@@ -43,6 +59,15 @@ enum class FilamentSyncMode {
pull ///< On-demand fetch via REST API (blocking call) pull ///< On-demand fetch via REST API (blocking call)
}; };
enum class CameraStreamMode {
none = 0,
http, // LAN or Cloud
https, // LAN or Cloud over TLS
rtsp, // LAN only
webrtc, // Cloud only
http_snapshot // HTTP endpoint returning one image per request
};
/** /**
* IPrinterAgent - Interface for printer operations. * IPrinterAgent - Interface for printer operations.
* *
@@ -84,10 +109,112 @@ public:
*/ */
virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0; virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0;
// why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect
// gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's
// publish funnel turns into a dialog.
virtual int command_ams_refresh_rfid(std::string, int, int, int, bool)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_ams_calibrate(std::string, int, int, bool)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_ams_select_tray(std::string, std::string, int, bool)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_start_camera(std::string)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
private:
int publish_command_json(std::string dev_id, const nlohmann::json& j, bool lan_mode)
{
return lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0)
: send_message(dev_id, j.dump(), 0, 0);
}
public:
virtual int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G90 \n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish_command_json(dev_id, j, lan_mode);
}
virtual int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G29 \n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish_command_json(dev_id, j, lan_mode);
}
virtual int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_homing) {
j["print"]["command"] = "back_to_center";
} else {
j["print"]["command"] = "gcode_line";
j["print"]["param"] = is_printing ? "G28 X\n" : "G28 \n";
}
return publish_command_json(dev_id, j, lan_mode);
}
virtual int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_bed_ctrl) {
j["print"]["command"] = "set_bed_temp";
j["print"]["temp"] = temp;
} else {
j["print"]["command"] = "gcode_line";
j["print"]["param"] = (boost::format("M140 S%1%\n") % temp).str();
}
return publish_command_json(dev_id, j, lan_mode);
}
virtual int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = (boost::format("M104 S%1%\n") % temp).str();
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish_command_json(dev_id, j, lan_mode);
}
virtual int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode)
{
(void) supports_mqtt_axis_control;
double value = input_val;
if (!is_core_xy && (axis == "Y" || axis == "Z"))
value = -input_val;
std::string value_str = (boost::format("%.1f") % (value * unit)).str();
std::string gcode;
if (axis == "X" || axis == "Y" || axis == "Z") {
gcode = (boost::format("G91 \nG1 %1%%2% F%3%\nG90 \n") % axis % value_str % speed).str();
} else if (axis == "E") {
gcode = (boost::format("M83 \nG0 %1%%2% F%3%\n") % axis % value_str % speed).str();
} else {
return -1;
}
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish_command_json(dev_id, j, lan_mode);
}
/**
* Default LAN account username for this agent's protocol, if it has a fixed one.
* Returns an empty string if the agent has no fixed default (e.g. caller must supply one).
*/
virtual std::string default_lan_username() const { return {}; }
/** /**
* Establish a direct LAN connection to a printer. * Establish a direct LAN connection to a printer.
*/ */
virtual int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) = 0; virtual int connect_printer(const PrinterConnectionParams& params) = 0;
/** /**
* Tear down the active LAN printer connection. * Tear down the active LAN printer connection.
@@ -105,12 +232,16 @@ public:
/** /**
* Validate current user certificates for the printer. * Validate current user certificates for the printer.
*/ */
virtual int check_cert() = 0; virtual int check_cert() { return BAMBU_NETWORK_SUCCESS; }
/** /**
* Install or refresh device certificate for LAN TLS. * Install or refresh device certificate for LAN TLS.
*/ */
virtual void install_device_cert(std::string dev_id, bool lan_only) = 0; virtual void install_device_cert(std::string dev_id, bool lan_only)
{
(void) dev_id;
(void) lan_only;
}
// ======================================================================== // ========================================================================
// Discovery // Discovery
@@ -126,7 +257,11 @@ public:
/** /**
* Ping the binding endpoint to check printer readiness. * Ping the binding endpoint to check printer readiness.
*/ */
virtual int ping_bind(std::string ping_code) = 0; virtual int ping_bind(std::string ping_code)
{
(void) ping_code;
return BAMBU_NETWORK_SUCCESS;
}
/** /**
* Perform binding detection/handshake on a LAN printer. * Perform binding detection/handshake on a LAN printer.
@@ -136,23 +271,50 @@ public:
/** /**
* Execute the multi-stage printer binding workflow. * Execute the multi-stage printer binding workflow.
*/ */
virtual int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) = 0; virtual int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link,
std::string timezone, bool improved, OnUpdateStatusFn update_fn)
{
(void) dev_ip;
(void) dev_id;
(void) dev_model;
(void) sec_link;
(void) timezone;
(void) improved;
(void) update_fn;
return BAMBU_NETWORK_SUCCESS;
}
/** /**
* Remove the association between account and printer. * Remove the association between account and printer.
*/ */
virtual int unbind(std::string dev_id) = 0; virtual int unbind(std::string dev_id)
{
(void) dev_id;
return BAMBU_NETWORK_SUCCESS;
}
/** /**
* Request a one-time bind ticket from the server. * Request a one-time bind ticket from the server.
*/ */
virtual int request_bind_ticket(std::string* ticket) = 0; virtual int request_bind_ticket(std::string* ticket)
{
if (ticket)
*ticket = {};
return BAMBU_NETWORK_SUCCESS;
}
/** /**
* Fetch the cloud snapshot image captured at a print failure. * Fetch the cloud snapshot image captured at a print failure.
* Returns 0 if the request was dispatched; the image body arrives via callback(body, http_status). * Returns 0 if the request was dispatched; the image body arrives via callback(body, http_status).
*/ */
virtual int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) = 0; virtual int get_hms_snapshot(std::string dev_id, std::string file_name,
std::function<void(std::string, int)> callback)
{
(void) dev_id;
(void) file_name;
(void) callback;
return -1;
}
/** /**
* Register callback for fatal HTTP errors. * Register callback for fatal HTTP errors.
@@ -168,7 +330,7 @@ public:
virtual std::string get_user_selected_machine() = 0; virtual std::string get_user_selected_machine() = 0;
/** /**
* Update the selected machine preference. * Update the selected cloud machine preference.
*/ */
virtual int set_user_selected_machine(std::string dev_id) = 0; virtual int set_user_selected_machine(std::string dev_id) = 0;
@@ -284,12 +446,20 @@ public:
*/ */
virtual FilamentSyncMode get_filament_sync_mode() const { return FilamentSyncMode::none; } virtual FilamentSyncMode get_filament_sync_mode() const { return FilamentSyncMode::none; }
/**
* Get the camera stream mode for this agent. This value can be deterministic and derived at
* runtime if the printer supports multiple camera stream modes. E.g. LAN => HTTP/HTTPS/RTSP, Cloud => WebRTC.
*
* @return CameraStreamMode indicating how the camera stream is obtained or used:
*/
virtual CameraStreamMode get_camera_stream_mode() const { return CameraStreamMode::none; }
/** /**
* Refresh filament info from the printer synchronously. * Refresh filament info from the printer synchronously.
* Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull. * Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull.
* Populates the MachineObject's DevFilaSystem with fetched filament data. * Populates the MachineObject's DevFilaSystem with fetched filament data.
*/ */
virtual bool fetch_filament_info(std::string dev_id) { return false; } virtual bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) { return false; }
/** /**
* Translate one filament id across the printer boundary. * Translate one filament id across the printer boundary.
@@ -300,6 +470,12 @@ public:
*/ */
virtual std::string to_orca_filament_id(const std::string& printer_filament_id) const { return printer_filament_id; } virtual std::string to_orca_filament_id(const std::string& printer_filament_id) const { return printer_filament_id; }
virtual std::string from_orca_filament_id(const std::string& orca_filament_id) const { return orca_filament_id; } virtual std::string from_orca_filament_id(const std::string& orca_filament_id) const { return orca_filament_id; }
/**
* Get the current camera stream URL for this agent's active machine.
* Only meaningful when get_camera_stream_mode() returns an HTTP, HTTPS, or RTSP mode.
*/
virtual std::string get_camera_url() const { return {}; }
}; };
} // namespace Slic3r } // namespace Slic3r
+20 -5
View File
@@ -38,12 +38,25 @@ wxString Moonraker::get_test_failed_msg(wxString &msg) const
std::string Moonraker::make_url(const std::string &path) const std::string Moonraker::make_url(const std::string &path) const
{ {
if (m_host.find("http://") == 0 || m_host.find("https://") == 0) { const bool has_scheme = m_host.find("http://") == 0 || m_host.find("https://") == 0;
if (m_host.back() == '/') const bool use_ssl = m_host.find("https://") == 0;
return (boost::format("%1%%2%") % m_host % path).str(); std::string base = has_scheme ? m_host : ("http://" + m_host);
return (boost::format("%1%/%2%") % m_host % path).str();
const size_t authority_start = base.find("://") + 3;
const size_t authority_end = base.find('/', authority_start);
const std::string authority = base.substr(authority_start, authority_end - authority_start);
const size_t closing_bracket = authority.rfind(']');
const bool has_port = closing_bracket != std::string::npos
? closing_bracket + 1 < authority.size() && authority[closing_bracket + 1] == ':'
: authority.find(':') != std::string::npos;
if (!has_port) {
const std::string default_port = use_ssl ? ":7130" : ":7125";
base.insert(authority_end == std::string::npos ? base.size() : authority_end, default_port);
} }
return (boost::format("http://%1%/%2%") % m_host % path).str();
if (base.back() == '/')
return (boost::format("%1%%2%") % base % path).str();
return (boost::format("%1%/%2%") % base % path).str();
} }
void Moonraker::set_auth(Http &http) const void Moonraker::set_auth(Http &http) const
@@ -53,6 +66,8 @@ void Moonraker::set_auth(Http &http) const
// filled the user/password fields — those are PrusaLink/OctoPrint conventions. // filled the user/password fields — those are PrusaLink/OctoPrint conventions.
if (!m_apikey.empty()) if (!m_apikey.empty())
http.header("X-Api-Key", m_apikey); http.header("X-Api-Key", m_apikey);
const bool use_ssl = m_host.find("https://") == 0;
http.tls_verify(use_ssl);
if (!m_cafile.empty()) if (!m_cafile.empty())
http.ca_file(m_cafile); http.ca_file(m_cafile);
} }
File diff suppressed because it is too large Load Diff
+134 -32
View File
@@ -9,11 +9,48 @@
#include <set> #include <set>
#include <string> #include <string>
#include <thread> #include <thread>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
namespace Slic3r { namespace Slic3r {
class Http;
bool moonraker_is_light_name(const std::string& name);
class MoonrakerWebsocket
{
public:
enum class ReadResult
{
message,
timeout,
closed,
error,
};
MoonrakerWebsocket(bool secure, std::string api_key, std::string ca_file);
~MoonrakerWebsocket();
void connect(const std::string& host, const std::string& port, std::chrono::seconds timeout);
void tls_handshake(const std::string& host);
void handshake(const std::string& host, const std::string& target);
void text(bool enabled);
void write(const std::string& body);
ReadResult read(std::string& payload, std::string& error_message);
void close();
void expires_after(std::chrono::seconds timeout);
void abort();
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
class MoonrakerPrinterAgent : public IPrinterAgent class MoonrakerPrinterAgent : public IPrinterAgent
{ {
public: public:
@@ -28,24 +65,15 @@ public:
// Communication // Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; int connect_printer(const PrinterConnectionParams& params) override;
int disconnect_printer() override; int disconnect_printer() override;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
// Certificates
int check_cert() override;
void install_device_cert(std::string dev_id, bool lan_only) override;
// Discovery // Discovery
bool start_discovery(bool start, bool sending) override; bool start_discovery(bool start, bool sending) override;
// Binding // Binding
int ping_bind(std::string ping_code) override;
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override;
int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override;
int unbind(std::string dev_id) override;
int request_bind_ticket(std::string* ticket) override;
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override;
int set_server_callback(OnServerErrFn fn) override; int set_server_callback(OnServerErrFn fn) override;
// Machine Selection // Machine Selection
@@ -68,12 +96,20 @@ public:
int set_on_local_connect_fn(OnLocalConnectedFn fn) override; int set_on_local_connect_fn(OnLocalConnectedFn fn) override;
int set_on_local_message_fn(OnMessageFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override;
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
// Pull-mode agent (on-demand filament sync) CameraStreamMode get_camera_stream_mode() const override;
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } std::string get_camera_url() const override;
bool fetch_filament_info(std::string dev_id) override;
protected: protected:
struct ConnectionSettings
{
std::string dev_id;
std::string base_url;
std::string api_key;
bool use_ssl = false;
std::string ca_file;
};
struct MoonrakerDeviceInfo struct MoonrakerDeviceInfo
{ {
std::string dev_id; std::string dev_id;
@@ -85,7 +121,9 @@ protected:
std::string dev_name; std::string dev_name;
std::string version; std::string version;
std::string klippy_state; std::string klippy_state;
float nozzle_diameter = 0.0f;
bool use_ssl = false; bool use_ssl = false;
std::string ca_file;
} device_info; } device_info;
// Tray data for AMS payload building // Tray data for AMS payload building
@@ -103,15 +141,24 @@ protected:
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays); void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
// Methods that derived classes may need to override or access // Methods that derived classes may need to override or access
virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); virtual bool init_device_info(const PrinterConnectionParams& params);
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; virtual bool fetch_device_info(const ConnectionSettings& connection, MoonrakerDeviceInfo& info, std::string& error) const;
ConnectionSettings get_connection_settings() const;
void configure_http(Http& http, const ConnectionSettings& connection) const;
static float parse_nozzle_diameter(const nlohmann::json& response);
// State access for derived classes // State access for derived classes
mutable std::recursive_mutex state_mutex; mutable std::recursive_mutex state_mutex;
// Counts detached fetch_filament_info() background threads currently touching `this`
// (see QidiPrinterAgent::fetch_filament_info). Those threads hold a raw `this` with no
// other lifetime protection, so the destructor waits for this to reach 0 before any part
// of the object is torn down — see ~MoonrakerPrinterAgent().
std::atomic<int> filament_fetch_in_flight{0};
// Helpers // Helpers
bool is_numeric(const std::string& value); bool is_numeric(const std::string& value);
std::string normalize_base_url(std::string host, const std::string& port); std::string normalize_base_url(bool use_ssl, const std::string& host, const std::string& port);
std::string sanitize_filename(const std::string& filename); std::string sanitize_filename(const std::string& filename);
std::string join_url(const std::string& base_url, const std::string& path) const; std::string join_url(const std::string& base_url, const std::string& path) const;
@@ -121,23 +168,42 @@ protected:
// Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility // Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility
static std::string map_filament_type_to_generic_id(const std::string& filament_type); static std::string map_filament_type_to_generic_id(const std::string& filament_type);
// Send a G-code script via Moonraker (/printer/gcode/script)
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
bool send_gcode(const std::string& dev_id, const std::string& gcode,
const ConnectionSettings& connection) const;
bool post_print_action(const std::string& action) const;
bool post_print_action(const std::string& action,
const ConnectionSettings& connection) const;
bool send_ws_rpc(const std::string& method, const nlohmann::json& params);
virtual void on_status_loop_tick(const std::string& dev_id) {}
// Queue work that may use agent state. The command worker is joined during
// destruction, so queued commands cannot outlive the agent.
void enqueue_command(std::function<void()> fn);
private: private:
int handle_request(const std::string& dev_id, const std::string& json_str); int handle_request(const std::string& dev_id, const std::string& json_str);
int send_version_info(const std::string& dev_id); int send_version_info(const std::string& dev_id);
int send_access_code(const std::string& dev_id); int send_access_code(const std::string& dev_id);
bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set<std::string>& objects, std::string& error) const; bool fetch_object_list(const ConnectionSettings& connection, std::set<std::string>& objects, std::string& error) const;
bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; bool query_printer_status(const ConnectionSettings& connection, nlohmann::json& status, std::string& error) const;
bool send_gcode(const std::string& dev_id, const std::string& gcode) const; bool send_gcode_sync(const std::string& dev_id, const std::string& gcode) const;
void send_gcode_async(const std::string& dev_id, const std::string& gcode,
std::function<void(bool)> on_result = {}) const;
void announce_printhost_device(); void announce_printhost_device();
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg);
void dispatch_printer_connected(const std::string& dev_id); void dispatch_printer_connected(const std::string& dev_id);
void dispatch_message(const std::string& dev_id, const std::string& payload); void dispatch_message(const std::string& dev_id, const std::string& payload);
void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key); void start_status_stream(const std::string& dev_id, ConnectionSettings connection);
void stop_status_stream(); void stop_status_stream();
void run_status_stream(std::string dev_id, std::string base_url, std::string api_key); void run_status_stream(std::string dev_id, ConnectionSettings connection);
void handle_ws_message(const std::string& dev_id, const std::string& payload); void handle_ws_message(std::string dev_id, std::string payload, ConnectionSettings connection);
void refresh_thumbnail_url(const ConnectionSettings& connection);
void update_status_cache(const nlohmann::json& updates); void update_status_cache(const nlohmann::json& updates);
nlohmann::json build_print_payload_locked() const; nlohmann::json build_print_payload_locked() const;
@@ -148,22 +214,28 @@ private:
// File upload // File upload
bool upload_gcode(const std::string& local_path, const std::string& filename, bool upload_gcode(const std::string& local_path, const std::string& filename,
const std::string& base_url, const std::string& api_key, const ConnectionSettings& connection,
OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
// JSON-RPC helper // Start a print of a previously uploaded G-code file (path relative to the
bool send_jsonrpc_command(const std::string& base_url, const std::string& api_key, // Moonraker gcodes root).
const nlohmann::json& request, std::string& response) const; bool start_print_file(const ConnectionSettings& connection,
const std::string& filename, std::string& error_msg) const;
// Connection thread management // Connection thread management
void perform_connection_async(const std::string& dev_id, void perform_connection_async(const std::string& dev_id,
const std::string& base_url, ConnectionSettings connection,
const std::string& api_key,
uint64_t generation); uint64_t generation);
// why: a printer with no /server/webcams/list entry can still name its stream directly;
// subclasses (e.g. printers with a fixed webcam path) can override this instead.
virtual std::string webcam_stream_override(const std::string& base_url) const { return {}; }
void refresh_webcam_info() const;
bool fetch_webcam_info(const ConnectionSettings& connection, uint64_t generation) const;
// System-specific filament fetch methods // System-specific filament fetch methods
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index); bool fetch_hh_filament_info(const ConnectionSettings& connection, std::vector<AmsTrayData>& trays, int& max_lane_index);
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index); bool fetch_moonraker_filament_data(const ConnectionSettings& connection, std::vector<AmsTrayData>& trays, int& max_lane_index);
// JSON helper methods // JSON helper methods
static std::string safe_json_string(const nlohmann::json& obj, const char* key); static std::string safe_json_string(const nlohmann::json& obj, const char* key);
@@ -189,15 +261,38 @@ private:
mutable std::recursive_mutex payload_mutex; mutable std::recursive_mutex payload_mutex;
nlohmann::json status_cache; nlohmann::json status_cache;
// note: guarded by payload_mutex; filled by refresh_thumbnail_url(), empty url = looked up, none found
std::string thumbnail_filename;
std::string thumbnail_url;
mutable std::string webcam_stream_url;
mutable CameraStreamMode webcam_stream_mode = CameraStreamMode::none;
mutable uint64_t webcam_info_last_lookup_ms = 0;
mutable uint64_t webcam_info_generation = 0;
unsigned thumbnail_lookup_attempts = 0;
static constexpr uint64_t WEBCAM_INFO_REFRESH_INTERVAL_MS = 1000;
std::atomic<int> next_jsonrpc_id{1}; std::atomic<int> next_jsonrpc_id{1};
std::set<std::string> available_objects; // Track for feature detection std::set<std::string> available_objects; // Track for feature detection
bool assumed_light_on = false;
std::atomic<bool> ws_stop{false}; std::atomic<bool> ws_stop{false};
std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection
std::atomic<uint64_t> ws_last_emit_ms{0}; std::atomic<uint64_t> ws_last_emit_ms{0};
std::thread ws_thread; std::thread ws_thread;
// stop_status_stream() invokes ws_abort_io to wake a blocked synchronous
// ws.read()/ws.write()/handshake in run_status_stream(): ws_stop is only
// observed between reads, and Beast's expires_after() does not bound
// synchronous operations.
std::mutex ws_abort_mutex;
std::function<void()> ws_abort_io; // guarded by ws_abort_mutex
// AMS/filament refresh cadence, independent of telemetry dispatch so a steady
// stream of status updates can't starve it (ws_last_emit_ms is reset by those).
static constexpr uint64_t AMS_REFRESH_INTERVAL_MS = 10000;
std::atomic<uint64_t> ams_last_fetch_ms{0};
// Throttling configuration for WebSocket updates // Throttling configuration for WebSocket updates
// Critical changes (state transitions) dispatch immediately; telemetry is throttled // Critical changes (state transitions) dispatch immediately; telemetry is throttled
static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry
@@ -207,7 +302,14 @@ private:
// Connection thread management // Connection thread management
std::atomic<uint64_t> connect_generation{0}; std::atomic<uint64_t> connect_generation{0};
std::thread connect_thread; std::thread connect_thread;
std::recursive_mutex connect_mutex; mutable std::recursive_mutex connect_mutex;
void run_command_worker();
std::thread cmd_thread;
std::deque<std::function<void()>> cmd_queue;
std::mutex cmd_mutex;
std::condition_variable cmd_cv;
bool cmd_stop = false;
}; };
} // namespace Slic3r } // namespace Slic3r
+154 -18
View File
@@ -1,11 +1,10 @@
#include <stdio.h> #include "NetworkAgent.hpp"
#include <stdlib.h> #include <stdlib.h>
#include <set>
#include <algorithm>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include "libslic3r/Utils.hpp" #include <nlohmann/json.hpp>
#include "NetworkAgent.hpp" #include "IPrinterAgent.hpp"
#include "BBLNetworkPlugin.hpp" #include "BBLNetworkPlugin.hpp"
namespace Slic3r { namespace Slic3r {
@@ -115,6 +114,8 @@ void NetworkAgent::add_cloud_agent(const std::string& provider, std::shared_ptr<
void NetworkAgent::set_printer_agent(std::shared_ptr<IPrinterAgent> printer_agent) void NetworkAgent::set_printer_agent(std::shared_ptr<IPrinterAgent> printer_agent)
{ {
m_user_machine_list_generation.fetch_add(1);
// Disconnect all callbacks from the old agent // Disconnect all callbacks from the old agent
auto old_printer_agent = m_printer_agent; auto old_printer_agent = m_printer_agent;
@@ -432,10 +433,26 @@ int NetworkAgent::check_user_task_report(int* task_id, bool* printable, const st
int NetworkAgent::get_user_print_info(unsigned int* http_code, std::string* http_body, const std::string& provider) int NetworkAgent::get_user_print_info(unsigned int* http_code, std::string* http_body, const std::string& provider)
{ {
const std::string request_agent_id = m_printer_agent_id;
const std::uint64_t request_generation = m_user_machine_list_generation.fetch_add(1) + 1;
const auto cloud_agent = get_cloud_agent(provider); const auto cloud_agent = get_cloud_agent(provider);
if (cloud_agent) if (!cloud_agent)
return cloud_agent->get_user_print_info(http_code, http_body); return -1;
return -1;
const int result = cloud_agent->get_user_print_info(http_code, http_body);
if (result == 0 && http_body) {
try {
nlohmann::json response = nlohmann::json::parse(*http_body);
response["provider"] = provider;
response["agent_id"] = request_agent_id;
response["generation"] = request_generation;
*http_body = response.dump();
}
catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " metadata injection exception=" << e.what();
}
}
return result;
} }
int NetworkAgent::get_user_tasks(TaskQueryParams params, std::string* http_body, const std::string& provider) int NetworkAgent::get_user_tasks(TaskQueryParams params, std::string* http_body, const std::string& provider)
@@ -515,6 +532,15 @@ int NetworkAgent::get_camera_url(std::string dev_id, std::function<void(std::str
return -1; return -1;
} }
std::unique_ptr<ICameraSignalingChannel>
NetworkAgent::create_camera_signaling_channel(const std::string& dev_id, const std::string& provider)
{
const auto cloud_agent = get_cloud_agent(provider);
if (cloud_agent)
return cloud_agent->create_camera_signaling_channel(dev_id);
return nullptr;
}
int NetworkAgent::get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback, const std::string& provider) int NetworkAgent::get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback, const std::string& provider)
{ {
const auto cloud_agent = get_cloud_agent(provider); const auto cloud_agent = get_cloud_agent(provider);
@@ -767,10 +793,81 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos
return -1; return -1;
} }
int NetworkAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode)
{ {
if (m_printer_agent) if (m_printer_agent)
return m_printer_agent->connect_printer(dev_id, dev_ip, username, password, use_ssl); return m_printer_agent->command_ams_refresh_rfid(dev_id, ams_id, slot_id, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_ams_calibrate(dev_id, ams_id, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_ams_select_tray(dev_id, tray_id, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_start_camera(std::string dev_id)
{
if (m_printer_agent)
return m_printer_agent->command_start_camera(dev_id);
return -1;
}
int NetworkAgent::command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_xyz_abs(dev_id, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_auto_leveling(dev_id, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_go_home(dev_id, is_printing, supports_mqtt_homing, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_set_bed(dev_id, temp, supports_mqtt_bed_ctrl, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_set_nozzle(dev_id, temp, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_axis_control(dev_id, axis, unit, input_val, speed, is_core_xy, supports_mqtt_axis_control, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::connect_printer(const PrinterConnectionParams& params)
{
if (m_printer_agent)
return m_printer_agent->connect_printer(params);
return -1; return -1;
} }
@@ -788,6 +885,13 @@ int NetworkAgent::send_message_to_printer(std::string dev_id, std::string json_s
return -1; return -1;
} }
std::string NetworkAgent::default_lan_username() const
{
if (m_printer_agent)
return m_printer_agent->default_lan_username();
return {};
}
int NetworkAgent::check_cert() int NetworkAgent::check_cert()
{ {
if (m_printer_agent) if (m_printer_agent)
@@ -846,8 +950,14 @@ std::string NetworkAgent::get_user_selected_machine()
int NetworkAgent::set_user_selected_machine(std::string dev_id) int NetworkAgent::set_user_selected_machine(std::string dev_id)
{ {
if (m_printer_agent) BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::set_user_selected_machine: dev_id=" << dev_id
return m_printer_agent->set_user_selected_machine(dev_id); << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : "<null>");
if (m_printer_agent) {
const int result = m_printer_agent->set_user_selected_machine(dev_id);
BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::set_user_selected_machine: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::set_user_selected_machine: no printer agent";
return -1; return -1;
} }
@@ -867,15 +977,27 @@ int NetworkAgent::stop_subscribe(std::string module)
int NetworkAgent::add_subscribe(std::vector<std::string> dev_list) int NetworkAgent::add_subscribe(std::vector<std::string> dev_list)
{ {
if (m_printer_agent) BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::add_subscribe: count=" << dev_list.size()
return m_printer_agent->add_subscribe(std::move(dev_list)); << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : "<null>");
if (m_printer_agent) {
const int result = m_printer_agent->add_subscribe(std::move(dev_list));
BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::add_subscribe: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::add_subscribe: no printer agent";
return -1; return -1;
} }
int NetworkAgent::del_subscribe(std::vector<std::string> dev_list) int NetworkAgent::del_subscribe(std::vector<std::string> dev_list)
{ {
if (m_printer_agent) BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::del_subscribe: count=" << dev_list.size()
return m_printer_agent->del_subscribe(std::move(dev_list)); << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : "<null>");
if (m_printer_agent) {
const int result = m_printer_agent->del_subscribe(std::move(dev_list));
BOOST_LOG_TRIVIAL(trace) << "NetworkAgent::del_subscribe: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::del_subscribe: no printer agent";
return -1; return -1;
} }
@@ -921,10 +1043,10 @@ FilamentSyncMode NetworkAgent::get_filament_sync_mode() const
return FilamentSyncMode::none; return FilamentSyncMode::none;
} }
bool NetworkAgent::fetch_filament_info(std::string dev_id) bool NetworkAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
if (m_printer_agent) { if (m_printer_agent) {
return m_printer_agent->fetch_filament_info(dev_id); return m_printer_agent->fetch_filament_info(dev_id, sync_mode);
} }
return false; return false;
} }
@@ -943,6 +1065,20 @@ std::string NetworkAgent::from_orca_filament_id(const std::string& orca_filament
return orca_filament_id; return orca_filament_id;
} }
CameraStreamMode NetworkAgent::get_camera_stream_mode() const
{
if (m_printer_agent)
return m_printer_agent->get_camera_stream_mode();
return CameraStreamMode::none;
}
std::string NetworkAgent::get_local_camera_stream_url() const
{
if (m_printer_agent)
return m_printer_agent->get_camera_url();
return {};
}
int NetworkAgent::request_bind_ticket(std::string* ticket) int NetworkAgent::request_bind_ticket(std::string* ticket)
{ {
if (m_printer_agent) if (m_printer_agent)
+25 -2
View File
@@ -2,16 +2,22 @@
#define __NETWORK_Agent_HPP__ #define __NETWORK_Agent_HPP__
#include "bambu_networking.hpp" #include "bambu_networking.hpp"
#include "libslic3r/ProjectTask.hpp" #include "libslic3r/ProjectTask.hpp"
#include "ICloudServiceAgent.hpp" #include "ICloudServiceAgent.hpp"
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include <map> #include <map>
#include <atomic>
#include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
namespace Slic3r { namespace Slic3r {
class IPrinterAgent;
// Forward declaration // Forward declaration
class BBLNetworkPlugin; class BBLNetworkPlugin;
@@ -50,6 +56,7 @@ public:
// Sub-agent accessors // Sub-agent accessors
std::shared_ptr<ICloudServiceAgent> get_cloud_agent(const std::string& provider = ORCA_CLOUD_PROVIDER) const; std::shared_ptr<ICloudServiceAgent> get_cloud_agent(const std::string& provider = ORCA_CLOUD_PROVIDER) const;
std::shared_ptr<IPrinterAgent> get_printer_agent() const { return m_printer_agent; } std::shared_ptr<IPrinterAgent> get_printer_agent() const { return m_printer_agent; }
std::uint64_t get_user_machine_list_generation() const { return m_user_machine_list_generation.load(); }
// Shared agent management // Shared agent management
void add_cloud_agent(const std::string& provider, std::shared_ptr<ICloudServiceAgent> agent); void add_cloud_agent(const std::string& provider, std::shared_ptr<ICloudServiceAgent> agent);
@@ -109,6 +116,7 @@ public:
int query_bind_status(std::vector<std::string> query_list, unsigned int* http_code, std::string* http_body, const std::string& provider = ORCA_CLOUD_PROVIDER); int query_bind_status(std::vector<std::string> query_list, unsigned int* http_code, std::string* http_body, const std::string& provider = ORCA_CLOUD_PROVIDER);
int modify_printer_name(std::string dev_id, std::string dev_name, const std::string& provider = ORCA_CLOUD_PROVIDER); int modify_printer_name(std::string dev_id, std::string dev_name, const std::string& provider = ORCA_CLOUD_PROVIDER);
int get_camera_url(std::string dev_id, std::function<void(std::string)> callback, const std::string& provider = ORCA_CLOUD_PROVIDER); int get_camera_url(std::string dev_id, std::function<void(std::string)> callback, const std::string& provider = ORCA_CLOUD_PROVIDER);
std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id, const std::string& provider = ORCA_CLOUD_PROVIDER);
int get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback, const std::string& provider = ORCA_CLOUD_PROVIDER); int get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback, const std::string& provider = ORCA_CLOUD_PROVIDER);
int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out, const std::string& provider = ORCA_CLOUD_PROVIDER); int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out, const std::string& provider = ORCA_CLOUD_PROVIDER);
int get_model_publish_url(std::string* url, const std::string& provider = ORCA_CLOUD_PROVIDER); int get_model_publish_url(std::string* url, const std::string& provider = ORCA_CLOUD_PROVIDER);
@@ -142,9 +150,21 @@ public:
int set_on_local_message_fn(OnMessageFn fn); int set_on_local_message_fn(OnMessageFn fn);
int set_server_callback(OnServerErrFn fn); int set_server_callback(OnServerErrFn fn);
int send_message(std::string dev_id, std::string json_str, int qos, int flag); int send_message(std::string dev_id, std::string json_str, int qos, int flag);
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); int command_ams_refresh_rfid(std::string dev_id, int ams_id, int slot_id, int sequence_id, bool lan_mode);
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode);
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode);
int command_start_camera(std::string dev_id);
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode);
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode);
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode);
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode);
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode);
int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode);
int connect_printer(const PrinterConnectionParams& params);
int disconnect_printer(); int disconnect_printer();
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag); int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag);
std::string default_lan_username() const;
int check_cert(); int check_cert();
void install_device_cert(std::string dev_id, bool lan_only); void install_device_cert(std::string dev_id, bool lan_only);
bool start_discovery(bool start, bool sending); bool start_discovery(bool start, bool sending);
@@ -164,7 +184,9 @@ public:
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
FilamentSyncMode get_filament_sync_mode() const; FilamentSyncMode get_filament_sync_mode() const;
bool fetch_filament_info(std::string dev_id); bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull);
CameraStreamMode get_camera_stream_mode() const;
std::string get_local_camera_stream_url() const;
std::string to_orca_filament_id(const std::string& printer_filament_id) const; std::string to_orca_filament_id(const std::string& printer_filament_id) const;
std::string from_orca_filament_id(const std::string& orca_filament_id) const; std::string from_orca_filament_id(const std::string& orca_filament_id) const;
int request_bind_ticket(std::string* ticket); int request_bind_ticket(std::string* ticket);
@@ -195,6 +217,7 @@ private:
std::map<std::string, std::shared_ptr<ICloudServiceAgent>> m_cloud_agents; std::map<std::string, std::shared_ptr<ICloudServiceAgent>> m_cloud_agents;
std::shared_ptr<IPrinterAgent> m_printer_agent; std::shared_ptr<IPrinterAgent> m_printer_agent;
std::string m_printer_agent_id; std::string m_printer_agent_id;
std::atomic<std::uint64_t> m_user_machine_list_generation{0};
}; };
} }
+33 -5
View File
@@ -16,8 +16,10 @@
#include <mutex> #include <mutex>
#include <utility> #include <utility>
#include <slic3r/GUI/GUI_App.hpp> #include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/I18N.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp> #include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/msgdlg.h>
namespace Slic3r { namespace Slic3r {
namespace { namespace {
@@ -314,6 +316,21 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
std::shared_ptr<IPrinterAgent> cached_agent; std::shared_ptr<IPrinterAgent> cached_agent;
auto reject_conflicting_capability = [plugin_key, capability_name](const std::string& error_message) {
if (!wxTheApp || GUI::wxGetApp().is_closing())
return;
GUI::wxGetApp().CallAfter([plugin_key, capability_name, error_message]() {
if (GUI::wxGetApp().is_closing())
return;
PluginManager& manager = PluginManager::instance();
manager.set_plugin_error(plugin_key, error_message);
// note: the unload callback triggered by disabling will call deregister,
// which will be a no-op since the printer agent is never registered
manager.set_capability_enabled({PluginCapabilityType::PrinterConnection, capability_name, plugin_key}, false);
wxMessageBox(wxString::FromUTF8(error_message.c_str()), _L("Plugins"), wxOK | wxICON_WARNING, GUI::wxGetApp().GetTopWindow());
});
};
{ {
std::lock_guard<std::mutex> lock(s_registry_mutex); std::lock_guard<std::mutex> lock(s_registry_mutex);
@@ -332,9 +349,10 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
auto& python_agent_ids = get_python_printer_agent_ids(); auto& python_agent_ids = get_python_printer_agent_ids();
for (const auto& pair : python_agent_ids) { for (const auto& pair : python_agent_ids) {
if (pair.first != capability_key && pair.second == info.id) { if (pair.first != capability_key && pair.second == info.id) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses duplicate agent ID '" << info.id const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
<< "' already registered by capability '" << pair.first.second << "' from plugin '" "' is already registered by capability '" + pair.first.second + "' from plugin '" + pair.first.first + "'.";
<< pair.first.first << "'"; BOOST_LOG_TRIVIAL(warning) << error_message;
reject_conflicting_capability(error_message);
return; return;
} }
} }
@@ -354,12 +372,22 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
auto& agents = get_printer_agents(); auto& agents = get_printer_agents();
auto agent_it = agents.find(info.id); auto agent_it = agents.find(info.id);
// why: reject only when the ID is owned by SOMEONE ELSE - a built-in has an empty
// plugin_identifier, another plugin/capability has a different plugin_full_ref. When it
// IS the same plugin_full_ref, this capability is just re-registering itself, so fall
// through and refresh.
if (agent_it != agents.end() && agent_it->second.plugin_identifier != plugin_full_ref) { if (agent_it != agents.end() && agent_it->second.plugin_identifier != plugin_full_ref) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses agent ID '" << info.id const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
<< "' already registered by '" << agent_it->second.display_name << "'"; "' is already registered by '" + agent_it->second.display_name + "'.";
BOOST_LOG_TRIVIAL(warning) << error_message;
reject_conflicting_capability(error_message);
return; return;
} }
// why: insert_or_assign, not emplace - reaching here means the ID is new, or the same
// capability is re-registering (same plugin_full_ref). In the re-register case we WANT to
// overwrite so the factory closure points at the current live capability instance; emplace
// would silently keep the stale entry.
agents.insert_or_assign(info.id, PrinterAgentInfo(info.id, info.name, plugin_full_ref, std::move(factory))); agents.insert_or_assign(info.id, PrinterAgentInfo(info.id, info.name, plugin_full_ref, std::move(factory)));
python_agent_ids[capability_key] = info.id; python_agent_ids[capability_key] = info.id;
+335 -10
View File
@@ -1,11 +1,15 @@
#include "OrcaCloudServiceAgent.hpp" #include "OrcaCloudServiceAgent.hpp"
#include "Http.hpp" #include "Http.hpp"
#include "ICameraSignalingChannel.hpp"
#include "OrcaCloudSignalingChannel.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include <boost/asio.hpp> #include <boost/asio.hpp>
#include <boost/beast/core/detail/base64.hpp> #include <boost/beast/core/detail/base64.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/uuid/uuid.hpp> #include <boost/uuid/uuid.hpp>
@@ -21,14 +25,18 @@
#include <openssl/hmac.h> #include <openssl/hmac.h>
#include <openssl/rand.h> #include <openssl/rand.h>
#include <openssl/sha.h> #include <openssl/sha.h>
#include <openssl/ssl.h>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <condition_variable>
#include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <optional> #include <optional>
#include <random> #include <random>
#include <set>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -83,6 +91,7 @@ constexpr const char* ORCA_UNSUBSCRIBE_PLUGINS = "/api/v1/plugins/subscriptions"
constexpr const char* ORCA_PLUGINS_MINE = "/api/v1/plugins/mine"; constexpr const char* ORCA_PLUGINS_MINE = "/api/v1/plugins/mine";
constexpr const char* ORCA_PLUGINS_BASE = "/api/v1/plugins"; constexpr const char* ORCA_PLUGINS_BASE = "/api/v1/plugins";
constexpr const char* ORCA_PLUGIN_DOWNLOAD_URL = "/api/v1/plugins/download"; constexpr const char* ORCA_PLUGIN_DOWNLOAD_URL = "/api/v1/plugins/download";
constexpr const char* ORCA_CLOUD_PRINTER = "/api/v1/printers";
constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login"; constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login";
@@ -491,6 +500,7 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
, api_base_url(ORCA_DEFAULT_API_URL) , api_base_url(ORCA_DEFAULT_API_URL)
, auth_base_url(ORCA_DEFAULT_AUTH_URL) , auth_base_url(ORCA_DEFAULT_AUTH_URL)
, cloud_base_url(ORCA_DEFAULT_CLOUD_URL) , cloud_base_url(ORCA_DEFAULT_CLOUD_URL)
, mqtt_connection(std::make_unique<OrcaMqttConnection>())
{ {
auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY; auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY;
pkce_bundle.loopback_port = choose_loopback_port(); pkce_bundle.loopback_port = choose_loopback_port();
@@ -501,6 +511,8 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
OrcaCloudServiceAgent::~OrcaCloudServiceAgent() OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
{ {
if (mqtt_connection)
mqtt_connection->stop();
if (refresh_thread.joinable()) { if (refresh_thread.joinable()) {
refresh_thread.join(); refresh_thread.join();
} }
@@ -941,22 +953,45 @@ bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) { retu
int OrcaCloudServiceAgent::connect_server() int OrcaCloudServiceAgent::connect_server()
{ {
const bool logged_in = is_user_login();
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::connect_server logged_in=" << logged_in
<< " api_base_url=" << api_base_url;
if (!logged_in) {
if (mqtt_connection)
mqtt_connection->stop();
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
is_connected = false;
}
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: connect_server requires a logged-in user";
invoke_server_connected_callback(-1, 401);
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
std::string response; std::string response;
unsigned int http_code = 0; unsigned int http_code = 0;
int result = http_get(ORCA_HEALTH_PATH, &response, &http_code); int result = http_get(ORCA_HEALTH_PATH, &response, &http_code);
bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300); bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health result=" << result << " http_code=" << http_code
<< " connected=" << connected << " response_bytes=" << response.size();
// connect_server() remains a REST health probe. The long-lived fleet MQTT socket
// is started lazily by set_user_selected_machine -> configure_selected_printer_mqtt;
// subscriptions queued before that point are replayed when it starts.
{ {
std::lock_guard<std::recursive_mutex> lock(state_mutex); std::lock_guard<std::recursive_mutex> lock(state_mutex);
is_connected = connected; is_connected = connected;
} }
invoke_server_connected_callback(connected ? 0 : -1, http_code); invoke_server_connected_callback(connected ? 0 : -1, http_code);
return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
} }
bool OrcaCloudServiceAgent::is_server_connected() bool OrcaCloudServiceAgent::is_server_connected()
{ {
// The REST health probe is the signal; the per-printer MQTT socket does not gate
// whole-cloud connectivity (one printer reconnecting must not report the whole
// cloud as lost).
std::lock_guard<std::recursive_mutex> lock(state_mutex); std::lock_guard<std::recursive_mutex> lock(state_mutex);
return is_connected; return is_connected;
} }
@@ -977,13 +1012,238 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module)
int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list) int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list)
{ {
(void) dev_list; BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::add_subscribe count=" << dev_list.size()
return BAMBU_NETWORK_SUCCESS; << " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null");
if (!is_user_login() || !mqtt_connection) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: add_subscribe rejected because cloud is not ready";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
bool queued = true;
for (const std::string& dev_id : dev_list)
queued = mqtt_connection->subscribe(dev_id) && queued;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_subscribe queued=" << queued;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
} }
int OrcaCloudServiceAgent::del_subscribe(std::vector<std::string> dev_list) int OrcaCloudServiceAgent::del_subscribe(std::vector<std::string> dev_list)
{ {
(void) dev_list; BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::del_subscribe count=" << dev_list.size()
<< " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null");
if (!is_user_login() || !mqtt_connection) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: del_subscribe rejected because cloud is not ready";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
bool queued = true;
for (const std::string& dev_id : dev_list)
queued = mqtt_connection->unsubscribe(dev_id) && queued;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_subscribe queued=" << queued;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& dev_id,
OrcaMqttConnection::StateHandler state_handler)
{
(void) dev_id;
if (!ensure_token_fresh("configure_selected_printer_mqtt"))
{
BOOST_LOG_TRIVIAL(warning) << "ensure_token_fresh returned false";
return BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
OrcaMqttConnection::Config cfg;
cfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt";
cfg.use_tls = true;
cfg.bearer_provider = [this] { return get_access_token(); };
cfg.client_id = "OrcaSlicer";
cfg.keepalive_seconds = 300;
{
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
m_selected_printer_mqtt_url = cfg.url;
}
if (mqtt_connection->is_running()) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT connection already running";
return BAMBU_NETWORK_SUCCESS;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: configuring fleet MQTT endpoint=" << cfg.url;
// NOTE: no lock is held across start() — it blocks for the whole initial connect
// attempt (up to ~10s), and the message handler below re-enters callback_mutex on
// the MQTT worker thread.
const bool ok = mqtt_connection->start(
cfg,
[this](const std::string& id, const std::string& payload) { deliver_cloud_message(id, payload); },
std::move(state_handler));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT start returned=" << ok;
return ok ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
void OrcaCloudServiceAgent::teardown_selected_printer_mqtt()
{
if (mqtt_connection) {
mqtt_connection->stop();
// The connection object is reused for the next printer; drop this printer's
// report topic so its 1:1 socket does not re-subscribe the previous device.
mqtt_connection->clear_subscriptions();
}
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
m_selected_printer_mqtt_url.clear();
}
std::string OrcaCloudServiceAgent::selected_printer_mqtt_url() const
{
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
return m_selected_printer_mqtt_url;
}
void OrcaCloudServiceAgent::deliver_cloud_message(const std::string& dev_id, const std::string& payload)
{
OnMessageFn callback;
{
std::lock_guard<std::mutex> lock(callback_mutex);
callback = printer_status_callback;
}
if (callback)
callback(dev_id, payload);
}
int OrcaCloudServiceAgent::set_printer_status_callback(OnMessageFn fn)
{
std::lock_guard<std::mutex> lock(callback_mutex);
printer_status_callback = std::move(fn);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: printer status callback=" << (printer_status_callback ? "set" : "clear");
return BAMBU_NETWORK_SUCCESS;
}
int OrcaCloudServiceAgent::send_printer_command(const std::string& dev_id, const std::string& body)
{
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: send_printer_command dev_id=" << dev_id
<< " body_bytes=" << body.size() << " logged_in=" << is_user_login();
if (dev_id.empty() || !is_user_login()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_printer_command rejected";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + dev_id + "/commands";
std::string response;
unsigned int http_code = 0;
int result = http_post(path, body, &response, &http_code);
BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: command dev=" << dev_id
<< " http=" << http_code << " result=" << result
<< " response_bytes=" << response.size();
return (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300)
? BAMBU_NETWORK_SUCCESS
: BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::upload_gcode_via_cloud(const std::string& dev_id,
const std::string& local_gcode_path,
std::string* job_id,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn)
{
if (dev_id.empty() || local_gcode_path.empty() || !is_user_login())
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
if (cancel_fn && cancel_fn())
return BAMBU_NETWORK_ERR_CANCELED;
// Step 1: POST print-jobs/uploads -> a short-lived presigned R2 PUT URL. No
// metadata rides this request; filename/start are only relevant to the HTTP
// .../start finalize route, which this MQTT-driven flow does not call.
const std::string uploads_path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/uploads";
std::string response;
unsigned int http_code = 0;
int result = http_post(uploads_path, "{}", &response, &http_code);
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/uploads failed http_code=" << http_code;
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
std::string upload_job_id;
std::string upload_url;
try {
const nlohmann::json j = nlohmann::json::parse(response);
upload_job_id = j.value("job_id", "");
upload_url = j.value("upload_url", "");
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: failed to parse print-jobs/uploads response: " << e.what();
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
if (upload_job_id.empty() || upload_url.empty()) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: print-jobs/uploads response missing job_id/upload_url";
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
if (cancel_fn && cancel_fn())
return BAMBU_NETWORK_ERR_CANCELED;
// Step 2: PUT the G-code straight to R2 with the one-time URL from step 1. This
// is a scoped, PUT-only, short-TTL capability with no bearer token of its own,
// so it bypasses http_put (which always prefixes api_base_url and attaches the
// cloud session's Authorization header - neither belongs on an R2 PUT).
bool canceled = false;
unsigned put_status = 0;
std::string put_error;
Http::put(upload_url)
.tls_verify(true)
.header("Content-Type", "text/x.gcode")
.set_put_body(boost::filesystem::path(local_gcode_path))
.timeout_connect(5)
.timeout_max(300) // large G-code over a slow link
.on_progress([&](Http::Progress progress, bool& cancel) {
if (cancel_fn && cancel_fn()) {
cancel = true;
canceled = true;
return;
}
if (update_fn && progress.ultotal > 0) {
const int percent = static_cast<int>((progress.ulnow * 100) / progress.ultotal);
update_fn(PrintingStageUpload, percent, "Uploading...");
}
})
.on_complete([&](std::string, unsigned status) { put_status = status; })
.on_error([&](std::string, std::string err, unsigned status) {
put_status = status;
put_error = std::move(err);
})
.perform_sync();
if (canceled)
return BAMBU_NETWORK_ERR_CANCELED;
if (put_status < 200 || put_status >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: R2 upload failed status=" << put_status << " error=" << put_error;
return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED;
}
if (job_id)
*job_id = std::move(upload_job_id);
return BAMBU_NETWORK_SUCCESS;
}
int OrcaCloudServiceAgent::start_cloud_print_job(const std::string& dev_id,
const std::string& job_id,
const std::string& filename,
bool start)
{
if (dev_id.empty() || job_id.empty() || !is_user_login())
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
nlohmann::json body;
if (!filename.empty())
body["filename"] = filename;
body["start"] = start;
const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/" +
Http::url_encode(job_id) + "/start";
std::string response;
unsigned int http_code = 0;
const int result = http_post(path, body.dump(), &response, &http_code);
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/" << job_id << "/start failed http_code=" << http_code;
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
@@ -2024,6 +2284,10 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
void OrcaCloudServiceAgent::clear_session() void OrcaCloudServiceAgent::clear_session()
{ {
if (mqtt_connection) {
mqtt_connection->stop();
mqtt_connection->clear_subscriptions();
}
{ {
std::lock_guard<std::mutex> lock(session_mutex); std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{}; session = SessionInfo{};
@@ -2139,7 +2403,11 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon
return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
} }
int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) int OrcaCloudServiceAgent::http_post(const std::string& path,
const std::string& body,
std::string* response_body,
unsigned int* http_code,
const std::string& content_type)
{ {
std::string url = api_base_url + path; std::string url = api_base_url + path;
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url; BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url;
@@ -2163,7 +2431,7 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
http.header("Authorization", "Bearer " + token); http.header("Authorization", "Bearer " + token);
} }
http.header("Content-Type", "application/json"); http.header("Content-Type", content_type);
http.set_post_body(body); http.set_post_body(body);
http.on_complete([&](std::string resp_body, unsigned resp_status) { http.on_complete([&](std::string resp_body, unsigned resp_status) {
@@ -2623,11 +2891,65 @@ int OrcaCloudServiceAgent::check_user_task_report(int* task_id, bool* printable)
int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::string* http_body) int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::string* http_body)
{ {
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info (stub)"; std::string response;
unsigned int code = 0;
int result = http_get(ORCA_CLOUD_PRINTER, &response, &code);
if (http_code) if (http_code)
*http_code = 200; *http_code = code;
if (http_body)
*http_body = "{}"; if (result != 0 || code != 200) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info failed - http_code=" << code << ", response=" << response;
return result != 0 ? result : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
}
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: get_user_print_info fetched - http_code=" << code << ", response=" << response;
try {
auto resp_json = nlohmann::json::parse(response);
nlohmann::json devices = nlohmann::json::array();
for (const auto& printer : resp_json.value("data", nlohmann::json::array())) {
nlohmann::json device;
std::string role = printer.value("access_role", "");
// A printer with the role "view" only has monitoring access for orca cloud.
// The printer is owned by a different person and was shared to the current user without
// any permission to control the printer so we discard this printer. Comment this out if
// OrcaSlicer wants to support view only printers.
if (role.empty() || role == "viewer")
continue;
device["dev_id"] = printer.value("id", "");
device["dev_name"] = printer.value("name", "");
if (printer.contains("model") && printer["model"].is_string())
device["dev_model_name"] = printer["model"].get<std::string>();
bool online = false;
if (printer.contains("status_snapshot") && printer["status_snapshot"].is_object()) {
const auto& status = printer["status_snapshot"].value("status", nlohmann::json::object());
online = status.value("connection", nlohmann::json::object()).value("state", "") == "online";
if (status.contains("job") && status["job"].is_object())
device["task_status"] = status["job"].value("state", "");
}
device["dev_online"] = online;
devices.push_back(device);
}
if (http_body) {
nlohmann::json out;
out["devices"] = devices;
*http_body = out.dump();
}
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info parsed - device_count=" << devices.size()
<< ", devices=" << devices.dump();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info parse exception - " << e.what();
return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
}
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
@@ -2709,6 +3031,9 @@ int OrcaCloudServiceAgent::get_camera_url(std::string dev_id, std::function<void
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
std::unique_ptr<ICameraSignalingChannel> OrcaCloudServiceAgent::create_camera_signaling_channel(const std::string& dev_id)
{ return std::make_unique<OrcaCloudSignalingChannel>(this, dev_id); }
int OrcaCloudServiceAgent::get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback) int OrcaCloudServiceAgent::get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback)
{ {
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_design_staffpick (stub)"; BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_design_staffpick (stub)";
+59 -1
View File
@@ -1,7 +1,14 @@
#ifndef __ORCA_CLOUD_SERVICE_AGENT_HPP__ #ifndef __ORCA_CLOUD_SERVICE_AGENT_HPP__
#define __ORCA_CLOUD_SERVICE_AGENT_HPP__ #define __ORCA_CLOUD_SERVICE_AGENT_HPP__
#include "ICameraSignalingChannel.hpp"
#include "ICloudServiceAgent.hpp" #include "ICloudServiceAgent.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <cstdlib> #include <cstdlib>
#include <string> #include <string>
#include <map> #include <map>
@@ -9,12 +16,17 @@
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <functional> #include <functional>
#include <condition_variable>
#include <cstdint>
#include <set>
#include <memory> #include <memory>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "OrcaMqttConnection.hpp"
class wxSecretStore; class wxSecretStore;
namespace Slic3r { namespace Slic3r {
@@ -151,6 +163,8 @@ public:
// Configuration // Configuration
void configure_urls(AppConfig* app_config); void configure_urls(AppConfig* app_config);
// Hostname only; cloud REST, MQTT, and WebRTC signaling use fixed TLS
// endpoints on port 443.
void set_api_base_url(const std::string& url); void set_api_base_url(const std::string& url);
void set_auth_base_url(const std::string& url); void set_auth_base_url(const std::string& url);
void set_cloud_base_url(const std::string& url); void set_cloud_base_url(const std::string& url);
@@ -206,6 +220,19 @@ public:
int add_subscribe(std::vector<std::string> dev_list) override; int add_subscribe(std::vector<std::string> dev_list) override;
int del_subscribe(std::vector<std::string> dev_list) override; int del_subscribe(std::vector<std::string> dev_list) override;
void enable_multi_machine(bool enable) override; void enable_multi_machine(bool enable) override;
int set_printer_status_callback(OnMessageFn fn);
int send_printer_command(const std::string& dev_id, const std::string& body);
int upload_gcode_via_cloud(const std::string& dev_id,
const std::string& local_gcode_path,
std::string* job_id,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn);
int start_cloud_print_job(const std::string& dev_id,
const std::string& job_id,
const std::string& filename,
bool start = true);
// ======================================================================== // ========================================================================
// ICloudServiceAgent Interface Implementation - Settings Synchronization // ICloudServiceAgent Interface Implementation - Settings Synchronization
@@ -241,6 +268,7 @@ public:
// ICloudServiceAgent Interface Implementation - Model Mall & Publishing // ICloudServiceAgent Interface Implementation - Model Mall & Publishing
// ======================================================================== // ========================================================================
int get_camera_url(std::string dev_id, std::function<void(std::string)> callback) override; int get_camera_url(std::string dev_id, std::function<void(std::string)> callback) override;
std::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id) override;
int get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback) override; int get_design_staffpick(int offset, int limit, std::function<void(std::string)> callback) override;
int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) override; int start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) override;
int get_model_publish_url(std::string* url) override; int get_model_publish_url(std::string* url) override;
@@ -346,7 +374,29 @@ public:
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = ""); static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
OrcaMqttConnection* get_mqtt_connection() noexcept {
return mqtt_connection.get();
}
const OrcaMqttConnection* get_mqtt_connection() const noexcept {
return mqtt_connection.get();
}
// Account-scoped cloud socket: wss://<api_base_url>/api/v1/printers/mqtt.
// configure_ blocks for the duration of the initial connect attempt, so callers
// drive it off the UI thread; teardown_ is synchronous. The dev_id argument is
// retained for source compatibility with the printer-agent lifecycle; it does
// not participate in endpoint construction.
int configure_selected_printer_mqtt(const std::string& dev_id,
OrcaMqttConnection::StateHandler state_handler = {});
void teardown_selected_printer_mqtt();
// Test hook: the wss:// URL of the current fleet socket ("" when none).
std::string selected_printer_mqtt_url() const;
private: private:
// Fans one inbound fleet MQTT message out to printer_status_callback.
void deliver_cloud_message(const std::string& dev_id, const std::string& payload);
// Sync protocol helpers // Sync protocol helpers
int sync_pull( int sync_pull(
std::function<void(const SyncPullResponse&)> on_success, std::function<void(const SyncPullResponse&)> on_success,
@@ -370,7 +420,11 @@ private:
// HTTP request helpers // HTTP request helpers
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code); int http_get(const std::string& path, std::string* response_body, unsigned int* http_code);
int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); int http_post(const std::string& path,
const std::string& body,
std::string* response_body,
unsigned int* http_code,
const std::string& content_type = "application/json");
int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code); int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code);
std::map<std::string, std::string> data_headers(); std::map<std::string, std::string> data_headers();
@@ -423,6 +477,9 @@ private:
std::chrono::system_clock::now().time_since_epoch()).count()}; std::chrono::system_clock::now().time_since_epoch()).count()};
// Member variables - connection state // Member variables - connection state
std::unique_ptr<OrcaMqttConnection> mqtt_connection;
std::string m_selected_printer_mqtt_url; // guarded by m_selected_url_mutex
mutable std::mutex m_selected_url_mutex;
bool is_connected{false}; bool is_connected{false};
bool enable_track{false}; bool enable_track{false};
bool multi_machine_enabled{false}; bool multi_machine_enabled{false};
@@ -436,6 +493,7 @@ private:
AppOnHttpErrorFn on_http_error_fn; AppOnHttpErrorFn on_http_error_fn;
GetCountryCodeFn get_country_code_fn; GetCountryCodeFn get_country_code_fn;
QueueOnMainFn queue_on_main_fn; QueueOnMainFn queue_on_main_fn;
OnMessageFn printer_status_callback;
mutable std::mutex callback_mutex; mutable std::mutex callback_mutex;
// Thread safety // Thread safety
@@ -0,0 +1,337 @@
#include "OrcaCloudSignalingChannel.hpp"
#include "Http.hpp"
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/beast/core.hpp>
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
#include <openssl/ssl.h>
#include <cctype>
#include <iomanip>
#include <sstream>
#include <stdexcept>
namespace Slic3r {
OrcaCloudSignalingChannel::OrcaCloudSignalingChannel(OrcaCloudServiceAgent* cloud, std::string dev_id)
: m_cloud(cloud)
, m_dev_id(std::move(dev_id))
{
}
OrcaCloudSignalingChannel::~OrcaCloudSignalingChannel()
{
close();
}
void OrcaCloudSignalingChannel::open()
{
bool expected = false;
if (!m_open.compare_exchange_strong(expected, true))
return;
m_stop.store(false);
m_thread = std::thread([this] { run(); });
}
void OrcaCloudSignalingChannel::close()
{
m_stop.store(true);
std::shared_ptr<Connection> conn;
Http::Ptr request;
{
std::lock_guard<std::mutex> lock(m_mutex);
conn = m_conn;
request = m_inflight_requests;
}
// m_conn is installed only after the live-token request completes, so
// cancel the request independently before waiting for the worker.
if (request)
request->cancel();
if (conn) {
// Established session: close the socket on the io_context's own thread so
// the pending async_read completes and io_context.run() unwinds.
boost::asio::post(conn->io_context, [conn] {
boost::system::error_code ec;
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
boost::beast::get_lowest_layer(conn->websocket).close(ec);
});
// Pre-run() phase (still in the synchronous connect/handshake): best-effort
// direct interruption.
boost::system::error_code ec;
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
boost::beast::get_lowest_layer(conn->websocket).close(ec);
}
if (m_thread.joinable())
m_thread.join();
m_open.store(false);
}
void OrcaCloudSignalingChannel::send_offer(std::string sdp)
{
send_json(nlohmann::json{{"type", "webrtc.offer"}, {"sdp", std::move(sdp)}}.dump());
}
void OrcaCloudSignalingChannel::send_ice(std::string candidate, std::string mid)
{
send_json(nlohmann::json{{"type", "webrtc.ice"},
{"candidate", std::move(candidate)},
{"sdpMid", std::move(mid)}}
.dump());
}
std::string OrcaCloudSignalingChannel::encode_path_component(const std::string& value)
{
std::ostringstream encoded;
encoded << std::uppercase << std::hex;
for (unsigned char c : value) {
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
encoded << c;
else
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<unsigned int>(c);
}
return encoded.str();
}
void OrcaCloudSignalingChannel::unavailable(CameraUnavailableReason reason, std::string detail)
{
if (on_unavailable)
on_unavailable(reason, std::move(detail));
}
void OrcaCloudSignalingChannel::run()
{
try {
if (!m_cloud || !m_cloud->ensure_token_fresh("camera")) {
unavailable(CameraUnavailableReason::Error, "Unable to refresh OrcaCloud credentials");
m_open.store(false);
return;
}
const std::string token = m_cloud->get_access_token();
// OrcaCloud exposes a bare API hostname. WebRTC signaling uses the
// fixed HTTPS/WSS endpoints on port 443; custom schemes, ports, and
// base paths are not supported by this agent.
const std::string host = m_cloud->get_cloud_service_host();
if (token.empty() || host.empty()) {
unavailable(CameraUnavailableReason::Error, "OrcaCloud session is unavailable");
m_open.store(false);
return;
}
const std::string live_token_url =
"https://" + host + "/api/v1/printers/" + encode_path_component(m_dev_id) + "/live-token";
BOOST_LOG_TRIVIAL(info) << "signaling: POST " << live_token_url << " (dev_id=" << m_dev_id << ")";
nlohmann::json token_response;
std::string token_body;
unsigned int http_code = 0;
auto request = std::make_shared<Slic3r::Http>(Http::post(live_token_url));
request->set_post_body(std::string("{}"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.tls_verify(true)
.timeout_max(30)
.on_complete([&token_body, &http_code](std::string body, unsigned status) {
http_code = status;
token_body = std::move(body);
})
.on_error([&http_code](std::string, std::string, unsigned status) {
http_code = status;
});
{
std::lock_guard<std::mutex> lock(m_mutex);
m_inflight_requests = request;
}
// If close() raced with request setup, make sure this request is
// cancelled before entering the blocking call.
if (m_stop.load())
request->cancel();
try {
request->perform_sync();
} catch (...) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_inflight_requests == request)
m_inflight_requests.reset();
throw;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_inflight_requests == request)
m_inflight_requests.reset();
}
try {
token_response = nlohmann::json::parse(token_body);
} catch (const std::exception&) {
}
if (http_code < 200 || http_code >= 300 || !token_response.contains("token")) {
unavailable(CameraUnavailableReason::Error, "Unable to mint camera live token");
m_open.store(false);
return;
}
std::vector<CameraIceServer> ice_servers;
if (token_response.contains("ice_servers") && token_response["ice_servers"].is_array()) {
for (const auto& entry : token_response["ice_servers"]) {
if (entry.is_string()) {
ice_servers.push_back({entry.get<std::string>(), {}, {}});
} else if (entry.is_object()) {
// RTCIceServer.urls is "string | string[]" (Cloudflare
// Realtime returns an array). Emit one CameraIceServer per
// URL, sharing the credentials.
const std::string username = entry.value("username", std::string{});
const std::string credential = entry.value("credential", std::string{});
const auto add_url = [&](const nlohmann::json& url) {
if (url.is_string() && !url.get<std::string>().empty())
ice_servers.push_back({url.get<std::string>(), username, credential});
};
const auto urls = entry.find("urls");
if (urls != entry.end()) {
if (urls->is_array()) {
for (const auto& url : *urls)
add_url(url);
} else {
add_url(*urls);
}
}
}
}
}
auto conn = std::make_shared<Connection>();
conn->ssl_context.set_default_verify_paths();
Http::add_platform_root_certificates(conn->ssl_context.native_handle());
{
std::lock_guard<std::mutex> lock(m_mutex);
m_conn = conn;
}
auto& websocket = conn->websocket;
boost::asio::ip::tcp::resolver resolver(conn->io_context);
const auto endpoints = resolver.resolve(host, "443");
boost::asio::connect(boost::beast::get_lowest_layer(websocket), endpoints);
if (!SSL_set_tlsext_host_name(websocket.next_layer().native_handle(), host.c_str()))
throw std::runtime_error("Unable to configure TLS server name");
websocket.next_layer().set_verify_mode(boost::asio::ssl::verify_peer);
websocket.next_layer().set_verify_callback(boost::asio::ssl::host_name_verification(host));
websocket.next_layer().handshake(boost::asio::ssl::stream_base::client);
const std::string ws_target = "/api/v1/printers/" + encode_path_component(m_dev_id) +
"/camera/live?token=" +
encode_path_component(token_response["token"].get<std::string>());
websocket.handshake(host, ws_target);
BOOST_LOG_TRIVIAL(info) << "signaling: websocket handshake ok (" << ice_servers.size()
<< " ice servers)";
if (on_ready)
on_ready(std::move(ice_servers));
send_json(nlohmann::json{{"type", "camera.mode"}, {"mode", "webrtc"}}.dump());
// Async read loop, driven by the connection's own io_context. run()
// returns once close() has shut the socket down, giving a bounded,
// deadlock-free teardown from any thread.
do_read(conn);
conn->io_context.run();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: run() exception: " << e.what();
if (!m_stop.load())
unavailable(CameraUnavailableReason::Closed, e.what());
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_conn.reset();
}
m_open.store(false);
}
void OrcaCloudSignalingChannel::do_read(std::shared_ptr<Connection> conn)
{
auto buffer = std::make_shared<boost::beast::flat_buffer>();
conn->websocket.async_read(
*buffer, [this, conn, buffer](boost::system::error_code ec, std::size_t) {
if (ec) {
if (!m_stop.load())
unavailable(CameraUnavailableReason::Closed, ec.message());
return; // do not re-arm; io_context.run() unwinds
}
const std::string raw = boost::beast::buffers_to_string(buffer->data());
// A malformed or unexpectedly-shaped message must not tear down the
// session: parse/dispatch is guarded.
try {
dispatch_message(nlohmann::json::parse(raw), raw);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: ignoring malformed message: " << e.what()
<< " raw=" << raw.substr(0, 256);
}
if (!m_stop.load())
do_read(conn);
});
}
// Returns the string at key, or "" if absent or not a string (JSON null included).
static std::string json_string(const nlohmann::json& object, const char* key)
{
const auto it = object.find(key);
return (it != object.end() && it->is_string()) ? it->get<std::string>() : std::string{};
}
void OrcaCloudSignalingChannel::dispatch_message(const nlohmann::json& message, const std::string& raw)
{
const std::string type = json_string(message, "type");
BOOST_LOG_TRIVIAL(info) << "signaling: recv type=" << type << " raw=" << raw.substr(0, 256);
if (type == "webrtc.answer") {
const std::string sdp = json_string(message, "sdp");
if (on_answer && !sdp.empty())
on_answer(sdp);
} else if (type == "webrtc.ice" && message.contains("candidate")) {
// The peer may send "candidate" as a flat string or as a nested
// RTCIceCandidateInit object { candidate, sdpMid, sdpMLineIndex }.
const nlohmann::json& candidate = message["candidate"];
std::string sdp_candidate;
std::string mid = json_string(message, "sdpMid");
if (candidate.is_string()) {
sdp_candidate = candidate.get<std::string>();
} else if (candidate.is_object()) {
sdp_candidate = json_string(candidate, "candidate");
std::string nested_mid = json_string(candidate, "sdpMid");
if (!nested_mid.empty())
mid = std::move(nested_mid);
}
if (on_ice && !sdp_candidate.empty())
on_ice(sdp_candidate, mid);
} else if (type == "webrtc.unavailable") {
const std::string reason = json_string(message, "reason");
unavailable(reason == "busy" ? CameraUnavailableReason::Busy
: reason == "disabled" ? CameraUnavailableReason::Disabled
: CameraUnavailableReason::Error,
reason.empty() ? "error" : reason);
}
}
void OrcaCloudSignalingChannel::send_json(const std::string& message)
{
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(m_mutex);
conn = m_conn;
}
if (!conn || m_stop.load())
return;
// Serialize the write onto the io_context thread (same thread that runs
// async_read), so reads and writes never touch the stream concurrently.
auto payload = std::make_shared<std::string>(message);
boost::asio::post(conn->io_context, [this, conn, payload] {
if (m_stop.load())
return;
boost::system::error_code ec;
conn->websocket.write(boost::asio::buffer(*payload), ec);
if (ec && !m_stop.load())
unavailable(CameraUnavailableReason::Closed, ec.message());
});
}
} // namespace Slic3r
@@ -0,0 +1,66 @@
#pragma once
#include "ICameraSignalingChannel.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "Http.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <nlohmann/json_fwd.hpp>
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
namespace Slic3r {
class OrcaCloudSignalingChannel : public ICameraSignalingChannel {
public:
OrcaCloudSignalingChannel(OrcaCloudServiceAgent* cloud, std::string dev_id);
~OrcaCloudSignalingChannel() override;
void open() override;
void close() override;
void send_offer(std::string sdp) override;
void send_ice(std::string candidate, std::string mid) override;
private:
using WebSocket = boost::beast::websocket::stream<
boost::beast::ssl_stream<boost::asio::ip::tcp::socket>>;
// The io_context and ssl_context must outlive the websocket stream that
// references them. Bundling them here with the stream declared last makes
// the destruction order correct (stream first, then contexts), and lets a
// single shared_ptr own the whole set.
struct Connection {
boost::asio::io_context io_context;
boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tls_client};
WebSocket websocket{io_context, ssl_context};
};
void run();
void do_read(std::shared_ptr<Connection> conn);
void dispatch_message(const nlohmann::json& message, const std::string& raw);
void send_json(const std::string& message);
void unavailable(CameraUnavailableReason reason, std::string detail);
static std::string encode_path_component(const std::string& value);
OrcaCloudServiceAgent* m_cloud;
Http::Ptr m_inflight_requests{nullptr};
std::string m_dev_id;
std::atomic<bool> m_stop{false};
std::atomic<bool> m_open{false};
std::thread m_thread;
mutable std::mutex m_mutex;
std::shared_ptr<Connection> m_conn;
};
} // namespace Slic3r
+803
View File
@@ -0,0 +1,803 @@
#include "OrcaMqttConnection.hpp"
#include "Http.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <openssl/ssl.h>
#include <algorithm>
#include <chrono>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <utility>
namespace Slic3r {
struct OrcaMqttConnection::Connection {
boost::asio::io_context io_context;
boost::asio::ssl::context ssl_context;
boost::asio::ip::tcp::resolver resolver;
boost::asio::steady_timer keepalive_timer;
boost::beast::flat_buffer read_buffer;
std::deque<std::shared_ptr<std::vector<uint8_t>>> outbound_packets;
boost::system::error_code terminal_error;
std::atomic_bool async_session_started{false};
bool write_in_progress{false};
// Exactly one of these is engaged once ws_handshake() has run: wss for
// wss:// endpoints, ws for plaintext ws://.
std::optional<TlsWebSocket> wss;
std::optional<PlainWebSocket> ws;
Connection()
: ssl_context(boost::asio::ssl::context::tls_client)
, resolver(io_context)
, keepalive_timer(io_context)
{}
};
namespace {
// Apply / clear a tcp_stream timeout on whichever websocket is engaged.
// Templated on the connection type only because Connection is a private nested
// type: a deduced parameter needs no (inaccessible) name for it.
template<class Conn> void expires_after(Conn& conn, std::chrono::seconds timeout) {
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_after(timeout);
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_after(timeout);
}
template<class Conn> void expires_never(Conn& conn) {
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_never();
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_never();
}
} // namespace
OrcaMqttConnection::~OrcaMqttConnection() { stop(); }
bool OrcaMqttConnection::start(const Config& config, MessageHandler on_message, StateHandler on_state) {
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
stop();
{
std::lock_guard<std::mutex> lock(mutex);
current_config = config;
this->on_message = std::move(on_message);
this->on_state = std::move(on_state);
initial_result = false;
initial_completed = false;
connected = false;
m_last_connack_rc.store(-1);
}
stopping.store(false);
worker = std::thread(&OrcaMqttConnection::run, this);
std::unique_lock<std::mutex> lock(mutex);
if (!initial_cv.wait_for(lock, std::chrono::seconds(10), [this] { return initial_completed; })) {
initial_completed = true;
initial_result = false;
}
return initial_result;
}
void OrcaMqttConnection::stop() {
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
stopping.store(true);
state_cv.notify_all();
{
std::lock_guard<std::mutex> lock(connection_mutex);
if (active_connection) {
if (active_connection->async_session_started.load()) {
// The worker owns the live WebSocket. Stop dispatching its
// asynchronous operations; the worker closes the socket after
// leaving the event loop.
active_connection->io_context.stop();
} else {
// Setup still uses synchronous operations on the worker. Wake a
// pending resolve/connect/CONNACK read without competing with a
// live asynchronous session.
auto shutdown_socket = [](auto& websocket) {
auto& socket = boost::beast::get_lowest_layer(websocket).socket();
boost::system::error_code socket_error;
if (socket.cancel(socket_error))
return;
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error))
return;
if (socket.close(socket_error))
return;
};
if (active_connection->wss)
shutdown_socket(*active_connection->wss);
else if (active_connection->ws)
shutdown_socket(*active_connection->ws);
active_connection->resolver.cancel();
}
}
}
if (worker.joinable())
worker.join();
{
std::lock_guard<std::mutex> lock(mutex);
connected = false;
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
if (!initial_completed) {
initial_completed = true;
initial_result = false;
}
}
initial_cv.notify_all();
}
bool OrcaMqttConnection::is_running() const {
return worker.joinable() && !stopping.load();
}
void OrcaMqttConnection::flush_subscription_change() {
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn = active_connection;
}
bool connacked;
{
std::lock_guard<std::mutex> lock(mutex);
connacked = connected;
}
if (!conn || !connacked)
return; // no live MQTT session yet — the worker sends the set on CONNACK
boost::asio::post(conn->io_context, [this, conn] {
if (!stopping.load() && connected.load())
send_pending_subscriptions(conn);
});
}
bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
if (dev_id.empty()) {
return false;
}
const std::string topic = report_topic(dev_id);
if (topic.size() > 96) { // MQTT topic filter cap enforced by the service
return false;
}
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(topic) != 0 && pending_unsubscriptions.count(topic) == 0) {
return true;
}
subscriptions.insert(topic);
pending_unsubscriptions.erase(topic);
pending_subscriptions.insert(topic);
}
state_cv.notify_all();
flush_subscription_change(); // ask the worker to emit SUBSCRIBE now (no reconnect)
return true;
}
bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) {
const std::string topic = report_topic(dev_id);
{
std::lock_guard<std::mutex> lock(mutex);
subscriptions.erase(topic);
acknowledged_subscriptions.erase(topic);
pending_subscriptions.erase(topic);
pending_unsubscriptions.insert(topic);
for (auto it = pending_subscribe_packets.begin(); it != pending_subscribe_packets.end();) {
if (it->second == topic)
it = pending_subscribe_packets.erase(it);
else
++it;
}
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
if (it->first == dev_id)
it = pending_requests.erase(it);
else
++it;
}
}
state_cv.notify_all();
flush_subscription_change(); // ask the worker to emit UNSUBSCRIBE now (no reconnect)
return true;
}
void OrcaMqttConnection::clear_subscriptions() {
std::lock_guard<std::mutex> lock(mutex);
subscriptions.clear();
pending_subscriptions.clear();
pending_unsubscriptions.clear();
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
}
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();
}
void OrcaMqttConnection::append_string(std::vector<uint8_t>& packet, const std::string& value) {
if (value.size() > 0xffff)
throw std::runtime_error("MQTT string is too long");
packet.push_back(static_cast<uint8_t>(value.size() >> 8));
packet.push_back(static_cast<uint8_t>(value.size() & 0xff));
packet.insert(packet.end(), value.begin(), value.end());
}
void OrcaMqttConnection::prepend_remaining_length(std::vector<uint8_t>& packet, size_t length) {
std::vector<uint8_t> encoded;
do {
uint8_t byte = static_cast<uint8_t>(length % 128);
length /= 128;
if (length != 0)
byte |= 0x80;
encoded.push_back(byte);
} while (length != 0);
packet.insert(packet.begin() + 1, encoded.begin(), encoded.end());
}
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;
}
std::string OrcaMqttConnection::report_topic(const std::string& device_id) { return "device/" + device_id + "/report"; }
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;
}
std::vector<uint8_t> OrcaMqttConnection::make_ping_packet() { return {0xc0, 0}; }
void OrcaMqttConnection::ws_write(Connection& conn, const std::vector<uint8_t>& packet) {
if (packet.empty()) {
return;
}
if (conn.wss) {
conn.wss->binary(true);
conn.wss->write(boost::asio::buffer(packet));
} else if (conn.ws) {
conn.ws->binary(true);
conn.ws->write(boost::asio::buffer(packet));
}
}
void OrcaMqttConnection::close_connection(Connection& conn) {
boost::system::error_code error;
if (conn.wss) {
auto& socket = boost::beast::get_lowest_layer(*conn.wss).socket();
if (socket.cancel(error))
return;
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
return;
if (socket.close(error))
return;
} else if (conn.ws) {
auto& socket = boost::beast::get_lowest_layer(*conn.ws).socket();
if (socket.cancel(error))
return;
if (socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, error))
return;
if (socket.close(error))
return;
}
}
void OrcaMqttConnection::enqueue_packet(const std::shared_ptr<Connection>& conn,
std::vector<uint8_t> packet) {
if (!conn || packet.empty())
return;
conn->outbound_packets.emplace_back(std::make_shared<std::vector<uint8_t>>(std::move(packet)));
start_async_write(conn);
}
void OrcaMqttConnection::start_async_write(const std::shared_ptr<Connection>& conn) {
if (!conn || conn->write_in_progress || conn->outbound_packets.empty() || stopping.load())
return;
conn->write_in_progress = true;
const auto packet = conn->outbound_packets.front();
auto on_write = [this, conn](const boost::system::error_code& error, std::size_t) {
conn->write_in_progress = false;
if (error) {
if (!stopping.load())
conn->terminal_error = error;
conn->io_context.stop();
return;
}
conn->outbound_packets.pop_front();
start_async_write(conn);
};
if (conn->wss) {
conn->wss->binary(true);
conn->wss->async_write(boost::asio::buffer(*packet), std::move(on_write));
} else if (conn->ws) {
conn->ws->binary(true);
conn->ws->async_write(boost::asio::buffer(*packet), std::move(on_write));
} else {
conn->write_in_progress = false;
conn->outbound_packets.pop_front();
}
}
void OrcaMqttConnection::start_async_read(const std::shared_ptr<Connection>& conn) {
if (!conn || stopping.load())
return;
auto on_read = [this, conn](const boost::system::error_code& error, std::size_t) {
if (error) {
if (!stopping.load())
conn->terminal_error = error;
conn->io_context.stop();
return;
}
const std::string packet = boost::beast::buffers_to_string(conn->read_buffer.data());
conn->read_buffer.consume(conn->read_buffer.size());
handle_packet(packet);
start_async_read(conn);
};
if (conn->wss)
conn->wss->async_read(conn->read_buffer, std::move(on_read));
else if (conn->ws)
conn->ws->async_read(conn->read_buffer, std::move(on_read));
}
void OrcaMqttConnection::schedule_keepalive(const std::shared_ptr<Connection>& conn) {
const int keepalive = current_config.keepalive_seconds;
if (!conn || keepalive <= 0 || stopping.load())
return;
conn->keepalive_timer.expires_after(std::chrono::seconds(std::max(1, keepalive / 2)));
conn->keepalive_timer.async_wait([this, conn](const boost::system::error_code& error) {
if (error || stopping.load())
return;
enqueue_packet(conn, make_ping_packet());
schedule_keepalive(conn);
});
}
void OrcaMqttConnection::post_packet(const std::shared_ptr<Connection>& conn,
std::vector<uint8_t> packet) {
if (!conn || packet.empty())
return;
boost::asio::post(conn->io_context, [this, conn, packet = std::move(packet)]() mutable {
if (!stopping.load())
enqueue_packet(conn, std::move(packet));
});
}
std::size_t OrcaMqttConnection::ws_read(Connection& conn, boost::beast::flat_buffer& buffer,
boost::system::error_code& ec) {
if (conn.wss)
return conn.wss->read(buffer, ec);
if (conn.ws)
return conn.ws->read(buffer, ec);
ec = boost::asio::error::not_connected;
return 0;
}
void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint) {
const auto results = conn.resolver.resolve(endpoint.host, endpoint.port);
std::string token;
if (config.bearer_provider) {
token = config.bearer_provider();
}
auto decorator = [token](boost::beast::websocket::request_type& request) {
request.set(boost::beast::http::field::user_agent, "OrcaSlicer");
if (!token.empty())
request.set(boost::beast::http::field::authorization, "Bearer " + token);
request.set("Sec-WebSocket-Protocol", "mqtt");
};
boost::beast::http::response<boost::beast::http::string_body> response;
boost::system::error_code handshake_error;
if (config.use_tls) {
// stop() inspects the engaged optional under connection_mutex; publish it
// under the same lock, then release before the blocking connect.
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn.wss.emplace(conn.io_context, conn.ssl_context);
}
auto& websocket = *conn.wss;
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
// Set SNI before the TLS handshake so the cloud edge selects the correct
// certificate.
auto& tls_stream = websocket.next_layer();
if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), endpoint.host.c_str()))
throw std::runtime_error("failed to set Orca Cloud TLS server name");
if (!config.ca_file.empty())
conn.ssl_context.load_verify_file(config.ca_file);
else
conn.ssl_context.set_default_verify_paths();
Http::add_platform_root_certificates(conn.ssl_context.native_handle());
tls_stream.set_verify_mode(boost::asio::ssl::verify_peer);
tls_stream.set_verify_callback(boost::asio::ssl::host_name_verification(endpoint.host));
tls_stream.handshake(boost::asio::ssl::stream_base::client);
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
} else {
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn.ws.emplace(conn.io_context);
}
auto& websocket = *conn.ws;
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
}
if (handshake_error) {
throw boost::system::system_error(handshake_error, "Orca WebSocket handshake");
}
if (response["Sec-WebSocket-Protocol"] != "mqtt") {
throw std::runtime_error("Orca WebSocket did not negotiate MQTT");
}
}
bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::string& payload) {
if (dev_id.empty()) {
return false;
}
if (!connected.load()) {
return false;
}
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn = active_connection;
}
if (!conn) {
return false;
}
const std::string report = report_topic(dev_id);
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(report) != 0 && acknowledged_subscriptions.count(report) == 0) {
pending_requests.emplace_back(dev_id, payload);
return true;
}
}
post_packet(conn, make_publish_packet(request_topic(dev_id), payload));
return true;
}
void OrcaMqttConnection::connect_and_read() {
auto connection = std::make_shared<Connection>();
{
std::lock_guard<std::mutex> lock(connection_mutex);
active_connection = connection;
if (stopping.load()) {
active_connection.reset();
return;
}
}
auto clear_connection = [this, connection] {
close_connection(*connection);
std::lock_guard<std::mutex> lock(connection_mutex);
if (active_connection == connection)
active_connection.reset();
};
try {
Endpoint endpoint;
if (!parse_endpoint(current_config.url, endpoint)) {
throw std::runtime_error("invalid Orca Cloud WebSocket endpoint");
}
ws_handshake(*connection, current_config, endpoint);
expires_never(*connection);
// Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the
// CONNECT username/password fields are omitted entirely (the cloud form).
const bool use_bearer = static_cast<bool>(current_config.bearer_provider);
ws_write(*connection, make_connect_packet(current_config.client_id,
use_bearer ? std::string() : current_config.username,
use_bearer ? std::string() : current_config.password,
current_config.keepalive_seconds));
boost::beast::flat_buffer buffer;
expires_after(*connection, std::chrono::seconds(10));
boost::system::error_code connack_error;
ws_read(*connection, buffer, connack_error);
if (connack_error)
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
const std::string connack = boost::beast::buffers_to_string(buffer.data());
// rc: 0 accepted, 1..5 refusal, -1 malformed/not a CONNACK.
const int rc = (connack.size() == 4 && static_cast<uint8_t>(connack[0]) == 0x20)
? static_cast<int>(static_cast<uint8_t>(connack[3]))
: -1;
m_last_connack_rc.store(rc);
if (rc != 0) {
if (rc == 4 || rc == 5) {
// Bad credentials / not authorized — retrying cannot help. Make run()'s
// loop exit and unblock any waiting start().
stopping.store(true);
{
std::lock_guard<std::mutex> lock(mutex);
initial_completed = true;
initial_result = false;
}
initial_cv.notify_all();
}
throw std::runtime_error("Orca MQTT CONNECT refused rc=" + std::to_string(rc));
}
// The subscription acknowledgement belongs to this MQTT session. Clear
// the previous session's state before notifying the owner, because the
// reconnect callback immediately queues the printer's initial requests.
{
std::lock_guard<std::mutex> lock(mutex);
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
}
connection->async_session_started.store(true);
notify_state(true);
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
send_current_subscriptions(connection);
start_async_read(connection);
schedule_keepalive(connection);
const std::size_t handlers_run = connection->io_context.run();
if (handlers_run == 0 && !stopping.load())
throw std::runtime_error("Orca MQTT event loop stopped unexpectedly");
if (connection->terminal_error && !stopping.load())
throw boost::system::system_error(connection->terminal_error, "read Orca MQTT message");
clear_connection();
if (!stopping.load())
notify_state(false);
} catch (...) {
clear_connection();
throw;
}
}
void OrcaMqttConnection::send_current_subscriptions(const std::shared_ptr<Connection>& conn) {
std::vector<std::string> topics;
{
std::lock_guard<std::mutex> lock(mutex);
topics.assign(subscriptions.begin(), subscriptions.end());
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
for (const std::string& topic : topics)
pending_subscriptions.erase(topic);
}
for (const std::string& topic : topics) {
const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
}
}
void OrcaMqttConnection::send_pending_subscriptions(const std::shared_ptr<Connection>& conn) {
std::vector<std::string> subscribe_topics;
std::vector<std::string> unsubscribe_topics;
{
std::lock_guard<std::mutex> lock(mutex);
subscribe_topics.assign(pending_subscriptions.begin(), pending_subscriptions.end());
unsubscribe_topics.assign(pending_unsubscriptions.begin(), pending_unsubscriptions.end());
pending_subscriptions.clear();
pending_unsubscriptions.clear();
}
for (const std::string& topic : subscribe_topics) {
const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
enqueue_packet(conn, make_subscribe_packet(packet_id, topic, 1));
}
for (const std::string& topic : unsubscribe_topics) {
const uint16_t packet_id = next_packet_id++;
enqueue_packet(conn, make_unsubscribe_packet(packet_id, topic));
}
}
void OrcaMqttConnection::handle_packet(const std::string& packet) {
if (packet.size() < 2) {
return;
}
const uint8_t header = static_cast<uint8_t>(packet[0]);
const uint8_t packet_type = header >> 4;
if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status.
if (packet_type == 9 && packet.size() >= 5) {
const uint16_t packet_id = (static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) |
static_cast<unsigned int>(static_cast<uint8_t>(packet[3]));
std::ostringstream result_codes;
for (size_t index = 4; index < packet.size(); ++index) {
if (index != 4)
result_codes << ',';
result_codes << "0x" << std::hex << static_cast<unsigned int>(static_cast<uint8_t>(packet[index]));
}
// Each production SUBSCRIBE packet currently contains one topic.
// MQTT grants QoS 0 or 1 for a requested QoS 1 subscription; 0x80
// means the subscription was rejected.
const uint8_t result = static_cast<uint8_t>(packet[4]);
std::string topic;
std::deque<std::pair<std::string, std::string>> requests;
{
std::lock_guard<std::mutex> lock(mutex);
auto pending = pending_subscribe_packets.find(packet_id);
if (pending != pending_subscribe_packets.end()) {
topic = pending->second;
pending_subscribe_packets.erase(pending);
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
if (report_topic(it->first) == topic) {
requests.push_back(std::move(*it));
it = pending_requests.erase(it);
} else {
++it;
}
}
if (result == 0 || result == 1) {
acknowledged_subscriptions.insert(topic);
}
}
}
if (topic.empty()) {
} else if (result == 0 || result == 1) {
for (const auto& request : requests) {
if (!send_request(request.first, request.second)) {
}
}
} else {
}
}
return;
}
size_t index = 1;
size_t multiplier = 1;
size_t remaining = 0;
uint8_t encoded = 0;
do {
if (index >= packet.size() || multiplier > 128 * 128 * 128) {
return;
}
encoded = static_cast<uint8_t>(packet[index++]);
remaining += (encoded & 0x7f) * multiplier;
multiplier *= 128;
} while ((encoded & 0x80) != 0);
const size_t remaining_end = index + remaining;
if (remaining_end > packet.size() || remaining < 2 || index + 2 > remaining_end) {
return;
}
const uint16_t topic_length = (static_cast<uint8_t>(packet[index]) << 8) |
static_cast<uint8_t>(packet[index + 1]);
index += 2;
if (topic_length > packet.size() - index) {
return;
}
const std::string topic(packet.data() + index, topic_length);
index += topic_length;
if (((header >> 1) & 0x03) != 0) {
if (index + 2 > remaining_end) {
return;
}
index += 2; // QoS 1/2 packet identifier; the service currently sends QoS 0.
}
const size_t payload_size = remaining_end - index;
// topic is "device/<id>/report" (or "/request"); hand the id up, drop anything else.
std::string dev_id;
if (topic.rfind("device/", 0) == 0) {
const size_t id_start = 7;
const size_t id_end = topic.rfind('/');
if (id_end != std::string::npos && id_end > id_start)
dev_id = topic.substr(id_start, id_end - id_start);
}
if (dev_id.empty()) {
} else if (on_message) {
on_message(dev_id, packet.substr(index, remaining_end - index));
} else {
}
}
void OrcaMqttConnection::notify_state(bool is_now_connected) {
StateHandler callback;
bool initial = false;
{
std::lock_guard<std::mutex> lock(mutex);
connected = is_now_connected;
initial = !initial_completed;
if (initial) {
initial_result = is_now_connected;
initial_completed = true;
}
callback = on_state;
}
if (initial)
initial_cv.notify_all();
else if (callback)
callback(is_now_connected, false);
}
void OrcaMqttConnection::run() {
while (!stopping.load()) {
const int retry_seconds = reconnect_delay_seconds.load();
const uint64_t attempt = ++m_attempt_number;
try {
connect_and_read();
} catch (const std::exception& error) {
if (!stopping.load())
notify_state(false);
}
if (stopping.load())
break;
// Grow the backoff only across attempts that never reached CONNACK; a
// successful connection resets reconnect_delay_seconds to 1 (connect_and_read).
reconnect_delay_seconds.store(std::min(retry_seconds * 2, 30));
std::unique_lock<std::mutex> lock(mutex);
state_cv.wait_for(lock, std::chrono::seconds(retry_seconds), [this] { return stopping.load(); });
}
}
} // namespace Slic3r
+155
View File
@@ -0,0 +1,155 @@
#ifndef slic3r_OrcaMqttConnection_hpp_
#define slic3r_OrcaMqttConnection_hpp_
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <cstddef>
#include <cstdint>
namespace Slic3r {
// Minimal MQTT 3.1.1 codec + WebSocket transport (ws:// and wss://), shared by the
// LAN (OrcaSonar) and cloud (fleet) printer connections. Both PUBLISH
// commands to device/<id>/request and SUBSCRIBE device/<id>/report; Config is the
// only per-transport difference.
class OrcaMqttConnection
{
public:
using TokenProvider = std::function<std::string()>;
using MessageHandler = std::function<void(const std::string&, const std::string&)>;
using StateHandler = std::function<void(bool connected, bool initial)>;
struct Endpoint { std::string host; std::string port; std::string target; };
struct Config {
std::string url;
bool use_tls = false;
std::string ca_file;
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 parse_endpoint(const std::string& url, Endpoint& endpoint);
// Build an MQTT 3.1.1 CONNECT packet. Clean-session is always set; the
// username/password connect flags and payload fields are added only when
// username is non-empty (the cloud form authenticates via a bearer on the
// WebSocket upgrade and omits CONNECT credentials). Public for unit tests.
static std::vector<uint8_t> make_connect_packet(const std::string& client_id,
const std::string& username,
const std::string& password,
int keepalive_seconds);
// Topic-string helpers for the per-device request/report channels and the
// MQTT 3.1.1 PUBLISH / SUBSCRIBE / UNSUBSCRIBE packet builders. All public
// for unit tests. make_publish_packet emits QoS 0 (no packet identifier).
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"
static std::vector<uint8_t> make_publish_packet(const std::string& topic, const std::string& payload);
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);
~OrcaMqttConnection();
bool start(const Config& config, MessageHandler on_message, StateHandler on_state);
void stop();
// True while the worker thread is alive (connected OR retrying). Lets callers
// avoid restarting a healthy connection.
bool is_running() const;
// True once CONNACK has been received and the socket has not since dropped.
bool is_connected() const { return connected.load(); }
bool subscribe(const std::string& dev_id);
bool unsubscribe(const std::string& dev_id);
// Last MQTT CONNACK return code: 0 ok, 1..5 refusal, -1 none seen this attempt.
int last_connack_rc() const { return m_last_connack_rc.load(); }
void clear_subscriptions();
bool send_request(const std::string& dev_id, const std::string& payload);
private:
// The endpoint may be either a TLS (wss://) or a plaintext (ws://) WebSocket;
// Connection holds whichever one is engaged and the ws_* helpers below
// dispatch on it.
using TlsWebSocket = boost::beast::websocket::stream<
boost::asio::ssl::stream<boost::beast::tcp_stream>>;
using PlainWebSocket = boost::beast::websocket::stream<boost::beast::tcp_stream>;
struct Connection;
static void append_string(std::vector<uint8_t>& packet, const std::string& value);
static void prepend_remaining_length(std::vector<uint8_t>& packet, size_t length);
static std::vector<uint8_t> make_ping_packet();
// Transport dispatch: each forwards to conn.wss (TLS) or conn.ws (plaintext).
// These synchronous operations are called only by the MQTT worker during
// connection setup. Once the MQTT session is established, all socket I/O is
// asynchronous and owned by that worker's io_context.
void ws_write(Connection& conn, const std::vector<uint8_t>& packet);
std::size_t ws_read(Connection& conn, boost::beast::flat_buffer& buffer, boost::system::error_code& ec);
void ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint);
void close_connection(Connection& conn);
void enqueue_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
void start_async_write(const std::shared_ptr<Connection>& conn);
void start_async_read(const std::shared_ptr<Connection>& conn);
void schedule_keepalive(const std::shared_ptr<Connection>& conn);
void post_packet(const std::shared_ptr<Connection>& conn, std::vector<uint8_t> packet);
// Ask the MQTT worker to emit subscription changes on its own io_context.
void flush_subscription_change();
void connect_and_read();
void send_current_subscriptions(const std::shared_ptr<Connection>& conn);
void send_pending_subscriptions(const std::shared_ptr<Connection>& conn);
void handle_packet(const std::string& packet);
void notify_state(bool is_now_connected);
void run();
std::atomic_bool stopping{true};
std::atomic_int reconnect_delay_seconds{1};
// Serialises the whole of start() and stop() against each other, so the UI
// thread's stop() (disconnect / dtor) cannot race the connect thread's start()
// into a concurrent worker.join(). Recursive because start() calls stop().
std::recursive_mutex lifecycle_mutex;
std::thread worker;
std::mutex mutex;
std::mutex connection_mutex;
std::shared_ptr<Connection> active_connection;
std::condition_variable initial_cv;
std::condition_variable state_cv;
Config current_config;
MessageHandler on_message;
StateHandler on_state;
// Full report-topic strings ("device/<id>/report"), not bare device ids.
std::set<std::string> subscriptions;
std::set<std::string> pending_subscriptions;
std::set<std::string> pending_unsubscriptions;
// Requests for a subscribed device wait until the corresponding SUBACK is
// received. Otherwise an immediate pushall response can be published by
// the broker before this client is actually subscribed to the report topic.
std::set<std::string> acknowledged_subscriptions;
std::map<uint16_t, std::string> pending_subscribe_packets;
std::deque<std::pair<std::string, std::string>> pending_requests;
std::atomic<uint16_t> next_packet_id{1};
std::atomic<int> m_last_connack_rc{-1};
uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence
bool initial_result{false};
bool initial_completed{false};
std::atomic_bool connected{false};
};
} // namespace Slic3r
#endif // slic3r_OrcaMqttConnection_hpp_
File diff suppressed because it is too large Load Diff
+137 -14
View File
@@ -3,19 +3,28 @@
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "ICloudServiceAgent.hpp" #include "ICloudServiceAgent.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "OrcaMqttConnection.hpp"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string> #include <string>
#include <mutex> #include <mutex>
#include <memory> #include <memory>
#include <thread>
namespace Slic3r { namespace Slic3r {
class OrcaCloudServiceAgent;
/** /**
* OrcaPrinterAgent - Stub implementation for printer operations. * OrcaPrinterAgent - OrcaSonar MQTT printer agent.
* *
* All printer-related operations are currently stubs that return success. * LAN and cloud commands use the same OrcaSonar protocol payloads; only the
* Actual printer connectivity requires the BBL SDK or future Orca implementation. * MQTT connection selected by route_send() differs.
*/ */
class OrcaPrinterAgent : public IPrinterAgent { class OrcaPrinterAgent : public IPrinterAgent
{
public: public:
explicit OrcaPrinterAgent(std::string log_dir); explicit OrcaPrinterAgent(std::string log_dir);
~OrcaPrinterAgent() override; ~OrcaPrinterAgent() override;
@@ -25,27 +34,20 @@ public:
// ======================================================================== // ========================================================================
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override; void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override;
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
// Communication // Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; int connect_printer(const PrinterConnectionParams& params) override;
int disconnect_printer() override; int disconnect_printer() override;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
// Certificates
int check_cert() override;
void install_device_cert(std::string dev_id, bool lan_only) override;
// Discovery // Discovery
bool start_discovery(bool start, bool sending) override; bool start_discovery(bool start, bool sending) override;
// Binding // Binding
int ping_bind(std::string ping_code) override;
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override;
int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override;
int unbind(std::string dev_id) override;
int request_bind_ticket(std::string* ticket) override;
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override;
int set_server_callback(OnServerErrFn fn) override; int set_server_callback(OnServerErrFn fn) override;
// Machine Selection // Machine Selection
@@ -77,10 +79,131 @@ public:
int set_on_local_message_fn(OnMessageFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override;
int command_ams_refresh_rfid(std::string dev_id, int ams_id, int tray_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_start_camera(std::string dev_id) override;
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override;
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override;
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override;
int command_axis_control(std::string dev_id,
std::string axis,
double unit,
double input_val,
int speed,
bool is_core_xy,
bool supports_mqtt_axis_control,
int sequence_id,
bool lan_mode) override;
// Test-only: drive emit_connect_sequence directly (no socket).
void run_connect_sequence_for_test(const std::string& dev_id)
{
emit_connect_sequence(dev_id, [](const std::string&) {}, [](const std::string&) {});
}
// Test-only: advance the LAN connection epoch without a connect/disconnect cycle.
void bump_lan_generation_for_test() { ++m_lan_generation; }
// Test-only: the same for the (independent) cloud selection epoch.
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::subscription; }
protected:
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
// thread via queue_on_main_fn when set). Body of every connection's MessageHandler.
void deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local);
// Extract OrcaSonar's print.ipcam.stream_mode from LAN reports before they
// are forwarded to the GUI. The getters below then read this agent-owned state.
void parse_ipcam_info(const std::string& dev_id, const std::string& payload);
// Orca-dialect -> Bambu-dialect compatibility shim for inbound reports: the single
// place Orca Protocol JSON is rewritten into the shapes MachineObject::parse_json
// already handles, so parse_json needs no Orca-specific changes. Self-contained
// (its cache is a function-local static) and deletable together with its call site
// once parse_json reads the Orca dialect natively. See the definition for the
// per-rule detail. Returns the payload unchanged when no rule applies.
std::string merge_capabilities(const std::string& dev_id, const std::string& payload);
// Report the asynchronous LAN connection state using the same callback contract as
// the other printer agents. The transport result cannot be returned by
// connect_printer(), which only starts the worker.
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& message);
// The LAN inbound-message handler for one connection generation: forwards to
// deliver_to_sink only while `generation` is still the live epoch.
std::function<void(const std::string&, const std::string&)> make_lan_message_handler(uint64_t generation);
// Pure LAN-address parsing + client-id. protected static so the test Probe reaches them.
static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port);
static std::string make_lan_client_id(const std::string& dev_id);
// Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none).
std::string lan_connection_target() const;
// Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall,
// info.get_version, info.get_capabilities. Runs identically on LAN and cloud.
void on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation);
// The post-connect command sequence, factored behind a seam so a test can
// observe the SUBSCRIBE + 4 request payloads without a live OrcaMqttConnection.
virtual void emit_connect_sequence(const std::string& dev_id,
std::function<void(const std::string&)> subscribe,
std::function<void(const std::string&)> request);
static std::string seq(int n); // decimal string in the OrcaSlicer 20000..29999 band
static std::string build_pushing_start(const std::string& sequence_id);
static std::string build_pushing_stop(const std::string& sequence_id);
static std::string build_pushall(const std::string& sequence_id);
static std::string build_get_version(const std::string& sequence_id);
static std::string build_get_capabilities(const std::string& sequence_id);
private: private:
class OrcaSonarDiscovery;
std::string log_dir; std::string log_dir;
std::string selected_machine; std::string selected_machine;
enum CurrentConn { NONE, CLOUD, LAN };
static const char* connection_type_name(CurrentConn connection);
// The transport for the printer currently selected by the UI. LAN and
// cloud sessions have separate connection objects, so this is selection
// state rather than an inference from whichever socket happens to exist.
CurrentConn m_current_connection = NONE;
std::shared_ptr<ICloudServiceAgent> m_cloud_agent; std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
std::unique_ptr<OrcaMqttConnection> lan_mqtt_connection;
// Two independent epochs: a cloud (de)selection must not fence the live LAN
// feed, and vice versa. Each transport's connect thread and inbound handler
// compare against their own counter only.
std::atomic<uint64_t> m_lan_generation{0};
std::atomic<uint64_t> m_cloud_generation{0};
// The short-lived threads that run the blocking initial connect for the current
// LAN / cloud session. Joined members (never detached) so they cannot outlive
// *this or the connection they hold a raw pointer to.
std::thread m_lan_connect_thread;
std::thread m_cloud_connect_thread;
std::unique_ptr<OrcaSonarDiscovery> m_discovery;
std::string m_lan_dev_id; // guarded by state_mutex
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
bool m_lan_use_ssl = false; // guarded by state_mutex
std::string m_lan_ca_file; // guarded by state_mutex
CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex
std::string m_camera_url; // guarded by state_mutex
OrcaCloudServiceAgent* get_orca_cloud_agent();
OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true);
static bool parse_nonnegative_command_id(const std::string& value, int& result);
// Route one command payload to device/<dev_id>/request on the LAN or the shared
// cloud connection. The uniform send path for both send_message* overrides.
int route_send(bool is_lan, const std::string& dev_id, const std::string& json_str);
// Callbacks // Callbacks
OnMsgArrivedFn on_ssdp_msg_fn; OnMsgArrivedFn on_ssdp_msg_fn;
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <string>
namespace Slic3r {
enum LiveviewLocal {
LVL_None,
LVL_Disable,
LVL_Local,
LVL_Rtsps,
LVL_Rtsp
};
enum LiveviewRemote {
LVR_None,
LVR_Tutk,
LVR_Agora,
LVR_TutkAgora
};
enum FileLocal {
FL_None,
FL_Local
};
enum FileRemote {
FR_None,
FR_Tutk,
FR_Agora,
FR_TutkAgora
};
} // namespace Slic3r
+176 -52
View File
@@ -1,5 +1,6 @@
#include "QidiPrinterAgent.hpp" #include "QidiPrinterAgent.hpp"
#include "Http.hpp" #include "Http.hpp"
#include "IPrinterAgent.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
@@ -8,6 +9,7 @@
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <cctype> #include <cctype>
#include <sstream> #include <sstream>
#include <thread>
using json = nlohmann::json; using json = nlohmann::json;
@@ -27,6 +29,15 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin
return false; return false;
} }
// RAII decrement for MoonrakerPrinterAgent::filament_fetch_in_flight — guarantees the
// counter drops back down on every exit path (early return or fall-through) inside the
// detached fetch thread below, so ~MoonrakerPrinterAgent()'s wait loop can't stall forever.
struct InFlightGuard
{
std::atomic<int>& counter;
~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); }
};
} // anonymous namespace } // anonymous namespace
const std::string QidiPrinterAgent_VERSION = "0.0.1"; const std::string QidiPrinterAgent_VERSION = "0.0.1";
@@ -40,51 +51,148 @@ AgentInfo QidiPrinterAgent::get_agent_info_static()
return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"}; return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"};
} }
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) FilamentSyncMode QidiPrinterAgent::get_filament_sync_mode() const
{ {
std::string error; if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
return FilamentSyncMode::subscription;
return FilamentSyncMode::pull;
}
// 1. Fetch device info and infer series_id bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
std::string series_id; {
{ if (sync_mode != get_filament_sync_mode())
MoonrakerDeviceInfo info;
if (fetch_device_info(device_info.base_url, device_info.api_key, info, error)) {
series_id = infer_series_id(info.model_id, info.dev_name);
}
}
if (series_id.empty()) {
// Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier.
series_id = infer_series_id(device_info.model_id, device_info.model_name);
}
// 2. Fetch filament dictionary
QidiFilamentDict dict;
if (!fetch_filament_dict(device_info.base_url, device_info.api_key, dict, error)) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error;
}
// 3. Fetch slot info and build AmsTrayData directly
std::vector<AmsTrayData> trays;
int box_count = 0;
if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error;
return false; return false;
}
// 4. Build the AMS payload // Snapshot only what the fetch needs, rather than reading device_info live from the
build_ams_payload(box_count, box_count * 4 - 1, trays); // background thread below — device_info can be concurrently rewritten by a reconnect
// on another thread while this fetch is still in flight.
ConnectionSettings connection = get_connection_settings();
std::string model_id = device_info.model_id;
std::string model_name = device_info.model_name;
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
std::thread([this, connection = std::move(connection), model_id, model_name]() mutable {
InFlightGuard guard{filament_fetch_in_flight};
std::string error;
// 1. Fetch device info and infer series_id
std::string series_id;
{
MoonrakerDeviceInfo info;
if (fetch_device_info(connection, info, error)) {
series_id = infer_series_id(info.model_id, info.dev_name);
}
}
if (series_id.empty()) {
// Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier.
series_id = infer_series_id(model_id, model_name);
}
// 2. Fetch filament dictionary
QidiFilamentDict dict;
if (!fetch_filament_dict(connection, dict, error)) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error;
}
// 3. Fetch slot info and build AmsTrayData directly
std::vector<AmsTrayData> trays;
int box_count = 0;
if (!fetch_slot_info(connection, dict, series_id, trays, box_count, error)) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error;
return;
}
// 4. Build the AMS payload
build_ams_payload(box_count, box_count * 4 - 1, trays);
}).detach();
return true; return true;
} }
bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const
const std::string& api_key, {
// enable_box mirrors task_use_ams: engage the multi-color box only when this
// job actually routes filament through it. (See qidi-ams-findings.md §2/§8.3 —
// if firmware treats enable_box as "a box exists" rather than "use it this job",
// switch this gate to HasAms()/box_count instead.)
const int enable = params.task_use_ams ? 1 : 0;
if (!send_gcode(device_info.dev_id, "SAVE_VARIABLE VARIABLE=enable_box VALUE=" + std::to_string(enable))) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set enable_box";
return false;
}
// When the box isn't used this job, leave the existing value_t<tool> slot
// assignments untouched (enable_box=0 is enough to disengage it).
if (!enable)
return true;
if (params.ams_mapping.empty()) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::apply_box_mapping: enable_box set but ams_mapping is empty";
return true;
}
// ams_mapping (v0) is a JSON array indexed by filament/tool; each value is the
// physical box slot (-1 = unmapped). Mirror it onto the printer's value_t<tool>
// variables: SAVE_VARIABLE VARIABLE=value_t<tool> VALUE='slot<n>'.
auto mapping = nlohmann::json::parse(params.ams_mapping, nullptr, /*allow_exceptions*/ false);
if (mapping.is_discarded() || !mapping.is_array()) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: invalid ams_mapping: " << params.ams_mapping;
return false;
}
for (size_t tool = 0; tool < mapping.size(); ++tool) {
if (!mapping[tool].is_number_integer())
continue;
const int slot = mapping[tool].get<int>();
if (slot < 0)
continue; // unmapped filament — skip
const std::string gcode = "SAVE_VARIABLE VARIABLE=value_t" + std::to_string(tool) +
" VALUE=\"'slot" + std::to_string(slot) + "'\"";
if (!send_gcode(device_info.dev_id, gcode)) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set value_t" << tool;
return false;
}
}
return true;
}
int QidiPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
{
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
return MoonrakerPrinterAgent::start_local_print(std::move(params), update_fn, cancel_fn);
}
int QidiPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
{
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
return MoonrakerPrinterAgent::start_print(std::move(params), update_fn, cancel_fn, wait_fn);
}
int QidiPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
{
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_WR_UPLOAD_FTP_FAILED;
return MoonrakerPrinterAgent::start_local_print_with_record(std::move(params), update_fn, cancel_fn, wait_fn);
}
int QidiPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
{
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
return MoonrakerPrinterAgent::start_sdcard_print(std::move(params), update_fn, cancel_fn);
}
bool QidiPrinterAgent::fetch_slot_info(const ConnectionSettings& connection,
const QidiFilamentDict& dict, const QidiFilamentDict& dict,
const std::string& series_id, const std::string& series_id,
std::vector<AmsTrayData>& trays, std::vector<AmsTrayData>& trays,
int& box_count, int& box_count,
std::string& error) std::string& error)
{ {
std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables"); std::string url = join_url(connection.base_url, "/printer/objects/query?save_variables=variables");
for (int i = 0; i < 16; ++i) { for (int i = 0; i < 16; ++i) {
url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button"; url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button";
} }
@@ -94,8 +202,9 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
std::string http_error; std::string http_error;
auto http = Http::get(url); auto http = Http::get(url);
if (!api_key.empty()) { configure_http(http, connection);
http.header("X-Api-Key", api_key); if (!connection.api_key.empty()) {
http.header("X-Api-Key", connection.api_key);
} }
http.timeout_connect(5) http.timeout_connect(5)
.timeout_max(10) .timeout_max(10)
@@ -120,20 +229,10 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
return false; return false;
} }
auto json = nlohmann::json::parse(response_body, nullptr, false, true); nlohmann::json status;
if (json.is_discarded()) { nlohmann::json variables;
error = "Invalid JSON response"; if (!parse_slot_response(response_body, status, variables, error))
return false; return false;
}
if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("save_variables") ||
!json["result"]["status"]["save_variables"].contains("variables")) {
error = "Unexpected JSON structure";
return false;
}
auto& variables = json["result"]["status"]["save_variables"]["variables"];
auto& status = json["result"]["status"];
box_count = variables.value("box_count", 1); box_count = variables.value("box_count", 1);
if (box_count < 0) { if (box_count < 0) {
@@ -208,20 +307,45 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
return true; return true;
} }
bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, bool QidiPrinterAgent::parse_slot_response(const std::string& response_body,
const std::string& api_key, nlohmann::json& status,
nlohmann::json& variables,
std::string& error)
{
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
if (json.is_discarded()) {
error = "Invalid JSON response";
return false;
}
if (!json.is_object() || !json.contains("result") || !json["result"].is_object() || !json["result"].contains("status") ||
!json["result"]["status"].is_object() || !json["result"]["status"].contains("save_variables") ||
!json["result"]["status"]["save_variables"].is_object() || !json["result"]["status"]["save_variables"].contains("variables") ||
!json["result"]["status"]["save_variables"]["variables"].is_object()) {
// why: Qidi firmware may send null here, but json::value() throws for it.
error = "Unexpected JSON structure: save_variables.variables must be an object";
return false;
}
status = json["result"]["status"];
variables = status["save_variables"]["variables"];
return true;
}
bool QidiPrinterAgent::fetch_filament_dict(const ConnectionSettings& connection,
QidiFilamentDict& dict, QidiFilamentDict& dict,
std::string& error) const std::string& error) const
{ {
std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg"); std::string url = join_url(connection.base_url, "/server/files/config/officiall_filas_list.cfg");
std::string response_body; std::string response_body;
bool success = false; bool success = false;
std::string http_error; std::string http_error;
auto http = Http::get(url); auto http = Http::get(url);
if (!api_key.empty()) { configure_http(http, connection);
http.header("X-Api-Key", api_key); if (!connection.api_key.empty()) {
http.header("X-Api-Key", connection.api_key);
} }
http.timeout_connect(5) http.timeout_connect(5)
.timeout_max(10) .timeout_max(10)
+21 -4
View File
@@ -1,7 +1,9 @@
#ifndef __QIDI_PRINTER_AGENT_HPP__ #ifndef __QIDI_PRINTER_AGENT_HPP__
#define __QIDI_PRINTER_AGENT_HPP__ #define __QIDI_PRINTER_AGENT_HPP__
#include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include "nlohmann/json_fwd.hpp"
#include <map> #include <map>
#include <string> #include <string>
@@ -19,9 +21,25 @@ public:
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
// Override filament sync (Qidi-specific implementation) // Override filament sync (Qidi-specific implementation)
bool fetch_filament_info(std::string dev_id) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
static bool parse_slot_response(const std::string& response_body,
nlohmann::json& status,
nlohmann::json& variables,
std::string& error);
// Print operations — emit QiDi multi-color box config, then delegate to base.
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
FilamentSyncMode get_filament_sync_mode() const override;
private: private:
// Push enable_box + value_t<tool> SAVE_VARIABLEs before a print starts.
// Returns false if any command fails (caller should abort the print).
bool apply_box_mapping(const PrintParams& params) const;
struct QidiFilamentDict struct QidiFilamentDict
{ {
std::map<int, std::string> colors; std::map<int, std::string> colors;
@@ -29,14 +47,13 @@ private:
}; };
// Qidi-specific methods // Qidi-specific methods
bool fetch_slot_info(const std::string& base_url, bool fetch_slot_info(const ConnectionSettings& connection,
const std::string& api_key,
const QidiFilamentDict& dict, const QidiFilamentDict& dict,
const std::string& series_id, const std::string& series_id,
std::vector<AmsTrayData>& trays, std::vector<AmsTrayData>& trays,
int& box_count, int& box_count,
std::string& error); std::string& error);
bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; bool fetch_filament_dict(const ConnectionSettings& connection, QidiFilamentDict& dict, std::string& error) const;
std::string normalize_filament_type(const std::string& filament_type); std::string normalize_filament_type(const std::string& filament_type);
std::string infer_series_id(const std::string& model_id, const std::string& dev_name); std::string infer_series_id(const std::string& model_id, const std::string& dev_name);
std::string normalize_model_key(std::string value); std::string normalize_model_key(std::string value);
+164 -106
View File
@@ -1,10 +1,14 @@
#include "SnapmakerPrinterAgent.hpp" #include "SnapmakerPrinterAgent.hpp"
#include "Http.hpp" #include "Http.hpp"
#include "IPrinterAgent.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <chrono>
#include <sstream>
#include <thread>
using json = nlohmann::json; using json = nlohmann::json;
@@ -13,6 +17,13 @@ namespace Slic3r {
namespace { namespace {
constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1"; constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1";
constexpr int64_t CAMERA_REFRESH_INTERVAL_MS = 300'000;
int64_t now_ms()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
// Safely access a parallel array by index, returning a fallback if out of bounds. // Safely access a parallel array by index, returning a fallback if out of bounds.
template<typename T> template<typename T>
@@ -69,6 +80,31 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection&
SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
void SnapmakerPrinterAgent::start_camera_monitor()
{
enqueue_command([this] {
send_ws_rpc("camera.start_monitor",
{{"domain", "lan"}, {"interval", 0}, {"expect_pw", false}});
});
m_camera_last_fire_ms.store(now_ms());
}
void SnapmakerPrinterAgent::on_status_loop_tick(const std::string& dev_id)
{
(void) dev_id;
const int64_t last = m_camera_last_fire_ms.load();
if (last == 0 || now_ms() - last >= CAMERA_REFRESH_INTERVAL_MS) {
start_camera_monitor();
}
}
int SnapmakerPrinterAgent::command_start_camera(std::string dev_id)
{
(void) dev_id;
start_camera_monitor();
return BAMBU_NETWORK_SUCCESS;
}
AgentInfo SnapmakerPrinterAgent::get_agent_info_static() AgentInfo SnapmakerPrinterAgent::get_agent_info_static()
{ {
return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"};
@@ -104,127 +140,149 @@ std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type
return base; return base;
} }
bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id) bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect"); (void) dev_id;
if (sync_mode != get_filament_sync_mode())
std::string response_body;
bool success = false;
std::string http_error;
auto http = Http::get(url);
if (!device_info.api_key.empty()) {
http.header("X-Api-Key", device_info.api_key);
}
http.timeout_connect(5)
.timeout_max(10)
.on_complete([&](std::string body, unsigned status) {
if (status == 200) {
response_body = body;
success = true;
} else {
http_error = "HTTP error: " + std::to_string(status);
}
})
.on_error([&](std::string body, std::string err, unsigned status) {
http_error = err;
if (status > 0) {
http_error += " (HTTP " + std::to_string(status) + ")";
}
})
.perform_sync();
if (!success) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error;
return false; return false;
}
auto json = nlohmann::json::parse(response_body, nullptr, false, true); const std::string base_url = device_info.base_url;
if (json.is_discarded()) { const std::string api_key = device_info.api_key;
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response";
return false;
}
// Navigate to result.status.print_task_config filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
if (!json.contains("result") || !json["result"].contains("status") ||
!json["result"]["status"].contains("print_task_config")) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response";
return false;
}
auto& ptc = json["result"]["status"]["print_task_config"]; std::thread([this, base_url, api_key]() {
struct InFlightGuard
{
std::atomic<int>& counter;
~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); }
} guard{filament_fetch_in_flight};
// Read parallel arrays from print_task_config const std::string url = join_url(base_url, "/printer/objects/query?print_task_config&filament_detect");
auto filament_exist = ptc.value("filament_exist", std::vector<bool>{});
auto filament_type = ptc.value("filament_type", std::vector<std::string>{});
auto filament_sub_type = ptc.value("filament_sub_type", std::vector<std::string>{});
auto filament_color = ptc.value("filament_color_rgba", std::vector<std::string>{});
auto filament_vendor = ptc.value("filament_vendor", std::vector<std::string>{});
const int slot_count = static_cast<int>(filament_exist.size()); std::string response_body;
if (slot_count == 0) { bool success = false;
BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported"; std::string http_error;
return false;
}
// Read NFC filament_detect data for temperature info (optional) auto http = Http::get(url);
nlohmann::json nfc_info; if (!api_key.empty()) {
if (json["result"]["status"].contains("filament_detect") && http.header("X-Api-Key", api_key);
json["result"]["status"]["filament_detect"].contains("info")) { }
nfc_info = json["result"]["status"]["filament_detect"]["info"]; http.timeout_connect(5)
} .timeout_max(10)
.on_complete([&](std::string body, unsigned status) {
static const std::string empty_str; if (status == 200) {
static const std::string default_color = "FFFFFFFF"; response_body = body;
success = true;
std::vector<AmsTrayData> trays;
trays.reserve(slot_count);
for (int i = 0; i < slot_count; ++i) {
AmsTrayData tray;
tray.slot_index = i;
tray.has_filament = filament_exist[i];
if (tray.has_filament) {
tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str),
safe_at(filament_sub_type, i, empty_str));
tray.tray_color = safe_at(filament_color, i, default_color);
auto* bundle = GUI::wxGetApp().preset_bundle;
// Try to find a matching preset for this filament based on vendor, type and color.
// If not found, default to traditional search by type only or generic type mapping.
if (bundle) {
std::string vendor = safe_at(filament_vendor, i, empty_str);
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type,
tray.tray_color);
if (!filament_id.empty()) {
tray.tray_info_idx = filament_id;
BOOST_LOG_TRIVIAL(warning) << "Filament sync: Found manufacturer-specific profile for slot " << i << ": "
<< filament_id;
} else { } else {
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); http_error = "HTTP error: " + std::to_string(status);
} }
} else { })
tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); .on_error([&](std::string body, std::string err, unsigned status) {
} http_error = err;
if (status > 0) {
http_error += " (HTTP " + std::to_string(status) + ")";
}
})
.perform_sync();
// Extract NFC temperature data if available if (!success) {
if (nfc_info.is_array() && i < static_cast<int>(nfc_info.size()) && nfc_info[i].is_object()) { BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error;
auto& nfc_slot = nfc_info[i]; return;
std::string vendor = nfc_slot.value("VENDOR", "NONE");
if (vendor != "NONE" && !vendor.empty()) {
tray.bed_temp = nfc_slot.value("BED_TEMP", 0);
tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0);
}
}
} }
trays.emplace_back(std::move(tray)); auto json = nlohmann::json::parse(response_body, nullptr, false, true);
} if (json.is_discarded()) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response";
return;
}
// Navigate to result.status.print_task_config
if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("print_task_config")) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response";
return;
}
auto& ptc = json["result"]["status"]["print_task_config"];
// Read parallel arrays from print_task_config
auto filament_exist = ptc.value("filament_exist", std::vector<bool>{});
auto filament_type = ptc.value("filament_type", std::vector<std::string>{});
auto filament_sub_type = ptc.value("filament_sub_type", std::vector<std::string>{});
auto filament_color = ptc.value("filament_color_rgba", std::vector<std::string>{});
auto filament_vendor = ptc.value("filament_vendor", std::vector<std::string>{});
const int slot_count = static_cast<int>(filament_exist.size());
if (slot_count == 0) {
BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported";
return;
}
// Read NFC filament_detect data for temperature info (optional)
nlohmann::json nfc_info;
if (json["result"]["status"].contains("filament_detect") && json["result"]["status"]["filament_detect"].contains("info")) {
nfc_info = json["result"]["status"]["filament_detect"]["info"];
}
static const std::string empty_str;
static const std::string default_color = "FFFFFFFF";
std::vector<AmsTrayData> trays;
trays.reserve(slot_count);
for (int i = 0; i < slot_count; ++i) {
AmsTrayData tray;
tray.slot_index = i;
tray.has_filament = filament_exist[i];
if (tray.has_filament) {
tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), safe_at(filament_sub_type, i, empty_str));
tray.tray_color = safe_at(filament_color, i, default_color);
auto* bundle = GUI::wxGetApp().preset_bundle;
// Try to find a matching preset for this filament based on vendor, type and color.
// If not found, default to traditional search by type only or generic type mapping.
if (bundle) {
std::string vendor = safe_at(filament_vendor, i, empty_str);
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type,
tray.tray_color);
if (!filament_id.empty()) {
tray.tray_info_idx = filament_id;
BOOST_LOG_TRIVIAL(warning)
<< "Filament sync: Found manufacturer-specific profile for slot " << i << ": " << filament_id;
} else {
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
}
} else {
tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type);
}
// Extract NFC temperature data if available
if (nfc_info.is_array() && i < static_cast<int>(nfc_info.size()) && nfc_info[i].is_object()) {
auto& nfc_slot = nfc_info[i];
std::string vendor = nfc_slot.value("VENDOR", "NONE");
if (vendor != "NONE" && !vendor.empty()) {
tray.bed_temp = nfc_slot.value("BED_TEMP", 0);
tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0);
}
}
}
trays.emplace_back(std::move(tray));
}
build_ams_payload(1, slot_count - 1, trays);
}).detach();
build_ams_payload(1, slot_count - 1, trays);
return true; return true;
} }
FilamentSyncMode SnapmakerPrinterAgent::get_filament_sync_mode() const
{
if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
return FilamentSyncMode::subscription;
return FilamentSyncMode::pull;
}
} // namespace Slic3r } // namespace Slic3r
+13 -1
View File
@@ -1,7 +1,10 @@
#pragma once #pragma once
#include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include <atomic>
#include <cstdint>
#include <string> #include <string>
namespace Slic3r { namespace Slic3r {
@@ -15,11 +18,20 @@ public:
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
bool fetch_filament_info(std::string dev_id) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
FilamentSyncMode get_filament_sync_mode() const override;
int command_start_camera(std::string dev_id) override;
CameraStreamMode get_camera_stream_mode() const override { return CameraStreamMode::http_snapshot; }
std::string get_camera_url() const override { return device_info.base_url + "/server/files/camera/monitor.jpg"; }
private: private:
// Combine filament_type + filament_sub_type into a unified type string // Combine filament_type + filament_sub_type into a unified type string
static std::string combine_filament_type(const std::string& type, const std::string& sub_type); static std::string combine_filament_type(const std::string& type, const std::string& sub_type);
void start_camera_monitor();
void on_status_loop_tick(const std::string& dev_id) override;
std::atomic<int64_t> m_camera_last_fire_ms{0};
}; };
} // namespace Slic3r } // namespace Slic3r
@@ -23,6 +23,14 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
.value("Pull", FilamentSyncMode::pull) .value("Pull", FilamentSyncMode::pull)
.export_values(); .export_values();
py::enum_<CameraStreamMode>(printer_agent_module, "CameraStreamMode")
.value("None_", CameraStreamMode::none)
.value("HTTP", CameraStreamMode::http)
.value("HTTPS", CameraStreamMode::https)
.value("HTTP_SNAPSHOT", CameraStreamMode::http_snapshot)
.value("RTSP", CameraStreamMode::rtsp)
.export_values();
py::class_<AgentInfo>(printer_agent_module, "AgentInfo") py::class_<AgentInfo>(printer_agent_module, "AgentInfo")
.def(py::init<>()) .def(py::init<>())
.def(py::init([](std::string id, std::string name, std::string version, std::string description) { .def(py::init([](std::string id, std::string name, std::string version, std::string description) {
@@ -89,6 +97,16 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
.def_readwrite("task_ext_change_assist", &PrintParams::task_ext_change_assist) .def_readwrite("task_ext_change_assist", &PrintParams::task_ext_change_assist)
.def_readwrite("try_emmc_print", &PrintParams::try_emmc_print); .def_readwrite("try_emmc_print", &PrintParams::try_emmc_print);
py::class_<PrinterConnectionParams>(printer_agent_module, "PrinterConnectionParams")
.def(py::init<>())
.def_readwrite("dev_id", &PrinterConnectionParams::dev_id)
.def_readwrite("host", &PrinterConnectionParams::host)
.def_readwrite("port", &PrinterConnectionParams::port)
.def_readwrite("username", &PrinterConnectionParams::username)
.def_readwrite("password", &PrinterConnectionParams::password)
.def_readwrite("use_ssl", &PrinterConnectionParams::use_ssl)
.def_readwrite("ca_file", &PrinterConnectionParams::ca_file);
py::class_<PrinterAgentPluginCapability, PluginCapabilityInterface, PyPrinterAgentPluginCapabilityTrampoline, std::shared_ptr<PrinterAgentPluginCapability>>( py::class_<PrinterAgentPluginCapability, PluginCapabilityInterface, PyPrinterAgentPluginCapabilityTrampoline, std::shared_ptr<PrinterAgentPluginCapability>>(
printer_agent_module, "PrinterAgentBase") printer_agent_module, "PrinterAgentBase")
.def(py::init<>()) .def(py::init<>())
@@ -98,6 +116,16 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
.def("disconnect_printer", &PrinterAgentPluginCapability::disconnect_printer) .def("disconnect_printer", &PrinterAgentPluginCapability::disconnect_printer)
.def("send_message", &PrinterAgentPluginCapability::send_message) .def("send_message", &PrinterAgentPluginCapability::send_message)
.def("send_message_to_printer", &PrinterAgentPluginCapability::send_message_to_printer) .def("send_message_to_printer", &PrinterAgentPluginCapability::send_message_to_printer)
.def("command_ams_refresh_rfid", &PrinterAgentPluginCapability::command_ams_refresh_rfid)
.def("command_ams_calibrate", &PrinterAgentPluginCapability::command_ams_calibrate)
.def("command_ams_select_tray", &PrinterAgentPluginCapability::command_ams_select_tray)
.def("command_start_camera", &PrinterAgentPluginCapability::command_start_camera)
.def("command_xyz_abs", &PrinterAgentPluginCapability::command_xyz_abs)
.def("command_auto_leveling", &PrinterAgentPluginCapability::command_auto_leveling)
.def("command_go_home", &PrinterAgentPluginCapability::command_go_home)
.def("command_set_bed", &PrinterAgentPluginCapability::command_set_bed)
.def("command_set_nozzle", &PrinterAgentPluginCapability::command_set_nozzle)
.def("command_axis_control", &PrinterAgentPluginCapability::command_axis_control)
.def("start_discovery", &PrinterAgentPluginCapability::start_discovery) .def("start_discovery", &PrinterAgentPluginCapability::start_discovery)
.def("bind_detect", &PrinterAgentPluginCapability::bind_detect) .def("bind_detect", &PrinterAgentPluginCapability::bind_detect)
.def("get_user_selected_machine", &PrinterAgentPluginCapability::get_user_selected_machine) .def("get_user_selected_machine", &PrinterAgentPluginCapability::get_user_selected_machine)
@@ -105,7 +133,9 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
.def("start_send_gcode_to_sdcard", &PrinterAgentPluginCapability::start_send_gcode_to_sdcard) .def("start_send_gcode_to_sdcard", &PrinterAgentPluginCapability::start_send_gcode_to_sdcard)
.def("start_local_print", &PrinterAgentPluginCapability::start_local_print) .def("start_local_print", &PrinterAgentPluginCapability::start_local_print)
.def("get_filament_sync_mode", &PrinterAgentPluginCapability::get_filament_sync_mode) .def("get_filament_sync_mode", &PrinterAgentPluginCapability::get_filament_sync_mode)
.def("get_camera_stream_mode", &PrinterAgentPluginCapability::get_camera_stream_mode)
.def("fetch_filament_info", &PrinterAgentPluginCapability::fetch_filament_info) .def("fetch_filament_info", &PrinterAgentPluginCapability::fetch_filament_info)
.def("get_camera_url", &PrinterAgentPluginCapability::get_camera_url)
.def("check_cert", &PrinterAgentPluginCapability::check_cert) .def("check_cert", &PrinterAgentPluginCapability::check_cert)
.def("install_device_cert", &PrinterAgentPluginCapability::install_device_cert) .def("install_device_cert", &PrinterAgentPluginCapability::install_device_cert)
.def("ping_bind", &PrinterAgentPluginCapability::ping_bind) .def("ping_bind", &PrinterAgentPluginCapability::ping_bind)
@@ -5,7 +5,6 @@
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -24,60 +23,39 @@ public:
PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; } PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; }
// set_cloud_agent is the host-managed dependency injection point — the host hands the // set_cloud_agent is the host-managed dependency injection point — the host hands the
// capability its ICloudServiceAgent — so it is the one operation kept native here. Every // capability its ICloudServiceAgent — so it is the one operation kept native here. Optional
// other IPrinterAgent operation is pure: the Python plugin must implement all of them. // operations inherit IPrinterAgent's defaults; the remaining operations are plugin-defined.
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) final override { (void) cloud; } void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) final override { (void) cloud; }
AgentInfo get_agent_info() override = 0; AgentInfo get_agent_info() override = 0;
int connect_printer( int connect_printer(const PrinterConnectionParams& params) override = 0;
std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override = 0; int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0; bool start_discovery(bool start, bool sending) override = 0;
bool start_discovery(bool start, bool sending) override = 0; int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0;
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0; std::string get_user_selected_machine() override = 0;
std::string get_user_selected_machine() override = 0; int set_user_selected_machine(std::string dev_id) override = 0;
int set_user_selected_machine(std::string dev_id) override = 0; int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0;
int start_send_gcode_to_sdcard(PrintParams params, int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn,
OnWaitFn wait_fn) override = 0;
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
FilamentSyncMode get_filament_sync_mode() const override = 0;
bool fetch_filament_info(std::string dev_id) override = 0;
int check_cert() override = 0; int set_server_callback(OnServerErrFn fn) override = 0;
void install_device_cert(std::string dev_id, bool lan_only) override = 0;
int ping_bind(std::string ping_code) override = 0;
int bind(std::string dev_ip,
std::string dev_id,
std::string dev_model,
std::string sec_link,
std::string timezone,
bool improved,
OnUpdateStatusFn update_fn) override = 0;
int unbind(std::string dev_id) override = 0;
// request_bind_ticket has a std::string* out-param that cannot round-trip through a
// pybind11 override directly; the trampoline wraps it (the Python plugin returns a
// (result, ticket) tuple), so it stays pure here like the rest.
int request_bind_ticket(std::string* ticket) override = 0;
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override = 0;
int set_server_callback(OnServerErrFn fn) override = 0;
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0; int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0;
int start_local_print_with_record(PrintParams params, int start_local_print_with_record(PrintParams params,
OnUpdateStatusFn update_fn, OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn, WasCancelledFn cancel_fn,
OnWaitFn wait_fn) override = 0; OnWaitFn wait_fn) override = 0;
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0; int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override = 0;
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override = 0;
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override = 0;
int set_on_message_fn(OnMessageFn fn) override = 0;
int set_on_user_message_fn(OnMessageFn fn) override = 0;
int set_on_local_connect_fn(OnLocalConnectedFn fn) override = 0;
int set_on_local_message_fn(OnMessageFn fn) override = 0;
int set_queue_on_main_fn(QueueOnMainFn fn) override = 0;
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override = 0;
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override = 0;
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override = 0;
int set_on_message_fn(OnMessageFn fn) override = 0;
int set_on_user_message_fn(OnMessageFn fn) override = 0;
int set_on_local_connect_fn(OnLocalConnectedFn fn) override = 0;
int set_on_local_message_fn(OnMessageFn fn) override = 0;
int set_queue_on_main_fn(QueueOnMainFn fn) override = 0;
}; };
} // namespace Slic3r } // namespace Slic3r
@@ -5,7 +5,10 @@
#include "../../PyPluginTrampoline.hpp" #include "../../PyPluginTrampoline.hpp"
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "pybind11/pybind11.h"
#include <slic3r/plugin/PluginAuditManager.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
#include <type_traits> #include <type_traits>
@@ -22,6 +25,12 @@
} ORCA_PY_AGENT_CATCH(name) \ } ORCA_PY_AGENT_CATCH(name) \
return printer_agent_failure<ret>() return printer_agent_failure<ret>()
#define ORCA_PY_AGENT_OVERRIDE_DEFAULT(ret, name, ...) \
try { \
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, ret, PrinterAgentPluginCapability, name, ##__VA_ARGS__); \
} ORCA_PY_AGENT_CATCH(name) \
return printer_agent_failure<ret>()
namespace Slic3r { namespace Slic3r {
// NetworkAgent's no-agent answer: -1 for a status code, the empty value (false, "", none) otherwise. // NetworkAgent's no-agent answer: -1 for a status code, the empty value (false, "", none) otherwise.
template<typename T> T printer_agent_failure() template<typename T> T printer_agent_failure()
@@ -42,9 +51,9 @@ public:
ORCA_PY_AGENT_OVERRIDE(AgentInfo, get_agent_info); ORCA_PY_AGENT_OVERRIDE(AgentInfo, get_agent_info);
} }
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override int connect_printer(const PrinterConnectionParams& params) override
{ {
ORCA_PY_AGENT_OVERRIDE(int, connect_printer, dev_id, dev_ip, username, password, use_ssl); ORCA_PY_AGENT_OVERRIDE(int, connect_printer, params);
} }
int disconnect_printer() override int disconnect_printer() override
@@ -62,6 +71,61 @@ public:
ORCA_PY_AGENT_OVERRIDE(int, send_message_to_printer, dev_id, json_str, qos, flag); ORCA_PY_AGENT_OVERRIDE(int, send_message_to_printer, dev_id, json_str, qos, flag);
} }
int command_ams_refresh_rfid(std::string dev_id, int ams_id,int slot_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_ams_refresh_rfid, dev_id, ams_id, slot_id, sequence_id, lan_mode);
}
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_ams_calibrate, dev_id, ams_id, sequence_id, lan_mode);
}
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_ams_select_tray, dev_id, tray_id, sequence_id, lan_mode);
}
int command_start_camera(std::string dev_id) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_start_camera, dev_id);
}
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_xyz_abs, dev_id, sequence_id, lan_mode);
}
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_auto_leveling, dev_id, sequence_id, lan_mode);
}
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing,
int sequence_id, bool lan_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_go_home, dev_id, is_printing, supports_mqtt_homing, sequence_id, lan_mode);
}
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl,
int sequence_id, bool lan_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_set_bed, dev_id, temp, supports_mqtt_bed_ctrl, sequence_id, lan_mode);
}
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_set_nozzle, dev_id, temp, sequence_id, lan_mode);
}
int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val,
int speed, bool is_core_xy, bool supports_mqtt_axis_control,
int sequence_id, bool lan_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, command_axis_control, dev_id, axis, unit, input_val, speed,
is_core_xy, supports_mqtt_axis_control, sequence_id, lan_mode);
}
bool start_discovery(bool start, bool sending) override bool start_discovery(bool start, bool sending) override
{ {
ORCA_PY_AGENT_OVERRIDE(bool, start_discovery, start, sending); ORCA_PY_AGENT_OVERRIDE(bool, start_discovery, start, sending);
@@ -95,12 +159,22 @@ public:
FilamentSyncMode get_filament_sync_mode() const override FilamentSyncMode get_filament_sync_mode() const override
{ {
ORCA_PY_AGENT_OVERRIDE(FilamentSyncMode, get_filament_sync_mode); ORCA_PY_AGENT_OVERRIDE_DEFAULT(FilamentSyncMode, get_filament_sync_mode);
} }
bool fetch_filament_info(std::string dev_id) override CameraStreamMode get_camera_stream_mode() const override
{ {
ORCA_PY_AGENT_OVERRIDE(bool, fetch_filament_info, dev_id); ORCA_PY_AGENT_OVERRIDE_DEFAULT(CameraStreamMode, get_camera_stream_mode);
}
std::string get_camera_url() const override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(std::string, get_camera_url);
}
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) override
{
ORCA_PY_AGENT_OVERRIDE_DEFAULT(bool, fetch_filament_info, dev_id, sync_mode);
} }
int check_cert() override int check_cert() override
@@ -110,7 +184,7 @@ public:
void install_device_cert(std::string dev_id, bool lan_only) override void install_device_cert(std::string dev_id, bool lan_only) override
{ {
ORCA_PY_AGENT_OVERRIDE(void, install_device_cert, dev_id, lan_only); ORCA_PY_AGENT_OVERRIDE_DEFAULT(void, install_device_cert, dev_id, lan_only);
} }
int ping_bind(std::string ping_code) override int ping_bind(std::string ping_code) override
@@ -145,7 +219,7 @@ public:
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override
{ {
ORCA_PY_AGENT_OVERRIDE(int, get_hms_snapshot, dev_id, file_name, callback); ORCA_PY_AGENT_OVERRIDE_DEFAULT(int, get_hms_snapshot, dev_id, file_name, callback);
} }
int set_server_callback(OnServerErrFn fn) override int set_server_callback(OnServerErrFn fn) override
@@ -196,6 +270,8 @@ public:
// request_bind_ticket returns its ticket through a std::string* out-param, which pybind11 // request_bind_ticket returns its ticket through a std::string* out-param, which pybind11
// cannot marshal back through a plain override. We dispatch manually: the Python plugin // cannot marshal back through a plain override. We dispatch manually: the Python plugin
// returns a (result, ticket) tuple, which we unpack into the int result and the out-param. // returns a (result, ticket) tuple, which we unpack into the int result and the out-param.
// Not required to be implemented, so a missing override answers with the same failure value
// as the other printer-agent operations. Leave the out-param untouched on failure.
int request_bind_ticket(std::string* ticket) override int request_bind_ticket(std::string* ticket) override
{ {
try { try {
@@ -207,7 +283,7 @@ public:
pybind11::function override = pybind11::function override =
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket"); pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
if (!override) if (!override)
pybind11::pybind11_fail("Tried to call pure virtual function \"PrinterAgentPluginCapability::request_bind_ticket\""); return printer_agent_failure<int>();
try { try {
pybind11::tuple result = override().cast<pybind11::tuple>(); pybind11::tuple result = override().cast<pybind11::tuple>();
if (ticket) if (ticket)
+13 -3
View File
@@ -57,18 +57,28 @@ function(orcaslicer_copy_test_dlls)
# into it). Copy them next to the executable and give it an $ORIGIN # into it). Copy them next to the executable and give it an $ORIGIN
# rpath so the loader finds them when the tests run on the CI unit-test # rpath so the loader finds them when the tests run on the CI unit-test
# runner, which only receives this build/tests tree. # runner, which only receives this build/tests tree.
get_target_property(_linked_libs ${_TEST_NAME}_tests LINK_LIBRARIES) #
# Optional first arg: the target to patch, for directories that define
# more than one test executable. Defaults to ${_TEST_NAME}_tests so
# existing single-executable callers don't need to pass it.
if (ARGC GREATER 0)
set(_target ${ARGV0})
else()
set(_target ${_TEST_NAME}_tests)
endif()
get_target_property(_linked_libs ${_target} LINK_LIBRARIES)
if (NOT "libslic3r_gui" IN_LIST _linked_libs) if (NOT "libslic3r_gui" IN_LIST _linked_libs)
return() return()
endif() endif()
set_property(TARGET ${_TEST_NAME}_tests PROPERTY BUILD_RPATH "$ORIGIN") set_property(TARGET ${_target} PROPERTY BUILD_RPATH "$ORIGIN")
set(_configs ${CMAKE_CONFIGURATION_TYPES}) set(_configs ${CMAKE_CONFIGURATION_TYPES})
if (NOT _configs) if (NOT _configs)
set(_configs "${CMAKE_BUILD_TYPE}") set(_configs "${CMAKE_BUILD_TYPE}")
endif() endif()
foreach(_cfg IN LISTS _configs) foreach(_cfg IN LISTS _configs)
orcaslicer_copy_sos(${_TEST_NAME}_tests "${_cfg}" "" _unused_sos) orcaslicer_copy_sos(${_target} "${_cfg}" "" _unused_sos)
endforeach() endforeach()
endif() endif()
endfunction() endfunction()
+9 -1
View File
@@ -5,12 +5,20 @@ add_executable(${_TEST_NAME}_tests
test_creality_cfs_match.cpp test_creality_cfs_match.cpp
test_dev_mapping.cpp test_dev_mapping.cpp
test_filament_bitmap_utils.cpp test_filament_bitmap_utils.cpp
test_device_progress.cpp
test_device_manager_integration.cpp
test_web_media_controller.cpp
test_network_versions.cpp test_network_versions.cpp
test_action_source.cpp test_action_source.cpp
test_plugin_host_api.cpp test_plugin_host_api.cpp
test_plugin_capability_config.cpp test_plugin_capability_config.cpp
test_plugin_config.cpp test_plugin_config.cpp
test_plugin_capabilities_in_use.cpp test_plugin_capabilities_in_use.cpp
test_plugin_status.cpp
test_printer_agent.cpp
test_qidi_printer_agent.cpp
test_orca_mqtt_connection.cpp
test_orca_printer_agent.cpp
test_plugin_install.cpp test_plugin_install.cpp
test_plugin_lifecycle.cpp test_plugin_lifecycle.cpp
test_plugin_printer_agent.cpp test_plugin_printer_agent.cpp
@@ -30,7 +38,7 @@ endif ()
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r_gui libslic3r pybind11::embed Catch2::Catch2WithMain) target_link_libraries(${_TEST_NAME}_tests test_common libslic3r_gui libslic3r pybind11::embed Catch2::Catch2WithMain)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
orcaslicer_copy_test_dlls() orcaslicer_copy_test_dlls(${_TEST_NAME}_tests)
if (WIN32) if (WIN32)
add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD
+325
View File
@@ -0,0 +1,325 @@
#pragma once
// In-process plaintext MQTT-over-WebSocket broker for the OrcaMqtt tests.
//
// It speaks just enough of MQTT 3.1.1 to drive OrcaMqttConnection /
// OrcaPrinterAgent end to end without a real network: CONNECT/CONNACK,
// SUBSCRIBE/SUBACK, UNSUBSCRIBE/UNSUBACK, client PUBLISH (QoS 0), PINGREQ and
// DISCONNECT. The outbound PUBLISH frame is built with the production
// OrcaMqttConnection::make_publish_packet() so the tests never depend on a
// second, hand-rolled MQTT encoder.
#include <slic3r/Utils/OrcaMqttConnection.hpp>
#include <boost/asio.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <utility>
#include <vector>
namespace orca_mqtt_test {
namespace net = boost::asio;
namespace beast = boost::beast;
namespace ws = boost::beast::websocket;
using tcp = boost::asio::ip::tcp;
// Decode an MQTT remaining-length varint starting at packet[offset].
// Returns {value, bytes_consumed}; bytes_consumed == 0 means malformed.
inline std::pair<std::size_t, std::size_t> mqtt_decode_remaining_length(const std::string& packet, std::size_t offset)
{
std::size_t value = 0;
std::size_t multiplier = 1;
std::size_t used = 0;
while (offset + used < packet.size() && used < 4) {
const std::uint8_t byte = static_cast<std::uint8_t>(packet[offset + used]);
value += static_cast<std::size_t>(byte & 0x7f) * multiplier;
multiplier *= 128;
++used;
if ((byte & 0x80) == 0)
return {value, used};
}
return {0, 0};
}
inline bool mqtt_topic_is_request(const std::string& topic)
{
static const std::string suffix = "/request";
return topic.size() >= suffix.size() &&
topic.compare(topic.size() - suffix.size(), suffix.size(), suffix) == 0;
}
class MockBroker
{
public:
// refuse_auth: answer every CONNECT with CONNACK rc 5 (not authorized) and
// close, so the reconnect/refusal paths can be exercised.
explicit MockBroker(bool refuse_auth = false) : m_refuse_auth(refuse_auth), m_acceptor(m_io)
{
const tcp::endpoint endpoint(net::ip::make_address("127.0.0.1"), 0);
m_acceptor.open(endpoint.protocol());
m_acceptor.set_option(net::socket_base::reuse_address(true));
m_acceptor.bind(endpoint);
m_acceptor.listen(net::socket_base::max_listen_connections);
m_port = std::to_string(m_acceptor.local_endpoint().port());
// why: a non-blocking acceptor lets the accept loop poll a stop flag, so
// the destructor never has to interrupt a blocking accept().
m_acceptor.non_blocking(true);
m_thread = std::thread([this] { run(); });
}
~MockBroker()
{
m_stopping.store(true);
drop_client(); // unblocks the worker's blocking read
if (m_thread.joinable())
m_thread.join();
boost::system::error_code ec;
m_acceptor.close(ec); // after join: the acceptor is worker-owned
m_io.stop();
}
MockBroker(const MockBroker&) = delete;
MockBroker& operator=(const MockBroker&) = delete;
std::string ws_url() const { return "ws://127.0.0.1:" + m_port + "/mqtt"; }
std::pair<std::string, std::string> host_port() const { return {std::string("127.0.0.1"), m_port}; }
// Server -> client PUBLISH on device/<dev_id>/report.
void push_report(const std::string& dev_id, const std::string& payload)
{
const std::vector<std::uint8_t> packet =
Slic3r::OrcaMqttConnection::make_publish_packet("device/" + dev_id + "/report", payload);
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_stream || !m_stream_ready)
return;
boost::system::error_code ec;
m_stream->binary(true);
m_stream->write(net::buffer(packet), ec); // a vanished client is not a test failure
}
// Force-close the live client socket; the worker's read returns an error and
// the accept loop picks up the client's reconnect.
void drop_client()
{
std::lock_guard<std::mutex> lock(m_mutex);
close_client_locked();
}
// Payloads the client PUBLISHed to any device/<id>/request topic.
std::vector<std::string> received_requests() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_received_requests;
}
// MQTT CONNECTs seen; increments again after a reconnect.
int connect_count() const { return m_connect_count.load(); }
// MQTT PINGREQs seen while the client has no other traffic.
int ping_count() const { return m_ping_count.load(); }
private:
void run()
{
try {
while (!m_stopping.load()) {
tcp::socket socket(m_io);
boost::system::error_code ec;
m_acceptor.accept(socket, ec);
if (ec == net::error::would_block || ec == net::error::try_again) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
continue;
}
if (ec)
return;
try {
serve(std::move(socket));
} catch (...) {
// a client dying mid-session must not take the broker down
}
std::lock_guard<std::mutex> lock(m_mutex);
close_client_locked();
m_stream.reset();
}
} catch (...) {
// never let an exception escape the broker thread
}
}
void serve(tcp::socket socket)
{
ws::stream<beast::tcp_stream>* stream = nullptr;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_stream.emplace(std::move(socket));
m_stream_ready = false;
stream = &*m_stream;
}
// why: no io_context is ever run here, so a tcp_stream timer would never
// fire; the sync operations below carry no timeout of their own.
beast::get_lowest_layer(*stream).expires_never();
stream->set_option(ws::stream_base::decorator(
[](ws::response_type& res) { res.set("Sec-WebSocket-Protocol", "mqtt"); }));
boost::system::error_code ec;
stream->accept(ec);
if (ec)
return;
stream->binary(true);
{
std::lock_guard<std::mutex> lock(m_mutex);
m_stream_ready = true;
}
read_loop(*stream);
}
// The client sends every MQTT packet as one binary WebSocket message, so one
// read yields exactly one packet.
void read_loop(ws::stream<beast::tcp_stream>& stream)
{
beast::flat_buffer buffer;
while (!m_stopping.load()) {
boost::system::error_code ec;
buffer.clear();
stream.read(buffer, ec);
if (ec)
return;
const std::string packet = beast::buffers_to_string(buffer.data());
if (packet.empty())
continue;
if (!handle_packet(stream, packet))
return;
}
}
// Returns false when the session must be closed.
bool handle_packet(ws::stream<beast::tcp_stream>& stream, const std::string& packet)
{
switch (static_cast<std::uint8_t>(packet[0]) & 0xf0) {
case 0x10: { // CONNECT
++m_connect_count;
if (m_refuse_auth) {
write_packet(stream, {0x20, 0x02, 0x00, 0x05}); // CONNACK not authorized
return false;
}
write_packet(stream, {0x20, 0x02, 0x00, 0x00}); // CONNACK accepted
return true;
}
case 0x80: { // SUBSCRIBE (0x82) - packet id follows the remaining-length varint
const auto id = packet_id(packet);
if (id)
write_packet(stream, {0x90, 0x03, id->first, id->second, 0x00}); // SUBACK, QoS 0
return true;
}
case 0xa0: { // UNSUBSCRIBE (0xa2)
const auto id = packet_id(packet);
if (id)
write_packet(stream, {0xb0, 0x02, id->first, id->second}); // UNSUBACK
return true;
}
case 0x30: { // PUBLISH, QoS 0 (no packet identifier)
record_publish(packet);
return true;
}
case 0xc0: // PINGREQ
++m_ping_count;
write_packet(stream, {0xd0, 0x00});
return true;
case 0xe0: // DISCONNECT
return false;
default:
return true;
}
}
// The two packet-identifier bytes sitting right after the remaining-length varint.
static std::optional<std::pair<std::uint8_t, std::uint8_t>> packet_id(const std::string& packet)
{
const auto varint = mqtt_decode_remaining_length(packet, 1);
if (varint.second == 0)
return std::nullopt;
const std::size_t pos = 1 + varint.second;
if (pos + 2 > packet.size())
return std::nullopt;
return std::make_pair(static_cast<std::uint8_t>(packet[pos]), static_cast<std::uint8_t>(packet[pos + 1]));
}
void record_publish(const std::string& packet)
{
const auto varint = mqtt_decode_remaining_length(packet, 1);
if (varint.second == 0)
return;
std::size_t pos = 1 + varint.second;
if (pos + 2 > packet.size())
return;
const std::size_t topic_len = (static_cast<std::size_t>(static_cast<std::uint8_t>(packet[pos])) << 8) |
static_cast<std::uint8_t>(packet[pos + 1]);
pos += 2;
if (pos + topic_len > packet.size())
return;
const std::string topic = packet.substr(pos, topic_len);
pos += topic_len;
const std::size_t end = std::min(packet.size(), 1 + varint.second + varint.first);
if (end < pos)
return;
if (!mqtt_topic_is_request(topic))
return;
std::lock_guard<std::mutex> lock(m_mutex);
m_received_requests.push_back(packet.substr(pos, end - pos));
}
// Every write - the worker's own replies and push_report() from the test
// thread - is serialised by m_mutex. The production client keeps all of its
// WebSocket operations on its MQTT worker instead.
void write_packet(ws::stream<beast::tcp_stream>& stream, const std::vector<std::uint8_t>& packet)
{
std::lock_guard<std::mutex> lock(m_mutex);
boost::system::error_code ec;
stream.binary(true);
stream.write(net::buffer(packet), ec);
}
void close_client_locked()
{
if (!m_stream)
return;
m_stream_ready = false;
boost::system::error_code ec;
auto& socket = beast::get_lowest_layer(*m_stream).socket();
if (socket.cancel(ec))
return;
// shutdown() before close() is what actually wakes a blocking read on the
// worker thread; close() alone does not on POSIX.
if (socket.shutdown(tcp::socket::shutdown_both, ec))
return;
if (socket.close(ec))
return;
}
const bool m_refuse_auth;
net::io_context m_io;
tcp::acceptor m_acceptor;
std::string m_port;
std::thread m_thread;
std::atomic_bool m_stopping{false};
std::atomic<int> m_connect_count{0};
std::atomic<int> m_ping_count{0};
mutable std::mutex m_mutex;
std::optional<ws::stream<beast::tcp_stream>> m_stream; // guarded by m_mutex
bool m_stream_ready = false; // guarded by m_mutex
std::vector<std::string> m_received_requests; // guarded by m_mutex
};
} // namespace orca_mqtt_test
+31
View File
@@ -26,6 +26,37 @@
using json = nlohmann::json; using json = nlohmann::json;
using namespace Slic3r; using namespace Slic3r;
TEST_CASE("AMS tray placeholder state follows the latest status", "[DevFilaSystem]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
const json empty_slot = json::parse(R"({
"ams": {
"tray_exist_bits": "0",
"ams": [ { "id": "0", "info": "00000001", "tray": [
{ "id": "0", "tray_slot_placeholder": "1", "tray_color": "00000000" }
] } ]
}
})");
DevFilaSystemParser::ParseV1_0(empty_slot, &obj, obj.GetFilaSystem().get(), false);
DevAmsTray* tray = obj.GetFilaSystem()->GetAmsTray("0", "0");
REQUIRE(tray != nullptr);
REQUIRE(tray->is_slot_placeholder);
const json loaded_slot = json::parse(R"({
"ams": {
"tray_exist_bits": "1",
"ams": [ { "id": "0", "info": "00000001", "tray": [
{ "id": "0", "tray_color": "FF0000FF" }
] } ]
}
})");
DevFilaSystemParser::ParseV1_0(loaded_slot, &obj, obj.GetFilaSystem().get(), false);
CHECK_FALSE(tray->is_slot_placeholder);
}
TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]") TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]")
{ {
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1"); MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
@@ -0,0 +1,159 @@
#include <catch2/catch_all.hpp>
#include <slic3r/GUI/DeviceCore/DevManager.h>
#include <slic3r/GUI/DeviceManager.hpp>
#include <libslic3r/AppConfig.hpp>
#include <slic3r/Utils/NetworkAgent.hpp>
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
#include <slic3r/Utils/OrcaPrinterAgent.hpp>
#include <nlohmann/json.hpp>
#include <memory>
#include <cstdint>
#include <string>
#include <utility>
using namespace Slic3r;
using json = nlohmann::json;
namespace {
class StubCloudAgent final : public OrcaCloudServiceAgent
{
public:
StubCloudAgent() : OrcaCloudServiceAgent("") {}
int get_user_print_info(unsigned int* http_code, std::string* http_body) override
{
if (http_code)
*http_code = 200;
if (http_body)
*http_body = R"({"devices":[]})";
return 0;
}
std::string get_user_name() override { return "integration-test-user"; }
};
class TestPrinterAgent final : public OrcaPrinterAgent
{
public:
explicit TestPrinterAgent(std::string id)
: OrcaPrinterAgent(""), m_info{std::move(id), "Integration Test Agent", "1.0", "test agent"}
{
}
AgentInfo get_agent_info() override { return m_info; }
private:
AgentInfo m_info;
};
struct ScopedAppConfig
{
AppConfig config;
};
std::string machine_list_response(const std::string& provider, const std::string& agent_id,
std::uint64_t generation, const std::string& name)
{
json machine;
machine["dev_id"] = "integration-device";
machine["dev_name"] = name;
machine["dev_online"] = true;
machine["task_status"] = "idle";
json response;
response["provider"] = provider;
response["agent_id"] = agent_id;
response["generation"] = generation;
response["devices"] = json::array({machine});
return response.dump();
}
} // namespace
TEST_CASE("Network agent stamps user-machine responses with request context", "[DeviceManager][integration]")
{
auto cloud = std::make_shared<StubCloudAgent>();
NetworkAgent network(cloud, nullptr);
network.set_printer_agent(std::make_shared<TestPrinterAgent>("integration-agent-a"));
const std::uint64_t generation_before = network.get_user_machine_list_generation();
unsigned int http_code = 0;
std::string body;
REQUIRE(network.get_user_print_info(&http_code, &body, ORCA_CLOUD_PROVIDER) == 0);
const json response = json::parse(body);
CHECK(http_code == 200);
CHECK(response["provider"] == ORCA_CLOUD_PROVIDER);
CHECK(response["agent_id"] == "integration-agent-a");
CHECK(response["generation"] == generation_before + 1);
}
TEST_CASE("Device manager ignores stale cloud machine responses", "[DeviceManager][integration]")
{
ScopedAppConfig app_config;
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("integration-agent"));
network.set_printer_agent(std::make_shared<TestPrinterAgent>("integration-agent"));
DeviceManager manager(&network, false, &app_config.config);
const std::uint64_t current_generation = network.get_user_machine_list_generation();
manager.parse_user_print_info(machine_list_response(ORCA_CLOUD_PROVIDER, "integration-agent",
current_generation, "Fresh name"));
auto machines = manager.get_user_machinelist();
REQUIRE(machines.size() == 1);
REQUIRE(machines.at("integration-device") != nullptr);
CHECK(machines.at("integration-device")->get_dev_name() == "Fresh name");
manager.parse_user_print_info(machine_list_response(BBL_CLOUD_PROVIDER, "integration-agent",
current_generation, "Stale provider"));
manager.parse_user_print_info(machine_list_response(ORCA_CLOUD_PROVIDER, "other-agent",
current_generation, "Stale agent"));
manager.parse_user_print_info(machine_list_response(ORCA_CLOUD_PROVIDER, "integration-agent",
current_generation + 1, "Stale generation"));
machines = manager.get_user_machinelist();
REQUIRE(machines.size() == 1);
CHECK(machines.at("integration-device")->get_dev_name() == "Fresh name");
}
TEST_CASE("Device manager filters and rehomes devices by printer-agent ownership", "[DeviceManager][integration]")
{
ScopedAppConfig app_config;
auto agent_a = std::make_shared<TestPrinterAgent>("integration-agent-a");
auto agent_b = std::make_shared<TestPrinterAgent>("integration-agent-b");
NetworkAgent network(nullptr, agent_a);
DeviceManager manager(&network, false, &app_config.config);
BBLocalMachine machine;
machine.dev_id = "integration-lan-device";
machine.dev_name = "Integration LAN device";
machine.dev_ip = "192.0.2.10";
machine.printer_type = "C11";
MachineObject* object = manager.insert_local_device(machine, "lan", "free", "", "access-code");
REQUIRE(object != nullptr);
CHECK(object->printer_agent_id == "integration-agent-a");
CHECK(manager.get_my_machine_list("integration-agent-a").count(machine.dev_id) == 1);
CHECK(manager.get_my_machine_list("integration-agent-b").empty());
network.set_printer_agent(agent_b);
CHECK(manager.get_my_machine_list("integration-agent-b").empty());
manager.on_machine_alive(R"({
"dev_name":"Rediscovered device",
"dev_id":"integration-lan-device",
"dev_ip":"192.0.2.10",
"dev_type":"C11",
"dev_signal":"strong",
"connect_type":"lan",
"bind_state":"free"
})");
CHECK(object->printer_agent_id == "integration-agent-b");
CHECK(manager.get_my_machine_list("integration-agent-a").empty());
CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1);
}
+141
View File
@@ -0,0 +1,141 @@
// why: match the GUI include order to avoid rpcndr.h byte/std::byte
// ambiguity in the Windows COM headers.
// why: wx/timer.h must precede DeviceManager.hpp because
// DeviceErrorDialog.hpp uses wxTimerEvent.
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_all.hpp>
#include <stdexcept>
#include <wx/timer.h>
#include "slic3r/GUI/DeviceManager.hpp"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
using namespace Slic3r;
TEST_CASE("Integer progress reaches the shared subtask", "[DeviceManager][Progress]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
machine.update_print_progress(json(37));
REQUIRE(machine.mc_print_percent == 37);
BBLSubTask* subtask = machine.get_subtask();
REQUIRE(subtask != nullptr);
CHECK(subtask->task_progress == 37);
}
TEST_CASE("String progress reaches the shared subtask", "[DeviceManager][Progress]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
machine.update_print_progress(json("41"));
REQUIRE(machine.mc_print_percent == 41);
BBLSubTask* subtask = machine.get_subtask();
REQUIRE(subtask != nullptr);
CHECK(subtask->task_progress == 41);
}
TEST_CASE("Floating-point progress preserves the previous shared value", "[DeviceManager][Progress]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
machine.update_print_progress(json(29));
REQUIRE(machine.mc_print_percent == 29);
BBLSubTask* subtask = machine.get_subtask();
REQUIRE(subtask != nullptr);
REQUIRE(subtask->task_progress == 29);
machine.update_print_progress(json(29.5));
BBLSubTask* current_subtask = machine.get_subtask();
REQUIRE(current_subtask != nullptr);
REQUIRE(current_subtask == subtask);
CHECK(machine.mc_print_percent == 29);
CHECK(current_subtask->task_progress == 29);
}
TEST_CASE("Unsupported progress values leave a fresh machine unchanged", "[DeviceManager][Progress]")
{
SECTION("boolean") {
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
REQUIRE(machine.subtask_ == nullptr);
machine.update_print_progress(json(true));
CHECK(machine.mc_print_percent == 0);
CHECK(machine.subtask_ == nullptr);
}
SECTION("null") {
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
REQUIRE(machine.subtask_ == nullptr);
machine.update_print_progress(json(nullptr));
CHECK(machine.mc_print_percent == 0);
CHECK(machine.subtask_ == nullptr);
}
SECTION("object") {
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
REQUIRE(machine.subtask_ == nullptr);
machine.update_print_progress(json::object());
CHECK(machine.mc_print_percent == 0);
CHECK(machine.subtask_ == nullptr);
}
SECTION("array") {
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
REQUIRE(machine.subtask_ == nullptr);
machine.update_print_progress(json::array());
CHECK(machine.mc_print_percent == 0);
CHECK(machine.subtask_ == nullptr);
}
}
TEST_CASE("Malformed string progress leaves a fresh machine unchanged", "[DeviceManager][Progress]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
REQUIRE(machine.subtask_ == nullptr);
CHECK_NOTHROW(machine.update_print_progress(json("not-a-percent")));
CHECK(machine.mc_print_percent == 0);
CHECK(machine.subtask_ == nullptr);
}
TEST_CASE("Zero progress replaces active shared progress", "[DeviceManager][Progress]")
{
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
machine.update_print_progress(json(63));
BBLSubTask* subtask = machine.get_subtask();
REQUIRE(subtask != nullptr);
REQUIRE(machine.mc_print_percent == 63);
REQUIRE(subtask->task_progress == 63);
machine.set_print_state("FAILED");
machine.update_print_progress(json(0));
BBLSubTask* current_subtask = machine.get_subtask();
REQUIRE(current_subtask != nullptr);
REQUIRE(current_subtask == subtask);
REQUIRE(machine.mc_print_percent == 0);
CHECK(current_subtask->task_progress == 0);
}
@@ -0,0 +1,243 @@
#include <catch2/catch_test_macros.hpp>
#include <slic3r/Utils/OrcaMqttConnection.hpp>
#include "orca_mqtt_mock_broker.hpp"
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using Slic3r::OrcaMqttConnection;
// Offset of the CONNECT variable header: 1 (fixed header) + N remaining-length varint bytes.
static size_t mqtt_varheader_offset(const std::vector<uint8_t>& p) {
size_t i = 1;
while (i < p.size() && (p[i] & 0x80)) ++i; // skip varint continuation bytes
return i + 1; // + the final varint byte
}
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));
}
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 fixed header
const size_t v = mqtt_varheader_offset(p);
CHECK(p[v + 0] == 0x00); CHECK(p[v + 1] == 0x04); // protocol name length
CHECK(p[v + 2] == 'M'); CHECK(p[v + 3] == 'Q');
CHECK(p[v + 4] == 'T'); CHECK(p[v + 5] == 'T');
CHECK(p[v + 6] == 0x04); // protocol level 3.1.1
CHECK(p[v + 7] == 0x02); // connect flags: clean session only
CHECK(((p[v + 8] << 8) | p[v + 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);
const size_t v = mqtt_varheader_offset(p);
CHECK(p[v + 7] == (0x02 | 0x80 | 0x40)); // clean session + username + password flags
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);
}
// Auth precedence (spec O3): when a bearer_provider is configured, connect_and_read
// passes empty CONNECT credentials, so the packet must carry clean-session only and
// no username/password flags or payload fields. (The precedence branch itself lives
// in connect_and_read; the [.integration] cloud-style round trip exercises it live.)
TEST_CASE("OrcaMqtt CONNECT omits creds when a bearer is configured", "[OrcaMqtt]") {
auto p = OrcaMqttConnection::make_connect_packet("cid", "", "", 60);
const size_t v = mqtt_varheader_offset(p);
CHECK(p[v + 7] == 0x02); // clean session only: no 0x80 / 0x40
const std::string blob(p.begin(), p.end());
CHECK(blob.find("orcasonar") == std::string::npos);
}
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
const size_t v = mqtt_varheader_offset(p);
CHECK(((p[v] << 8) | p[v + 1]) == 7); // packet id
CHECK(p.back() == 1); // requested QoS
}
TEST_CASE("OrcaMqtt send_request refuses when not connected", "[OrcaMqtt]") {
OrcaMqttConnection conn;
CHECK_FALSE(conn.send_request("abc", "{\"pushing\":{\"command\":\"pushall\",\"sequence_id\":\"20001\"}}"));
}
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();
}
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);
}
// --- End-to-end integration: OrcaMqttConnection against the in-process MockBroker.
// All hidden behind [.integration] (run explicitly). These prove a LAN-style config
// (CONNECT username/password) and a cloud-style config (bearer on the WS upgrade,
// no CONNECT creds) drive the *same* OrcaMqttConnection code path with identical
// assertions.
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"; }
// A mutex + condition_variable rather than a promise: the handler runs on the MQTT
// worker thread and a second inbound message would throw std::future_error there.
std::mutex got_mutex;
std::condition_variable got_cv;
bool got_any = false;
std::string got_id, got_payload;
REQUIRE(conn.start(cfg,
[&](const std::string& id, const std::string& payload){
{
std::lock_guard<std::mutex> l(got_mutex);
if (got_any) return; // keep the first message only
got_any = true; got_id = id; got_payload = payload;
}
got_cv.notify_all();
},
[](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"}})");
std::string id, payload;
{
std::unique_lock<std::mutex> l(got_mutex);
REQUIRE(got_cv.wait_for(l, std::chrono::seconds(3), [&]{ return got_any; }));
id = got_id; payload = got_payload;
}
CHECK(id == "dev-1");
CHECK(payload.find("push_status") != std::string::npos);
// the client's command reached the broker on the request topic. The mock records
// the PUBLISH on its own read-loop thread, so poll rather than check immediately.
std::vector<std::string> reqs;
for (int i = 0; i < 200; ++i) {
reqs = broker.received_requests();
if (!reqs.empty()) break;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
REQUIRE(reqs.size() >= 1);
CHECK(reqs.front().find("pushall") != std::string::npos);
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); }
TEST_CASE("OrcaMqtt keepalive runs while the connection is idle", "[OrcaMqtt][.integration]") {
orca_mqtt_test::MockBroker broker;
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg;
cfg.url = broker.ws_url();
cfg.keepalive_seconds = 2;
REQUIRE(conn.start(cfg, [](const std::string&, const std::string&) {}, [](bool, bool) {}));
for (int i = 0; i < 200 && broker.ping_count() == 0; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
CHECK(broker.ping_count() > 0);
conn.stop();
}
TEST_CASE("OrcaMqtt reconnects and re-subscribes after a socket drop", "[OrcaMqtt][.integration]") {
orca_mqtt_test::MockBroker broker;
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg; cfg.url = broker.ws_url(); cfg.use_tls = false; cfg.username = "u"; cfg.password = "p";
std::mutex m; std::vector<std::string> got;
REQUIRE(conn.start(cfg,
[&](const std::string&, const std::string& p){ std::lock_guard<std::mutex> l(m); got.push_back(p); },
[](bool,bool){}));
REQUIRE(conn.subscribe("dev-1"));
broker.drop_client();
// the worker reconnects with ~1s backoff
for (int i = 0; i < 300 && broker.connect_count() < 2; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(20));
CHECK(broker.connect_count() >= 2);
// a report after the reconnect must still be delivered -> the SUBSCRIBE was re-sent
broker.push_report("dev-1", R"({"print":{"command":"push_status","sequence_id":"20002"}})");
bool delivered = false;
for (int i = 0; i < 200 && !delivered; ++i) {
{ std::lock_guard<std::mutex> l(m); delivered = !got.empty(); }
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
CHECK(delivered);
conn.stop();
}
TEST_CASE("OrcaMqtt auth rejection is terminal (no retry storm)", "[OrcaMqtt][.integration]") {
orca_mqtt_test::MockBroker broker(/*refuse_auth=*/true);
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg; cfg.url = broker.ws_url(); cfg.use_tls = false; cfg.username = "u"; cfg.password = "bad";
const bool ok = conn.start(cfg, [](const std::string&, const std::string&){}, [](bool,bool){});
CHECK_FALSE(ok);
CHECK(conn.last_connack_rc() == 5);
// worker must have stopped itself (rc 5 is terminal) — give it a moment
for (int i = 0; i < 100 && conn.is_running(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
CHECK_FALSE(conn.is_running());
// and it must NOT have hammered the broker with retries
std::this_thread::sleep_for(std::chrono::milliseconds(200));
CHECK(broker.connect_count() <= 2);
conn.stop();
}
@@ -0,0 +1,208 @@
#include <catch2/catch_test_macros.hpp>
#include <slic3r/Utils/IPrinterAgent.hpp>
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
#include <slic3r/Utils/OrcaPrinterAgent.hpp>
#include "orca_mqtt_mock_broker.hpp"
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include <thread>
#include <vector>
using Slic3r::OrcaPrinterAgent;
namespace {
// Probe exposes the protected internals the tests drive.
struct Probe : OrcaPrinterAgent {
using OrcaPrinterAgent::OrcaPrinterAgent;
using OrcaPrinterAgent::deliver_to_sink;
using OrcaPrinterAgent::parse_lan_endpoint;
using OrcaPrinterAgent::make_lan_client_id;
using OrcaPrinterAgent::lan_connection_target;
};
}
TEST_CASE("OrcaPrinterAgent forwards a status payload to on_message_fn", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
std::string got_id, got_payload;
agent.set_on_message_fn([&](std::string id, std::string p){ got_id = std::move(id); got_payload = std::move(p); });
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status"}})", /*local=*/false);
CHECK(got_id == "dev-1");
CHECK(got_payload.find("push_status") != std::string::npos);
}
TEST_CASE("OrcaPrinterAgent stamps the get_capabilities nozzle diameter onto push_status frames", "[OrcaPrinterAgent]") {
Probe agent("/tmp");
std::string last_payload;
agent.set_on_message_fn([&](std::string, std::string p){ last_payload = std::move(p); });
// Before any capabilities reply, a push_status frame is forwarded untouched.
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":10}})", /*local=*/false);
CHECK(last_payload.find("nozzle_diameter") == std::string::npos);
// The get_capabilities reply is forwarded verbatim; its topology nozzle diameter
// is cached for the device.
agent.deliver_to_sink(
"dev-1",
R"({"info":{"command":"get_capabilities","capabilities":{"topology":{"tools":[{"id":"T0","nozzle":{"diameter_mm":0.4}}]}}}})",
/*local=*/false);
CHECK(last_payload.find("\"command\":\"get_capabilities\"") != std::string::npos);
CHECK(last_payload.find("\"print\"") == std::string::npos);
// Later push_status frames for that device get the cached diameter plus a neutral
// nozzle_type, so MachineObject::parse_json's legacy nozzle parser can run.
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","mc_percent":20}})", /*local=*/false);
CHECK(last_payload.find("\"nozzle_diameter\":0.4") != std::string::npos);
CHECK(last_payload.find("\"nozzle_type\":\"N/A\"") != std::string::npos);
// A different device is unaffected.
agent.deliver_to_sink("dev-2", R"({"print":{"command":"push_status"}})", /*local=*/false);
CHECK(last_payload.find("nozzle_diameter") == std::string::npos);
// A frame that already carries real nozzle data is not overridden.
agent.deliver_to_sink("dev-1", R"({"print":{"command":"push_status","nozzle_diameter":0.6}})", /*local=*/false);
CHECK(last_payload.find("\"nozzle_diameter\":0.6") != std::string::npos);
CHECK(last_payload.find("N/A") == std::string::npos);
}
TEST_CASE("OrcaPrinterAgent::parse_lan_endpoint", "[OrcaPrinterAgent]") {
std::string h, p;
REQUIRE(Probe::parse_lan_endpoint("192.168.1.9", h, p));
CHECK(h == "192.168.1.9"); CHECK(p == "8280");
REQUIRE(Probe::parse_lan_endpoint("http://host.local:9000/x", h, p));
CHECK(h == "host.local"); CHECK(p == "9000");
CHECK_FALSE(Probe::parse_lan_endpoint("", h, p));
}
TEST_CASE("OrcaPrinterAgent::make_lan_client_id is stable and prefixed", "[OrcaPrinterAgent]") {
const auto a = Probe::make_lan_client_id("dev-1");
const auto b = Probe::make_lan_client_id("dev-1");
CHECK(a == b); // drawn once per process
CHECK(a.rfind("orcaslicer-lan-dev-1-", 0) == 0);
}
TEST_CASE("connect_printer wires up a LAN Config", "[OrcaPrinterAgent][.integration]") {
Probe agent("/tmp");
Slic3r::PrinterConnectionParams params{
"dev-1", "10.255.255.1", "orcasonar", "code", "", false, ""
};
const int rc = agent.connect_printer(params);
CHECK(rc == BAMBU_NETWORK_SUCCESS);
CHECK(agent.lan_connection_target() == "ws://10.255.255.1:8280/mqtt");
CHECK(agent.get_user_selected_machine().empty()); // LAN path must not touch the cloud selection
agent.disconnect_printer();
}
TEST_CASE("post-connect sequence is subscribe then 4 requests in order", "[OrcaPrinterAgent]") {
struct SeqProbe : 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");
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)
if (auto pos = c.find("sequence_id"); pos != std::string::npos)
CHECK(c.substr(pos).find("\"2") != std::string::npos);
}
// Hidden: spawns the connect worker and attempts a real (failing) connect.
TEST_CASE("selecting a cloud printer configures the fleet socket", "[OrcaPrinterAgent][.integration]") {
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");
// The configure runs on the connect worker; poll rather than racing it.
std::string url;
for (int i = 0; i < 300; ++i) {
url = cloud->selected_printer_mqtt_url();
if (!url.empty()) break;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
CHECK(url == "wss://api.example.com/api/v1/printers/mqtt");
agent.set_user_selected_machine(""); // selection changes do not tear down the fleet socket
CHECK(cloud->selected_printer_mqtt_url() == "wss://api.example.com/api/v1/printers/mqtt");
}
TEST_CASE("a stale-generation inbound message is dropped", "[OrcaPrinterAgent]") {
struct GenProbe : OrcaPrinterAgent {
using OrcaPrinterAgent::OrcaPrinterAgent;
using OrcaPrinterAgent::make_lan_message_handler; // expose for the test
};
GenProbe 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);
// m_lan_generation starts at 0; two bumps -> 2, so the epoch-1 handler is stale.
agent.bump_lan_generation_for_test();
agent.bump_lan_generation_for_test();
handler_gen1("dev-1", "{}"); // late callback from gen 1
CHECK(hits == 0);
}
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"); // no session -> connect_server short-circuits before any probe
cloud->connect_server();
REQUIRE(cloud->get_mqtt_connection() != nullptr); // created in the ctor
CHECK_FALSE(cloud->get_mqtt_connection()->is_running()); // never started
CHECK(cloud->selected_printer_mqtt_url().empty());
}
TEST_CASE("send_message* reject when there is 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);
CHECK(agent.send_message("", "{}", 0, 0) == BAMBU_NETWORK_ERR_INVALID_HANDLE); // empty dev_id
}
TEST_CASE("send_message_to_printer publishes on the LAN connection", "[OrcaPrinterAgent][.integration]") {
orca_mqtt_test::MockBroker broker;
OrcaPrinterAgent agent("/tmp");
const auto ep = broker.host_port();
agent.connect_printer(Slic3r::PrinterConnectionParams{"dev-1", ep.first + ":" + ep.second, "orcasonar", "code", "", false, ""});
for (int i = 0; i < 150 && broker.connect_count() == 0; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(20));
REQUIRE(broker.connect_count() >= 1);
CHECK(agent.send_message_to_printer("dev-1", R"({"print":{"command":"pause","sequence_id":"20007"}})", 0, 0)
== BAMBU_NETWORK_SUCCESS);
// on_connected also publishes 4 requests; poll until "pause" specifically shows up.
bool saw_pause = false;
for (int i = 0; i < 150 && !saw_pause; ++i) {
for (const auto& r : broker.received_requests())
if (r.find("pause") != std::string::npos) { saw_pause = true; break; }
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
CHECK(saw_pause);
agent.disconnect_printer();
}
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(Slic3r::PrinterConnectionParams{"dev-1", "127.0.0.1:1", "orcasonar", "code", "", false, ""}); // nothing listening: instant ECONNREFUSED
agent.reset(); // ~OrcaPrinterAgent must stop the conn, join the thread, and not hang/crash
}
SUCCEED();
}

Some files were not shown because too many files have changed in this diff Show More