Compare commits

..
Author SHA1 Message Date
Ian Chua b2bc4e358b fix: updated stale docs 2026-09-15 15:00:59 +08:00
Ian Chua dafa9ba83e Revert "fix: latent ams in use bug"
This reverts commit 910dbcd2e6.
2026-09-15 14:36:43 +08:00
Ian Chua 910dbcd2e6 fix: latent ams in use bug 2026-09-15 14:34:50 +08:00
Ian Chua 482380ca3d feat: restore printer agent implementations 2026-09-15 13:35:55 +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
121 changed files with 9961 additions and 2209 deletions
-23
View File
@@ -684,18 +684,6 @@ jobs:
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/Release/OrcaSlicer_profile_validator' path: './build/src/Release/OrcaSlicer_profile_validator'
# generate_system_cache is what scripts/build_preset_cache.sh bakes the
# <vendor>.opc caches with; it was already built by the "Build system
# preset cache (Linux)" step above. The .opc format is 64-bit
# little-endian native, i.e. identical across every platform Orca ships,
# so only the Linux binary is published.
- name: Upload generate_system_cache Ubuntu
if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: actions/upload-artifact@v7
with:
name: generate_system_cache_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/dev-utils/Release/generate_system_cache'
- name: Deploy Ubuntu release - name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
@@ -726,17 +714,6 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
max_releases: 1 max_releases: 1
- name: Deploy Ubuntu generate_system_cache release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/src/dev-utils/Release/generate_system_cache
asset_name: generate_system_cache_Linux${{ env.ubuntu-ver-str }}_nightly
asset_content_type: application/octet-stream
max_releases: 1
- name: Deploy orca_custom_preset_tests - name: Deploy orca_custom_preset_tests
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: WebFreak001/deploy-nightly@v3.2.0 uses: WebFreak001/deploy-nightly@v3.2.0
-219
View File
@@ -1,219 +0,0 @@
# Nightly parity checks from OrcaSlicer/orca-test-repo, kept out of the
# per-build "Run external slicer regression tests" step because they take far
# longer than that step's budget:
# effect - the CLI override sweep's full effect stage: every landed option
# re-sliced on its own to see whether it changes the G-code
# harness - the GUI-vs-CLI parity harness (metrics only, never fails)
# Both test the latest successful build_all.yml Linux AppImage from main, with
# sources checked out at the commit that build was made from. Nothing here
# gates a build or a PR.
name: Parity Nightly
on:
schedule:
# build_all.yml starts at 17:00 UTC and has finished by ~20:00
- cron: "0 21 * * *"
workflow_dispatch:
inputs:
test_repo_ref:
description: "orca-test-repo ref to run"
required: false
default: "main"
build_branch:
description: "branch whose latest successful build_all artifact to test"
required: false
default: "main"
fixtures:
description: "harness fixture ids, space-separated (empty = all)"
required: false
default: ""
cli_presets:
description: "harness lane C presets: flat = flatten inherits first, raw = leaf profile as-is"
required: false
default: "flat"
permissions:
contents: read
actions: read
jobs:
build:
name: Find the build to test
# Don't run scheduled checks on forks
if: github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer'
runs-on: ubuntu-24.04
outputs:
run_id: ${{ steps.find.outputs.run_id }}
head_sha: ${{ steps.find.outputs.head_sha }}
steps:
- id: find
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run list --workflow build_all.yml \
--branch "${{ inputs.build_branch || 'main' }}" \
--status success --limit 1 --json databaseId,headSha \
--jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \
>> "$GITHUB_OUTPUT"
cat "$GITHUB_OUTPUT"
effect:
name: Override sweep effect stage (shard ${{ matrix.shard }})
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
# orca-test-repo's parity/effect_routing.json holds a 2-way split,
# ~12.5 min a shard on this runner
shard: [0, 1]
steps:
- &checkout-suite
name: Check out the test suite
uses: actions/checkout@v7
with:
repository: OrcaSlicer/orca-test-repo
ref: ${{ inputs.test_repo_ref || 'main' }}
path: orca-test-repo
# The AppImage ships only packed preset caches, so profiles and the CLI
# option surface come from the sources the build was made from
- &checkout-slicer
name: Check out OrcaSlicer at the build's commit
uses: actions/checkout@v7
with:
ref: ${{ needs.build.outputs.head_sha }}
path: slicer
lfs: 'false'
- &extract-appimage
name: Download and extract the Linux AppImage
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run download "${{ needs.build.outputs.run_id }}" --dir appimage \
--pattern "OrcaSlicer_Linux_ubuntu_2404*"
appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1)
[ -n "$appimage" ] || { echo "no x86_64 AppImage in run ${{ needs.build.outputs.run_id }}"; exit 1; }
chmod +x "$appimage"
"$appimage" --appimage-extract > /dev/null
# The bare binary cannot find the AppImage's bundled libraries; AppRun
# sets them up and execs it, so exit codes and signals pass through
[ -x squashfs-root/AppRun ] || { echo "no AppRun in the AppImage"; exit 1; }
echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV"
echo "ORCA_SOURCE=$PWD/slicer" >> "$GITHUB_ENV"
- name: Install the AppImage's host runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install suite dependencies
run: pip install -r orca-test-repo/requirements.txt
- name: Run the override sweep with the full effect stage
id: run
continue-on-error: true
working-directory: orca-test-repo
run: |
set -o pipefail
# -rA keeps the per-stage summaries, which pytest otherwise swallows
# for passing tests
python -m pytest test_cli_overrides.py -c pytest.ini -v -rA \
--effect-full --effect-shard ${{ matrix.shard }}/2 \
--orca-bin "$ORCA_BIN" --orca-source "$ORCA_SOURCE" \
2>&1 | tee ../sweep.log
- name: Publish job summary
if: always()
run: |
{
echo "## Override sweep effect stage, shard ${{ matrix.shard }}/2"
echo "Build ${{ needs.build.outputs.head_sha }} (run ${{ needs.build.outputs.run_id }})"
echo '```'
grep -E "\[override sweep" sweep.log || echo "no stage summaries, see the log"
grep -E "^=+ .*(passed|failed)" sweep.log | tail -1 || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload the override report
if: always()
uses: actions/upload-artifact@v7
with:
name: override-report-shard${{ matrix.shard }}
path: |
orca-test-repo/.pytest_cache/override_report.json
sweep.log
if-no-files-found: warn
retention-days: 30
# The sweep step continues on error so the summary and report still get
# published; this puts the failure back on the job
- name: Fail the job if the sweep failed
if: steps.run.outcome == 'failure'
run: |
echo "the override sweep failed, see the job summary and the uploaded report" >&2
exit 1
harness:
name: GUI-vs-CLI parity harness
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- *checkout-suite
- *checkout-slicer
- *extract-appimage
- name: Install display tooling and the AppImage's host runtime
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb xdotool imagemagick openbox mesa-utils \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- name: Run the parity harness
run: |
set -euo pipefail
fixtures=()
for f in ${{ inputs.fixtures || '' }}; do
fixtures+=(--fixture "$f")
done
# 2 GUI displays: ~1.5 cores peak / ~1.9 GB on this 4-vCPU runner,
# and each fixture is fully isolated, so results match a serial run
python3 orca-test-repo/parity/run_parity.py \
--slicer-root "$ORCA_SOURCE" --bin "$ORCA_BIN" \
--cli-presets "${{ inputs.cli_presets || 'flat' }}" \
--gui-workers 2 --out "$PWD/parity-out" "${fixtures[@]}"
- name: Publish job summary
if: always()
run: |
if [ -f parity-out/report.md ]; then
cat parity-out/report.md >> "$GITHUB_STEP_SUMMARY"
else
echo "the harness produced no report, see the log" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Drop per-lane datadirs before upload
if: always()
run: rm -rf parity-out/*/seed parity-out/*/datadir-* || true
- name: Upload the scorecard and evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: parity-scorecard
path: parity-out/
if-no-files-found: warn
retention-days: 30
-253
View File
@@ -1,253 +0,0 @@
name: Post-merge profiles
# Push-triggered counterpart to check_profiles.yml (which only gates PRs). When a
# profile change lands on main or a release branch, rebuild the affected vendors'
# binary preset caches (<vendor>.opc) and publish each as a versioned ZIP asset on
# a per-Orca-version release of the profiles repo. From there OrcaCloud's OTA
# Manager lists the asset, a maintainer attaches a changelog and hits Publish, and
# only then does it become a live OTA update - this workflow does none of that
# last part (no changelog, no R2, no webhook).
#
# Asset contract expected by OrcaCloud's release scanner:
# ^(\d+\.\d+\.\d+)_([^_]+)_(\d+(?:\.\d+){3})_(\d{12})\.zip$
# <orca_ver>_<vendor>_<profile_version>_<UTC yyyymmddHHMM>.zip (zip root: <vendor>.opc)
#
# Setup (App + secrets): docs/ota/post-merge-profiles-setup.md
on:
push:
branches:
- main
- release/*
paths:
- 'resources/profiles/**'
- '.github/workflows/post_merge_profiles.yml'
workflow_dispatch:
permissions:
contents: read
# One run per branch; let a run finish rather than cancel it, since it publishes.
concurrency:
group: post-merge-profiles-${{ github.ref }}
cancel-in-progress: false
env:
# generate_system_cache is published to this repo's own nightly-builds release
# by build_orca.yml's Linux leg. The job guard pins github.repository to
# OrcaSlicer/OrcaSlicer, so this resolves there.
TOOL_REPO: ${{ github.repository }}
TOOL_ASSET: generate_system_cache_Linux_Ubuntu2404_nightly
# Where per-vendor ZIP assets are published; OrcaCloud's OTA reads this repo.
PROFILES_OWNER: OrcaSlicer
PROFILES_REPO: orcaslicer-profiles
jobs:
publish_profile_caches:
name: Publish profile caches
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
# FOLDER_MERGERS is an environment-scoped variable, shared with the PR
# merge bot. Keep this environment free of protection rules so this
# push-triggered job does not wait for a reviewer.
environment: merge-delegation
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# Enough history to reach github.event.before for the changed-vendor
# diff on a normal push; deeper pushes fall back to HEAD^..HEAD in the
# step below. fetch-depth: 0 would clone all of OrcaSlicer's history.
fetch-depth: 50
- name: Resolve changed vendors
id: vendors
shell: bash
env:
FOLDER_MERGERS: ${{ vars.FOLDER_MERGERS }}
run: |
set -euo pipefail
base='${{ github.event.before }}'
head='${{ github.sha }}'
# Zero SHA (branch created / force push) or manual dispatch: fall back
# to this commit's own diff.
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
base="$head^"
fi
echo "Diffing $base..$head"
mapfile -t candidates < <(
git diff --name-only "$base" "$head" -- resources/profiles \
| sed -nE 's#^resources/profiles/([^/]+)/.*#\1#p; s#^resources/profiles/([^/]+)\.json$#\1#p' \
| sort -u
)
vendors=()
for v in "${candidates[@]:-}"; do
[ -n "$v" ] || continue
json="resources/profiles/$v.json"
# A vendor has a manifest plus either a preset directory or a version
# field; this drops non-vendor files such as blacklist.json.
if [ -f "$json" ] && { [ -d "resources/profiles/$v" ] || jq -e '.version' "$json" >/dev/null 2>&1; }; then
vendors+=("$v")
fi
done
if [ "${#vendors[@]}" -eq 0 ]; then
echo "No changed vendor profiles in this push; nothing to publish."
echo "vendors=" >> "$GITHUB_OUTPUT"
exit 0
fi
# A vendor is eligible only when both the profile directory and its
# sibling bundle JSON are covered by at least one FOLDER_MERGERS
# grant. The account part is intentionally ignored here: this is a
# post-merge safety check, not an authorization check for a command.
grants=()
while IFS= read -r raw_line; do
line="${raw_line#"${raw_line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[ -n "$line" ] || continue
[[ "$line" == \#* ]] && continue
[[ "$line" == *:* ]] || continue
grant="${line#*:}"
grant="${grant#"${grant%%[![:space:]]*}"}"
grant="${grant%"${grant##*[![:space:]]}"}"
while [[ "$grant" == */ ]]; do grant="${grant%/}"; done
grants+=("$grant")
done <<< "${FOLDER_MERGERS:-}"
is_granted() {
local path="$1"
local grant
for grant in "${grants[@]:-}"; do
if [[ "$path" == "$grant" || "$path" == "$grant/"* ]]; then
return 0
fi
done
return 1
}
unauthorized=()
for v in "${vendors[@]}"; do
if ! is_granted "resources/profiles/$v" || ! is_granted "resources/profiles/$v.json"; then
unauthorized+=("$v")
fi
done
if [ "${#unauthorized[@]}" -ne 0 ]; then
echo "Changed vendor profiles are not covered by FOLDER_MERGERS: ${unauthorized[*]}"
echo "No profile caches will be published for this push."
echo "vendors=" >> "$GITHUB_OUTPUT"
exit 0
fi
printf 'Changed vendors: %s\n' "${vendors[*]}"
echo "vendors=${vendors[*]}" >> "$GITHUB_OUTPUT"
- name: Resolve Orca version
id: orca
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
raw="$(sed -nE 's/^set\(SoftFever_VERSION "([^"]+)".*/\1/p' version.inc | head -1)"
[ -n "$raw" ] || { echo "::error::could not read SoftFever_VERSION from version.inc"; exit 1; }
orca_ver="${raw%%-*}"
if ! [[ "$orca_ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Orca version '$orca_ver' (from '$raw') is not X.Y.Z"; exit 1
fi
# release_tag is what the desktop client sends as orca_version and what
# OrcaCloud keys R2 on; orca_ver (X.Y.Z) is the asset-name prefix.
echo "release_tag=$raw" >> "$GITHUB_OUTPUT"
echo "orca_ver=$orca_ver" >> "$GITHUB_OUTPUT"
echo "Orca version: release_tag=$raw asset_prefix=$orca_ver"
- name: Validate profile versions
id: pver
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
: > "$RUNNER_TEMP/pver.tsv"
for v in ${{ steps.vendors.outputs.vendors }}; do
pv="$(jq -r '.version // empty' "resources/profiles/$v.json")"
if ! [[ "$pv" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::vendor $v version '${pv:-<none>}' must be 4 numeric parts (A.B.C.D) for the OTA asset name; fix resources/profiles/$v.json"
exit 1
fi
printf '%s\t%s\n' "$v" "$pv" >> "$RUNNER_TEMP/pver.tsv"
echo "$v -> $pv"
done
- name: Download generate_system_cache
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
# gh (with the default token) rather than an unauthenticated curl: keeps
# working if TOOL_REPO is ever private and avoids anonymous rate limits.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release download nightly-builds --repo "$TOOL_REPO" \
--pattern "$TOOL_ASSET" --output generate_system_cache --clobber
chmod +x generate_system_cache
- name: Build caches and package assets
id: pkg
if: steps.vendors.outputs.vendors != ''
shell: bash
run: |
set -euo pipefail
# One timestamp for the whole run so a multi-vendor merge groups together.
ts="$(date -u +%Y%m%d%H%M)"
orca_ver='${{ steps.orca.outputs.orca_ver }}'
out="$RUNNER_TEMP/assets"
mkdir -p "$out"
for v in ${{ steps.vendors.outputs.vendors }}; do
./generate_system_cache -p "$GITHUB_WORKSPACE/resources/profiles" -v "$v" -l 2
opc="resources/profiles/$v.opc"
[ -f "$opc" ] || { echo "::error::$opc was not generated"; exit 1; }
pv="$(awk -F'\t' -v v="$v" '$1==v{print $2}' "$RUNNER_TEMP/pver.tsv")"
name="${orca_ver}_${v}_${pv}_${ts}.zip"
( cd resources/profiles && zip -q -j "$out/$name" "$v.opc" )
echo "packaged $name"
done
echo "dir=$out" >> "$GITHUB_OUTPUT"
- name: Mint profiles-repo token
id: token
if: steps.vendors.outputs.vendors != ''
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PROFILES_APP_ID }}
private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }}
owner: ${{ env.PROFILES_OWNER }}
repositories: ${{ env.PROFILES_REPO }}
- name: Publish assets to profiles release
if: steps.vendors.outputs.vendors != ''
shell: bash
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
RELEASE_TAG: ${{ steps.orca.outputs.release_tag }}
ASSET_DIR: ${{ steps.pkg.outputs.dir }}
run: |
set -euo pipefail
repo="$PROFILES_OWNER/$PROFILES_REPO"
if ! gh release view "$RELEASE_TAG" --repo "$repo" >/dev/null 2>&1; then
echo "Creating release $RELEASE_TAG on $repo"
gh release create "$RELEASE_TAG" --repo "$repo" \
--title "$RELEASE_TAG" --notes "Profile cache assets for Orca $RELEASE_TAG." \
--latest=false
fi
# Asset names are timestamp-unique; a clash means a bug, so don't --clobber.
gh release upload "$RELEASE_TAG" --repo "$repo" "$ASSET_DIR"/*.zip
{
echo "### Published to \`$repo\` release \`$RELEASE_TAG\`"
for f in "$ASSET_DIR"/*.zip; do echo "- \`$(basename "$f")\`"; done
} >> "$GITHUB_STEP_SUMMARY"
+71 -9
View File
@@ -587,15 +587,10 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
add_compile_options(-Wno-${w}) add_compile_options(-Wno-${w})
endforeach () endforeach ()
# GCC is not built in CI, so don't throw errors CI won't catch. # Turn everything else into an error. Dependency headers are exempt because the SYSTEM
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their diagnostics out,
add_compile_options(-Werror=return-type) # apart from GCC's maybe-uninitialized, demoted below.
else ()
# Turn everything else into an error. Dependency headers are exempt because the
# SYSTEM include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their
# diagnostics out.
add_compile_options(-Werror) add_compile_options(-Werror)
endif ()
# Demoted. Remove a name once its category is cleared on every compiler. # Demoted. Remove a name once its category is cleared on every compiler.
set(warnings_demoted) set(warnings_demoted)
@@ -617,6 +612,20 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
cast-function-type-mismatch cast-function-type-mismatch
) )
endif () endif ()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND warnings_demoted
# maybe-uninitialized runs after inlining and reports inside boost/variant,
# boost/tuple and the bundled clipper header even with -isystem.
maybe-uninitialized
# array-bounds is reported once, where ConfigOptionVector::set_at inlines
# into OrcaSlicer.cpp on a branch the preceding type test rules out.
array-bounds
# template-id-cdtor is a GCC 14+ warning in the bundled Clipper2 headers.
template-id-cdtor
)
endif ()
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND warnings_demoted list(APPEND warnings_demoted
# enum-constexpr-conversion is a Clang warning that defaults to an error, # enum-constexpr-conversion is a Clang warning that defaults to an error,
@@ -821,6 +830,49 @@ find_package(OpenSSL REQUIRED)
find_package(CURL REQUIRED) find_package(CURL REQUIRED)
find_package(Freetype REQUIRED) find_package(Freetype REQUIRED)
if (SLIC3R_GUI)
# LibDataChannel's installed export references its bundled dependencies,
# but does not install their CMake targets. Recreate those targets from
# the same dependency prefix before loading the LibDataChannel config.
if (NOT TARGET Usrsctp::usrsctp)
find_library(_ORCA_USRSCTP_LIBRARY NAMES usrsctp
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
if (_ORCA_USRSCTP_LIBRARY)
add_library(Usrsctp::usrsctp UNKNOWN IMPORTED GLOBAL)
set_target_properties(Usrsctp::usrsctp PROPERTIES
IMPORTED_LOCATION "${_ORCA_USRSCTP_LIBRARY}"
IMPORTED_LINK_INTERFACE_LANGUAGES C
INTERFACE_LINK_LIBRARIES "Threads::Threads")
endif()
endif()
if (NOT TARGET 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) add_library(libcurl INTERFACE)
target_link_libraries(libcurl INTERFACE CURL::libcurl) target_link_libraries(libcurl INTERFACE CURL::libcurl)
@@ -1103,6 +1155,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll ${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll
${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll ${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll
${CMAKE_PREFIX_PATH}/bin/freetype.dll ${CMAKE_PREFIX_PATH}/bin/freetype.dll
${CMAKE_PREFIX_PATH}/bin/avformat-61.dll
${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll ${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll
${CMAKE_PREFIX_PATH}/bin/swresample-5.dll ${CMAKE_PREFIX_PATH}/bin/swresample-5.dll
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll ${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
@@ -1142,6 +1195,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${_out_dir}/TKXSBase.dll ${_out_dir}/TKXSBase.dll
${_out_dir}/freetype.dll ${_out_dir}/freetype.dll
${_out_dir}/avformat-61.dll
${_out_dir}/avcodec-61.dll ${_out_dir}/avcodec-61.dll
${_out_dir}/swresample-5.dll ${_out_dir}/swresample-5.dll
${_out_dir}/swscale-8.dll ${_out_dir}/swscale-8.dll
@@ -1164,7 +1218,10 @@ function(orcaslicer_copy_sos target config postfix output_sos)
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}") set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
endif () endif ()
file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so file(COPY ${CMAKE_PREFIX_PATH}/lib/libavformat.so
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61.1.100
${CMAKE_PREFIX_PATH}/lib/libavcodec.so
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61 ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100 ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100
${CMAKE_PREFIX_PATH}/lib/libavutil.so ${CMAKE_PREFIX_PATH}/lib/libavutil.so
@@ -1179,6 +1236,9 @@ function(orcaslicer_copy_sos target config postfix output_sos)
DESTINATION ${_out_dir}) DESTINATION ${_out_dir})
set(${output_sos} set(${output_sos}
${_out_dir}/libavformat.so
${_out_dir}/libavformat.so.61
${_out_dir}/libavformat.so.61.1.100
${_out_dir}/libavcodec.so ${_out_dir}/libavcodec.so
${_out_dir}/libavcodec.so.61 ${_out_dir}/libavcodec.so.61
${_out_dir}/libavcodec.so.61.3.100 ${_out_dir}/libavcodec.so.61.3.100
@@ -1308,6 +1368,8 @@ endif ()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux") if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LIBRARY_FILES set(LIBRARY_FILES
${LIBDIR_BIN}/libavformat.so.61
${LIBDIR_BIN}/libavformat.so.61.1.100
${LIBDIR_BIN}/libavcodec.so.61 ${LIBDIR_BIN}/libavcodec.so.61
${LIBDIR_BIN}/libavcodec.so.61.3.100 ${LIBDIR_BIN}/libavcodec.so.61.3.100
${LIBDIR_BIN}/libavutil.so.59 ${LIBDIR_BIN}/libavutil.so.59
+3
View File
@@ -389,6 +389,8 @@ if(NOT OPENSSL_FOUND)
set(OPENSSL_PKG dep_OpenSSL) set(OPENSSL_PKG dep_OpenSSL)
endif() endif()
include(DataChannel/DataChannel.cmake)
# we don't want to load a "wrong" openssl when loading curl # we don't want to load a "wrong" openssl when loading curl
# so, just don't even bother # so, just don't even bother
# ...i think this is how it works? change if wrong # ...i think this is how it works? change if wrong
@@ -461,6 +463,7 @@ set(_dep_list
dep_wxInspector dep_wxInspector
dep_FFMPEG dep_FFMPEG
dep_Assimp dep_Assimp
dep_DataChannel
) )
if (MSVC) if (MSVC)
+20
View File
@@ -0,0 +1,20 @@
# 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
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
)
+2 -2
View File
@@ -69,14 +69,14 @@ else ()
--disable-filters --disable-filters
--enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test* --enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test*
--disable-protocols --disable-protocols
--enable-protocol=file,fd,pipe,rtp,udp --enable-protocol=file,fd,pipe,http,rtp,tcp,udp
--disable-muxers --disable-muxers
--enable-muxer=rtp --enable-muxer=rtp
--disable-encoders --disable-encoders
--disable-decoders --disable-decoders
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv* --enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
--disable-demuxers --disable-demuxers
--enable-demuxer=h264,mp3,mov --enable-demuxer=h264,mp3,mov,mpjpeg,rtsp,sdp
--disable-zlib --disable-zlib
--disable-avdevice --disable-avdevice
BUILD_IN_SOURCE ON BUILD_IN_SOURCE ON
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "Snapmaker", "name": "Snapmaker",
"version": "02.04.00.13", "version": "02.04.00.12",
"force_update": "0", "force_update": "0",
"description": "Snapmaker configurations", "description": "Snapmaker configurations",
"machine_model_list": [ "machine_model_list": [
@@ -15,13 +15,13 @@
"1" "1"
], ],
"cool_plate_temp": [ "cool_plate_temp": [
"100" "105"
], ],
"cool_plate_temp_initial_layer": [ "cool_plate_temp_initial_layer": [
"100" "105"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
"100" "105"
], ],
"eng_plate_temp_initial_layer": [ "eng_plate_temp_initial_layer": [
"100" "100"
@@ -48,7 +48,7 @@
"Polymaker" "Polymaker"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"100" "105"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"100" "100"
@@ -72,7 +72,7 @@
"110.8" "110.8"
], ],
"textured_plate_temp": [ "textured_plate_temp": [
"100" "105"
], ],
"textured_plate_temp_initial_layer": [ "textured_plate_temp_initial_layer": [
"100" "100"
@@ -15,16 +15,16 @@
"1" "1"
], ],
"cool_plate_temp": [ "cool_plate_temp": [
"100" "105"
], ],
"cool_plate_temp_initial_layer": [ "cool_plate_temp_initial_layer": [
"100" "105"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
"100" "105"
], ],
"eng_plate_temp_initial_layer": [ "eng_plate_temp_initial_layer": [
"100" "105"
], ],
"fan_cooling_layer_time": [ "fan_cooling_layer_time": [
"12" "12"
@@ -51,10 +51,10 @@
"Polymaker" "Polymaker"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"100" "105"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"100" "105"
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"300" "300"
@@ -81,10 +81,10 @@
"110" "110"
], ],
"textured_plate_temp": [ "textured_plate_temp": [
"100" "105"
], ],
"textured_plate_temp_initial_layer": [ "textured_plate_temp_initial_layer": [
"100" "105"
], ],
"filament_type": [ "filament_type": [
"ABS" "ABS"
@@ -9,10 +9,10 @@
"" ""
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"100" "110"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"100" "105"
], ],
"overhang_fan_speed": [ "overhang_fan_speed": [
"20" "20"
@@ -9,7 +9,7 @@
"" ""
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"100" "110"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"100" "100"
+3 -1
View File
@@ -575,7 +575,7 @@ function CapabilityCanRun(plugin, capability) {
} }
function IsPluginChecked(plugin) { function IsPluginChecked(plugin) {
return GetStatus(plugin) === "Activated"; return plugin.is_loaded;
} }
function HasMixedCapabilityState(plugin) { function HasMixedCapabilityState(plugin) {
@@ -1347,6 +1347,8 @@ function StatusDescription(plugin) {
return "This plugin is still loading."; return "This plugin is still loading.";
case "Error": case "Error":
return "This plugin is blocked until its error is fixed."; return "This plugin is blocked until its error is fixed.";
case "RuntimeError":
return "This plugin is loaded but a capability reported an error.";
case "Inactive": case "Inactive":
default: default:
return "This plugin is inactive. Activate it to install or load it."; return "This plugin is inactive. Activate it to install or load it.";
@@ -424,6 +424,11 @@ body.pane-resizing {
font-weight: 600; font-weight: 600;
} }
.status-cell.status-runtimeerror {
color: var(--plugin-status-warn);
font-weight: 600;
}
.status-cell.status-loading { .status-cell.status-loading {
color: var(--plugin-status-warn); color: var(--plugin-status-warn);
font-weight: 600; font-weight: 600;
@@ -680,6 +685,11 @@ body.pane-resizing {
color: var(--plugin-status-danger); color: var(--plugin-status-danger);
} }
.detail-status-chip.status-runtimeerror {
background: var(--plugin-status-warn-bg);
color: var(--plugin-status-warn);
}
.detail-status-chip.status-loading { .detail-status-chip.status-loading {
background: var(--plugin-status-warn-bg); background: var(--plugin-status-warn-bg);
color: var(--plugin-status-warn); color: var(--plugin-status-warn);
+7 -105
View File
@@ -53,7 +53,6 @@ using namespace nlohmann;
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/GCode.hpp" #include "libslic3r/GCode.hpp"
@@ -163,7 +162,6 @@ std::map<int, std::string> cli_errors = {
{CLI_FILAMENT_CAN_NOT_MAP, "Some filaments cannot be mapped to correct extruders for multi-extruder Printer."}, {CLI_FILAMENT_CAN_NOT_MAP, "Some filaments cannot be mapped to correct extruders for multi-extruder Printer."},
{CLI_ONLY_ONE_TPU_SUPPORTED, "Not support printing 2 or more TPU filaments."}, {CLI_ONLY_ONE_TPU_SUPPORTED, "Not support printing 2 or more TPU filaments."},
{CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER, "Some filaments cannot be printed on the extruder mapped to."}, {CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER, "Some filaments cannot be printed on the extruder mapped to."},
{CLI_MIXED_FILAMENT_INVALID, "A mixed filament is invalid: its components are different filament types, or it has no filament of its own."},
{CLI_SLICING_ERROR, "Failed slicing the model. Please verify the slicing of all plates on Orca Slicer before uploading."}, {CLI_SLICING_ERROR, "Failed slicing the model. Please verify the slicing of all plates on Orca Slicer before uploading."},
{CLI_GCODE_PATH_CONFLICTS, " G-code conflicts detected after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer. If the file slices normally in Orca Slicer, try moving the wipe tower further from other models, as we use more conservative parameters for it during upload."}, {CLI_GCODE_PATH_CONFLICTS, " G-code conflicts detected after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer. If the file slices normally in Orca Slicer, try moving the wipe tower further from other models, as we use more conservative parameters for it during upload."},
{CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, "Found G-code in unprintable area of multi-extruder printers after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer."} {CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, "Found G-code in unprintable area of multi-extruder printers after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer."}
@@ -1387,25 +1385,6 @@ int CLI::run(int argc, char **argv)
if (downward_check_option) if (downward_check_option)
downward_check = downward_check_option->value; downward_check = downward_check_option->value;
// --export-settings - writes its JSON to stdout, so reject every action or transform that may write there
// too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
// sliced or exported.
if (std::find(m_actions.begin(), m_actions.end(), "export_settings") != m_actions.end() && m_config.opt_string("export_settings") == "-") {
static const std::set<std::string> stdout_compatible = { "export_settings", "uptodate", "load_defaultfila", "min_save",
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
for (const std::vector<std::string> *opt_keys : { &m_actions, &m_transforms }) {
for (const std::string &opt_key : *opt_keys) {
if (stdout_compatible.count(opt_key) == 0) {
std::string flag = opt_key;
std::replace(flag.begin(), flag.end(), '_', '-');
boost::nowide::cerr << "--export-settings - cannot be combined with --" << flag << std::endl;
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
flush_and_exit(CLI_INVALID_PARAMS);
}
}
}
}
bool start_gui = m_actions.empty() && !downward_check; bool start_gui = m_actions.empty() && !downward_check;
if (start_gui) { if (start_gui) {
BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl; BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl;
@@ -2029,21 +2008,19 @@ int CLI::run(int argc, char **argv)
} }
}; };
// One resolver for the whole run, so presets from the same vendor tree share its load. auto resolve_preset = [&ensure_cli_preset_bundle](const std::string &file, DynamicPrintConfig &config,
std::unique_ptr<PresetBundle> system_preset_resolver;
auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
std::string &config_type, const std::string &config_from, std::string &config_type, const std::string &config_from,
bool probe_type, std::string &error) { bool probe_type, std::string &error) {
const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS); const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
if (!probe_type && (inherits == nullptr || inherits->value.empty())) if (!probe_type && (inherits == nullptr || inherits->value.empty()))
return true; return true;
std::unique_ptr<PresetBundle> source_bundle;
PresetBundle *bundle = nullptr; PresetBundle *bundle = nullptr;
bool allow_source_manifest = false; bool allow_source_manifest = false;
if (config_from == "system") { if (config_from == "system") {
if (!system_preset_resolver) source_bundle = std::make_unique<PresetBundle>();
system_preset_resolver = std::make_unique<PresetBundle>(); bundle = source_bundle.get();
bundle = system_preset_resolver.get();
allow_source_manifest = true; allow_source_manifest = true;
} else { } else {
bundle = ensure_cli_preset_bundle(error); bundle = ensure_cli_preset_bundle(error);
@@ -3723,15 +3700,6 @@ int CLI::run(int argc, char **argv)
} }
} }
// A mixed slot never reaches a nozzle, so its row and column stay empty, as in the GUI.
// Command line options are not merged into m_print_config yet, so they win here.
const ConfigOptionBools *is_mixed_opt = m_extra_config.option<ConfigOptionBools>("filament_is_mixed");
if (!is_mixed_opt)
is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed");
auto is_mixed_slot = [is_mixed_opt](int idx) {
return is_mixed_opt && idx < static_cast<int>(is_mixed_opt->values.size()) && is_mixed_opt->values[idx];
};
for (size_t nozzle_id = 0; nozzle_id < new_extruder_count; ++nozzle_id) { for (size_t nozzle_id = 0; nozzle_id < new_extruder_count; ++nozzle_id) {
std::vector<double> flush_vol_mtx = get_flush_volumes_matrix(flush_vol_matrix, nozzle_id, new_extruder_count); std::vector<double> flush_vol_mtx = get_flush_volumes_matrix(flush_vol_matrix, nozzle_id, new_extruder_count);
for (int from_idx = 0; from_idx < project_filament_count; from_idx++) { for (int from_idx = 0; from_idx < project_filament_count; from_idx++) {
@@ -3741,7 +3709,7 @@ int CLI::run(int argc, char **argv)
bool is_from_support = filament_is_support->get_at(from_idx); bool is_from_support = filament_is_support->get_at(from_idx);
for (int to_idx = 0; to_idx < project_filament_count; to_idx++) { for (int to_idx = 0; to_idx < project_filament_count; to_idx++) {
bool is_to_support = filament_is_support->get_at(to_idx); bool is_to_support = filament_is_support->get_at(to_idx);
if (from_idx == to_idx || is_mixed_slot(from_idx) || is_mixed_slot(to_idx)) { if (from_idx == to_idx) {
flush_vol_mtx[project_filament_count * from_idx + to_idx] = 0.f; flush_vol_mtx[project_filament_count * from_idx + to_idx] = 0.f;
} else { } else {
int flushing_volume = 0; int flushing_volume = 0;
@@ -3969,22 +3937,6 @@ int CLI::run(int argc, char **argv)
// Normalizing after importing the 3MFs / AMFs // Normalizing after importing the 3MFs / AMFs
m_print_config.normalize_fdm(); m_print_config.normalize_fdm();
// A mixed slot is virtual but still needs a filament entry of its own. Without one, feature
// filament ids aimed at it fall outside the filament count, are reset to the first filament
// and the model silently prints in a single colour.
if (const auto *is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed")) {
const auto &is_mixed = is_mixed_opt->values;
for (size_t slot = static_cast<size_t>(std::max(filament_count, 0)); slot < is_mixed.size(); ++slot) {
if (!is_mixed[slot])
continue;
BOOST_LOG_TRIVIAL(error) << boost::format("mixed filament slot %1% has no filament of its own, only %2% filaments are loaded; "
"load one filament per slot, including each mixed one")
% (slot + 1) % filament_count;
record_exit_reson(outfile_dir, CLI_MIXED_FILAMENT_INVALID, 0, cli_errors[CLI_MIXED_FILAMENT_INVALID], sliced_info);
flush_and_exit(CLI_MIXED_FILAMENT_INVALID);
}
}
m_print_config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology", true)->value = printer_technology; m_print_config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology", true)->value = printer_technology;
bool has_wipe_tower_position = m_print_config.option<ConfigOptionFloats>("wipe_tower_x") && m_print_config.option<ConfigOptionFloats>("wipe_tower_y"); bool has_wipe_tower_position = m_print_config.option<ConfigOptionFloats>("wipe_tower_x") && m_print_config.option<ConfigOptionFloats>("wipe_tower_y");
@@ -4039,15 +3991,6 @@ int CLI::run(int argc, char **argv)
bool is_smooth_timelapse = false; bool is_smooth_timelapse = false;
if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth)) if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth))
is_smooth_timelapse = true; is_smooth_timelapse = true;
// A mixed filament swaps between its components every layer, so it needs the tower even when
// every loaded preset is the same.
if (disable_wipe_tower_after_mapping) {
if (const auto *is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed");
is_mixed_opt && has_any_mixed_filament(is_mixed_opt->values)) {
disable_wipe_tower_after_mapping = false;
BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to a mixed filament")%__LINE__;
}
}
if (disable_wipe_tower_after_mapping) { if (disable_wipe_tower_after_mapping) {
if (is_smooth_timelapse) if (is_smooth_timelapse)
{ {
@@ -5369,7 +5312,7 @@ int CLI::run(int argc, char **argv)
//skip this object due to be locked in plate //skip this object due to be locked in plate
ap.itemid = locked_aps.size(); ap.itemid = locked_aps.size();
locked_aps.emplace_back(ap); locked_aps.emplace_back(ap);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx; boost::nowide::cout <<__FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx;
} }
} }
} }
@@ -5958,11 +5901,7 @@ int CLI::run(int argc, char **argv)
//FIXME check for mixing the FFF / SLA parameters. //FIXME check for mixing the FFF / SLA parameters.
// or better save fff_print_config vs. sla_print_config // or better save fff_print_config vs. sla_print_config
//m_print_config.save(m_config.opt_string("save")); //m_print_config.save(m_config.opt_string("save"));
const std::string &settings_file = m_config.opt_string(opt_key); m_print_config.save_to_json(m_config.opt_string(opt_key), std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION));
if (settings_file == "-")
m_print_config.save_to_json(boost::nowide::cout, "project_settings", "project", SoftFever_VERSION, /*replace_invalid_utf8=*/true);
else
m_print_config.save_to_json(settings_file, std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION));
} else if (opt_key == "info") { } else if (opt_key == "info") {
// --info works on unrepaired model // --info works on unrepaired model
for (Model &model : m_models) { for (Model &model : m_models) {
@@ -6258,36 +6197,6 @@ int CLI::run(int argc, char **argv)
flush_and_exit(CLI_ONLY_ONE_TPU_SUPPORTED); flush_and_exit(CLI_ONLY_ONE_TPU_SUPPORTED);
} }
// Same type gate as the GUI's Sidebar::has_broken_mixed_filament: refuse a plate that uses a
// mixed slot whose components are different filament types. Missing or out-of-range
// components never get here, validate() already rejects them for the whole project.
const auto *is_mixed_opt = m_print_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *components_opt = m_print_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && components_opt && has_any_mixed_filament(is_mixed_opt->values)) {
const auto &is_mixed = is_mixed_opt->values;
const auto &components = components_opt->values;
const size_t num_physical = static_cast<size_t>(filament_count) - static_cast<size_t>(std::count(is_mixed.begin(), is_mixed.end(), true));
std::vector<std::string> physical_types(num_physical);
for (size_t f_index = 0; f_index < num_physical; ++f_index) {
std::string displayed_type;
physical_types[f_index] = m_print_config.get_filament_type(displayed_type, static_cast<int>(f_index));
if (physical_types[f_index].empty())
physical_types[f_index] = "PLA";
}
const std::vector<size_t> mismatched_slots = check_mixed_filament_type_consistency(is_mixed, components, physical_types);
// plate_filaments has mixed slots expanded to their components; the gate needs the slots.
const std::vector<int> plate_slots = mismatched_slots.empty() ? std::vector<int>() :
part_plate->get_extruders_under_cli(true, m_print_config, false);
for (size_t slot : mismatched_slots) {
if (std::find(plate_slots.begin(), plate_slots.end(), static_cast<int>(slot) + 1) == plate_slots.end())
continue;
BOOST_LOG_TRIVIAL(error) << boost::format("plate %1%: mixed filament %2% mixes components of different filament types")
% (index + 1) % (slot + 1);
record_exit_reson(outfile_dir, CLI_MIXED_FILAMENT_INVALID, index + 1, cli_errors[CLI_MIXED_FILAMENT_INVALID], sliced_info);
flush_and_exit(CLI_MIXED_FILAMENT_INVALID);
}
}
if (new_extruder_count > 1) { if (new_extruder_count > 1) {
std::vector<std::vector<int>> unprintable_filament_vec; std::vector<std::vector<int>> unprintable_filament_vec;
for (const std::set<int>& filamnt_ids : unprintable_filament_ids) { for (const std::set<int>& filamnt_ids : unprintable_filament_ids) {
@@ -7740,13 +7649,6 @@ bool CLI::setup(int argc, char **argv)
this->print_help(); this->print_help();
return false; return false;
} }
// Orca: resolve here, while the process is still in the directory the user invoked it from.
// GUI_App's constructor moves the working directory to <data_dir>/log, long before the GUI
// opens these files in post_init(), and a relative path would then resolve against that.
for (std::string &input_file : m_input_files)
input_file = resolve_cli_input_path(input_file);
// Parse actions and transform options. // Parse actions and transform options.
for (auto const &opt_key : opt_order) { for (auto const &opt_key : opt_order) {
if (cli_actions_config_def.has(opt_key)) if (cli_actions_config_def.has(opt_key))
+2 -16
View File
@@ -23,8 +23,7 @@ int main(int argc, char* argv[])
#else #else
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory") ("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
#endif #endif
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)") ("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)");
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional; generate the cache for this vendor only (the Orca filament library is always included as the inheritance base). All vendors if not specified.");
// clang-format on // clang-format on
po::variables_map vm; po::variables_map vm;
@@ -39,7 +38,6 @@ int main(int argc, char* argv[])
const std::string profiles_path = vm["path"].as<std::string>(); const std::string profiles_path = vm["path"].as<std::string>();
const int log_level = vm["log_level"].as<int>(); const int log_level = vm["log_level"].as<int>();
const std::string vendor = vm["vendor"].as<std::string>();
if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) { if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n"; std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n";
@@ -61,12 +59,8 @@ int main(int argc, char* argv[])
preset_bundle->set_is_validation_mode(true); preset_bundle->set_is_validation_mode(true);
preset_bundle->set_default_suppressed(true); preset_bundle->set_default_suppressed(true);
preset_bundle->set_generate_vendor_caches(true); preset_bundle->set_generate_vendor_caches(true);
// Empty == every vendor. Otherwise only this vendor (plus the always-loaded
// Orca filament library) is parsed, so only its <vendor>.opc is written.
preset_bundle->set_vendor_to_validate(vendor);
std::cout << "Loading system presets from: " << profiles_path std::cout << "Loading system presets from: " << profiles_path << "\n";
<< (vendor.empty() ? "" : " (vendor: " + vendor + ")") << "\n";
try { try {
// In validation mode data_dir() is the profiles directory set above, so the // In validation mode data_dir() is the profiles directory set above, so the
@@ -77,14 +71,6 @@ int main(int argc, char* argv[])
return 1; return 1;
} }
// A specific vendor must have produced its own cache; the always-loaded
// filament library alone would otherwise mask a misspelt or removed name.
if (!vendor.empty() && !fs::exists(fs::path(profiles_path) / (vendor + ".opc"))) {
std::cerr << "No cache was generated for vendor '" << vendor << "' under " << profiles_path
<< " - check the vendor name.\n";
return 1;
}
size_t cache_count = 0; size_t cache_count = 0;
for (auto& entry : fs::directory_iterator(profiles_path)) for (auto& entry : fs::directory_iterator(profiles_path))
if (boost::iends_with(entry.path().string(), ".opc")) if (boost::iends_with(entry.path().string(), ".opc"))
+6 -15
View File
@@ -7,7 +7,6 @@
#include <algorithm> #include <algorithm>
#include <assert.h> #include <assert.h>
#include <fstream> #include <fstream>
#include <sstream>
#include <iostream> #include <iostream>
#include <iomanip> #include <iomanip>
#include <regex> #include <regex>
@@ -1516,19 +1515,6 @@ std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value
//BBS: add json support //BBS: add json support
void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const
{
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
std::ostringstream ss;
this->save_to_json(ss, name, from, version);
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
c << ss.str();
c.close();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
}
void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const
{ {
json j; json j;
//record the headers //record the headers
@@ -1575,7 +1561,12 @@ void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const s
j["plugins"] = unique_refs; j["plugins"] = unique_refs;
} }
os << j.dump(1, '\t', false, replace_invalid_utf8 ? json::error_handler_t::replace : json::error_handler_t::strict) << std::endl; boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
c << j.dump(1, '\t') << std::endl;
c.close();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
} }
void ConfigBase::save(const std::string &file) const void ConfigBase::save(const std::string &file) const
-3
View File
@@ -2825,9 +2825,6 @@ public:
//BBS: add json support //BBS: add json support
void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const; void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const;
// Same document, written to a stream. Invalid UTF-8 in a string value throws nlohmann's type_error unless
// replace_invalid_utf8 is set, which writes U+FFFD instead (for callers such as stdout with no handler).
void save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8 = false) const;
// Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin // Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin
// dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json() // dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json()
+13 -23
View File
@@ -1595,25 +1595,6 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
return sparse_infill_polylines; return sparse_infill_polylines;
} }
// Returns the filament id (1-based) the region is ironed with, or -1 when the
// region is not ironed. AllSolid always irons. TopSurfaces and TopmostOnly need
// either some top shells or, in spiral mode, more than one bottom shell, and
// TopmostOnly additionally needs the layer to be the topmost one.
int Layer::choose_ironing_extruder(const PrintRegionConfig &cfg,
bool spiral_mode,
bool is_topmost_layer)
{
if (cfg.ironing_type == IroningType::NoIroning)
return -1;
const bool gate = (cfg.ironing_type == IroningType::AllSolid)
|| ((cfg.top_shell_layers > 0 || (spiral_mode && cfg.bottom_shell_layers > 1))
&& (cfg.ironing_type == IroningType::TopSurfaces
|| (cfg.ironing_type == IroningType::TopmostOnly && is_topmost_layer)));
if (!gate)
return -1;
return cfg.top_surface_filament_id;
}
// Create ironing extrusions over top surfaces. // Create ironing extrusions over top surfaces.
void Layer::make_ironing() void Layer::make_ironing()
{ {
@@ -1683,10 +1664,19 @@ void Layer::make_ironing()
if (! layerm->slices.empty()) { if (! layerm->slices.empty()) {
IroningParams ironing_params; IroningParams ironing_params;
const PrintRegionConfig &config = layerm->region().config(); const PrintRegionConfig &config = layerm->region().config();
ironing_params.extruder = Layer::choose_ironing_extruder( if (config.ironing_type != IroningType::NoIroning &&
config, (config.ironing_type == IroningType::AllSolid ||
/*spiral_mode=*/this->object()->print()->config().spiral_mode, ((config.top_shell_layers > 0 || (this->object()->print()->config().spiral_mode && config.bottom_shell_layers > 1)) &&
/*is_topmost_layer=*/layerm->layer()->upper_layer == nullptr); (config.ironing_type == IroningType::TopSurfaces ||
(config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) {
if (config.outer_wall_filament_id == config.top_surface_filament_id || config.wall_loops == 0) {
// Iron the whole face.
ironing_params.extruder = config.top_surface_filament_id;
} else {
// Iron just the infill.
ironing_params.extruder = config.top_surface_filament_id;
}
}
if (ironing_params.extruder != -1) { if (ironing_params.extruder != -1) {
//TODO just_infill is currently not used. //TODO just_infill is currently not used.
ironing_params.just_infill = false; ironing_params.just_infill = false;
-6
View File
@@ -16,7 +16,6 @@ using LayerPtrs = std::vector<Layer*>;
class LayerRegion; class LayerRegion;
using LayerRegionPtrs = std::vector<LayerRegion*>; using LayerRegionPtrs = std::vector<LayerRegion*>;
class PrintRegion; class PrintRegion;
class PrintRegionConfig;
class PrintObject; class PrintObject;
class Print; class Print;
@@ -201,11 +200,6 @@ public:
FillAdaptive::Octree *support_fill_octree, FillAdaptive::Octree *support_fill_octree,
FillLightning::Generator* lightning_generator) const; FillLightning::Generator* lightning_generator) const;
void make_ironing(); void make_ironing();
// Returns the filament id (1-based) the region is ironed with, or -1 when the
// region is not ironed.
static int choose_ironing_extruder(const PrintRegionConfig &cfg,
bool spiral_mode,
bool is_topmost_layer);
void make_contour_z(const sla::IndexedMesh &mesh); void make_contour_z(const sla::IndexedMesh &mesh);
void export_region_slices_to_svg(const char *path) const; void export_region_slices_to_svg(const char *path) const;
+97 -86
View File
@@ -25,6 +25,7 @@
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/algorithm/clamp.hpp> #include <boost/algorithm/clamp.hpp>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/join.hpp>
#include <boost/range/adaptor/transformed.hpp> #include <boost/range/adaptor/transformed.hpp>
#include <boost/nowide/cstdio.hpp> #include <boost/nowide/cstdio.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
@@ -484,7 +485,7 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem) else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem)
compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable;
auto collection_for_type = [](const PresetBundle &bundle, Preset::Type preset_type) -> const PresetCollection * { auto collection_for_type = [](PresetBundle &bundle, Preset::Type preset_type) -> PresetCollection * {
switch (preset_type) { switch (preset_type) {
case Preset::TYPE_PRINT: return &bundle.prints; case Preset::TYPE_PRINT: return &bundle.prints;
case Preset::TYPE_FILAMENT: return &bundle.filaments; case Preset::TYPE_FILAMENT: return &bundle.filaments;
@@ -493,15 +494,15 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
} }
}; };
const PresetCollection *collection = collection_for_type(*this, type); PresetCollection *collection = collection_for_type(*this, type);
if (collection == nullptr) { if (collection == nullptr) {
error = "Unsupported preset type"; error = "Unsupported preset type";
return false; return false;
} }
const boost::filesystem::path source_path = boost::filesystem::absolute(source_file).lexically_normal(); const boost::filesystem::path source_path = boost::filesystem::absolute(source_file).lexically_normal();
auto find_loaded = [&](const PresetBundle &bundle) -> const Preset * { auto find_loaded = [&](PresetBundle &bundle) -> const Preset * {
const PresetCollection *loaded_collection = collection_for_type(bundle, type); PresetCollection *loaded_collection = collection_for_type(bundle, type);
const Preset *resolved = nullptr; const Preset *resolved = nullptr;
for (const Preset &preset : loaded_collection->get_presets()) { for (const Preset &preset : loaded_collection->get_presets()) {
if (preset.file.empty()) if (preset.file.empty())
@@ -549,11 +550,30 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
continue; continue;
try { try {
const PresetBundle *loaded = load_source_vendor(root_dir, vendor_id, compatibility_rule, error); PresetBundle library_bundle;
if (loaded == nullptr) const PresetBundle *base_bundle = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) {
library_bundle.m_preserve_vendor_source_paths = true;
library_bundle.load_vendor_configs_from_json(root_dir.string(), ORCA_FILAMENT_LIBRARY, LoadSystem,
compatibility_rule, nullptr, false);
if (library_bundle.error_count() != 0) {
error = "OrcaFilamentLibrary contains invalid presets";
return false; return false;
}
base_bundle = &library_bundle;
}
const Preset *resolved = find_loaded(*loaded); PresetBundle source_bundle;
source_bundle.m_preserve_vendor_source_paths = true;
source_bundle.load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem,
compatibility_rule, base_bundle, false);
if (source_bundle.error_count() != 0) {
error = "Vendor bundle contains invalid presets";
return false;
}
const Preset *resolved = find_loaded(source_bundle);
if (resolved == nullptr) { if (resolved == nullptr) {
if (error.empty()) if (error.empty())
error = "Source file is not an instantiated preset in its vendor manifest"; error = "Source file is not an instantiated preset in its vendor manifest";
@@ -572,37 +592,6 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
return false; return false;
} }
const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir,
const std::string &vendor_id,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error)
{
auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule);
if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end())
return it->second.get();
// The library loads with no base of its own, so the tree a vendor inherits from
// is the same one that resolves the library's own presets.
const PresetBundle *library = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) {
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error);
if (library == nullptr) {
error = "OrcaFilamentLibrary contains invalid presets";
return nullptr;
}
}
auto bundle = std::make_unique<PresetBundle>();
bundle->m_preserve_vendor_source_paths = true;
bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, false);
if (bundle->error_count() != 0) {
error = "Vendor bundle contains invalid presets";
return nullptr;
}
return m_source_vendor_bundles.emplace(std::move(key), std::move(bundle)).first->second.get();
}
bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type, bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type,
const std::string &source_file, const std::string &source_file,
ForwardCompatibilitySubstitutionRule compatibility_rule, ForwardCompatibilitySubstitutionRule compatibility_rule,
@@ -3824,6 +3813,27 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type"); ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map"); ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map"); ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
// 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 // Snapshot and temporarily strip mixed filament slots so AMS sync operates on physical
// filaments only. A mixed slot is virtual and has no tray to sync against; leaving it in // filaments only. A mixed slot is virtual and has no tray to sync against; leaving it in
@@ -3893,20 +3903,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
}; };
auto exist_colors = filament_color->values; auto exist_colors = filament_color->values;
std::vector<std::vector<std::string>> exist_multi_color_filment(exist_colors.size()); auto exist_multi_color_filment = decode_project_multi_colors(exist_colors);
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;
}
}
}
bool mapped_any = false; bool mapped_any = false;
if (use_map && !maps.empty()) { if (use_map && !maps.empty()) {
@@ -3982,11 +3979,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
auto exist_colors = filament_color->values; auto exist_colors = filament_color->values;
auto exist_color_types = filament_color_type->values; auto exist_color_types = filament_color_type->values;
auto exist_filament_presets = this->filament_presets; auto exist_filament_presets = this->filament_presets;
std::vector<std::vector<std::string>> exist_multi_color_filment; auto exist_multi_color_filment = decode_project_multi_colors(exist_colors);
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]};
}
for (size_t i = 0; i < exist_colors.size(); i++) { for (size_t i = 0; i < exist_colors.size(); i++) {
if (maps.find(i) != maps.end()) {//mapping exist if (maps.find(i) != maps.end()) {//mapping exist
auto valid_index = get_map_index(ams_array_maps, maps[i]); auto valid_index = get_map_index(ams_array_maps, maps[i]);
@@ -3994,7 +3987,10 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
exist_colors[i] = ams_filament_colors[valid_index]; exist_colors[i] = ams_filament_colors[valid_index];
exist_color_types[i] = ams_filament_color_types[valid_index]; exist_color_types[i] = ams_filament_color_types[valid_index];
exist_filament_presets[i] = ams_filament_presets[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 { } else {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (mapping exist)"; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (mapping exist)";
} }
@@ -4054,7 +4050,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
exist_filament_presets.push_back(need_append_colors[i].filament_preset); exist_filament_presets.push_back(need_append_colors[i].filament_preset);
exist_colors.push_back(need_append_colors[i].filament_color); exist_colors.push_back(need_append_colors[i].filament_color);
exist_color_types.push_back(need_append_colors[i].filament_color_type); 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; filament_color->values = exist_colors;
@@ -4072,6 +4068,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
auto exist_colors = filament_color->values; auto exist_colors = filament_color->values;
auto exist_color_types = filament_color_type->values; auto exist_color_types = filament_color_type->values;
auto exist_presets = this->filament_presets; 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 tray_count = ams_filament_presets.size();
size_t total = std::max(tray_count, exist_presets.size()); size_t total = std::max(tray_count, exist_presets.size());
@@ -4081,25 +4078,34 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
std::vector<std::string> result_presets; std::vector<std::string> result_presets;
std::vector<std::vector<std::string>> result_multi_colors; std::vector<std::vector<std::string>> result_multi_colors;
for (size_t i = 0; i < total; i++) { for (size_t i = 0; i < total; ++i) {
bool is_loaded = (i < ams_infos.size() && ams_infos[i].valid); const bool is_loaded = i < ams_infos.size() && ams_infos[i].valid;
if (is_loaded) { 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_colors.push_back(ams_filament_colors[i]);
result_color_types.push_back(ams_filament_color_types[i]); result_color_types.push_back(ams_filament_color_types[i]);
result_presets.push_back(ams_filament_presets[i]); result_presets.push_back(ams_filament_presets[i]);
result_multi_colors.push_back( if (i < ams_multi_color_filment.size() && !ams_multi_color_filment[i].empty()) {
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i] result_multi_colors.push_back(ams_multi_color_filment[i]);
: std::vector<std::string>{ams_filament_colors[i]}); } else {
} else if (i < exist_presets.size()) { // why: old project components belong to the replaced
// Empty tray or beyond tray count: keep existing filament // 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_colors.push_back(exist_colors[i]);
result_color_types.push_back(exist_color_types[i]); result_color_types.push_back(exist_color_types[i]);
result_presets.push_back(exist_presets[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 { } 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) { auto it = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) {
return f.is_compatible && f.is_system return f.is_compatible && f.is_system
&& boost::algorithm::starts_with(f.name, "Generic "); && boost::algorithm::starts_with(f.name, "Generic ");
@@ -4173,23 +4179,28 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
void PresetBundle::update_filament_multi_color() void PresetBundle::update_filament_multi_color()
{ {
std::vector<std::string> exsit_multi_colors; const ConfigOptionStrings *filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
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);
}
}
ConfigOptionStrings *filament_multi_colour = project_config.option<ConfigOptionStrings>("filament_multi_colour"); ConfigOptionStrings *filament_multi_colour = project_config.option<ConfigOptionStrings>("filament_multi_colour");
filament_multi_colour->resize(exsit_multi_colors.size()); // note: cheap defence only. Both keys live in s_project_options and the
filament_multi_colour->values = exsit_multi_colors; // 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]};
}
}
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) std::vector<int> PresetBundle::get_used_tpu_filaments(const std::vector<int> &used_filaments)
-12
View File
@@ -11,7 +11,6 @@
#include <map> #include <map>
#include <set> #include <set>
#include <shared_mutex> #include <shared_mutex>
#include <tuple>
#include <unordered_map> #include <unordered_map>
#include <optional> #include <optional>
#include <array> #include <array>
@@ -653,17 +652,6 @@ private:
bool m_generate_vendor_caches { false }; bool m_generate_vendor_caches { false };
bool m_preserve_vendor_source_paths { false }; bool m_preserve_vendor_source_paths { false };
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset
// resolved through this bundle shares one load per source root and vendor. The
// filament library is one such tree, shared by every vendor under its root.
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule>, std::unique_ptr<PresetBundle>>
m_source_vendor_bundles;
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
const std::string &vendor_id,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error);
// Orca: validation only - flag any printer with two or more compatible // Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match). // filament presets sharing one filament_id (ambiguous AMS subtype match).
bool check_duplicate_filament_subtypes() const; bool check_duplicate_filament_subtypes() const;
+177 -40
View File
@@ -10936,28 +10936,6 @@ std::vector<int> DynamicPrintConfig::update_values_to_printer_extruders(DynamicP
return variant_index; return variant_index;
} }
// Regathers a vector option's values through per-slot source indices (one input index per
// output slot). Out-of-range indices keep the first value, matching get_at's fallback.
template<typename OptType, typename ValueType>
static void gather_option_values(const char *caller, const std::string &key, OptType *opt, const std::vector<int> &slot_param_indices)
{
if (!opt || opt->values.empty()) {
BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key;
return;
}
std::vector<ValueType> new_values;
new_values.reserve(slot_param_indices.size());
for (int idx : slot_param_indices) {
if (idx < 0 || static_cast<size_t>(idx) >= opt->values.size()) {
BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx;
new_values.emplace_back(opt->values.front());
}
else
new_values.emplace_back(opt->values[idx]);
}
opt->values = std::move(new_values);
}
void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::set<std::string>& key_set, std::string id_name, std::string variant_name) void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::set<std::string>& key_set, std::string id_name, std::string variant_name)
{ {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: extruder_count %2%, extruder_nozzle_volume_count %3%")%__LINE__ %extruder_count %extruder_nozzle_volume_count; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: extruder_count %2%, extruder_nozzle_volume_count %3%")%__LINE__ %extruder_count %extruder_nozzle_volume_count;
@@ -11035,18 +11013,155 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: can not find opt define for %2%")%__LINE__%key; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: can not find opt define for %2%")%__LINE__%key;
continue; continue;
} }
// An empty option has no first value to fall back on; give it one registered default per filament.
if (auto *vec = dynamic_cast<ConfigOptionVectorBase*>(this->option(key)); vec && vec->empty() && optdef->default_value)
vec->resize(filament_count, optdef->default_value.get());
switch (optdef->type) { switch (optdef->type) {
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(__FUNCTION__, key, this->option<ConfigOptionStrings>(key), variant_index); break; case coStrings:
case coInts: gather_option_values<ConfigOptionInts, int>(__FUNCTION__, key, this->option<ConfigOptionInts>(key), variant_index); break; {
case coFloats: gather_option_values<ConfigOptionFloats, double>(__FUNCTION__, key, this->option<ConfigOptionFloats>(key), variant_index); break; ConfigOptionStrings * opt = this->option<ConfigOptionStrings>(key);
case coPercents: gather_option_values<ConfigOptionPercents, double>(__FUNCTION__, key, this->option<ConfigOptionPercents>(key), variant_index); break; if (!opt) {
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(__FUNCTION__, key, this->option<ConfigOptionFloatsOrPercents>(key), variant_index); break; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(__FUNCTION__, key, this->option<ConfigOptionBools>(key), variant_index); break; break;
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(__FUNCTION__, key, this->option<ConfigOptionEnumsGeneric>(key), variant_index); break; }
std::vector<std::string> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coInts:
{
ConfigOptionInts * opt = this->option<ConfigOptionInts>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<int> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coFloats:
{
ConfigOptionFloats * opt = this->option<ConfigOptionFloats>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<double> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coPercents:
{
ConfigOptionPercents * opt = this->option<ConfigOptionPercents>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<double> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coFloatsOrPercents:
{
ConfigOptionFloatsOrPercents * opt = this->option<ConfigOptionFloatsOrPercents>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<FloatOrPercent> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coBools:
{
ConfigOptionBools * opt = this->option<ConfigOptionBools>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<unsigned char> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coEnums:
{
ConfigOptionEnumsGeneric * opt = this->option<ConfigOptionEnumsGeneric>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<int> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
default: default:
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key;
break; break;
@@ -11065,6 +11180,28 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
} }
} }
// Regathers a vector option's values through per-slot source indices (one input index per
// output slot). Out-of-range indices keep the first value, matching get_at's fallback.
template<typename OptType, typename ValueType>
static void gather_option_values(const std::string &key, OptType *opt, const std::vector<int> &slot_param_indices)
{
if (!opt || opt->values.empty()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key;
return;
}
std::vector<ValueType> new_values;
new_values.reserve(slot_param_indices.size());
for (int idx : slot_param_indices) {
if (idx < 0 || static_cast<size_t>(idx) >= opt->values.size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx;
new_values.emplace_back(opt->values.front());
}
else
new_values.emplace_back(opt->values[idx]);
}
opt->values = std::move(new_values);
}
void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(DynamicPrintConfig& printer_config, void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(DynamicPrintConfig& printer_config,
const std::unordered_map<int, std::vector<FilamentVariantUse>>& filament_variant_uses, const std::unordered_map<int, std::vector<FilamentVariantUse>>& filament_variant_uses,
int extruder_count, int extruder_nozzle_volume_count, int extruder_count, int extruder_nozzle_volume_count,
@@ -11159,13 +11296,13 @@ void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(Dy
continue; continue;
} }
switch (optdef->type) { switch (optdef->type) {
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(__FUNCTION__, key, this->option<ConfigOptionStrings>(key), slot_param_indices); break; case coStrings: gather_option_values<ConfigOptionStrings, std::string>(key, this->option<ConfigOptionStrings>(key), slot_param_indices); break;
case coInts: gather_option_values<ConfigOptionInts, int>(__FUNCTION__, key, this->option<ConfigOptionInts>(key), slot_param_indices); break; case coInts: gather_option_values<ConfigOptionInts, int>(key, this->option<ConfigOptionInts>(key), slot_param_indices); break;
case coFloats: gather_option_values<ConfigOptionFloats, double>(__FUNCTION__, key, this->option<ConfigOptionFloats>(key), slot_param_indices); break; case coFloats: gather_option_values<ConfigOptionFloats, double>(key, this->option<ConfigOptionFloats>(key), slot_param_indices); break;
case coPercents: gather_option_values<ConfigOptionPercents, double>(__FUNCTION__, key, this->option<ConfigOptionPercents>(key), slot_param_indices); break; case coPercents: gather_option_values<ConfigOptionPercents, double>(key, this->option<ConfigOptionPercents>(key), slot_param_indices); break;
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(__FUNCTION__, key, this->option<ConfigOptionFloatsOrPercents>(key), slot_param_indices); break; case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(key, this->option<ConfigOptionFloatsOrPercents>(key), slot_param_indices); break;
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(__FUNCTION__, key, this->option<ConfigOptionBools>(key), slot_param_indices); break; case coBools: gather_option_values<ConfigOptionBools, unsigned char>(key, this->option<ConfigOptionBools>(key), slot_param_indices); break;
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(__FUNCTION__, key, this->option<ConfigOptionEnumsGeneric>(key), slot_param_indices); break; case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(key, this->option<ConfigOptionEnumsGeneric>(key), slot_param_indices); break;
default: default:
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key;
break; break;
@@ -11916,7 +12053,7 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def = this->add("export_settings", coString); def = this->add("export_settings", coString);
def->label = L("Export Settings"); def->label = L("Export Settings");
def->tooltip = L("This exports settings to a file. Use - to write them to stdout."); def->tooltip = L("This exports settings to a file.");
def->cli_params = "settings.json"; def->cli_params = "settings.json";
def->set_default_value(new ConfigOptionString("output.json")); def->set_default_value(new ConfigOptionString("output.json"));
-4
View File
@@ -70,7 +70,6 @@
#define CLI_FILAMENT_CAN_NOT_MAP -66 #define CLI_FILAMENT_CAN_NOT_MAP -66
#define CLI_ONLY_ONE_TPU_SUPPORTED -67 #define CLI_ONLY_ONE_TPU_SUPPORTED -67
#define CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER -68 #define CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER -68
#define CLI_MIXED_FILAMENT_INVALID -69
#define CLI_SLICING_ERROR -100 #define CLI_SLICING_ERROR -100
#define CLI_GCODE_PATH_CONFLICTS -101 #define CLI_GCODE_PATH_CONFLICTS -101
@@ -314,9 +313,6 @@ extern unsigned get_current_pid();
std::string per_user_temp_id(); std::string per_user_temp_id();
// Per-user temp root under `base`; an empty `user_id` returns `base` unchanged. // Per-user temp root under `base`; an empty `user_id` returns `base` unchanged.
std::string per_user_temp_dir(const std::string &base, const std::string &user_id); std::string per_user_temp_dir(const std::string &base, const std::string &user_id);
// Completes a relative command line input path against the current working directory. Absolute
// paths and custom open protocol URLs are returned unchanged.
std::string resolve_cli_input_path(const std::string &path);
// BBS: backup & restore // BBS: backup & restore
std::string get_process_name(int pid); std::string get_process_name(int pid);
-13
View File
@@ -1339,19 +1339,6 @@ std::string per_user_temp_dir(const std::string &base, const std::string &user_i
return base + "/orcaslicer_" + user_id; return base + "/orcaslicer_" + user_id;
} }
std::string resolve_cli_input_path(const std::string &path)
{
const boost::filesystem::path input(path);
if (path.empty() || is_supported_open_protocol(path) || input.is_absolute())
return path;
boost::system::error_code ec;
const boost::filesystem::path resolved = boost::filesystem::system_complete(input, ec);
if (ec)
return path;
return resolved.lexically_normal().make_preferred().string();
}
// BBS: backup & restore // BBS: backup & restore
std::string get_process_name(int pid) std::string get_process_name(int pid)
{ {
+19 -6
View File
@@ -226,6 +226,7 @@ set(SLIC3R_GUI_SOURCES
GUI/GLToolbar.hpp GUI/GLToolbar.hpp
GUI/ImageDPIFrame.cpp GUI/ImageDPIFrame.cpp
GUI/ImageDPIFrame.hpp GUI/ImageDPIFrame.hpp
GUI/IMediaController.hpp
GUI/GUI_App.cpp GUI/GUI_App.cpp
GUI/GUI_App.hpp GUI/GUI_App.hpp
GUI/GUI_AuxiliaryList.cpp GUI/GUI_AuxiliaryList.cpp
@@ -347,6 +348,8 @@ set(SLIC3R_GUI_SOURCES
GUI/MediaFilePanel.h GUI/MediaFilePanel.h
GUI/MediaPlayCtrl.cpp GUI/MediaPlayCtrl.cpp
GUI/MediaPlayCtrl.h GUI/MediaPlayCtrl.h
GUI/WebRtcMediaController.cpp
GUI/WebRtcMediaController.hpp
GUI/MeshUtils.cpp GUI/MeshUtils.cpp
GUI/MeshUtils.hpp GUI/MeshUtils.hpp
GUI/ModelMall.cpp GUI/ModelMall.cpp
@@ -536,6 +539,8 @@ set(SLIC3R_GUI_SOURCES
GUI/WebUserLoginDialog.hpp GUI/WebUserLoginDialog.hpp
GUI/WebViewDialog.cpp GUI/WebViewDialog.cpp
GUI/WebViewDialog.hpp GUI/WebViewDialog.hpp
GUI/WebMediaController.hpp
GUI/WebMediaController.cpp
GUI/Widgets/AMSControl.cpp GUI/Widgets/AMSControl.cpp
GUI/Widgets/AMSControl.hpp GUI/Widgets/AMSControl.hpp
GUI/Widgets/AMSItem.cpp GUI/Widgets/AMSItem.cpp
@@ -726,10 +731,15 @@ set(SLIC3R_GUI_SOURCES
Utils/NetworkAgentFactory.cpp Utils/NetworkAgentFactory.cpp
Utils/ICloudServiceAgent.hpp Utils/ICloudServiceAgent.hpp
Utils/IPrinterAgent.hpp Utils/IPrinterAgent.hpp
Utils/ICameraSignalingChannel.hpp
Utils/OrcaCloudServiceAgent.cpp Utils/OrcaCloudServiceAgent.cpp
Utils/OrcaCloudServiceAgent.hpp Utils/OrcaCloudServiceAgent.hpp
Utils/OrcaMqttConnection.cpp
Utils/OrcaMqttConnection.hpp
Utils/OrcaPrinterAgent.cpp Utils/OrcaPrinterAgent.cpp
Utils/OrcaPrinterAgent.hpp Utils/OrcaPrinterAgent.hpp
Utils/OrcaCloudSignalingChannel.cpp
Utils/OrcaCloudSignalingChannel.hpp
Utils/QidiPrinterAgent.cpp Utils/QidiPrinterAgent.cpp
Utils/QidiPrinterAgent.hpp Utils/QidiPrinterAgent.hpp
Utils/SnapmakerPrinterAgent.cpp Utils/SnapmakerPrinterAgent.cpp
@@ -870,7 +880,7 @@ else()
set(_opengl_link_lib OpenGL::GL) set(_opengl_link_lib OpenGL::GL)
endif() endif()
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise pybind11::embed) target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto LibDataChannel::LibDataChannel noise::noise pybind11::embed)
if (CMAKE_SYSTEM_NAME STREQUAL "Linux") if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
# Linux finds wxWidgets in module mode, whose include dirs and definitions # Linux finds wxWidgets in module mode, whose include dirs and definitions
@@ -927,29 +937,32 @@ endif ()
if (APPLE) if (APPLE)
# Static FFmpeg from the deps install: nothing to bundle into the .app, # Static FFmpeg from the deps install: nothing to bundle into the .app,
# no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil. # no rpath/install_name handling. Order matters: avformat -> avcodec -> swscale -> avutil.
find_library(LIBAVFORMAT_LIBRARY NAMES libavformat.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) if (NOT LIBAVFORMAT_LIBRARY OR NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "Static FFmpeg (libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.") message(FATAL_ERROR "Static FFmpeg (libavformat.a/libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.")
endif () endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) target_link_libraries(libslic3r_gui ${LIBAVFORMAT_LIBRARY} ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
elseif (WIN32) elseif (WIN32)
# Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config, # Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config,
# so resolve the import libraries out of the deps prefix directly; the DLLs # so resolve the import libraries out of the deps prefix directly; the DLLs
# are copied next to the executable by the top level CMakeLists. # are copied next to the executable by the top level CMakeLists.
find_library(LIBAVFORMAT_LIBRARY NAMES avformat PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "FFmpeg (avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.") message(FATAL_ERROR "FFmpeg (avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.")
endif () endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) target_link_libraries(libslic3r_gui ${LIBAVFORMAT_LIBRARY} ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
else () else ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavformat
libavcodec libavcodec
libswscale libswscale
libavutil libavutil
+27
View File
@@ -45,6 +45,25 @@ int AVVideoDecoder::open(Bambu_StreamInfo const &info)
return 0; return 0;
} }
int AVVideoDecoder::open(AVCodecParameters const &parameters)
{
if (avcodec_parameters_to_context(codec_ctx_, &parameters) < 0)
return -1;
auto codec = avcodec_find_decoder(codec_ctx_->codec_id);
if (codec == nullptr) {
fprintf(stderr, "AVVideoDecoder: unsupported codec!\n");
return -1;
}
if (avcodec_open2(codec_ctx_, codec, nullptr) < 0) {
fprintf(stderr, "AVVideoDecoder: could not open codec\n");
return -1;
}
frame_ = av_frame_alloc();
return frame_ == nullptr ? -1 : 0;
}
int AVVideoDecoder::decode(const Bambu_Sample &sample) int AVVideoDecoder::decode(const Bambu_Sample &sample)
{ {
int ret = -1; int ret = -1;
@@ -71,6 +90,14 @@ int AVVideoDecoder::decode(const Bambu_Sample &sample)
return ret; return ret;
} }
int AVVideoDecoder::decode(const AVPacket &packet)
{
int ret = avcodec_send_packet(codec_ctx_, &packet);
if (ret == 0)
got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0;
return ret;
}
int AVVideoDecoder::flush() int AVVideoDecoder::flush()
{ {
int ret = avcodec_send_packet(codec_ctx_, nullptr); int ret = avcodec_send_packet(codec_ctx_, nullptr);
+10
View File
@@ -23,8 +23,10 @@ public:
public: public:
int open(Bambu_StreamInfo const &info); int open(Bambu_StreamInfo const &info);
int open(AVCodecParameters const &parameters);
int decode(Bambu_Sample const &sample); int decode(Bambu_Sample const &sample);
int decode(AVPacket const &packet);
int flush(); int flush();
@@ -34,6 +36,14 @@ public:
bool toWxBitmap(wxBitmap &bitmap, wxSize const & size); bool toWxBitmap(wxBitmap &bitmap, wxSize const & size);
// Native size of the most recently decoded frame, or an unspecified size if
// nothing has decoded yet. Lets a caller learn the video dimensions when the
// container/probe could not report them up front.
wxSize decoded_frame_size() const
{
return got_frame_ && frame_ ? wxSize{frame_->width, frame_->height} : wxSize{};
}
private: private:
AVCodecContext *codec_ctx_ = nullptr; AVCodecContext *codec_ctx_ = nullptr;
AVFrame * frame_ = nullptr; AVFrame * frame_ = nullptr;
@@ -8,6 +8,7 @@
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "DeviceCore/DevConfig.h" #include "DeviceCore/DevConfig.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevExtruderSystem.h" #include "DeviceCore/DevExtruderSystem.h"
#include "DeviceCore/DevFilaBlackList.h" #include "DeviceCore/DevFilaBlackList.h"
#include "DeviceCore/DevFilaSystem.h" #include "DeviceCore/DevFilaSystem.h"
@@ -1650,6 +1651,11 @@ bool CalibrationPresetPage::is_blocking_printing()
auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
auto target_model = obj_->printer_type; auto target_model = obj_->printer_type;
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) {
return false;
}
if (source_model != target_model) { if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine(); std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
-60
View File
@@ -28,7 +28,6 @@ wxEND_EVENT_TABLE()
wxDEFINE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDEFINE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent);
wxDEFINE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); wxDEFINE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent);
wxDEFINE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent);
#define CAMERAPOPUP_CLICK_INTERVAL 20 #define CAMERAPOPUP_CLICK_INTERVAL 20
@@ -102,34 +101,6 @@ CameraPopup::CameraPopup(wxWindow *parent)
top_sizer->Add(0, 0, wxALL, 0); top_sizer->Add(0, 0, wxALL, 0);
} }
// Orca: custom IP camera source — lets the user point Live Video at any camera URL (Orca feature; not in the reference)
m_custom_camera_input_confirm = new Button(m_panel, _L("Enable"));
m_custom_camera_input_confirm->SetBackgroundColor(wxColour(38, 166, 154));
m_custom_camera_input_confirm->SetBorderColor(wxColour(38, 166, 154));
m_custom_camera_input_confirm->SetTextColor(wxColour(0xFFFFFE));
m_custom_camera_input_confirm->SetFont(Label::Body_14);
m_custom_camera_input_confirm->SetMinSize(wxSize(FromDIP(90), FromDIP(30)));
m_custom_camera_input_confirm->SetPosition(wxDefaultPosition);
m_custom_camera_input_confirm->SetCornerRadius(FromDIP(12));
m_custom_camera_input = new TextInput(m_panel, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, wxDefaultSize);
m_custom_camera_input->GetTextCtrl()->SetHint(_L("Hostname or IP"));
m_custom_camera_input->GetTextCtrl()->SetFont(Label::Body_14);
m_custom_camera_hint = new wxStaticText(m_panel, wxID_ANY, _L("Custom camera source"));
m_custom_camera_hint->Wrap(-1);
m_custom_camera_hint->SetFont(Label::Head_14);
m_custom_camera_hint->SetForegroundColour(TEXT_COL);
m_custom_camera_input_confirm->Bind(wxEVT_BUTTON, &CameraPopup::on_camera_source_changed, this);
if (!wxGetApp().app_config->get("camera", "custom_source").empty()) {
m_custom_camera_input->GetTextCtrl()->SetValue(wxGetApp().app_config->get("camera", "custom_source"));
set_custom_cam_button_state(wxGetApp().app_config->get("camera", "enable_custom_source") == "true");
}
top_sizer->Add(m_custom_camera_hint, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5));
top_sizer->Add(0, 0, wxALL, 0);
top_sizer->Add(m_custom_camera_input, 2, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxALL, FromDIP(5));
top_sizer->Add(m_custom_camera_input_confirm, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5));
main_sizer->Add(top_sizer, 0, wxALL, FromDIP(10)); main_sizer->Add(top_sizer, 0, wxALL, FromDIP(10));
auto url = wxString(L"https://www.orcaslicer.com/wiki/"); // Orca: neutral wiki link (vendor URL removed) auto url = wxString(L"https://www.orcaslicer.com/wiki/"); // Orca: neutral wiki link (vendor URL removed)
@@ -184,37 +155,6 @@ void CameraPopup::sdcard_absent_hint()
GetEventHandler()->ProcessEvent(evt); GetEventHandler()->ProcessEvent(evt);
} }
void CameraPopup::on_camera_source_changed(wxCommandEvent &event)
{
if (m_obj && !m_custom_camera_input->GetTextCtrl()->IsEmpty()) {
handle_camera_source_change();
}
}
void CameraPopup::handle_camera_source_change()
{
m_custom_camera_enabled = !m_custom_camera_enabled;
set_custom_cam_button_state(m_custom_camera_enabled);
wxGetApp().app_config->set("camera", "custom_source", m_custom_camera_input->GetTextCtrl()->GetValue().ToStdString());
wxGetApp().app_config->set("camera", "enable_custom_source", m_custom_camera_enabled);
wxCommandEvent evt(EVT_CAM_SOURCE_CHANGE);
evt.SetEventObject(this);
GetEventHandler()->ProcessEvent(evt);
}
void CameraPopup::set_custom_cam_button_state(bool state)
{
m_custom_camera_enabled = state;
auto stateColour = state ? wxColour(170, 0, 0) : wxColour(38, 166, 154);
auto stateText = state ? "Disable" : "Enable";
m_custom_camera_input_confirm->SetBackgroundColor(stateColour);
m_custom_camera_input_confirm->SetBorderColor(stateColour);
m_custom_camera_input_confirm->SetLabel(_L(stateText));
}
void CameraPopup::on_switch_recording(wxCommandEvent& event) void CameraPopup::on_switch_recording(wxCommandEvent& event)
{ {
if (!m_obj) return; if (!m_obj) return;
+4 -9
View File
@@ -15,14 +15,12 @@
#include "Widgets/SwitchButton.hpp" #include "Widgets/SwitchButton.hpp"
#include "Widgets/RadioBox.hpp" #include "Widgets/RadioBox.hpp"
#include "Widgets/PopupWindow.hpp" #include "Widgets/PopupWindow.hpp"
#include "Widgets/TextInput.hpp"
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent);
wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent);
wxDECLARE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent);
class CameraPopup : public PopupWindow class CameraPopup : public PopupWindow
{ {
@@ -53,9 +51,6 @@ protected:
void on_switch_recording(wxCommandEvent& event); void on_switch_recording(wxCommandEvent& event);
void on_set_resolution(); void on_set_resolution();
void sdcard_absent_hint(); void sdcard_absent_hint();
void on_camera_source_changed(wxCommandEvent& event);
void handle_camera_source_change();
void set_custom_cam_button_state(bool state);
wxWindow * create_item_radiobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left); wxWindow * create_item_radiobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left);
void select_curr_radiobox(int btn_idx); void select_curr_radiobox(int btn_idx);
@@ -76,10 +71,10 @@ private:
wxStaticText* m_text_liveview_retry; wxStaticText* m_text_liveview_retry;
SwitchButton* m_switch_liveview_retry; SwitchButton* m_switch_liveview_retry;
#endif //BBL_RELEASE_TO_PUBLIC #endif //BBL_RELEASE_TO_PUBLIC
wxStaticText* m_custom_camera_hint; // wxStaticText* m_custom_camera_hint;
TextInput* m_custom_camera_input; // TextInput* m_custom_camera_input;
Button* m_custom_camera_input_confirm; // Button* m_custom_camera_input_confirm;
bool m_custom_camera_enabled{ false }; // bool m_custom_camera_enabled{ false };
wxStaticText* m_text_resolution; wxStaticText* m_text_resolution;
wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM]; wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM];
wxScrolledWindow *m_panel; wxScrolledWindow *m_panel;
+3 -1
View File
@@ -35,7 +35,9 @@ ConnectPrinterDialog::ConnectPrinterDialog(wxWindow *parent, wxWindowID id, cons
sizer_connect = new wxBoxSizer(wxHORIZONTAL); sizer_connect = new wxBoxSizer(wxHORIZONTAL);
m_textCtrl_code = new TextInput(this, wxEmptyString); m_textCtrl_code = new TextInput(this, wxEmptyString);
m_textCtrl_code->GetTextCtrl()->SetMaxLength(10); // OrcaSonar uses a 12-character base32 access code. Keep this field long
// enough for it while retaining the existing validation for LAN codes.
m_textCtrl_code->GetTextCtrl()->SetMaxLength(12);
m_textCtrl_code->SetFont(Label::Body_14); m_textCtrl_code->SetFont(Label::Body_14);
m_textCtrl_code->SetCornerRadius(FromDIP(5)); m_textCtrl_code->SetCornerRadius(FromDIP(5));
m_textCtrl_code->SetSize(wxSize(FromDIP(330), FromDIP(40))); m_textCtrl_code->SetSize(wxSize(FromDIP(330), FromDIP(40)));
+15
View File
@@ -59,6 +59,21 @@ public:
/*printer*/ /*printer*/
// info // info
static std::map<std::string, std::string> get_all_model_id_with_name(); static std::map<std::string, std::string> get_all_model_id_with_name();
// A printer agent may not know the physical model. Keep that case optional so
// model compatibility checks do not turn missing identity into a hard error.
static bool is_optional_printer_model_id(const std::string& model_id)
{
if (model_id.empty())
return true;
if (model_id.size() != 9)
return false;
static constexpr char generic_model_id[] = "orcasonar";
return std::equal(model_id.begin(), model_id.end(), generic_model_id,
[](char lhs, char rhs) {
return static_cast<char>(std::tolower(static_cast<unsigned char>(lhs))) == rhs;
});
}
static std::string get_printer_type(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "printer_type"); } static std::string get_printer_type(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "printer_type"); }
static std::string get_printer_display_name(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "display_name"); } static std::string get_printer_display_name(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "display_name"); }
static std::string get_printer_series_str(std::string type_str) { return get_value_from_config<std::string>(type_str, "printer_series"); } static std::string get_printer_series_str(std::string type_str) { return get_value_from_config<std::string>(type_str, "printer_series"); }
+3 -3
View File
@@ -762,9 +762,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
{ {
curr_tray->remain = -1; curr_tray->remain = -1;
} }
if (tray_it->contains("tray_slot_placeholder")) { // The tray objects are reused across status updates. Reset this
curr_tray->is_slot_placeholder = true; // state when a previously empty slot receives a filament again.
} curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder");
int ams_id_int = 0; int ams_id_int = 0;
int tray_id_int = 0; int tray_id_int = 0;
try try
+67 -20
View File
@@ -14,6 +14,8 @@
#include "libslic3r/Time.hpp" #include "libslic3r/Time.hpp"
#include "IPrinterAgent.hpp"
using namespace nlohmann; using namespace nlohmann;
namespace { namespace {
@@ -264,6 +266,10 @@ namespace Slic3r
/* update userMachineList info */ /* update userMachineList info */
auto it = userMachineList.find(dev_id); auto it = userMachineList.find(dev_id);
if (it != userMachineList.end()) { if (it != userMachineList.end()) {
// A reused entry may have been created while another printer agent was active.
// The response was obtained through the current agent, so move ownership with
// the entry; otherwise agent-scoped lists hide it after a preset switch.
it->second->printer_agent_id = get_current_printer_agent_id();
if (it->second->get_dev_ip() != dev_ip || if (it->second->get_dev_ip() != dev_ip ||
it->second->bind_state != bind_state || it->second->bind_state != bind_state ||
it->second->bind_sec_link != sec_link || it->second->bind_sec_link != sec_link ||
@@ -294,6 +300,9 @@ namespace Slic3r
// update properties // update properties
/* ip changed */ /* ip changed */
obj = it->second; obj = it->second;
// A reused LAN entry may have been discovered while another printer agent was
// active. The current discovery message establishes ownership for this agent.
obj->printer_agent_id = get_current_printer_agent_id();
if (obj->get_dev_ip().compare(dev_ip) != 0) { if (obj->get_dev_ip().compare(dev_ip) != 0) {
if ( connection_name.empty() ) { if ( connection_name.empty() ) {
@@ -405,6 +414,9 @@ namespace Slic3r
auto it = localMachineList.find(machine.dev_id); auto it = localMachineList.find(machine.dev_id);
if (it != localMachineList.end()) { if (it != localMachineList.end()) {
obj = it->second; obj = it->second;
// insert_local_device is called by the active agent, so a reused entry must follow
// that agent as well; otherwise the agent-scoped printer list hides it.
obj->printer_agent_id = get_current_printer_agent_id();
} else { } else {
obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip); obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip);
obj->printer_agent_id = get_current_printer_agent_id(); obj->printer_agent_id = get_current_printer_agent_id();
@@ -534,25 +546,15 @@ namespace Slic3r
OnSelectedMachineChanged(previous_selected_machine, selected_machine); OnSelectedMachineChanged(previous_selected_machine, selected_machine);
} }
void DeviceManager::clear_other_devices(const std::string& target_agent_id) void DeviceManager::clear_other_devices()
{ {
// why: on agent swap, keep "My Devices" but drop the transient "Other Devices" // Device entries are now scoped by printer_agent_id when they are presented. Keep
// Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own. // agent-owned discoveries across a switch so agents without automatic discovery (and
// // plugins whose devices have not received an access code yet) do not lose their list.
// Also drop "My Devices" stamped by a different agent than the one we're swapping to // Entries without an owner are legacy/unscoped and cannot safely be shown.
// (target_agent_id, passed by the caller since the live agent hasn't been repointed yet
// at this point): otherwise a device first discovered under agent A survives every swap
// with a stale printer_agent_id, stays hidden from every agent's filtered list, and only
// gets re-tagged if something happens to delete and re-create it (e.g. account logout).
// Dropping it here instead lets the new agent's start_discovery re-insert and re-stamp it
// like any other fresh device.
const auto my = get_my_machine_list();
for (auto it = localMachineList.begin(); it != localMachineList.end();) for (auto it = localMachineList.begin(); it != localMachineList.end();)
{ {
const bool is_my_device = my.find(it->first) != my.end(); if (!it->second || it->second->printer_agent_id.empty())
const bool agent_mismatch = !target_agent_id.empty() && it->second &&
it->second->printer_agent_id != target_agent_id;
if (!is_my_device || agent_mismatch)
{ {
delete it->second; delete it->second;
it = localMachineList.erase(it); it = localMachineList.erase(it);
@@ -570,6 +572,19 @@ namespace Slic3r
<< " cur_selected=" << selected_machine; << " cur_selected=" << selected_machine;
auto my_machine_list = get_my_machine_list(); auto my_machine_list = get_my_machine_list();
auto it = my_machine_list.find(dev_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";
}
// disconnect last if dev_id difference from previous one // disconnect last if dev_id difference from previous one
auto last_selected = my_machine_list.find(selected_machine); auto last_selected = my_machine_list.find(selected_machine);
@@ -580,7 +595,9 @@ namespace Slic3r
m_agent->disconnect_printer(); m_agent->disconnect_printer();
} }
else if (last_selected->second->connection_type() == "cloud") { else if (last_selected->second->connection_type() == "cloud") {
m_agent->set_user_selected_machine(""); const int result = m_agent->set_user_selected_machine("");
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cleared previous cloud selection dev_id="
<< selected_machine << " result=" << result;
} }
} }
@@ -632,7 +649,9 @@ namespace Slic3r
{ {
// diff dev_id, cloud => set_user_selected_machine(new) // diff dev_id, cloud => set_user_selected_machine(new)
BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new cloud machine, dev_id =" << dev_id; BOOST_LOG_TRIVIAL(info) << "set_selected_machine: select new cloud machine, dev_id =" << dev_id;
m_agent->set_user_selected_machine(dev_id); const int result = m_agent->set_user_selected_machine(dev_id);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: set new cloud selection dev_id="
<< dev_id << " result=" << result;
it->second->reset(); it->second->reset();
} }
else else
@@ -660,6 +679,8 @@ namespace Slic3r
selected_machine = dev_id; selected_machine = dev_id;
record_user_last_machine(selected_machine); record_user_last_machine(selected_machine);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: DeviceManager selection complete selected_machine="
<< selected_machine;
return true; return true;
} }
@@ -690,7 +711,9 @@ namespace Slic3r
dev_list.push_back(it->first); dev_list.push_back(it->first);
BOOST_LOG_TRIVIAL(trace) << "add_user_subscribe: " << it->first; BOOST_LOG_TRIVIAL(trace) << "add_user_subscribe: " << it->first;
} }
m_agent->add_subscribe(dev_list); const int result = m_agent->add_subscribe(dev_list);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_user_subscribe count=" << dev_list.size()
<< " result=" << result;
} }
@@ -703,7 +726,9 @@ namespace Slic3r
dev_list.push_back(it->first); dev_list.push_back(it->first);
BOOST_LOG_TRIVIAL(trace) << "del_user_subscribe: " << it->first; BOOST_LOG_TRIVIAL(trace) << "del_user_subscribe: " << it->first;
} }
m_agent->del_subscribe(dev_list); const int result = m_agent->del_subscribe(dev_list);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_user_subscribe count=" << dev_list.size()
<< " result=" << result;
} }
void DeviceManager::subscribe_device_list(std::vector<std::string> dev_list) void DeviceManager::subscribe_device_list(std::vector<std::string> dev_list)
@@ -845,6 +870,9 @@ namespace Slic3r
/* update field */ /* update field */
obj = iter->second; obj = iter->second;
obj->set_dev_id(dev_id); obj->set_dev_id(dev_id);
// A device can be rediscovered by a different agent after a preset
// switch while retaining the same MachineObject instance.
obj->printer_agent_id = get_current_printer_agent_id();
} }
else else
{ {
@@ -864,6 +892,12 @@ namespace Slic3r
if (!obj) continue; if (!obj) continue;
// Orca cloud printers are only ever delivered through this REST
// account list; tag them so DeviceManager's cloud/lan branches
// (subscribe + deselect in set_selected_machine) treat them right.
if (provider == "orca")
obj->dev_connection_type = "cloud";
if (!elem["dev_id"].is_null()) if (!elem["dev_id"].is_null())
obj->set_dev_id(elem["dev_id"].get<std::string>()); obj->set_dev_id(elem["dev_id"].get<std::string>());
if (!elem["dev_name"].is_null()) if (!elem["dev_name"].is_null())
@@ -895,6 +929,12 @@ namespace Slic3r
acc_code.erase(std::remove(acc_code.begin(), acc_code.end(), '\n'), acc_code.end()); acc_code.erase(std::remove(acc_code.begin(), acc_code.end(), '\n'), acc_code.end());
obj->set_access_code(acc_code); obj->set_access_code(acc_code);
} }
BOOST_LOG_TRIVIAL(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 //remove MachineObject from userMachineList
@@ -910,6 +950,9 @@ namespace Slic3r
iterat++; 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) catch (std::exception& e)
@@ -926,10 +969,14 @@ namespace Slic3r
unsigned int http_code; unsigned int http_code;
std::string body; std::string body;
int result = m_agent->get_user_print_info(&http_code, &body, provider); int result = m_agent->get_user_print_info(&http_code, &body, provider);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: get_user_print_info provider=" << provider
<< " result=" << result << " http_code=" << http_code
<< " body_bytes=" << body.size();
if (result == 0) if (result == 0)
{ {
// parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map. // parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map.
// on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info. // on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info.
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: queueing parse_user_print_info on UI thread";
Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); }); Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); });
} }
} }
+3 -4
View File
@@ -74,10 +74,9 @@ public:
void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); } void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); }
void clean_user_info(bool keep_local_selection = false); void clean_user_info(bool keep_local_selection = false);
// target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check, // Retain agent-owned LAN discoveries across a switch; the active-agent list filter keeps
// just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the // entries from other agents hidden while allowing them to reappear when switched back.
// live one - this runs before the live agent is repointed. void clear_other_devices();
void clear_other_devices(const std::string& target_agent_id = "");
void load_last_machine(); void load_last_machine();
void update_user_machine_list_info(const std::string& provider); void update_user_machine_list_info(const std::string& provider);
+190 -82
View File
@@ -53,6 +53,7 @@
#include "DeviceCore/DevStatus.h" #include "DeviceCore/DevStatus.h"
#include "DeviceCore/DevUpgrade.h" #include "DeviceCore/DevUpgrade.h"
#include "IPrinterAgent.hpp"
#define CALI_DEBUG #define CALI_DEBUG
#define MINUTE_30 1800000 //ms #define MINUTE_30 1800000 //ms
@@ -373,8 +374,22 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str)
wxString MachineObject::get_printer_type_display_str() const wxString MachineObject::get_printer_type_display_str() const
{ {
std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type); std::string display_name = DevPrinterConfigUtil::get_printer_display_name(printer_type);
// Bambu printers use m_resource_file_path + "/printers/" + type_str + ".json", which is a semantic that only works for their profiles.
// For any other profile, we can simply consult preset bundle if the model_id exists.
if (display_name.empty()) {
for (const auto& [vendor_id, vendor] : GUI::wxGetApp().preset_bundle->vendors) {
for (const auto& model : vendor.models) {
if (printer_type == model.model_id)
display_name = model.name;
}
}
}
if (!display_name.empty()) if (!display_name.empty())
return display_name; return display_name;
else if (printer_type == "orcasonar")
return "OrcaSonar Printer";
else else
return _L("Unknown"); return _L("Unknown");
} }
@@ -1341,7 +1356,6 @@ int MachineObject::command_get_access_code() {
return this->publish_json(j); return this->publish_json(j);
} }
int MachineObject::command_request_push_all(bool request_now) int MachineObject::command_request_push_all(bool request_now)
{ {
auto curr_time = std::chrono::system_clock::now(); auto curr_time = std::chrono::system_clock::now();
@@ -1473,26 +1487,29 @@ int MachineObject::command_upgrade_module(std::string url, std::string module_ty
int MachineObject::command_xyz_abs() int MachineObject::command_xyz_abs()
{ {
return this->publish_gcode("G90 \n"); if (!m_agent) return -1;
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() 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() int MachineObject::command_go_home()
{ {
if (m_support_mqtt_homing) 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());
json j; if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
j["print"]["command"] = "back_to_center"; show_unsupported_dlg(rtn);
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++); return rtn;
return this->publish_json(j);
}
// gcode command
return this->is_in_printing() ? this->publish_gcode("G28 X\n") : this->publish_gcode("G28 \n");
} }
int MachineObject::command_task_partskip(std::vector<int> part_ids) int MachineObject::command_task_partskip(std::vector<int> part_ids)
@@ -1614,23 +1631,20 @@ int MachineObject::command_stop_buzzer()
int MachineObject::command_set_bed(int temp) int MachineObject::command_set_bed(int temp)
{ {
if (m_support_mqtt_bet_ctrl) if (!m_agent) return -1;
{ int rtn = m_agent->command_set_bed(get_dev_id(), temp, m_support_mqtt_bet_ctrl, MachineObject::m_sequence_id++, is_lan_mode_printer());
json j; if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
j["print"]["command"] = "set_bed_temp"; show_unsupported_dlg(rtn);
j["print"]["temp"] = temp; return rtn;
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
return this->publish_json(j);
}
std::string gcode_str = (boost::format("M140 S%1%\n") % temp).str();
return this->publish_gcode(gcode_str);
} }
int MachineObject::command_set_nozzle(int temp) int MachineObject::command_set_nozzle(int temp)
{ {
std::string gcode_str = (boost::format("M104 S%1%\n") % temp).str(); if (!m_agent) return -1;
return this->publish_gcode(gcode_str); 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) 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) int MachineObject::command_ams_calibrate(int ams_id)
{ {
std::string gcode_cmd = (boost::format("M620 C%1% \n") % ams_id).str(); if (!m_agent) return -1;
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; int rtn = m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
return this->publish_gcode(gcode_cmd); 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) 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) int MachineObject::command_ams_refresh_rfid(std::string tray_id)
{ {
std::string gcode_cmd = (boost::format("M620 R%1% \n") % tray_id).str(); if (!m_agent) return -1;
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; int rtn = m_agent->command_ams_refresh_rfid(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
return this->publish_gcode(gcode_cmd); 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) 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); 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) int MachineObject::command_ams_select_tray(std::string tray_id)
{ {
std::string gcode_cmd = (boost::format("M620 P%1% \n") % tray_id).str(); if (!m_agent) return -1;
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd; int rtn = m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
return this->publish_gcode(gcode_cmd); 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) 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) int MachineObject::command_axis_control(std::string axis, double unit, double input_val, int speed)
{ {
if (m_support_mqtt_axis_control) if (!m_agent) return -1;
{ int rtn = m_agent->command_axis_control(get_dev_id(), axis, unit, input_val, speed, is_core_xy(),
int dir = input_val > 0 ? 1 : -1; m_support_mqtt_axis_control, MachineObject::m_sequence_id++, is_lan_mode_printer());
// i3-arch printers move the bed for Y/Z, so the on-screen direction is if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
// reversed — same negation the g-code fallback below applies. show_unsupported_dlg(rtn);
if (!is_core_xy() && (axis.compare("Y") == 0 || axis.compare("Z") == 0)) { return rtn;
dir = -dir;
}
json j;
j["print"]["command"] = "xyz_ctrl";
j["print"]["axis"] = axis;
j["print"]["dir"] = dir;
j["print"]["mode"] = (std::abs(input_val) >= 10) ? 1 : 0;
j["print"]["sequence_id"] = std::to_string(MachineObject::m_sequence_id++);
return this->publish_json(j);
}
double value = input_val;
if (!is_core_xy()) {
if ( axis.compare("Y") == 0
|| axis.compare("Z") == 0) {
value = -1.0 * input_val;
}
}
char cmd[256];
if (axis.compare("X") == 0
|| axis.compare("Y") == 0
|| axis.compare("Z") == 0) {
sprintf(cmd, "M211 S \nM211 X1 Y1 Z1\nM1002 push_ref_mode\nG91 \nG1 %s%0.1f F%d\nM1002 pop_ref_mode\nM211 R\n", axis.c_str(), value * unit, speed);
}
else if (axis.compare("E") == 0) {
sprintf(cmd, "M83 \nG0 %s%0.1f F%d\n", axis.c_str(), value * unit, speed);
}
else {
return -1;
}
return this->publish_gcode(cmd);
} }
int MachineObject::command_extruder_control(int nozzle_id, double val) int MachineObject::command_extruder_control(int nozzle_id, double val)
@@ -2619,7 +2610,12 @@ void MachineObject::reset()
vt_slot.erase(vt_slot.begin() + 1); vt_slot.erase(vt_slot.begin() + 1);
} }
} }
// why: reset reuses MachineObject, so release its lazy subtask
// before dropping the pointer to prevent reconnect leaks.
if (subtask_) {
delete subtask_;
subtask_ = nullptr; subtask_ = nullptr;
}
has_extra_flow_type = false; has_extra_flow_type = false;
m_partskip_ids.clear(); m_partskip_ids.clear();
} }
@@ -2629,10 +2625,24 @@ void MachineObject::set_print_state(std::string status)
print_status = 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) int MachineObject::connect(bool use_openssl)
{ {
if (get_dev_ip().empty()) return -1; if (get_dev_ip().empty()) return -1;
std::string username = "bblp"; std::string username = m_agent ? m_agent->default_lan_username() : std::string();
std::string password = get_access_code(); std::string password = get_access_code();
if (m_agent) { 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; BOOST_LOG_TRIVIAL(error) << "publish_json: " << json_item.dump() << " code: " << rtn;
} }
// why: the agent is the only thing that knows what it can translate, so it reports
// not-supported in its return value and this - the single funnel every command_* builder
// passes through - is the one place that turns it into something the user sees. No list of
// unsupported commands is needed anywhere: an agent that has no case for a command says so.
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) {
show_unsupported_dlg(rtn);
}
return rtn; return rtn;
} }
@@ -3046,6 +3064,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
} }
} catch (...) {} } catch (...) {}
try {
if (j.contains("info"))
parse_new_info2(j["info"]);
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "parse_json: failed to parse OrcaSonar capability info";
}
try { try {
if (auto ptr = m_fila_system->GetAmsFirmwareSwitch().lock()) { if (auto ptr = m_fila_system->GetAmsFirmwareSwitch().lock()) {
ptr->ParseFirmwareSwitch(j); ptr->ParseFirmwareSwitch(j);
@@ -3298,10 +3323,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
print_type = jj["print_type"].get<std::string>(); print_type = jj["print_type"].get<std::string>();
} }
if (jj.contains("mc_percent")) { if (jj.contains("mc_percent")) {
if (jj["mc_percent"].is_string()) update_print_progress(jj["mc_percent"]);
mc_print_percent = stoi(j["print"]["mc_percent"].get<std::string>());
else if (jj["mc_percent"].is_number_integer())
mc_print_percent = j["print"]["mc_percent"].get<int>();
} }
if (jj.contains("mc_print_sub_stage")) { if (jj.contains("mc_print_sub_stage")) {
if (jj["mc_print_sub_stage"].is_number_integer()) if (jj["mc_print_sub_stage"].is_number_integer())
@@ -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>(); this->task_id_ = jj["task_id"].get<std::string>();
} }
if (jj.contains("thumbnail_url") && jj["thumbnail_url"].is_string())
m_agent_thumbnail_url = jj["thumbnail_url"].get<std::string>();
if (jj.contains("job_attr")) { if (jj.contains("job_attr")) {
int jobAttr = jj["job_attr"].get<int>(); int jobAttr = jj["job_attr"].get<int>();
jobState_ = get_flag_bits(jobAttr, 4, 4); jobState_ = get_flag_bits(jobAttr, 4, 4);
@@ -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); update_slice_info(jj["project_id"].get<std::string>(), jj["profile_id"].get<std::string>(), jj["subtask_id"].get<std::string>(), plate_index);
BBLSubTask* curr_task = get_subtask(); BBLSubTask* curr_task = get_subtask();
if (curr_task) { if (curr_task) {
curr_task->task_progress = mc_print_percent;
curr_task->printing_status = print_status; curr_task->printing_status = print_status;
curr_task->task_id = jj["subtask_id"].get<std::string>(); curr_task->task_id = jj["subtask_id"].get<std::string>();
} }
@@ -3819,6 +3843,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
has_ipcam = true; has_ipcam = true;
} else { } else {
has_ipcam = false; has_ipcam = false;
webcam_stream_url.clear();
} }
} }
if (ipcam.contains("resolution")) { if (ipcam.contains("resolution")) {
@@ -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" liveview_local = local_rtsp_url.empty() ? LVL_None : local_rtsp_url == "disable"
? LVL_Disable : boost::algorithm::starts_with(local_rtsp_url, "rtsps") ? LVL_Rtsps : LVL_Rtsp; ? LVL_Disable : boost::algorithm::starts_with(local_rtsp_url, "rtsps") ? LVL_Rtsps : LVL_Rtsp;
} }
if (ipcam.contains("stream_url") && ipcam["stream_url"].is_string()) {
webcam_stream_url = ipcam["stream_url"].get<std::string>();
}
if (ipcam.contains("tutk_server")) { if (ipcam.contains("tutk_server")) {
tutk_state = ipcam["tutk_server"].get<std::string>(); tutk_state = ipcam["tutk_server"].get<std::string>();
} }
@@ -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) { static bool is_hex_digit(char c) {
return std::isxdigit(static_cast<unsigned char>(c)) != 0; return std::isxdigit(static_cast<unsigned char>(c)) != 0;
} }
+11 -24
View File
@@ -18,6 +18,7 @@
#include "boost/bimap/bimap.hpp" #include "boost/bimap/bimap.hpp"
#include "libslic3r/calib.hpp" #include "libslic3r/calib.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "slic3r/Utils/PrinterNetworkTypes.hpp"
#include "DeviceCore/DevDefs.h" #include "DeviceCore/DevDefs.h"
#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevConfigUtil.h"
@@ -100,7 +101,6 @@ struct DevPrintTaskRatingInfo;
// given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware. // given nozzle diameter (mm), bucketed per nozzle size to mirror the printer firmware.
bool is_stringing_prone_filament(const std::string& filament_id, float nozzle_diameter); bool is_stringing_prone_filament(const std::string& filament_id, float nozzle_diameter);
class MachineObject class MachineObject
{ {
private: private:
@@ -547,30 +547,12 @@ public:
bool xcam_first_layer_inspector { false }; bool xcam_first_layer_inspector { false };
time_t xcam_first_layer_hold_start = 0; time_t xcam_first_layer_hold_start = 0;
std::string local_rtsp_url; std::string local_rtsp_url;
std::string webcam_stream_url;
std::string tutk_state; std::string tutk_state;
enum LiveviewLocal { LiveviewLocal liveview_local{ LiveviewLocal::LVL_None };
LVL_None, LiveviewRemote liveview_remote{ LiveviewRemote::LVR_None};
LVL_Disable, FileLocal file_local{ FileLocal::FL_None };
LVL_Local, FileRemote file_remote{ FileRemote::FR_None };
LVL_Rtsps,
LVL_Rtsp
} liveview_local{ LVL_None };
enum LiveviewRemote {
LVR_None,
LVR_Tutk,
LVR_Agora,
LVR_TutkAgora
} liveview_remote{ LVR_None };
enum FileLocal {
FL_None,
FL_Local
} file_local{ FL_None };
enum FileRemote {
FR_None,
FR_Tutk,
FR_Agora,
FR_TutkAgora
} file_remote{ FR_None };
enum PlateMakerDectect : int enum PlateMakerDectect : int
{ {
@@ -708,6 +690,8 @@ public:
std::string subtask_id_; std::string subtask_id_;
std::string job_id_; std::string job_id_;
std::string last_subtask_id_; std::string last_subtask_id_;
// note: printer-agent-supplied thumbnail url, empty when the agent supplies none.
std::string m_agent_thumbnail_url;
BBLSliceInfo* slice_info {nullptr}; BBLSliceInfo* slice_info {nullptr};
boost::thread* get_slice_info_thread { nullptr }; boost::thread* get_slice_info_thread { nullptr };
boost::thread* get_model_task_thread { nullptr }; boost::thread* get_model_task_thread { nullptr };
@@ -765,6 +749,7 @@ public:
int command_set_printer_nozzle(std::string nozzle_type, float diameter); int command_set_printer_nozzle(std::string nozzle_type, float diameter);
int command_set_printer_nozzle2(int id, std::string nozzle_type, float diameter); int command_set_printer_nozzle2(int id, std::string nozzle_type, float diameter);
int command_get_access_code(); int command_get_access_code();
int command_start_camera();
int command_ack_proceed(json& proceed); int command_ack_proceed(json& proceed);
int command_purification_disable(); int command_purification_disable();
int command_dont_remind_next_time(json& mqtt_guard_json); int command_dont_remind_next_time(json& mqtt_guard_json);
@@ -894,6 +879,7 @@ public:
static bool is_in_printing_status(std::string status); static bool is_in_printing_status(std::string status);
void set_print_state(std::string status); void set_print_state(std::string status);
void update_print_progress(const json& value);
bool is_connected(); bool is_connected();
bool is_connecting(); bool is_connecting();
@@ -961,6 +947,7 @@ public:
/*for parse new info*/ /*for parse new info*/
bool check_enable_np(const json& print) const; bool check_enable_np(const json& print) const;
void parse_new_info(json print); void parse_new_info(json print);
void parse_new_info2(const json& info);
int get_flag_bits(std::string str, int start, int count = 1) const; int get_flag_bits(std::string str, int start, int count = 1) const;
uint32_t get_flag_bits_no_border(std::string str, int start_idx, int count = 1) const; uint32_t get_flag_bits_no_border(std::string str, int start_idx, int count = 1) const;
int get_flag_bits(int num, int start, int count = 1, int base = 10) const; int get_flag_bits(int num, int start, int count = 1, int base = 10) const;
+8 -3
View File
@@ -2098,6 +2098,12 @@ void GLCanvas3D::render(bool only_init)
_render_selection_center(); _render_selection_center();
#endif // ENABLE_RENDER_SELECTION_CENTER #endif // ENABLE_RENDER_SELECTION_CENTER
// we need to set the mouse's scene position here because the depth buffer
// could be invalidated by the following gizmo render methods
// this position is used later into on_mouse() to drag the objects
if (m_picking_enabled)
m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast<coord_t>());
// sidebar hints need to be rendered before the gizmos because the depth buffer // sidebar hints need to be rendered before the gizmos because the depth buffer
// could be invalidated by the following gizmo render methods // could be invalidated by the following gizmo render methods
_render_selection_sidebar_hints(); _render_selection_sidebar_hints();
@@ -4485,13 +4491,12 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box(); BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box();
volume_bbox.offset(1.0); volume_bbox.offset(1.0);
const bool is_cut_connector_selected = m_selection.is_any_connector(); const bool is_cut_connector_selected = m_selection.is_any_connector();
const Vec3d scene_position = _mouse_to_3d(pos); if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position) && !is_cut_connector_selected) {
if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(scene_position) && !is_cut_connector_selected) {
m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None; m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None;
// The dragging operation is initiated. // The dragging operation is initiated.
m_mouse.drag.move_volume_idx = volume_idx; m_mouse.drag.move_volume_idx = volume_idx;
m_selection.setup_cache(); m_selection.setup_cache();
m_mouse.drag.start_position_3D = scene_position; m_mouse.drag.start_position_3D = m_mouse.scene_position;
m_sequential_print_clearance_first_displacement = true; m_sequential_print_clearance_first_displacement = true;
m_moving = true; m_moving = true;
+1
View File
@@ -337,6 +337,7 @@ class GLCanvas3D
bool dragging{ false }; bool dragging{ false };
Vec2d position{ DBL_MAX, DBL_MAX }; Vec2d position{ DBL_MAX, DBL_MAX };
Vec3d scene_position{ DBL_MAX, DBL_MAX, DBL_MAX };
bool ignore_left_up{ false }; bool ignore_left_up{ false };
Drag drag; Drag drag;
bool ignore_right_up; bool ignore_right_up;
+19 -13
View File
@@ -3,6 +3,7 @@
#include "libslic3r/Technologies.hpp" #include "libslic3r/Technologies.hpp"
#include "libslic3r/Platform.hpp" #include "libslic3r/Platform.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "DeviceCore/DevConfigUtil.h"
#include "BindDialog.hpp" #include "BindDialog.hpp"
#include "DeviceManager.hpp" #include "DeviceManager.hpp"
#include "HMS.hpp" #include "HMS.hpp"
@@ -2403,6 +2404,11 @@ bool GUI_App::is_blocking_printing(MachineObject *obj_)
PresetBundle *preset_bundle = wxGetApp().preset_bundle; PresetBundle *preset_bundle = wxGetApp().preset_bundle;
std::string source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); std::string source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) {
return false;
}
if (source_model != target_model) { if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine(); std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
@@ -3961,13 +3967,9 @@ void GUI_App::set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent)
m_agent->set_user_selected_machine(""); m_agent->set_user_selected_machine("");
// note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer) // note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer)
dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS
// why: drop stale LAN discoveries; keep My Devices, but only those belonging to the // why: retain agent-owned LAN discoveries so agents without automatic discovery (for
// agent we're about to swap to, so a device stamped by the outgoing agent doesn't // example the Moonraker-based Qidi/Snapmaker agents) can reuse them after a switch.
// linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh. dev->clear_other_devices();
// agent is null when clearing the live agent entirely (e.g. plugin unload); there's no
// target to filter against then, so fall back to the original "keep all My Devices"
// behavior rather than guessing.
dev->clear_other_devices(agent ? agent->get_agent_info().id : std::string());
} }
m_agent->set_printer_agent(agent); m_agent->set_printer_agent(agent);
@@ -4010,6 +4012,7 @@ void GUI_App::switch_printer_agent()
std::string log_dir = data_dir(); std::string log_dir = data_dir();
std::string cloud_agent_id = agent_info.id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER; std::string cloud_agent_id = agent_info.id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << agent_info.id;
std::shared_ptr<ICloudServiceAgent> cloud_agent = m_agent->get_cloud_agent(cloud_agent_id); std::shared_ptr<ICloudServiceAgent> cloud_agent = m_agent->get_cloud_agent(cloud_agent_id);
// Create new printer agent via registry // Create new printer agent via registry
@@ -4023,8 +4026,10 @@ void GUI_App::switch_printer_agent()
return; return;
} }
// The factory caches agents per ID, so an identical pointer means the agent type is unchanged. // Compare the registered IDs, not only the implementation pointer. Different registry IDs
if (m_agent->get_printer_agent() == new_printer_agent) { // may intentionally be backed by the same implementation object (especially for plugins).
const auto current_printer_agent = m_agent->get_printer_agent();
if (current_printer_agent && current_printer_agent->get_agent_info().id == effective_agent_id) {
// Orca: the agent type is unchanged (e.g. switching between two Moonraker/Klipper // Orca: the agent type is unchanged (e.g. switching between two Moonraker/Klipper
// printer presets), so the selected machine and the agent's cached device_info still // printer presets), so the selected machine and the agent's cached device_info still
// point at the previously active printer preset. Re-select the machine when the new // point at the previously active printer preset. Re-select the machine when the new
@@ -4983,11 +4988,12 @@ bool GUI_App::is_user_login(const std::string& provider/* = ORCA_CLOUD_PROVIDER*
return false; return false;
} }
const std::string& GUI_App::get_printer_cloud_provider() const std::string GUI_App::get_printer_cloud_provider() const
{ {
// Orca todo: this need to be revisted. currently it is mainly used for device manager and related clausses and only bambu machines use them. std::string provider = preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent");
// if (provider.empty())
return BBL_CLOUD_PROVIDER; provider = ORCA_CLOUD_PROVIDER;
return provider;
} }
+1 -1
View File
@@ -495,7 +495,7 @@ public:
bool check_login(const std::string& provider = ORCA_CLOUD_PROVIDER); bool check_login(const std::string& provider = ORCA_CLOUD_PROVIDER);
void get_login_info(const std::string& provider = ORCA_CLOUD_PROVIDER); void get_login_info(const std::string& provider = ORCA_CLOUD_PROVIDER);
bool is_user_login(const std::string& provider = ORCA_CLOUD_PROVIDER); bool is_user_login(const std::string& provider = ORCA_CLOUD_PROVIDER);
const std::string& get_printer_cloud_provider() const; std::string get_printer_cloud_provider() const;
void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER); void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER); void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
+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/FileTransferUtils.hpp"
#include "slic3r/Utils/BBLNetworkPlugin.hpp" #include "slic3r/Utils/BBLNetworkPlugin.hpp"
#include "NetworkAgent.hpp"
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
@@ -203,7 +204,7 @@ void PrintJob::process(Ctl &ctl)
params.dev_ip = m_dev_ip; params.dev_ip = m_dev_ip;
params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; params.use_ssl_for_ftp = m_local_use_ssl_for_ftp;
params.use_ssl_for_mqtt = m_local_use_ssl; params.use_ssl_for_mqtt = m_local_use_ssl;
params.username = "bblp"; params.username = m_agent->default_lan_username();
params.password = m_access_code; params.password = m_access_code;
// check access code and ip address // check access code and ip address
+2 -2
View File
@@ -126,7 +126,7 @@ void SendJob::process(Ctl &ctl)
if (m_is_check_mode) { if (m_is_check_mode) {
PrintParams verify_params; PrintParams verify_params;
verify_params.dev_ip = m_dev_ip; verify_params.dev_ip = m_dev_ip;
verify_params.username = "bblp"; verify_params.username = agent->default_lan_username();
verify_params.password = m_access_code; verify_params.password = m_access_code;
verify_params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; verify_params.use_ssl_for_ftp = m_local_use_ssl_for_ftp;
verify_params.use_ssl_for_mqtt = m_local_use_ssl; verify_params.use_ssl_for_mqtt = m_local_use_ssl;
@@ -211,7 +211,7 @@ void SendJob::process(Ctl &ctl)
// local print access // local print access
params.dev_ip = m_dev_ip; params.dev_ip = m_dev_ip;
params.username = "bblp"; params.username = agent->default_lan_username();
params.password = m_access_code; params.password = m_access_code;
params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; params.use_ssl_for_ftp = m_local_use_ssl_for_ftp;
params.use_ssl_for_mqtt = m_local_use_ssl; params.use_ssl_for_mqtt = m_local_use_ssl;
+259 -18
View File
@@ -7,6 +7,7 @@
#include "DeviceManager.hpp" #include "DeviceManager.hpp"
#include "DeviceCore/DevConfigUtil.h" #include "DeviceCore/DevConfigUtil.h"
#include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include "I18N.hpp" #include "I18N.hpp"
@@ -15,11 +16,14 @@
#include "slic3r/Utils/BBLNetworkPlugin.hpp" #include "slic3r/Utils/BBLNetworkPlugin.hpp"
#include <algorithm>
#include <boost/lexical_cast.hpp> #include <boost/lexical_cast.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/nowide/cstdio.hpp> #include <boost/nowide/cstdio.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include <boost/nowide/utf8_codecvt.hpp> #include <boost/nowide/utf8_codecvt.hpp>
#include <slic3r/GUI/DeviceManager.hpp>
#undef pid_t #undef pid_t
#include <boost/process.hpp> #include <boost/process.hpp>
#ifdef __WIN32__ #ifdef __WIN32__
@@ -139,6 +143,11 @@ MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const w
MediaPlayCtrl::~MediaPlayCtrl() 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); boost::unique_lock lock(m_mutex);
m_tasks.push_back("<exit>"); m_tasks.push_back("<exit>");
@@ -151,8 +160,76 @@ MediaPlayCtrl::~MediaPlayCtrl()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << this; 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) 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::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() : ""; std::string machine = obj ? obj->get_dev_id() : "";
if (obj) { if (obj) {
m_camera_exists = obj->has_ipcam; m_camera_exists = obj->has_ipcam;
@@ -167,12 +244,12 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
if (DevPrinterConfigUtil::get_printer_series_str(obj->printer_type) == "series_o" && BBLNetworkPlugin::instance().use_legacy_network()) { 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 // 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 { } else {
m_camera_exists = false; m_camera_exists = false;
m_lan_mode = false; m_lan_mode = false;
m_lan_proto = MachineObject::LVL_None; m_lan_proto = LiveviewLocal::LVL_None;
m_lan_ip.clear(); m_lan_ip.clear();
m_lan_passwd.clear(); m_lan_passwd.clear();
m_dev_ver.clear(); m_dev_ver.clear();
@@ -182,8 +259,6 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
} }
Enable(obj && obj->is_info_ready() && obj->m_push_count > 0); Enable(obj && obj->is_info_ready() && obj->m_push_count > 0);
if (machine == m_machine) { if (machine == m_machine) {
if (m_last_state == MEDIASTATE_IDLE && IsEnabled())
Play();
return; return;
} }
m_machine = machine; m_machine = machine;
@@ -254,6 +329,87 @@ void refresh_agora_url(char const* device, char const* dev_ver, char const* chan
void MediaPlayCtrl::Play() 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::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
NetworkAgent* agent = wxGetApp().getAgent();
if (agent && agent->get_printer_agent()->get_agent_info().id != BBL_PRINTER_AGENT_ID) {
Stop(_L("Camera not available"));
return;
}
break;
}
if (!m_next_retry.IsValid() || wxDateTime::Now() < m_next_retry) if (!m_next_retry.IsValid() || wxDateTime::Now() < m_next_retry)
return; return;
if (!IsShownOnScreen()) if (!IsShownOnScreen())
@@ -283,14 +439,14 @@ void MediaPlayCtrl::Play()
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play: " << m_lan_proto << m_remote_proto << m_disable_lan; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play: " << m_lan_proto << m_remote_proto << m_disable_lan;
NetworkAgent *agent = wxGetApp().getAgent(); NetworkAgent *agent = wxGetApp().getAgent();
std::string agent_version = agent ? agent->get_version() : ""; std::string agent_version = agent ? agent->get_version() : "";
if (m_lan_proto > MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) { if (m_lan_proto > LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) {
m_disable_lan = m_remote_proto && !m_lan_mode; // try remote next time m_disable_lan = m_remote_proto && !m_lan_mode; // try remote next time
std::string url; std::string url;
if (m_lan_proto == MachineObject::LVL_Local) if (m_lan_proto == LiveviewLocal::LVL_Local)
url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd; url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd;
else if (m_lan_proto == MachineObject::LVL_Rtsps) else if (m_lan_proto == LiveviewLocal::LVL_Rtsps)
url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps"; url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps";
else if (m_lan_proto == MachineObject::LVL_Rtsp) else if (m_lan_proto == LiveviewLocal::LVL_Rtsp)
url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp"; url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp";
url += "&device=" + m_machine; url += "&device=" + m_machine;
url += "&net_ver=" + agent_version; url += "&net_ver=" + agent_version;
@@ -312,8 +468,8 @@ void MediaPlayCtrl::Play()
// !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_Disable (*) // !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_Disable (*)
// !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_None (x) // !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_None (x)
if (m_lan_proto <= MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto)) { if (m_lan_proto <= LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto)) {
Stop(m_lan_proto == MachineObject::LVL_None Stop(m_lan_proto == LiveviewLocal::LVL_None
? _L("A problem occurred. Please update the printer firmware and try again.") ? _L("A problem occurred. Please update the printer firmware and try again.")
: _L("LAN Only Liveview is off. Please turn on the liveview on printer screen.")); : _L("LAN Only Liveview is off. Please turn on the liveview on printer screen."));
return; return;
@@ -381,8 +537,70 @@ void MediaPlayCtrl::Play()
void start_ping_test(); 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) void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
{ {
const bool webrtc_active = m_webrtc_ctrl && (m_last_mode == CameraStreamMode::webrtc ||
current_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 (current_mode()) {
case CameraStreamMode::http:
case CameraStreamMode::http_snapshot: {
const bool snapshot = current_mode() == CameraStreamMode::http_snapshot;
if (m_last_state != MEDIASTATE_IDLE) {
if (snapshot) {
if (m_web_ctrl) m_web_ctrl->Stop();
} else {
// http 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; int last_state = m_last_state;
if (m_last_state != MEDIASTATE_IDLE) { if (m_last_state != MEDIASTATE_IDLE) {
@@ -454,15 +672,38 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(5 * m_failed_retry); m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(5 * m_failed_retry);
} }
void MediaPlayCtrl::on_webrtc_status(WebRtcMediaController::Status status)
{
// Drop CallAfter-queued events from a superseded 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() void MediaPlayCtrl::TogglePlay()
{ {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::TogglePlay"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::TogglePlay";
if (m_last_state != MEDIASTATE_IDLE) { if (m_last_state != MEDIASTATE_IDLE) {
m_next_retry = wxDateTime(); m_next_retry = wxDateTime();
m_web_user_stopped = true;
Stop(); Stop();
} else { } else {
m_failed_retry = 0; m_failed_retry = 0;
m_user_triggered = true; m_user_triggered = true;
m_web_user_stopped = false;
if (m_last_user_play + wxTimeSpan::Minutes(5) < wxDateTime::Now()) { if (m_last_user_play + wxTimeSpan::Minutes(5) < wxDateTime::Now()) {
m_last_failed_codes.clear(); m_last_failed_codes.clear();
m_last_user_play = wxDateTime::Now(); m_last_user_play = wxDateTime::Now();
@@ -528,13 +769,13 @@ void MediaPlayCtrl::ToggleStream()
wxGetApp().app_config->set("not_show_vcamera_stop_prev", "1"); wxGetApp().app_config->set("not_show_vcamera_stop_prev", "1");
if (res == wxID_CANCEL) return; if (res == wxID_CANCEL) return;
} }
if (m_lan_proto > MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) { if (m_lan_proto > LiveviewLocal::LVL_Disable && (m_lan_mode || !m_remote_proto) && !m_disable_lan && !m_lan_ip.empty()) {
std::string url; std::string url;
if (m_lan_proto == MachineObject::LVL_Local) if (m_lan_proto == LiveviewLocal::LVL_Local)
url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd; url = "bambu:///local/" + m_lan_ip + ".?port=6000&user=" + m_lan_user + "&passwd=" + m_lan_passwd;
else if (m_lan_proto == MachineObject::LVL_Rtsps) else if (m_lan_proto == LiveviewLocal::LVL_Rtsps)
url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps"; url = "bambu:///rtsps___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsps";
else if (m_lan_proto == MachineObject::LVL_Rtsp) else if (m_lan_proto == LiveviewLocal::LVL_Rtsp)
url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp"; url = "bambu:///rtsp___" + m_lan_user + ":" + m_lan_passwd + "@" + m_lan_ip + "/streaming/live/1?proto=rtsp";
url += "&device=" + into_u8(m_machine); url += "&device=" + into_u8(m_machine);
url += "&dev_ver=" + m_dev_ver; url += "&dev_ver=" + m_dev_ver;
@@ -666,7 +907,8 @@ void MediaPlayCtrl::load()
{ {
m_last_state = MEDIASTATE_LOADING; m_last_state = MEDIASTATE_LOADING;
SetStatus(_L("Loading...")); SetStatus(_L("Loading..."));
if (wxGetApp().app_config->get("internal_developer_mode") == "true") { const auto mode = current_mode();
if (mode != CameraStreamMode::rtsp && mode != CameraStreamMode::http) {
std::string file_h264 = data_dir() + "/video.h264"; std::string file_h264 = data_dir() + "/video.h264";
std::string file_info = data_dir() + "/video.info"; std::string file_info = data_dir() + "/video.info";
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl dump video to " << file_h264; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl dump video to " << file_h264;
@@ -686,9 +928,8 @@ void MediaPlayCtrl::on_show_hide(wxShowEvent &evt)
evt.Skip(); evt.Skip();
if (m_isBeingDeleted) return; if (m_isBeingDeleted) return;
m_failed_retry = 0; m_failed_retry = 0;
if (m_next_retry.IsValid()) // Try open 2 seconds later, to avoid quick play/stop if (!IsShownOnScreen())
m_next_retry = wxDateTime::Now() + wxTimeSpan::Seconds(2); Stop();
IsShownOnScreen() ? Play() : Stop();
} }
void MediaPlayCtrl::media_proc() void MediaPlayCtrl::media_proc()
+17
View File
@@ -9,6 +9,9 @@
#define MediaPlayCtrl_h #define MediaPlayCtrl_h
#include "wxMediaCtrl3.h" #include "wxMediaCtrl3.h"
#include "IMediaController.hpp"
#include "WebRtcMediaController.hpp"
#include "slic3r/Utils/IPrinterAgent.hpp"
#include <wx/panel.h> #include <wx/panel.h>
@@ -36,6 +39,10 @@ public:
void SetMachineObject(MachineObject * obj); void SetMachineObject(MachineObject * obj);
void SetWebMediaController(IMediaController *ctrl);
void StopWebStream();
bool IsStreaming() const; bool IsStreaming() const;
void ToggleStream(); void ToggleStream();
@@ -54,6 +61,7 @@ protected:
void TogglePlay(); void TogglePlay();
void SetStatus(wxString const &msg, bool hyperlink = true); void SetStatus(wxString const &msg, bool hyperlink = true);
void on_webrtc_status(WebRtcMediaController::Status status);
private: private:
void load(); void load();
@@ -66,6 +74,8 @@ private:
static bool get_stream_url(std::string *url = nullptr); static bool get_stream_url(std::string *url = nullptr);
CameraStreamMode current_mode() const;
private: private:
static inline const wxMediaState MEDIASTATE_IDLE = static_cast<wxMediaState>(3); static inline const wxMediaState MEDIASTATE_IDLE = static_cast<wxMediaState>(3);
static inline const wxMediaState MEDIASTATE_INITIALIZING = static_cast<wxMediaState>(4); static inline const wxMediaState MEDIASTATE_INITIALIZING = static_cast<wxMediaState>(4);
@@ -76,6 +86,13 @@ private:
std::shared_ptr<int> m_token = std::make_shared<int>(0); std::shared_ptr<int> m_token = std::make_shared<int>(0);
wxMediaCtrl3 * m_media_ctrl; wxMediaCtrl3 * m_media_ctrl;
IMediaController * m_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; wxMediaState m_last_state = MEDIASTATE_IDLE;
std::string m_machine; std::string m_machine;
int m_lan_proto = 0; int m_lan_proto = 0;
+11 -1
View File
@@ -34,6 +34,8 @@
#include "DeviceCore/DevManager.h" #include "DeviceCore/DevManager.h"
#include <boost/log/trivial.hpp>
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
@@ -259,6 +261,7 @@ void MonitorPanel::msw_rescale()
void MonitorPanel::select_machine(std::string machine_sn) void MonitorPanel::select_machine(std::string machine_sn)
{ {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::select_machine queueing machine_sn=" << machine_sn;
wxCommandEvent *event = new wxCommandEvent(wxEVT_COMMAND_CHOICE_SELECTED); wxCommandEvent *event = new wxCommandEvent(wxEVT_COMMAND_CHOICE_SELECTED);
event->SetString(machine_sn); event->SetString(machine_sn);
wxQueueEvent(this, event); wxQueueEvent(this, event);
@@ -276,13 +279,20 @@ void MonitorPanel::on_timer(wxTimerEvent& event)
void MonitorPanel::on_select_printer(wxCommandEvent& event) void MonitorPanel::on_select_printer(wxCommandEvent& event)
{ {
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
const std::string requested_dev_id = event.GetString().ToStdString();
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MonitorPanel::on_select_printer requested_dev_id="
<< requested_dev_id << " device_manager=" << (dev ? "set" : "null");
if (!dev) return; if (!dev) return;
if ( dev->get_selected_machine() && (dev->get_selected_machine()->get_dev_id() != event.GetString().ToStdString()) && m_hms_panel) { if ( dev->get_selected_machine() && (dev->get_selected_machine()->get_dev_id() != event.GetString().ToStdString()) && m_hms_panel) {
m_hms_panel->clear_hms_tag(); m_hms_panel->clear_hms_tag();
} }
if (!dev->set_selected_machine(event.GetString().ToStdString())) const bool selected = dev->set_selected_machine(requested_dev_id);
BOOST_LOG_TRIVIAL(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; return;
set_default(); set_default();
+6
View File
@@ -3,6 +3,7 @@
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "DeviceCore/DevConfigUtil.h"
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
@@ -114,6 +115,11 @@ bool DeviceItem::is_blocking_printing(MachineObject* obj_)
PresetBundle* preset_bundle = wxGetApp().preset_bundle; PresetBundle* preset_bundle = wxGetApp().preset_bundle;
source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) {
return false;
}
if (source_model != target_model) { if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine(); std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
+2 -2
View File
@@ -1717,7 +1717,7 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode, const Dynam
return plate_extruders; return plate_extruders;
} }
std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config, bool expand_mixed_slots) const std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const
{ {
std::vector<int> plate_extruders; std::vector<int> plate_extruders;
@@ -1878,7 +1878,7 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
// Expand mixed filament slots to their physical components. A mixed slot is virtual and // Expand mixed filament slots to their physical components. A mixed slot is virtual and
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead. // physical filaments it resolves to instead.
if (expand_mixed_slots) { {
auto* is_mixed_opt = full_config.option<ConfigOptionBools>("filament_is_mixed"); auto* is_mixed_opt = full_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = full_config.option<ConfigOptionStrings>("filament_mixed_components"); auto* comp_strs_opt = full_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
+1 -2
View File
@@ -350,8 +350,7 @@ public:
// get used filaments from config, 1 based idx // get used filaments from config, 1 based idx
std::vector<int> get_extruders(bool conside_custom_gcode = false) const; std::vector<int> get_extruders(bool conside_custom_gcode = false) const;
std::vector<int> get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const; std::vector<int> get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const;
// expand_mixed_slots = false keeps mixed filament slots as slots instead of their components. std::vector<int> get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const;
std::vector<int> get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config, bool expand_mixed_slots = true) const;
std::vector<int> get_extruders_without_support(bool conside_custom_gcode = false) const; std::vector<int> get_extruders_without_support(bool conside_custom_gcode = false) const;
// get used filaments from gcode result, 1 based idx // get used filaments from gcode result, 1 based idx
std::vector<int> get_used_filaments(); std::vector<int> get_used_filaments();
+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(); std::string machine_print_name = obj->get_show_printer_type();
PresetBundle *preset_bundle = wxGetApp().preset_bundle; PresetBundle *preset_bundle = wxGetApp().preset_bundle;
std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle);
Preset* machine_preset = get_printer_preset(obj); const bool optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type);
if (!machine_preset) { const bool optional_target_model = DevPrinterConfigUtil::is_optional_printer_model_id(target_model_id);
Preset* machine_preset = optional_printer_model ? nullptr : get_printer_preset(obj);
if (!optional_printer_model && !optional_target_model && !machine_preset) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty";
return false; return false;
} }
if (machine_print_name != target_model_id) { if (!optional_printer_model && !optional_target_model && machine_print_name != target_model_id) {
MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n" MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n"
"Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); "Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO);
if (dlg.ShowModal() == wxID_NO) { if (dlg.ShowModal() == wxID_NO) {
@@ -2199,6 +2201,11 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
return; return;
} }
if (DevPrinterConfigUtil::is_optional_printer_model_id(obj->printer_type)) {
clear_all_sync_status();
return;
}
bool printer_synced = false; bool printer_synced = false;
// 1. update printer status // 1. update printer status
const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset();
@@ -5799,11 +5806,30 @@ void Sidebar::load_ams_list(MachineObject* obj)
filament_ams_list = build_filament_ams_list(obj); filament_ams_list = build_filament_ams_list(obj);
} }
bool device_change = false;
const std::string& device = obj ? obj->get_dev_id() : ""; const std::string& device = obj ? obj->get_dev_id() : "";
if (p->ams_list_device != device) { const bool same_device = p->ams_list_device == device;
// Keep sync metadata out of the device payload, but preserve it across a
// subscription refresh when the physical filament in a slot is unchanged.
// Otherwise the refreshed configs differ only by the missing
// filament_changed key, causing combo boxes to rebuild and lose their
// transient post-sync badges.
auto &previous_filament_ams_list = wxGetApp().preset_bundle->filament_ams_list;
for (auto &entry : filament_ams_list) {
auto previous = previous_filament_ams_list.find(entry.first);
const auto *previous_changed = previous == previous_filament_ams_list.end() ? nullptr :
dynamic_cast<const ConfigOptionBool *>(previous->second.option("filament_changed"));
if (!same_device || previous_changed == nullptr ||
previous->second.opt_string("filament_id", 0u) != entry.second.opt_string("filament_id", 0u)) {
continue;
}
entry.second.set_key_value("filament_changed",
new ConfigOptionBool{previous_changed->value});
}
bool device_change = !same_device;
if (device_change) {
p->ams_list_device = device; p->ams_list_device = device;
device_change = true;
} }
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size();
if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change)
@@ -5813,9 +5839,27 @@ void Sidebar::load_ams_list(MachineObject* obj)
wxGetApp().preset_bundle->filament_ams_list = filament_ams_list; wxGetApp().preset_bundle->filament_ams_list = filament_ams_list;
for (auto c : p->combos_filament){ for (auto c : p->combos_filament){
c->set_sync_badge(false);
c->update(); c->update();
if (device_change) { }
c->ShowBadge(false);//change printer,then clear badge
if (!device_change) {
size_t combo_index = 0;
for (const auto &entry : filament_ams_list) {
const auto &tray = entry.second;
const bool has_filament = !tray.opt_string("filament_id", 0u).empty();
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
tray.opt_bool("filament_slot_placeholder", 0u);
if (!has_filament && !is_placeholder) {
continue;
}
if (combo_index >= p->combos_filament.size()) {
break;
}
const auto *filament_changed = dynamic_cast<const ConfigOptionBool *>(tray.option("filament_changed"));
p->combos_filament[combo_index]->set_sync_badge(
has_filament && !is_placeholder && filament_changed != nullptr && filament_changed->value);
++combo_index;
} }
} }
@@ -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.") : auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") :
_L("Filament type and color information have been synchronized, but slot information is not included."); _L("Filament type and color information have been synchronized, but slot information is not included.");
c->SetToolTip(tip); c->SetToolTip(tip);
c->ShowBadge(true); c->set_sync_badge(true);
}; };
{ // badge ams filament { // badge ams filament
clear_combos_filament_badge(); clear_combos_filament_badge();
if (sync_result.direct_sync) { if (sync_result.direct_sync) {
// Orca: PresetBundle::sync_ams_list rebuilds combos_filament // A placeholder contributes a preserved project filament to the
// 1:1 from the AMS trays that produce a combo (loaded trays + placeholders; non-placeholder // overwrite result, but it is not AMS-sourced and must not get a
// empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The // sync badge. Non-placeholder empty trays are omitted entirely.
// previous per-tray index walked the full filament_ams_list (including the skipped empties), size_t combo_index = 0;
// so an empty slot before a loaded one dropped the badge for the trailing filaments. for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) {
for (auto &c : p->combos_filament) { const auto &tray = entry.second;
badge_combox_filament(c); const bool has_filament = !tray.opt_string("filament_id", 0u).empty();
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
tray.opt_bool("filament_slot_placeholder", 0u);
if (!has_filament && !is_placeholder) {
continue;
}
if (combo_index >= p->combos_filament.size()) {
break;
}
if (is_placeholder) {
p->combos_filament[combo_index]->set_sync_badge(false);
} else {
badge_combox_filament(p->combos_filament[combo_index]);
}
++combo_index;
} }
} }
} }
@@ -6268,6 +6326,11 @@ template<typename T> void setup_dialog_position(T& info)
void Sidebar::pop_sync_nozzle_and_ams_dialog() { void Sidebar::pop_sync_nozzle_and_ams_dialog() {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog";
auto agent = wxGetApp().getAgent();
if (!agent || agent->get_filament_sync_mode() == FilamentSyncMode::none) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " filament synchronization is not supported; skipping dialog";
return;
}
wxTheApp->CallAfter([this]() { wxTheApp->CallAfter([this]() {
SyncNozzleAndAmsDialog::InputInfo temp_na_info; SyncNozzleAndAmsDialog::InputInfo temp_na_info;
wxPoint big_btn_pt; wxPoint big_btn_pt;
@@ -6399,17 +6462,14 @@ void Sidebar::clear_combos_filament_badge()
{ {
auto &combos_filament = p->combos_filament; auto &combos_filament = p->combos_filament;
for (auto &c : combos_filament) { // clear flag for (auto &c : combos_filament) { // clear flag
c->ShowBadge(false); c->set_sync_badge(false);
} }
} }
void Sidebar::udpate_combos_filament_badge() { void Sidebar::udpate_combos_filament_badge() {
auto &combos_filament = p->combos_filament; auto &combos_filament = p->combos_filament;
for (auto &c : combos_filament) { for (auto &c : combos_filament) {
auto selection = c->GetSelection(); c->update_badge_according_flag();
auto select_flag = c->GetFlag(selection);
auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS;
c->ShowBadge(ok);
} }
} }
@@ -12219,7 +12279,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
sidebar->auto_calc_flushing_volumes(idx); sidebar->auto_calc_flushing_volumes(idx);
} }
auto select_flag = combo->GetFlag(selection); auto select_flag = combo->GetFlag(selection);
combo->ShowBadge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); combo->set_sync_badge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS);
q->on_filament_change(idx); q->on_filament_change(idx);
} }
bool select_preset = !combo->selection_is_changed_according_to_physical_printers(); bool select_preset = !combo->selection_is_changed_according_to_physical_printers();
@@ -20815,9 +20875,14 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning)
} }
if (!check_printer_initialized(obj, true, popup_warning)) if (!check_printer_initialized(obj, true, popup_warning))
return false; return false;
Preset * machine_preset = get_printer_preset(obj); const std::string machine_model = obj->printer_type;
if (!machine_preset) PresetBundle *preset_bundle = wxGetApp().preset_bundle;
const std::string selected_model = preset_bundle ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) : std::string();
if (!DevPrinterConfigUtil::is_optional_printer_model_id(machine_model) &&
!DevPrinterConfigUtil::is_optional_printer_model_id(selected_model) &&
!get_printer_preset(obj)) {
return false; return false;
}
if (wxGetApp().is_blocking_printing()) { if (wxGetApp().is_blocking_printing()) {
if (popup_warning) { if (popup_warning) {
+18
View File
@@ -11,6 +11,7 @@ namespace Slic3r
// IMPORTANT: ordinal order is the Plugins dialog Status sort priority. // IMPORTANT: ordinal order is the Plugins dialog Status sort priority.
Activated, Activated,
Error, Error,
RuntimeError,
Inactive, Inactive,
Loading Loading
}; };
@@ -21,11 +22,28 @@ namespace Slic3r
{ {
case PluginStatus::Activated: return "Activated"; case PluginStatus::Activated: return "Activated";
case PluginStatus::Error: return "Error"; case PluginStatus::Error: return "Error";
case PluginStatus::RuntimeError: return "RuntimeError";
case PluginStatus::Inactive: return "Inactive"; case PluginStatus::Inactive: return "Inactive";
case PluginStatus::Loading: return "Loading"; case PluginStatus::Loading: return "Loading";
} }
return "Inactive"; return "Inactive";
} }
// why: a plugin whose module is live but whose catalog carries an error is a
// RUNTIME fault (e.g. a capability rejected at register time) - it stays
// loaded/checked and is only flagged, distinct from a load-time Error where
// the module never came up. Loading wins over both so an in-flight reload
// never flashes an error.
inline PluginStatus resolve_plugin_status(bool loading, bool has_error, bool is_loaded)
{
if (loading)
return PluginStatus::Loading;
if (has_error)
return is_loaded ? PluginStatus::RuntimeError : PluginStatus::Error;
if (is_loaded)
return PluginStatus::Activated;
return PluginStatus::Inactive;
}
} }
} // namespace Slic3r::GUI } // namespace Slic3r::GUI
+5 -8
View File
@@ -237,6 +237,7 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
payload_item["label"] = dialog_item.display_name; payload_item["label"] = dialog_item.display_name;
payload_item["source"] = to_string(dialog_item.source); payload_item["source"] = to_string(dialog_item.source);
payload_item["status"] = to_string(dialog_item.status); payload_item["status"] = to_string(dialog_item.status);
payload_item["is_loaded"] = dialog_item.is_loaded;
payload_item["error"] = dialog_item.error_text; payload_item["error"] = dialog_item.error_text;
payload_item["update_status"] = to_string(dialog_item.update_status); payload_item["update_status"] = to_string(dialog_item.update_status);
payload_item["unauthorized"] = dialog_item.unauthorized; payload_item["unauthorized"] = dialog_item.unauthorized;
@@ -381,14 +382,7 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
item.sharing_token = descriptor.sharing_token; item.sharing_token = descriptor.sharing_token;
item.thumbnail_url = descriptor.thumbnail_url; item.thumbnail_url = descriptor.thumbnail_url;
if (item.loading) item.status = resolve_plugin_status(item.loading, item.has_error, item.is_loaded);
item.status = PluginStatus::Loading;
else if (item.has_error)
item.status = PluginStatus::Error;
else if (item.is_loaded)
item.status = PluginStatus::Activated;
else
item.status = PluginStatus::Inactive;
item.available_actions = evaluate_action_policy(item); item.available_actions = evaluate_action_policy(item);
const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(), const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(),
@@ -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; BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key;
// A user-disabled plugin has no meaningful error state.
if (!manager.clear_plugin_error(plugin_key))
BOOST_LOG_TRIVIAL(warning) << "Failed to clear plugin error for " << plugin_key << " (failed to find)";
// A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later. // A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later.
if (m_activating_plugin_key == plugin_key) if (m_activating_plugin_key == plugin_key)
m_activating_plugin_key.clear(); m_activating_plugin_key.clear();
+2 -1
View File
@@ -63,6 +63,7 @@ std::string PrePrintChecker::get_print_status_info(PrintDialogStatus status)
case PrintStatusRackReading: return "PrintStatusRackReading"; case PrintStatusRackReading: return "PrintStatusRackReading";
case PrintStatusRackNozzleNumUnmeetWarning: return "PrintStatusRackNozzleNumUnmeetWarning"; case PrintStatusRackNozzleNumUnmeetWarning: return "PrintStatusRackNozzleNumUnmeetWarning";
case PrintStatusHasUnreliableNozzleWarning: return "PrintStatusHasUnreliableNozzleWarning"; case PrintStatusHasUnreliableNozzleWarning: return "PrintStatusHasUnreliableNozzleWarning";
case PrintStatusOptionalPrinterModel: return "PrintStatusOptionalPrinterModel";
case PrintStatusWarningExtFilamentNotMatch: return "PrintStatusWarningExtFilamentNotMatch"; case PrintStatusWarningExtFilamentNotMatch: return "PrintStatusWarningExtFilamentNotMatch";
case PrintStatusFilamentWarningNozzleHRC: return "PrintStatusFilamentWarningNozzleHRC"; case PrintStatusFilamentWarningNozzleHRC: return "PrintStatusFilamentWarningNozzleHRC";
case PrintStatusTPUUnsupportCaliOn: return "PrintStatusTPUUnsupportCaliOn"; case PrintStatusTPUUnsupportCaliOn: return "PrintStatusTPUUnsupportCaliOn";
@@ -104,6 +105,7 @@ wxString PrePrintChecker::get_pre_state_msg(PrintDialogStatus status)
case PrintStatusNeedConsistencyUpgrading: return _L("Cannot send the print job to a printer whose firmware must be updated."); case PrintStatusNeedConsistencyUpgrading: return _L("Cannot send the print job to a printer whose firmware must be updated.");
case PrintStatusBlankPlate: return _L("Cannot send a print job for an empty plate."); case PrintStatusBlankPlate: return _L("Cannot send a print job for an empty plate.");
case PrintStatusTimelapseNoSdcard: return _L("Storage needs to be inserted to record timelapse."); case PrintStatusTimelapseNoSdcard: return _L("Storage needs to be inserted to record timelapse.");
case PrintStatusOptionalPrinterModel: return _L("The selected printer model could not be identified, so compatibility with the print file configuration cannot be verified. Please verify the printer preset before sending.");
case PrintStatusMixAmsAndVtSlotWarning: return _L("You have selected both external and AMS filaments for an extruder. You will need to manually switch the external filament during printing."); case PrintStatusMixAmsAndVtSlotWarning: return _L("You have selected both external and AMS filaments for an extruder. You will need to manually switch the external filament during printing.");
case PrintStatusTPUUnsupportAutoCali: return _L("TPU 90A/TPU 85A is too soft and does not support automatic Flow Dynamics calibration."); case PrintStatusTPUUnsupportAutoCali: return _L("TPU 90A/TPU 85A is too soft and does not support automatic Flow Dynamics calibration.");
case PrintStatusWarningKvalueNotUsed: return _L("Set dynamic flow calibration to 'OFF' to enable custom dynamic flow value."); case PrintStatusWarningKvalueNotUsed: return _L("Set dynamic flow calibration to 'OFF' to enable custom dynamic flow value.");
@@ -379,4 +381,3 @@ bool PrinterMsgPanel::UpdateInfos(const std::vector<prePrintInfo>& infos)
} }
}; };
+1
View File
@@ -112,6 +112,7 @@ enum PrintDialogStatus : unsigned int {
// Orca: a nozzle diameter that differs from the one the printer remembers is a warning, // Orca: a nozzle diameter that differs from the one the printer remembers is a warning,
// not an error, so non-standard nozzles can still be printed with. // not an error, so non-standard nozzles can still be printed with.
PrintStatusNozzleDiameterMismatch, PrintStatusNozzleDiameterMismatch,
PrintStatusOptionalPrinterModel,
PrintStatusPrinterWarningEnd, PrintStatusPrinterWarningEnd,
// Warnings for filament // Warnings for filament
+11 -3
View File
@@ -361,7 +361,8 @@ wxString PresetComboBox::get_preset_item_name(unsigned int index)
return GetString(index); return GetString(index);
} }
std::map<std::string, MachineObject *> machine_list = dev->get_my_machine_list(); std::map<std::string, MachineObject *> machine_list =
dev->get_my_machine_list(dev->get_current_printer_agent_id());
if (machine_list.empty()) { if (machine_list.empty()) {
assert(false); assert(false);
m_selected_dev_id.clear(); m_selected_dev_id.clear();
@@ -479,7 +480,8 @@ void PresetComboBox::add_connected_printers(std::string selected, bool alias_nam
if (!dev) if (!dev)
return; return;
std::map<std::string, MachineObject *> machine_list = dev->get_my_machine_list(); std::map<std::string, MachineObject *> machine_list =
dev->get_my_machine_list(dev->get_current_printer_agent_id());
if (machine_list.empty()) if (machine_list.empty())
return; return;
@@ -999,7 +1001,13 @@ void PlaterPresetComboBox::update_badge_according_flag() {
auto selection = GetSelection(); auto selection = GetSelection();
auto select_flag = GetFlag(selection); auto select_flag = GetFlag(selection);
auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS;
ShowBadge(ok); ShowBadge(m_sync_badge || ok);
}
void PlaterPresetComboBox::set_sync_badge(bool show)
{
m_sync_badge = show;
ShowBadge(show);
} }
bool PlaterPresetComboBox::switch_to_tab() bool PlaterPresetComboBox::switch_to_tab()
+2
View File
@@ -205,6 +205,7 @@ public:
void msw_rescale() override; void msw_rescale() override;
void OnSelect(wxCommandEvent& evt) override; void OnSelect(wxCommandEvent& evt) override;
void update_badge_according_flag(); void update_badge_according_flag();
void set_sync_badge(bool show);
FilamentColor get_cur_color_info(); FilamentColor get_cur_color_info();
void show_default_color_picker(); void show_default_color_picker();
@@ -214,6 +215,7 @@ public:
private: private:
// BBS // BBS
wxColor m_color; wxColor m_color;
bool m_sync_badge{false};
}; };
+3
View File
@@ -1037,6 +1037,9 @@ size_t PublishSettingsDialog::section_group_for(Section kind)
section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style); section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style);
section.mixed_tabs->SetFont(Label::Body_14); section.mixed_tabs->SetFont(Label::Body_14);
section.mixed_tabs->SetBackgroundColour(GetBackgroundColour()); section.mixed_tabs->SetBackgroundColour(GetBackgroundColour());
// The mixed tabs carry full swatch compositions: give them a touch more room than the
// filament tabs so neighbouring compositions stay distinguishable (must precede AppendItem).
section.mixed_tabs->SetItemSpace(FromDIP(3));
page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2)); page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2));
section.mixed_tabs->Hide(); section.mixed_tabs->Hide();
} }
+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_ip = m_input_ip->GetTextCtrl()->GetValue();
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
// ORCA enabling / disabling buttons with conditions enough to change its style // ORCA enabling / disabling buttons with conditions enough to change its style
m_button_ok->Enable(isIp(str_ip.ToStdString()) && str_access_code.Length() == 8); m_button_ok->Enable(isIp(str_ip.ToStdString()) &&
(str_access_code.IsEmpty() || str_access_code.Length() >= 8));
Layout(); Layout();
Fit(); Fit();
@@ -1801,6 +1802,8 @@ void InputIpAddressDialog::on_ok(wxMouseEvent& evt)
m_trouble_shoot->Hide(); m_trouble_shoot->Hide();
std::string str_ip = m_input_ip->GetTextCtrl()->GetValue().ToStdString(); std::string str_ip = m_input_ip->GetTextCtrl()->GetValue().ToStdString();
std::string str_access_code = m_input_access_code->GetTextCtrl()->GetValue().ToStdString(); std::string str_access_code = m_input_access_code->GetTextCtrl()->GetValue().ToStdString();
if (str_access_code.empty())
str_access_code = "88888888";
std::string str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both).ToStdString(); std::string str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both).ToStdString();
// Serial number should not contain lower case letters, and bambu_network plugin crashes // Serial number should not contain lower case letters, and bambu_network plugin crashes
// if user entered the wrong serial number, so we call `Upper()` here. // if user entered the wrong serial number, so we call `Upper()` here.
@@ -1835,6 +1838,8 @@ void InputIpAddressDialog::on_send_retry()
Fit(); Fit();
wxString ip = m_input_ip->GetTextCtrl()->GetValue(); wxString ip = m_input_ip->GetTextCtrl()->GetValue();
wxString str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); wxString str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
if (str_access_code.IsEmpty())
str_access_code = "88888888";
// check support function // check support function
if (!m_obj) return; if (!m_obj) return;
@@ -2056,7 +2061,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
auto str_ip = m_input_ip->GetTextCtrl()->GetValue(); auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue(); auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
if (str_access_code.empty()) { if (str_access_code.IsEmpty()) {
str_access_code = "88888888"; str_access_code = "88888888";
} }
@@ -2072,7 +2077,8 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
} }
// ORCA enabling / disabling buttons with conditions enough to change its style // ORCA enabling / disabling buttons with conditions enough to change its style
bool enable_btns = isIp(str_ip.ToStdString()) && str_access_code.Length() == 8 && invalid_access_code; bool valid_access_code_length = str_access_code.IsEmpty() || str_access_code.Length() >= 8;
bool enable_btns = isIp(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
m_button_manual_setup->Enable(enable_btns); m_button_manual_setup->Enable(enable_btns);
m_button_ok->Enable(enable_btns); m_button_ok->Enable(enable_btns);
+74 -12
View File
@@ -21,6 +21,7 @@
#include "Jobs/PlaterWorker.hpp" #include "Jobs/PlaterWorker.hpp"
#include "DeviceCore/DevConfig.h" #include "DeviceCore/DevConfig.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevNozzleSystem.h" #include "DeviceCore/DevNozzleSystem.h"
#include "DeviceCore/DevNozzleRack.h" #include "DeviceCore/DevNozzleRack.h"
#include "DeviceCore/DevExtensionTool.h" #include "DeviceCore/DevExtensionTool.h"
@@ -1125,7 +1126,10 @@ bool SelectMachineDialog::do_ams_mapping(MachineObject *obj_,bool use_ams)
int filament_result = 0; int filament_result = 0;
std::vector<bool> map_opt; //four values: use_left_ams, use_right_ams, use_left_ext, use_right_ext std::vector<bool> map_opt; //four values: use_left_ams, use_right_ams, use_left_ext, use_right_ext
if (nozzle_nums > 1){ // Orca: only do the per-physical-extruder left/right split when the device actually reports
// 2+ extruders. A non-BBL multi-nozzle printer (e.g. Snapmaker U1) reports a single extruder
// with one filament pool, so it maps as a single surface via the else branch below.
if (nozzle_nums > 1 && obj_->GetExtderSystem()->GetTotalExtderCount() > 1){
//get nozzle property, the extders are same? //get nozzle property, the extders are same?
if (true/*!can_hybrid_mapping(obj_get_extder_data())*/){ if (true/*!can_hybrid_mapping(obj_get_extder_data())*/){
std::vector<FilamentInfo> m_ams_mapping_result_left, m_ams_mapping_result_right; std::vector<FilamentInfo> m_ams_mapping_result_left, m_ams_mapping_result_right;
@@ -2312,8 +2316,10 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
} else if (status == PrintDialogStatus::PrintStatusNoSdcard) { } else if (status == PrintDialogStatus::PrintStatusNoSdcard) {
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
Enable_Send_Button(false); Enable_Send_Button(false);
}else if (status == PrintDialogStatus::PrintStatusUnsupportedPrinter) { }else if (status == PrintDialogStatus::PrintStatusUnsupportedPrinter ||
status == PrintDialogStatus::PrintStatusOptionalPrinterModel) {
wxString msg_text; wxString msg_text;
const bool block_send = status == PrintDialogStatus::PrintStatusUnsupportedPrinter;
try try
{ {
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
@@ -2339,17 +2345,21 @@ void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector<wxSt
auto target_print_name = wxString(DevPrinterConfigUtil::get_printer_display_name(target_model_id)); auto target_print_name = wxString(DevPrinterConfigUtil::get_printer_display_name(target_model_id));
target_print_name.Replace(wxT("Bambu Lab "), wxEmptyString); target_print_name.Replace(wxT("Bambu Lab "), wxEmptyString);
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); msg_text = wxString::Format(_L("The selected printer (%s) is incompatible with the print file configuration (%s). Please adjust the printer preset in the prepare page or choose a compatible printer on this page."), sourcet_print_name, target_print_name);
} else {
msg_text = wxString::Format(_L("The selected printer (%s) has an unknown model, so compatibility with the print file configuration (%s) cannot be verified. Please verify the printer preset before sending."), sourcet_print_name, target_print_name);
}
msg = msg_text; msg = msg_text;
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
Enable_Send_Button(false); Enable_Send_Button(!block_send);
} }
catch (...) catch (...)
{ {
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
Enable_Send_Button(false); Enable_Send_Button(!block_send);
} }
@@ -2526,6 +2536,11 @@ bool SelectMachineDialog::is_blocking_printing(MachineObject* obj_)
} }
} }
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) {
return false;
}
if (source_model != target_model) { if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine(); std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
@@ -2622,6 +2637,10 @@ bool SelectMachineDialog::is_same_printer_model()
if(preset_bundle == nullptr) return result; if(preset_bundle == nullptr) return result;
const auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); const auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
const auto target_model = obj_->printer_type; const auto target_model = obj_->printer_type;
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) {
return true;
}
// Orca: ignore P1P -> P1S // Orca: ignore P1P -> P1S
if (source_model != target_model) { if (source_model != target_model) {
if ((source_model == "C12" && target_model == "C11") || (source_model == "C11" && target_model == "C12") || if ((source_model == "C12" && target_model == "C11") || (source_model == "C11" && target_model == "C12") ||
@@ -3696,9 +3715,31 @@ void SelectMachineDialog::on_send_print()
m_print_job->on_success([this]() { finish_mode(); }); m_print_job->on_success([this]() { finish_mode(); });
m_print_job->on_check_ip_address_fail([this]() { m_print_job->on_check_ip_address_fail([this]() {
// 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); wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS);
wxQueueEvent(this, evt); 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(); wxGetApp().show_ip_address_enter_dialog();
}
});
}); });
// update ota version // update ota version
@@ -4533,11 +4574,13 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
// check nozzle data valid // check nozzle data valid
{ {
if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine || // Commented out the following as ntUndefine and 0.0f are default values
installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) { // (signifying that the value is not given) that should PASS, not fail
show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid); // if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine ||
return false; // installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) {
} // show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid);
// return false;
// }
if (obj_->is_nozzle_flow_type_supported() && if (obj_->is_nozzle_flow_type_supported() &&
installed_ext_nozzle.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) { installed_ext_nozzle.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) {
@@ -4566,7 +4609,10 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
// check nozzle diameter // check nozzle diameter
{ {
if (slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) { // 0.0f is default when there is no nozzle diameter is given.
// In nozzle_diameter == 0.0f case, it passes and does not require a comparison
if (installed_ext_nozzle.GetNozzleDiameter() > 0.0f &&
slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) {
std::vector<wxString> msg_params; std::vector<wxString> msg_params;
if (ext_sys->GetTotalExtderCount() == 2) { if (ext_sys->GetTotalExtderCount() == 2) {
const wxString& mismatch_nozzle_str = _get_nozzle_name(ext_sys->GetTotalExtderCount(), slicing_ext_idx); const wxString& mismatch_nozzle_str = _get_nozzle_name(ext_sys->GetTotalExtderCount(), slicing_ext_idx);
@@ -4760,6 +4806,22 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_)
return; return;
} }
bool has_optional_printer_model = DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type);
if (m_print_type == PrintFromType::FROM_NORMAL) {
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
has_optional_printer_model = has_optional_printer_model ||
(preset_bundle && DevPrinterConfigUtil::is_optional_printer_model_id(
preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle)));
} else if (m_print_type == PrintFromType::FROM_SDCARD_VIEW && !m_required_data_plate_data_list.empty()) {
has_optional_printer_model = has_optional_printer_model ||
DevPrinterConfigUtil::is_optional_printer_model_id(
m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id);
}
if (has_optional_printer_model) {
show_status(PrintDialogStatus::PrintStatusOptionalPrinterModel);
}
if (is_blocking_printing(obj_)) { if (is_blocking_printing(obj_)) {
show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter); show_status(PrintDialogStatus::PrintStatusUnsupportedPrinter);
return; return;
@@ -5482,8 +5544,8 @@ void SelectMachineDialog::reset_and_sync_ams_list()
item = new MaterialItem(m_filament_left_panel, colour_rgb, _L(display_materials[extruder])); item = new MaterialItem(m_filament_left_panel, colour_rgb, _L(display_materials[extruder]));
m_sizer_ams_mapping_left->Add(item, 0, wxALL, FromDIP(5)); m_sizer_ams_mapping_left->Add(item, 0, wxALL, FromDIP(5));
} }
else if (m_filaments_map[extruder] == 2) else // map == 2, or (non-BBL multi-nozzle) 3+; update_material_item_pos() collapses
{ { // these into the single panel when the device reports < 2 extruders.
item = new MaterialItem(m_filament_right_panel, colour_rgb, _L(display_materials[extruder])); item = new MaterialItem(m_filament_right_panel, colour_rgb, _L(display_materials[extruder]));
m_sizer_ams_mapping_right->Add(item, 0, wxALL, FromDIP(5)); m_sizer_ams_mapping_right->Add(item, 0, wxALL, FromDIP(5));
} }
+96 -27
View File
@@ -24,6 +24,7 @@
#include "BitmapCache.hpp" #include "BitmapCache.hpp"
#include "DeviceCore/DevManager.h" #include "DeviceCore/DevManager.h"
#include "DeviceCore/DevConfigUtil.h"
#include "DeviceCore/DevStorage.h" #include "DeviceCore/DevStorage.h"
#include "slic3r/Utils/FileTransferUtils.hpp" #include "slic3r/Utils/FileTransferUtils.hpp"
@@ -300,7 +301,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater)
m_storage_panel->Layout(); m_storage_panel->Layout();
// try to connect // try to connect
m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL); m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(400), -1), wxALIGN_CENTER_HORIZONTAL);
m_statictext_printer_msg->SetFont(::Label::Body_13); m_statictext_printer_msg->SetFont(::Label::Body_13);
m_statictext_printer_msg->SetForegroundColour(*wxBLACK); m_statictext_printer_msg->SetForegroundColour(*wxBLACK);
m_statictext_printer_msg->Hide(); m_statictext_printer_msg->Hide();
@@ -760,9 +761,25 @@ void SendToPrinterDialog::update_priner_status_msg(wxString msg, bool is_warning
if (str_new != str_old) { if (str_new != str_old) {
if (m_statictext_printer_msg->GetLabel() != msg) { if (m_statictext_printer_msg->GetLabel() != msg) {
m_statictext_printer_msg->SetLabel(msg); m_statictext_printer_msg->SetLabel(msg);
m_statictext_printer_msg->SetMinSize(wxSize(FromDIP(400), -1)); const int wrap_width = FromDIP(400);
m_statictext_printer_msg->SetMaxSize(wxSize(FromDIP(400), -1)); m_statictext_printer_msg->Wrap(wrap_width);
m_statictext_printer_msg->Wrap(FromDIP(400)); int line_count = 1;
const wxString wrapped_label = m_statictext_printer_msg->GetLabel();
for (size_t i = 0; i < wrapped_label.length(); ++i) {
if (wrapped_label[i] == '\n')
++line_count;
}
wxCoord text_width = 0;
wxCoord text_height = 0;
m_statictext_printer_msg->GetTextExtent(msg, &text_width, &text_height);
const int extent_line_count = text_width > 0 ?
std::max(1, (static_cast<int>(text_width) + wrap_width - 1) / wrap_width) : 1;
line_count = std::max(line_count, extent_line_count);
const int line_height = std::max(m_statictext_printer_msg->GetCharHeight(), static_cast<int>(text_height));
const int min_height = std::max(m_statictext_printer_msg->GetBestSize().GetHeight(),
line_count * line_height + FromDIP(2));
m_statictext_printer_msg->SetMinSize(wxSize(wrap_width, min_height));
m_statictext_printer_msg->SetMaxSize(wxDefaultSize);
m_statictext_printer_msg->Show(); m_statictext_printer_msg->Show();
Layout(); Layout();
Fit(); Fit();
@@ -1334,6 +1351,11 @@ bool SendToPrinterDialog::is_blocking_printing(MachineObject* obj_)
auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); auto source_model = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle);
auto target_model = obj_->printer_type; auto target_model = obj_->printer_type;
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) {
return false;
}
if (source_model != target_model) { if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine(); std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
@@ -1486,6 +1508,9 @@ void SendToPrinterDialog::show_status(PrintDialogStatus status, std::vector<wxSt
Enable_Send_Button(false); Enable_Send_Button(false);
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
} else if (status == PrintDialogStatus::PrintStatusPublicInitFailed) { } else if (status == PrintDialogStatus::PrintStatusPublicInitFailed) {
wxString msg_text = _L(
"Failed to initialize the printer file transfer. Please check the connection and try again.");
update_print_status_msg(msg_text, true, true);
Enable_Send_Button(false); Enable_Send_Button(false);
Enable_Refresh_Button(true); Enable_Refresh_Button(true);
} else if (status == PrintDialogStatus::PrintStatusPublicUploadFiled) { } else if (status == PrintDialogStatus::PrintStatusPublicUploadFiled) {
@@ -1663,28 +1688,16 @@ extern void refresh_agora_url(char const *device, char const *dev_ver, char
void SendToPrinterDialog::GetConnection() void SendToPrinterDialog::GetConnection()
{ {
DeviceManager *dm = GUI::wxGetApp().getDeviceManager(); DeviceManager *dm = GUI::wxGetApp().getDeviceManager();
MachineObject *obj = dm ? dm->get_selected_machine() : nullptr;
MachineObject *obj = dm->get_selected_machine(); if (!obj)
if (obj == nullptr) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : obj is empty"; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : obj is empty";
m_connection_status = ConnectionStatus::NOT_START; if (obj && !obj->get_file_remote())
}
int remote_proto = obj->get_file_remote();
if (!remote_proto) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : remote_proto is not support"; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : remote_proto is not support";
m_connection_status = ConnectionStatus::NOT_START; if (obj && obj->is_camera_busy_off())
}
if (obj->is_camera_busy_off()) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy"; 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())
{ {
@@ -1709,20 +1722,41 @@ void SendToPrinterDialog::GetConnection()
m_url_timer->GetId()); m_url_timer->GetId());
m_url_timer->StartOnce(8000); m_url_timer->StartOnce(8000);
if (agent) { if (obj && agent)
{
std::string dev_ver = obj->get_ota_version();
std::string dev_id = obj->get_dev_id();
if (m_tcp_try_connect) { if (m_tcp_try_connect) {
std::string devIP = obj->get_dev_ip(); std::string devIP = obj->get_dev_ip();
std::string accessCode = obj->get_access_code(); std::string accessCode = obj->get_access_code();
std::string url = "bambu:///local/" + devIP + "?port=6000&user=" + "bblp" + "&passwd=" + accessCode; std::string url = "bambu:///local/" + devIP + "?port=6000&user=" + "bblp" + "&passwd=" + accessCode;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp, dev_id=" << dev_id
<< ", dev_ip=" << devIP << ", access_code_len=" << accessCode.size();
try
{
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url); 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) { m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg)
CallAfter([this, is_success, err_code, error_msg]() { {
CallAfter([this, is_success, err_code, error_msg]()
{
OnConnection(is_success, err_code, error_msg); OnConnection(is_success, err_code, error_msg);
}); });
}); });
m_filetransfer_tunnel->start_connect(); m_filetransfer_tunnel->start_connect();
} }
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": tcp FileTransferTunnel unavailable for dev_id=" <<
dev_id
<< " dev_ip=" << devIP << ": " << e.what();
if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop();
m_filetransfer_tunnel.reset();
m_connection_status = ConnectionStatus::CONNECTION_FAILED;
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
}
}
else if (m_tutk_try_connect) else if (m_tutk_try_connect)
{ {
std::string protocols[] = {"", "\"tutk\"", "\"agora\"", "\"tutk\",\"agora\""}; std::string protocols[] = {"", "\"tutk\"", "\"agora\"", "\"tutk\",\"agora\""};
@@ -1749,12 +1783,29 @@ void SendToPrinterDialog::GetConnection()
if (boost::algorithm::starts_with(url, "bambu:///")) if (boost::algorithm::starts_with(url, "bambu:///"))
{ {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tutk"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tutk";
try
{
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url); 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) { m_filetransfer_tunnel->on_connection(
CallAfter([this, is_success, err_code, error_msg]() { OnConnection(is_success, err_code, error_msg); }); [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__ << ": tutk FileTransferTunnel unavailable: " << e.
what();
if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop();
m_filetransfer_tunnel.reset();
m_connection_status = ConnectionStatus::CONNECTION_FAILED;
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
}
}
else else
{ {
std::string res = ""; std::string res = "";
@@ -1853,7 +1904,16 @@ void SendToPrinterDialog::ResetTunnelAndJob()
void SendToPrinterDialog::CreateMediaAbilityJob() void SendToPrinterDialog::CreateMediaAbilityJob()
{ {
nlohmann::json media_ability = {{"cmd_type", 7}}; nlohmann::json media_ability = {{"cmd_type", 7}};
try
{
m_filetransfer_mediability_job = std::make_unique<FileTransferJob>(module(), std::string(media_ability.dump())); m_filetransfer_mediability_job = std::make_unique<FileTransferJob>(module(), std::string(media_ability.dump()));
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what();
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
return;
}
m_filetransfer_mediability_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) { m_filetransfer_mediability_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) {
//this pl //this pl
CallAfter([this, res, resp_ec, json_res] { CallAfter([this, res, resp_ec, json_res] {
@@ -1912,7 +1972,16 @@ void SendToPrinterDialog::CreateUploadFileJob(const std::string &path, const std
upload_params["file_path"] = path; upload_params["file_path"] = path;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob";
try
{
m_filetransfer_uploadfile_job = std::make_unique<FileTransferJob>(module(), std::string(upload_params.dump())); m_filetransfer_uploadfile_job = std::make_unique<FileTransferJob>(module(), std::string(upload_params.dump()));
}
catch (const std::exception& e)
{
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what();
show_status(PrintDialogStatus::PrintStatusPublicUploadFiled);
return;
}
m_filetransfer_uploadfile_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) { // m_filetransfer_uploadfile_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) { //
CallAfter([this, res, resp_ec, json_res, bin_res] { CallAfter([this, res, resp_ec, json_res, bin_res] {
UploadFileRessultCallback(res, resp_ec,json_res, bin_res); UploadFileRessultCallback(res, resp_ec,json_res, bin_res);
+132 -85
View File
@@ -12,6 +12,7 @@
#include "MsgDialog.hpp" #include "MsgDialog.hpp"
#include "slic3r/Utils/Http.hpp" #include "slic3r/Utils/Http.hpp"
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "DeviceErrorDialog.hpp" #include "DeviceErrorDialog.hpp"
@@ -1493,27 +1494,26 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
m_setting_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); m_setting_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24)));
m_setting_button->SetBackgroundColour(STATUS_TITLE_BG); m_setting_button->SetBackgroundColour(STATUS_TITLE_BG);
m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0); // m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0);
m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); // m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24)));
m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG); // m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG);
m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp()); // m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp());
m_camera_switch_button->Bind(wxEVT_LEFT_DOWN, &StatusBasePanel::on_camera_switch_toggled, this); // m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) {
m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) { // const std::string js_request_pip = R"(
const std::string js_request_pip = R"( // document.querySelector('video').requestPictureInPicture();
document.querySelector('video').requestPictureInPicture(); // )";
)"; // m_custom_camera_view->RunScript(js_request_pip);
m_custom_camera_view->RunScript(js_request_pip); // });
}); // m_camera_switch_button->Hide();
m_camera_switch_button->Hide();
m_bitmap_sdcard_img->SetToolTip(_L("Storage")); m_bitmap_sdcard_img->SetToolTip(_L("Storage"));
m_bitmap_timelapse_img->SetToolTip(_L("Timelapse")); m_bitmap_timelapse_img->SetToolTip(_L("Timelapse"));
m_bitmap_recording_img->SetToolTip(_L("Video")); m_bitmap_recording_img->SetToolTip(_L("Video"));
m_bitmap_vcamera_img->SetToolTip(_L("Go Live")); m_bitmap_vcamera_img->SetToolTip(_L("Go Live"));
m_setting_button->SetToolTip(_L("Camera Setting")); m_setting_button->SetToolTip(_L("Camera Setting"));
m_camera_switch_button->SetToolTip(_L("Switch Camera View")); // m_camera_switch_button->SetToolTip(_L("Switch Camera View"));
bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); // bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
bSizer_monitoring_title->Add(m_bitmap_sdcard_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_sdcard_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
bSizer_monitoring_title->Add(m_bitmap_timelapse_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_timelapse_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
bSizer_monitoring_title->Add(m_bitmap_recording_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_recording_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5));
@@ -1536,19 +1536,18 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString); m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString);
m_custom_camera_view->EnableContextMenu(false); m_custom_camera_view->EnableContextMenu(false);
Bind(wxEVT_WEBVIEW_NAVIGATING, &StatusBasePanel::on_webview_navigating, this, m_custom_camera_view->GetId()); Bind(wxEVT_WEBVIEW_NAVIGATING, &StatusBasePanel::on_webview_navigating, this, m_custom_camera_view->GetId());
m_web_media_controller = std::make_unique<WebMediaController>(m_custom_camera_view);
m_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40))); m_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40)));
m_media_play_ctrl->SetWebMediaController(m_web_media_controller.get());
m_custom_camera_view->Hide(); m_custom_camera_view->Hide();
m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) { // m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) {
if (evt.GetString() == "leavepictureinpicture") { // if (evt.GetString() == "leavepictureinpicture") {
// When leaving PiP, video gets paused in some cases and toggling play // // When leaving PiP, video gets paused in some cases and toggling play
// programmatically does not work. // // programmatically does not work.
m_custom_camera_view->Reload(); // m_custom_camera_view->Reload();
} // }
else if (evt.GetString() == "enterpictureinpicture") { // });
toggle_builtin_camera();
}
});
sizer->Add(m_media_ctrl, 1, wxEXPAND | wxALL, 0); sizer->Add(m_media_ctrl, 1, wxEXPAND | wxALL, 0);
sizer->Add(m_custom_camera_view, 1, wxEXPAND | wxALL, 0); sizer->Add(m_custom_camera_view, 1, wxEXPAND | wxALL, 0);
@@ -1558,10 +1557,6 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
// //
// sizer->Add(media_ctrl_panel, 1, wxEXPAND | wxALL, 1); // sizer->Add(media_ctrl_panel, 1, wxEXPAND | wxALL, 1);
if (wxGetApp().app_config->get("camera", "enable_custom_source") == "true") {
handle_camera_source_change();
}
return sizer; return sizer;
} }
@@ -2311,6 +2306,27 @@ void StatusPanel::update_camera_state(MachineObject* obj)
{ {
if (!obj) return; if (!obj) return;
auto agent = wxGetApp().getAgent();
const auto camera_mode = agent ? agent->get_camera_stream_mode() : CameraStreamMode::none;
const bool use_webview = camera_mode == CameraStreamMode::http_snapshot;
if (use_webview) {
//m_camera_switch_button->Hide();
if (!m_custom_camera_view->IsShown()) {
// why: do not reload the WebView URL per tick, or redirects can cause a reload loop.
// MediaPlayCtrl (via its WebMediaController) owns loading/playing the stream itself.
m_custom_camera_view->Show();
m_media_ctrl->Hide();
}
} else {
if (m_custom_camera_view->IsShown()) {
m_custom_camera_view->Hide();
// Stop the snapshot WebView before switching to native playback
// or leaving the camera mode.
m_media_play_ctrl->StopWebStream();
}
m_media_ctrl->Show();
}
//sdcard //sdcard
auto sdcard_state = obj->GetStorage()->get_sdcard_state(); auto sdcard_state = obj->GetStorage()->get_sdcard_state();
if (m_last_sdcard != sdcard_state) { if (m_last_sdcard != sdcard_state) {
@@ -2342,7 +2358,12 @@ void StatusPanel::update_camera_state(MachineObject* obj)
m_last_recording = obj->is_recording() ? 1 : 0; m_last_recording = obj->is_recording() ? 1 : 0;
} }
if (!m_bitmap_recording_img->IsShown()) { if (use_webview) {
if (m_bitmap_recording_img->IsShown()) {
m_bitmap_recording_img->Hide();
m_panel_monitoring_title->Layout();
}
} else if (!m_bitmap_recording_img->IsShown()) {
m_bitmap_recording_img->Show(); m_bitmap_recording_img->Show();
m_panel_monitoring_title->Layout(); m_panel_monitoring_title->Layout();
} }
@@ -2399,6 +2420,8 @@ void StatusPanel::update_camera_state(MachineObject* obj)
bool show_vcamera = m_media_play_ctrl->IsStreaming(); bool show_vcamera = m_media_play_ctrl->IsStreaming();
m_camera_popup->update(show_vcamera); m_camera_popup->update(show_vcamera);
} }
m_setting_button->Show(!use_webview);
} }
StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name) StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name)
@@ -2686,7 +2709,8 @@ void StatusPanel::on_subtask_partskip(wxCommandEvent &event)
void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event) void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event)
{ {
if (obj) { if (obj) {
if (obj->can_resume()) { const bool was_resume = obj->can_resume();
if (was_resume) {
BOOST_LOG_TRIVIAL(info) << "monitor: resume current print task dev_id =" << obj->get_dev_id(); BOOST_LOG_TRIVIAL(info) << "monitor: resume current print task dev_id =" << obj->get_dev_id();
obj->command_task_resume(); obj->command_task_resume();
} }
@@ -2694,6 +2718,13 @@ void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event)
BOOST_LOG_TRIVIAL(info) << "monitor: pause current print task dev_id =" << obj->get_dev_id(); BOOST_LOG_TRIVIAL(info) << "monitor: pause current print task dev_id =" << obj->get_dev_id();
obj->command_task_pause(); obj->command_task_pause();
} }
if (is_moonraker_agent()) {
m_pause_resume_pending = true;
m_pause_resume_was_resume = was_resume;
m_pause_resume_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6);
m_pause_resume_machine_id = obj->get_dev_id();
m_project_task_panel->enable_pause_resume_button(false, was_resume ? "resume_disable" : "pause_disable");
}
} }
} }
@@ -2705,6 +2736,12 @@ void StatusPanel::on_subtask_abort(wxCommandEvent &event)
if (obj) { if (obj) {
BOOST_LOG_TRIVIAL(info) << "monitor: stop current print task dev_id =" << obj->get_dev_id(); BOOST_LOG_TRIVIAL(info) << "monitor: stop current print task dev_id =" << obj->get_dev_id();
obj->command_task_abort(); obj->command_task_abort();
if (is_moonraker_agent()) {
m_abort_pending = true;
m_abort_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6);
m_abort_machine_id = obj->get_dev_id();
m_project_task_panel->enable_abort_button(false);
}
} }
}); });
} }
@@ -3678,6 +3715,25 @@ void StatusPanel::update_model_info()
void StatusPanel::update_subtask(MachineObject *obj) void StatusPanel::update_subtask(MachineObject *obj)
{ {
if (!obj) return; if (!obj) return;
const auto now = std::chrono::steady_clock::now();
if (m_pause_resume_pending) {
if (!is_moonraker_agent() || m_pause_resume_machine_id != obj->get_dev_id() ||
obj->can_resume() != m_pause_resume_was_resume) {
m_pause_resume_pending = false;
} else if (now >= m_pause_resume_deadline) {
BOOST_LOG_TRIVIAL(warning) << "StatusPanel: Moonraker pause/resume command did not change printer state";
m_pause_resume_pending = false;
}
}
if (m_abort_pending) {
if (!is_moonraker_agent() || m_abort_machine_id != obj->get_dev_id() || obj->print_status == "FAILED" ||
obj->print_status == "FINISH" || obj->print_status == "IDLE") {
m_abort_pending = false;
} else if (now >= m_abort_deadline) {
BOOST_LOG_TRIVIAL(warning) << "StatusPanel: Moonraker abort command did not change printer state";
m_abort_pending = false;
}
}
if (m_current_print_mode != PRINGINT) { if (m_current_print_mode != PRINGINT) {
if (calib_bitmap == nullptr) { if (calib_bitmap == nullptr) {
m_calib_mode = get_obj_calibration_mode(obj, m_calib_method, cali_stage); m_calib_mode = get_obj_calibration_mode(obj, m_calib_method, cali_stage);
@@ -3785,11 +3841,13 @@ void StatusPanel::update_subtask(MachineObject *obj)
} }
update_basic_print_data(false); update_basic_print_data(false);
} else { } else {
if (!m_pause_resume_pending) {
if (obj->can_resume()) { if (obj->can_resume()) {
m_project_task_panel->enable_pause_resume_button(true, "resume"); m_project_task_panel->enable_pause_resume_button(true, "resume");
} else { } else {
m_project_task_panel->enable_pause_resume_button(true, "pause"); m_project_task_panel->enable_pause_resume_button(true, "pause");
} }
}
m_project_task_panel->enable_partskip_button(obj, true); m_project_task_panel->enable_partskip_button(obj, true);
// update printing stage // update printing stage
m_project_task_panel->update_left_time(obj->mc_left_time); m_project_task_panel->update_left_time(obj->mc_left_time);
@@ -3845,7 +3903,9 @@ void StatusPanel::update_subtask(MachineObject *obj)
m_project_task_panel->market_scoring_hide(); m_project_task_panel->market_scoring_hide();
} }
} else { // model printing is not finished, hide scoring page } else { // model printing is not finished, hide scoring page
if (!m_abort_pending) {
m_project_task_panel->enable_abort_button(true); m_project_task_panel->enable_abort_button(true);
}
m_project_task_panel->market_scoring_hide(); m_project_task_panel->market_scoring_hide();
m_project_task_panel->get_request_failed_panel()->Hide(); m_project_task_panel->get_request_failed_panel()->Hide();
} }
@@ -3920,9 +3980,18 @@ void StatusPanel::update_cloud_subtask(MachineObject *obj)
update_calib_bitmap(); update_calib_bitmap();
if (obj->slice_info) { if (obj->slice_info) {
m_request_url = wxString(obj->slice_info->thumbnail_url); m_request_url = wxString(obj->slice_info->thumbnail_url);
if (!m_request_url.IsEmpty()) { load_thumbnail_from_url(m_request_url, obj);
}
}
}
bool StatusPanel::load_thumbnail_from_url(const wxString &url, MachineObject *obj)
{
if (url.IsEmpty())
return false;
wxImage img; wxImage img;
std::map<wxString, wxImage>::iterator it = img_list.find(m_request_url); std::map<wxString, wxImage>::iterator it = img_list.find(url);
if (it != img_list.end()) { 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)) { if (m_current_print_mode != PrintingTaskType::CALIBRATION ||(m_calib_mode == CalibMode::Calib_Flow_Rate && m_calib_method == CalibrationMethod::CALI_METHOD_MANUAL)) {
img = it->second; img = it->second;
@@ -3938,21 +4007,34 @@ void StatusPanel::update_cloud_subtask(MachineObject *obj)
task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL; task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL;
BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image"; BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image";
} else { } else {
m_request_url = url;
web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url); web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url);
BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url; BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url;
web_request.Start(); web_request.Start();
m_start_loading_thumbnail = false; m_start_loading_thumbnail = false;
} }
} return true;
}
}
} }
void StatusPanel::update_sdcard_subtask(MachineObject *obj) void StatusPanel::update_sdcard_subtask(MachineObject *obj)
{ {
if (!obj) return; if (!obj) return;
if (!m_load_sdcard_thumbnail) { const wxString thumbnail_url = wxString(obj->m_agent_thumbnail_url);
if (!thumbnail_url.IsEmpty()) {
// why: Moonraker has no prediction or weight data, so keep it on the sdcard path.
if (m_request_url != thumbnail_url || !m_load_sdcard_thumbnail) {
if (web_request.IsOk() && web_request.GetState() == wxWebRequest::State_Active)
web_request.Cancel();
update_calib_bitmap();
m_request_url = thumbnail_url;
load_thumbnail_from_url(thumbnail_url, obj);
m_load_sdcard_thumbnail = true;
}
return;
}
if (!m_load_sdcard_thumbnail || !m_request_url.IsEmpty()) {
update_calib_bitmap(); update_calib_bitmap();
if (m_current_print_mode != PrintingTaskType::CALIBRATION) { if (m_current_print_mode != PrintingTaskType::CALIBRATION) {
m_project_task_panel->get_bitmap_thumbnail()->SetBitmap(m_thumbnail_sdcard.bmp()); m_project_task_panel->get_bitmap_thumbnail()->SetBitmap(m_thumbnail_sdcard.bmp());
@@ -3960,11 +4042,14 @@ void StatusPanel::update_sdcard_subtask(MachineObject *obj)
} }
task_thumbnail_state = ThumbnailState::SDCARD_THUMBNAIL; task_thumbnail_state = ThumbnailState::SDCARD_THUMBNAIL;
m_load_sdcard_thumbnail = true; m_load_sdcard_thumbnail = true;
m_request_url.clear();
} }
} }
void StatusPanel::reset_printing_values() void StatusPanel::reset_printing_values()
{ {
m_pause_resume_pending = false;
m_abort_pending = false;
m_project_task_panel->enable_partskip_button(nullptr, false); m_project_task_panel->enable_partskip_button(nullptr, false);
m_project_task_panel->enable_pause_resume_button(false, "pause_disable"); m_project_task_panel->enable_pause_resume_button(false, "pause_disable");
m_project_task_panel->enable_abort_button(false); m_project_task_panel->enable_abort_button(false);
@@ -3990,6 +4075,12 @@ void StatusPanel::reset_printing_values()
this->Layout(); this->Layout();
} }
bool StatusPanel::is_moonraker_agent() const
{
auto* agent = wxGetApp().getAgent();
return agent && std::dynamic_pointer_cast<Slic3r::MoonrakerPrinterAgent>(agent->get_printer_agent()) != nullptr;
}
void StatusPanel::on_axis_ctrl_xy(wxCommandEvent &event) void StatusPanel::on_axis_ctrl_xy(wxCommandEvent &event)
{ {
if (!obj) return; if (!obj) return;
@@ -4959,7 +5050,6 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event)
} }
sdcard_hint_dlg->on_show(); sdcard_hint_dlg->on_show();
}); });
m_camera_popup->Bind(EVT_CAM_SOURCE_CHANGE, &StatusPanel::on_camera_source_change, this);
wxWindow* ctrl = (wxWindow*)event.GetEventObject(); wxWindow* ctrl = (wxWindow*)event.GetEventObject();
wxPoint pos = ctrl->ClientToScreen(wxPoint(0, 0)); wxPoint pos = ctrl->ClientToScreen(wxPoint(0, 0));
wxSize sz = ctrl->GetSize(); wxSize sz = ctrl->GetSize();
@@ -4971,54 +5061,6 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event)
} }
} }
void StatusBasePanel::on_camera_source_change(wxCommandEvent& event)
{
handle_camera_source_change();
}
void StatusBasePanel::handle_camera_source_change()
{
const auto new_cam_url = wxGetApp().app_config->get("camera", "custom_source");
const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
if (enabled && !new_cam_url.empty()) {
m_custom_camera_view->LoadURL(new_cam_url);
toggle_custom_camera();
m_camera_switch_button->Show();
} else {
toggle_builtin_camera();
m_camera_switch_button->Hide();
}
}
void StatusBasePanel::toggle_builtin_camera()
{
m_custom_camera_view->Hide();
m_media_ctrl->Show();
m_media_play_ctrl->Show();
}
void StatusBasePanel::toggle_custom_camera()
{
const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
if (enabled) {
m_custom_camera_view->Show();
m_media_ctrl->Hide();
m_media_play_ctrl->Hide();
}
}
void StatusBasePanel::on_camera_switch_toggled(wxMouseEvent& event)
{
const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
if (enabled && m_media_ctrl->IsShown()) {
toggle_custom_camera();
} else {
toggle_builtin_camera();
}
}
void StatusBasePanel::remove_controls() void StatusBasePanel::remove_controls()
{ {
const std::string js_cleanup_video_element = R"( const std::string js_cleanup_video_element = R"(
@@ -5171,6 +5213,11 @@ bool StatusPanel::is_stage_list_info_changed(MachineObject *obj)
void StatusPanel::set_default() void StatusPanel::set_default()
{ {
BOOST_LOG_TRIVIAL(trace) << "status_panel: set_default"; BOOST_LOG_TRIVIAL(trace) << "status_panel: set_default";
if (m_custom_camera_view->IsShown()) {
m_custom_camera_view->Hide();
m_media_ctrl->Show();
m_media_play_ctrl->StopWebStream();
}
obj = nullptr; obj = nullptr;
last_subtask = nullptr; last_subtask = nullptr;
last_tray_exist_bits = -1; last_tray_exist_bits = -1;
+15 -6
View File
@@ -14,7 +14,10 @@
#include <wx/sizer.h> #include <wx/sizer.h>
#include <wx/gbsizer.h> #include <wx/gbsizer.h>
#include <wx/webrequest.h> #include <wx/webrequest.h>
#include <chrono>
#include <memory>
#include "MediaPlayCtrl.h" #include "MediaPlayCtrl.h"
#include "WebMediaController.hpp"
#include "AMSSetting.hpp" #include "AMSSetting.hpp"
#include "Calibration.hpp" #include "Calibration.hpp"
#include "CalibrationWizardPage.hpp" #include "CalibrationWizardPage.hpp"
@@ -439,7 +442,7 @@ protected:
wxStaticBitmap *m_bitmap_sdcard_img; wxStaticBitmap *m_bitmap_sdcard_img;
wxStaticBitmap *m_bitmap_static_use_time; wxStaticBitmap *m_bitmap_static_use_time;
wxStaticBitmap *m_bitmap_static_use_weight; wxStaticBitmap *m_bitmap_static_use_weight;
wxStaticBitmap* m_camera_switch_button; // wxStaticBitmap* m_camera_switch_button;
wxMediaCtrl3 * m_media_ctrl; wxMediaCtrl3 * m_media_ctrl;
@@ -461,6 +464,8 @@ protected:
ScalableButton *m_button_abort; ScalableButton *m_button_abort;
Button * m_button_clean; Button * m_button_clean;
wxWebView * m_custom_camera_view{nullptr}; wxWebView * m_custom_camera_view{nullptr};
std::unique_ptr<WebMediaController> m_web_media_controller;
wxSimplebook* m_extruder_book; wxSimplebook* m_extruder_book;
std::vector<ExtruderImage *> m_extruderImage; std::vector<ExtruderImage *> m_extruderImage;
@@ -576,13 +581,8 @@ protected:
virtual void on_axis_ctrl_e_up_10(wxCommandEvent &event) { event.Skip(); } virtual void on_axis_ctrl_e_up_10(wxCommandEvent &event) { event.Skip(); }
virtual void on_axis_ctrl_e_down_10(wxCommandEvent &event) { event.Skip(); } virtual void on_axis_ctrl_e_down_10(wxCommandEvent &event) { event.Skip(); }
virtual void on_nozzle_selected(wxCommandEvent &event) { event.Skip(); } virtual void on_nozzle_selected(wxCommandEvent &event) { event.Skip(); }
void on_camera_source_change(wxCommandEvent& event);
void handle_camera_source_change();
void remove_controls(); void remove_controls();
void on_webview_navigating(wxWebViewEvent& evt); void on_webview_navigating(wxWebViewEvent& evt);
void on_camera_switch_toggled(wxMouseEvent& event);
void toggle_custom_camera();
void toggle_builtin_camera();
public: public:
StatusBasePanel(wxWindow * parent, StatusBasePanel(wxWindow * parent,
@@ -629,6 +629,7 @@ class StatusPanel : public StatusBasePanel
{ {
private: private:
friend class MonitorPanel; friend class MonitorPanel;
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
protected: protected:
std::shared_ptr<SliceInfoPopup> m_slice_info_popup; std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
@@ -685,6 +686,13 @@ protected:
CalibrationMethod m_calib_method; CalibrationMethod m_calib_method;
int cali_stage; int cali_stage;
PrintingTaskType m_current_print_mode = PrintingTaskType::NOT_CLEAR; PrintingTaskType m_current_print_mode = PrintingTaskType::NOT_CLEAR;
bool m_pause_resume_pending = false;
bool m_pause_resume_was_resume = false;
std::chrono::steady_clock::time_point m_pause_resume_deadline;
std::string m_pause_resume_machine_id;
bool m_abort_pending = false;
std::chrono::steady_clock::time_point m_abort_deadline;
std::string m_abort_machine_id;
void init_scaled_buttons(); void init_scaled_buttons();
void create_tasklist_info(); void create_tasklist_info();
@@ -787,6 +795,7 @@ protected:
void update_calib_bitmap(); void update_calib_bitmap();
void reset_printing_values(); void reset_printing_values();
bool is_moonraker_agent() const;
void on_webrequest_state(wxWebRequestEvent &evt); void on_webrequest_state(wxWebRequestEvent &evt);
bool is_task_changed(MachineObject* obj); bool is_task_changed(MachineObject* obj);
+13 -1
View File
@@ -1874,6 +1874,11 @@ 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 (m_required_data_plate_data_list.size() > 0) { source_model = m_required_data_plate_data_list[m_print_plate_idx]->printer_model_id; }
} }
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(target_model)) {
return false;
}
if (source_model != target_model) { if (source_model != target_model) {
std::vector<std::string> compatible_machine = obj_->get_compatible_machine(); std::vector<std::string> compatible_machine = obj_->get_compatible_machine();
vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model); vector<std::string>::iterator it = find(compatible_machine.begin(), compatible_machine.end(), source_model);
@@ -1930,7 +1935,13 @@ bool SyncAmsInfoDialog::is_same_printer_model()
if (obj_ == nullptr) { return result; } if (obj_ == nullptr) { return result; }
PresetBundle *preset_bundle = wxGetApp().preset_bundle; PresetBundle *preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) != obj_->printer_type) { const std::string source_model = preset_bundle ? preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) : std::string();
if (DevPrinterConfigUtil::is_optional_printer_model_id(source_model) ||
DevPrinterConfigUtil::is_optional_printer_model_id(obj_->printer_type)) {
return true;
}
if (preset_bundle && source_model != obj_->printer_type) {
if ((obj_->is_support_upgrade_kit && obj_->installed_upgrade_kit) && (preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == "C12")) { if ((obj_->is_support_upgrade_kit && obj_->installed_upgrade_kit) && (preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == "C12")) {
return true; return true;
} }
@@ -3206,6 +3217,7 @@ SyncAmsInfoDialog::~SyncAmsInfoDialog() {
void SyncAmsInfoDialog::set_info(SyncInfo &info) void SyncAmsInfoDialog::set_info(SyncInfo &info)
{ {
m_input_info = info; m_input_info = info;
reinit_dialog();
} }
void SyncAmsInfoDialog::update_lan_machine_list() void SyncAmsInfoDialog::update_lan_machine_list()
+86
View File
@@ -0,0 +1,86 @@
#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();"
"}"
"refreshCameraFrame();"
"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()
{
m_url.clear();
if (m_webview)
m_webview->Stop();
}
// wxMediaState WebMediaController::GetState()
// {
// return wxMediaState{};
// }
// int WebMediaController::GetLastError() const
// {
// return 0;
// }
// wxSize WebMediaController::GetVideoSize() const
// {
// return wxSize{};
// }
}} // namespace Slic3r::GUI
+36
View File
@@ -0,0 +1,36 @@
#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;
// wxMediaState GetState() override;
// int GetLastError() const override;
// wxSize GetVideoSize() const override;
private:
wxWebView * m_webview;
std::string m_url;
CameraStreamMode m_stream_mode = CameraStreamMode::http;
};
}} // namespace Slic3r::GUI
+398
View File
@@ -0,0 +1,398 @@
#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::Verbose, [](rtc::LogLevel level, std::string message) {
BOOST_LOG_TRIVIAL(info) << "[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();
}
}
configuration.iceServers.emplace_back("stun:stun.cloudflare.com:3478");
configuration.iceServers.emplace_back("stun:stun.l.google.com:19302");
auto peer_connection = std::make_shared<rtc::PeerConnection>(std::move(configuration));
BOOST_LOG_TRIVIAL(info) << "WebRTC: creating peer connection with " << configuration.iceServers.size()
<< " ice servers";
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
+3 -6
View File
@@ -311,11 +311,8 @@ void Button::render(wxDC& dc)
} }
} }
auto szContent = textSize; auto szContent = textSize;
// Whether the measured content reserved the text/icon gap. macOS measures an empty label
// as 0-high, so the gap is skipped there; the dot must not advance past it in that case.
const bool gap_reserved = szContent.y > 0;
if (icon.bmp().IsOk()) { if (icon.bmp().IsOk()) {
if (gap_reserved) { if (szContent.y > 0) {
//BBS norrow size between text and icon //BBS norrow size between text and icon
if (vertical) if (vertical)
szContent.y += spacing; szContent.y += spacing;
@@ -360,10 +357,10 @@ void Button::render(wxDC& dc)
dc.DrawBitmap(icon.bmp(), pt); dc.DrawBitmap(icon.bmp(), pt);
//BBS norrow size between text and icon //BBS norrow size between text and icon
if (vertical) { if (vertical) {
pt.y += szIcon.y + (gap_reserved ? spacing : 0); pt.y += szIcon.y + spacing;
pt.x = rcContent.x; pt.x = rcContent.x;
} else { } else {
pt.x += szIcon.x + (gap_reserved ? spacing : 0); pt.x += szIcon.x + spacing;
pt.y = rcContent.y; pt.y = rcContent.y;
} }
} }
+16 -7
View File
@@ -99,7 +99,7 @@ int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* cli
btns.push_back(btn); btns.push_back(btn);
if (btns.size() > 1) if (btns.size() > 1)
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0}); sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL); sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, item_space);
sizer->AddStretchSpacer(1); sizer->AddStretchSpacer(1);
relayout(); relayout();
return btns.size() - 1; return btns.size() - 1;
@@ -256,12 +256,12 @@ void TabCtrl::relayout()
int item = sel + 1; int item = sel + 1;
int first = 0; int first = 0;
for (int i = 0; i < item; ++i) for (int i = 0; i < item; ++i)
offset += btns[i]->GetMinSize().x; offset += btns[i]->GetMinSize().x + item_space * 2;
if (item < btns.size()) if (item < btns.size())
offset += btns[item]->GetMinSize().x; offset += btns[item]->GetMinSize().x + item_space * 2;
int width = GetSize().x; int width = GetSize().x;
for (int i = 0; i < btns.size(); ++i) { for (int i = 0; i < btns.size(); ++i) {
auto size = btns[i]->GetMinSize().x; auto size = btns[i]->GetMinSize().x + item_space * 2;
if (i < sel && offset > width) { if (i < sel && offset > width) {
sizer->Show(i * 2 + 1, false); sizer->Show(i * 2 + 1, false);
sizer->Show(i * 2 + 2, false); sizer->Show(i * 2 + 2, false);
@@ -284,17 +284,26 @@ void TabCtrl::relayout()
if (item >= btns.size()) if (item >= btns.size())
--item; --item;
// Keep spacing 2 ~ 10 TAB_BUTTON_SPACE // Keep spacing 2 ~ 10 TAB_BUTTON_SPACE
int b = GetSize().x - offset - 10 - (item + 1 - first) * 16; int b = GetSize().x - offset - 10 - (item + 1 - first) * item_space * 8;
sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0}); sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0});
Layout(); Layout();
} }
void TabCtrl::SetItemSpace(int space)
{
if (space < 0 || space == item_space)
return;
item_space = space;
relayout();
Refresh();
}
int TabCtrl::GetFullSize() const int TabCtrl::GetFullSize() const
{ {
// Mirrors relayout(): a 10px leading spacer plus every button's min width. // Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing.
int width = 10; int width = 10;
for (const Button* btn : btns) for (const Button* btn : btns)
width += btn->GetMinSize().x; width += btn->GetMinSize().x + item_space * 2;
return width; return width;
} }
+5
View File
@@ -14,6 +14,7 @@ class TabCtrl : public StaticBox
int sel = -1; int sel = -1;
wxFont bold; wxFont bold;
int item_space = 2; // space around each button, both sides (SetItemSpace)
public: public:
TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0); TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0);
@@ -63,6 +64,10 @@ public:
int GetNextVisible(int item) const; int GetNextVisible(int item) const;
bool IsVisible(unsigned int item) const; bool IsVisible(unsigned int item) const;
// Extra space around each tab button (in px on both sides). Defaults to the control-wide
// standard; call before appending items so every button picks it up.
void SetItemSpace(int space);
int GetFullSize() const; int GetFullSize() const;
private: private:
+283 -1
View File
@@ -4,6 +4,14 @@
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <wx/dcclient.h> #include <wx/dcclient.h>
#include <cstdarg>
#include <cstdlib>
#include <cstring>
#include <mutex>
extern "C" {
#include <libavformat/avformat.h>
#include <libavutil/log.h>
}
#ifdef __WIN32__ #ifdef __WIN32__
#include <versionhelpers.h> #include <versionhelpers.h>
#include <wx/msw/registry.h> #include <wx/msw/registry.h>
@@ -43,9 +51,13 @@ wxMediaCtrl3::~wxMediaCtrl3()
m_thread.join(); m_thread.join();
} }
static void adjust_frame_size(wxSize& frame, wxSize const& video, wxSize const& window);
void wxMediaCtrl3::Load(wxURI url) void wxMediaCtrl3::Load(wxURI url)
{ {
std::unique_lock<std::mutex> lk(m_mutex); std::unique_lock<std::mutex> lk(m_mutex);
if (m_external)
return;
m_video_size = wxDefaultSize; m_video_size = wxDefaultSize;
m_error = 0; m_error = 0;
m_url.reset(new wxURI(url)); m_url.reset(new wxURI(url));
@@ -55,6 +67,8 @@ void wxMediaCtrl3::Load(wxURI url)
void wxMediaCtrl3::Play() void wxMediaCtrl3::Play()
{ {
std::unique_lock<std::mutex> lk(m_mutex); std::unique_lock<std::mutex> lk(m_mutex);
if (m_external)
return;
if (m_state != wxMEDIASTATE_PLAYING) { if (m_state != wxMEDIASTATE_PLAYING) {
m_state = wxMEDIASTATE_PLAYING; m_state = wxMEDIASTATE_PLAYING;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED); wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
@@ -74,6 +88,62 @@ void wxMediaCtrl3::Stop()
Refresh(); Refresh();
} }
void wxMediaCtrl3::SetExternalFrame(const wxImage& frame, wxSize videoSize)
{
if (!frame.IsOk())
return;
{
std::unique_lock<std::mutex> lk(m_mutex);
if (!m_external)
return;
m_frame = frame;
m_video_size = videoSize.IsFullySpecified() ? videoSize : frame.GetSize();
adjust_frame_size(m_frame_size, m_video_size, GetSize());
}
CallAfter([this] { Refresh(); });
}
#ifdef _WIN32
void wxMediaCtrl3::SetExternalFrame(const wxBitmap& frame, wxSize videoSize)
{
if (!frame.IsOk())
return;
{
std::unique_lock<std::mutex> lk(m_mutex);
if (!m_external)
return;
m_frame = frame;
m_video_size = videoSize.IsFullySpecified() ? videoSize : frame.GetSize();
adjust_frame_size(m_frame_size, m_video_size, GetSize());
}
CallAfter([this] { Refresh(); });
}
#endif
void wxMediaCtrl3::BeginExternalStream()
{
std::unique_lock<std::mutex> lk(m_mutex);
m_external = true;
m_url.reset();
m_active_url.reset();
m_video_size = wxDefaultSize;
m_frame = wxImage(m_idle_image);
m_cond.notify_all();
Refresh();
}
void wxMediaCtrl3::EndExternalStream()
{
std::unique_lock<std::mutex> lk(m_mutex);
m_external = false;
m_url.reset();
m_active_url.reset();
m_video_size = wxDefaultSize;
m_frame = wxImage(m_idle_image);
m_cond.notify_all();
Refresh();
}
void wxMediaCtrl3::SetIdleImage(wxString const &image) void wxMediaCtrl3::SetIdleImage(wxString const &image)
{ {
if (m_idle_image == image) if (m_idle_image == image)
@@ -185,6 +255,210 @@ void wxMediaCtrl3::bambu_log(void *ctx, int level, tchar const *msg2)
BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data(); BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data();
} }
// FFmpeg's own diagnostics (HTTP status, "Invalid data found", demuxer choice,
// missing stream dimensions, ...) are otherwise swallowed: a failed camera open
// only surfaces as wxMediaCtrl3's generic error code, which MediaPlayCtrl maps to
// the misleading "Player is malfunctioning" string. Forward them to the Orca log
// instead. Verbosity defaults to AV_LOG_VERBOSE and can be raised at runtime with
// ORCA_FFMPEG_LOG_LEVEL=debug|trace|... (or lowered to warning/error/quiet).
static int ffmpeg_log_level_from_env()
{
const char *env = std::getenv("ORCA_FFMPEG_LOG_LEVEL");
if (env == nullptr || *env == '\0')
return AV_LOG_VERBOSE;
const wxString v = wxString(env).Lower();
if (v == "quiet") return AV_LOG_QUIET;
if (v == "panic") return AV_LOG_PANIC;
if (v == "fatal") return AV_LOG_FATAL;
if (v == "error") return AV_LOG_ERROR;
if (v == "warning") return AV_LOG_WARNING;
if (v == "info") return AV_LOG_INFO;
if (v == "verbose") return AV_LOG_VERBOSE;
if (v == "debug") return AV_LOG_DEBUG;
if (v == "trace") return AV_LOG_TRACE;
return AV_LOG_VERBOSE;
}
static void ffmpeg_log_callback(void *avcl, int level, const char *fmt, va_list vl)
{
if (level > av_log_get_level())
return;
thread_local int print_prefix = 1;
char line[1024];
av_log_format_line2(avcl, level, fmt, vl, line, (int) sizeof(line), &print_prefix);
size_t len = std::strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r' || line[len - 1] == ' '))
line[--len] = '\0';
if (len == 0)
return;
if (level <= AV_LOG_ERROR)
BOOST_LOG_TRIVIAL(error) << "ffmpeg: " << line;
else if (level <= AV_LOG_WARNING)
BOOST_LOG_TRIVIAL(warning) << "ffmpeg: " << line;
else if (level <= AV_LOG_INFO)
BOOST_LOG_TRIVIAL(info) << "ffmpeg: " << line;
else
BOOST_LOG_TRIVIAL(debug) << "ffmpeg: " << line;
}
static void install_ffmpeg_logger()
{
av_log_set_level(ffmpeg_log_level_from_env());
av_log_set_callback(&ffmpeg_log_callback);
}
int wxMediaCtrl3::ffmpeg_interrupt_callback(void *opaque)
{
auto *ctrl = static_cast<wxMediaCtrl3 *>(opaque);
std::lock_guard<std::mutex> lock(ctrl->m_mutex);
return ctrl->m_url != ctrl->m_active_url;
}
int wxMediaCtrl3::PlayFfmpeg(std::shared_ptr<wxURI> const &url, std::unique_lock<std::mutex> &lock)
{
static std::once_flag logger_once;
std::call_once(logger_once, install_ffmpeg_logger);
if (avformat_network_init() < 0)
return 2;
AVFormatContext *format_context = avformat_alloc_context();
if (!format_context) {
avformat_network_deinit();
return 2;
}
format_context->interrupt_callback = {&wxMediaCtrl3::ffmpeg_interrupt_callback, this};
format_context->flags |= AVFMT_FLAG_NOBUFFER;
format_context->max_delay = 0;
m_active_url = url;
auto finish = [&](int error) {
lock.unlock();
avformat_close_input(&format_context);
avformat_network_deinit();
lock.lock();
m_active_url.reset();
return error;
};
const std::string uri = url->BuildURI().ToUTF8().data();
const wxString scheme = url->GetScheme();
const bool http_stream = scheme.CmpNoCase("http") == 0 || scheme.CmpNoCase("https") == 0;
AVDictionary *options = nullptr;
if (http_stream) {
// Live multipart MJPEG. fflags=nobuffer / AVFMT_FLAG_NOBUFFER / max_delay=0
// (set above) are the low-latency levers - they disable the demuxer
// read-ahead queue. probesize / analyzeduration only bound the one-off
// avformat_find_stream_info() at open; a 32-byte budget returned before a
// whole JPEG frame was seen, so width/height came back unset and the open
// was rejected. Give it room to identify one frame (a startup cost only).
// rw_timeout / timeout bound a wedged connect or read so a stale stream
// fails fast and is retried, instead of the reader thread hanging.
// avioflags=direct is deliberately NOT set: unbuffered reads make the
// mpjpeg demuxer emit "Packet corrupt" and bail on any short read across
// a multipart boundary.
av_dict_set(&options, "fflags", "nobuffer", 0);
av_dict_set(&options, "probesize", "5000000", 0);
av_dict_set(&options, "analyzeduration", "1000000", 0);
av_dict_set(&options, "rw_timeout", "5000000", 0);
av_dict_set(&options, "timeout", "5000000", 0);
} else {
av_dict_set(&options, "rtsp_transport", "tcp", 0);
}
lock.unlock();
int error = avformat_open_input(&format_context, uri.c_str(), nullptr, &options);
av_dict_free(&options);
lock.lock();
if (error < 0)
return finish(2);
lock.unlock();
error = avformat_find_stream_info(format_context, nullptr);
lock.lock();
if (error < 0)
return finish(2);
const int video_stream = av_find_best_stream(format_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (video_stream < 0)
return finish(2);
AVVideoDecoder decoder;
if (decoder.open(*format_context->streams[video_stream]->codecpar) < 0)
return finish(2);
// Prefer the dimensions the container reported. A small probe budget, or a
// camera that doesn't announce a size up front, can leave these unset - in
// that case fill them in from the first frame that decodes (below) rather
// than failing the open outright.
auto apply_video_size = [&](wxSize size) {
if (!size.IsFullySpecified() || size.x <= 0 || size.y <= 0)
return false;
m_video_size = size;
adjust_frame_size(m_frame_size, m_video_size, GetSize());
NotifyStopped();
return true;
};
bool have_size = apply_video_size({format_context->streams[video_stream]->codecpar->width,
format_context->streams[video_stream]->codecpar->height});
int size_probe_frames = 0; // frames spent still waiting for a usable size
AVPacket *packet = av_packet_alloc();
if (!packet)
return finish(2);
while (m_url == url) {
lock.unlock();
error = av_read_frame(format_context, packet);
lock.lock();
if (m_url != url)
break;
if (error < 0)
break;
if (packet->stream_index == video_stream) {
const int decode_error = decoder.decode(*packet);
if (decode_error == 0) {
if (!have_size) {
have_size = apply_video_size(decoder.decoded_frame_size());
if (!have_size) {
av_packet_unref(packet);
// MJPEG yields a sized frame on the first full packet; if
// several seconds of frames never do, treat it as a bad
// stream instead of sitting in "Loading..." forever.
if (++size_probe_frames > 120)
break;
continue;
}
}
auto frame_size = m_frame_size;
lock.unlock();
#ifdef _WIN32
wxBitmap frame;
decoder.toWxBitmap(frame, frame_size);
#else
wxImage frame;
decoder.toWxImage(frame, frame_size);
#endif
lock.lock();
if (m_url != url)
break;
if (frame.IsOk())
m_frame = frame;
if (!m_refresh_pending.exchange(true)) {
CallAfter([this] {
m_refresh_pending.store(false);
Refresh();
});
}
}
}
av_packet_unref(packet);
}
av_packet_free(&packet);
return finish(m_url == url ? 2 : 1);
}
void wxMediaCtrl3::PlayThread() void wxMediaCtrl3::PlayThread()
{ {
using namespace std::chrono_literals; using namespace std::chrono_literals;
@@ -197,9 +471,16 @@ void wxMediaCtrl3::PlayThread()
continue; continue;
if (!url->HasScheme()) if (!url->HasScheme())
break; 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(); lk.unlock();
Bambu_Tunnel tunnel = nullptr; Bambu_Tunnel tunnel = nullptr;
int error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8()); error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8());
if (error == 0) { if (error == 0) {
Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this); Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this);
error = Bambu_Open(tunnel); error = Bambu_Open(tunnel);
@@ -294,6 +575,7 @@ void wxMediaCtrl3::PlayThread()
tunnel = nullptr; tunnel = nullptr;
lk.lock(); lk.lock();
} }
}
if (m_url == url) if (m_url == url)
m_error = error; m_error = error;
m_frame_size = wxDefaultSize; m_frame_size = wxDefaultSize;
+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); void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
#define BAMBU_DYNAMIC #define BAMBU_DYNAMIC
#include <atomic>
#include <condition_variable> #include <condition_variable>
#include <thread> #include <thread>
#ifndef _WIN32
#include <wx/image.h> #include <wx/image.h>
#endif
#include "Printer/BambuTunnel.h" #include "Printer/BambuTunnel.h"
class AVVideoDecoder; class AVVideoDecoder;
@@ -41,6 +40,16 @@ public:
void Stop(); 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); void SetIdleImage(wxString const & image);
wxMediaState GetState(); wxMediaState GetState();
@@ -59,8 +68,10 @@ protected:
void DoSetSize(int x, int y, int width, int height, int sizeFlags) override; void DoSetSize(int x, int y, int width, int height, int sizeFlags) override;
static void bambu_log(void *ctx, int level, tchar const *msg); static void bambu_log(void *ctx, int level, tchar const *msg);
static int ffmpeg_interrupt_callback(void *opaque);
void PlayThread(); void PlayThread();
int PlayFfmpeg(std::shared_ptr<wxURI> const &url, std::unique_lock<std::mutex> &lock);
void NotifyStopped(); void NotifyStopped();
@@ -77,12 +88,15 @@ private:
#endif #endif
std::shared_ptr<wxURI> m_url; std::shared_ptr<wxURI> m_url;
std::shared_ptr<wxURI> m_active_url;
bool m_external = false;
std::uint64_t m_last_PTS{0}; std::uint64_t m_last_PTS{0};
std::chrono::system_clock::time_point m_last_PTS_expected; std::chrono::system_clock::time_point m_last_PTS_expected;
std::chrono::system_clock::time_point m_last_PTS_practical; std::chrono::system_clock::time_point m_last_PTS_practical;
std::mutex m_mutex; std::mutex m_mutex;
std::condition_variable m_cond; std::condition_variable m_cond;
std::thread m_thread; std::thread m_thread;
std::atomic_bool m_refresh_pending{false};
}; };
#endif /* wxMediaCtrl3_h */ #endif /* wxMediaCtrl3_h */
+173 -2
View File
@@ -1,8 +1,11 @@
#include "BBLPrinterAgent.hpp" #include "BBLPrinterAgent.hpp"
#include "BBLNetworkPlugin.hpp" #include "BBLNetworkPlugin.hpp"
#include "IPrinterAgent.hpp"
#include "NetworkAgentFactory.hpp" #include "NetworkAgentFactory.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "NetworkAgent.hpp"
#include <boost/format.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@@ -10,6 +13,10 @@ using json = nlohmann::json;
#include <type_traits> #include <type_traits>
#include <unordered_map> #include <unordered_map>
#include <memory>
#include <nlohmann/json.hpp>
#include <cmath>
#include <slic3r/GUI/DeviceManager.hpp>
namespace Slic3r { namespace Slic3r {
@@ -133,7 +140,7 @@ BBLPrinterAgent::~BBLPrinterAgent() = default;
void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
{ {
m_cloud_agent = cloud; (void) cloud;
// BBL DLL manages tokens internally, so this is just for interface compliance // BBL DLL manages tokens internally, so this is just for interface compliance
} }
@@ -141,6 +148,163 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
// Communication // Communication
// ============================================================================ // ============================================================================
std::string BBLPrinterAgent::ams_refresh_rfid_gcode(const std::string& tray_id)
{
return (boost::format("M620 R%1% \n") % tray_id).str();
}
std::string BBLPrinterAgent::ams_calibrate_gcode(int ams_id)
{
return (boost::format("M620 C%1% \n") % ams_id).str();
}
std::string BBLPrinterAgent::ams_select_tray_gcode(const std::string& tray_id)
{
return (boost::format("M620 P%1% \n") % tray_id).str();
}
int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
const std::string gcode = ams_refresh_rfid_gcode(tray_id);
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode)
{
const std::string gcode = ams_calibrate_gcode(ams_id);
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
const std::string gcode = ams_select_tray_gcode(tray_id);
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G90 \n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = "G29 \n";
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_homing) {
j["print"]["command"] = "back_to_center";
return publish(dev_id, j, lan_mode);
}
j["print"]["command"] = "gcode_line";
j["print"]["param"] = is_printing ? "G28 X\n" : "G28 \n";
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_bed_ctrl) {
j["print"]["command"] = "set_bed_temp";
j["print"]["temp"] = temp;
return publish(dev_id, j, lan_mode);
}
j["print"]["command"] = "gcode_line";
j["print"]["param"] = (boost::format("M140 S%1%\n") % temp).str();
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = (boost::format("M104 S%1%\n") % temp).str();
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode)
{
nlohmann::json j;
j["print"]["sequence_id"] = std::to_string(sequence_id);
if (supports_mqtt_axis_control) {
int dir = input_val > 0 ? 1 : -1;
// i3-arch printers move the bed for Y/Z, so the on-screen direction is
// reversed -- same negation the g-code fallback below applies.
if (!is_core_xy && (axis == "Y" || axis == "Z")) {
dir = -dir;
}
j["print"]["command"] = "xyz_ctrl";
j["print"]["axis"] = axis;
j["print"]["dir"] = dir;
j["print"]["mode"] = (std::abs(input_val) >= 10) ? 1 : 0;
return publish(dev_id, j, lan_mode);
}
double value = input_val;
if (!is_core_xy && (axis == "Y" || axis == "Z")) {
value = -1.0 * input_val;
}
std::string value_str = (boost::format("%.1f") % (value * unit)).str();
std::string gcode;
if (axis == "X" || axis == "Y" || axis == "Z") {
gcode = (boost::format("M211 S \nM211 X1 Y1 Z1\nM1002 push_ref_mode\nG91 \nG1 %1%%2% F%3%\nM1002 pop_ref_mode\nM211 R\n")
% axis % value_str % speed).str();
} else if (axis == "E") {
gcode = (boost::format("M83 \nG0 %1%%2% F%3%\n") % axis % value_str % speed).str();
} else {
return -1;
}
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode)
{
const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0);
if (rtn == 0) {
BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn;
} else {
BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn;
}
return rtn;
}
int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag)
{ {
json_str = from_orca_payload(std::move(json_str)); json_str = from_orca_payload(std::move(json_str));
@@ -473,8 +637,15 @@ int BBLPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateS
int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
{ {
return dispatch_start<func_start_send_gcode_to_sdcard_legacy, func_start_send_gcode_to_sdcard_0203>( int result = dispatch_start<func_start_send_gcode_to_sdcard_legacy, func_start_send_gcode_to_sdcard_0203>(
BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn); BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn);
if (result != 0) {
BOOST_LOG_TRIVIAL(error) << "start_send_gcode_to_sdcard failed: result=" << result
<< ", try_emmc_print=" << params.try_emmc_print
<< ", legacy_mode=" << BBLNetworkPlugin::instance().use_legacy_network()
<< ", dev_ip=" << params.dev_ip << ", dev_id=" << params.dev_id;
}
return result;
} }
int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
+17 -1
View File
@@ -5,6 +5,7 @@
#include "ICloudServiceAgent.hpp" #include "ICloudServiceAgent.hpp"
#include <string> #include <string>
#include <memory> #include <memory>
#include <nlohmann/json.hpp>
namespace Slic3r { namespace Slic3r {
@@ -28,9 +29,23 @@ public:
// Communication // Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
static std::string ams_refresh_rfid_gcode(const std::string& tray_id);
static std::string ams_calibrate_gcode(int ams_id);
static std::string ams_select_tray_gcode(const std::string& tray_id);
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override;
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override;
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override;
int command_axis_control(std::string dev_id, std::string axis, double unit, double input_val, int speed,
bool is_core_xy, bool supports_mqtt_axis_control, int sequence_id, bool lan_mode) override;
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override;
int disconnect_printer() override; int disconnect_printer() override;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
std::string default_lan_username() const override { return "bblp"; }
// Certificates // Certificates
int check_cert() override; int check_cert() override;
@@ -99,7 +114,8 @@ public:
static std::string from_orca_payload(std::string json_text); static std::string from_orca_payload(std::string json_text);
private: private:
std::shared_ptr<ICloudServiceAgent> m_cloud_agent; // why: the lan/cloud DECISION stays machine-side; keep this mechanical branch in sync with publish_json.
int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode);
}; };
} // namespace Slic3r } // namespace Slic3r
+4 -1
View File
@@ -282,8 +282,11 @@ bool CrealityPrintAgent::parse_cfs_response(const std::string& response,
return true; return true;
} }
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id) bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
if (sync_mode != get_filament_sync_mode())
return false;
if (device_info.dev_ip.empty()) { if (device_info.dev_ip.empty()) {
BOOST_LOG_TRIVIAL(warning) BOOST_LOG_TRIVIAL(warning)
<< "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent"; << "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent";
+2 -1
View File
@@ -1,6 +1,7 @@
#ifndef __CREALITY_PRINT_AGENT_HPP__ #ifndef __CREALITY_PRINT_AGENT_HPP__
#define __CREALITY_PRINT_AGENT_HPP__ #define __CREALITY_PRINT_AGENT_HPP__
#include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include <string> #include <string>
@@ -41,7 +42,7 @@ public:
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
bool fetch_filament_info(std::string dev_id) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
// Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into // Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into
// a flat list of loaded slots, plus the count of CFS boxes the printer reports. // a flat list of loaded slots, plus the count of CFS boxes the printer reports.
@@ -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
+69 -2
View File
@@ -11,6 +11,10 @@
#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability #define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability
#include <string> #include <string>
#include <memory> #include <memory>
#include <vector>
#include <functional>
#include <cstdint>
#include "ICameraSignalingChannel.hpp"
namespace Slic3r { namespace Slic3r {
@@ -43,6 +47,14 @@ enum class FilamentSyncMode {
pull ///< On-demand fetch via REST API (blocking call) pull ///< On-demand fetch via REST API (blocking call)
}; };
enum class CameraStreamMode {
none = 0,
http, // LAN or Cloud
rtsp, // LAN only
webrtc, // Cloud only
http_snapshot // HTTP endpoint returning one image per request
};
/** /**
* IPrinterAgent - Interface for printer operations. * IPrinterAgent - Interface for printer operations.
* *
@@ -84,6 +96,38 @@ public:
*/ */
virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0; virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0;
// why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect
// gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's
// publish funnel turns into a dialog.
virtual int command_ams_refresh_rfid(std::string, 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. * Establish a direct LAN connection to a printer.
*/ */
@@ -168,7 +212,7 @@ public:
virtual std::string get_user_selected_machine() = 0; virtual std::string get_user_selected_machine() = 0;
/** /**
* Update the selected machine preference. * Update the selected cloud machine preference.
*/ */
virtual int set_user_selected_machine(std::string dev_id) = 0; virtual int set_user_selected_machine(std::string dev_id) = 0;
@@ -284,12 +328,20 @@ public:
*/ */
virtual FilamentSyncMode get_filament_sync_mode() const { return FilamentSyncMode::none; } virtual FilamentSyncMode get_filament_sync_mode() const { return FilamentSyncMode::none; }
/**
* Get the camera stream mode for this agent. This value can be deterministic and derived at
* runtime if the printer supports multiple camera stream modes. E.g. LAN => HTTP/RTSP, Cloud => WebRTC.
*
* @return CameraStreamMode indicating how the camera stream is obtained or used:
*/
virtual CameraStreamMode get_camera_stream_mode() const { return CameraStreamMode::none; }
/** /**
* Refresh filament info from the printer synchronously. * Refresh filament info from the printer synchronously.
* Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull. * Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull.
* Populates the MachineObject's DevFilaSystem with fetched filament data. * Populates the MachineObject's DevFilaSystem with fetched filament data.
*/ */
virtual bool fetch_filament_info(std::string dev_id) { return false; } virtual bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) { return false; }
/** /**
* Translate one filament id across the printer boundary. * Translate one filament id across the printer boundary.
@@ -300,6 +352,21 @@ public:
*/ */
virtual std::string to_orca_filament_id(const std::string& printer_filament_id) const { return printer_filament_id; } virtual std::string to_orca_filament_id(const std::string& printer_filament_id) const { return printer_filament_id; }
virtual std::string from_orca_filament_id(const std::string& orca_filament_id) const { return orca_filament_id; } virtual std::string from_orca_filament_id(const std::string& orca_filament_id) const { return orca_filament_id; }
/**
* Get the current camera stream URL for this agent's active machine.
* Only meaningful when get_camera_stream_mode() returns an HTTP 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 } // namespace Slic3r
File diff suppressed because it is too large Load Diff
+72 -10
View File
@@ -9,11 +9,16 @@
#include <set> #include <set>
#include <string> #include <string>
#include <thread> #include <thread>
#include <condition_variable>
#include <deque>
#include <functional>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
namespace Slic3r { namespace Slic3r {
bool moonraker_is_light_name(const std::string& name);
class MoonrakerPrinterAgent : public IPrinterAgent class MoonrakerPrinterAgent : public IPrinterAgent
{ {
public: public:
@@ -68,10 +73,9 @@ public:
int set_on_local_connect_fn(OnLocalConnectedFn fn) override; int set_on_local_connect_fn(OnLocalConnectedFn fn) override;
int set_on_local_message_fn(OnMessageFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override;
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
// Pull-mode agent (on-demand filament sync) CameraStreamMode get_camera_stream_mode() const override;
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } std::string get_camera_url() const override;
bool fetch_filament_info(std::string dev_id) override;
protected: protected:
struct MoonrakerDeviceInfo struct MoonrakerDeviceInfo
@@ -85,6 +89,7 @@ protected:
std::string dev_name; std::string dev_name;
std::string version; std::string version;
std::string klippy_state; std::string klippy_state;
float nozzle_diameter = 0.0f;
bool use_ssl = false; bool use_ssl = false;
} device_info; } device_info;
@@ -105,10 +110,17 @@ protected:
// Methods that derived classes may need to override or access // Methods that derived classes may need to override or access
virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl); virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl);
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const;
static float parse_nozzle_diameter(const nlohmann::json& response);
// State access for derived classes // State access for derived classes
mutable std::recursive_mutex state_mutex; mutable std::recursive_mutex state_mutex;
// Counts detached fetch_filament_info() background threads currently touching `this`
// (see QidiPrinterAgent::fetch_filament_info). Those threads hold a raw `this` with no
// other lifetime protection, so the destructor waits for this to reach 0 before any part
// of the object is torn down — see ~MoonrakerPrinterAgent().
std::atomic<int> filament_fetch_in_flight{0};
// Helpers // Helpers
bool is_numeric(const std::string& value); bool is_numeric(const std::string& value);
std::string normalize_base_url(std::string host, const std::string& port); std::string normalize_base_url(std::string host, const std::string& port);
@@ -121,6 +133,18 @@ protected:
// Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility // Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility
static std::string map_filament_type_to_generic_id(const std::string& filament_type); static std::string map_filament_type_to_generic_id(const std::string& filament_type);
// Send a G-code script via Moonraker (/printer/gcode/script)
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
bool send_gcode(const std::string& dev_id, const std::string& gcode,
const std::string& base_url, const std::string& api_key) const;
bool post_print_action(const std::string& action) const;
bool post_print_action(const std::string& action,
const std::string& base_url, const std::string& api_key) const;
bool send_ws_rpc(const std::string& method, const nlohmann::json& params);
virtual void on_status_loop_tick(const std::string& dev_id) {}
private: private:
int handle_request(const std::string& dev_id, const std::string& json_str); int handle_request(const std::string& dev_id, const std::string& json_str);
int send_version_info(const std::string& dev_id); int send_version_info(const std::string& dev_id);
@@ -128,7 +152,6 @@ private:
bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set<std::string>& objects, std::string& error) const; bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set<std::string>& objects, std::string& error) const;
bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const;
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
void announce_printhost_device(); void announce_printhost_device();
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg);
@@ -137,7 +160,8 @@ private:
void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key); void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key);
void stop_status_stream(); void stop_status_stream();
void run_status_stream(std::string dev_id, std::string base_url, std::string api_key); void run_status_stream(std::string dev_id, std::string base_url, std::string api_key);
void handle_ws_message(const std::string& dev_id, const std::string& payload); void handle_ws_message(std::string dev_id, std::string payload, std::string base_url, std::string api_key);
void refresh_thumbnail_url(std::string base_url, std::string api_key);
void update_status_cache(const nlohmann::json& updates); void update_status_cache(const nlohmann::json& updates);
nlohmann::json build_print_payload_locked() const; nlohmann::json build_print_payload_locked() const;
@@ -151,9 +175,10 @@ private:
const std::string& base_url, const std::string& api_key, const std::string& base_url, const std::string& api_key,
OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
// JSON-RPC helper // Start a print of a previously uploaded G-code file (path relative to the
bool send_jsonrpc_command(const std::string& base_url, const std::string& api_key, // Moonraker gcodes root).
const nlohmann::json& request, std::string& response) const; bool start_print_file(const std::string& base_url, const std::string& api_key,
const std::string& filename, std::string& error_msg) const;
// Connection thread management // Connection thread management
void perform_connection_async(const std::string& dev_id, void perform_connection_async(const std::string& dev_id,
@@ -161,6 +186,12 @@ private:
const std::string& api_key, const std::string& api_key,
uint64_t generation); uint64_t generation);
// why: a printer with no /server/webcams/list entry can still name its stream directly;
// subclasses (e.g. printers with a fixed webcam path) can override this instead.
virtual std::string webcam_stream_override(const std::string& base_url) const { return {}; }
void refresh_webcam_info() const;
bool fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation) const;
// System-specific filament fetch methods // System-specific filament fetch methods
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index); bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index);
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index); bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index);
@@ -189,15 +220,38 @@ private:
mutable std::recursive_mutex payload_mutex; mutable std::recursive_mutex payload_mutex;
nlohmann::json status_cache; nlohmann::json status_cache;
// note: guarded by payload_mutex; filled by refresh_thumbnail_url(), empty url = looked up, none found
std::string thumbnail_filename;
std::string thumbnail_url;
mutable std::string webcam_stream_url;
mutable CameraStreamMode webcam_stream_mode = CameraStreamMode::none;
mutable uint64_t webcam_info_last_lookup_ms = 0;
mutable uint64_t webcam_info_generation = 0;
unsigned thumbnail_lookup_attempts = 0;
static constexpr uint64_t WEBCAM_INFO_REFRESH_INTERVAL_MS = 1000;
std::atomic<int> next_jsonrpc_id{1}; std::atomic<int> next_jsonrpc_id{1};
std::set<std::string> available_objects; // Track for feature detection std::set<std::string> available_objects; // Track for feature detection
bool assumed_light_on = false;
std::atomic<bool> ws_stop{false}; std::atomic<bool> ws_stop{false};
std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection
std::atomic<uint64_t> ws_last_emit_ms{0}; std::atomic<uint64_t> ws_last_emit_ms{0};
std::thread ws_thread; std::thread ws_thread;
// stop_status_stream() invokes ws_abort_io to wake a blocked synchronous
// ws.read()/ws.write()/handshake in run_status_stream(): ws_stop is only
// observed between reads, and Beast's expires_after() does not bound
// synchronous operations.
std::mutex ws_abort_mutex;
std::function<void()> ws_abort_io; // guarded by ws_abort_mutex
// AMS/filament refresh cadence, independent of telemetry dispatch so a steady
// stream of status updates can't starve it (ws_last_emit_ms is reset by those).
static constexpr uint64_t AMS_REFRESH_INTERVAL_MS = 10000;
std::atomic<uint64_t> ams_last_fetch_ms{0};
// Throttling configuration for WebSocket updates // Throttling configuration for WebSocket updates
// Critical changes (state transitions) dispatch immediately; telemetry is throttled // Critical changes (state transitions) dispatch immediately; telemetry is throttled
static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry
@@ -207,7 +261,15 @@ private:
// Connection thread management // Connection thread management
std::atomic<uint64_t> connect_generation{0}; std::atomic<uint64_t> connect_generation{0};
std::thread connect_thread; std::thread connect_thread;
std::recursive_mutex connect_mutex; mutable std::recursive_mutex connect_mutex;
void enqueue_command(std::function<void()> fn);
void run_command_worker();
std::thread cmd_thread;
std::deque<std::function<void()>> cmd_queue;
std::mutex cmd_mutex;
std::condition_variable cmd_cv;
bool cmd_stop = false;
}; };
} // namespace Slic3r } // namespace Slic3r
+127 -8
View File
@@ -4,6 +4,7 @@
#include <algorithm> #include <algorithm>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include "IPrinterAgent.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "NetworkAgent.hpp" #include "NetworkAgent.hpp"
#include "BBLNetworkPlugin.hpp" #include "BBLNetworkPlugin.hpp"
@@ -767,6 +768,77 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos
return -1; 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) 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) if (m_printer_agent)
@@ -788,6 +860,13 @@ int NetworkAgent::send_message_to_printer(std::string dev_id, std::string json_s
return -1; return -1;
} }
std::string NetworkAgent::default_lan_username() const
{
if (m_printer_agent)
return m_printer_agent->default_lan_username();
return {};
}
int NetworkAgent::check_cert() int NetworkAgent::check_cert()
{ {
if (m_printer_agent) if (m_printer_agent)
@@ -846,8 +925,14 @@ std::string NetworkAgent::get_user_selected_machine()
int NetworkAgent::set_user_selected_machine(std::string dev_id) int NetworkAgent::set_user_selected_machine(std::string dev_id)
{ {
if (m_printer_agent) BOOST_LOG_TRIVIAL(info) << "NetworkAgent::set_user_selected_machine: dev_id=" << dev_id
return m_printer_agent->set_user_selected_machine(dev_id); << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : "<null>");
if (m_printer_agent) {
const int result = m_printer_agent->set_user_selected_machine(dev_id);
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::set_user_selected_machine: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::set_user_selected_machine: no printer agent";
return -1; return -1;
} }
@@ -867,15 +952,27 @@ int NetworkAgent::stop_subscribe(std::string module)
int NetworkAgent::add_subscribe(std::vector<std::string> dev_list) int NetworkAgent::add_subscribe(std::vector<std::string> dev_list)
{ {
if (m_printer_agent) BOOST_LOG_TRIVIAL(info) << "NetworkAgent::add_subscribe: count=" << dev_list.size()
return m_printer_agent->add_subscribe(std::move(dev_list)); << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : "<null>");
if (m_printer_agent) {
const int result = m_printer_agent->add_subscribe(std::move(dev_list));
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::add_subscribe: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::add_subscribe: no printer agent";
return -1; return -1;
} }
int NetworkAgent::del_subscribe(std::vector<std::string> dev_list) int NetworkAgent::del_subscribe(std::vector<std::string> dev_list)
{ {
if (m_printer_agent) BOOST_LOG_TRIVIAL(info) << "NetworkAgent::del_subscribe: count=" << dev_list.size()
return m_printer_agent->del_subscribe(std::move(dev_list)); << " printer_agent=" << (m_printer_agent ? m_printer_agent->get_agent_info().id : "<null>");
if (m_printer_agent) {
const int result = m_printer_agent->del_subscribe(std::move(dev_list));
BOOST_LOG_TRIVIAL(info) << "NetworkAgent::del_subscribe: result=" << result;
return result;
}
BOOST_LOG_TRIVIAL(warning) << "NetworkAgent::del_subscribe: no printer agent";
return -1; return -1;
} }
@@ -921,10 +1018,10 @@ FilamentSyncMode NetworkAgent::get_filament_sync_mode() const
return FilamentSyncMode::none; return FilamentSyncMode::none;
} }
bool NetworkAgent::fetch_filament_info(std::string dev_id) bool NetworkAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
if (m_printer_agent) { if (m_printer_agent) {
return m_printer_agent->fetch_filament_info(dev_id); return m_printer_agent->fetch_filament_info(dev_id, sync_mode);
} }
return false; return false;
} }
@@ -943,6 +1040,28 @@ std::string NetworkAgent::from_orca_filament_id(const std::string& orca_filament
return orca_filament_id; return orca_filament_id;
} }
CameraStreamMode NetworkAgent::get_camera_stream_mode() const
{
if (m_printer_agent)
return m_printer_agent->get_camera_stream_mode();
return CameraStreamMode::none;
}
std::string NetworkAgent::get_local_camera_stream_url() const
{
if (m_printer_agent)
return m_printer_agent->get_camera_url();
return {};
}
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) int NetworkAgent::request_bind_ticket(std::string* ticket)
{ {
if (m_printer_agent) if (m_printer_agent)
+20 -1
View File
@@ -2,9 +2,11 @@
#define __NETWORK_Agent_HPP__ #define __NETWORK_Agent_HPP__
#include "bambu_networking.hpp" #include "bambu_networking.hpp"
#include "libslic3r/ProjectTask.hpp" #include "libslic3r/ProjectTask.hpp"
#include "ICloudServiceAgent.hpp" #include "ICloudServiceAgent.hpp"
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include <map> #include <map>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -12,6 +14,8 @@
namespace Slic3r { namespace Slic3r {
class IPrinterAgent;
// Forward declaration // Forward declaration
class BBLNetworkPlugin; class BBLNetworkPlugin;
@@ -142,9 +146,21 @@ public:
int set_on_local_message_fn(OnMessageFn fn); int set_on_local_message_fn(OnMessageFn fn);
int set_server_callback(OnServerErrFn fn); int set_server_callback(OnServerErrFn fn);
int send_message(std::string dev_id, std::string json_str, int qos, int flag); int send_message(std::string dev_id, std::string json_str, int qos, int flag);
int 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 connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl);
int disconnect_printer(); int disconnect_printer();
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag); int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag);
std::string default_lan_username() const;
int check_cert(); int check_cert();
void install_device_cert(std::string dev_id, bool lan_only); void install_device_cert(std::string dev_id, bool lan_only);
bool start_discovery(bool start, bool sending); bool start_discovery(bool start, bool sending);
@@ -164,7 +180,10 @@ public:
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
FilamentSyncMode get_filament_sync_mode() const; FilamentSyncMode get_filament_sync_mode() const;
bool fetch_filament_info(std::string dev_id); bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull);
CameraStreamMode get_camera_stream_mode() const;
std::string get_local_camera_stream_url() const;
std::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 to_orca_filament_id(const std::string& printer_filament_id) const;
std::string from_orca_filament_id(const std::string& orca_filament_id) const; std::string from_orca_filament_id(const std::string& orca_filament_id) const;
int request_bind_ticket(std::string* ticket); int request_bind_ticket(std::string* ticket);
+33 -5
View File
@@ -16,8 +16,10 @@
#include <mutex> #include <mutex>
#include <utility> #include <utility>
#include <slic3r/GUI/GUI_App.hpp> #include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/I18N.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp> #include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/msgdlg.h>
namespace Slic3r { namespace Slic3r {
namespace { namespace {
@@ -314,6 +316,21 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
std::shared_ptr<IPrinterAgent> cached_agent; std::shared_ptr<IPrinterAgent> cached_agent;
auto reject_conflicting_capability = [plugin_key, capability_name](const std::string& error_message) {
if (!wxTheApp || GUI::wxGetApp().is_closing())
return;
GUI::wxGetApp().CallAfter([plugin_key, capability_name, error_message]() {
if (GUI::wxGetApp().is_closing())
return;
PluginManager& manager = PluginManager::instance();
manager.set_plugin_error(plugin_key, error_message);
// note: the unload callback triggered by disabling will call deregister,
// which will be a no-op since the printer agent is never registered
manager.set_capability_enabled({PluginCapabilityType::PrinterConnection, capability_name, plugin_key}, false);
wxMessageBox(wxString::FromUTF8(error_message.c_str()), _L("Plugins"), wxOK | wxICON_WARNING, GUI::wxGetApp().GetTopWindow());
});
};
{ {
std::lock_guard<std::mutex> lock(s_registry_mutex); std::lock_guard<std::mutex> lock(s_registry_mutex);
@@ -332,9 +349,10 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
auto& python_agent_ids = get_python_printer_agent_ids(); auto& python_agent_ids = get_python_printer_agent_ids();
for (const auto& pair : python_agent_ids) { for (const auto& pair : python_agent_ids) {
if (pair.first != capability_key && pair.second == info.id) { if (pair.first != capability_key && pair.second == info.id) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses duplicate agent ID '" << info.id const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
<< "' already registered by capability '" << pair.first.second << "' from plugin '" "' is already registered by capability '" + pair.first.second + "' from plugin '" + pair.first.first + "'.";
<< pair.first.first << "'"; BOOST_LOG_TRIVIAL(warning) << error_message;
reject_conflicting_capability(error_message);
return; return;
} }
} }
@@ -354,12 +372,22 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
auto& agents = get_printer_agents(); auto& agents = get_printer_agents();
auto agent_it = agents.find(info.id); auto agent_it = agents.find(info.id);
// why: reject only when the ID is owned by SOMEONE ELSE - a built-in has an empty
// plugin_identifier, another plugin/capability has a different plugin_full_ref. When it
// IS the same plugin_full_ref, this capability is just re-registering itself, so fall
// through and refresh.
if (agent_it != agents.end() && agent_it->second.plugin_identifier != plugin_full_ref) { if (agent_it != agents.end() && agent_it->second.plugin_identifier != plugin_full_ref) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses agent ID '" << info.id const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
<< "' already registered by '" << agent_it->second.display_name << "'"; "' is already registered by '" + agent_it->second.display_name + "'.";
BOOST_LOG_TRIVIAL(warning) << error_message;
reject_conflicting_capability(error_message);
return; return;
} }
// why: insert_or_assign, not emplace - reaching here means the ID is new, or the same
// capability is re-registering (same plugin_full_ref). In the re-register case we WANT to
// overwrite so the factory closure points at the current live capability instance; emplace
// would silently keep the stale entry.
agents.insert_or_assign(info.id, PrinterAgentInfo(info.id, info.name, plugin_full_ref, std::move(factory))); agents.insert_or_assign(info.id, PrinterAgentInfo(info.id, info.name, plugin_full_ref, std::move(factory)));
python_agent_ids[capability_key] = info.id; python_agent_ids[capability_key] = info.id;
+324 -10
View File
@@ -6,6 +6,8 @@
#include <boost/asio.hpp> #include <boost/asio.hpp>
#include <boost/beast/core/detail/base64.hpp> #include <boost/beast/core/detail/base64.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/uuid/uuid.hpp> #include <boost/uuid/uuid.hpp>
@@ -21,14 +23,18 @@
#include <openssl/hmac.h> #include <openssl/hmac.h>
#include <openssl/rand.h> #include <openssl/rand.h>
#include <openssl/sha.h> #include <openssl/sha.h>
#include <openssl/ssl.h>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <condition_variable>
#include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <optional> #include <optional>
#include <random> #include <random>
#include <set>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -83,6 +89,7 @@ constexpr const char* ORCA_UNSUBSCRIBE_PLUGINS = "/api/v1/plugins/subscriptions"
constexpr const char* ORCA_PLUGINS_MINE = "/api/v1/plugins/mine"; constexpr const char* ORCA_PLUGINS_MINE = "/api/v1/plugins/mine";
constexpr const char* ORCA_PLUGINS_BASE = "/api/v1/plugins"; constexpr const char* ORCA_PLUGINS_BASE = "/api/v1/plugins";
constexpr const char* ORCA_PLUGIN_DOWNLOAD_URL = "/api/v1/plugins/download"; constexpr const char* ORCA_PLUGIN_DOWNLOAD_URL = "/api/v1/plugins/download";
constexpr const char* ORCA_CLOUD_PRINTER = "/api/v1/printers";
constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login"; constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login";
@@ -491,6 +498,7 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
, api_base_url(ORCA_DEFAULT_API_URL) , api_base_url(ORCA_DEFAULT_API_URL)
, auth_base_url(ORCA_DEFAULT_AUTH_URL) , auth_base_url(ORCA_DEFAULT_AUTH_URL)
, cloud_base_url(ORCA_DEFAULT_CLOUD_URL) , cloud_base_url(ORCA_DEFAULT_CLOUD_URL)
, mqtt_connection(std::make_unique<OrcaMqttConnection>())
{ {
auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY; auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY;
pkce_bundle.loopback_port = choose_loopback_port(); pkce_bundle.loopback_port = choose_loopback_port();
@@ -501,6 +509,8 @@ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir)
OrcaCloudServiceAgent::~OrcaCloudServiceAgent() OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
{ {
if (mqtt_connection)
mqtt_connection->stop();
if (refresh_thread.joinable()) { if (refresh_thread.joinable()) {
refresh_thread.join(); refresh_thread.join();
} }
@@ -941,22 +951,45 @@ bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) { retu
int OrcaCloudServiceAgent::connect_server() int OrcaCloudServiceAgent::connect_server()
{ {
const bool logged_in = is_user_login();
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::connect_server logged_in=" << logged_in
<< " api_base_url=" << api_base_url;
if (!logged_in) {
if (mqtt_connection)
mqtt_connection->stop();
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
is_connected = false;
}
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: connect_server requires a logged-in user";
invoke_server_connected_callback(-1, 401);
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
std::string response; std::string response;
unsigned int http_code = 0; unsigned int http_code = 0;
int result = http_get(ORCA_HEALTH_PATH, &response, &http_code); int result = http_get(ORCA_HEALTH_PATH, &response, &http_code);
bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300); bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: cloud health result=" << result << " http_code=" << http_code
<< " connected=" << connected << " response_bytes=" << response.size();
// connect_server() remains a REST health probe. The long-lived fleet MQTT socket
// is started lazily by set_user_selected_machine -> configure_selected_printer_mqtt;
// subscriptions queued before that point are replayed when it starts.
{ {
std::lock_guard<std::recursive_mutex> lock(state_mutex); std::lock_guard<std::recursive_mutex> lock(state_mutex);
is_connected = connected; is_connected = connected;
} }
invoke_server_connected_callback(connected ? 0 : -1, http_code); invoke_server_connected_callback(connected ? 0 : -1, http_code);
return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
} }
bool OrcaCloudServiceAgent::is_server_connected() bool OrcaCloudServiceAgent::is_server_connected()
{ {
// The REST health probe is the signal; the per-printer MQTT socket does not gate
// whole-cloud connectivity (one printer reconnecting must not report the whole
// cloud as lost).
std::lock_guard<std::recursive_mutex> lock(state_mutex); std::lock_guard<std::recursive_mutex> lock(state_mutex);
return is_connected; return is_connected;
} }
@@ -977,13 +1010,232 @@ int OrcaCloudServiceAgent::stop_subscribe(std::string module)
int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list) int OrcaCloudServiceAgent::add_subscribe(std::vector<std::string> dev_list)
{ {
(void) dev_list; BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::add_subscribe count=" << dev_list.size()
return BAMBU_NETWORK_SUCCESS; << " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null");
if (!is_user_login() || !mqtt_connection) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: add_subscribe rejected because cloud is not ready";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
bool queued = true;
for (const std::string& dev_id : dev_list)
queued = mqtt_connection->subscribe(dev_id) && queued;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: add_subscribe queued=" << queued;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
} }
int OrcaCloudServiceAgent::del_subscribe(std::vector<std::string> dev_list) int OrcaCloudServiceAgent::del_subscribe(std::vector<std::string> dev_list)
{ {
(void) dev_list; BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: OrcaCloudServiceAgent::del_subscribe count=" << dev_list.size()
<< " logged_in=" << is_user_login() << " mqtt_connection=" << (mqtt_connection ? "set" : "null");
if (!is_user_login() || !mqtt_connection) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: del_subscribe rejected because cloud is not ready";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
bool queued = true;
for (const std::string& dev_id : dev_list)
queued = mqtt_connection->unsubscribe(dev_id) && queued;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: del_subscribe queued=" << queued;
return queued ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::configure_selected_printer_mqtt(const std::string& dev_id,
OrcaMqttConnection::StateHandler state_handler)
{
(void) dev_id;
OrcaMqttConnection::Config cfg;
cfg.url = "wss://" + api_base_url + "/api/v1/printers/mqtt";
cfg.use_tls = true;
cfg.bearer_provider = [this] { return get_access_token(); };
cfg.client_id = "OrcaSlicer";
cfg.keepalive_seconds = 300;
{
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
m_selected_printer_mqtt_url = cfg.url;
}
if (mqtt_connection->is_running()) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT connection already running";
return BAMBU_NETWORK_SUCCESS;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: configuring fleet MQTT endpoint=" << cfg.url;
// NOTE: no lock is held across start() — it blocks for the whole initial connect
// attempt (up to ~10s), and the message handler below re-enters callback_mutex on
// the MQTT worker thread.
const bool ok = mqtt_connection->start(
cfg,
[this](const std::string& id, const std::string& payload) { deliver_cloud_message(id, payload); },
std::move(state_handler));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: fleet MQTT start returned=" << ok;
return ok ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED;
}
void OrcaCloudServiceAgent::teardown_selected_printer_mqtt()
{
if (mqtt_connection) {
mqtt_connection->stop();
// The connection object is reused for the next printer; drop this printer's
// report topic so its 1:1 socket does not re-subscribe the previous device.
mqtt_connection->clear_subscriptions();
}
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
m_selected_printer_mqtt_url.clear();
}
std::string OrcaCloudServiceAgent::selected_printer_mqtt_url() const
{
std::lock_guard<std::mutex> lock(m_selected_url_mutex);
return m_selected_printer_mqtt_url;
}
void OrcaCloudServiceAgent::deliver_cloud_message(const std::string& dev_id, const std::string& payload)
{
OnMessageFn callback;
{
std::lock_guard<std::mutex> lock(callback_mutex);
callback = printer_status_callback;
}
if (callback)
callback(dev_id, payload);
}
int OrcaCloudServiceAgent::set_printer_status_callback(OnMessageFn fn)
{
std::lock_guard<std::mutex> lock(callback_mutex);
printer_status_callback = std::move(fn);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: printer status callback=" << (printer_status_callback ? "set" : "clear");
return BAMBU_NETWORK_SUCCESS;
}
int OrcaCloudServiceAgent::send_printer_command(const std::string& dev_id, const std::string& body)
{
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: send_printer_command dev_id=" << dev_id
<< " body_bytes=" << body.size() << " logged_in=" << is_user_login();
if (dev_id.empty() || !is_user_login()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_printer_command rejected";
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
}
const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + dev_id + "/commands";
std::string response;
unsigned int http_code = 0;
int result = http_post(path, body, &response, &http_code);
BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: command dev=" << dev_id
<< " http=" << http_code << " result=" << result
<< " response_bytes=" << response.size();
return (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300)
? BAMBU_NETWORK_SUCCESS
: BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
int OrcaCloudServiceAgent::upload_gcode_via_cloud(const std::string& dev_id,
const std::string& local_gcode_path,
std::string* job_id,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn)
{
if (dev_id.empty() || local_gcode_path.empty() || !is_user_login())
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
if (cancel_fn && cancel_fn())
return BAMBU_NETWORK_ERR_CANCELED;
// Step 1: POST print-jobs/uploads -> a short-lived presigned R2 PUT URL. No
// metadata rides this request; filename/start are only relevant to the HTTP
// .../start finalize route, which this MQTT-driven flow does not call.
const std::string uploads_path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/uploads";
std::string response;
unsigned int http_code = 0;
int result = http_post(uploads_path, "{}", &response, &http_code);
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/uploads failed http_code=" << http_code;
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
std::string upload_job_id;
std::string upload_url;
try {
const nlohmann::json j = nlohmann::json::parse(response);
upload_job_id = j.value("job_id", "");
upload_url = j.value("upload_url", "");
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: failed to parse print-jobs/uploads response: " << e.what();
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
if (upload_job_id.empty() || upload_url.empty()) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: print-jobs/uploads response missing job_id/upload_url";
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
if (cancel_fn && cancel_fn())
return BAMBU_NETWORK_ERR_CANCELED;
// Step 2: PUT the G-code straight to R2 with the one-time URL from step 1. This
// is a scoped, PUT-only, short-TTL capability with no bearer token of its own,
// so it bypasses http_put (which always prefixes api_base_url and attaches the
// cloud session's Authorization header - neither belongs on an R2 PUT).
bool canceled = false;
unsigned put_status = 0;
std::string put_error;
Http::put(upload_url)
.tls_verify(true)
.header("Content-Type", "text/x.gcode")
.set_put_body(boost::filesystem::path(local_gcode_path))
.timeout_connect(5)
.timeout_max(300) // large G-code over a slow link
.on_progress([&](Http::Progress progress, bool& cancel) {
if (cancel_fn && cancel_fn()) {
cancel = true;
canceled = true;
return;
}
if (update_fn && progress.ultotal > 0) {
const int percent = static_cast<int>((progress.ulnow * 100) / progress.ultotal);
update_fn(PrintingStageUpload, percent, "Uploading...");
}
})
.on_complete([&](std::string, unsigned status) { put_status = status; })
.on_error([&](std::string, std::string err, unsigned status) {
put_status = status;
put_error = std::move(err);
})
.perform_sync();
if (canceled)
return BAMBU_NETWORK_ERR_CANCELED;
if (put_status < 200 || put_status >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: R2 upload failed status=" << put_status << " error=" << put_error;
return BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED;
}
if (job_id)
*job_id = std::move(upload_job_id);
return BAMBU_NETWORK_SUCCESS;
}
int OrcaCloudServiceAgent::start_cloud_print_job(const std::string& dev_id,
const std::string& job_id,
const std::string& filename,
bool start)
{
if (dev_id.empty() || job_id.empty() || !is_user_login())
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
nlohmann::json body;
if (!filename.empty())
body["filename"] = filename;
body["start"] = start;
const std::string path = std::string(ORCA_CLOUD_PRINTER) + "/" + Http::url_encode(dev_id) + "/print-jobs/" +
Http::url_encode(job_id) + "/start";
std::string response;
unsigned int http_code = 0;
const int result = http_post(path, body.dump(), &response, &http_code);
if (result != BAMBU_NETWORK_SUCCESS || http_code < 200 || http_code >= 300) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: print-jobs/" << job_id << "/start failed http_code=" << http_code;
return BAMBU_NETWORK_ERR_CONNECT_FAILED;
}
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
@@ -2024,6 +2276,10 @@ bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool noti
void OrcaCloudServiceAgent::clear_session() void OrcaCloudServiceAgent::clear_session()
{ {
if (mqtt_connection) {
mqtt_connection->stop();
mqtt_connection->clear_subscriptions();
}
{ {
std::lock_guard<std::mutex> lock(session_mutex); std::lock_guard<std::mutex> lock(session_mutex);
session = SessionInfo{}; session = SessionInfo{};
@@ -2139,7 +2395,11 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon
return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
} }
int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) int OrcaCloudServiceAgent::http_post(const std::string& path,
const std::string& body,
std::string* response_body,
unsigned int* http_code,
const std::string& content_type)
{ {
std::string url = api_base_url + path; std::string url = api_base_url + path;
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url; BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url;
@@ -2163,7 +2423,7 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
http.header("Authorization", "Bearer " + token); http.header("Authorization", "Bearer " + token);
} }
http.header("Content-Type", "application/json"); http.header("Content-Type", content_type);
http.set_post_body(body); http.set_post_body(body);
http.on_complete([&](std::string resp_body, unsigned resp_status) { http.on_complete([&](std::string resp_body, unsigned resp_status) {
@@ -2623,11 +2883,65 @@ int OrcaCloudServiceAgent::check_user_task_report(int* task_id, bool* printable)
int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::string* http_body) int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::string* http_body)
{ {
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info (stub)"; std::string response;
unsigned int code = 0;
int result = http_get(ORCA_CLOUD_PRINTER, &response, &code);
if (http_code) if (http_code)
*http_code = 200; *http_code = code;
if (http_body)
*http_body = "{}"; if (result != 0 || code != 200) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info failed - http_code=" << code << ", response=" << response;
return result != 0 ? result : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
}
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: get_user_print_info fetched - http_code=" << code << ", response=" << response;
try {
auto resp_json = nlohmann::json::parse(response);
nlohmann::json devices = nlohmann::json::array();
for (const auto& printer : resp_json.value("data", nlohmann::json::array())) {
nlohmann::json device;
std::string role = printer.value("access_role", "");
// A printer with the role "view" only has monitoring access for orca cloud.
// The printer is owned by a different person and was shared to the current user without
// any permission to control the printer so we discard this printer. Comment this out if
// OrcaSlicer wants to support view only printers.
if (role.empty() || role == "viewer")
continue;
device["dev_id"] = printer.value("id", "");
device["dev_name"] = printer.value("name", "");
if (printer.contains("model") && printer["model"].is_string())
device["dev_model_name"] = printer["model"].get<std::string>();
bool online = false;
if (printer.contains("status_snapshot") && printer["status_snapshot"].is_object()) {
const auto& status = printer["status_snapshot"].value("status", nlohmann::json::object());
online = status.value("connection", nlohmann::json::object()).value("state", "") == "online";
if (status.contains("job") && status["job"].is_object())
device["task_status"] = status["job"].value("state", "");
}
device["dev_online"] = online;
devices.push_back(device);
}
if (http_body) {
nlohmann::json out;
out["devices"] = devices;
*http_body = out.dump();
}
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info parsed - device_count=" << devices.size()
<< ", devices=" << devices.dump();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: get_user_print_info parse exception - " << e.what();
return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED;
}
return BAMBU_NETWORK_SUCCESS; return BAMBU_NETWORK_SUCCESS;
} }
+55 -1
View File
@@ -2,6 +2,12 @@
#define __ORCA_CLOUD_SERVICE_AGENT_HPP__ #define __ORCA_CLOUD_SERVICE_AGENT_HPP__
#include "ICloudServiceAgent.hpp" #include "ICloudServiceAgent.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <cstdlib> #include <cstdlib>
#include <string> #include <string>
#include <map> #include <map>
@@ -9,12 +15,17 @@
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <functional> #include <functional>
#include <condition_variable>
#include <cstdint>
#include <set>
#include <memory> #include <memory>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "OrcaMqttConnection.hpp"
class wxSecretStore; class wxSecretStore;
namespace Slic3r { namespace Slic3r {
@@ -206,6 +217,19 @@ public:
int add_subscribe(std::vector<std::string> dev_list) override; int add_subscribe(std::vector<std::string> dev_list) override;
int del_subscribe(std::vector<std::string> dev_list) override; int del_subscribe(std::vector<std::string> dev_list) override;
void enable_multi_machine(bool enable) override; void enable_multi_machine(bool enable) override;
int set_printer_status_callback(OnMessageFn fn);
int send_printer_command(const std::string& dev_id, const std::string& body);
int upload_gcode_via_cloud(const std::string& dev_id,
const std::string& local_gcode_path,
std::string* job_id,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn);
int start_cloud_print_job(const std::string& dev_id,
const std::string& job_id,
const std::string& filename,
bool start = true);
// ======================================================================== // ========================================================================
// ICloudServiceAgent Interface Implementation - Settings Synchronization // ICloudServiceAgent Interface Implementation - Settings Synchronization
@@ -346,7 +370,29 @@ public:
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = ""); static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
OrcaMqttConnection* get_mqtt_connection() noexcept {
return mqtt_connection.get();
}
const OrcaMqttConnection* get_mqtt_connection() const noexcept {
return mqtt_connection.get();
}
// Account-scoped cloud socket: wss://<api_base_url>/api/v1/printers/mqtt.
// configure_ blocks for the duration of the initial connect attempt, so callers
// drive it off the UI thread; teardown_ is synchronous. The dev_id argument is
// retained for source compatibility with the printer-agent lifecycle; it does
// not participate in endpoint construction.
int configure_selected_printer_mqtt(const std::string& dev_id,
OrcaMqttConnection::StateHandler state_handler = {});
void teardown_selected_printer_mqtt();
// Test hook: the wss:// URL of the current fleet socket ("" when none).
std::string selected_printer_mqtt_url() const;
private: private:
// Fans one inbound fleet MQTT message out to printer_status_callback.
void deliver_cloud_message(const std::string& dev_id, const std::string& payload);
// Sync protocol helpers // Sync protocol helpers
int sync_pull( int sync_pull(
std::function<void(const SyncPullResponse&)> on_success, std::function<void(const SyncPullResponse&)> on_success,
@@ -370,7 +416,11 @@ private:
// HTTP request helpers // HTTP request helpers
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code); int http_get(const std::string& path, std::string* response_body, unsigned int* http_code);
int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); int http_post(const std::string& path,
const std::string& body,
std::string* response_body,
unsigned int* http_code,
const std::string& content_type = "application/json");
int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code); int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code); int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code);
std::map<std::string, std::string> data_headers(); std::map<std::string, std::string> data_headers();
@@ -423,6 +473,9 @@ private:
std::chrono::system_clock::now().time_since_epoch()).count()}; std::chrono::system_clock::now().time_since_epoch()).count()};
// Member variables - connection state // Member variables - connection state
std::unique_ptr<OrcaMqttConnection> mqtt_connection;
std::string m_selected_printer_mqtt_url; // guarded by m_selected_url_mutex
mutable std::mutex m_selected_url_mutex;
bool is_connected{false}; bool is_connected{false};
bool enable_track{false}; bool enable_track{false};
bool multi_machine_enabled{false}; bool multi_machine_enabled{false};
@@ -436,6 +489,7 @@ private:
AppOnHttpErrorFn on_http_error_fn; AppOnHttpErrorFn on_http_error_fn;
GetCountryCodeFn get_country_code_fn; GetCountryCodeFn get_country_code_fn;
QueueOnMainFn queue_on_main_fn; QueueOnMainFn queue_on_main_fn;
OnMessageFn printer_status_callback;
mutable std::mutex callback_mutex; mutable std::mutex callback_mutex;
// Thread safety // Thread safety
@@ -0,0 +1,320 @@
#include "OrcaCloudSignalingChannel.hpp"
#include "Http.hpp"
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/beast/core.hpp>
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
#include <openssl/ssl.h>
#include <cctype>
#include <iomanip>
#include <sstream>
#include <stdexcept>
namespace Slic3r {
OrcaCloudSignalingChannel::OrcaCloudSignalingChannel(std::shared_ptr<ICloudServiceAgent> cloud, std::string dev_id)
: m_cloud(std::move(cloud))
, m_dev_id(std::move(dev_id))
{
}
OrcaCloudSignalingChannel::~OrcaCloudSignalingChannel()
{
close();
}
void OrcaCloudSignalingChannel::open()
{
bool expected = false;
if (!m_open.compare_exchange_strong(expected, true))
return;
m_stop.store(false);
m_thread = std::thread([this] { run(); });
}
void OrcaCloudSignalingChannel::close()
{
m_stop.store(true);
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(m_mutex);
conn = m_conn;
}
if (conn) {
// Established session: close the socket on the io_context's own thread so
// the pending async_read completes and io_context.run() unwinds.
boost::asio::post(conn->io_context, [conn] {
boost::system::error_code ec;
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
boost::beast::get_lowest_layer(conn->websocket).close(ec);
});
// Pre-run() phase (still in the synchronous connect/handshake): best-effort
// direct interruption.
boost::system::error_code ec;
boost::beast::get_lowest_layer(conn->websocket).cancel(ec);
boost::beast::get_lowest_layer(conn->websocket).close(ec);
}
if (m_thread.joinable())
m_thread.join();
m_open.store(false);
}
void OrcaCloudSignalingChannel::send_offer(std::string sdp)
{
send_json(nlohmann::json{{"type", "webrtc.offer"}, {"sdp", std::move(sdp)}}.dump());
}
void OrcaCloudSignalingChannel::send_ice(std::string candidate, std::string mid)
{
send_json(nlohmann::json{{"type", "webrtc.ice"},
{"candidate", std::move(candidate)},
{"sdpMid", std::move(mid)}}
.dump());
}
std::string OrcaCloudSignalingChannel::encode_path_component(const std::string& value)
{
std::ostringstream encoded;
encoded << std::uppercase << std::hex;
for (unsigned char c : value) {
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
encoded << c;
else
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<unsigned int>(c);
}
return encoded.str();
}
std::string OrcaCloudSignalingChannel::host_without_scheme(std::string value)
{
const auto scheme = value.find("://");
if (scheme != std::string::npos)
value.erase(0, scheme + 3);
const auto slash = value.find('/');
if (slash != std::string::npos)
value.erase(slash);
return value;
}
void OrcaCloudSignalingChannel::unavailable(CameraUnavailableReason reason, std::string detail)
{
if (on_unavailable)
on_unavailable(reason, std::move(detail));
}
void OrcaCloudSignalingChannel::run()
{
try {
if (!m_cloud || !m_cloud->ensure_token_fresh("camera")) {
unavailable(CameraUnavailableReason::Error, "Unable to refresh OrcaCloud credentials");
m_open.store(false);
return;
}
const std::string token = m_cloud->get_access_token();
const std::string host = host_without_scheme(m_cloud->get_cloud_service_host());
if (token.empty() || host.empty()) {
unavailable(CameraUnavailableReason::Error, "OrcaCloud session is unavailable");
m_open.store(false);
return;
}
const std::string live_token_url =
"https://" + host + "/api/v1/printers/" + encode_path_component(m_dev_id) + "/live-token";
BOOST_LOG_TRIVIAL(info) << "signaling: POST " << live_token_url << " (dev_id=" << m_dev_id << ")";
nlohmann::json token_response;
std::string token_body;
std::string token_error;
unsigned int http_code = 0;
auto request = Http::post(live_token_url);
request.set_post_body(std::string("{}"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.tls_verify(true)
.timeout_max(30)
.on_complete([&token_body, &http_code](std::string body, unsigned status) {
http_code = status;
token_body = std::move(body);
})
.on_error([&token_body, &token_error, &http_code](std::string body, std::string error, unsigned status) {
http_code = status;
token_body = std::move(body);
token_error = std::move(error);
})
.perform_sync();
BOOST_LOG_TRIVIAL(info) << "signaling: live-token HTTP " << http_code
<< (token_error.empty() ? "" : " error=" + token_error)
<< " body=" << token_body.substr(0, 512);
try {
token_response = nlohmann::json::parse(token_body);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: live-token body is not JSON: " << e.what();
}
if (http_code < 200 || http_code >= 300 || !token_response.contains("token")) {
unavailable(CameraUnavailableReason::Error, "Unable to mint camera live token");
m_open.store(false);
return;
}
std::vector<CameraIceServer> ice_servers;
if (token_response.contains("ice_servers") && token_response["ice_servers"].is_array()) {
for (const auto& entry : token_response["ice_servers"]) {
if (entry.is_string()) {
ice_servers.push_back({entry.get<std::string>(), {}, {}});
} else if (entry.is_object()) {
// RTCIceServer.urls is "string | string[]" (Cloudflare
// Realtime returns an array). Emit one CameraIceServer per
// URL, sharing the credentials.
const std::string username = entry.value("username", std::string{});
const std::string credential = entry.value("credential", std::string{});
const auto add_url = [&](const nlohmann::json& url) {
if (url.is_string() && !url.get<std::string>().empty())
ice_servers.push_back({url.get<std::string>(), username, credential});
};
const auto urls = entry.find("urls");
if (urls != entry.end()) {
if (urls->is_array()) {
for (const auto& url : *urls)
add_url(url);
} else {
add_url(*urls);
}
}
}
}
}
auto conn = std::make_shared<Connection>();
conn->ssl_context.set_default_verify_paths();
{
std::lock_guard<std::mutex> lock(m_mutex);
m_conn = conn;
}
auto& websocket = conn->websocket;
boost::asio::ip::tcp::resolver resolver(conn->io_context);
const auto endpoints = resolver.resolve(host, "443");
boost::asio::connect(boost::beast::get_lowest_layer(websocket), endpoints);
if (!SSL_set_tlsext_host_name(websocket.next_layer().native_handle(), host.c_str()))
throw std::runtime_error("Unable to configure TLS server name");
websocket.next_layer().set_verify_mode(boost::asio::ssl::verify_peer);
websocket.next_layer().handshake(boost::asio::ssl::stream_base::client);
const std::string ws_target = "/api/v1/printers/" + encode_path_component(m_dev_id) +
"/camera/live?token=" +
encode_path_component(token_response["token"].get<std::string>());
websocket.handshake(host, ws_target);
BOOST_LOG_TRIVIAL(info) << "signaling: websocket handshake ok (" << ice_servers.size()
<< " ice servers)";
if (on_ready)
on_ready(std::move(ice_servers));
send_json(nlohmann::json{{"type", "camera.mode"}, {"mode", "webrtc"}}.dump());
// Async read loop, driven by the connection's own io_context. run()
// returns once close() has shut the socket down, giving a bounded,
// deadlock-free teardown from any thread.
do_read(conn);
conn->io_context.run();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: run() exception: " << e.what();
if (!m_stop.load())
unavailable(CameraUnavailableReason::Closed, e.what());
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_conn.reset();
}
m_open.store(false);
}
void OrcaCloudSignalingChannel::do_read(std::shared_ptr<Connection> conn)
{
auto buffer = std::make_shared<boost::beast::flat_buffer>();
conn->websocket.async_read(
*buffer, [this, conn, buffer](boost::system::error_code ec, std::size_t) {
if (ec) {
if (!m_stop.load())
unavailable(CameraUnavailableReason::Closed, ec.message());
return; // do not re-arm; io_context.run() unwinds
}
const std::string raw = boost::beast::buffers_to_string(buffer->data());
// A malformed or unexpectedly-shaped message must not tear down the
// session: parse/dispatch is guarded.
try {
dispatch_message(nlohmann::json::parse(raw), raw);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "signaling: ignoring malformed message: " << e.what()
<< " raw=" << raw.substr(0, 256);
}
if (!m_stop.load())
do_read(conn);
});
}
// Returns the string at key, or "" if absent or not a string (JSON null included).
static std::string json_string(const nlohmann::json& object, const char* key)
{
const auto it = object.find(key);
return (it != object.end() && it->is_string()) ? it->get<std::string>() : std::string{};
}
void OrcaCloudSignalingChannel::dispatch_message(const nlohmann::json& message, const std::string& raw)
{
const std::string type = json_string(message, "type");
BOOST_LOG_TRIVIAL(info) << "signaling: recv type=" << type << " raw=" << raw.substr(0, 256);
if (type == "webrtc.answer") {
const std::string sdp = json_string(message, "sdp");
if (on_answer && !sdp.empty())
on_answer(sdp);
} else if (type == "webrtc.ice" && message.contains("candidate")) {
// The peer may send "candidate" as a flat string or as a nested
// RTCIceCandidateInit object { candidate, sdpMid, sdpMLineIndex }.
const nlohmann::json& candidate = message["candidate"];
std::string sdp_candidate;
std::string mid = json_string(message, "sdpMid");
if (candidate.is_string()) {
sdp_candidate = candidate.get<std::string>();
} else if (candidate.is_object()) {
sdp_candidate = json_string(candidate, "candidate");
std::string nested_mid = json_string(candidate, "sdpMid");
if (!nested_mid.empty())
mid = std::move(nested_mid);
}
if (on_ice && !sdp_candidate.empty())
on_ice(sdp_candidate, mid);
} else if (type == "webrtc.unavailable") {
const std::string reason = json_string(message, "reason");
unavailable(reason == "busy" ? CameraUnavailableReason::Busy
: reason == "disabled" ? CameraUnavailableReason::Disabled
: CameraUnavailableReason::Error,
reason.empty() ? "error" : reason);
}
}
void OrcaCloudSignalingChannel::send_json(const std::string& message)
{
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(m_mutex);
conn = m_conn;
}
if (!conn || m_stop.load())
return;
// Serialize the write onto the io_context thread (same thread that runs
// async_read), so reads and writes never touch the stream concurrently.
auto payload = std::make_shared<std::string>(message);
boost::asio::post(conn->io_context, [this, conn, payload] {
if (m_stop.load())
return;
boost::system::error_code ec;
conn->websocket.write(boost::asio::buffer(*payload), ec);
if (ec && !m_stop.load())
unavailable(CameraUnavailableReason::Closed, ec.message());
});
}
} // namespace Slic3r
@@ -0,0 +1,63 @@
#pragma once
#include "ICameraSignalingChannel.hpp"
#include "ICloudServiceAgent.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <nlohmann/json_fwd.hpp>
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
namespace Slic3r {
class OrcaCloudSignalingChannel : public ICameraSignalingChannel {
public:
OrcaCloudSignalingChannel(std::shared_ptr<ICloudServiceAgent> cloud, std::string dev_id);
~OrcaCloudSignalingChannel() override;
void open() override;
void close() override;
void send_offer(std::string sdp) override;
void send_ice(std::string candidate, std::string mid) override;
private:
using WebSocket = boost::beast::websocket::stream<
boost::beast::ssl_stream<boost::asio::ip::tcp::socket>>;
// The io_context and ssl_context must outlive the websocket stream that
// references them. Bundling them here with the stream declared last makes
// the destruction order correct (stream first, then contexts), and lets a
// single shared_ptr own the whole set.
struct Connection {
boost::asio::io_context io_context;
boost::asio::ssl::context ssl_context{boost::asio::ssl::context::tls_client};
WebSocket websocket{io_context, ssl_context};
};
void run();
void do_read(std::shared_ptr<Connection> conn);
void dispatch_message(const nlohmann::json& message, const std::string& raw);
void send_json(const std::string& message);
void unavailable(CameraUnavailableReason reason, std::string detail);
static std::string encode_path_component(const std::string& value);
static std::string host_without_scheme(std::string value);
std::shared_ptr<ICloudServiceAgent> m_cloud;
std::string m_dev_id;
std::atomic<bool> m_stop{false};
std::atomic<bool> m_open{false};
std::thread m_thread;
mutable std::mutex m_mutex;
std::shared_ptr<Connection> m_conn;
};
} // namespace Slic3r
+839
View File
@@ -0,0 +1,839 @@
#include "OrcaMqttConnection.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/log/trivial.hpp>
#include <openssl/ssl.h>
#include <algorithm>
#include <chrono>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <utility>
namespace Slic3r {
struct OrcaMqttConnection::Connection {
boost::asio::io_context io_context;
boost::asio::ssl::context ssl_context;
boost::asio::ip::tcp::resolver resolver;
// Exactly one of these is engaged once ws_handshake() has run: wss for
// wss:// endpoints, ws for plaintext ws://.
std::optional<TlsWebSocket> wss;
std::optional<PlainWebSocket> ws;
Connection()
: ssl_context(boost::asio::ssl::context::tls_client)
, resolver(io_context)
{}
};
namespace {
// Apply / clear a tcp_stream timeout on whichever websocket is engaged.
// Templated on the connection type only because Connection is a private nested
// type: a deduced parameter needs no (inaccessible) name for it.
template<class Conn> void expires_after(Conn& conn, std::chrono::seconds timeout) {
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_after(timeout);
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_after(timeout);
}
template<class Conn> void expires_never(Conn& conn) {
if (conn.wss) boost::beast::get_lowest_layer(*conn.wss).expires_never();
else if (conn.ws) boost::beast::get_lowest_layer(*conn.ws).expires_never();
}
} // namespace
OrcaMqttConnection::~OrcaMqttConnection() { stop(); }
bool OrcaMqttConnection::start(const Config& config, MessageHandler on_message, StateHandler on_state) {
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start url=" << config.url
<< " use_tls=" << config.use_tls
<< " bearer_provider=" << (config.bearer_provider ? "set" : "null")
<< " username_present=" << (!config.username.empty())
<< " password_present=" << (!config.password.empty())
<< " client_id=" << config.client_id
<< " keepalive_seconds=" << config.keepalive_seconds
<< " message_callback=" << (on_message ? "set" : "null")
<< " state_callback=" << (on_state ? "set" : "null");
stop();
{
std::lock_guard<std::mutex> lock(mutex);
current_config = config;
this->on_message = std::move(on_message);
this->on_state = std::move(on_state);
initial_result = false;
initial_completed = false;
connected = false;
m_last_connack_rc.store(-1);
}
stopping.store(false);
worker = std::thread(&OrcaMqttConnection::run, this);
std::unique_lock<std::mutex> lock(mutex);
if (!initial_cv.wait_for(lock, std::chrono::seconds(10), [this] { return initial_completed; })) {
initial_completed = true;
initial_result = false;
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT initial connection timed out after 10 seconds"
<< " url=" << current_config.url
<< " last_connack_rc=" << m_last_connack_rc.load()
<< " connected=" << connected.load()
<< "; worker will retry";
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT start initial_result=" << initial_result
<< " initial_completed=" << initial_completed
<< " last_connack_rc=" << m_last_connack_rc.load()
<< " worker_running=" << (worker.joinable() && !stopping.load());
return initial_result;
}
void OrcaMqttConnection::stop() {
std::lock_guard<std::recursive_mutex> lifecycle_lock(lifecycle_mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT stop requested"
<< " url=" << current_config.url
<< " connected=" << connected.load()
<< " worker_joinable=" << worker.joinable()
<< " last_connack_rc=" << m_last_connack_rc.load();
stopping.store(true);
state_cv.notify_all();
{
std::lock_guard<std::mutex> lock(connection_mutex);
if (active_connection) {
// Generic so it accepts either the TLS or the plaintext websocket.
auto shutdown_socket = [](auto& websocket) {
auto& socket = boost::beast::get_lowest_layer(websocket).socket();
boost::system::error_code socket_error;
socket.cancel(socket_error);
socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, socket_error);
socket.close(socket_error);
};
if (active_connection->wss)
shutdown_socket(*active_connection->wss);
else if (active_connection->ws)
shutdown_socket(*active_connection->ws);
active_connection->resolver.cancel();
}
}
if (worker.joinable())
worker.join();
{
std::lock_guard<std::mutex> lock(mutex);
connected = false;
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
if (!initial_completed) {
initial_completed = true;
initial_result = false;
}
}
initial_cv.notify_all();
}
bool OrcaMqttConnection::is_running() const {
return worker.joinable() && !stopping.load();
}
void OrcaMqttConnection::flush_subscription_change() {
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn = active_connection;
}
bool connacked;
{
std::lock_guard<std::mutex> lock(mutex);
connacked = connected;
}
if (!conn || !connacked)
return; // no live MQTT session yet — the worker sends the set on CONNACK
// beast permits a concurrent writer while the worker is blocked in
// websocket.read(); every write is serialised by write_mutex inside ws_write().
try {
send_pending_subscriptions(*conn);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: direct subscription write failed (" << e.what()
<< "); worker will resend the full set on reconnect";
}
}
bool OrcaMqttConnection::subscribe(const std::string& dev_id) {
if (dev_id.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected empty dev_id";
return false;
}
const std::string topic = report_topic(dev_id);
if (topic.size() > 96) { // MQTT topic filter cap enforced by the service
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT subscribe rejected oversized topic=" << topic;
return false;
}
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(topic) != 0 && pending_unsubscriptions.count(topic) == 0) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe already queued or active topic=" << topic
<< " acknowledged=" << (acknowledged_subscriptions.count(topic) != 0);
return true;
}
subscriptions.insert(topic);
pending_unsubscriptions.erase(topic);
pending_subscriptions.insert(topic);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT subscribe queued topic=" << topic
<< " total_subscriptions=" << subscriptions.size()
<< " connected=" << connected.load();
}
state_cv.notify_all();
flush_subscription_change(); // emit SUBSCRIBE now on the live socket (no reconnect)
return true;
}
bool OrcaMqttConnection::unsubscribe(const std::string& dev_id) {
const std::string topic = report_topic(dev_id);
{
std::lock_guard<std::mutex> lock(mutex);
subscriptions.erase(topic);
acknowledged_subscriptions.erase(topic);
pending_subscriptions.erase(topic);
pending_unsubscriptions.insert(topic);
for (auto it = pending_subscribe_packets.begin(); it != pending_subscribe_packets.end();) {
if (it->second == topic)
it = pending_subscribe_packets.erase(it);
else
++it;
}
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
if (it->first == dev_id)
it = pending_requests.erase(it);
else
++it;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT unsubscribe queued topic=" << topic
<< " total_subscriptions=" << subscriptions.size()
<< " connected=" << connected.load();
}
state_cv.notify_all();
flush_subscription_change(); // emit UNSUBSCRIBE now on the live socket (no reconnect)
return true;
}
void OrcaMqttConnection::clear_subscriptions() {
std::lock_guard<std::mutex> lock(mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT clear subscriptions count=" << subscriptions.size();
subscriptions.clear();
pending_subscriptions.clear();
pending_unsubscriptions.clear();
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
pending_requests.clear();
}
bool OrcaMqttConnection::parse_endpoint(const std::string& url, Endpoint& endpoint) {
std::string rest;
std::string default_port;
if (url.rfind("wss://", 0) == 0) { rest = url.substr(6); default_port = "443"; }
else if (url.rfind("ws://", 0) == 0) { rest = url.substr(5); default_port = "80"; }
else return false;
const auto slash = rest.find('/');
const std::string authority = rest.substr(0, slash);
endpoint.target = (slash == std::string::npos) ? "/" : rest.substr(slash);
// host[:port] — leave an unbracketed IPv6 literal alone
const auto colon = authority.rfind(':');
if (colon != std::string::npos && authority.find(']') == std::string::npos) {
endpoint.host = authority.substr(0, colon);
endpoint.port = authority.substr(colon + 1);
} else {
endpoint.host = authority;
endpoint.port = default_port;
}
return !endpoint.host.empty() && !endpoint.port.empty() && !endpoint.target.empty();
}
void OrcaMqttConnection::append_string(std::vector<uint8_t>& packet, const std::string& value) {
if (value.size() > 0xffff)
throw std::runtime_error("MQTT string is too long");
packet.push_back(static_cast<uint8_t>(value.size() >> 8));
packet.push_back(static_cast<uint8_t>(value.size() & 0xff));
packet.insert(packet.end(), value.begin(), value.end());
}
void OrcaMqttConnection::prepend_remaining_length(std::vector<uint8_t>& packet, size_t length) {
std::vector<uint8_t> encoded;
do {
uint8_t byte = static_cast<uint8_t>(length % 128);
length /= 128;
if (length != 0)
byte |= 0x80;
encoded.push_back(byte);
} while (length != 0);
packet.insert(packet.begin() + 1, encoded.begin(), encoded.end());
}
std::vector<uint8_t> OrcaMqttConnection::make_connect_packet(
const std::string& client_id, const std::string& username,
const std::string& password, int keepalive_seconds) {
std::vector<uint8_t> packet{0x10};
append_string(packet, "MQTT");
packet.push_back(4); // protocol level 3.1.1
uint8_t flags = 0x02; // clean session
if (!username.empty()) { flags |= 0x80; if (!password.empty()) flags |= 0x40; }
packet.push_back(flags);
packet.push_back(static_cast<uint8_t>(keepalive_seconds >> 8));
packet.push_back(static_cast<uint8_t>(keepalive_seconds & 0xff));
append_string(packet, client_id.empty() ? "OrcaSlicer" : client_id);
if (!username.empty()) {
append_string(packet, username);
if (!password.empty()) append_string(packet, password);
}
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::string OrcaMqttConnection::report_topic(const std::string& device_id) { return "device/" + device_id + "/report"; }
std::string OrcaMqttConnection::request_topic(const std::string& id) { return "device/" + id + "/request"; }
std::vector<uint8_t> OrcaMqttConnection::make_publish_packet(const std::string& topic, const std::string& payload) {
std::vector<uint8_t> packet{0x30}; // PUBLISH, QoS 0, no retain
append_string(packet, topic); // no packet id at QoS 0
packet.insert(packet.end(), payload.begin(), payload.end());
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::vector<uint8_t> OrcaMqttConnection::make_subscribe_packet(uint16_t id, const std::string& topic, uint8_t qos) {
std::vector<uint8_t> packet{0x82};
packet.push_back(id >> 8); packet.push_back(id & 0xff);
append_string(packet, topic);
packet.push_back(qos);
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::vector<uint8_t> OrcaMqttConnection::make_unsubscribe_packet(uint16_t id, const std::string& topic) {
std::vector<uint8_t> packet{0xA2};
packet.push_back(id >> 8); packet.push_back(id & 0xff);
append_string(packet, topic);
prepend_remaining_length(packet, packet.size() - 1);
return packet;
}
std::vector<uint8_t> OrcaMqttConnection::make_ping_packet() { return {0xc0, 0}; }
void OrcaMqttConnection::ws_write(Connection& conn, const std::vector<uint8_t>& packet) {
if (packet.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: attempted to send empty MQTT packet";
return;
}
// Writes come from the worker thread AND, for dynamic (un)subscribes, the
// caller thread. Serialise them; the worker's concurrent read is fine (beast
// allows one reader + one writer).
std::lock_guard<std::mutex> lock(write_mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending MQTT packet type=0x" << std::hex
<< static_cast<unsigned int>(packet[0] >> 4) << std::dec
<< " bytes=" << packet.size();
if (conn.wss) {
conn.wss->binary(true);
conn.wss->write(boost::asio::buffer(packet));
} else if (conn.ws) {
conn.ws->binary(true);
conn.ws->write(boost::asio::buffer(packet));
}
}
std::size_t OrcaMqttConnection::ws_read(Connection& conn, boost::beast::flat_buffer& buffer,
boost::system::error_code& ec) {
if (conn.wss)
return conn.wss->read(buffer, ec);
if (conn.ws)
return conn.ws->read(buffer, ec);
ec = boost::asio::error::not_connected;
return 0;
}
void OrcaMqttConnection::ws_close(Connection& conn) {
boost::system::error_code close_error;
if (conn.wss)
conn.wss->close(boost::beast::websocket::close_code::normal, close_error);
else if (conn.ws)
conn.ws->close(boost::beast::websocket::close_code::normal, close_error);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection closed code=" << close_error.value()
<< " message=" << close_error.message();
}
void OrcaMqttConnection::ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: WebSocket resolve starting host=" << endpoint.host
<< " port=" << endpoint.port << " target=" << endpoint.target
<< " tls=" << config.use_tls;
const auto results = conn.resolver.resolve(endpoint.host, endpoint.port);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT DNS resolution succeeded host=" << endpoint.host;
std::string token;
if (config.bearer_provider) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: requesting bearer token for WebSocket upgrade";
token = config.bearer_provider();
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: bearer token callback completed token_present=" << !token.empty();
}
auto decorator = [token](boost::beast::websocket::request_type& request) {
request.set(boost::beast::http::field::user_agent, "OrcaSlicer");
if (!token.empty())
request.set(boost::beast::http::field::authorization, "Bearer " + token);
request.set("Sec-WebSocket-Protocol", "mqtt");
};
boost::beast::http::response<boost::beast::http::string_body> response;
boost::system::error_code handshake_error;
if (config.use_tls) {
// stop() inspects the engaged optional under connection_mutex; publish it
// under the same lock, then release before the blocking connect.
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn.wss.emplace(conn.io_context, conn.ssl_context);
}
auto& websocket = *conn.wss;
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host
<< " port=" << endpoint.port;
// Set SNI before the TLS handshake so the cloud edge selects the correct
// certificate.
auto& tls_stream = websocket.next_layer();
if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), endpoint.host.c_str()))
throw std::runtime_error("failed to set Orca Cloud TLS server name");
conn.ssl_context.set_default_verify_paths();
tls_stream.set_verify_mode(boost::asio::ssl::verify_peer);
tls_stream.set_verify_callback(boost::asio::ssl::host_name_verification(endpoint.host));
tls_stream.handshake(boost::asio::ssl::stream_base::client);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TLS handshake completed host=" << endpoint.host;
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending TLS WebSocket upgrade target=" << endpoint.target
<< " bearer_header=" << (!token.empty());
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
} else {
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn.ws.emplace(conn.io_context);
}
auto& websocket = *conn.ws;
auto& stream = boost::beast::get_lowest_layer(websocket);
stream.expires_after(std::chrono::seconds(10));
stream.connect(results);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT TCP connection established host=" << endpoint.host
<< " port=" << endpoint.port << " (plaintext)";
websocket.set_option(boost::beast::websocket::stream_base::decorator(decorator));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending plaintext WebSocket upgrade target=" << endpoint.target
<< " bearer_header=" << (!token.empty());
websocket.handshake(response, endpoint.host, endpoint.target, handshake_error);
}
if (handshake_error) {
// Surface the server's HTTP status so a persistent rejection (stale token,
// missing api key, wrong route) is diagnosable from the log rather than an
// opaque "handshake declined".
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: WS handshake rejected http="
<< response.result_int() << " (" << response.reason() << "), "
<< handshake_error.message();
throw boost::system::system_error(handshake_error, "Orca WebSocket handshake");
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: WebSocket handshake completed http=" << response.result_int()
<< " negotiated_protocol=" << response["Sec-WebSocket-Protocol"];
if (response["Sec-WebSocket-Protocol"] != "mqtt") {
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: WebSocket handshake did not negotiate MQTT";
throw std::runtime_error("Orca WebSocket did not negotiate MQTT");
}
}
bool OrcaMqttConnection::send_request(const std::string& dev_id, const std::string& payload) {
if (dev_id.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected empty dev_id";
return false;
}
if (!connected.load()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected because connection is not ready"
<< " dev_id=" << dev_id << " last_connack_rc=" << m_last_connack_rc.load();
return false;
}
std::shared_ptr<Connection> conn;
{
std::lock_guard<std::mutex> lock(connection_mutex);
conn = active_connection;
}
if (!conn) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT send_request rejected because active connection is null"
<< " dev_id=" << dev_id;
return false;
}
const std::string report = report_topic(dev_id);
{
std::lock_guard<std::mutex> lock(mutex);
if (subscriptions.count(report) != 0 && acknowledged_subscriptions.count(report) == 0) {
pending_requests.emplace_back(dev_id, payload);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT request queued until SUBACK"
<< " dev_id=" << dev_id << " payload_bytes=" << payload.size()
<< " pending_requests=" << pending_requests.size();
return true;
}
}
try {
// ws_write() serialises the write via write_mutex; do not lock it here.
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PUBLISH request dev_id=" << dev_id
<< " topic=" << request_topic(dev_id)
<< " payload_bytes=" << payload.size();
ws_write(*conn, make_publish_packet(request_topic(dev_id), payload));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: send_request failed dev_id=" << dev_id
<< " (" << e.what() << ")";
return false;
}
return true;
}
void OrcaMqttConnection::connect_and_read() {
m_connection_stage = "creating connection";
auto connection = std::make_shared<Connection>();
{
std::lock_guard<std::mutex> lock(connection_mutex);
active_connection = connection;
if (stopping.load())
return;
}
m_connection_stage = "parsing endpoint";
Endpoint endpoint;
if (!parse_endpoint(current_config.url, endpoint)) {
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: invalid MQTT endpoint=" << current_config.url;
throw std::runtime_error("invalid Orca Cloud WebSocket endpoint");
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connecting host=" << endpoint.host
<< " port=" << endpoint.port << " target=" << endpoint.target;
m_connection_stage = "WebSocket handshake";
ws_handshake(*connection, current_config, endpoint);
m_connection_stage = "sending MQTT CONNECT";
expires_never(*connection);
// Auth precedence: a bearer_provider authenticates the WebSocket upgrade, so the
// CONNECT username/password fields are omitted entirely (the cloud form).
const bool use_bearer = static_cast<bool>(current_config.bearer_provider);
ws_write(*connection, make_connect_packet(current_config.client_id,
use_bearer ? std::string() : current_config.username,
use_bearer ? std::string() : current_config.password,
current_config.keepalive_seconds));
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNECT packet sent";
m_connection_stage = "waiting for MQTT CONNACK";
boost::beast::flat_buffer buffer;
expires_after(*connection, std::chrono::seconds(10));
boost::system::error_code connack_error;
ws_read(*connection, buffer, connack_error);
if (connack_error)
throw boost::system::system_error(connack_error, "read Orca MQTT CONNACK");
const std::string connack = boost::beast::buffers_to_string(buffer.data());
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT CONNACK received bytes=" << connack.size()
<< " header=" << (connack.empty() ? -1 : static_cast<int>(static_cast<uint8_t>(connack[0])))
<< " return_code=" << (connack.size() > 3 ? static_cast<int>(static_cast<uint8_t>(connack[3])) : -1);
// rc: 0 accepted, 1..5 refusal, -1 malformed/not a CONNACK.
const int rc = (connack.size() == 4 && static_cast<uint8_t>(connack[0]) == 0x20)
? static_cast<int>(static_cast<uint8_t>(connack[3]))
: -1;
m_last_connack_rc.store(rc);
if (rc != 0) {
BOOST_LOG_TRIVIAL(error) << "Orca diagnostic: MQTT CONNECT refused rc=" << rc;
if (rc == 4 || rc == 5) {
// Bad credentials / not authorized — retrying cannot help. Make run()'s
// loop exit and unblock any waiting start().
stopping.store(true);
{
std::lock_guard<std::mutex> lock(mutex);
initial_completed = true;
initial_result = false;
}
initial_cv.notify_all();
}
throw std::runtime_error("Orca MQTT CONNECT refused rc=" + std::to_string(rc));
}
m_connection_stage = "reading MQTT messages";
// The subscription acknowledgement belongs to this MQTT session. Clear
// the previous session's state before notifying the owner, because the
// reconnect callback immediately queues the printer's initial requests.
{
std::lock_guard<std::mutex> lock(mutex);
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
}
notify_state(true);
reconnect_delay_seconds.store(1); // a fresh CONNACK resets the backoff
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection is ready; sending current subscriptions";
send_current_subscriptions(*connection);
std::chrono::steady_clock::time_point next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
while (!stopping.load()) {
send_pending_subscriptions(*connection);
// Keepalive is driven every iteration, not only from the read-timeout branch:
// a printer pushing faster than the 1s read deadline would otherwise keep the
// read hot and the broker would drop us at 1.5 x keepalive.
if (std::chrono::steady_clock::now() >= next_ping) {
ws_write(*connection, make_ping_packet());
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT PINGREQ sent";
next_ping = std::chrono::steady_clock::now() + std::chrono::seconds(30);
}
buffer.consume(buffer.size());
m_connection_stage = "reading MQTT frame";
expires_after(*connection, std::chrono::seconds(1));
boost::system::error_code error;
ws_read(*connection, buffer, error);
if (error == boost::beast::error::timeout)
continue;
if (error) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT WebSocket read failed code=" << error.value()
<< " message=" << error.message();
throw boost::system::system_error(error, "read Orca MQTT message");
}
handle_packet(boost::beast::buffers_to_string(buffer.data()));
}
ws_close(*connection);
if (!stopping.load())
notify_state(false);
}
void OrcaMqttConnection::send_current_subscriptions(Connection& conn) {
std::vector<std::string> topics;
{
std::lock_guard<std::mutex> lock(mutex);
topics.assign(subscriptions.begin(), subscriptions.end());
acknowledged_subscriptions.clear();
pending_subscribe_packets.clear();
for (const std::string& topic : topics)
pending_subscriptions.erase(topic);
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending current MQTT subscriptions count=" << topics.size();
for (const std::string& topic : topics) {
const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending SUBSCRIBE topic=" << topic << " packet_id=" << packet_id;
ws_write(conn, make_subscribe_packet(packet_id, topic, 1));
}
}
void OrcaMqttConnection::send_pending_subscriptions(Connection& conn) {
std::vector<std::string> subscribe_topics;
std::vector<std::string> unsubscribe_topics;
{
std::lock_guard<std::mutex> lock(mutex);
subscribe_topics.assign(pending_subscriptions.begin(), pending_subscriptions.end());
unsubscribe_topics.assign(pending_unsubscriptions.begin(), pending_unsubscriptions.end());
pending_subscriptions.clear();
pending_unsubscriptions.clear();
}
for (const std::string& topic : subscribe_topics) {
const uint16_t packet_id = next_packet_id++;
{
std::lock_guard<std::mutex> lock(mutex);
pending_subscribe_packets[packet_id] = topic;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending SUBSCRIBE topic=" << topic
<< " packet_id=" << packet_id;
ws_write(conn, make_subscribe_packet(packet_id, topic, 1));
}
for (const std::string& topic : unsubscribe_topics) {
const uint16_t packet_id = next_packet_id++;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: sending pending UNSUBSCRIBE topic=" << topic
<< " packet_id=" << packet_id;
ws_write(conn, make_unsubscribe_packet(packet_id, topic));
}
}
void OrcaMqttConnection::handle_packet(const std::string& packet) {
if (packet.size() < 2) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: received undersized MQTT packet bytes=" << packet.size();
return;
}
const uint8_t header = static_cast<uint8_t>(packet[0]);
const uint8_t packet_type = header >> 4;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received MQTT packet type=" << static_cast<unsigned int>(packet_type)
<< " header=0x" << std::hex << static_cast<unsigned int>(header) << std::dec
<< " bytes=" << packet.size();
if (packet_type != 3) { // Only QoS 0 PUBLISH carries printer status.
if (packet_type == 9 && packet.size() >= 5) {
const uint16_t packet_id = (static_cast<unsigned int>(static_cast<uint8_t>(packet[2])) << 8) |
static_cast<unsigned int>(static_cast<uint8_t>(packet[3]));
std::ostringstream result_codes;
for (size_t index = 4; index < packet.size(); ++index) {
if (index != 4)
result_codes << ',';
result_codes << "0x" << std::hex << static_cast<unsigned int>(static_cast<uint8_t>(packet[index]));
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received SUBACK packet_id="
<< packet_id
<< " result_codes=" << result_codes.str();
// Each production SUBSCRIBE packet currently contains one topic.
// MQTT grants QoS 0 or 1 for a requested QoS 1 subscription; 0x80
// means the subscription was rejected.
const uint8_t result = static_cast<uint8_t>(packet[4]);
std::string topic;
std::deque<std::pair<std::string, std::string>> requests;
{
std::lock_guard<std::mutex> lock(mutex);
auto pending = pending_subscribe_packets.find(packet_id);
if (pending != pending_subscribe_packets.end()) {
topic = pending->second;
pending_subscribe_packets.erase(pending);
for (auto it = pending_requests.begin(); it != pending_requests.end();) {
if (report_topic(it->first) == topic) {
requests.push_back(std::move(*it));
it = pending_requests.erase(it);
} else {
++it;
}
}
if (result == 0 || result == 1) {
acknowledged_subscriptions.insert(topic);
}
}
}
if (topic.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: SUBACK has no pending topic packet_id=" << packet_id;
} else if (result == 0 || result == 1) {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: report subscription active topic=" << topic
<< " granted_qos=" << static_cast<unsigned int>(result)
<< " releasing_requests=" << requests.size();
for (const auto& request : requests) {
if (!send_request(request.first, request.second)) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: queued MQTT request could not be sent"
<< " after SUBACK dev_id=" << request.first;
}
}
} else {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: report subscription rejected topic=" << topic
<< " result_code=0x" << std::hex << static_cast<unsigned int>(result) << std::dec
<< " dropped_requests=" << requests.size();
}
}
return;
}
size_t index = 1;
size_t multiplier = 1;
size_t remaining = 0;
uint8_t encoded = 0;
do {
if (index >= packet.size() || multiplier > 128 * 128 * 128) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH remaining length";
return;
}
encoded = static_cast<uint8_t>(packet[index++]);
remaining += (encoded & 0x7f) * multiplier;
multiplier *= 128;
} while ((encoded & 0x80) != 0);
const size_t remaining_end = index + remaining;
if (remaining_end > packet.size() || remaining < 2 || index + 2 > remaining_end) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH body remaining=" << remaining
<< " packet_bytes=" << packet.size();
return;
}
const uint16_t topic_length = (static_cast<uint8_t>(packet[index]) << 8) |
static_cast<uint8_t>(packet[index + 1]);
index += 2;
if (topic_length > packet.size() - index) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH topic length=" << topic_length;
return;
}
const std::string topic(packet.data() + index, topic_length);
index += topic_length;
if (((header >> 1) & 0x03) != 0) {
if (index + 2 > remaining_end) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: malformed MQTT PUBLISH packet identifier";
return;
}
index += 2; // QoS 1/2 packet identifier; the service currently sends QoS 0.
}
const size_t payload_size = remaining_end - index;
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: received PUBLISH topic=" << topic
<< " payload_bytes=" << payload_size
<< " message_callback=" << (on_message ? "set" : "null");
// topic is "device/<id>/report" (or "/request"); hand the id up, drop anything else.
std::string dev_id;
if (topic.rfind("device/", 0) == 0) {
const size_t id_start = 7;
const size_t id_end = topic.rfind('/');
if (id_end != std::string::npos && id_end > id_start)
dev_id = topic.substr(id_start, id_end - id_start);
}
if (dev_id.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH on unrecognized topic=" << topic;
} else if (on_message) {
on_message(dev_id, packet.substr(index, remaining_end - index));
} else {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: dropping PUBLISH because message callback is not set";
}
}
void OrcaMqttConnection::notify_state(bool is_now_connected) {
StateHandler callback;
bool initial = false;
{
std::lock_guard<std::mutex> lock(mutex);
connected = is_now_connected;
initial = !initial_completed;
if (initial) {
initial_result = is_now_connected;
initial_completed = true;
}
callback = on_state;
}
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT state changed connected=" << is_now_connected
<< " initial=" << initial << " state_callback=" << (callback ? "set" : "null");
if (initial)
initial_cv.notify_all();
else if (callback)
callback(is_now_connected, false);
}
void OrcaMqttConnection::run() {
while (!stopping.load()) {
const int retry_seconds = reconnect_delay_seconds.load();
const uint64_t attempt = ++m_attempt_number;
m_connection_stage = "starting attempt";
try {
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT connection attempt=" << attempt
<< " retry_delay=" << retry_seconds
<< " url=" << current_config.url;
connect_and_read();
} catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(warning) << "Orca diagnostic: MQTT connection attempt=" << attempt
<< " failed stage=" << m_connection_stage
<< " error=" << error.what()
<< " last_connack_rc=" << m_last_connack_rc.load()
<< " stopping=" << stopping.load();
if (!stopping.load())
notify_state(false);
}
if (stopping.load())
break;
// Grow the backoff only across attempts that never reached CONNACK; a
// successful connection resets reconnect_delay_seconds to 1 (connect_and_read).
reconnect_delay_seconds.store(std::min(retry_seconds * 2, 30));
std::unique_lock<std::mutex> lock(mutex);
BOOST_LOG_TRIVIAL(info) << "Orca diagnostic: MQTT waiting before reconnect seconds=" << retry_seconds;
state_cv.wait_for(lock, std::chrono::seconds(retry_seconds), [this] { return stopping.load(); });
}
}
} // namespace Slic3r
+152
View File
@@ -0,0 +1,152 @@
#ifndef slic3r_OrcaMqttConnection_hpp_
#define slic3r_OrcaMqttConnection_hpp_
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <cstddef>
#include <cstdint>
namespace Slic3r {
// Minimal MQTT 3.1.1 codec + WebSocket transport (ws:// and wss://), shared by the
// LAN (OrcaSonar) and cloud (fleet) printer connections. Both PUBLISH
// commands to device/<id>/request and SUBSCRIBE device/<id>/report; Config is the
// only per-transport difference.
class OrcaMqttConnection
{
public:
using TokenProvider = std::function<std::string()>;
using MessageHandler = std::function<void(const std::string&, const std::string&)>;
using StateHandler = std::function<void(bool connected, bool initial)>;
struct Endpoint { std::string host; std::string port; std::string target; };
struct Config {
std::string url;
bool use_tls = false;
TokenProvider bearer_provider; // set => bearer on WS upgrade, CONNECT creds omitted
std::string username;
std::string password;
std::string client_id = "OrcaSlicer";
int keepalive_seconds = 60;
};
static bool parse_endpoint(const std::string& url, Endpoint& endpoint);
// Build an MQTT 3.1.1 CONNECT packet. Clean-session is always set; the
// username/password connect flags and payload fields are added only when
// username is non-empty (the cloud form authenticates via a bearer on the
// WebSocket upgrade and omits CONNECT credentials). Public for unit tests.
static std::vector<uint8_t> make_connect_packet(const std::string& client_id,
const std::string& username,
const std::string& password,
int keepalive_seconds);
// Topic-string helpers for the per-device request/report channels and the
// MQTT 3.1.1 PUBLISH / SUBSCRIBE / UNSUBSCRIBE packet builders. All public
// for unit tests. make_publish_packet emits QoS 0 (no packet identifier).
static std::string request_topic(const std::string& dev_id); // "device/<id>/request"
static std::string report_topic(const std::string& dev_id); // "device/<id>/report"
static std::vector<uint8_t> make_publish_packet(const std::string& topic, const std::string& payload);
static std::vector<uint8_t> make_subscribe_packet(uint16_t packet_id, const std::string& topic, uint8_t qos);
static std::vector<uint8_t> make_unsubscribe_packet(uint16_t packet_id, const std::string& topic);
~OrcaMqttConnection();
bool start(const Config& config, MessageHandler on_message, StateHandler on_state);
void stop();
// True while the worker thread is alive (connected OR retrying). Lets callers
// avoid restarting a healthy connection.
bool is_running() const;
// True once CONNACK has been received and the socket has not since dropped.
bool is_connected() const { return connected.load(); }
bool subscribe(const std::string& dev_id);
bool unsubscribe(const std::string& dev_id);
// Last MQTT CONNACK return code: 0 ok, 1..5 refusal, -1 none seen this attempt.
int last_connack_rc() const { return m_last_connack_rc.load(); }
void clear_subscriptions();
bool send_request(const std::string& dev_id, const std::string& payload);
private:
// The endpoint may be either a TLS (wss://) or a plaintext (ws://) WebSocket;
// Connection holds whichever one is engaged and the ws_* helpers below
// dispatch on it.
using TlsWebSocket = boost::beast::websocket::stream<
boost::asio::ssl::stream<boost::beast::tcp_stream>>;
using PlainWebSocket = boost::beast::websocket::stream<boost::beast::tcp_stream>;
struct Connection;
static void append_string(std::vector<uint8_t>& packet, const std::string& value);
static void prepend_remaining_length(std::vector<uint8_t>& packet, size_t length);
static std::vector<uint8_t> make_ping_packet();
// Transport dispatch: each forwards to conn.wss (TLS) or conn.ws (plaintext).
void ws_write(Connection& conn, const std::vector<uint8_t>& packet); // locks write_mutex
std::size_t ws_read(Connection& conn, boost::beast::flat_buffer& buffer, boost::system::error_code& ec);
void ws_handshake(Connection& conn, const Config& config, const Endpoint& endpoint);
void ws_close(Connection& conn);
// Emit a queued SUBSCRIBE/UNSUBSCRIBE on the live socket right now (from the
// caller thread), so a selection change is applied without waiting for the
// blocking read loop to next return. No-op if no CONNACKed socket exists yet
// (the worker sends the set on connect). The WebSocket is never dropped for a
// subscription change.
void flush_subscription_change();
void connect_and_read();
void send_current_subscriptions(Connection& conn);
void send_pending_subscriptions(Connection& conn);
void handle_packet(const std::string& packet);
void notify_state(bool is_now_connected);
void run();
std::atomic_bool stopping{true};
std::atomic_int reconnect_delay_seconds{1};
// Serialises the whole of start() and stop() against each other, so the UI
// thread's stop() (disconnect / dtor) cannot race the connect thread's start()
// into a concurrent worker.join(). Recursive because start() calls stop().
std::recursive_mutex lifecycle_mutex;
std::thread worker;
std::mutex mutex;
std::mutex connection_mutex;
std::mutex write_mutex; // serialises every websocket write (worker + caller threads)
std::shared_ptr<Connection> active_connection;
std::condition_variable initial_cv;
std::condition_variable state_cv;
Config current_config;
MessageHandler on_message;
StateHandler on_state;
// Full report-topic strings ("device/<id>/report"), not bare device ids.
std::set<std::string> subscriptions;
std::set<std::string> pending_subscriptions;
std::set<std::string> pending_unsubscriptions;
// Requests for a subscribed device wait until the corresponding SUBACK is
// received. Otherwise an immediate pushall response can be published by
// the broker before this client is actually subscribed to the report topic.
std::set<std::string> acknowledged_subscriptions;
std::map<uint16_t, std::string> pending_subscribe_packets;
std::deque<std::pair<std::string, std::string>> pending_requests;
std::atomic<uint16_t> next_packet_id{1};
std::atomic<int> m_last_connack_rc{-1};
uint64_t m_attempt_number{0}; // worker-thread diagnostic sequence
std::string m_connection_stage; // worker-thread diagnostic stage
bool initial_result{false};
bool initial_completed{false};
std::atomic_bool connected{false};
};
} // namespace Slic3r
#endif // slic3r_OrcaMqttConnection_hpp_
File diff suppressed because it is too large Load Diff
+141 -5
View File
@@ -3,19 +3,28 @@
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "ICloudServiceAgent.hpp" #include "ICloudServiceAgent.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "OrcaMqttConnection.hpp"
#include <atomic>
#include <cstdint>
#include <functional>
#include <string> #include <string>
#include <mutex> #include <mutex>
#include <memory> #include <memory>
#include <thread>
namespace Slic3r { namespace Slic3r {
class OrcaCloudServiceAgent;
/** /**
* OrcaPrinterAgent - Stub implementation for printer operations. * OrcaPrinterAgent - OrcaSonar MQTT printer agent.
* *
* All printer-related operations are currently stubs that return success. * LAN and cloud commands use the same OrcaSonar protocol payloads; only the
* Actual printer connectivity requires the BBL SDK or future Orca implementation. * MQTT connection selected by route_send() differs.
*/ */
class OrcaPrinterAgent : public IPrinterAgent { class OrcaPrinterAgent : public IPrinterAgent
{
public: public:
explicit OrcaPrinterAgent(std::string log_dir); explicit OrcaPrinterAgent(std::string log_dir);
~OrcaPrinterAgent() override; ~OrcaPrinterAgent() override;
@@ -25,6 +34,10 @@ public:
// ======================================================================== // ========================================================================
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override; void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) override;
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
std::unique_ptr<ICameraSignalingChannel>
create_camera_signaling_channel(const std::string& dev_id) override;
// Communication // Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override; int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
@@ -42,7 +55,13 @@ public:
// Binding // Binding
int ping_bind(std::string ping_code) override; int ping_bind(std::string ping_code) override;
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override; int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override;
int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override; int 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 unbind(std::string dev_id) override;
int request_bind_ticket(std::string* ticket) 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 get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override;
@@ -77,10 +96,127 @@ public:
int set_on_local_message_fn(OnMessageFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override;
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_start_camera(std::string dev_id) override;
int command_xyz_abs(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_auto_leveling(std::string dev_id, int sequence_id, bool lan_mode) override;
int command_go_home(std::string dev_id, bool is_printing, bool supports_mqtt_homing, int sequence_id, bool lan_mode) override;
int command_set_bed(std::string dev_id, int temp, bool supports_mqtt_bed_ctrl, int sequence_id, bool lan_mode) override;
int command_set_nozzle(std::string dev_id, int temp, int sequence_id, bool lan_mode) override;
int command_axis_control(std::string dev_id,
std::string axis,
double unit,
double input_val,
int speed,
bool is_core_xy,
bool supports_mqtt_axis_control,
int sequence_id,
bool lan_mode) override;
// Test-only: drive emit_connect_sequence directly (no socket).
void run_connect_sequence_for_test(const std::string& dev_id)
{
emit_connect_sequence(dev_id, [](const std::string&) {}, [](const std::string&) {});
}
// Test-only: advance the LAN connection epoch without a connect/disconnect cycle.
void bump_lan_generation_for_test() { ++m_lan_generation; }
// Test-only: the same for the (independent) cloud selection epoch.
void bump_cloud_generation_for_test() { ++m_cloud_generation; }
protected:
// Forward one inbound printer message to on_message_fn or on_local_message_fn (marshalled onto the UI
// thread via queue_on_main_fn when set). Body of every connection's MessageHandler.
void deliver_to_sink(const std::string& dev_id, const std::string& payload, bool local);
// Extract OrcaSonar's print.ipcam.stream_mode from LAN reports before they
// are forwarded to the GUI. The getters below then read this agent-owned state.
void parse_ipcam_info(const std::string& dev_id, const std::string& payload);
// Orca-dialect -> Bambu-dialect compatibility shim for inbound reports: the single
// place Orca Protocol JSON is rewritten into the shapes MachineObject::parse_json
// already handles, so parse_json needs no Orca-specific changes. Self-contained
// (its cache is a function-local static) and deletable together with its call site
// once parse_json reads the Orca dialect natively. See the definition for the
// per-rule detail. Returns the payload unchanged when no rule applies.
std::string merge_capabilities(const std::string& dev_id, const std::string& payload);
// Report the asynchronous LAN connection state using the same callback contract as
// the other printer agents. The transport result cannot be returned by
// connect_printer(), which only starts the worker.
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& message);
// The LAN inbound-message handler for one connection generation: forwards to
// deliver_to_sink only while `generation` is still the live epoch.
std::function<void(const std::string&, const std::string&)> make_lan_message_handler(uint64_t generation);
// Pure LAN-address parsing + client-id. protected static so the test Probe reaches them.
static bool parse_lan_endpoint(const std::string& dev_ip, std::string& host, std::string& port);
static std::string make_lan_client_id(const std::string& dev_id);
// Test hook: the ws:// URL connect_printer built for the current LAN session ("" if none).
std::string lan_connection_target() const;
// Shared post-connect sequence: SUBSCRIBE, then pushing.start, pushall,
// info.get_version, info.get_capabilities. Runs identically on LAN and cloud.
void on_connected(const std::string& dev_id, OrcaMqttConnection* conn, uint64_t generation);
// The post-connect command sequence, factored behind a seam so a test can
// observe the SUBSCRIBE + 4 request payloads without a live OrcaMqttConnection.
virtual void emit_connect_sequence(const std::string& dev_id,
std::function<void(const std::string&)> subscribe,
std::function<void(const std::string&)> request);
static std::string seq(int n); // decimal string in the OrcaSlicer 20000..29999 band
static std::string build_pushing_start(const std::string& sequence_id);
static std::string build_pushing_stop(const std::string& sequence_id);
static std::string build_pushall(const std::string& sequence_id);
static std::string build_get_version(const std::string& sequence_id);
static std::string build_get_capabilities(const std::string& sequence_id);
private: private:
class OrcaSonarDiscovery;
std::string log_dir; std::string log_dir;
std::string selected_machine; std::string selected_machine;
enum CurrentConn { NONE, CLOUD, LAN };
static const char* connection_type_name(CurrentConn connection);
// The transport for the printer currently selected by the UI. LAN and
// cloud sessions have separate connection objects, so this is selection
// state rather than an inference from whichever socket happens to exist.
CurrentConn m_current_connection = NONE;
std::shared_ptr<ICloudServiceAgent> m_cloud_agent; std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
std::unique_ptr<OrcaMqttConnection> lan_mqtt_connection;
// Two independent epochs: a cloud (de)selection must not fence the live LAN
// feed, and vice versa. Each transport's connect thread and inbound handler
// compare against their own counter only.
std::atomic<uint64_t> m_lan_generation{0};
std::atomic<uint64_t> m_cloud_generation{0};
// The short-lived threads that run the blocking initial connect for the current
// LAN / cloud session. Joined members (never detached) so they cannot outlive
// *this or the connection they hold a raw pointer to.
std::thread m_lan_connect_thread;
std::thread m_cloud_connect_thread;
std::unique_ptr<OrcaSonarDiscovery> m_discovery;
std::string m_lan_dev_id; // guarded by state_mutex
std::string m_lan_url; // guarded by state_mutex — the Config.url of the live LAN session
CameraStreamMode m_camera_stream_mode = CameraStreamMode::none; // guarded by state_mutex
std::string m_camera_url; // guarded by state_mutex
OrcaCloudServiceAgent* get_orca_cloud_agent();
OrcaMqttConnection* get_appropriate_mqtt_connection(bool is_lan = true);
static bool parse_nonnegative_command_id(const std::string& value, int& result);
// Route one command payload to device/<dev_id>/request on the LAN or the shared
// cloud connection. The uniform send path for both send_message* overrides.
int route_send(bool is_lan, const std::string& dev_id, const std::string& json_str);
// Callbacks // Callbacks
OnMsgArrivedFn on_ssdp_msg_fn; OnMsgArrivedFn on_ssdp_msg_fn;
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <string>
namespace Slic3r {
enum LiveviewLocal {
LVL_None,
LVL_Disable,
LVL_Local,
LVL_Rtsps,
LVL_Rtsp
};
enum LiveviewRemote {
LVR_None,
LVR_Tutk,
LVR_Agora,
LVR_TutkAgora
};
enum FileLocal {
FL_None,
FL_Local
};
enum FileRemote {
FR_None,
FR_Tutk,
FR_Agora,
FR_TutkAgora
};
} // namespace Slic3r
+144 -19
View File
@@ -1,5 +1,6 @@
#include "QidiPrinterAgent.hpp" #include "QidiPrinterAgent.hpp"
#include "Http.hpp" #include "Http.hpp"
#include "IPrinterAgent.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
@@ -8,6 +9,7 @@
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <cctype> #include <cctype>
#include <sstream> #include <sstream>
#include <thread>
using json = nlohmann::json; using json = nlohmann::json;
@@ -27,6 +29,15 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin
return false; return false;
} }
// RAII decrement for MoonrakerPrinterAgent::filament_fetch_in_flight — guarantees the
// counter drops back down on every exit path (early return or fall-through) inside the
// detached fetch thread below, so ~MoonrakerPrinterAgent()'s wait loop can't stall forever.
struct InFlightGuard
{
std::atomic<int>& counter;
~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); }
};
} // anonymous namespace } // anonymous namespace
const std::string QidiPrinterAgent_VERSION = "0.0.1"; const std::string QidiPrinterAgent_VERSION = "0.0.1";
@@ -40,42 +51,141 @@ AgentInfo QidiPrinterAgent::get_agent_info_static()
return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"}; return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"};
} }
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id) FilamentSyncMode QidiPrinterAgent::get_filament_sync_mode() const
{ {
if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
return FilamentSyncMode::subscription;
return FilamentSyncMode::pull;
}
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{
if (sync_mode != get_filament_sync_mode())
return false;
// Snapshot only what the fetch needs, rather than reading device_info live from the
// background thread below — device_info can be concurrently rewritten by a reconnect
// on another thread while this fetch is still in flight.
std::string base_url = device_info.base_url;
std::string api_key = device_info.api_key;
std::string model_id = device_info.model_id;
std::string model_name = device_info.model_name;
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
std::thread([this, base_url, api_key, model_id, model_name]() {
InFlightGuard guard{filament_fetch_in_flight};
std::string error; std::string error;
// 1. Fetch device info and infer series_id // 1. Fetch device info and infer series_id
std::string series_id; std::string series_id;
{ {
MoonrakerDeviceInfo info; MoonrakerDeviceInfo info;
if (fetch_device_info(device_info.base_url, device_info.api_key, info, error)) { if (fetch_device_info(base_url, api_key, info, error)) {
series_id = infer_series_id(info.model_id, info.dev_name); series_id = infer_series_id(info.model_id, info.dev_name);
} }
} }
if (series_id.empty()) { if (series_id.empty()) {
// Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier. // Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier.
series_id = infer_series_id(device_info.model_id, device_info.model_name); series_id = infer_series_id(model_id, model_name);
} }
// 2. Fetch filament dictionary // 2. Fetch filament dictionary
QidiFilamentDict dict; QidiFilamentDict dict;
if (!fetch_filament_dict(device_info.base_url, device_info.api_key, dict, error)) { if (!fetch_filament_dict(base_url, api_key, dict, error)) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error; BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error;
} }
// 3. Fetch slot info and build AmsTrayData directly // 3. Fetch slot info and build AmsTrayData directly
std::vector<AmsTrayData> trays; std::vector<AmsTrayData> trays;
int box_count = 0; int box_count = 0;
if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) { if (!fetch_slot_info(base_url, api_key, dict, series_id, trays, box_count, error)) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error; BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error;
return false; return;
} }
// 4. Build the AMS payload // 4. Build the AMS payload
build_ams_payload(box_count, box_count * 4 - 1, trays); build_ams_payload(box_count, box_count * 4 - 1, trays);
}).detach();
return true; return true;
} }
bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const
{
// enable_box mirrors task_use_ams: engage the multi-color box only when this
// job actually routes filament through it. (See qidi-ams-findings.md §2/§8.3 —
// if firmware treats enable_box as "a box exists" rather than "use it this job",
// switch this gate to HasAms()/box_count instead.)
const int enable = params.task_use_ams ? 1 : 0;
if (!send_gcode(device_info.dev_id, "SAVE_VARIABLE VARIABLE=enable_box VALUE=" + std::to_string(enable))) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set enable_box";
return false;
}
// When the box isn't used this job, leave the existing value_t<tool> slot
// assignments untouched (enable_box=0 is enough to disengage it).
if (!enable)
return true;
if (params.ams_mapping.empty()) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::apply_box_mapping: enable_box set but ams_mapping is empty";
return true;
}
// ams_mapping (v0) is a JSON array indexed by filament/tool; each value is the
// physical box slot (-1 = unmapped). Mirror it onto the printer's value_t<tool>
// variables: SAVE_VARIABLE VARIABLE=value_t<tool> VALUE='slot<n>'.
auto mapping = nlohmann::json::parse(params.ams_mapping, nullptr, /*allow_exceptions*/ false);
if (mapping.is_discarded() || !mapping.is_array()) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: invalid ams_mapping: " << params.ams_mapping;
return false;
}
for (size_t tool = 0; tool < mapping.size(); ++tool) {
if (!mapping[tool].is_number_integer())
continue;
const int slot = mapping[tool].get<int>();
if (slot < 0)
continue; // unmapped filament — skip
const std::string gcode = "SAVE_VARIABLE VARIABLE=value_t" + std::to_string(tool) +
" VALUE=\"'slot" + std::to_string(slot) + "'\"";
if (!send_gcode(device_info.dev_id, gcode)) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set value_t" << tool;
return false;
}
}
return true;
}
int QidiPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
{
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
return MoonrakerPrinterAgent::start_local_print(std::move(params), update_fn, cancel_fn);
}
int QidiPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
{
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
return MoonrakerPrinterAgent::start_print(std::move(params), update_fn, cancel_fn, wait_fn);
}
int QidiPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
{
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_WR_UPLOAD_FTP_FAILED;
return MoonrakerPrinterAgent::start_local_print_with_record(std::move(params), update_fn, cancel_fn, wait_fn);
}
int QidiPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
{
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
return MoonrakerPrinterAgent::start_sdcard_print(std::move(params), update_fn, cancel_fn);
}
bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
const std::string& api_key, const std::string& api_key,
const QidiFilamentDict& dict, const QidiFilamentDict& dict,
@@ -120,20 +230,10 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
return false; return false;
} }
auto json = nlohmann::json::parse(response_body, nullptr, false, true); nlohmann::json status;
if (json.is_discarded()) { nlohmann::json variables;
error = "Invalid JSON response"; if (!parse_slot_response(response_body, status, variables, error))
return false; return false;
}
if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("save_variables") ||
!json["result"]["status"]["save_variables"].contains("variables")) {
error = "Unexpected JSON structure";
return false;
}
auto& variables = json["result"]["status"]["save_variables"]["variables"];
auto& status = json["result"]["status"];
box_count = variables.value("box_count", 1); box_count = variables.value("box_count", 1);
if (box_count < 0) { if (box_count < 0) {
@@ -208,6 +308,31 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
return true; return true;
} }
bool QidiPrinterAgent::parse_slot_response(const std::string& response_body,
nlohmann::json& status,
nlohmann::json& variables,
std::string& error)
{
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
if (json.is_discarded()) {
error = "Invalid JSON response";
return false;
}
if (!json.is_object() || !json.contains("result") || !json["result"].is_object() || !json["result"].contains("status") ||
!json["result"]["status"].is_object() || !json["result"]["status"].contains("save_variables") ||
!json["result"]["status"]["save_variables"].is_object() || !json["result"]["status"]["save_variables"].contains("variables") ||
!json["result"]["status"]["save_variables"]["variables"].is_object()) {
// why: Qidi firmware may send null here, but json::value() throws for it.
error = "Unexpected JSON structure: save_variables.variables must be an object";
return false;
}
status = json["result"]["status"];
variables = status["save_variables"]["variables"];
return true;
}
bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url,
const std::string& api_key, const std::string& api_key,
QidiFilamentDict& dict, QidiFilamentDict& dict,
+19 -1
View File
@@ -1,7 +1,9 @@
#ifndef __QIDI_PRINTER_AGENT_HPP__ #ifndef __QIDI_PRINTER_AGENT_HPP__
#define __QIDI_PRINTER_AGENT_HPP__ #define __QIDI_PRINTER_AGENT_HPP__
#include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include "nlohmann/json_fwd.hpp"
#include <map> #include <map>
#include <string> #include <string>
@@ -19,9 +21,25 @@ public:
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
// Override filament sync (Qidi-specific implementation) // Override filament sync (Qidi-specific implementation)
bool fetch_filament_info(std::string dev_id) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
static bool parse_slot_response(const std::string& response_body,
nlohmann::json& status,
nlohmann::json& variables,
std::string& error);
// Print operations — emit QiDi multi-color box config, then delegate to base.
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
FilamentSyncMode get_filament_sync_mode() const override;
private: private:
// Push enable_box + value_t<tool> SAVE_VARIABLEs before a print starts.
// Returns false if any command fails (caller should abort the print).
bool apply_box_mapping(const PrintParams& params) const;
struct QidiFilamentDict struct QidiFilamentDict
{ {
std::map<int, std::string> colors; std::map<int, std::string> colors;
+74 -16
View File
@@ -1,10 +1,14 @@
#include "SnapmakerPrinterAgent.hpp" #include "SnapmakerPrinterAgent.hpp"
#include "Http.hpp" #include "Http.hpp"
#include "IPrinterAgent.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <chrono>
#include <sstream>
#include <thread>
using json = nlohmann::json; using json = nlohmann::json;
@@ -13,6 +17,13 @@ namespace Slic3r {
namespace { namespace {
constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1"; constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1";
constexpr int64_t CAMERA_REFRESH_INTERVAL_MS = 300'000;
int64_t now_ms()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
// Safely access a parallel array by index, returning a fallback if out of bounds. // Safely access a parallel array by index, returning a fallback if out of bounds.
template<typename T> template<typename T>
@@ -69,6 +80,31 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection&
SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
void SnapmakerPrinterAgent::start_camera_monitor()
{
std::thread([this] {
send_ws_rpc("camera.start_monitor",
{{"domain", "lan"}, {"interval", 0}, {"expect_pw", false}});
}).detach();
m_camera_last_fire_ms.store(now_ms());
}
void SnapmakerPrinterAgent::on_status_loop_tick(const std::string& dev_id)
{
(void) dev_id;
const int64_t last = m_camera_last_fire_ms.load();
if (last == 0 || now_ms() - last >= CAMERA_REFRESH_INTERVAL_MS) {
start_camera_monitor();
}
}
int SnapmakerPrinterAgent::command_start_camera(std::string dev_id)
{
(void) dev_id;
start_camera_monitor();
return BAMBU_NETWORK_SUCCESS;
}
AgentInfo SnapmakerPrinterAgent::get_agent_info_static() AgentInfo SnapmakerPrinterAgent::get_agent_info_static()
{ {
return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"};
@@ -104,17 +140,33 @@ std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type
return base; return base;
} }
bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id) bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect"); (void) dev_id;
if (sync_mode != get_filament_sync_mode())
return false;
const std::string base_url = device_info.base_url;
const std::string api_key = device_info.api_key;
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
std::thread([this, base_url, api_key]() {
struct InFlightGuard
{
std::atomic<int>& counter;
~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); }
} guard{filament_fetch_in_flight};
const std::string url = join_url(base_url, "/printer/objects/query?print_task_config&filament_detect");
std::string response_body; std::string response_body;
bool success = false; bool success = false;
std::string http_error; std::string http_error;
auto http = Http::get(url); auto http = Http::get(url);
if (!device_info.api_key.empty()) { if (!api_key.empty()) {
http.header("X-Api-Key", device_info.api_key); http.header("X-Api-Key", api_key);
} }
http.timeout_connect(5) http.timeout_connect(5)
.timeout_max(10) .timeout_max(10)
@@ -136,20 +188,19 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id)
if (!success) { if (!success) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error; BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error;
return false; return;
} }
auto json = nlohmann::json::parse(response_body, nullptr, false, true); auto json = nlohmann::json::parse(response_body, nullptr, false, true);
if (json.is_discarded()) { if (json.is_discarded()) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response"; BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response";
return false; return;
} }
// Navigate to result.status.print_task_config // Navigate to result.status.print_task_config
if (!json.contains("result") || !json["result"].contains("status") || if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("print_task_config")) {
!json["result"]["status"].contains("print_task_config")) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response"; BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response";
return false; return;
} }
auto& ptc = json["result"]["status"]["print_task_config"]; auto& ptc = json["result"]["status"]["print_task_config"];
@@ -164,13 +215,12 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id)
const int slot_count = static_cast<int>(filament_exist.size()); const int slot_count = static_cast<int>(filament_exist.size());
if (slot_count == 0) { if (slot_count == 0) {
BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported"; BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported";
return false; return;
} }
// Read NFC filament_detect data for temperature info (optional) // Read NFC filament_detect data for temperature info (optional)
nlohmann::json nfc_info; nlohmann::json nfc_info;
if (json["result"]["status"].contains("filament_detect") && if (json["result"]["status"].contains("filament_detect") && json["result"]["status"]["filament_detect"].contains("info")) {
json["result"]["status"]["filament_detect"].contains("info")) {
nfc_info = json["result"]["status"]["filament_detect"]["info"]; nfc_info = json["result"]["status"]["filament_detect"]["info"];
} }
@@ -186,8 +236,7 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id)
tray.has_filament = filament_exist[i]; tray.has_filament = filament_exist[i];
if (tray.has_filament) { if (tray.has_filament) {
tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), safe_at(filament_sub_type, i, empty_str));
safe_at(filament_sub_type, i, empty_str));
tray.tray_color = safe_at(filament_color, i, default_color); tray.tray_color = safe_at(filament_color, i, default_color);
auto* bundle = GUI::wxGetApp().preset_bundle; auto* bundle = GUI::wxGetApp().preset_bundle;
@@ -200,8 +249,8 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id)
if (!filament_id.empty()) { if (!filament_id.empty()) {
tray.tray_info_idx = filament_id; tray.tray_info_idx = filament_id;
BOOST_LOG_TRIVIAL(warning) << "Filament sync: Found manufacturer-specific profile for slot " << i << ": " BOOST_LOG_TRIVIAL(warning)
<< filament_id; << "Filament sync: Found manufacturer-specific profile for slot " << i << ": " << filament_id;
} else { } else {
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
} }
@@ -224,7 +273,16 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id)
} }
build_ams_payload(1, slot_count - 1, trays); build_ams_payload(1, slot_count - 1, trays);
}).detach();
return true; return true;
} }
FilamentSyncMode SnapmakerPrinterAgent::get_filament_sync_mode() const
{
if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
return FilamentSyncMode::subscription;
return FilamentSyncMode::pull;
}
} // namespace Slic3r } // namespace Slic3r
+13 -1
View File
@@ -1,7 +1,10 @@
#pragma once #pragma once
#include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include <atomic>
#include <cstdint>
#include <string> #include <string>
namespace Slic3r { namespace Slic3r {
@@ -15,11 +18,20 @@ public:
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
bool fetch_filament_info(std::string dev_id) override; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
FilamentSyncMode get_filament_sync_mode() const override;
int command_start_camera(std::string dev_id) override;
CameraStreamMode get_camera_stream_mode() const override { return CameraStreamMode::http_snapshot; }
std::string get_camera_url() const override { return device_info.base_url + "/server/files/camera/monitor.jpg"; }
private: private:
// Combine filament_type + filament_sub_type into a unified type string // Combine filament_type + filament_sub_type into a unified type string
static std::string combine_filament_type(const std::string& type, const std::string& sub_type); static std::string combine_filament_type(const std::string& type, const std::string& sub_type);
void start_camera_monitor();
void on_status_loop_tick(const std::string& dev_id) override;
std::atomic<int64_t> m_camera_last_fire_ms{0};
}; };
} // namespace Slic3r } // namespace Slic3r
@@ -23,6 +23,14 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
.value("Pull", FilamentSyncMode::pull) .value("Pull", FilamentSyncMode::pull)
.export_values(); .export_values();
py::enum_<CameraStreamMode>(printer_agent_module, "CameraStreamMode")
.value("None_", CameraStreamMode::none)
.value("HTTP", CameraStreamMode::http)
.value("HTTP_SNAPSHOT", CameraStreamMode::http_snapshot)
.value("RTSP", CameraStreamMode::rtsp)
.value("WebRTC", CameraStreamMode::webrtc)
.export_values();
py::class_<AgentInfo>(printer_agent_module, "AgentInfo") py::class_<AgentInfo>(printer_agent_module, "AgentInfo")
.def(py::init<>()) .def(py::init<>())
.def(py::init([](std::string id, std::string name, std::string version, std::string description) { .def(py::init([](std::string id, std::string name, std::string version, std::string description) {
@@ -105,7 +113,9 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
.def("start_send_gcode_to_sdcard", &PrinterAgentPluginCapability::start_send_gcode_to_sdcard) .def("start_send_gcode_to_sdcard", &PrinterAgentPluginCapability::start_send_gcode_to_sdcard)
.def("start_local_print", &PrinterAgentPluginCapability::start_local_print) .def("start_local_print", &PrinterAgentPluginCapability::start_local_print)
.def("get_filament_sync_mode", &PrinterAgentPluginCapability::get_filament_sync_mode) .def("get_filament_sync_mode", &PrinterAgentPluginCapability::get_filament_sync_mode)
.def("get_camera_stream_mode", &PrinterAgentPluginCapability::get_camera_stream_mode)
.def("fetch_filament_info", &PrinterAgentPluginCapability::fetch_filament_info) .def("fetch_filament_info", &PrinterAgentPluginCapability::fetch_filament_info)
.def("get_camera_url", &PrinterAgentPluginCapability::get_camera_url)
.def("check_cert", &PrinterAgentPluginCapability::check_cert) .def("check_cert", &PrinterAgentPluginCapability::check_cert)
.def("install_device_cert", &PrinterAgentPluginCapability::install_device_cert) .def("install_device_cert", &PrinterAgentPluginCapability::install_device_cert)
.def("ping_bind", &PrinterAgentPluginCapability::ping_bind) .def("ping_bind", &PrinterAgentPluginCapability::ping_bind)
@@ -30,21 +30,19 @@ public:
AgentInfo get_agent_info() override = 0; AgentInfo get_agent_info() override = 0;
int connect_printer( int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override = 0;
std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override = 0;
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0; int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0; int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
bool start_discovery(bool start, bool sending) override = 0; bool start_discovery(bool start, bool sending) override = 0;
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0; int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0;
std::string get_user_selected_machine() override = 0; std::string get_user_selected_machine() override = 0;
int set_user_selected_machine(std::string dev_id) override = 0; int set_user_selected_machine(std::string dev_id) override = 0;
int start_send_gcode_to_sdcard(PrintParams params, int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0;
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn,
OnWaitFn wait_fn) override = 0;
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0; int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
FilamentSyncMode get_filament_sync_mode() const override = 0; FilamentSyncMode get_filament_sync_mode() const override = 0;
bool fetch_filament_info(std::string dev_id) override = 0; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override = 0;
CameraStreamMode get_camera_stream_mode() const override = 0;
std::string get_camera_url() const override = 0;
int check_cert() override = 0; int check_cert() override = 0;
void install_device_cert(std::string dev_id, bool lan_only) override = 0; void install_device_cert(std::string dev_id, bool lan_only) override = 0;

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