Compare commits

...
Author SHA1 Message Date
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
82 changed files with 3736 additions and 957 deletions
+54 -1
View File
@@ -821,6 +821,49 @@ find_package(OpenSSL REQUIRED)
find_package(CURL 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 libSRTP::srtp2)
find_library(_ORCA_SRTP_LIBRARY NAMES srtp2
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
if (_ORCA_SRTP_LIBRARY)
add_library(libSRTP::srtp2 UNKNOWN IMPORTED GLOBAL)
set_target_properties(libSRTP::srtp2 PROPERTIES
IMPORTED_LOCATION "${_ORCA_SRTP_LIBRARY}"
IMPORTED_LINK_INTERFACE_LANGUAGES C
INTERFACE_LINK_LIBRARIES "OpenSSL::Crypto")
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)
target_link_libraries(libcurl INTERFACE CURL::libcurl)
@@ -1103,6 +1146,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll
${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.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/swresample-5.dll
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
@@ -1142,6 +1186,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${_out_dir}/TKXSBase.dll
${_out_dir}/freetype.dll
${_out_dir}/avformat-61.dll
${_out_dir}/avcodec-61.dll
${_out_dir}/swresample-5.dll
${_out_dir}/swscale-8.dll
@@ -1164,7 +1209,10 @@ function(orcaslicer_copy_sos target config postfix output_sos)
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
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.3.100
${CMAKE_PREFIX_PATH}/lib/libavutil.so
@@ -1179,6 +1227,9 @@ function(orcaslicer_copy_sos target config postfix output_sos)
DESTINATION ${_out_dir})
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.61
${_out_dir}/libavcodec.so.61.3.100
@@ -1308,6 +1359,8 @@ endif ()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
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.3.100
${LIBDIR_BIN}/libavutil.so.59
+11 -4
View File
@@ -375,10 +375,6 @@ include(libnoise/libnoise.cmake)
include(Draco/Draco.cmake)
include(FFMPEG/FFMPEG.cmake)
include(Assimp/Assimp.cmake)
# 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
# a grep across the repo shows it is used for other things
@@ -389,6 +385,16 @@ if(NOT OPENSSL_FOUND)
set(OPENSSL_PKG dep_OpenSSL)
endif()
include(FFMPEG/FFMPEG.cmake)
include(Assimp/Assimp.cmake)
find_package(LibDataChannel QUIET)
set(DATACHANNEL_PKG "")
if (NOT LibDataChannel_FOUND)
include(DataChannel/DataChannel.cmake)
set(DATACHANNEL_PKG dep_DataChannel)
endif()
# we don't want to load a "wrong" openssl when loading curl
# so, just don't even bother
# ...i think this is how it works? change if wrong
@@ -461,6 +467,7 @@ set(_dep_list
dep_wxInspector
dep_FFMPEG
dep_Assimp
${DATACHANNEL_PKG}
)
if (MSVC)
+21
View File
@@ -0,0 +1,21 @@
# libdatachannel is the native ICE/DTLS/SCTP/SRTP implementation used by the
# GUI WebRTC camera controller. Keep the source revision fixed: the signaling
# protocol is evolving independently of this transport dependency.
orcaslicer_add_cmake_project(DataChannel
DEPENDS ${OPENSSL_PKG}
CMAKE_ARGS
-DNO_EXAMPLES=ON
-DNO_TESTS=ON
-DNO_WEBSOCKET=ON
-DNO_MEDIA=OFF
-DUSE_NICE=OFF
-DUSE_SYSTEM_SRTP=OFF
-DUSE_SYSTEM_JUICE=OFF
-DUSE_SYSTEM_USRSCTP=OFF
-DOPENSSL_ROOT_DIR:PATH=${DESTDIR}
-DOPENSSL_USE_STATIC_LIBS=ON
GIT_REPOSITORY https://github.com/paullouisageneau/libdatachannel.git
GIT_TAG v0.22.2
GIT_SHALLOW ON
GIT_SUBMODULES_RECURSE ON
)
+19 -3
View File
@@ -1,5 +1,16 @@
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)
set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG")
@@ -9,6 +20,7 @@ if (MSVC)
set(PREBUILD_HASH_x64 "e65916020ddb9ef84b2666dfbcbfc9b1d67f69d15b4a66db53754637bf2d498c")
ExternalProject_Add(dep_FFMPEG
${_ffmpeg_depends}
URL ${PREBUILD_URL_${DEPS_ARCH}}
URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}}
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
@@ -21,6 +33,8 @@ if (MSVC)
)
else ()
set(_openssl_cmd --enable-openssl)
if (APPLE)
set(_minos_cmd
"--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
@@ -52,10 +66,11 @@ else ()
endif()
ExternalProject_Add(dep_FFMPEG
${_ffmpeg_depends}
URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
CONFIGURE_COMMAND ${_conf_cmd}
CONFIGURE_COMMAND ${_ffmpeg_configure_command}
${_cross_cmd}
${_pic_cmd}
${_arch_cmd}
@@ -63,20 +78,21 @@ else ()
"--prefix=${DESTDIR}"
${_link_cmd}
${_minos_cmd}
${_openssl_cmd}
--disable-doc
--enable-small
--disable-outdevs
--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*
--disable-protocols
--enable-protocol=file,fd,pipe,rtp,udp
--enable-protocol=file,fd,pipe,http,https,rtp,tcp,udp
--disable-muxers
--enable-muxer=rtp
--disable-encoders
--disable-decoders
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
--disable-demuxers
--enable-demuxer=h264,mp3,mov
--enable-demuxer=h264,mp3,mov,mpjpeg,rtsp,sdp
--disable-zlib
--disable-avdevice
BUILD_IN_SOURCE ON
+3 -1
View File
@@ -575,7 +575,7 @@ function CapabilityCanRun(plugin, capability) {
}
function IsPluginChecked(plugin) {
return GetStatus(plugin) === "Activated";
return plugin.is_loaded;
}
function HasMixedCapabilityState(plugin) {
@@ -1347,6 +1347,8 @@ function StatusDescription(plugin) {
return "This plugin is still loading.";
case "Error":
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":
default:
return "This plugin is inactive. Activate it to install or load it.";
@@ -424,6 +424,11 @@ body.pane-resizing {
font-weight: 600;
}
.status-cell.status-runtimeerror {
color: var(--plugin-status-warn);
font-weight: 600;
}
.status-cell.status-loading {
color: var(--plugin-status-warn);
font-weight: 600;
@@ -680,6 +685,11 @@ body.pane-resizing {
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 {
background: var(--plugin-status-warn-bg);
color: var(--plugin-status-warn);
@@ -325,6 +325,13 @@ modules:
sha256: deedcabe339165214a3637df4c86a507aef0d793cf8774ff68735f4737e8ddbc
dest: external-packages/FFMPEG
# libdatachannel v0.22.2
- type: git
url: https://github.com/paullouisageneau/libdatachannel.git
tag: v0.22.2
commit: b3390b4e01e97071dd054684870c4bb5221794bf
dest: deps/build_flatpak/dep_DataChannel-prefix/src/dep_DataChannel
# ---------------------------------------------------------------
# Fallback archives for deps normally provided by the GNOME SDK.
# These are only used if find_package() fails to locate them.
+70 -47
View File
@@ -25,6 +25,7 @@
#include <boost/filesystem.hpp>
#include <boost/algorithm/clamp.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/join.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/nowide/cstdio.hpp>
#include <boost/nowide/fstream.hpp>
@@ -3824,6 +3825,27 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
// why: project filament_multi_colour stores space-joined components per
// filament; decode once so every merge branch seeds from the same view,
// falling back to the main color where the project has no components.
auto decode_project_multi_colors = [this](const std::vector<std::string> &main_colors) {
std::vector<std::vector<std::string>> decoded(main_colors.size());
for (size_t i = 0; i < main_colors.size(); ++i) decoded[i] = {main_colors[i]};
const ConfigOptionStrings *project_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
if (project_multi_color) {
for (size_t i = 0; i < std::min(decoded.size(), project_multi_color->values.size()); ++i) {
std::vector<std::string> colors = split_string(project_multi_color->values[i], ' ');
// why: a whitespace-only persisted entry splits into empty
// tokens, so keep the main-color fallback rather than
// serializing an empty component.
colors.erase(std::remove_if(colors.begin(), colors.end(),
[](const std::string &c) { return c.empty(); }),
colors.end());
if (!colors.empty()) decoded[i] = colors;
}
}
return decoded;
};
// 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
@@ -3893,20 +3915,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
};
auto exist_colors = filament_color->values;
std::vector<std::vector<std::string>> exist_multi_color_filment(exist_colors.size());
for (size_t i = 0; i < exist_colors.size(); i++) {
exist_multi_color_filment[i] = {exist_colors[i]};
}
ConfigOptionStrings *project_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
if (project_multi_color) {
for (size_t i = 0; i < std::min(exist_multi_color_filment.size(), project_multi_color->values.size()); i++) {
std::vector<std::string> colors = split_string(project_multi_color->values[i], ' ');
if (!colors.empty()) {
exist_multi_color_filment[i] = colors;
}
}
}
auto exist_multi_color_filment = decode_project_multi_colors(exist_colors);
bool mapped_any = false;
if (use_map && !maps.empty()) {
@@ -3982,11 +3991,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
auto exist_colors = filament_color->values;
auto exist_color_types = filament_color_type->values;
auto exist_filament_presets = this->filament_presets;
std::vector<std::vector<std::string>> exist_multi_color_filment;
exist_multi_color_filment.resize(exist_colors.size());
for (int i = 0; i < exist_colors.size(); i++) {
exist_multi_color_filment[i] = {exist_colors[i]};
}
auto exist_multi_color_filment = decode_project_multi_colors(exist_colors);
for (size_t i = 0; i < exist_colors.size(); i++) {
if (maps.find(i) != maps.end()) {//mapping exist
auto valid_index = get_map_index(ams_array_maps, maps[i]);
@@ -3994,7 +3999,10 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
exist_colors[i] = ams_filament_colors[valid_index];
exist_color_types[i] = ams_filament_color_types[valid_index];
exist_filament_presets[i] = ams_filament_presets[valid_index];
exist_multi_color_filment[i] = ams_multi_color_filment[valid_index];
// why: a single-color agent tray reports no components, so
// fall back to the printer main color rather than keeping
// the replaced project filament's components.
exist_multi_color_filment[i] = ams_multi_color_filment[valid_index].empty() ? std::vector<std::string>{ams_filament_colors[valid_index]} : ams_multi_color_filment[valid_index];
} else {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (mapping exist)";
}
@@ -4054,7 +4062,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
exist_filament_presets.push_back(need_append_colors[i].filament_preset);
exist_colors.push_back(need_append_colors[i].filament_color);
exist_color_types.push_back(need_append_colors[i].filament_color_type);
exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color);
exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color.empty() ? std::vector<std::string>{need_append_colors[i].filament_color} : need_append_colors[i].mutli_filament_color);
}
}
filament_color->values = exist_colors;
@@ -4072,6 +4080,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
auto exist_colors = filament_color->values;
auto exist_color_types = filament_color_type->values;
auto exist_presets = this->filament_presets;
auto existing_multi_colors = decode_project_multi_colors(exist_colors);
size_t tray_count = ams_filament_presets.size();
size_t total = std::max(tray_count, exist_presets.size());
@@ -4081,25 +4090,34 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
std::vector<std::string> result_presets;
std::vector<std::vector<std::string>> result_multi_colors;
for (size_t i = 0; i < total; i++) {
bool is_loaded = (i < ams_infos.size() && ams_infos[i].valid);
for (size_t i = 0; i < total; ++i) {
const bool is_loaded = i < ams_infos.size() && ams_infos[i].valid;
if (is_loaded) {
// Loaded tray: use tray's filament data
// The printer replaces the project filament at this index.
result_colors.push_back(ams_filament_colors[i]);
result_color_types.push_back(ams_filament_color_types[i]);
result_presets.push_back(ams_filament_presets[i]);
result_multi_colors.push_back(
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
: std::vector<std::string>{ams_filament_colors[i]});
} else if (i < exist_presets.size()) {
// Empty tray or beyond tray count: keep existing filament
if (i < ams_multi_color_filment.size() && !ams_multi_color_filment[i].empty()) {
result_multi_colors.push_back(ams_multi_color_filment[i]);
} else {
// why: old project components belong to the replaced
// filament, so use the new printer filament's main color.
result_multi_colors.push_back({ams_filament_colors[i]});
}
// why: update_multi_material_filament_presets() can grow
// filament_presets alone to the extruder count, so presets
// may outrun the colour arrays; fall through to a generic
// filament rather than read past them.
} else if (i < exist_presets.size() && i < exist_colors.size() && i < exist_color_types.size()) {
// An empty or absent printer slot retains the project filament.
result_colors.push_back(exist_colors[i]);
result_color_types.push_back(exist_color_types[i]);
result_presets.push_back(exist_presets[i]);
result_multi_colors.push_back({exist_colors[i]});
// note: already carries the project components or its
// main-color fallback.
result_multi_colors.push_back(existing_multi_colors[i]);
} else {
// New slot beyond existing count: prefer a generic filament preset
// Neither source has a filament, so create a generic one.
auto it = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) {
return f.is_compatible && f.is_system
&& boost::algorithm::starts_with(f.name, "Generic ");
@@ -4173,23 +4191,28 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
void PresetBundle::update_filament_multi_color()
{
std::vector<std::string> exsit_multi_colors;
for (auto &fil_item : ams_multi_color_filment){
if (fil_item.empty()) break;
if (fil_item.size() == 1)
exsit_multi_colors.push_back(fil_item[0]);
else {
std::string colors = "";
for (auto &color : fil_item){
colors += color + " ";
}
colors.erase(colors.size() - 1); // remove last space
exsit_multi_colors.push_back(colors);
const ConfigOptionStrings *filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *filament_multi_colour = project_config.option<ConfigOptionStrings>("filament_multi_colour");
// note: cheap defence only. Both keys live in s_project_options and the
// constructor applies them unconditionally, so this never fires today.
if (!filament_color || !filament_multi_colour) return;
// note: this is std::vector::resize(), which safely creates empty inner
// vectors. Not ConfigOptionStrings::resize(), which throws without a
// default value.
ams_multi_color_filment.resize(filament_color->values.size());
for (size_t i = 0; i < filament_color->values.size(); ++i) {
if (ams_multi_color_filment[i].empty()) {
// why: every final main color requires one parallel
// multi-color entry.
ams_multi_color_filment[i] = {filament_color->values[i]};
}
}
ConfigOptionStrings *filament_multi_colour = project_config.option<ConfigOptionStrings>("filament_multi_colour");
filament_multi_colour->resize(exsit_multi_colors.size());
filament_multi_colour->values = exsit_multi_colors;
std::vector<std::string> serialized;
serialized.reserve(ams_multi_color_filment.size());
for (const auto &components : ams_multi_color_filment)
serialized.push_back(boost::algorithm::join(components, " "));
// why: assignment sets both size and contents, so no resize is needed.
filament_multi_colour->values = std::move(serialized);
}
std::vector<int> PresetBundle::get_used_tpu_filaments(const std::vector<int> &used_filaments)
+15 -6
View File
@@ -226,6 +226,7 @@ set(SLIC3R_GUI_SOURCES
GUI/GLToolbar.hpp
GUI/ImageDPIFrame.cpp
GUI/ImageDPIFrame.hpp
GUI/IMediaController.hpp
GUI/GUI_App.cpp
GUI/GUI_App.hpp
GUI/GUI_AuxiliaryList.cpp
@@ -347,6 +348,8 @@ set(SLIC3R_GUI_SOURCES
GUI/MediaFilePanel.h
GUI/MediaPlayCtrl.cpp
GUI/MediaPlayCtrl.h
GUI/WebRtcMediaController.cpp
GUI/WebRtcMediaController.hpp
GUI/MeshUtils.cpp
GUI/MeshUtils.hpp
GUI/ModelMall.cpp
@@ -536,6 +539,8 @@ set(SLIC3R_GUI_SOURCES
GUI/WebUserLoginDialog.hpp
GUI/WebViewDialog.cpp
GUI/WebViewDialog.hpp
GUI/WebMediaController.hpp
GUI/WebMediaController.cpp
GUI/Widgets/AMSControl.cpp
GUI/Widgets/AMSControl.hpp
GUI/Widgets/AMSItem.cpp
@@ -726,6 +731,7 @@ set(SLIC3R_GUI_SOURCES
Utils/NetworkAgentFactory.cpp
Utils/ICloudServiceAgent.hpp
Utils/IPrinterAgent.hpp
Utils/ICameraSignalingChannel.hpp
Utils/OrcaCloudServiceAgent.cpp
Utils/OrcaCloudServiceAgent.hpp
Utils/OrcaPrinterAgent.cpp
@@ -870,7 +876,7 @@ else()
set(_opengl_link_lib OpenGL::GL)
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")
# Linux finds wxWidgets in module mode, whose include dirs and definitions
@@ -927,29 +933,32 @@ endif ()
if (APPLE)
# 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(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)
if (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.")
if (NOT LIBAVFORMAT_LIBRARY OR NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
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 ()
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)
elseif (WIN32)
# 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
# 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(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)
if (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.")
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)
else ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavformat
libavcodec
libswscale
libavutil
+27
View File
@@ -45,6 +45,25 @@ int AVVideoDecoder::open(Bambu_StreamInfo const &info)
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 ret = -1;
@@ -71,6 +90,14 @@ int AVVideoDecoder::decode(const Bambu_Sample &sample)
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 ret = avcodec_send_packet(codec_ctx_, nullptr);
+10
View File
@@ -23,8 +23,10 @@ public:
public:
int open(Bambu_StreamInfo const &info);
int open(AVCodecParameters const &parameters);
int decode(Bambu_Sample const &sample);
int decode(AVPacket const &packet);
int flush();
@@ -34,6 +36,14 @@ public:
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:
AVCodecContext *codec_ctx_ = 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 = dev->get_my_machine_list();
m_bind_machine_list = dev->get_my_machine_list(dev->get_current_printer_agent_id());
//sort list
std::vector<std::pair<std::string, MachineObject*>> user_machine_list;
+3 -12
View File
@@ -8,6 +8,7 @@
#include "libslic3r/Print.hpp"
#include "DeviceCore/DevConfig.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevExtruderSystem.h"
#include "DeviceCore/DevFilaBlackList.h"
#include "DeviceCore/DevFilaSystem.h"
@@ -1647,18 +1648,8 @@ bool CalibrationPresetPage::is_blocking_printing()
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;
const auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
bool CalibrationPresetPage::is_nozzle_info_synced() const
+1 -61
View File
@@ -28,7 +28,6 @@ wxEND_EVENT_TABLE()
wxDEFINE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent);
wxDEFINE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent);
wxDEFINE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent);
#define CAMERAPOPUP_CLICK_INTERVAL 20
@@ -82,7 +81,7 @@ CameraPopup::CameraPopup(wxWindow *parent)
top_sizer->Add(m_text_liveview_retry, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5));
top_sizer->Add(m_switch_liveview_retry, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5));
m_switch_liveview_retry->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &e) {
m_switch_liveview_retry->Bind(wxEVT_TOGGLEBUTTON, [](wxCommandEvent &e) {
wxGetApp().app_config->set("liveview", "auto_retry", e.IsChecked());
e.Skip();
});
@@ -102,34 +101,6 @@ CameraPopup::CameraPopup(wxWindow *parent)
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));
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);
}
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)
{
if (!m_obj) return;
+4 -9
View File
@@ -15,14 +15,12 @@
#include "Widgets/SwitchButton.hpp"
#include "Widgets/RadioBox.hpp"
#include "Widgets/PopupWindow.hpp"
#include "Widgets/TextInput.hpp"
namespace Slic3r {
namespace GUI {
wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent);
wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent);
wxDECLARE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent);
class CameraPopup : public PopupWindow
{
@@ -53,9 +51,6 @@ protected:
void on_switch_recording(wxCommandEvent& event);
void on_set_resolution();
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);
void select_curr_radiobox(int btn_idx);
@@ -76,10 +71,10 @@ private:
wxStaticText* m_text_liveview_retry;
SwitchButton* m_switch_liveview_retry;
#endif //BBL_RELEASE_TO_PUBLIC
wxStaticText* m_custom_camera_hint;
TextInput* m_custom_camera_input;
Button* m_custom_camera_input_confirm;
bool m_custom_camera_enabled{ false };
// wxStaticText* m_custom_camera_hint;
// TextInput* m_custom_camera_input;
// Button* m_custom_camera_input_confirm;
// bool m_custom_camera_enabled{ false };
wxStaticText* m_text_resolution;
wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM];
wxScrolledWindow *m_panel;
+3 -1
View File
@@ -35,7 +35,9 @@ ConnectPrinterDialog::ConnectPrinterDialog(wxWindow *parent, wxWindowID id, cons
sizer_connect = new wxBoxSizer(wxHORIZONTAL);
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->SetCornerRadius(FromDIP(5));
m_textCtrl_code->SetSize(wxSize(FromDIP(330), FromDIP(40)));
+16 -1
View File
@@ -1,5 +1,7 @@
#include "DevConfigUtil.h"
#include "slic3r/GUI/DeviceManager.hpp"
#include <wx/dir.h>
#include <boost/filesystem/operations.hpp>
#include "../I18N.hpp"
@@ -41,6 +43,19 @@ static void _toolhead_translation_markers()
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()
{
@@ -405,4 +420,4 @@ std::string DevPrinterConfigUtil::get_toolhead_display_name(
return result;
}
};
};
+7 -1
View File
@@ -25,6 +25,8 @@
namespace Slic3r
{
class MachineObject;
/// Toolhead component type (extruder / nozzle / hotend)
enum class ToolHeadComponent {
Extruder,
@@ -59,6 +61,10 @@ public:
/*printer*/
// info
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_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"); }
@@ -227,4 +233,4 @@ static std::string _parse_printer_type(const std::string &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;
}
if (tray_it->contains("tray_slot_placeholder")) {
curr_tray->is_slot_placeholder = true;
}
// The tray objects are reused across status updates. Reset this
// 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 tray_id_int = 0;
try
@@ -999,4 +999,4 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
}
}
}
}
+89 -24
View File
@@ -3,6 +3,7 @@
#include <exception>
#include "DevManager.h"
#include "CloudProvider.hpp"
#include "DevUtil.h"
// TODO: remove this include
@@ -14,6 +15,8 @@
#include "libslic3r/Time.hpp"
#include "IPrinterAgent.hpp"
using namespace nlohmann;
namespace {
@@ -264,6 +267,10 @@ namespace Slic3r
/* update userMachineList info */
auto it = userMachineList.find(dev_id);
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 ||
it->second->bind_state != bind_state ||
it->second->bind_sec_link != sec_link ||
@@ -294,6 +301,9 @@ namespace Slic3r
// update properties
/* ip changed */
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 ( connection_name.empty() ) {
@@ -405,6 +415,9 @@ namespace Slic3r
auto it = localMachineList.find(machine.dev_id);
if (it != localMachineList.end()) {
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 {
obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip);
obj->printer_agent_id = get_current_printer_agent_id();
@@ -497,7 +510,7 @@ namespace Slic3r
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);
if (it != list.end())
{
@@ -534,25 +547,15 @@ namespace Slic3r
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"
// Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own.
//
// Also drop "My Devices" stamped by a different agent than the one we're swapping to
// (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();
// Device entries are now scoped by printer_agent_id when they are presented. Keep
// 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.
// Entries without an owner are legacy/unscoped and cannot safely be shown.
for (auto it = localMachineList.begin(); it != localMachineList.end();)
{
const bool is_my_device = my.find(it->first) != my.end();
const bool agent_mismatch = !target_agent_id.empty() && it->second &&
it->second->printer_agent_id != target_agent_id;
if (!is_my_device || agent_mismatch)
if (!it->second || it->second->printer_agent_id.empty())
{
delete it->second;
it = localMachineList.erase(it);
@@ -568,8 +571,22 @@ namespace Slic3r
{
BOOST_LOG_TRIVIAL(info) << "set_selected_machine=" << dev_id
<< " 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);
BOOST_LOG_TRIVIAL(info) << "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(info) << "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 {
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
auto last_selected = my_machine_list.find(selected_machine);
@@ -580,7 +597,9 @@ namespace Slic3r
m_agent->disconnect_printer();
}
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(info) << "Orca diagnostic: cleared previous cloud selection dev_id="
<< selected_machine << " result=" << result;
}
}
@@ -632,7 +651,9 @@ namespace Slic3r
{
// diff dev_id, cloud => set_user_selected_machine(new)
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(info) << "Orca diagnostic: set new cloud selection dev_id="
<< dev_id << " result=" << result;
it->second->reset();
}
else
@@ -660,6 +681,8 @@ namespace Slic3r
selected_machine = dev_id;
record_user_last_machine(selected_machine);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: DeviceManager selection complete selected_machine="
<< selected_machine;
return true;
}
@@ -690,7 +713,9 @@ namespace Slic3r
dev_list.push_back(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(info) << "Orca diagnostic: add_user_subscribe count=" << dev_list.size()
<< " result=" << result;
}
@@ -703,7 +728,9 @@ namespace Slic3r
dev_list.push_back(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(info) << "Orca diagnostic: del_user_subscribe count=" << dev_list.size()
<< " result=" << result;
}
void DeviceManager::subscribe_device_list(std::vector<std::string> dev_list)
@@ -822,7 +849,23 @@ namespace Slic3r
try
{
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 provider = j.contains("provider") ? j["provider"].get<std::string>()
: GUI::wxGetApp().get_printer_cloud_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 != GUI::wxGetApp().get_printer_cloud_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
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << j;
@@ -845,11 +888,14 @@ namespace Slic3r
/* update field */
obj = iter->second;
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
{
obj = new MachineObject(this, m_agent, "", "", "");
obj->printer_agent_id = get_current_printer_agent_id();
obj->printer_agent_id = agent_id;
if (m_agent)
{
obj->set_bind_status(m_agent->get_user_name(provider));
@@ -864,6 +910,12 @@ namespace Slic3r
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())
obj->set_dev_id(elem["dev_id"].get<std::string>());
if (!elem["dev_name"].is_null())
@@ -895,6 +947,12 @@ namespace Slic3r
acc_code.erase(std::remove(acc_code.begin(), acc_code.end(), '\n'), acc_code.end());
obj->set_access_code(acc_code);
}
BOOST_LOG_TRIVIAL(info) << "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
@@ -910,6 +968,9 @@ namespace Slic3r
iterat++;
}
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: parse_user_print_info complete provider=" << provider
<< " parsed_count=" << new_list.size()
<< " stored_count=" << userMachineList.size();
}
}
catch (std::exception& e)
@@ -926,10 +987,14 @@ namespace Slic3r
unsigned int http_code;
std::string body;
int result = m_agent->get_user_print_info(&http_code, &body, provider);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: get_user_print_info provider=" << provider
<< " result=" << result << " http_code=" << http_code
<< " body_bytes=" << body.size();
if (result == 0)
{
// 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.
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: queueing parse_user_print_info on UI thread";
Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); });
}
}
+4 -5
View File
@@ -74,10 +74,9 @@ public:
void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); }
void clean_user_info(bool keep_local_selection = false);
// target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check,
// just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the
// live one - this runs before the live agent is repointed.
void clear_other_devices(const std::string& target_agent_id = "");
// Retain agent-owned LAN discoveries across a switch; the active-agent list filter keeps
// entries from other agents hidden while allowing them to reappear when switched back.
void clear_other_devices();
void load_last_machine();
void update_user_machine_list_info(const std::string& provider);
@@ -154,4 +153,4 @@ public:
protected:
virtual void on_timer(wxTimerEvent& event);
};
};
};
+191 -83
View File
@@ -53,6 +53,7 @@
#include "DeviceCore/DevStatus.h"
#include "DeviceCore/DevUpgrade.h"
#include "IPrinterAgent.hpp"
#define CALI_DEBUG
#define MINUTE_30 1800000 //ms
@@ -373,8 +374,22 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str)
wxString MachineObject::get_printer_type_display_str() const
{
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())
return display_name;
else if (printer_type == "orcasonar")
return "OrcaSonar Printer";
else
return _L("Unknown");
}
@@ -1341,7 +1356,6 @@ int MachineObject::command_get_access_code() {
return this->publish_json(j);
}
int MachineObject::command_request_push_all(bool request_now)
{
auto curr_time = std::chrono::system_clock::now();
@@ -1473,26 +1487,29 @@ int MachineObject::command_upgrade_module(std::string url, std::string module_ty
int MachineObject::command_xyz_abs()
{
return this->publish_gcode("G90 \n");
if (!m_agent) return -1;
int rtn = m_agent->command_xyz_abs(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_auto_leveling()
{
return this->publish_gcode("G29 \n");
if (!m_agent) return -1;
int rtn = m_agent->command_auto_leveling(get_dev_id(), MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_go_home()
{
if (m_support_mqtt_homing)
{
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");
if (!m_agent) return -1;
int rtn = m_agent->command_go_home(get_dev_id(), this->is_in_printing(), m_support_mqtt_homing, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_task_partskip(std::vector<int> part_ids)
@@ -1614,23 +1631,20 @@ int MachineObject::command_stop_buzzer()
int MachineObject::command_set_bed(int temp)
{
if (m_support_mqtt_bet_ctrl)
{
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);
if (!m_agent) return -1;
int rtn = m_agent->command_set_bed(get_dev_id(), temp, m_support_mqtt_bet_ctrl, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_set_nozzle(int temp)
{
std::string gcode_str = (boost::format("M104 S%1%\n") % temp).str();
return this->publish_gcode(gcode_str);
if (!m_agent) return -1;
int rtn = m_agent->command_set_nozzle(get_dev_id(), temp, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_set_nozzle_new(int nozzle_id, int temp)
@@ -1735,9 +1749,11 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
int MachineObject::command_ams_calibrate(int ams_id)
{
std::string gcode_cmd = (boost::format("M620 C%1% \n") % ams_id).str();
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd;
return this->publish_gcode(gcode_cmd);
if (!m_agent) return -1;
int rtn = m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
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)
@@ -1775,9 +1791,11 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
int MachineObject::command_ams_refresh_rfid(std::string tray_id)
{
std::string gcode_cmd = (boost::format("M620 R%1% \n") % tray_id).str();
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd;
return this->publish_gcode(gcode_cmd);
if (!m_agent) return -1;
int rtn = m_agent->command_ams_refresh_rfid(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id)
@@ -1790,12 +1808,20 @@ int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id)
return this->publish_json(j);
}
int MachineObject::command_start_camera()
{
if (!m_agent) return -1;
return m_agent->command_start_camera(get_dev_id());
}
int MachineObject::command_ams_select_tray(std::string tray_id)
{
std::string gcode_cmd = (boost::format("M620 P%1% \n") % tray_id).str();
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd;
return this->publish_gcode(gcode_cmd);
if (!m_agent) return -1;
int rtn = m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_ams_control(std::string action)
@@ -1954,47 +1980,12 @@ 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)
{
if (m_support_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.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);
if (!m_agent) return -1;
int rtn = m_agent->command_axis_control(get_dev_id(), axis, unit, input_val, speed, is_core_xy(),
m_support_mqtt_axis_control, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_extruder_control(int nozzle_id, double val)
@@ -2619,7 +2610,12 @@ void MachineObject::reset()
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;
m_partskip_ids.clear();
}
@@ -2629,10 +2625,24 @@ void MachineObject::set_print_state(std::string status)
print_status = status;
}
// why: printer agents can report progress without BBL cloud task identity.
void MachineObject::update_print_progress(const json& value)
{
if (value.is_string())
mc_print_percent = stoi(value.get<std::string>());
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(bool use_openssl)
{
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();
if (m_agent) {
@@ -2742,6 +2752,14 @@ int MachineObject::publish_json(const json& json_item, int qos, int flag)
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;
}
@@ -3046,6 +3064,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
}
} 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 {
if (auto ptr = m_fila_system->GetAmsFirmwareSwitch().lock()) {
ptr->ParseFirmwareSwitch(j);
@@ -3298,10 +3323,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
print_type = jj["print_type"].get<std::string>();
}
if (jj.contains("mc_percent")) {
if (jj["mc_percent"].is_string())
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>();
update_print_progress(jj["mc_percent"]);
}
if (jj.contains("mc_print_sub_stage")) {
if (jj["mc_print_sub_stage"].is_number_integer())
@@ -3471,6 +3493,9 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
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")) {
int jobAttr = jj["job_attr"].get<int>();
jobState_ = get_flag_bits(jobAttr, 4, 4);
@@ -3516,7 +3541,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);
BBLSubTask* curr_task = get_subtask();
if (curr_task) {
curr_task->task_progress = mc_print_percent;
curr_task->printing_status = print_status;
curr_task->task_id = jj["subtask_id"].get<std::string>();
}
@@ -3819,6 +3843,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
has_ipcam = true;
} else {
has_ipcam = false;
webcam_stream_url.clear();
}
}
if (ipcam.contains("resolution")) {
@@ -3853,6 +3878,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"
? 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")) {
tutk_state = ipcam["tutk_server"].get<std::string>();
}
@@ -5432,6 +5460,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) {
return std::isxdigit(static_cast<unsigned char>(c)) != 0;
}
+12 -25
View File
@@ -18,6 +18,7 @@
#include "boost/bimap/bimap.hpp"
#include "libslic3r/calib.hpp"
#include "libslic3r/Utils.hpp"
#include "slic3r/Utils/PrinterNetworkTypes.hpp"
#include "DeviceCore/DevDefs.h"
#include "DeviceCore/DevConfigUtil.h"
@@ -100,7 +101,6 @@ struct DevPrintTaskRatingInfo;
// 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);
class MachineObject
{
private:
@@ -307,7 +307,7 @@ public:
bool ams_support_virtual_tray { true };
time_t ams_user_setting_start = 0;
time_t ams_switch_filament_start = 0;
AmsStatusMain ams_status_main;
AmsStatusMain ams_status_main = AmsStatusMain::AMS_STATUS_MAIN_IDLE;
int ams_status_sub;
int ams_version = 0;
@@ -547,30 +547,12 @@ public:
bool xcam_first_layer_inspector { false };
time_t xcam_first_layer_hold_start = 0;
std::string local_rtsp_url;
std::string webcam_stream_url;
std::string tutk_state;
enum LiveviewLocal {
LVL_None,
LVL_Disable,
LVL_Local,
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 };
LiveviewLocal liveview_local{ LiveviewLocal::LVL_None };
LiveviewRemote liveview_remote{ LiveviewRemote::LVR_None};
FileLocal file_local{ FileLocal::FL_None };
FileRemote file_remote{ FileRemote::FR_None };
enum PlateMakerDectect : int
{
@@ -708,6 +690,8 @@ public:
std::string subtask_id_;
std::string job_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};
boost::thread* get_slice_info_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_nozzle2(int id, std::string nozzle_type, float diameter);
int command_get_access_code();
int command_start_camera();
int command_ack_proceed(json& proceed);
int command_purification_disable();
int command_dont_remind_next_time(json& mqtt_guard_json);
@@ -894,6 +879,7 @@ public:
static bool is_in_printing_status(std::string status);
void set_print_state(std::string status);
void update_print_progress(const json& value);
bool is_connected();
bool is_connecting();
@@ -961,6 +947,7 @@ public:
/*for parse new info*/
bool check_enable_np(const json& print) const;
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;
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;
+20 -35
View File
@@ -3,6 +3,7 @@
#include "libslic3r/Technologies.hpp"
#include "libslic3r/Platform.hpp"
#include "GUI_App.hpp"
#include "DeviceCore/DevConfigUtil.h"
#include "BindDialog.hpp"
#include "DeviceManager.hpp"
#include "HMS.hpp"
@@ -23,7 +24,6 @@
#include <boost/locale/encoding_utf.hpp>
#include <boost/log/detail/native_typeof.hpp>
#include <libslic3r/Config.hpp>
#include <mutex>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/event.h>
@@ -40,9 +40,11 @@
#include <iterator>
#include <exception>
#include <cstdlib>
#include <mutex>
#include <regex>
#include <thread>
#include <string_view>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>
@@ -87,7 +89,6 @@
#include "libslic3r/Thread.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Color.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/host/PluginHostUi.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
@@ -107,19 +108,18 @@
#include "../Utils/PrintHost.hpp"
#include "../Utils/Process.hpp"
#include "../Utils/wxInspectorPlugins/Registration.hpp"
#include "../Utils/MacDarkMode.hpp"
#include "../Utils/Http.hpp"
#include "../Utils/InstanceID.hpp"
#include "../Utils/UndoRedo.hpp"
#include "slic3r/Config/Snapshot.hpp"
#include "Preferences.hpp"
#include "Tab.hpp"
#include "SysInfoDialog.hpp"
#include "UpdateDialogs.hpp"
#include "Mouse3DController.hpp"
#include "RemovableDriveManager.hpp"
#include "InstanceCheck.hpp"
#ifdef __APPLE__
#include "../Utils/MacDarkMode.hpp"
#include "DeepLinkHandlerMac.h"
#endif
#include "NotificationManager.hpp"
@@ -128,8 +128,6 @@
#include "PrintHostDialogs.hpp"
#include "NetworkPluginDialog.hpp"
#include "DesktopIntegrationDialog.hpp"
#include "SendSystemInfoDialog.hpp"
#include "ParamsDialog.hpp"
#include "KBShortcutsDialog.hpp"
#include "DownloadProgressDialog.hpp"
#include "TroubleshootDialog.hpp"
@@ -140,7 +138,6 @@
#include "Widgets/ProgressDialog.hpp"
//BBS: DailyTip and UserGuide Dialog
#include "WebDownPluginDlg.hpp"
#include "WebGuideDialog.hpp"
#include "ReleaseNote.hpp"
#include "PrivacyUpdateDialog.hpp"
@@ -2385,14 +2382,8 @@ 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_)
@@ -2403,14 +2394,7 @@ bool GUI_App::is_blocking_printing(MachineObject *obj_)
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
std::string 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;
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
// If formatted for github, plaintext with OpenGL extensions enclosed into <details>.
@@ -3961,20 +3945,16 @@ void GUI_App::set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent)
m_agent->set_user_selected_machine("");
// note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer)
dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS
// why: drop stale LAN discoveries; keep My Devices, but only those belonging to the
// agent we're about to swap to, so a device stamped by the outgoing agent doesn't
// linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh.
// 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());
// why: retain agent-owned LAN discoveries so agents without automatic discovery (for
// example the Moonraker-based Qidi/Snapmaker agents) can reuse them after a switch.
dev->clear_other_devices();
}
m_agent->set_printer_agent(agent);
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())
return stored_id;
@@ -4010,6 +3990,7 @@ void GUI_App::switch_printer_agent()
std::string log_dir = data_dir();
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);
// Create new printer agent via registry
@@ -4023,8 +4004,10 @@ void GUI_App::switch_printer_agent()
return;
}
// The factory caches agents per ID, so an identical pointer means the agent type is unchanged.
if (m_agent->get_printer_agent() == new_printer_agent) {
// Compare the registered IDs, not only the implementation pointer. Different registry IDs
// 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
// 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
@@ -4983,14 +4966,16 @@ bool GUI_App::is_user_login(const std::string& provider/* = ORCA_CLOUD_PROVIDER*
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.
//
return BBL_CLOUD_PROVIDER;
const std::string agent_id = resolve_printer_agent_id(
preset_bundle ? preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent")
: 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 result = false;
+2 -5
View File
@@ -23,9 +23,6 @@
#include <wx/snglinst.h>
#include <wx/msgdlg.h>
#include <mutex>
#include <stack>
//#define BBL_HAS_FIRST_PAGE 1
#define STUDIO_INACTIVE_TIMEOUT 15*60*1000
#define LOG_FILES_MAX_NUM 30
@@ -369,7 +366,7 @@ public:
// Reconcile the live printer agent with the stored preset selection.
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
// then, all resolve and canonical would just be ORCA<->""
std::string canonical_printer_agent_id(const std::string& picked_id);
@@ -495,7 +492,7 @@ public:
bool check_login(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);
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_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <wx/mediactrl.h>
#include <wx/uri.h>
#include <memory>
#include <slic3r/Utils/IPrinterAgent.hpp>
namespace Slic3r { namespace GUI {
class IMediaController
{
public:
virtual ~IMediaController() = default;
virtual void Load(wxURI url) = 0;
// The default keeps existing media controllers unaware of camera-specific modes.
virtual void Load(wxURI url, CameraStreamMode mode)
{
(void) mode;
Load(url);
}
virtual void Play() = 0;
virtual void Stop() = 0;
virtual wxMediaState GetState() { return wxMediaState{}; }
virtual int GetLastError() const { return {}; };
virtual wxSize GetVideoSize() const { return {}; };
virtual void StartSession(std::unique_ptr<ICameraSignalingChannel> channel)
{
(void) channel;
}
virtual void StopSession() {}
private:
};
}} // namespace Slic3r::GUI
+2 -1
View File
@@ -14,6 +14,7 @@
#include "slic3r/Utils/FileTransferUtils.hpp"
#include "slic3r/Utils/BBLNetworkPlugin.hpp"
#include "NetworkAgent.hpp"
namespace Slic3r {
namespace GUI {
@@ -203,7 +204,7 @@ void PrintJob::process(Ctl &ctl)
params.dev_ip = m_dev_ip;
params.use_ssl_for_ftp = m_local_use_ssl_for_ftp;
params.use_ssl_for_mqtt = m_local_use_ssl;
params.username = "bblp";
params.username = m_agent->default_lan_username();
params.password = m_access_code;
// check access code and ip address
+2 -2
View File
@@ -126,7 +126,7 @@ void SendJob::process(Ctl &ctl)
if (m_is_check_mode) {
PrintParams verify_params;
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.use_ssl_for_ftp = m_local_use_ssl_for_ftp;
verify_params.use_ssl_for_mqtt = m_local_use_ssl;
@@ -211,7 +211,7 @@ void SendJob::process(Ctl &ctl)
// local print access
params.dev_ip = m_dev_ip;
params.username = "bblp";
params.username = agent->default_lan_username();
params.password = m_access_code;
params.use_ssl_for_ftp = m_local_use_ssl_for_ftp;
params.use_ssl_for_mqtt = m_local_use_ssl;
+1 -1
View File
@@ -4311,7 +4311,7 @@ void MainFrame::load_printer_url()
if (auto *device_manager = wxGetApp().getDeviceManager()) {
auto *machine = device_manager->get_selected_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)
machine = machines.begin()->second;
}
+263 -18
View File
@@ -7,6 +7,7 @@
#include "DeviceManager.hpp"
#include "DeviceCore/DevConfigUtil.h"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/AppConfig.hpp"
#include "I18N.hpp"
@@ -15,11 +16,14 @@
#include "slic3r/Utils/BBLNetworkPlugin.hpp"
#include <algorithm>
#include <boost/lexical_cast.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/cstdio.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/nowide/utf8_codecvt.hpp>
#include <slic3r/GUI/DeviceManager.hpp>
#undef pid_t
#include <boost/process.hpp>
#ifdef __WIN32__
@@ -139,6 +143,11 @@ MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const w
MediaPlayCtrl::~MediaPlayCtrl()
{
m_webrtc_stopping = true;
if (m_webrtc_ctrl)
m_webrtc_ctrl->StopSession();
m_media_ctrl->EndExternalStream();
m_webrtc_stopping = false;
{
boost::unique_lock lock(m_mutex);
m_tasks.push_back("<exit>");
@@ -151,8 +160,77 @@ MediaPlayCtrl::~MediaPlayCtrl()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << this;
}
void MediaPlayCtrl::SetWebMediaController(IMediaController *ctrl)
{
m_web_ctrl = ctrl;
}
CameraStreamMode MediaPlayCtrl::current_mode() const
{
auto agent = wxGetApp().getAgent();
return agent ? agent->get_camera_stream_mode() : CameraStreamMode::none;
}
void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
{
const CameraStreamMode mode = current_mode();
if (mode != m_last_mode) {
if (m_last_state != MEDIASTATE_IDLE) {
m_failed_code = 0; // a mode switch is not a stream failure - don't arm back-off
Stop(" ");
}
m_last_mode = mode;
}
switch (mode) {
case CameraStreamMode::http:
case CameraStreamMode::https:
case CameraStreamMode::http_snapshot:
case CameraStreamMode::rtsp: {
std::string machine = obj ? obj->get_dev_id() : "";
auto agent = wxGetApp().getAgent();
std::string url = agent ? agent->get_local_camera_stream_url() : "";
m_camera_exists = !url.empty();
Enable(obj && m_camera_exists);
bool changed = machine != m_machine || url != m_agent_camera_url;
m_machine = machine;
m_agent_camera_url = url;
m_url = from_u8(url);
if (!changed) {
return;
}
// A genuine machine/URL switch: not a failure, so drop any pending
// failure back-off before (re)starting on the new target.
m_web_user_stopped = false;
m_failed_code = 0;
m_failed_retry = 0;
m_next_retry = wxDateTime();
if (m_last_state != MEDIASTATE_IDLE)
Stop(" ");
return;
}
case CameraStreamMode::webrtc: {
std::string machine = obj ? obj->get_dev_id() : "";
m_camera_exists = obj != nullptr;
Enable(obj != nullptr);
const bool 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_machine = machine;
m_url.clear();
m_agent_camera_url.clear();
if (!changed) {
return;
}
m_web_user_stopped = false;
if (m_last_state != MEDIASTATE_IDLE)
Stop(" ");
return;
}
default:
break;
}
std::string machine = obj ? obj->get_dev_id() : "";
if (obj) {
m_camera_exists = obj->has_ipcam;
@@ -167,12 +245,12 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
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;
m_remote_proto = LiveviewRemote::LVR_None;
}
} else {
m_camera_exists = false;
m_lan_mode = false;
m_lan_proto = MachineObject::LVL_None;
m_lan_proto = LiveviewLocal::LVL_None;
m_lan_ip.clear();
m_lan_passwd.clear();
m_dev_ver.clear();
@@ -182,8 +260,6 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
}
Enable(obj && obj->is_info_ready() && obj->m_push_count > 0);
if (machine == m_machine) {
if (m_last_state == MEDIASTATE_IDLE && IsEnabled())
Play();
return;
}
m_machine = machine;
@@ -254,6 +330,90 @@ void refresh_agora_url(char const* device, char const* dev_ver, char const* chan
void MediaPlayCtrl::Play()
{
switch (current_mode()) {
case CameraStreamMode::http_snapshot:
if (!m_next_retry.IsValid() || wxDateTime::Now() < m_next_retry)
return;
if (!IsShownOnScreen()) return;
if (m_last_state != MEDIASTATE_IDLE) return;
if (m_machine.empty() || !IsEnabled() || !m_camera_exists || m_url.IsEmpty() || !m_web_ctrl) {
Stop(_L("Please confirm if the printer is connected."));
return;
}
m_button_play->SetIcon("media_stop");
m_web_ctrl->Load(wxURI(m_url), current_mode());
m_web_ctrl->Play();
m_last_state = wxMEDIASTATE_PLAYING;
SetStatus(_L("Playing..."), false);
return;
case CameraStreamMode::http:
case CameraStreamMode::https:
case CameraStreamMode::rtsp:
if (m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry)
return;
if (!IsShownOnScreen()) return;
if (m_last_state != MEDIASTATE_IDLE) return;
m_failed_code = 0;
if (m_machine.empty() || !IsEnabled() || !m_camera_exists || m_url.IsEmpty()) {
Stop(_L("Please confirm if the printer is connected."));
return;
}
m_button_play->SetIcon("media_stop");
load();
return;
case CameraStreamMode::webrtc: {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play webrtc: last_state=" << m_last_state
<< " next_retry_valid=" << m_next_retry.IsValid()
<< " next_retry_future=" << (m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry)
<< " failed_retry=" << m_failed_retry << " shown=" << IsShownOnScreen();
if (m_webrtc_ctrl && m_webrtc_ctrl->is_active()) {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play webrtc: session already active, ignoring";
return;
}
if (m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry)
return;
if (!IsShownOnScreen() || m_last_state != MEDIASTATE_IDLE)
return;
m_failed_code = 0;
if (m_machine.empty() || !IsEnabled() || !m_camera_exists) {
Stop(_L("Please confirm if the printer is connected."));
return;
}
auto agent = wxGetApp().getAgent();
auto channel = agent ? agent->create_camera_signaling_channel(m_machine) : nullptr;
if (!channel) {
Stop(_L("Sign in to OrcaCloud to view the camera."));
return;
}
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_button_play->SetIcon("media_stop");
m_media_ctrl->BeginExternalStream();
m_last_state = MEDIASTATE_INITIALIZING;
SetStatus(_L("Initializing..."), false);
m_webrtc_stopping = false;
m_webrtc_ctrl->StartSession(std::move(channel));
m_webrtc_epoch = m_webrtc_ctrl->epoch();
return;
}
default: // assumed to be CameraStreamMode::none
if (NetworkAgent* agent = wxGetApp().getAgent()) {
if (auto printer_agent = agent->get_printer_agent()) {
if (printer_agent->get_agent_info().id != BBL_PRINTER_AGENT_ID) {
return;
}
}
}
break;
}
if (!m_next_retry.IsValid() || wxDateTime::Now() < m_next_retry)
return;
if (!IsShownOnScreen())
@@ -283,14 +443,14 @@ void MediaPlayCtrl::Play()
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() : "";
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
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;
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";
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 += "&device=" + m_machine;
url += "&net_ver=" + agent_version;
@@ -312,8 +472,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_None (x)
if (m_lan_proto <= MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto)) {
Stop(m_lan_proto == MachineObject::LVL_None
if (m_lan_proto <= LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto)) {
Stop(m_lan_proto == LiveviewLocal::LVL_None
? _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."));
return;
@@ -381,8 +541,70 @@ void MediaPlayCtrl::Play()
void start_ping_test();
void MediaPlayCtrl::StopWebStream()
{
if (m_last_state == MEDIASTATE_IDLE)
return;
if (m_web_ctrl)
m_web_ctrl->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)
{
const bool webrtc_active = m_webrtc_ctrl && m_last_mode == CameraStreamMode::webrtc;
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_webrtc_stopping = true;
m_webrtc_ctrl->StopSession();
m_media_ctrl->EndExternalStream();
m_webrtc_stopping = false;
}
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_web_ctrl) m_web_ctrl->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_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;
if (m_last_state != MEDIASTATE_IDLE) {
@@ -454,15 +676,38 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
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 StartSession 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()
{
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::TogglePlay";
if (m_last_state != MEDIASTATE_IDLE) {
m_next_retry = wxDateTime();
m_web_user_stopped = true;
Stop();
} else {
m_failed_retry = 0;
m_user_triggered = true;
m_web_user_stopped = false;
if (m_last_user_play + wxTimeSpan::Minutes(5) < wxDateTime::Now()) {
m_last_failed_codes.clear();
m_last_user_play = wxDateTime::Now();
@@ -528,13 +773,13 @@ void MediaPlayCtrl::ToggleStream()
wxGetApp().app_config->set("not_show_vcamera_stop_prev", "1");
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;
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;
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";
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 += "&device=" + into_u8(m_machine);
url += "&dev_ver=" + m_dev_ver;
@@ -666,7 +911,8 @@ void MediaPlayCtrl::load()
{
m_last_state = MEDIASTATE_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_info = data_dir() + "/video.info";
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl dump video to " << file_h264;
@@ -686,9 +932,8 @@ void MediaPlayCtrl::on_show_hide(wxShowEvent &evt)
evt.Skip();
if (m_isBeingDeleted) return;
m_failed_retry = 0;
if (m_next_retry.IsValid()) // Try open 2 seconds later, to avoid quick play/stop
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(2);
IsShownOnScreen() ? Play() : Stop();
if (!IsShownOnScreen())
Stop();
}
void MediaPlayCtrl::media_proc()
+17
View File
@@ -9,6 +9,9 @@
#define MediaPlayCtrl_h
#include "wxMediaCtrl3.h"
#include "IMediaController.hpp"
#include "WebRtcMediaController.hpp"
#include "slic3r/Utils/IPrinterAgent.hpp"
#include <wx/panel.h>
@@ -36,6 +39,10 @@ public:
void SetMachineObject(MachineObject * obj);
void SetWebMediaController(IMediaController *ctrl);
void StopWebStream();
bool IsStreaming() const;
void ToggleStream();
@@ -54,6 +61,7 @@ protected:
void TogglePlay();
void SetStatus(wxString const &msg, bool hyperlink = true);
void on_webrtc_status(WebRtcMediaController::Status status);
private:
void load();
@@ -66,6 +74,8 @@ private:
static bool get_stream_url(std::string *url = nullptr);
CameraStreamMode current_mode() const;
private:
static inline const wxMediaState MEDIASTATE_IDLE = static_cast<wxMediaState>(3);
static inline const wxMediaState MEDIASTATE_INITIALIZING = static_cast<wxMediaState>(4);
@@ -76,6 +86,13 @@ private:
std::shared_ptr<int> m_token = std::make_shared<int>(0);
wxMediaCtrl3 * m_media_ctrl;
IMediaController * m_web_ctrl = nullptr;
std::unique_ptr<WebRtcMediaController> m_webrtc_ctrl;
CameraStreamMode m_last_mode = CameraStreamMode::none;
bool m_webrtc_stopping = false;
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;
std::string m_machine;
int m_lan_proto = 0;
+11 -1
View File
@@ -34,6 +34,8 @@
#include "DeviceCore/DevManager.h"
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace GUI {
@@ -259,6 +261,7 @@ void MonitorPanel::msw_rescale()
void MonitorPanel::select_machine(std::string machine_sn)
{
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::select_machine queueing machine_sn=" << machine_sn;
wxCommandEvent *event = new wxCommandEvent(wxEVT_COMMAND_CHOICE_SELECTED);
event->SetString(machine_sn);
wxQueueEvent(this, event);
@@ -276,13 +279,20 @@ void MonitorPanel::on_timer(wxTimerEvent& event)
void MonitorPanel::on_select_printer(wxCommandEvent& event)
{
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
const std::string requested_dev_id = event.GetString().ToStdString();
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::on_select_printer requested_dev_id="
<< requested_dev_id << " device_manager=" << (dev ? "set" : "null");
if (!dev) return;
if ( dev->get_selected_machine() && (dev->get_selected_machine()->get_dev_id() != event.GetString().ToStdString()) && m_hms_panel) {
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(info) << "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;
set_default();
+2 -10
View File
@@ -3,6 +3,7 @@
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "DeviceCore/DevConfigUtil.h"
namespace Slic3r {
namespace GUI {
@@ -108,21 +109,12 @@ 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;
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
void DeviceItem::update_item(const DeviceItem* item)
+1 -1
View File
@@ -159,4 +159,4 @@ private:
void OnApplyDialog(wxCommandEvent &event);
};
}} // namespace Slic3r::GUI
}} // namespace Slic3r::GUI
+89 -24
View File
@@ -2002,12 +2002,14 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
std::string machine_print_name = obj->get_show_printer_type();
PresetBundle *preset_bundle = wxGetApp().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);
if (!machine_preset) {
const bool optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type);
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";
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"
"Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO);
if (dlg.ShowModal() == wxID_NO) {
@@ -2199,6 +2201,11 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
return;
}
if (DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type)) {
clear_all_sync_status();
return;
}
bool printer_synced = false;
// 1. update printer status
const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset();
@@ -5799,11 +5806,30 @@ void Sidebar::load_ams_list(MachineObject* obj)
filament_ams_list = build_filament_ams_list(obj);
}
bool device_change = false;
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;
device_change = true;
}
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)
@@ -5813,9 +5839,27 @@ void Sidebar::load_ams_list(MachineObject* obj)
wxGetApp().preset_bundle->filament_ams_list = filament_ams_list;
for (auto c : p->combos_filament){
c->set_sync_badge(false);
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;
}
}
@@ -5989,18 +6033,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.") :
_L("Filament type and color information have been synchronized, but slot information is not included.");
c->SetToolTip(tip);
c->ShowBadge(true);
c->set_sync_badge(true);
};
{ // badge ams filament
clear_combos_filament_badge();
if (sync_result.direct_sync) {
// Orca: PresetBundle::sync_ams_list rebuilds combos_filament
// 1:1 from the AMS trays that produce a combo (loaded trays + placeholders; non-placeholder
// empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The
// previous per-tray index walked the full filament_ams_list (including the skipped empties),
// so an empty slot before a loaded one dropped the badge for the trailing filaments.
for (auto &c : p->combos_filament) {
badge_combox_filament(c);
// A placeholder contributes a preserved project filament to the
// overwrite result, but it is not AMS-sourced and must not get a
// sync badge. Non-placeholder empty trays are omitted entirely.
size_t combo_index = 0;
for (const auto &entry : wxGetApp().preset_bundle->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;
}
if (is_placeholder) {
p->combos_filament[combo_index]->set_sync_badge(false);
} else {
badge_combox_filament(p->combos_filament[combo_index]);
}
++combo_index;
}
}
}
@@ -6268,6 +6326,11 @@ template<typename T> void setup_dialog_position(T& info)
void Sidebar::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]() {
SyncNozzleAndAmsDialog::InputInfo temp_na_info;
wxPoint big_btn_pt;
@@ -6399,17 +6462,14 @@ void Sidebar::clear_combos_filament_badge()
{
auto &combos_filament = p->combos_filament;
for (auto &c : combos_filament) { // clear flag
c->ShowBadge(false);
c->set_sync_badge(false);
}
}
void Sidebar::udpate_combos_filament_badge() {
auto &combos_filament = p->combos_filament;
for (auto &c : combos_filament) {
auto selection = c->GetSelection();
auto select_flag = c->GetFlag(selection);
auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS;
c->ShowBadge(ok);
c->update_badge_according_flag();
}
}
@@ -12219,7 +12279,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
sidebar->auto_calc_flushing_volumes(idx);
}
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);
}
bool select_preset = !combo->selection_is_changed_according_to_physical_printers();
@@ -20815,9 +20875,14 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning)
}
if (!check_printer_initialized(obj, true, popup_warning))
return false;
Preset * machine_preset = get_printer_preset(obj);
if (!machine_preset)
const std::string machine_model = obj->printer_type;
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;
}
if (wxGetApp().is_blocking_printing()) {
if (popup_warning) {
+18
View File
@@ -11,6 +11,7 @@ namespace Slic3r
// IMPORTANT: ordinal order is the Plugins dialog Status sort priority.
Activated,
Error,
RuntimeError,
Inactive,
Loading
};
@@ -21,11 +22,28 @@ namespace Slic3r
{
case PluginStatus::Activated: return "Activated";
case PluginStatus::Error: return "Error";
case PluginStatus::RuntimeError: return "RuntimeError";
case PluginStatus::Inactive: return "Inactive";
case PluginStatus::Loading: return "Loading";
}
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
+5 -8
View File
@@ -237,6 +237,7 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
payload_item["label"] = dialog_item.display_name;
payload_item["source"] = to_string(dialog_item.source);
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["update_status"] = to_string(dialog_item.update_status);
payload_item["unauthorized"] = dialog_item.unauthorized;
@@ -381,14 +382,7 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
item.sharing_token = descriptor.sharing_token;
item.thumbnail_url = descriptor.thumbnail_url;
if (item.loading)
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.status = resolve_plugin_status(item.loading, item.has_error, item.is_loaded);
item.available_actions = evaluate_action_policy(item);
const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(),
@@ -664,6 +658,9 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
}
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.
if (m_activating_plugin_key == plugin_key)
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 PrintStatusRackNozzleNumUnmeetWarning: return "PrintStatusRackNozzleNumUnmeetWarning";
case PrintStatusHasUnreliableNozzleWarning: return "PrintStatusHasUnreliableNozzleWarning";
case PrintStatusOptionalPrinterModel: return "PrintStatusOptionalPrinterModel";
case PrintStatusWarningExtFilamentNotMatch: return "PrintStatusWarningExtFilamentNotMatch";
case PrintStatusFilamentWarningNozzleHRC: return "PrintStatusFilamentWarningNozzleHRC";
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 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 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 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.");
@@ -379,4 +381,3 @@ bool PrinterMsgPanel::UpdateInfos(const std::vector<prePrintInfo>& infos)
}
};
+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,
// not an error, so non-standard nozzles can still be printed with.
PrintStatusNozzleDiameterMismatch,
PrintStatusOptionalPrinterModel,
PrintStatusPrinterWarningEnd,
// Warnings for filament
+11 -3
View File
@@ -361,7 +361,8 @@ wxString PresetComboBox::get_preset_item_name(unsigned int 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()) {
assert(false);
m_selected_dev_id.clear();
@@ -479,7 +480,8 @@ void PresetComboBox::add_connected_printers(std::string selected, bool alias_nam
if (!dev)
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())
return;
@@ -999,7 +1001,13 @@ void PlaterPresetComboBox::update_badge_according_flag() {
auto selection = GetSelection();
auto select_flag = GetFlag(selection);
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()
+2
View File
@@ -205,6 +205,7 @@ public:
void msw_rescale() override;
void OnSelect(wxCommandEvent& evt) override;
void update_badge_according_flag();
void set_sync_badge(bool show);
FilamentColor get_cur_color_info();
void show_default_color_picker();
@@ -214,6 +215,7 @@ public:
private:
// BBS
wxColor m_color;
bool m_sync_badge{false};
};
+9 -3
View File
@@ -1736,7 +1736,8 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj)
auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
// 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(isIp(str_ip.ToStdString()) &&
(str_access_code.IsEmpty() || str_access_code.Length() >= 8));
Layout();
Fit();
@@ -1801,6 +1802,8 @@ void InputIpAddressDialog::on_ok(wxMouseEvent& evt)
m_trouble_shoot->Hide();
std::string str_ip = m_input_ip->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();
// 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.
@@ -1835,6 +1838,8 @@ void InputIpAddressDialog::on_send_retry()
Fit();
wxString ip = m_input_ip->GetTextCtrl()->GetValue();
wxString str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
if (str_access_code.IsEmpty())
str_access_code = "88888888";
// check support function
if (!m_obj) return;
@@ -2056,7 +2061,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
auto str_ip = m_input_ip->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";
}
@@ -2072,7 +2077,8 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
}
// 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 = isIp(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
m_button_manual_setup->Enable(enable_btns);
m_button_ok->Enable(enable_btns);
+76 -29
View File
@@ -21,6 +21,7 @@
#include "Jobs/PlaterWorker.hpp"
#include "DeviceCore/DevConfig.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevNozzleSystem.h"
#include "DeviceCore/DevNozzleRack.h"
#include "DeviceCore/DevExtensionTool.h"
@@ -1125,7 +1126,10 @@ bool SelectMachineDialog::do_ams_mapping(MachineObject *obj_,bool use_ams)
int filament_result = 0;
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?
if (true/*!can_hybrid_mapping(obj_get_extder_data())*/){
std::vector<FilamentInfo> m_ams_mapping_result_left, m_ams_mapping_result_right;
@@ -2284,9 +2288,7 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
// Fill the real per-printer max color count into the %s template.
if (!params.empty())
msg = wxString::Format(m_pre_print_checker.get_pre_state_msg(status), params[0], params[0]);
}
else if (status == PrintDialogStatus::PrintStatusAmsMappingU0Invalid) {
} else if (status == PrintDialogStatus::PrintStatusAmsMappingU0Invalid) {
wxString msg_text;
if (params.size() > 1)
msg_text = wxString::Format(_L("Filament %s does not match the filament in AMS slot %s. Please update the printer firmware to support AMS slot assignment."), params[0], params[1]);
@@ -2312,8 +2314,10 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
} else if (status == PrintDialogStatus::PrintStatusNoSdcard) {
Enable_Refresh_Button(true);
Enable_Send_Button(false);
}else if (status == PrintDialogStatus::PrintStatusUnsupportedPrinter) {
} else if (status == PrintDialogStatus::PrintStatusUnsupportedPrinter ||
status == PrintDialogStatus::PrintStatusOptionalPrinterModel) {
wxString msg_text;
const bool block_send = status == PrintDialogStatus::PrintStatusUnsupportedPrinter;
try
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
@@ -2339,17 +2343,21 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
auto target_print_name = wxString(DevPrinterConfigUtil::get_printer_display_name(target_model_id));
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;
Enable_Refresh_Button(true);
Enable_Send_Button(false);
Enable_Send_Button(!block_send);
}
catch (...)
{
Enable_Refresh_Button(true);
Enable_Send_Button(false);
Enable_Send_Button(!block_send);
}
@@ -2526,15 +2534,7 @@ bool SelectMachineDialog::is_blocking_printing(MachineObject* obj_)
}
}
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;
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
static std::unordered_set<int> _get_used_nozzle_idxes()
@@ -2622,6 +2622,10 @@ bool SelectMachineDialog::is_same_printer_model()
if(preset_bundle == nullptr) return result;
const auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
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
if (source_model != target_model) {
if ((source_model == "C12" && target_model == "C11") || (source_model == "C11" && target_model == "C12") ||
@@ -3696,10 +3700,32 @@ void SelectMachineDialog::on_send_print()
m_print_job->on_success([this]() { finish_mode(); });
m_print_job->on_check_ip_address_fail([this]() {
wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS);
wxQueueEvent(this, evt);
wxGetApp().show_ip_address_enter_dialog();
});
// Invoked from the PrintJob worker thread when the LAN pre-flight (file upload
// verification) fails. Marshal device/UI access to the main thread.
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
NetworkAgent* agent = wxGetApp().getAgent();
@@ -4533,11 +4559,13 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
// check nozzle data valid
{
if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine ||
installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) {
show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid);
return false;
}
// Commented out the following as ntUndefine and 0.0f are default values
// (signifying that the value is not given) that should PASS, not fail
// if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine ||
// installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) {
// show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid);
// return false;
// }
if (obj_->is_nozzle_flow_type_supported() &&
installed_ext_nozzle.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) {
@@ -4566,7 +4594,10 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
// 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;
if (ext_sys->GetTotalExtderCount() == 2) {
const wxString& mismatch_nozzle_str = _get_nozzle_name(ext_sys->GetTotalExtderCount(), slicing_ext_idx);
@@ -4760,6 +4791,22 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_)
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_)) {
show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter);
return;
@@ -5482,8 +5529,8 @@ void SelectMachineDialog::reset_and_sync_ams_list()
item = new MaterialItem(m_filament_left_panel, colour_rgb, _L(display_materials[extruder]));
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]));
m_sizer_ams_mapping_right->Add(item, 0, wxALL, FromDIP(5));
}
+122 -82
View File
@@ -24,6 +24,7 @@
#include "BitmapCache.hpp"
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevStorage.h"
#include "slic3r/Utils/FileTransferUtils.hpp"
@@ -300,7 +301,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater)
m_storage_panel->Layout();
// 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->SetForegroundColour(*wxBLACK);
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 (m_statictext_printer_msg->GetLabel() != msg) {
m_statictext_printer_msg->SetLabel(msg);
m_statictext_printer_msg->SetMinSize(wxSize(FromDIP(400), -1));
m_statictext_printer_msg->SetMaxSize(wxSize(FromDIP(400), -1));
m_statictext_printer_msg->Wrap(FromDIP(400));
const int wrap_width = FromDIP(400);
m_statictext_printer_msg->Wrap(wrap_width);
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();
Layout();
Fit();
@@ -1102,7 +1119,7 @@ void SendToPrinterDialog::update_user_printer()
wxArrayString machine_list_name;
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
for (auto it = option_list.begin(); it != option_list.end(); it++) {
@@ -1332,17 +1349,7 @@ bool SendToPrinterDialog::is_blocking_printing(MachineObject* obj_)
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;
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
void SendToPrinterDialog::Enable_Refresh_Button(bool en)
@@ -1413,79 +1420,68 @@ void SendToPrinterDialog::show_status(PrintDialogStatus status, std::vector<wxSt
update_print_status_msg(wxEmptyString, false, false);
Enable_Send_Button(false);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusInvalidPrinter) {
} else if (status == PrintDialogStatus::PrintStatusInvalidPrinter) {
update_print_status_msg(wxEmptyString, true, true);
Enable_Send_Button(false);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusConnectingServer) {
} else if (status == PrintDialogStatus::PrintStatusConnectingServer) {
wxString msg_text = _L("Connecting to server...");
update_print_status_msg(msg_text, true, true);
Enable_Send_Button(true);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusReading) {
} else if (status == PrintDialogStatus::PrintStatusReading) {
wxString msg_text = _L("Synchronizing device information...");
update_print_status_msg(msg_text, false, true);
Enable_Send_Button(false);
Enable_Refresh_Button(false);
}
else if (status == PrintDialogStatus::PrintStatusReadingFinished) {
} else if (status == PrintDialogStatus::PrintStatusReadingFinished) {
update_print_status_msg(wxEmptyString, false, true);
Enable_Send_Button(true);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusReadingTimeout) {
} else if (status == PrintDialogStatus::PrintStatusReadingTimeout) {
wxString msg_text = _L("Synchronizing device information timed out.");
update_print_status_msg(msg_text, true, true);
Enable_Send_Button(true);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusInUpgrading) {
} else if (status == PrintDialogStatus::PrintStatusInUpgrading) {
wxString msg_text = _L("Cannot send print tasks when an update is in progress");
update_print_status_msg(msg_text, true, true);
Enable_Send_Button(false);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusUnsupportedPrinter) {
} else if (status == PrintDialogStatus::PrintStatusUnsupportedPrinter) {
wxString msg_text = _L("The selected printer is incompatible with the chosen printer presets.");
update_print_status_msg(msg_text, true, true);
Enable_Send_Button(false);
Enable_Send_Button(true);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusRefreshingMachineList) {
} else if (status == PrintDialogStatus::PrintStatusRefreshingMachineList) {
update_print_status_msg(wxEmptyString, false, true);
Enable_Send_Button(false);
Enable_Refresh_Button(false);
}
else if (status == PrintDialogStatus::PrintStatusSending) {
} else if (status == PrintDialogStatus::PrintStatusSending) {
Enable_Send_Button(false);
Enable_Refresh_Button(false);
}
else if (status == PrintDialogStatus::PrintStatusSendingCanceled) {
} else if (status == PrintDialogStatus::PrintStatusSendingCanceled) {
Enable_Send_Button(true);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusNoSdcard) {
} else if (status == PrintDialogStatus::PrintStatusNoSdcard) {
wxString msg_text = _L("Storage needs to be inserted before send to printer.");
update_print_status_msg(msg_text, true, true);
Enable_Send_Button(false);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusNotOnTheSameLAN) {
} else if (status == PrintDialogStatus::PrintStatusNotOnTheSameLAN) {
wxString msg_text = _L("The printer is required to be on the same LAN as Orca Slicer.");
update_print_status_msg(msg_text, true, true);
Enable_Send_Button(false);
Enable_Refresh_Button(true);
}
else if (status == PrintDialogStatus::PrintStatusNotSupportedSendToSDCard) {
} else if (status == PrintDialogStatus::PrintStatusNotSupportedSendToSDCard) {
wxString msg_text = _L("The printer does not support sending to printer storage.");
update_print_status_msg(msg_text, true, true);
Enable_Send_Button(false);
Enable_Refresh_Button(true);
} 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_Refresh_Button(true);
} else if (status == PrintDialogStatus::PrintStatusPublicUploadFiled) {
@@ -1663,30 +1659,18 @@ extern void refresh_agora_url(char const *device, char const *dev_ver, char
void SendToPrinterDialog::GetConnection()
{
DeviceManager *dm = GUI::wxGetApp().getDeviceManager();
MachineObject *obj = dm ? dm->get_selected_machine() : nullptr;
MachineObject *obj = dm->get_selected_machine();
if (obj == nullptr) {
if (!obj)
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : obj is empty";
m_connection_status = ConnectionStatus::NOT_START;
}
int remote_proto = obj->get_file_remote();
if (!remote_proto) {
if (obj && !obj->get_file_remote())
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()) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy";
m_connection_status = ConnectionStatus::NOT_START;
}
NetworkAgent* agent = wxGetApp().getAgent();
NetworkAgent *agent = wxGetApp().getAgent();
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())
if (m_url_timer && m_url_timer->IsRunning())
{
m_url_timer->Stop();
}
@@ -1709,19 +1693,40 @@ void SendToPrinterDialog::GetConnection()
m_url_timer->GetId());
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) {
std::string devIP = obj->get_dev_ip();
std::string accessCode = obj->get_access_code();
std::string url = "bambu:///local/" + devIP + "?port=6000&user=" + "bblp" + "&passwd=" + accessCode;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp";
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);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp, dev_id=" << dev_id
<< ", dev_ip=" << devIP << ", access_code_len=" << accessCode.size();
try
{
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)
{
@@ -1749,11 +1754,28 @@ void SendToPrinterDialog::GetConnection()
if (boost::algorithm::starts_with(url, "bambu:///"))
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tutk";
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();
try
{
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();
}
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
{
@@ -1852,8 +1874,17 @@ void SendToPrinterDialog::ResetTunnelAndJob()
void SendToPrinterDialog::CreateMediaAbilityJob()
{
nlohmann::json media_ability = {{"cmd_type", 7}};
m_filetransfer_mediability_job = std::make_unique<FileTransferJob>(module(), std::string(media_ability.dump()));
nlohmann::json media_ability = {{"cmd_type", 7}};
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) {
//this pl
CallAfter([this, res, resp_ec, json_res] {
@@ -1908,11 +1939,20 @@ void SendToPrinterDialog::CreateUploadFileJob(const std::string &path, const std
{"cmd_type", 5},
};
upload_params["dest_storage"] = m_selected_storage;
upload_params["dest_name"] = name; // filenme no path
upload_params["file_path"] = path;
upload_params["dest_name"] = name; // filenme no path
upload_params["file_path"] = path;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob";
m_filetransfer_uploadfile_job = std::make_unique<FileTransferJob>(module(), std::string(upload_params.dump()));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob";
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) { //
CallAfter([this, res, resp_ec, json_res, bin_res] {
UploadFileRessultCallback(res, resp_ec,json_res, bin_res);
+105 -104
View File
@@ -22,6 +22,7 @@
#include <wx/mstream.h>
#include <wx/sstream.h>
#include <wx/zstream.h>
#include <chrono>
#include "DeviceCore/DevBed.h"
#include "DeviceCore/DevCtrl.h"
@@ -1462,7 +1463,7 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
#if !BBL_RELEASE_TO_PUBLIC
m_staticText_timelapse->Show();
m_bmToggleBtn_timelapse->Show();
m_bmToggleBtn_timelapse->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &e) {
m_bmToggleBtn_timelapse->Bind(wxEVT_TOGGLEBUTTON, [](wxCommandEvent &e) {
if (e.IsChecked())
wxGetApp().getAgent()->start_subscribe("tunnel");
else
@@ -1493,27 +1494,26 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
m_setting_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24)));
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->SetMinSize(wxSize(FromDIP(38), FromDIP(24)));
m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG);
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) {
const std::string js_request_pip = R"(
document.querySelector('video').requestPictureInPicture();
)";
m_custom_camera_view->RunScript(js_request_pip);
});
m_camera_switch_button->Hide();
// 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->SetBackgroundColour(STATUS_TITLE_BG);
// m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp());
// m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) {
// const std::string js_request_pip = R"(
// document.querySelector('video').requestPictureInPicture();
// )";
// m_custom_camera_view->RunScript(js_request_pip);
// });
// m_camera_switch_button->Hide();
m_bitmap_sdcard_img->SetToolTip(_L("Storage"));
m_bitmap_timelapse_img->SetToolTip(_L("Timelapse"));
m_bitmap_recording_img->SetToolTip(_L("Video"));
m_bitmap_vcamera_img->SetToolTip(_L("Go Live"));
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_timelapse_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->EnableContextMenu(false);
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->SetWebMediaController(m_web_media_controller.get());
m_custom_camera_view->Hide();
m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) {
if (evt.GetString() == "leavepictureinpicture") {
// When leaving PiP, video gets paused in some cases and toggling play
// programmatically does not work.
m_custom_camera_view->Reload();
}
else if (evt.GetString() == "enterpictureinpicture") {
toggle_builtin_camera();
}
});
// m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) {
// if (evt.GetString() == "leavepictureinpicture") {
// // When leaving PiP, video gets paused in some cases and toggling play
// // programmatically does not work.
// m_custom_camera_view->Reload();
// }
// });
sizer->Add(m_media_ctrl, 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);
if (wxGetApp().app_config->get("camera", "enable_custom_source") == "true") {
handle_camera_source_change();
}
return sizer;
}
@@ -2311,6 +2306,27 @@ void StatusPanel::update_camera_state(MachineObject* obj)
{
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
auto sdcard_state = obj->GetStorage()->get_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;
}
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_panel_monitoring_title->Layout();
}
@@ -2399,6 +2420,8 @@ void StatusPanel::update_camera_state(MachineObject* obj)
bool show_vcamera = m_media_play_ctrl->IsStreaming();
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)
@@ -3920,39 +3943,60 @@ void StatusPanel::update_cloud_subtask(MachineObject *obj)
update_calib_bitmap();
if (obj->slice_info) {
m_request_url = wxString(obj->slice_info->thumbnail_url);
if (!m_request_url.IsEmpty()) {
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;
}
}
load_thumbnail_from_url(m_request_url, obj);
}
}
}
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)
{
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();
if (m_current_print_mode != PrintingTaskType::CALIBRATION) {
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;
m_load_sdcard_thumbnail = true;
m_request_url.clear();
}
}
@@ -4959,7 +5004,6 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event)
}
sdcard_hint_dlg->on_show();
});
m_camera_popup->Bind(EVT_CAM_SOURCE_CHANGE, &StatusPanel::on_camera_source_change, this);
wxWindow* ctrl = (wxWindow*)event.GetEventObject();
wxPoint pos = ctrl->ClientToScreen(wxPoint(0, 0));
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()
{
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()
{
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;
last_subtask = nullptr;
last_tray_exist_bits = -1;
+6 -6
View File
@@ -14,7 +14,9 @@
#include <wx/sizer.h>
#include <wx/gbsizer.h>
#include <wx/webrequest.h>
#include <memory>
#include "MediaPlayCtrl.h"
#include "WebMediaController.hpp"
#include "AMSSetting.hpp"
#include "Calibration.hpp"
#include "CalibrationWizardPage.hpp"
@@ -439,7 +441,7 @@ protected:
wxStaticBitmap *m_bitmap_sdcard_img;
wxStaticBitmap *m_bitmap_static_use_time;
wxStaticBitmap *m_bitmap_static_use_weight;
wxStaticBitmap* m_camera_switch_button;
// wxStaticBitmap* m_camera_switch_button;
wxMediaCtrl3 * m_media_ctrl;
@@ -461,6 +463,8 @@ protected:
ScalableButton *m_button_abort;
Button * m_button_clean;
wxWebView * m_custom_camera_view{nullptr};
std::unique_ptr<WebMediaController> m_web_media_controller;
wxSimplebook* m_extruder_book;
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_down_10(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 on_webview_navigating(wxWebViewEvent& evt);
void on_camera_switch_toggled(wxMouseEvent& event);
void toggle_custom_camera();
void toggle_builtin_camera();
public:
StatusBasePanel(wxWindow * parent,
@@ -629,6 +628,7 @@ class StatusPanel : public StatusBasePanel
{
private:
friend class MonitorPanel;
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
protected:
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
+10 -10
View File
@@ -1863,7 +1863,6 @@ bool SyncAmsInfoDialog::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 = "";
if (m_print_type == PrintFromType::FROM_NORMAL) {
@@ -1874,13 +1873,7 @@ bool SyncAmsInfoDialog::is_blocking_printing(MachineObject *obj_)
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;
return !DevPrinterConfigUtil::is_printer_model_compatible(source_model, *obj_);
}
bool SyncAmsInfoDialog::is_same_nozzle_type(std::string &filament_type, NozzleType &tag_nozzle_type)
@@ -1930,7 +1923,13 @@ bool SyncAmsInfoDialog::is_same_printer_model()
if (obj_ == nullptr) { return result; }
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")) {
return true;
}
@@ -2131,7 +2130,7 @@ void SyncAmsInfoDialog::update_user_printer()
std::map<std::string, MachineObject *> option_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
for (auto it = option_list.begin(); it != option_list.end(); it++) {
@@ -3206,6 +3205,7 @@ SyncAmsInfoDialog::~SyncAmsInfoDialog() {
void SyncAmsInfoDialog::set_info(SyncInfo &info)
{
m_input_info = info;
reinit_dialog();
}
void SyncAmsInfoDialog::update_lan_machine_list()
+76
View File
@@ -0,0 +1,76 @@
#include "WebMediaController.hpp"
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
WebMediaController::WebMediaController(wxWebView* webview) : m_webview(webview)
{
if (!m_webview)
return;
m_webview->SetBackgroundColour(*wxBLACK);
m_webview->SetPage("<html><head><style>html,body{margin:0;height:100%;background:#000;}</style></head><body></body></html>", "");
}
void WebMediaController::Load(wxURI url) { Load(url, CameraStreamMode::http); }
void WebMediaController::Load(wxURI url, CameraStreamMode mode)
{
m_url = url.BuildURI().ToStdString();
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_url.clear();
}
}} // namespace Slic3r::GUI
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <slic3r/GUI/IMediaController.hpp>
#include <string>
class wxWebView;
namespace Slic3r { namespace GUI {
class WebMediaController : public IMediaController
{
public:
explicit WebMediaController(wxWebView* webview);
void Load(wxURI url) override;
void Load(wxURI url, CameraStreamMode mode) override;
void Play() override;
void Stop() override;
private:
wxWebView* m_webview;
std::string m_url;
CameraStreamMode m_stream_mode = CameraStreamMode::http;
};
}} // namespace Slic3r::GUI
+395
View File
@@ -0,0 +1,395 @@
#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()
{
StopSession();
}
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::StartSession(std::unique_ptr<ICameraSignalingChannel> channel)
{
// 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::StopSession()
{
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
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include "IMediaController.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 StartSession 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 StartSession(std::unique_ptr<ICameraSignalingChannel> channel) override;
void StopSession() override;
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 { StopSession(); }
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_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
+364 -82
View File
@@ -4,6 +4,14 @@
#include "libslic3r/Utils.hpp"
#include <boost/log/trivial.hpp>
#include <wx/dcclient.h>
#include <cstdarg>
#include <cstdlib>
#include <cstring>
#include <mutex>
extern "C" {
#include <libavformat/avformat.h>
#include <libavutil/log.h>
}
#ifdef __WIN32__
#include <versionhelpers.h>
#include <wx/msw/registry.h>
@@ -43,9 +51,13 @@ wxMediaCtrl3::~wxMediaCtrl3()
m_thread.join();
}
static void adjust_frame_size(wxSize& frame, wxSize const& video, wxSize const& window);
void wxMediaCtrl3::Load(wxURI url)
{
std::unique_lock<std::mutex> lk(m_mutex);
if (m_external)
return;
m_video_size = wxDefaultSize;
m_error = 0;
m_url.reset(new wxURI(url));
@@ -55,6 +67,8 @@ void wxMediaCtrl3::Load(wxURI url)
void wxMediaCtrl3::Play()
{
std::unique_lock<std::mutex> lk(m_mutex);
if (m_external)
return;
if (m_state != wxMEDIASTATE_PLAYING) {
m_state = wxMEDIASTATE_PLAYING;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
@@ -74,6 +88,62 @@ void wxMediaCtrl3::Stop()
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)
{
if (m_idle_image == image)
@@ -185,6 +255,210 @@ void wxMediaCtrl3::bambu_log(void *ctx, int level, tchar const *msg2)
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()
{
using namespace std::chrono_literals;
@@ -197,42 +471,22 @@ void wxMediaCtrl3::PlayThread()
continue;
if (!url->HasScheme())
break;
lk.unlock();
Bambu_Tunnel tunnel = nullptr;
int error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8());
if (error == 0) {
Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this);
error = Bambu_Open(tunnel);
if (error == 0)
error = Bambu_would_block;
}
lk.lock();
while (error == int(Bambu_would_block)) {
m_cond.wait_for(lk, 100ms);
if (m_url != url) {
error = 1;
break;
const wxString scheme = url->GetScheme();
const bool generic_ffmpeg = scheme.CmpNoCase("http") == 0 || scheme.CmpNoCase("https") == 0 ||
scheme.CmpNoCase("rtsp") == 0 || scheme.CmpNoCase("rtsps") == 0;
int error = 0;
if (generic_ffmpeg) {
error = PlayFfmpeg(url, lk);
} else {
lk.unlock();
Bambu_Tunnel tunnel = nullptr;
error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8());
if (error == 0) {
Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this);
error = Bambu_Open(tunnel);
if (error == 0)
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();
while (error == int(Bambu_would_block)) {
m_cond.wait_for(lk, 100ms);
@@ -240,59 +494,87 @@ void wxMediaCtrl3::PlayThread()
error = 1;
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();
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;
while (error == int(Bambu_would_block)) {
m_cond.wait_for(lk, 100ms);
if (m_url != url) {
error = 1;
break;
}
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) {
lk.unlock();
Bambu_Close(tunnel);
Bambu_Destroy(tunnel);
tunnel = nullptr;
lk.lock();
if (tunnel) {
lk.unlock();
Bambu_Close(tunnel);
Bambu_Destroy(tunnel);
tunnel = nullptr;
lk.lock();
}
}
if (m_url == url)
m_error = error;
+16 -2
View File
@@ -19,11 +19,10 @@ wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
#define BAMBU_DYNAMIC
#include <atomic>
#include <condition_variable>
#include <thread>
#ifndef _WIN32
#include <wx/image.h>
#endif
#include "Printer/BambuTunnel.h"
class AVVideoDecoder;
@@ -41,6 +40,16 @@ public:
void Stop();
// 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);
wxMediaState GetState();
@@ -59,8 +68,10 @@ protected:
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 int ffmpeg_interrupt_callback(void *opaque);
void PlayThread();
int PlayFfmpeg(std::shared_ptr<wxURI> const &url, std::unique_lock<std::mutex> &lock);
void NotifyStopped();
@@ -77,12 +88,15 @@ private:
#endif
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::chrono::system_clock::time_point m_last_PTS_expected;
std::chrono::system_clock::time_point m_last_PTS_practical;
std::mutex m_mutex;
std::condition_variable m_cond;
std::thread m_thread;
std::atomic_bool m_refresh_pending{false};
};
#endif /* wxMediaCtrl3_h */
+1 -1
View File
@@ -282,7 +282,7 @@ bool CrealityPrintAgent::parse_cfs_response(const std::string& response,
return true;
}
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id)
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/)
{
if (device_info.dev_ip.empty()) {
BOOST_LOG_TRIVIAL(warning)
+1 -1
View File
@@ -41,7 +41,7 @@ public:
static AgentInfo 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
// 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 : "");
}
} // namespace Slic3r
} // namespace Slic3r
+1 -1
View File
@@ -252,4 +252,4 @@ inline FileTransferModule &module()
return *detail::g_mod;
}
} // namespace Slic3r
} // namespace Slic3r
@@ -0,0 +1,40 @@
#pragma once
#include <functional>
#include <memory>
#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
+112 -9
View File
@@ -11,6 +11,10 @@
#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability
#include <string>
#include <memory>
#include <vector>
#include <functional>
#include <cstdint>
#include "ICameraSignalingChannel.hpp"
namespace Slic3r {
@@ -43,6 +47,15 @@ enum class FilamentSyncMode {
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.
*
@@ -84,6 +97,38 @@ public:
*/
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, std::string, 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; }
virtual int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
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)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
/**
* 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.
*/
@@ -105,12 +150,16 @@ public:
/**
* 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.
*/
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
@@ -126,7 +175,11 @@ public:
/**
* 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.
@@ -136,23 +189,50 @@ public:
/**
* 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.
*/
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.
*/
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.
* 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.
@@ -168,7 +248,7 @@ public:
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;
@@ -284,12 +364,20 @@ public:
*/
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.
* Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull.
* 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.
@@ -300,6 +388,21 @@ public:
*/
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; }
/**
* 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 {}; }
// Optional native camera signaling. Plugin agents retain the default
// nullptr until a plugin-facing WebRTC contract is defined.
virtual std::unique_ptr<ICameraSignalingChannel>
create_camera_signaling_channel(const std::string& dev_id)
{
(void) dev_id;
return nullptr;
}
};
} // namespace Slic3r
+1 -50
View File
@@ -191,14 +191,6 @@ int MoonrakerPrinterAgent::disconnect_printer()
return BAMBU_NETWORK_SUCCESS;
}
int MoonrakerPrinterAgent::check_cert() { return BAMBU_NETWORK_SUCCESS; }
void MoonrakerPrinterAgent::install_device_cert(std::string dev_id, bool lan_only)
{
(void) dev_id;
(void) lan_only;
}
bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending)
{
(void) sending;
@@ -208,12 +200,6 @@ bool MoonrakerPrinterAgent::start_discovery(bool start, bool sending)
return true;
}
int MoonrakerPrinterAgent::ping_bind(std::string ping_code)
{
(void) ping_code;
return BAMBU_NETWORK_SUCCESS;
}
int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect)
{
(void) sec_link;
@@ -230,41 +216,6 @@ int MoonrakerPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link,
return BAMBU_NETWORK_SUCCESS;
}
int MoonrakerPrinterAgent::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;
}
int MoonrakerPrinterAgent::unbind(std::string dev_id)
{
(void) dev_id;
return BAMBU_NETWORK_SUCCESS;
}
int MoonrakerPrinterAgent::request_bind_ticket(std::string* ticket)
{
if (ticket)
*ticket = "";
return BAMBU_NETWORK_SUCCESS;
}
int MoonrakerPrinterAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback)
{
// No BBL cloud snapshot source; report failure so the caller falls back.
(void) dev_id;
(void) file_name;
(void) callback;
return -1;
}
int MoonrakerPrinterAgent::set_server_callback(OnServerErrFn fn)
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
@@ -578,7 +529,7 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
}
}
bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id)
bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/)
{
std::vector<AmsTrayData> trays;
int max_lane_index = 0;
+1 -10
View File
@@ -32,20 +32,11 @@ public:
int disconnect_printer() 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
bool start_discovery(bool start, bool sending) override;
// 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(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;
// Machine Selection
@@ -71,7 +62,7 @@ public:
// Pull-mode agent (on-demand filament sync)
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; }
bool fetch_filament_info(std::string dev_id) override;
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
protected:
struct MoonrakerDeviceInfo
+149 -11
View File
@@ -4,6 +4,8 @@
#include <algorithm>
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
#include "IPrinterAgent.hpp"
#include "libslic3r/Utils.hpp"
#include "NetworkAgent.hpp"
#include "BBLNetworkPlugin.hpp"
@@ -115,6 +117,8 @@ void NetworkAgent::add_cloud_agent(const std::string& provider, std::shared_ptr<
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
auto old_printer_agent = m_printer_agent;
@@ -432,10 +436,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)
{
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);
if (cloud_agent)
return cloud_agent->get_user_print_info(http_code, http_body);
return -1;
if (!cloud_agent)
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)
@@ -767,6 +787,77 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos
return -1;
}
int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_ams_refresh_rfid(dev_id, tray_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(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl)
{
if (m_printer_agent)
@@ -788,6 +879,13 @@ int NetworkAgent::send_message_to_printer(std::string dev_id, std::string json_s
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()
{
if (m_printer_agent)
@@ -846,8 +944,14 @@ std::string NetworkAgent::get_user_selected_machine()
int NetworkAgent::set_user_selected_machine(std::string dev_id)
{
if (m_printer_agent)
return m_printer_agent->set_user_selected_machine(dev_id);
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::set_user_selected_machine: dev_id=" << 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(info) << "NetworkAgent::set_user_selected_machine: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::set_user_selected_machine: no printer agent";
return -1;
}
@@ -867,15 +971,27 @@ int NetworkAgent::stop_subscribe(std::string module)
int NetworkAgent::add_subscribe(std::vector<std::string> dev_list)
{
if (m_printer_agent)
return m_printer_agent->add_subscribe(std::move(dev_list));
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::add_subscribe: count=" << dev_list.size()
<< " 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(info) << "NetworkAgent::add_subscribe: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::add_subscribe: no printer agent";
return -1;
}
int NetworkAgent::del_subscribe(std::vector<std::string> dev_list)
{
if (m_printer_agent)
return m_printer_agent->del_subscribe(std::move(dev_list));
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::del_subscribe: count=" << dev_list.size()
<< " 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(info) << "NetworkAgent::del_subscribe: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::del_subscribe: no printer agent";
return -1;
}
@@ -921,10 +1037,10 @@ FilamentSyncMode NetworkAgent::get_filament_sync_mode() const
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) {
return m_printer_agent->fetch_filament_info(dev_id);
return m_printer_agent->fetch_filament_info(dev_id, sync_mode);
}
return false;
}
@@ -943,6 +1059,28 @@ std::string NetworkAgent::from_orca_filament_id(const std::string& orca_filament
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 {};
}
std::unique_ptr<ICameraSignalingChannel>
NetworkAgent::create_camera_signaling_channel(const std::string& dev_id)
{
if (m_printer_agent)
return m_printer_agent->create_camera_signaling_channel(dev_id);
return nullptr;
}
int NetworkAgent::request_bind_ticket(std::string* ticket)
{
if (m_printer_agent)
+24 -1
View File
@@ -2,16 +2,22 @@
#define __NETWORK_Agent_HPP__
#include "bambu_networking.hpp"
#include "libslic3r/ProjectTask.hpp"
#include "ICloudServiceAgent.hpp"
#include "IPrinterAgent.hpp"
#include <map>
#include <atomic>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace Slic3r {
class IPrinterAgent;
// Forward declaration
class BBLNetworkPlugin;
@@ -50,6 +56,7 @@ public:
// Sub-agent accessors
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::uint64_t get_user_machine_list_generation() const { return m_user_machine_list_generation.load(); }
// Shared agent management
void add_cloud_agent(const std::string& provider, std::shared_ptr<ICloudServiceAgent> agent);
@@ -142,9 +149,21 @@ public:
int set_on_local_message_fn(OnMessageFn fn);
int set_server_callback(OnServerErrFn fn);
int send_message(std::string dev_id, std::string json_str, int qos, int flag);
int command_ams_refresh_rfid(std::string dev_id, std::string tray_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(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl);
int disconnect_printer();
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();
void install_device_cert(std::string dev_id, bool lan_only);
bool start_discovery(bool start, bool sending);
@@ -164,7 +183,10 @@ public:
int start_local_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;
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::unique_ptr<ICameraSignalingChannel> create_camera_signaling_channel(const std::string& dev_id);
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;
int request_bind_ticket(std::string* ticket);
@@ -195,6 +217,7 @@ private:
std::map<std::string, std::shared_ptr<ICloudServiceAgent>> m_cloud_agents;
std::shared_ptr<IPrinterAgent> m_printer_agent;
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 <utility>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/I18N.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/msgdlg.h>
namespace Slic3r {
namespace {
@@ -314,6 +316,21 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
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);
@@ -332,9 +349,10 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
auto& python_agent_ids = get_python_printer_agent_ids();
for (const auto& pair : python_agent_ids) {
if (pair.first != capability_key && pair.second == info.id) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses duplicate agent ID '" << info.id
<< "' already registered by capability '" << pair.first.second << "' from plugin '"
<< pair.first.first << "'";
const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
"' is already registered by capability '" + pair.first.second + "' from plugin '" + pair.first.first + "'.";
BOOST_LOG_TRIVIAL(warning) << error_message;
reject_conflicting_capability(error_message);
return;
}
}
@@ -354,12 +372,22 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
auto& agents = get_printer_agents();
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) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses agent ID '" << info.id
<< "' already registered by '" << agent_it->second.display_name << "'";
const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
"' is already registered by '" + agent_it->second.display_name + "'.";
BOOST_LOG_TRIVIAL(warning) << error_message;
reject_conflicting_capability(error_message);
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)));
python_agent_ids[capability_key] = info.id;
+45 -4
View File
@@ -83,6 +83,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_BASE = "/api/v1/plugins";
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";
@@ -2623,11 +2624,51 @@ 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)
{
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)
*http_code = 200;
if (http_body)
*http_body = "{}";
*http_code = code;
if (result != 0 || code != 200)
return result != 0 ? result : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
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())) {
const std::string role = printer.value("access_role", "");
if (role.empty() || role == "viewer")
continue;
nlohmann::json device;
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(std::move(device));
}
if (http_body) {
nlohmann::json out;
out["devices"] = std::move(devices);
*http_body = out.dump();
}
} catch (const std::exception&) {
return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
}
return BAMBU_NETWORK_SUCCESS;
}
-49
View File
@@ -41,19 +41,6 @@ int OrcaPrinterAgent::send_message_to_printer(std::string dev_id, std::string js
return BAMBU_NETWORK_SUCCESS;
}
// ============================================================================
// Certificates - All Stubs
// ============================================================================
int OrcaPrinterAgent::check_cert()
{
return BAMBU_NETWORK_SUCCESS;
}
void OrcaPrinterAgent::install_device_cert(std::string dev_id, bool lan_only)
{
}
// ============================================================================
// Discovery - Stub
// ============================================================================
@@ -63,47 +50,11 @@ bool OrcaPrinterAgent::start_discovery(bool start, bool sending)
return true;
}
// ============================================================================
// Binding - All Stubs
// ============================================================================
int OrcaPrinterAgent::ping_bind(std::string ping_code)
{
return BAMBU_NETWORK_SUCCESS;
}
int OrcaPrinterAgent::bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect)
{
return BAMBU_NETWORK_SUCCESS;
}
int OrcaPrinterAgent::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)
{
return BAMBU_NETWORK_SUCCESS;
}
int OrcaPrinterAgent::unbind(std::string dev_id)
{
return BAMBU_NETWORK_SUCCESS;
}
int OrcaPrinterAgent::request_bind_ticket(std::string* ticket)
{
if (ticket)
*ticket = "";
return BAMBU_NETWORK_SUCCESS;
}
int OrcaPrinterAgent::get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback)
{
// No BBL cloud snapshot source; report failure so the caller falls back.
(void) dev_id;
(void) file_name;
(void) callback;
return -1;
}
int OrcaPrinterAgent::set_server_callback(OnServerErrFn fn)
{
std::lock_guard<std::mutex> lock(state_mutex);
-9
View File
@@ -32,20 +32,11 @@ public:
int disconnect_printer() 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
bool start_discovery(bool start, bool sending) override;
// 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(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;
// Machine Selection
+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
+1 -1
View File
@@ -40,7 +40,7 @@ AgentInfo QidiPrinterAgent::get_agent_info_static()
return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"};
}
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id)
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/)
{
std::string error;
+1 -1
View File
@@ -19,7 +19,7 @@ public:
AgentInfo get_agent_info() override { return get_agent_info_static(); }
// 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;
private:
struct QidiFilamentDict
+1 -1
View File
@@ -104,7 +104,7 @@ std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type
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");
+1 -1
View File
@@ -15,7 +15,7 @@ public:
static AgentInfo 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;
private:
// Combine filament_type + filament_sub_type into a unified type string
@@ -23,6 +23,15 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
.value("Pull", FilamentSyncMode::pull)
.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)
.value("WebRTC", CameraStreamMode::webrtc)
.export_values();
py::class_<AgentInfo>(printer_agent_module, "AgentInfo")
.def(py::init<>())
.def(py::init([](std::string id, std::string name, std::string version, std::string description) {
@@ -105,7 +114,9 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
.def("start_send_gcode_to_sdcard", &PrinterAgentPluginCapability::start_send_gcode_to_sdcard)
.def("start_local_print", &PrinterAgentPluginCapability::start_local_print)
.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("get_camera_url", &PrinterAgentPluginCapability::get_camera_url)
.def("check_cert", &PrinterAgentPluginCapability::check_cert)
.def("install_device_cert", &PrinterAgentPluginCapability::install_device_cert)
.def("ping_bind", &PrinterAgentPluginCapability::ping_bind)
@@ -24,60 +24,39 @@ public:
PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; }
// 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
// other IPrinterAgent operation is pure: the Python plugin must implement all of them.
// capability its ICloudServiceAgent — so it is the one operation kept native here. Optional
// operations inherit IPrinterAgent's defaults; the remaining operations are plugin-defined.
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) final override { (void) cloud; }
AgentInfo get_agent_info() override = 0;
int connect_printer(
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_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;
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0;
std::string get_user_selected_machine() 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_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 connect_printer(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_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;
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0;
std::string get_user_selected_machine() 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_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
int check_cert() 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 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_local_print_with_record(PrintParams params,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn,
OnWaitFn wait_fn) override = 0;
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
OnWaitFn wait_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
@@ -5,7 +5,10 @@
#include "../../PyPluginTrampoline.hpp"
#include "IPrinterAgent.hpp"
#include "pybind11/pybind11.h"
#include <slic3r/plugin/PluginAuditManager.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
namespace Slic3r {
class PyPrinterAgentPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PrinterAgentPluginCapability>
@@ -92,46 +95,59 @@ public:
FilamentSyncMode get_filament_sync_mode() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability,
[] {}, PYBIND11_OVERRIDE, FilamentSyncMode, PrinterAgentPluginCapability,
get_filament_sync_mode);
}
bool fetch_filament_info(std::string dev_id) override
CameraStreamMode get_camera_stream_mode() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
[] {}, PYBIND11_OVERRIDE, CameraStreamMode, PrinterAgentPluginCapability,
get_camera_stream_mode);
}
std::string get_camera_url() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, std::string, PrinterAgentPluginCapability, get_camera_url);
}
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id, sync_mode);
}
int check_cert() override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert);
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, check_cert);
}
void install_device_cert(std::string dev_id, bool lan_only) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
[] {}, PYBIND11_OVERRIDE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
lan_only);
}
int ping_bind(std::string ping_code) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
}
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
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
dev_model, sec_link, timezone, improved, update_fn);
}
int unbind(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id);
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, unbind, dev_id);
}
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
@@ -158,7 +174,7 @@ public:
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
[] {}, PYBIND11_OVERRIDE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
file_name, callback);
}
@@ -221,6 +237,8 @@ public:
// 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
// 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 falls back to the base default
// instead of failing, mirroring what PYBIND11_OVERRIDE does for the other optional methods.
int request_bind_ticket(std::string* ticket) override
{
ORCA_PY_AUDIT_SCOPE();
@@ -231,7 +249,7 @@ public:
pybind11::function override =
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
if (!override)
pybind11::pybind11_fail("Tried to call pure virtual function \"PrinterAgentPluginCapability::request_bind_ticket\"");
return PrinterAgentPluginCapability::request_bind_ticket(ticket);
try {
pybind11::tuple result = override().cast<pybind11::tuple>();
if (ticket)
+47 -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
# rpath so the loader finds them when the tests run on the CI unit-test
# 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)
return()
endif()
set_property(TARGET ${_TEST_NAME}_tests PROPERTY BUILD_RPATH "$ORIGIN")
set_property(TARGET ${_target} PROPERTY BUILD_RPATH "$ORIGIN")
set(_configs ${CMAKE_CONFIGURATION_TYPES})
if (NOT _configs)
set(_configs "${CMAKE_BUILD_TYPE}")
endif()
foreach(_cfg IN LISTS _configs)
orcaslicer_copy_sos(${_TEST_NAME}_tests "${_cfg}" "" _unused_sos)
orcaslicer_copy_sos(${_target} "${_cfg}" "" _unused_sos)
endforeach()
endif()
endfunction()
@@ -78,6 +88,40 @@ function(orcaslicer_discover_tests TARGET)
catch_discover_tests(${TARGET} ADD_TAGS_AS_LABELS)
endfunction()
# Stages a private Python runtime beside TARGET's output and (on macOS) links
# in the matching rpath, so an embedded pybind11 interpreter in TARGET can find
# libpython/the stdlib at runtime without depending on the build machine's
# Python install. Apply this to every test target that embeds an interpreter -
# duplicating the WIN32/APPLE blocks by hand lets them drift out of sync.
function(orcaslicer_stage_test_python_runtime TARGET)
if (WIN32)
add_custom_command(TARGET ${TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${TARGET}>/python"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_PREFIX_PATH}/libpython" "$<TARGET_FILE_DIR:${TARGET}>/python"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_PREFIX_PATH}/libpython/python${_bundled_python_abi}.dll"
"${CMAKE_PREFIX_PATH}/libpython/vcruntime140.dll"
"${CMAKE_PREFIX_PATH}/libpython/vcruntime140_1.dll"
"$<TARGET_FILE_DIR:${TARGET}>"
COMMENT "Copying Python runtime for ${TARGET}"
VERBATIM
)
elseif (APPLE)
target_link_options(${TARGET} PRIVATE
"LINKER:-rpath,@executable_path/python/lib")
add_custom_command(TARGET ${TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf
"$<TARGET_FILE_DIR:${TARGET}>/python"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_PREFIX_PATH}/libpython"
"$<TARGET_FILE_DIR:${TARGET}>/python"
COMMENT "Copying Python runtime for ${TARGET}"
VERBATIM
)
endif()
endfunction()
add_subdirectory(libnest2d)
add_subdirectory(libslic3r)
add_subdirectory(slic3rutils)
+26 -27
View File
@@ -5,12 +5,15 @@ add_executable(${_TEST_NAME}_tests
test_creality_cfs_match.cpp
test_dev_mapping.cpp
test_filament_bitmap_utils.cpp
test_device_progress.cpp
test_network_versions.cpp
test_action_source.cpp
test_plugin_host_api.cpp
test_plugin_capability_config.cpp
test_plugin_config.cpp
test_plugin_capabilities_in_use.cpp
test_plugin_status.cpp
test_printer_agent.cpp
test_plugin_install.cpp
test_plugin_lifecycle.cpp
test_slicing_pipeline_bindings.cpp
@@ -28,33 +31,29 @@ endif ()
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r_gui libslic3r pybind11::embed Catch2::Catch2WithMain)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
orcaslicer_copy_test_dlls()
orcaslicer_copy_test_dlls(${_TEST_NAME}_tests)
if (WIN32)
add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/python"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_PREFIX_PATH}/libpython" "$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/python"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_PREFIX_PATH}/libpython/python${_bundled_python_abi}.dll"
"${CMAKE_PREFIX_PATH}/libpython/vcruntime140.dll"
"${CMAKE_PREFIX_PATH}/libpython/vcruntime140_1.dll"
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>"
COMMENT "Copying Python runtime for slic3rutils plugin host API tests"
VERBATIM
)
elseif (APPLE)
target_link_options(${_TEST_NAME}_tests PRIVATE
"LINKER:-rpath,@executable_path/python/lib")
add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/python"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_PREFIX_PATH}/libpython"
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/python"
COMMENT "Copying Python runtime for macOS plugin host API tests"
VERBATIM
)
endif()
orcaslicer_stage_test_python_runtime(${_TEST_NAME}_tests)
orcaslicer_discover_tests(${_TEST_NAME}_tests)
# why: the loader runs on a detached worker thread, so its Python interpreter
# ownership model cannot share the embedded interpreter in the main test binary.
add_executable(printer_agent_plugin_tests test_printer_agent_plugin.cpp)
if (MSVC)
target_link_libraries(printer_agent_plugin_tests Setupapi.lib)
endif ()
target_link_libraries(printer_agent_plugin_tests test_common libslic3r_gui libslic3r pybind11::embed Catch2::Catch2)
set_property(TARGET printer_agent_plugin_tests PROPERTY FOLDER "tests")
# why: both targets' post-build steps stage/clear the same python/ directory
# beside the shared test output dir; serialize them so the copies can't race.
add_dependencies(printer_agent_plugin_tests ${_TEST_NAME}_tests)
orcaslicer_copy_test_dlls(printer_agent_plugin_tests)
orcaslicer_stage_test_python_runtime(printer_agent_plugin_tests)
orcaslicer_discover_tests(printer_agent_plugin_tests)
+31
View File
@@ -26,6 +26,37 @@
using json = nlohmann::json;
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]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.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_THROWS_AS(machine.update_print_progress(json("not-a-percent")), std::invalid_argument);
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);
}
+23
View File
@@ -0,0 +1,23 @@
#include <catch2/catch_all.hpp>
#include <slic3r/GUI/PluginStatus.hpp>
using Slic3r::GUI::PluginStatus;
using Slic3r::GUI::resolve_plugin_status;
TEST_CASE("resolve_plugin_status precedence", "[plugin][status]") {
// the new branch: loaded module + error -> runtime fault, not a load failure.
REQUIRE(resolve_plugin_status(/*loading*/ false, /*has_error*/ true, /*is_loaded*/ true) == PluginStatus::RuntimeError);
// error without a live module is a load-time Error.
REQUIRE(resolve_plugin_status(false, true, false) == PluginStatus::Error);
// loading wins over a pending error so a reload never flashes red.
REQUIRE(resolve_plugin_status(true, true, true) == PluginStatus::Loading);
// healthy loaded plugin.
REQUIRE(resolve_plugin_status(false, false, true) == PluginStatus::Activated);
// nothing loaded, no error.
REQUIRE(resolve_plugin_status(false, false, false) == PluginStatus::Inactive);
}
+167
View File
@@ -0,0 +1,167 @@
#include <catch2/catch_all.hpp>
#include <slic3r/Utils/NetworkAgentFactory.hpp>
#include <slic3r/plugin/PythonPluginBridge.hpp>
#include "python_test_support.hpp"
#include <pybind11/embed.h>
#include <pybind11/pybind11.h>
#include <string>
using namespace Slic3r;
namespace py = pybind11;
// ===========================================================================
// UNIT - printer-agent registry duplicate handling.
// Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in
// or previously registered agent.
// ===========================================================================
TEST_CASE("unit: printer-agent registry register / lookup / duplicate-reject", "[registry][unit]")
{
// why: the registry is process-global state shared by the test binary, and
// Catch2 may run cases in any order. Use an id that cannot collide with
// built-ins or other cases. Avoid SECTIONs because each section re-runs the
// body and would register the same id twice.
const std::string id = "orca-test::registry-probe-7f3a";
auto stub_factory = [](std::shared_ptr<ICloudServiceAgent>, const std::string&)
-> std::shared_ptr<IPrinterAgent> { return nullptr; };
REQUIRE_FALSE(NetworkAgentFactory::is_printer_agent_registered(id));
REQUIRE(NetworkAgentFactory::register_printer_agent(id, "Registry Probe", stub_factory));
REQUIRE(NetworkAgentFactory::is_printer_agent_registered(id));
// Re-registering the same id is rejected and does not replace the entry.
REQUIRE_FALSE(NetworkAgentFactory::register_printer_agent(id, "Impostor", stub_factory));
// The first registration's display name survives the rejected duplicate.
const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(id);
REQUIRE(info != nullptr);
CHECK(info->display_name == "Registry Probe");
// It appears exactly once in the UI-population list.
auto agents = NetworkAgentFactory::get_registered_printer_agents();
int count = 0;
for (const auto& a : agents)
if (a.id == id)
++count;
CHECK(count == 1);
}
// ===========================================================================
// INTEGRATION - the orca.printer_agent Python binding surface.
// Boots the embedded interpreter and asserts the C++ to Python contract that
// every printer-agent plugin subclasses. If a binding is renamed or removed,
// plugins fail at runtime even though C++ still compiles.
// ===========================================================================
// ensure_python_initialized()/import_orca_module() come from python_test_support.hpp:
// a bare scoped_interpreter with no PyConfig.home falls back to the build-time
// stdlib path, which doesn't exist beside this test binary, so Py_Initialize()
// fails to load the codecs needed for the filesystem encoding. The shared helper
// points PyConfig.home at the python/ runtime staged next to the test executable.
TEST_CASE("integration: orca.printer_agent binding surface", "[integration][Python]")
{
py::module_ orca = import_orca_module();
REQUIRE(py::hasattr(orca, "printer_agent"));
py::object pa = orca.attr("printer_agent");
// The base class every printer-agent plugin subclasses.
REQUIRE(py::hasattr(pa, "PrinterAgentBase"));
py::object base = pa.attr("PrinterAgentBase");
for (const char* method : { "get_agent_info", "connect_printer", "disconnect_printer",
"send_message", "start_discovery", "bind_detect",
"start_print", "get_filament_sync_mode" }) {
CAPTURE(method);
CHECK(py::hasattr(base, method));
}
// AgentInfo value type - the registry identity the host reads (id is the key).
REQUIRE(py::hasattr(pa, "AgentInfo"));
py::object info = pa.attr("AgentInfo")("moonraker", "Moonraker", "1.0", "test agent");
CHECK(info.attr("id").cast<std::string>() == "moonraker");
CHECK(info.attr("name").cast<std::string>() == "Moonraker");
// FilamentSyncMode enum the host queries to pick pull vs subscription.
REQUIRE(py::hasattr(pa, "FilamentSyncMode"));
py::object mode = pa.attr("FilamentSyncMode");
CHECK(py::hasattr(mode, "Pull"));
CHECK(py::hasattr(mode, "Subscription"));
CHECK(py::hasattr(mode, "None_"));
REQUIRE(py::hasattr(pa, "CameraStreamMode"));
py::object camera_mode = pa.attr("CameraStreamMode");
CHECK(py::hasattr(camera_mode, "HTTPS"));
// Plugin-type enum exposed at module root (host reads it without the GIL).
CHECK(py::hasattr(orca, "PluginType"));
}
// ===========================================================================
// INTEGRATION - plugin-registration API and discovery-context guards.
// These are the symbols every plugin package uses: the @orca.plugin decorator,
// orca.base, orca.register_capability, and the capability base modules. Checking
// them in the lightweight embedded-interpreter test catches binding breakage
// before the plugin-loader test needs to run.
// ===========================================================================
TEST_CASE("integration: orca plugin-registration API surface + discovery-context guards", "[integration][Python]")
{
py::module_ orca = import_orca_module();
// Module-level surface every plugin package relies on.
// note: no "gcode" module here - this branch has no G-code capability module;
// PostProcessing exists only as a PluginType value.
for (const char* name : { "plugin", "register_capability", "base", "PythonPluginBase",
"PluginType", "PluginResult", "script", "printer_agent", "host" }) {
CAPTURE(name);
CHECK(py::hasattr(orca, name));
}
// Plugin package base and capability base contract.
CHECK(py::hasattr(orca.attr("base"), "register_capabilities"));
py::object cap_base = orca.attr("PythonPluginBase");
for (const char* method : { "get_name", "get_type", "on_load", "on_unload" }) {
CAPTURE(method);
CHECK(py::hasattr(cap_base, method));
}
// The script capability module exposes its own base class.
CHECK(py::hasattr(orca.attr("script"), "ScriptPluginCapabilityBase"));
// PluginType enum carries the values that route a capability, including PrinterConnection.
// note: no PostProcessing value on this branch's binding.
py::object types = orca.attr("PluginType");
for (const char* value : { "PrinterConnection", "Script" }) {
CAPTURE(value);
CHECK(py::hasattr(types, value));
}
// note: this is testing behavior, not normal plugin loading.
// These APIs should only work while Orca is actively loading a plugin.
try {
// Calls Python's orca.register_capability(0) from C++.
// 0 is intentionally bogus. The important part is that there is no active
// plugin load context, so the function should reject the call immediately.
orca.attr("register_capability")(py::int_(0));
// If the call above does NOT throw, the test fails here.
FAIL("register_capability outside discovery context must raise");
} catch (const py::error_already_set& error) {
// pybind11 wraps Python exceptions as py::error_already_set.
// This checks the Python exception type is ValueError.
CHECK(error.matches(PyExc_ValueError));
}
try {
// This is the function behind @orca.plugin.
// Same logic as above.
orca.attr("plugin")(py::int_(0));
FAIL("@orca.plugin outside discovery context must raise");
} catch (const py::error_already_set& error) {
CHECK(error.matches(PyExc_ValueError));
}
}
@@ -0,0 +1,414 @@
#include <catch2/catch_all.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/Utils/NetworkAgentFactory.hpp>
#include <libslic3r/Utils.hpp> // for set_data_dir
#include <boost/filesystem.hpp>
#include <boost/system/error_code.hpp>
#include <catch2/catch_session.hpp>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace {
// why: embedding the fake plugin keeps this test self-contained. The PEP 723
// block declares a printer-connection plugin, and the decorated plugin package
// registers one PrinterAgentBase capability whose AgentInfo.id is the registry
// key asserted below.
constexpr const char* kFakePluginSource = R"PY(# /// script
# requires-python = ">=3.12"
# dependencies = []
#
# [tool.orcaslicer.plugin]
# name = "Lifecycle Test Agent"
# description = "Minimal printer-agent plugin for the lifecycle test."
# author = "tests"
# version = "1.0.0"
# type = "printer-connection"
# ///
import orca
class LifecycleTestAgentCapability(orca.printer_agent.PrinterAgentBase):
def get_name(self):
return "Lifecycle Test Agent"
def get_agent_info(self):
return orca.printer_agent.AgentInfo(
id="lifecycle-test-agent",
name="Lifecycle Test Agent",
version="1.0.0",
description="Lifecycle test printer agent",
)
@orca.plugin
class LifecycleTestPlugin(orca.base):
def register_capabilities(self):
orca.register_capability(LifecycleTestAgentCapability)
)PY";
// why: in production GUI_App::init_plugin_gui_wiring subscribes the agent-registry
// callbacks; the test binary has no GUI, so install the same UNLOAD-side wiring
// once so the tests exercise the production deregister-on-unload path.
// note: the load-side (register) wiring is deliberately NOT installed - the two
// concurrent load_plugin calls in the duplicate-id test would race for the id;
// the tests register manually, in a deterministic order, instead.
void install_agent_registry_wiring()
{
static bool installed = false;
if (installed)
return;
installed = true;
PluginManager& mgr = PluginManager::instance();
mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
mgr.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
});
}
} // namespace
// ===========================================================================
// PRINTER-AGENT PLUGIN LIFECYCLE: load, register, unload, deregister.
//
// This test uses its own executable because load_plugin runs on a detached worker
// thread that needs the GIL released on the main thread (the PythonInterpreter
// model). slic3rutils_tests' other Python tests hold the GIL on the main thread
// via a bare scoped_interpreter; the two models can't share one process.
//
// When bundled Python is unavailable in the test environment, the test is
// skipped so source-only or partially staged builds can still run the rest of
// the suite.
// ===========================================================================
TEST_CASE("plugin lifecycle: printer-agent load registers and unload deregisters", "[plugin][lifecycle][Python]")
{
const std::string plugin_key = "LifecycleTestAgent"; // entry-file stem
const std::string agent_id = "lifecycle-test-agent"; // AgentInfo.id from the plugin
// Stage a throwaway data directory. Plugins are discovered under
// <data_dir>/orca_plugins, so this controls which plugin is loaded.
const fs::path data_dir = fs::temp_directory_path() / "orca-plugin-lifecycle-test";
const fs::path plugin_dir = data_dir / "orca_plugins" / plugin_key;
{
boost::system::error_code ec;
fs::remove_all(data_dir, ec); // clear any stale run
}
fs::create_directories(plugin_dir);
{
std::ofstream out((plugin_dir / (plugin_key + ".py")).string(), std::ios::binary);
out << kFakePluginSource;
}
// why: best-effort cleanup even if an assertion throws.
struct DirGuard
{
fs::path p;
~DirGuard()
{
boost::system::error_code ec;
fs::remove_all(p, ec);
}
} guard{data_dir};
Slic3r::set_data_dir(data_dir.string());
// Initialize the plugin system on this thread. If the bundled Python home
// is not reachable, skip gracefully.
PluginManager& mgr = PluginManager::instance();
if (!mgr.initialize())
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
install_agent_registry_wiring();
// Discover synchronously so the catalog holds the descriptor before loading.
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
INFO("expected plugin_key (entry-file stem): " << plugin_key);
PluginDescriptor descriptor;
REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor));
// Load on the worker thread and block until it finishes.
std::string error;
mgr.load_plugin(plugin_key, /*skip_deps=*/true);
const bool loaded = mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error);
INFO("plugin load error: " << error);
REQUIRE(loaded);
REQUIRE(mgr.is_plugin_loaded(plugin_key));
// Resolve the one PrinterConnection capability and register it as an agent.
auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection);
REQUIRE(caps.size() == 1);
REQUIRE(caps[0] != nullptr);
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
// The AgentInfo.id returned by the plugin is now in the registry.
CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id));
// Unloading the plugin deregisters its Python-backed agent.
REQUIRE(mgr.unload_plugin(plugin_key));
// The registry no longer contains the agent id after unload.
CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(agent_id));
}
namespace {
// why: duplicate-id coverage needs two distinct plugins with different package
// keys and classes while both return the same AgentInfo.id.
std::string make_agent_plugin_source(const std::string& suffix, const std::string& display_name, const std::string& agent_id)
{
return std::string{}
+ "# /// script\n"
+ "# requires-python = \">=3.12\"\n"
+ "# dependencies = []\n"
+ "#\n"
+ "# [tool.orcaslicer.plugin]\n"
+ "# name = \"" + display_name + "\"\n"
+ "# description = \"Duplicate-id fake printer-agent plugin.\"\n"
+ "# author = \"tests\"\n"
+ "# version = \"1.0.0\"\n"
+ "# type = \"printer-connection\"\n"
+ "# ///\n"
+ "import orca\n"
+ "\n\n"
+ "class Cap" + suffix + "(orca.printer_agent.PrinterAgentBase):\n"
+ " def get_name(self):\n"
+ " return \"" + display_name + "\"\n"
+ "\n"
+ " def get_agent_info(self):\n"
+ " return orca.printer_agent.AgentInfo(\n"
+ " id=\"" + agent_id + "\", name=\"" + display_name + "\",\n"
+ " version=\"1.0.0\", description=\"duplicate id test\")\n"
+ "\n\n"
+ "@orca.plugin\n"
+ "class Plugin" + suffix + "(orca.base):\n"
+ " def register_capabilities(self):\n"
+ " orca.register_capability(Cap" + suffix + ")\n";
}
void stage_plugin(const fs::path& data_dir, const std::string& plugin_key, const std::string& source)
{
const fs::path dir = data_dir / "orca_plugins" / plugin_key;
fs::create_directories(dir);
std::ofstream out((dir / (plugin_key + ".py")).string(), std::ios::binary);
out << source;
}
} // namespace
// ===========================================================================
// DUPLICATE PRINTER-AGENT IDS
// When two loaded plugins return the same AgentInfo.id, the first registration
// keeps ownership. Unloading it removes the id, and the rejected plugin is not
// promoted automatically. Manual re-registration is required.
// ===========================================================================
TEST_CASE("duplicate agent id is rejected and the winner is not clobbered", "[plugin][lifecycle][Python]")
{
const std::string key_a = "DuplicateIdAgentA"; // winner, registered first
const std::string key_b = "DuplicateIdAgentB"; // duplicate, rejected
const std::string dup_id = "duplicate-id-agent"; // both plugins return this AgentInfo.id
const fs::path data_dir = fs::temp_directory_path() / "orca-duplicate-agent-test";
{
boost::system::error_code ec;
fs::remove_all(data_dir, ec);
}
stage_plugin(data_dir, key_a, make_agent_plugin_source("A", "Duplicate Id Agent A", dup_id));
stage_plugin(data_dir, key_b, make_agent_plugin_source("B", "Duplicate Id Agent B", dup_id));
struct DirGuard
{
fs::path p;
~DirGuard()
{
boost::system::error_code ec;
fs::remove_all(p, ec);
}
} guard{data_dir};
Slic3r::set_data_dir(data_dir.string());
PluginManager& mgr = PluginManager::instance();
if (!mgr.initialize())
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
install_agent_registry_wiring();
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
PluginDescriptor descriptor_a;
PluginDescriptor descriptor_b;
REQUIRE(mgr.try_get_valid_plugin_descriptor(key_a, descriptor_a));
REQUIRE(mgr.try_get_valid_plugin_descriptor(key_b, descriptor_b));
std::string error;
mgr.load_plugin(key_a, /*skip_deps=*/true);
mgr.load_plugin(key_b, /*skip_deps=*/true);
const bool loaded_a = mgr.wait_for_plugin_load(key_a, std::chrono::seconds(60), error);
INFO("plugin A load error: " << error);
REQUIRE(loaded_a);
const bool loaded_b = mgr.wait_for_plugin_load(key_b, std::chrono::seconds(60), error);
INFO("plugin B load error: " << error);
REQUIRE(loaded_b);
auto caps_a = mgr.get_plugin_capabilities(key_a, PluginCapabilityType::PrinterConnection);
auto caps_b = mgr.get_plugin_capabilities(key_b, PluginCapabilityType::PrinterConnection);
REQUIRE(caps_a.size() == 1);
REQUIRE(caps_b.size() == 1);
// Register A first as the owner, then B with the duplicate id.
NetworkAgentFactory::register_python_printer_agent(key_a, caps_a[0]->name());
NetworkAgentFactory::register_python_printer_agent(key_b, caps_b[0]->name());
// The shared id is registered, and still owned by A; B did not replace it.
CHECK(NetworkAgentFactory::is_printer_agent_registered(dup_id));
const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(dup_id);
REQUIRE(info != nullptr);
CHECK(info->plugin_identifier.find(key_a) != std::string::npos); // owned by A
CHECK(info->plugin_identifier.find(key_b) == std::string::npos); // B never took ownership
// Unload the owner. The id is not promoted to the still loaded duplicate.
REQUIRE(mgr.unload_plugin(key_a));
CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(dup_id));
mgr.unload_plugin(key_b); // the duplicate was loaded but never registered
}
// ===========================================================================
// NATIVE (BUILT-IN) AGENT ID COLLISION
// A plugin may not hijack a built-in agent id (e.g. "bbl"). The built-in keeps
// ownership and the plugin's agent is rejected.
// ===========================================================================
TEST_CASE("printer-agent plugin cannot claim a built-in agent id", "[plugin][lifecycle][Python]")
{
// Register the native built-ins so "bbl"/"orca" occupy the registry.
NetworkAgentFactory::register_all_agents();
REQUIRE(NetworkAgentFactory::is_printer_agent_registered(BBL_PRINTER_AGENT_ID));
const std::string plugin_key = "BuiltinClashAgent";
const fs::path data_dir = fs::temp_directory_path() / "orca-builtin-clash-test";
{
boost::system::error_code ec;
fs::remove_all(data_dir, ec);
}
stage_plugin(data_dir, plugin_key, make_agent_plugin_source("Clash", "Builtin Clash", BBL_PRINTER_AGENT_ID));
struct DirGuard
{
fs::path p;
~DirGuard()
{
boost::system::error_code ec;
fs::remove_all(p, ec);
}
} guard{data_dir};
Slic3r::set_data_dir(data_dir.string());
PluginManager& mgr = PluginManager::instance();
if (!mgr.initialize())
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
install_agent_registry_wiring();
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
PluginDescriptor descriptor;
REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor));
std::string error;
mgr.load_plugin(plugin_key, /*skip_deps=*/true);
REQUIRE(mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error));
auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection);
REQUIRE(caps.size() == 1);
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
// "bbl" is still the native built-in, not the plugin.
const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(BBL_PRINTER_AGENT_ID);
REQUIRE(info != nullptr);
CHECK_FALSE(info->is_plugin());
CHECK(info->plugin_identifier.find(plugin_key) == std::string::npos);
mgr.unload_plugin(plugin_key);
}
// ===========================================================================
// RE-REGISTERING THE SAME CAPABILITY IS A REFRESH, NOT A DUPLICATE
// The guard rejects only a DIFFERENT owner. The same capability registering its
// own id again must stay registered (insert_or_assign refreshes it in place).
// ===========================================================================
TEST_CASE("re-registering the same capability keeps its agent registered", "[plugin][lifecycle][Python]")
{
const std::string plugin_key = "ReRegisterAgent";
const std::string agent_id = "re-register-agent";
const fs::path data_dir = fs::temp_directory_path() / "orca-re-register-test";
{
boost::system::error_code ec;
fs::remove_all(data_dir, ec);
}
stage_plugin(data_dir, plugin_key, make_agent_plugin_source("Re", "Re Register", agent_id));
struct DirGuard
{
fs::path p;
~DirGuard()
{
boost::system::error_code ec;
fs::remove_all(p, ec);
}
} guard{data_dir};
Slic3r::set_data_dir(data_dir.string());
PluginManager& mgr = PluginManager::instance();
if (!mgr.initialize())
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
install_agent_registry_wiring();
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
PluginDescriptor descriptor;
REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor));
std::string error;
mgr.load_plugin(plugin_key, /*skip_deps=*/true);
REQUIRE(mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error));
auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection);
REQUIRE(caps.size() == 1);
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
REQUIRE(NetworkAgentFactory::is_printer_agent_registered(agent_id));
const PrinterAgentInfo* first = NetworkAgentFactory::get_printer_agent_info(agent_id);
REQUIRE(first != nullptr);
const std::string owner = first->plugin_identifier;
// The same capability registering again is a refresh: still registered, same owner, not rejected.
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id));
const PrinterAgentInfo* second = NetworkAgentFactory::get_printer_agent_info(agent_id);
REQUIRE(second != nullptr);
CHECK(second->plugin_identifier == owner);
mgr.unload_plugin(plugin_key);
}
// why: this binary embeds CPython through the PythonInterpreter singleton, which
// lives for the full process. Normal static destruction can tear Python down
// while C++ objects still hold Python handles, producing a Windows heap
// corruption after the assertions have finished. The app has an ordered shutdown
// path; this test harness does not, so it returns the Catch2 result through
// _Exit after flushing output.
int main(int argc, char* argv[])
{
const int result = Catch::Session().run(argc, argv);
std::cout.flush();
std::cerr.flush();
std::fflush(nullptr);
std::_Exit(result);
}