diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..da51e46f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# ignore file from exports + +# git files +.gitattributes export-ignore +.gitignore export-ignore +# github files +ISSUE_TEMPLATE.md export-ignore +CODE_OF_CONDUCT.md export-ignore +README.md export-ignore +# pycharm files +.editorconfig export-ignore +# all files within the Extras folder +Extras export-ignore + +octoprint_octolapse/_version.py export-subst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..a126e9e0 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,20 @@ +name: Create Plugin ZIP +on: + push: + tags: + - 'v*' # Wird ausgeführt, wenn du ein neues Tag wie "v1.0" erstellst + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out Repository + uses: actions/checkout@v3 + + - name: Erstelle ZIP-Archiv + run: zip -r master.zip Octolapse/builds + + - name: Release mit ZIP-Datei + uses: softprops/action-gh-release@v1 + with: + files: master.zip diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml new file mode 100644 index 00000000..489de0ef --- /dev/null +++ b/.github/workflows/master.yml @@ -0,0 +1,21 @@ +name: Create master.zip +on: + push: + branches: + - master # Change to 'master' if your branch is named master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Create ZIP Archive + run: zip -r master.zip . -x "*.git*" + + - name: Upload ZIP as Artifact + uses: actions/upload-artifact@v4 + with: + name: master-zip + path: master.zip diff --git a/Extras/Wiki/assets/img/putty/apt-get_dist_upgrade.jpg b/Extras/Wiki/assets/img/putty/apt-get_dist_upgrade.jpg new file mode 100644 index 00000000..7ca9beb3 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/apt-get_dist_upgrade.jpg differ diff --git a/Extras/Wiki/assets/img/putty/apt-get_update.jpg b/Extras/Wiki/assets/img/putty/apt-get_update.jpg new file mode 100644 index 00000000..7ca4aa18 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/apt-get_update.jpg differ diff --git a/Extras/Wiki/assets/img/putty/change_password.jpg b/Extras/Wiki/assets/img/putty/change_password.jpg new file mode 100644 index 00000000..ac5aec62 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/change_password.jpg differ diff --git a/Extras/Wiki/assets/img/putty/connect_via_putty.jpg b/Extras/Wiki/assets/img/putty/connect_via_putty.jpg new file mode 100644 index 00000000..f432bccd Binary files /dev/null and b/Extras/Wiki/assets/img/putty/connect_via_putty.jpg differ diff --git a/Extras/Wiki/assets/img/putty/edit_modules.jpg b/Extras/Wiki/assets/img/putty/edit_modules.jpg new file mode 100644 index 00000000..491367f7 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/edit_modules.jpg differ diff --git a/Extras/Wiki/assets/img/putty/modules_add_picam_driver.jpg b/Extras/Wiki/assets/img/putty/modules_add_picam_driver.jpg new file mode 100644 index 00000000..47f351bc Binary files /dev/null and b/Extras/Wiki/assets/img/putty/modules_add_picam_driver.jpg differ diff --git a/Extras/Wiki/assets/img/putty/modules_without_picam_driver.jpg b/Extras/Wiki/assets/img/putty/modules_without_picam_driver.jpg new file mode 100644 index 00000000..51cf1840 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/modules_without_picam_driver.jpg differ diff --git a/Extras/Wiki/assets/img/putty/octopi.txt_camera_auto.jpg b/Extras/Wiki/assets/img/putty/octopi.txt_camera_auto.jpg new file mode 100644 index 00000000..9d29994e Binary files /dev/null and b/Extras/Wiki/assets/img/putty/octopi.txt_camera_auto.jpg differ diff --git a/Extras/Wiki/assets/img/putty/octopi.txt_camera_usb.jpg b/Extras/Wiki/assets/img/putty/octopi.txt_camera_usb.jpg new file mode 100644 index 00000000..faf66d06 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/octopi.txt_camera_usb.jpg differ diff --git a/Extras/Wiki/assets/img/putty/octopi.txt_disabled_image_preferences.jpg b/Extras/Wiki/assets/img/putty/octopi.txt_disabled_image_preferences.jpg new file mode 100644 index 00000000..d35f1f97 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/octopi.txt_disabled_image_preferences.jpg differ diff --git a/Extras/Wiki/assets/img/putty/octopi.txt_enable_image_preferences.jpg b/Extras/Wiki/assets/img/putty/octopi.txt_enable_image_preferences.jpg new file mode 100644 index 00000000..ece3eb57 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/octopi.txt_enable_image_preferences.jpg differ diff --git a/Extras/Wiki/assets/img/putty/open_octopi.txt.jpg b/Extras/Wiki/assets/img/putty/open_octopi.txt.jpg new file mode 100644 index 00000000..a4a8a87f Binary files /dev/null and b/Extras/Wiki/assets/img/putty/open_octopi.txt.jpg differ diff --git a/Extras/Wiki/assets/img/putty/reboot.jpg b/Extras/Wiki/assets/img/putty/reboot.jpg new file mode 100644 index 00000000..30fc7246 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/reboot.jpg differ diff --git a/Extras/Wiki/assets/img/putty/rpi-update.jpg b/Extras/Wiki/assets/img/putty/rpi-update.jpg new file mode 100644 index 00000000..ab3f0f60 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/rpi-update.jpg differ diff --git a/Extras/Wiki/assets/img/putty/sign_in.jpg b/Extras/Wiki/assets/img/putty/sign_in.jpg new file mode 100644 index 00000000..3b0159d8 Binary files /dev/null and b/Extras/Wiki/assets/img/putty/sign_in.jpg differ diff --git a/Extras/Wiki/assets/img/putty/would_you_like_to_proceed.jpg b/Extras/Wiki/assets/img/putty/would_you_like_to_proceed.jpg new file mode 100644 index 00000000..484b9efb Binary files /dev/null and b/Extras/Wiki/assets/img/putty/would_you_like_to_proceed.jpg differ diff --git a/Extras/Wiki/assets/img/settings/close_octolapse_settings.jpg b/Extras/Wiki/assets/img/settings/close_octolapse_settings.jpg new file mode 100644 index 00000000..baf1bb89 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/close_octolapse_settings.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/add_edit_camera_profile.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/add_edit_camera_profile.jpg new file mode 100644 index 00000000..420268d5 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/add_edit_camera_profile.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/add_new_camera_profile.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/add_new_camera_profile.jpg new file mode 100644 index 00000000..fd649154 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/add_new_camera_profile.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/camera_type_and_addresses.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/camera_type_and_addresses.jpg new file mode 100644 index 00000000..91bdc65e Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/camera_type_and_addresses.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/customize_camera_profile.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/customize_camera_profile.jpg new file mode 100644 index 00000000..57c21b33 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/customize_camera_profile.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/edit_camera_profile.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/edit_camera_profile.jpg new file mode 100644 index 00000000..cfe7d447 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/edit_camera_profile.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/enable_custom_image_preferences.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/enable_custom_image_preferences.jpg new file mode 100644 index 00000000..829a1852 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/enable_custom_image_preferences.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/enable_custom_image_preferences_failed.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/enable_custom_image_preferences_failed.jpg new file mode 100644 index 00000000..daa93d2a Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/enable_custom_image_preferences_failed.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/general_options.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/general_options.jpg new file mode 100644 index 00000000..02134342 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/general_options.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/1080p_30fps.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/1080p_30fps.jpg new file mode 100644 index 00000000..1e2f6f1a Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/1080p_30fps.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/balance.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/balance.jpg new file mode 100644 index 00000000..91e52d0e Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/balance.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/camera_stream.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/camera_stream.jpg new file mode 100644 index 00000000..befade19 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/camera_stream.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/camera_stream_failed.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/camera_stream_failed.jpg new file mode 100644 index 00000000..8cdeb68a Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/camera_stream_failed.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/codec_controls.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/codec_controls.jpg new file mode 100644 index 00000000..92857ab5 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/codec_controls.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/custom_two_column_layout.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/custom_two_column_layout.jpg new file mode 100644 index 00000000..ca208efa Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/custom_two_column_layout.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/effects.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/effects.jpg new file mode 100644 index 00000000..5ad1bd9a Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/effects.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/exposure.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/exposure.jpg new file mode 100644 index 00000000..9c7d0c5d Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/exposure.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/exposure_manual_low.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/exposure_manual_low.jpg new file mode 100644 index 00000000..7f265828 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/exposure_manual_low.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/flip_rotate_image.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/flip_rotate_image.jpg new file mode 100644 index 00000000..9d7b7ccf Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/flip_rotate_image.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/general_settings.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/general_settings.jpg new file mode 100644 index 00000000..e696bab5 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/general_settings.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/misc.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/misc.jpg new file mode 100644 index 00000000..5c19f5f3 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/image_preferences/raspicam/misc.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/import_profile_not_selected.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/import_profile_not_selected.jpg new file mode 100644 index 00000000..03f9e6ae Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/import_profile_not_selected.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/rename_and_save_new_camera_profile.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/rename_and_save_new_camera_profile.jpg new file mode 100644 index 00000000..4dd438a9 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/rename_and_save_new_camera_profile.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/shortcut_to_camera_profiles_edit.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/shortcut_to_camera_profiles_edit.jpg new file mode 100644 index 00000000..039bfaa5 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/shortcut_to_camera_profiles_edit.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/test_camera_settings.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/test_camera_settings.jpg new file mode 100644 index 00000000..b6111b84 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/test_camera_settings.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/test_camera_settings_fail.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/test_camera_settings_fail.jpg new file mode 100644 index 00000000..31b5b54b Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/test_camera_settings_fail.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/test_camera_settings_success.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/test_camera_settings_success.jpg new file mode 100644 index 00000000..7e7fe63d Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/test_camera_settings_success.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/webcam_stream_address_absolute.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/webcam_stream_address_absolute.jpg new file mode 100644 index 00000000..072b09c8 Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/webcam_stream_address_absolute.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/camera/webcam_stream_address_relative.jpg b/Extras/Wiki/assets/img/settings/profiles/camera/webcam_stream_address_relative.jpg new file mode 100644 index 00000000..972987ff Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/camera/webcam_stream_address_relative.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/printer/slicer_type/automatic/default_extruder.jpg b/Extras/Wiki/assets/img/settings/profiles/printer/slicer_type/automatic/default_extruder.jpg new file mode 100644 index 00000000..9dc8627f Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/printer/slicer_type/automatic/default_extruder.jpg differ diff --git a/Extras/Wiki/assets/img/settings/profiles/printer/slicer_type/automatic/slicer_type_dropdown_automatic.jpg b/Extras/Wiki/assets/img/settings/profiles/printer/slicer_type/automatic/slicer_type_dropdown_automatic.jpg new file mode 100644 index 00000000..6eeeabfa Binary files /dev/null and b/Extras/Wiki/assets/img/settings/profiles/printer/slicer_type/automatic/slicer_type_dropdown_automatic.jpg differ diff --git a/Extras/Wiki/assets/img/slicers/Slic3r/enable_verbose_gcode.jpg b/Extras/Wiki/assets/img/slicers/Slic3r/enable_verbose_gcode.jpg new file mode 100644 index 00000000..7f709f4a Binary files /dev/null and b/Extras/Wiki/assets/img/slicers/Slic3r/enable_verbose_gcode.jpg differ diff --git a/Extras/Wiki/assets/img/slicers/cura/cura_help_about.jpg b/Extras/Wiki/assets/img/slicers/cura/cura_help_about.jpg new file mode 100644 index 00000000..797492f3 Binary files /dev/null and b/Extras/Wiki/assets/img/slicers/cura/cura_help_about.jpg differ diff --git a/Extras/Wiki/assets/img/slicers/cura/cura_manage_printer.jpg b/Extras/Wiki/assets/img/slicers/cura/cura_manage_printer.jpg new file mode 100644 index 00000000..b7503544 Binary files /dev/null and b/Extras/Wiki/assets/img/slicers/cura/cura_manage_printer.jpg differ diff --git a/Extras/Wiki/assets/img/slicers/cura/cura_manage_printers.jpg b/Extras/Wiki/assets/img/slicers/cura/cura_manage_printers.jpg new file mode 100644 index 00000000..bfd20282 Binary files /dev/null and b/Extras/Wiki/assets/img/slicers/cura/cura_manage_printers.jpg differ diff --git a/Extras/Wiki/assets/img/slicers/cura/cura_version.jpg b/Extras/Wiki/assets/img/slicers/cura/cura_version.jpg new file mode 100644 index 00000000..b3e2f42f Binary files /dev/null and b/Extras/Wiki/assets/img/slicers/cura/cura_version.jpg differ diff --git a/Extras/Wiki/assets/img/slicers/cura/start_gcode.jpg b/Extras/Wiki/assets/img/slicers/cura/start_gcode.jpg new file mode 100644 index 00000000..3966e917 Binary files /dev/null and b/Extras/Wiki/assets/img/slicers/cura/start_gcode.jpg differ diff --git a/Extras/Wiki/assets/img/slicers/cura/start_gcode_cura_script_added.jpg b/Extras/Wiki/assets/img/slicers/cura/start_gcode_cura_script_added.jpg new file mode 100644 index 00000000..ac4f8abf Binary files /dev/null and b/Extras/Wiki/assets/img/slicers/cura/start_gcode_cura_script_added.jpg differ diff --git a/Extras/Wiki/assets/img/tab/image_preferences_edit.jpg b/Extras/Wiki/assets/img/tab/image_preferences_edit.jpg new file mode 100644 index 00000000..1dbb868d Binary files /dev/null and b/Extras/Wiki/assets/img/tab/image_preferences_edit.jpg differ diff --git a/Extras/Wiki/assets/img/tab/octolapse_tab_expanded.jpg b/Extras/Wiki/assets/img/tab/octolapse_tab_expanded.jpg new file mode 100644 index 00000000..e10809b1 Binary files /dev/null and b/Extras/Wiki/assets/img/tab/octolapse_tab_expanded.jpg differ diff --git a/Extras/plugin_repository/_plugins/octolapse.md b/Extras/plugin_repository/_plugins/octolapse.md deleted file mode 100644 index 0a9d8468..00000000 --- a/Extras/plugin_repository/_plugins/octolapse.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -layout: plugin - -id: octolapse -title: Octolapse -description: Create a stabilized timelapse of your 3D prints. Highly customizable, loads of presets, lots of fun. Requires OctoPrint 1.3.9 or higher. -author: Brad Hochgesang -license: AGPL-3.0 -date: 2018-11-15 - -homepage: https://formerlurker.github.io/Octolapse/ -source: https://github.com/FormerLurker/Octolapse/ -archive: https://github.com/FormerLurker/Octolapse/archive/v0.3.4.zip - -tags: -- timelapse - -featuredimage: /assets/img/plugins/octolapse/tab_mini.png - -compatibility: - - octoprint: - - 1.3.9 - - os: - - linux - - windows - - macos - - freebsd ---- - - -# Octolapse - -
- Octolapse Tab -
- The Octolapse Tab -
-
-
- -***Octolapse is provided without warranties of any kind. By installing Octolapse you agree to accept all liability for any damage caused directly or indirectly by Octolapse. Use caution and never leave your printer unattended.*** - -Octolapse is designed to make stabilized timelapses of your prints with as little hassle as possible, and it's extremely configurable. Now you can create a silky smooth timelapse without a custom camera mount, no GCode customizations required. - -Octolapse moves the print bed and extruder into position before taking each snapshot, giving you a crisp image in every frame. Snapshots can be taken at each layer change, at specific height increments, after a period of time has elapsed, or when certain GCodes are detected. - -[Please support my work by becoming a patron.](https://www.patreon.com/bePatron?u=9588101) - -**Important**: *Octolapse requires OctoPrint v1.3.9 or higher and some features require OctoPrint v1.3.10rc1 or above.* You can check your OctoPrint version by looking in the lower left hand corner of your OctoPrint server home page. - -# Recent Changes -If you are upgrading from [v0.3.1](https://github.com/FormerLurker/Octolapse/releases/tag/v0.3.1), please read the [v0.3.4 release notes](https://github.com/FormerLurker/Octolapse/releases/tag/v0.3.4), a lot has changed. If you've had trouble running Octolapse in the past, you just might want to try it again! - -# What Octolapse Does - -
-
-
- {% include youtube.html vid="er0VCYen1MY" %} -
-
- - A timelapse of a double spiral vase made with Octolapse - -
-
-
-## Octolapse was designed with print quality in mind -* **Continuous tracking** of the X,Y, and Z axes. Octolapse knows where your extruder and bed will be at all times. -* **Extruder state detection** enables Octolapse to choose a good time take a snapshot, minimizing defects. -* Choose from dozens of existing **presets,** or use a **custom configuration** to maximize quality for your specific application. -* Customizable **Z-Hop and retract detection** to reduce stringing, maximizing quality. -* **Configurable stabilizations** allow you complete control of the X and Y position of each snapshot. You can choose a position as close to or as far away from your part as you wish. -* **Minimal impact on print time and print quality.** Octolapse normally takes between one and three seconds to take a snapshot. It even reports exactly how much time it's using after each snapshot! -* Use **High Quality Mode** to prevent snapshots over exterior perimeters using the new fully customizable **Feature Detection**. -* Allow or prevent snapshots in certain areas with **snapshot position restrictions.** This can be used to prevent snapshots over critical areas of your print or a delicate part. -* If you have a multi-material printer, you can even **restrict snapshots movements to your wipe tower**, virtually eliminating any quality considerations! -* Octolapse can **calculate intersections** between the printer path and any snapshot position restriction, allowing a snapshot even in the middle of an extrusion. -* **Stop the timelapse whenever you want!** If things go awry, you can prevent Octolapse from taking any further snapshots for the rest of the print. It will wait to render any snapshots that were already taken until after the print has finished. -* Use **test mode** to try out your timelapse settings without heating your bed or nozzle, turning on any fans, or extruding any filament. This saves time and plastic. It's also very useful for development and testing. -## Octolapse was also designed to make great timelapses -* **Synchronize your timelapses** with OctoPrint's built in plugin, keeping all of your videos in one place. -* Choose between **fixed frame rate** or **fixed length rendering.** Octolapse will automatically adjust the frame rate to match any desired length. You can even set a minimum or maximum acceptable frame rate. -* Add **pre or post roll frames** (not videos yet, sorry) to your snapshot preventing an abrupt start or finish. -* Choose from several output formats including **MP4 (libxvid and H.264), AVI, FLV, VOB, GIF, and MPEG.** *More coming soon, suggestions welcome.* _Note: *OctoPrint 1.3.10 is required to synchronize some formats with the built-in timelapse plugin.*_ -* **Control the bitrate** of your video. -* Add a **Custom Watermark** to your video. -* Add **Text Overlays** to your video using replacement tokens. Control the position, color, font and more! -* Control the number of **rendering threads** to reduce rendering time. -## To make good timelapses, you need good snapshots -* Slow camera or low frame rate? Octolapse allows you to **set a snapshot delay** before taking a snapshot to allow your camera enough time to get a clear image. -* **Control your camera settings** when using [mjegstreamer](https://sourceforge.net/projects/mjpg-streamer/) including: contrast, brightness, focus, white balance, pan, tilt, zoom, and much more. You can apply your custom settings before each print. -* **Rotate, flip, and transpose your snapshots** -* Use custom camera scripts to **trigger a DSLR**, post-process images, configure your camera, turn lighting on and off, control **GPIO**, or practically anything you can think of! Five script types are available. -* Use **Multiple cameras** to create multiple timelapses. -* Send gcode to your printer when it's time to take a snapshot via the new **Gcode Camera Type**. Supports for the **M240** gcode! -## Simplified Setup and Better Compatibility -* Select your **slicer type** to simplify printer setup by making the setting names and units match your slicer! Cura, Simplify 3d and Slic3r PE are fully supported. -* Enhanced and **simplified error reporting** and informational panels. -* All settings include links to the **Octolapse Wiki**, so you can get to the documentation quickly if you have questions. -* Octolapse now works with **Themeify**. - -# Installation - -[Read the installation instructions.](https://github.com/FormerLurker/Octolapse/wiki/Installation) - -After installation, [checkout the project's wiki pages](https://github.com/FormerLurker/Octolapse/wiki). It never hurts to know more about what you're doing! - -# Usage -[Learn how to start your first print here.](https://github.com/FormerLurker/Octolapse/wiki/Usage) It's a good idea to read about the settings before using Octolapse. I also recommend you [watch this brief tutorial](https://youtu.be/sDyg9lMqMG8), which explains how to configure your printer profile for use with the new **High Quality** snapshot profile. - -# Known Printer Support - -Octolapse is still in Beta but has been confirmed to work on several printers: - -* Genuine Prusa - Mk2, Mk2S, Mk2 w Multi Material, Mk3 -* Anet A8 - Beta - User Submitted -* Anycube I3 Mega - Beta - User Submitted -* CR-10 - Beta - User Submitted -* Dagoma Neva - Beta - User Submitted -* Irapid Black - Beta - User Submitted -* Monoprice Maker Select v2/Wanhao Duplicator i3 - Beta - User Submitted - Requires OctoPrint 1.3.7rc3 or above - -Please note that some settings may need to be adjusted depending on your slicer settings. - -## More Octolapses -
-
- {% include youtube.html vid="uBeVbDJKHw0" %} -
-
- - A user generated compilation created by WildRose Builds. Support this channel and please subscribe! - -
-
-
- -
-
- {% include youtube.html vid="dYbWfBCLNbI" %} -
-
- - The Milennium Falcon - -
-
-
- -
-
- {% include youtube.html vid="4kEHbRrp2Jk" %} -
-
- - The Moon - Animated X Axis - -
-
-
-
-
- {% include youtube.html vid="Ra5Jjq-nJfA" %} -
-
- - The obligatory benchy - -
-
-
- -## History of Octolapse -I got the idea for Octolapse when I attempted to manually make a [stabilized timelapse](https://youtu.be/xZlP4vpAKNc) by hand editing my GCode files. To accomplish this I used the excellent and simple [GCode System Commands](https://github.com/kantlivelong/OctoPrint-GCodesystemCommands) plugin. The timelapse worked great, but it required a lot of effort which I didn't want to put in every time. I received several requests for instructions on how to create a stabilized timelapse, so I decided to give plugin development a go. I've never done one before (or programmed python or knockout or anything open source), but figured I could contribute something good to the community. This is my "thank you" to all of the makers out there who have contributed your time and effort! - -## Report Problems -If you think you have found a bug in Octolapse, please create an issue on the official github.com page [here](https://github.com/FormerLurker/Octolapse/issues/new). In order to have your issue handled properly and quickly, please completely - -## License -View the [Octolapse license](https://github.com/FormerLurker/Octolapse/blob/master/LICENSE). - -
- -_Copyright (C) 2017 Brad Hochgesang - FormerLurker@pm.me_ diff --git a/Extras/plugin_repository/assets/img/plugins/octolapse/tab_mini.png b/Extras/plugin_repository/assets/img/plugins/octolapse/tab_mini.png deleted file mode 100644 index e5a79b69..00000000 Binary files a/Extras/plugin_repository/assets/img/plugins/octolapse/tab_mini.png and /dev/null differ diff --git a/Extras/plugin_repository/assets/img/yt-with-consent/4kEHbRrp2Jk.jpg b/Extras/plugin_repository/assets/img/yt-with-consent/4kEHbRrp2Jk.jpg deleted file mode 100644 index 28ac4a91..00000000 Binary files a/Extras/plugin_repository/assets/img/yt-with-consent/4kEHbRrp2Jk.jpg and /dev/null differ diff --git a/Extras/plugin_repository/assets/img/yt-with-consent/Ra5Jjq-nJfA.jpg b/Extras/plugin_repository/assets/img/yt-with-consent/Ra5Jjq-nJfA.jpg deleted file mode 100644 index 09454101..00000000 Binary files a/Extras/plugin_repository/assets/img/yt-with-consent/Ra5Jjq-nJfA.jpg and /dev/null differ diff --git a/Extras/plugin_repository/assets/img/yt-with-consent/dYbWfBCLNbI.jpg b/Extras/plugin_repository/assets/img/yt-with-consent/dYbWfBCLNbI.jpg deleted file mode 100644 index 38747053..00000000 Binary files a/Extras/plugin_repository/assets/img/yt-with-consent/dYbWfBCLNbI.jpg and /dev/null differ diff --git a/Extras/plugin_repository/assets/img/yt-with-consent/er0VCYen1MY.jpg b/Extras/plugin_repository/assets/img/yt-with-consent/er0VCYen1MY.jpg deleted file mode 100644 index 3c0687b2..00000000 Binary files a/Extras/plugin_repository/assets/img/yt-with-consent/er0VCYen1MY.jpg and /dev/null differ diff --git a/Extras/plugin_repository/assets/img/yt-with-consent/uBeVbDJKHw0.jpg b/Extras/plugin_repository/assets/img/yt-with-consent/uBeVbDJKHw0.jpg deleted file mode 100644 index de6b241d..00000000 Binary files a/Extras/plugin_repository/assets/img/yt-with-consent/uBeVbDJKHw0.jpg and /dev/null differ diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index af6cf830..b0851e6d 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,13 +1,21 @@ -FEATURE_REQUEST_DESCRIPTION_GOES_HERE +___REPLACE_THIS__FEATURE_REQUEST_DESCRIPTION_GOES_HERE #### Version of Octolapse @@ -35,7 +43,7 @@ FEATURE_REQUEST_DESCRIPTION_GOES_HERE branch, and not the default (master) branch. DO NOT OMIT --> -Octolapse Version: OCTOLAPSE_VERSION_GOES_HERE +Octolapse Version: ___REPLACE_THIS__OCTOLAPSE_VERSION_GOES_HERE #### Version of OctoPrint @@ -43,7 +51,7 @@ Octolapse Version: OCTOLAPSE_VERSION_GOES_HERE Can be found in the lower left corner of the web interface. DO NOT OMIT --> -OctoPrint Version: OCTOPRINT_VERSION_GOES_HERE +OctoPrint Version: ___REPLACE_THIS__OCTOPRINT_VERSION_GOES_HERE -Diagnostic Logging was Enabled: YES_OR_NO +Diagnostic Logging was Enabled: ___REPLACE_THIS__YES_OR_NO #### What were you doing when the problem occurred @@ -69,17 +77,17 @@ Be specific. Explain what you did as clearly as possible. It's best if you provide a list of steps you took that can be used to reproduce the error you encountered. --> -1. STEP_ONE_GOES_HERE -2. STEP_TWO_GOES_HERE -3. STEP_... +1. ___REPLACE_THIS__STEP_ONE_GOES_HERE +2. ___REPLACE_THIS__STEP_TWO_GOES_HERE +3. ___REPLACE_THIS__STEP_... #### What should have happened? -PUT_YOUR_DESCRIPTION_HERE +___REPLACE_THIS__PUT_YOUR_DESCRIPTION_HERE #### What happened instead? -PUT_YOUR_DESCRIPTION_HERE +___REPLACE_THIS__PUT_YOUR_DESCRIPTION_HERE #### Operating System running OctoPrint and Octolapse @@ -90,24 +98,24 @@ OctoPi's version can be found in /etc/octopi_version or in the lower left corner of the web interface. DO NOT OMIT --> -OS Name: OS_NAME_GOES_HERE -Os Version: OS_VERSION_GOES_HERE +OS Name: ___REPLACE_THIS__OS_NAME_GOES_HERE +Os Version: ___REPLACE_THIS__OS_VERSION_GOES_HERE #### Printer model & used firmware incl. version -Printer Model: PRINTER_MODEL_GOES_HERE -Printer Firmware Version: PRINTER_FIRMWARE_VERSION_GOES_HERE +Printer Model: ___REPLACE_THIS__PRINTER_MODEL_GOES_HERE +Printer Firmware Version: ___REPLACE_THIS__PRINTER_FIRMWARE_VERSION_GOES_HERE #### Browser and version of browser, operating system running browser -Browser: BROWSER_VERSION_GOES_HERE -Browser OS: BROWSER_OS_GOES_HERE +Browser: ___REPLACE_THIS__BROWSER_VERSION_GOES_HERE +Browser OS: ___REPLACE_THIS__BROWSER_OS_GOES_HERE #### Link to the gcode file you were printing when the problem occurred @@ -121,7 +129,7 @@ In any case, the gcode file is often very useful. On gist.github.com or pastebin.com. OMIT ONLY IF IT IS DEFINITELY NOT NEEDED !--> -Link to Gcode File: GCODE_FILE_LINK_GOES_HERE +Link to Gcode File: ___REPLACE_THIS__GCODE_FILE_LINK_GOES_HERE #### Link to settings.json @@ -142,7 +150,7 @@ to find all of the entries containing passwords. OMIT ONLY IF YOU HAVE ENTERED A PASSWORD TO ACCESS YOUR CAMERA WITHIN OCTOLAPSE !--> -Link to settings.json with all passwords removed: SETTINGS_JSON_LINK_GOES_HERE +Link to settings.json with all passwords removed: ___REPLACE_THIS__SETTINGS_JSON_LINK_GOES_HERE #### Link to plugin_octolapse.log @@ -163,7 +171,7 @@ Link to plugin_octolapse.log: LINK_GOES_HERE On gist.github.com or pastebin.com. DO NOT OMIT --> -Link to octoprint.log: LINK_GOES_HERE +Link to octoprint.log: ___REPLACE_THIS__LINK_GOES_HERE #### Link to contents of Javascript console in the browser @@ -173,7 +181,7 @@ settings, please inlude the javascript console output on gist.github.com or pastebin.com. OMIT ONLY IF IT IS DEFINITELY NOT NEEDED --> -Link to javascript console output: LINK_GOES_HERE +Link to javascript console output: ___REPLACE_THIS__LINK_GOES_HERE #### Screenshots and/or videos of the problem: @@ -181,7 +189,7 @@ Link to javascript console output: LINK_GOES_HERE This is really nice to include, especially if you're having trouble putting your issue into words. A picture speaks 1000 of them, afterall! --> -Screenshot/Video Links: LINKs_GO_HERE +Screenshot/Video Links: ___REPLACE_THIS__LINKs_GO_HERE ## Please consider becoming a patron diff --git a/MANIFEST.in b/MANIFEST.in index b304f51f..b2f463a8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,3 +5,6 @@ recursive-include octoprint_octolapse/static * recursive-include octoprint_octolapse/data * recursive-include octoprint_octolapse/data/images * recursive-include octoprint_octolapse/data/lib/c * + +include versioneer.py +include octoprint_octolapse/_version.py diff --git a/PYTHON311_COMPATIBILITY.md b/PYTHON311_COMPATIBILITY.md new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index af94c6ae..ab95f474 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,266 @@ # Octolapse -

- Octolapse Tab -
- Create a stabilized timelapse of your 3D prints. Highly customizable, loads of presets, lots of fun. -
-

-***Octolapse is provided without warranties of any kind. By installing Octolapse you agree to accept all liability for any damage caused directly or indirectly by Octolapse. Use caution and never leave your printer unattended.*** +
+ + The Octolapse Tab +
+ + The New and Improved Octolapse Tab + +
-Octolapse is designed to make stabilized timelapses of your prints with as little hassle as possible, and it's extremely configurable. Now you can create a silky smooth timelapse without a custom camera mount, no GCode customizations required. +***Octolapse is provided without warranties of any kind. By installing Octolapse, you agree to accept all liability for any damage caused directly or indirectly by Octolapse. Use caution and never leave your printer unattended.*** -Octolapse moves the print bed and extruder into position before taking each snapshot, giving you a crisp image in every frame. Snapshots can be taken at each layer change, at specific height increments, after a period of time has elapsed, or when certain GCodes are detected. +## What Octolapse Does -[Please support my work by becoming a patron.](https://www.patreon.com/bePatron?u=9588101) +Octolapse is designed to make stabilized timelapses of your prints with as little hassle as possible, and it's extremely configurable. Now you can create a silky smooth timelapse without a custom camera mount, and no GCode customizations are required. -**Important**: *Octolapse requires OctoPrint v1.3.9 or higher and some features require OctoPrint v1.3.10rc1 or above.* You can check your OctoPrint version by looking in the lower left hand corner of your OctoPrint server home page. +
+ + Double Spiral Vase Timelapse Taken with Octolapse +
+ + A Timelapse of a Double Spiral Vase Made with Octolapse + +
-# Recent Changes -If you are upgrading from [v0.3.1](https://github.com/FormerLurker/Octolapse/releases/tag/v0.3.1), please read the [v0.3.4 release notes](https://github.com/FormerLurker/Octolapse/releases/tag/v0.3.4), a lot has changed. If you've had trouble running Octolapse in the past, you just might want to try it again! +Octolapse moves the print bed and extruder into position before taking each snapshot, giving you a crisp image in every frame. Snapshots can be taken at each layer change, at specific height increments, after a period of time has elapsed, or when certain GCodes are detected. -# What Octolapse Does +**Important**: *Octolapse requires OctoPrint v1.3.9 or higher, and some features require OctoPrint v1.3.10rc1 or above.* You can check your OctoPrint version by looking in the lower left hand corner of your OctoPrint server home page. -

- - A timelapse of a double spiral vase made with Octolapse +## Getting Started + +Be sure to [read the getting started guide](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Getting-Started) on the [Octolapse Wiki](https://github.com/FormerLurker/Octolapse/wiki). This will save you a lot of hassle and allow you to unlock some of the best features of Octolapse. + +

+ + View the Getting Started Guide Companion Video on Youtube +
+ + View the Getting Started Guide Companion Video -
- - A timelapse of a double spiral vase made with Octolapse +
+ +## Recent Changes + +A **lot** has changed since the last release (v0.3.4). Over **18** months of development and over 600 commits went into this newest version. If you've had trouble installing or running Octolapse in the past, you just might want to try it again! + +I have been focusing on 3 items for V0.4: + +1. **Print Quality** - This is my #**1** concern, and a ton of effort has gone into reducing artifacts. +2. **Print Time** - Lots of folks have complained about the print time impact of Octolapse, and this is a totally legitimate concern. This has been handled by adding new *Smart* triggers that minimize travel distance. In fact, the new *Smart - Snap To Print* trigger *completely eliminates* travel moves from the stabilization! +3. **Simplify Setup** - Prior to V0.4, Octolapse was much more difficult to configure, especially for beginners. Incorrect setup leads to poor print quality as well as extreme frustration. This sad situation has been improved (but not completely solved) in several ways, including automatic slicer settings extraction and profile import/export/update functionality. + +## Highlights of V0.4 + +### Python 3 and Octoprint 1.4.0 Support + +Octolapse now runs on the newest versions of Python and is still backwards compatible with Python 2.7. Additionally, Octolapse runs on the newest version of OctoPrint. + +### Automatic Slicer Settings Detection + +Octolapse can now extract all the required slicer settings directly from your GCode file. You no longer need to copy your slicer settings into Octolapse before every print. This is my favorite new feature! + +
+ + Automatic Slicer Settings Configuration +
+ + Automatic Slicer Settings Configuration -
-

- -## Octolapse was designed with print quality in mind -* **Continuous tracking** of the X,Y, and Z axes. Octolapse knows where your extruder and bed will be at all times. -* **Extruder state detection** enables Octolapse to choose a good time take a snapshot, minimizing defects. -* Choose from dozens of existing **presets,** or use a **custom configuration** to maximize quality for your specific application. -* Customizable **Z-Hop and retract detection** to reduce stringing, maximizing quality. -* **Configurable stabilizations** allow you complete control of the X and Y position of each snapshot. You can choose a position as close to or as far away from your part as you wish. -* **Minimal impact on print time and print quality.** Octolapse normally takes between one and three seconds to take a snapshot. It even reports exactly how much time it's using after each snapshot! -* Use **High Quality Mode** to prevent snapshots over exterior perimeters using the new fully customizable **Feature Detection**. -* Allow or prevent snapshots in certain areas with **snapshot position restrictions.** This can be used to prevent snapshots over critical areas of your print or a delicate part. -* If you have a multi-material printer, you can even **restrict snapshots movements to your wipe tower**, virtually eliminating any quality considerations! -* Octolapse can **calculate intersections** between the printer path and any snapshot position restriction, allowing a snapshot even in the middle of an extrusion. -* **Stop the timelapse whenever you want!** If things go awry, you can prevent Octolapse from taking any further snapshots for the rest of the print. It will wait to render any snapshots that were already taken until after the print has finished. -* Use **test mode** to try out your timelapse settings without heating your bed or nozzle, turning on any fans, or extruding any filament. This saves time and plastic. It's also very useful for development and testing. -## Octolapse was also designed to make great timelapses -* **Synchronize your timelapses** with OctoPrint's built in plugin, keeping all of your videos in one place. -* Choose between **fixed frame rate** or **fixed length rendering.** Octolapse will automatically adjust the frame rate to match any desired length. You can even set a minimum or maximum acceptable frame rate. -* Add **pre or post roll frames** (not videos yet, sorry) to your snapshot preventing an abrupt start or finish. -* Choose from several output formats including **MP4 (libxvid and H.264), AVI, FLV, VOB, GIF, and MPEG.** *More coming soon, suggestions welcome.* _Note: *OctoPrint 1.3.10 is required to synchronize some formats with the built-in timelapse plugin.*_ -* **Control the bitrate** of your video. -* Add a **Custom Watermark** to your video. -* Add **Text Overlays** to your video using replacement tokens. Control the position, color, font and more! -* Control the number of **rendering threads** to reduce rendering time. -## To make good timelapses, you need good snapshots -* Slow camera or low frame rate? Octolapse allows you to **set a snapshot delay** before taking a snapshot to allow your camera enough time to get a clear image. -* **Control your camera settings** when using [mjegstreamer](https://sourceforge.net/projects/mjpg-streamer/) including: contrast, brightness, focus, white balance, pan, tilt, zoom, and much more. You can apply your custom settings before each print. -* **Rotate, flip, and transpose your snapshots** -* Use custom camera scripts to **trigger a DSLR**, post-process images, configure your camera, turn lighting on and off, control **GPIO**, or practically anything you can think of! Five script types are available. -* Use **Multiple cameras** to create multiple timelapses. -* Send gcode to your printer when it's time to take a snapshot via the new **Gcode Camera Type**. Supports for the **M240** gcode! -## Simplified Setup and Better Compatibility -* Select your **slicer type** to simplify printer setup by making the setting names and units match your slicer! Cura, Simplify 3d and Slic3r PE are fully supported. -* Enhanced and **simplified error reporting** and informational panels. -* All settings include links to the **Octolapse Wiki**, so you can get to the documentation quickly if you have questions. -* Octolapse now works with **Themeify**. - -# Installation - -[Read the installation instructions.](https://github.com/FormerLurker/Octolapse/wiki/Installation) - -After installation, [checkout the project's wiki pages](https://github.com/FormerLurker/Octolapse/wiki). It never hurts to know more about what you're doing! - -# Usage -[Learn how to start your first print here.](https://github.com/FormerLurker/Octolapse/wiki/Usage) It's a good idea to read about the settings before using Octolapse. I also recommend you [watch this brief tutorial](https://youtu.be/sDyg9lMqMG8), which explains how to configure your printer profile for use with the new **High Quality** snapshot profile. - -# Known Printer Support - -Octolapse is still in Beta but has been confirmed to work on several printers: - -* Genuine Prusa - Mk2, Mk2S, Mk2 w Multi Material, Mk3 -* Anet A8 - Beta - User Submitted -* Anycube I3 Mega - Beta - User Submitted -* CR-10 - Beta - User Submitted -* Dagoma Neva - Beta - User Submitted -* Irapid Black - Beta - User Submitted -* Monoprice Maker Select v2/Wanhao Duplicator i3 - Beta - User Submitted - Requires OctoPrint 1.3.7rc3 or above - -Please note that some settings may need to be adjusted depending on your slicer settings. +
-## More Octolapses -

- - User Created Compilation + +### Smart Triggers + +The new smart triggers read your entire Gcode file before starting a print, giving them a lot more information to make better decisions. They automatically detect print features and prefer to take snapshots over infill, wipe towers, or interior perimeters, and they avoid taking snapshots (where possible) over exterior perimeters. They also try to start snapshots as close to the stabilization point as possible, reducing travel. This all adds up to improvements in quality and a reduction in print time. Plus, you will get to see a preview of your timelapse **before** your print starts. Octolapse will inform you of potential problems *and* will help you to fix them. If you don't like what you see, you can cancel the print and change your settings saving you time and effort. + +

+ + Smart Trigger Snapshot Plan Preview +
+ + Smart Trigger Snapshot Plan Preview -
- - A user generated compilation created by WildRose Builds. Support this channel and please subscribe! +
+ + +### Improved Interface + +Thanks to UX advice from [Derek73](https://github.com/derek73), the Octolapse tab has been greatly improved. Highlights include: a larger snapshot preview, shortcuts to the Octolapse settings pages, video and image file browsers, rendering progress, unfinished rendering recovery, new and improved informational panels, and a more intuitive design. + +
+ + Improved UX +
+ + Improved UX -
-

+
+ +### Import/Export/Download and Automatically Update Settings -

- - The Milennium Falcon +Octolapse now has its own [profile repository](https://github.com/FormerLurker/Octolapse-Profiles)! Access a library of pre-configured profiles and download, customize, and share your settings with the world! You can export and import individual profiles or all of your Octolapse settings. If newer settings are available from the repository, Octolapse will notify you and can automatically update any of the pre-configured profiles. + +

+ + Import Profiles from the Repository +
+ + Import Profiles from the Repository -
- - The Milennium Falcon +
+ +### Enhanced Rendering Capabilities + +Now you can see detailed rendering progress and retry or alter renderings that never finished. I've added beta support for the H.265 codec (most Raspberry Pis don't have the memory for this though). I added new rendering overlay tokens, text outlining, and a default font for simplified setup. + +
+ + Renderings In Process +
+ + Renderings in Process -
-

+
-

- - The Moon - Animated X Axis +### Browse Videos and Image Archives + +I've finally added native file browsers to Octolapse! Now you can download, sort, or delete timelapses, all without leaving the Octolapse tab. If you enable the new **archive images** feature, you can also download snapshot images or re-render them using different settings. I've even added the ability to upload images into Octolapse! + +

+ + Videos and Images Browser +
+ + Videos and Images Browser -
- - The Moon - Animated X Axis +
+ +### Better Camera Controls + +Octolapse already included the ability to control camera settings like focus, exposure, and white balance. In the new version, Octolapse detects your camera's capabilities and renders a control page dynamically. Octolapse even has enhanced custom control pages for some cameras (like the Raspberry Pi cameras and many Logitech models). You can now watch your image change in real time as you adjust the settings. You can even stabilize your extruder to make adjusting the focus a snap! **Requires mjpg-streamer. Not compatible with [*Octoprint Anywhere*](https://plugins.octoprint.org/plugins/anywhere/) or [*The Spaghetti Detective*](https://plugins.octoprint.org/plugins/thespaghettidetective/)**. + +
+ + Custom Image Controls +
+ + Custom Image Controls -
-

+
+ +### Integrated Help System + +Octolapse now provides documentation for every single setting, and it's all built-in. Get your questions answered quickly by clicking on the help links (blue question marks). Many error popups now also have a help button explaining what the problem is and how to fix it. I've also added [versioneer](https://github.com/warner/python-versioneer), including a link within the Octolapse tab that points directly to the release notes or commit specific to the version you have installed. + +
+ Help With Common Print Start Issues
+ Help for Common Print Start Issues +
+ +
+ Automatic Detection of Print Quality Issues, With Help
+ Automatic Detection of Print Quality Issues, With Help +
+ +
+ Integrated Help Links in the Octolapse Tab
+ Integrated Help Links on the Octolapse Tab
+ Integrated Help Links Within the Octolapse Profiles
+ Integrated Help Links Within Each Profile +
+ +### Improved Camera Scripts and Tests + +Before and After snapshot Gcode scripts allow you to run custom Gcode before and/or after every snapshot. A new *After Print Ends* script is now available. All bash/bat scripts now have test buttons, making it much easier to test your custom scripts. + +
+ Additional Camera Scripts and Tests
+ Additional Camera Scripts and Tests +
+ +### New @OCTOLAPSE Commands + +Now you can use Gcode to tell Octolapse when to take snapshots. You can prevent Octolapse from taking snapshots within your start/end Gcode with the new ```@OCTOLAPSE STOP-SNAPSHOTS``` and ```@OCTOLAPSE START-SNAPSHOTS``` commands. You can also use the new ```@OCTOLAPSE TAKE-SNAPSHOT``` command to trigger a snapshot when using one of the GCode triggers. + +### Alpha Support for Multi-Material/Multi-Extruder Printers + +Octolapse now supports per-extruder/material slicer settings and offsets. It's even compatible with the *Automatic Slicer Settings Detection* feature. This is an *Alpha* feature, so it is probably a bit rough since I don't actually own a multi-extruder printer. + +### Better Logging + +I've added a custom module based logging system that should help with debugging. You can also clear and download logs right from your *Logging* profile. Exceptions are now logged automatically. This enhancement is really for me, but I thought I'd mention it here. + +### More Efficient Parsing and Processing + +In order to make the new *Smart* triggers as fast as possible and to reduce the CPU load, I've created a new GCode parser and position processor entirely in C++. This has increased performance by several orders of magnitude, especially when using the new *Smart* triggers. + +### Faster Snapshots -

- - The obligatory benchy +Octolapse now captures snapshots more quickly and performs all image manipulations on a background thread. This reduces print time and improves quality. + +### G2/G3 (Arc) Support +Octolapse now supports Arc commands. Now you can use the [Arc Welder](https://github.com/FormerLurker/ArcWelderPlugin) plugin with Octolapse. + +## Support Octolapse Development + +Please consider supporting my work by becoming a [patron](https://www.patreon.com/join/FormerLurker), a [Github Sponsor](https://github.com/sponsors/FormerLurker), or by sending me beer money via [PayPal](https://paypal.me/formerlurker). Almost all of the donations go towards offsetting the cost of development, which is substantial. Plus it always makes my day! If you cannot afford to leave a tip or just don't want to, that is fine too! Octolapse is [free and open source](https://raw.githubusercontent.com/FormerLurker/Octolapse/master/LICENSE) after all. + +## More Octolapses + +

+ + A user generated compilation created by WildRose Builds +
+ + A user-generated compilation created by WildRose Builds. Support this channel, and please subscribe! -
- - The obligatory benchy +
+
+
+ + A timelapse of The The 'Fillenium Malcon' +
+ + The 'Fillenium Malcon' -
-

+
+
+
+ + A Timelapse of the Moon +
+ + The Moon - Animated Stabilization + +
+
+
+ + A Timelapse of Benchy +
+ + The Obligatory Benchy + +
## History of Octolapse -I got the idea for Octolapse when I attempted to manually make a [stabilized timelapse](https://youtu.be/xZlP4vpAKNc) by hand editing my GCode files. To accomplish this I used the excellent and simple [GCode System Commands](https://github.com/kantlivelong/OctoPrint-GCodesystemCommands) plugin. The timelapse worked great, but it required a lot of effort which I didn't want to put in every time. I received several requests for instructions on how to create a stabilized timelapse, so I decided to give plugin development a go. I've never done one before (or programmed python or knockout or anything open source), but figured I could contribute something good to the community. This is my "thank you" to all of the makers out there who have contributed your time and effort! -## Report Problems -If you think you have found a bug in Octolapse, please create an issue on the official github.com page [here](https://github.com/FormerLurker/Octolapse/issues/new). In order to have your issue handled properly and quickly, please completely +I got the idea for Octolapse when I attempted to manually make a [stabilized timelapse](https://youtu.be/xZlP4vpAKNc) by hand-editing my GCode files. To accomplish this I used the excellent and simple [GCode System Commands](https://github.com/kantlivelong/OctoPrint-GCodesystemCommands) plugin. The timelapse worked great, but it required a lot of effort which I didn't want to put in every time. I received several requests for instructions on how to create a stabilized timelapse, so I decided to give plugin development a go. At that time I had never written any OctoPrint plugins (or programmed Python or Knockout or anything open source), but figured I could contribute something good to the community. + +On Jauary 20, 2018, I released the alpha version of Octolapse, and on March 24, 2018, I released the plugin on the [OctoPrint Plugin Repository](https://plugins.octoprint.org). Octolapse grew up pretty quickly, and on November 15, 2018, I released V0.3.4. Over a year and a half and 640+ commits later, I finally completed V0.4.0, which is a major rewrite of the software and includes all of the features discussed above. It took a long time to get here. + +Octolapse is my "thank you" to all of the makers out there who have contributed time and effort to this hobby. I hope that Octolapse can spread information about the 3D printing hobby by attracting a few new users. + +## Report Problems and Suggest Improvements + +If you think you have found a bug in Octolapse, please [see this guide for reporting issues](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Reporting-An-Issue). Maybe you have an idea for a cool new feature? Find out how to let me know about your idea [here](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Request-A-New-Feature). + +# License -## License View the [Octolapse license](https://github.com/FormerLurker/Octolapse/blob/master/LICENSE).
-_Copyright (C) 2017 Brad Hochgesang - FormerLurker@pm.me_ +_Copyright (C) 2023 Brad Hochgesang - FormerLurker@pm.me_ diff --git a/builds/build_2025_06_19.zip b/builds/build_2025_06_19.zip new file mode 100644 index 00000000..56cb745f Binary files /dev/null and b/builds/build_2025_06_19.zip differ diff --git a/builds/readme b/builds/readme new file mode 100644 index 00000000..f997190e --- /dev/null +++ b/builds/readme @@ -0,0 +1 @@ +here we create some builds diff --git a/octoprint_octolapse/__init__.py b/octoprint_octolapse/__init__.py index d0522b49..da92c021 100644 --- a/octoprint_octolapse/__init__.py +++ b/octoprint_octolapse/__init__.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -23,10 +23,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -# uncomment to enable faulthandler. Also need to uncomment faulthandler in plugin_requries in setup.py -#import faulthandler -#faulthandler.enable() -# enable faulthandler for c extension # Create the root logger. Note that it MUST be created before any imports that use the # plugin_octolapse.log.LoggingConfigurator, since it is a singleton and we want to be # the first to create it so that the name is correct. @@ -36,62 +32,87 @@ # so that we can logger = logging_configurator.get_logger("__init__") # be sure to configure the logger after we import all of our modules -from werkzeug.utils import secure_filename +import tornado +import errno +from octoprint.server import util, app +from octoprint.server.util.tornado import LargeResponseHandler, RequestlessExceptionLoggingMixin, CorsSupportMixin import sys import base64 import json import os -import shutil -import flask -import math +# remove unused imports +#from flask import request, send_file, jsonify, Response, stream_with_context, send_from_directory, current_app, Flask +from flask import request, jsonify import threading import uuid -import six +# remove unused imports +# import six import time -from six.moves import queue +# Remove python 2 support +# from six.moves import queue +import queue as queue from tempfile import mkdtemp -# import python 3 specific modules -if (sys.version_info) > (3,0): - import faulthandler - faulthandler.enable() - from distutils.version import LooseVersion from io import BytesIO - import octoprint.plugin -import octoprint.server import octoprint.filemanager from octoprint.events import Events -from octoprint.server import admin_permission from octoprint.server.util.flask import restricted_access - -import octoprint_octolapse.stabilization_preprocessing +# remove unused import +# import octoprint_octolapse.stabilization_preprocessing import octoprint_octolapse.camera as camera import octoprint_octolapse.render as render import octoprint_octolapse.snapshot as snapshot import octoprint_octolapse.utility as utility -from octoprint_octolapse.position import Position -from octoprint_octolapse.gcode import SnapshotGcodeGenerator -from octoprint_octolapse.gcode_parser import Commands -from octoprint_octolapse.render import TimelapseRenderJob, RenderingCallbackArgs +import octoprint_octolapse.error_messages as error_messages +from octoprint_octolapse.migration import migrate_files, get_version_from_settings_index +# remove unused import +# from octoprint_octolapse.position import Position +from octoprint_octolapse.stabilization_gcode import SnapshotGcodeGenerator +from octoprint_octolapse.gcode_commands import Commands +# remove unused import +# from octoprint_octolapse.render import TimelapseRenderJob, RenderingCallbackArgs from octoprint_octolapse.settings import OctolapseSettings, PrinterProfile, StabilizationProfile, TriggerProfile, \ - CameraProfile, RenderingProfile, DebugProfile, SlicerSettings, CuraSettings, OtherSlicerSettings, \ - Simplify3dSettings, Slic3rPeSettings, SettingsJsonEncoder, MjpgStreamer -from octoprint_octolapse.timelapse import Timelapse, TimelapseState + CameraProfile, RenderingProfile, LoggingProfile, SlicerSettings, CuraSettings, OtherSlicerSettings, \ + Simplify3dSettings, Slic3rPeSettings, SettingsJsonEncoder, MjpgStreamer, MainSettings +from octoprint_octolapse.timelapse import Timelapse, TimelapseState, TimelapseStartException from octoprint_octolapse.stabilization_preprocessing import StabilizationPreprocessingThread from octoprint_octolapse.messenger_worker import MessengerWorker, PluginMessage from octoprint_octolapse.settings_external import ExternalSettings, ExternalSettingsError - -try: - # noinspection PyCompatibility - from urlparse import urlparse -except ImportError: - # noinspection PyUnresolvedReferences - from urllib.parse import urlparse - +from octoprint_octolapse.render import RenderError, RenderingProcessor, RenderingCallbackArgs, RenderJobInfo +#import octoprint_octolapse_setuptools as octoprint_octolapse_setuptools +#import octoprint_octolapse_setuptools.github_release as github_release +# remove python 2 compatibility +#try: +# # noinspection PyCompatibility +# from urlparse import urlparse as urlparse +#except ImportError: +# # noinspection PyUnresolvedReferences +# from urllib.parse import urlparse as urlparse +from urllib.parse import urlparse as urlparse # configure all imported loggers logging_configurator.configure_loggers() + +def configure_debug_mode(): + # Conditional imports for OctoPrint or Python debug mode + # detect debug mode + if hasattr(sys, 'gettotalrefcount') or "--debug" in sys.argv: + logger.info("Debug mode detected.") + # import python 3 specific debug modules + logger.info("Python %s detected.", sys.version) + if sys.version_info > (3, 0): + import faulthandler + faulthandler.enable() + logger.info("Faulthandler enabled.") + else: + logger.info("Release mode detected.") + + +# configure debug mode +configure_debug_mode() + + class OctolapsePlugin( octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, @@ -106,11 +127,19 @@ class OctolapsePlugin( PREPROCESSING_CANCEL_TIMEOUT_SECONDS = 5 PREPROCESSING_NOTIFICATION_PERIOD_SECONDS = 1 + if LooseVersion(octoprint.server.VERSION) >= LooseVersion("1.4"): + import octoprint.access.permissions as permissions + admin_permission = permissions.Permissions.ADMIN + else: + import flask_principal + admin_permission = flask_principal.Permission(flask_principal.RoleNeed('admin')) + def __init__(self): + super(OctolapsePlugin, self).__init__() self._octolapse_settings = None # type: OctolapseSettings self._timelapse = None # type: Timelapse self.gcode_preprocessor = None - self._preprocessing_progress_queue = None + self._stabilization_preprocessor_thread = None self._preprocessing_cancel_event = threading.Event() self._plugin_message_queue = queue.Queue() @@ -121,510 +150,767 @@ def __init__(self): self.saved_timelapse_settings = None self.saved_snapshot_plans = None self.saved_parsed_command = None + self.saved_preprocessing_quality_issues = "" + self.saved_missed_snapshots = 0 + self.snapshot_plan_preview_autoclose = False + self.snapshot_plan_preview_close_time = 0 + self.autoclose_snapshot_preview_thread_lock = threading.Lock() # automatic update thread self.automatic_update_thread = None self.automatic_updates_notification_thread = None self.automatic_update_lock = threading.RLock() self.automatic_update_cancel = threading.Event() # contains a list of all profiles with available updates - self.automatic_updates_available = None # holds a list of all available profiles on the server for the current version self.available_profiles = None + # rendering processor and task queue + self._rendering_task_queue = queue.Queue(maxsize=0) + self._rendering_processor = None def get_sorting_key(self, context=None): return 1 - def get_current_debug_profile_function(self): + def get_current_logging_profile_function(self): if self._octolapse_settings is not None: - return self._octolapse_settings.profiles.current_debug_profile + return self._octolapse_settings.profiles.current_logging_profile def get_current_octolapse_settings(self): # returns a guaranteed up-to-date settings object return self._octolapse_settings - # Blueprint Plugin Mixin Requests - @octoprint.plugin.BlueprintPlugin.route("/downloadTimelapse/", methods=["GET"]) + @octoprint.plugin.BlueprintPlugin.route("/clearLog", methods=["POST"]) @restricted_access - @admin_permission.require(403) - def download_timelapse_request(self, filename): - """Restricted access function to download a timelapse""" - return self.get_download_file_response(self.get_timelapse_folder() + filename, filename) - - @octoprint.plugin.BlueprintPlugin.route("/snapshot", methods=["GET"]) - def snapshot_request(self): - file_type = flask.request.args.get('file_type') - guid = flask.request.args.get('camera_guid') + def clear_log_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + clear_all = request_values["clear_all"] + if clear_all: + logger.info("Clearing all log files.") + else: + logger.info("Rolling over most recent log.") + + logging_configurator.do_rollover(clear_all=clear_all) + return jsonify({"success": True}) + + @staticmethod + def file_name_allowed(name): + # Don't allow any subdirectory access + if name != os.path.basename(name): + return False + return True + + def get_file_directory(self, file_type, name): + extension = utility.get_extension_from_filename(name) + directory = None + allowed = False + if file_type == 'snapshot_archive': + directory = self._octolapse_settings.main_settings.get_snapshot_archive_directory( + self.get_plugin_data_folder() + ) + if extension in RenderingProfile.get_archive_formats(): + allowed = True + else: + has_directory = False + if file_type == 'timelapse_octolapse': + has_directory = True + directory = self._octolapse_settings.main_settings.get_timelapse_directory( + self.get_octoprint_timelapse_location() + ) + elif file_type == 'timelapse_octoprint': + has_directory = True + directory = self.get_octoprint_timelapse_location() + + if has_directory and extension in set(self.get_timelapse_extensions()): + allowed = True + if not allowed: + return None + return directory + + # Callback handler for /getSnapshot + # uses the OctolapseLargeResponseHandler + def get_snapshot_request(self, request_handler): + download_file_path = None + # get the args + file_type = request_handler.get_query_arguments('file_type')[0] + guid = request_handler.get_query_arguments('camera_guid')[0] + + if self._timelapse is not None: + temporary_folder = self._timelapse.get_current_temporary_folder() + else: + temporary_folder = self._octolapse_settings.main_settings.get_temporary_directory( + self.get_plugin_data_folder() + ) """Public access function to get the latest snapshot image""" if file_type == 'snapshot': # get the latest snapshot image - mime_type = 'image/jpeg' filename = utility.get_latest_snapshot_download_path( - self.get_plugin_data_folder(), guid, self._basefolder) - if not os.path.isfile(filename): - # we haven't captured any images, return the built in png. - mime_type = 'image/png' - filename = utility.get_no_snapshot_image_download_path( - self._basefolder) + temporary_folder, guid, self._basefolder) + # if not os.path.isfile(filename): + # # we haven't captured any images, return the built in png. + # filename = utility.get_no_snapshot_image_download_path( + # self._basefolder) elif file_type == 'thumbnail': # get the latest snapshot image - mime_type = 'image/jpeg' filename = utility.get_latest_snapshot_thumbnail_download_path( - self.get_plugin_data_folder(), guid, self._basefolder) - if not os.path.isfile(filename): - # we haven't captured any images, return the built in png. - mime_type = 'image/png' - filename = utility.get_no_snapshot_image_download_path( - self._basefolder) + temporary_folder, guid, self._basefolder) + # if not os.path.isfile(filename): + # # we haven't captured any images, return the built in png. + # filename = utility.get_no_snapshot_image_download_path( + # self._basefolder) else: # we don't recognize the snapshot type - mime_type = 'image/png' filename = utility.get_error_image_download_path(self._basefolder) - # not getting the latest image - return flask.send_file(filename, mimetype=mime_type, cache_timeout=-1) + if not os.path.isfile(filename): + raise tornado.web.HTTPError(404) + return filename - @octoprint.plugin.BlueprintPlugin.route("/downloadSettings", methods=["GET"]) - @restricted_access - @admin_permission.require(403) - def download_settings_request(self): - return self.get_download_file_response(self.get_settings_file_path(), "Settings.json") + # Callback Handler for /downloadFile + # uses the OctolapseLargeResponseHandler + def download_file_request(self, request_handler): - @octoprint.plugin.BlueprintPlugin.route("/downloadProfile", methods=["GET"]) - @restricted_access - @admin_permission.require(403) - def download_profile_request(self): - # get the parameters - profile_type = flask.request.args["profile_type"] - guid = flask.request.args["guid"] - # get the profile settings - profile_json = self._octolapse_settings.get_profile_export_json(profile_type, guid) - - # create a temp file - temp_directory = mkdtemp() - file_path = os.path.join(temp_directory,"profile_setting_json.json") - # see if the filename is valid - def delete_temp_path(file_path, temp_directory): + def clean_temp_folder(file_path=None, file_directory=None): # delete the temp file and directory - os.unlink(file_path) - shutil.rmtree(temp_directory) - - with open(file_path, "w") as settings_file: - settings_file.write(profile_json) - # create the download file response - download_file_response = self.get_download_file_response( - file_path, - "{0}_Profile.json".format(profile_type), - on_complete_callback=delete_temp_path, - on_complete_additional_args=temp_directory + if file_path: + os.unlink(file_path) + if file_directory and os.path.isdir(file_directory): + if not os.listdir(file_directory): + utility.rmtree(file_directory) + + download_file_path = None + # get the args + file_type = request_handler.get_query_arguments('type')[0] + if file_type == 'profile': + # get the parameters + profile_type = request_handler.get_query_arguments("profile_type")[0] + guid = request_handler.get_query_arguments("guid")[0] + # get the profile settings + profile_json = self._octolapse_settings.get_profile_export_json(profile_type, guid) + + # create a temp file + temp_directory = mkdtemp() + temp_file_path = os.path.join(temp_directory, "profile_setting_json.json") + + # write the settings file + with open(temp_file_path, "w") as settings_file: + settings_file.write(profile_json) + + request_handler.after_request_internal = clean_temp_folder + request_handler.after_request_internal_args = { + 'file_path': temp_file_path, + 'file_directory': temp_directory + } + # set the download file name and full path + request_handler.download_file_name = "{0}_Profile.json".format(profile_type) + full_path = temp_file_path + elif file_type == 'log': + full_path = self.get_log_file_path() + elif file_type == 'settings': + full_path = self.get_settings_file_path() + elif file_type == 'failed_rendering': + job_guid = request_handler.get_query_arguments("job_guid")[0] + camera_guid = request_handler.get_query_arguments("camera_guid")[0] + + temp_directory = self._octolapse_settings.main_settings.get_temporary_directory( + self.get_plugin_data_folder() ) - return download_file_response + temp_archive_directory = utility.get_temporary_archive_directory(temp_directory) + temp_archive_path = utility.get_temporary_archive_path(temp_directory) + + file_name = self._rendering_processor.archive_unfinished_job( + temp_directory, + job_guid, + camera_guid, + temp_archive_path, + is_download=True + ) + if file_name: + request_handler.download_file_name = file_name + + request_handler.after_request_internal = clean_temp_folder + request_handler.after_request_internal_args = { + 'file_path': temp_archive_path, + 'file_directory': temp_archive_directory + } + full_path = temp_archive_path + elif file_type in ['timelapse_octolapse', 'snapshot_archive', 'timelapse_octoprint']: + #file_name = utility.unquote(request_handler.get_query_arguments('name')[0]) + file_name = request_handler.get_query_arguments('name')[0] + # Don't allow any subdirectory access + if not OctolapsePlugin.file_name_allowed(file_name): + raise tornado.web.HTTPError(500) + + directory = self.get_file_directory(file_type, file_name) + if not directory: + raise tornado.web.HTTPError(500) + + # make sure the file exists + full_path = os.path.join(directory, file_name) + + if not os.path.isfile(full_path): + raise tornado.web.HTTPError(404) + return full_path + + @octoprint.plugin.BlueprintPlugin.route("/deleteFile", methods=["POST"]) + @restricted_access + def delete_file_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + # get the parameters + request_values = request.get_json() + file_type = request_values["type"] + file_name = utility.unquote(request_values["id"]) + file_size = request_values["size"] + client_id = request_values["client_id"] + + directory = self.get_file_directory(file_type, file_name) + if not directory: + return jsonify({ + 'success': False, + 'error': 'The requested file type is not allowed.' + }), 403 + + if not OctolapsePlugin.file_name_allowed(file_name): + return jsonify({ + 'success': False, + 'error': 'The requested file type is not allowed.' + }), 403 + + # get the full file path + full_path = os.path.join(directory, file_name) + # make sure the file exists + if not os.path.isfile(full_path): + return jsonify({ + 'success': False, + 'error': 'The requested file does not exist.' + }), 404 + # remove the file + utility.remove(full_path) + file_info = { + "type": file_type, + "name": file_name, + "size": file_size + } + self.send_files_changed_message(file_info, 'removed', client_id) + return jsonify({ + 'success': True + }) @octoprint.plugin.BlueprintPlugin.route("/stopTimelapse", methods=["POST"]) @restricted_access - @admin_permission.require(403) def stop_timelapse_request(self): - self._timelapse.stop_snapshots() - return json.dumps({'success': True}), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + self._timelapse.stop_snapshots() + return jsonify({'success': True}) @octoprint.plugin.BlueprintPlugin.route("/previewStabilization", methods=["POST"]) @restricted_access - @admin_permission.require(403) def preview_stabilization(self): - # make sure we aren't printing - if not self._printer.is_ready(): - error = "Cannot preview the stabilization because the printer is either printing or is a non-operational " \ - "state, or is disconnected. Check the 'State' of your printer on the left side of the screen " \ - "and make sure it is connected and operational" - logger.error(error) - return json.dumps({'success': False, 'error': error}), 200, {'ContentType': 'application/json'} - with self._printer.job_on_hold(): - # get the current stabilization - current_stabilization = self._octolapse_settings.profiles.current_stabilization() - assert(isinstance(current_stabilization, StabilizationProfile)) - # get the current trigger - current_trigger = self._octolapse_settings.profiles.current_trigger() - assert (isinstance(current_trigger, TriggerProfile)) - # get the current printer - current_printer = self._octolapse_settings.profiles.current_printer() - if current_printer is None: - error = "Cannot preview the stabilization because no printer profile is selected. Please select " \ - "and configure a printer profile and try again." + with OctolapsePlugin.admin_permission.require(http_exception=403): + # make sure we aren't printing + if not self._printer.is_ready(): + error = "Cannot preview the stabilization because the printer is either printing or is a non-operational " \ + "state, or is disconnected. Check the 'State' of your printer on the left side of the screen " \ + "and make sure it is connected and operational" logger.error(error) - return json.dumps({'success': False, 'error': error}), 200, {'ContentType': 'application/json'} - assert (isinstance(current_printer, PrinterProfile)) - # if both the X and Y axis are disabled, or the trigger is a smart trigger with snap to print enabled, return - # an error. - if ( - current_stabilization.x_type == StabilizationProfile.STABILIZATION_AXIS_TYPE_DISABLED and - current_stabilization.y_type == StabilizationProfile.STABILIZATION_AXIS_TYPE_DISABLED - ): - error = "Cannot preview the stabilization because both the X and Y axis are disabled. Please enable at " \ - "least one stabilized axis and try again." - logger.error(error) - return json.dumps({'success': False, 'error': error}), 200, {'ContentType': 'application/json'} - - if ( - current_trigger.trigger_type == TriggerProfile.TRIGGER_TYPE_SMART_LAYER and - current_trigger.smart_layer_snap_to_print - ): - error = "Cannot preview the stabilization when using a snap-to-print trigger." - logger.error(error) - return json.dumps({'success': False, 'error': error}), 200, {'ContentType': 'application/json'} - - gcode_array = Commands.string_to_gcode_array(current_printer.home_axis_gcode) - if len(gcode_array) < 1: - error = "Cannot preview stabilization. The home axis gcode script in your printer profile does not " \ - "contain any valid gcodes. Please add a script to home your printer to the printer profile and " \ - "try again." - logger.error(error) - return json.dumps({'success': False, 'error': error}), 200, {'ContentType': 'application/json'} - + return jsonify({'success': False, 'error': error}) + with self._printer.job_on_hold(): + # get the current stabilization + current_stabilization = self._octolapse_settings.profiles.current_stabilization() + assert(isinstance(current_stabilization, StabilizationProfile)) + # get the current trigger + current_trigger = self._octolapse_settings.profiles.current_trigger() + assert (isinstance(current_trigger, TriggerProfile)) + # get the current printer + current_printer = self._octolapse_settings.profiles.current_printer() + if current_printer is None: + error = "Cannot preview the stabilization because no printer profile is selected. Please select " \ + "and configure a printer profile and try again." + logger.error(error) + return jsonify({'success': False, 'error': error}), 200, {'ContentType': 'application/json'} + assert (isinstance(current_printer, PrinterProfile)) + # if both the X and Y axis are disabled, or the trigger is a smart trigger with snap to print enabled, return + # an error. + if ( + current_stabilization.x_type == StabilizationProfile.STABILIZATION_AXIS_TYPE_DISABLED and + current_stabilization.y_type == StabilizationProfile.STABILIZATION_AXIS_TYPE_DISABLED + ): + error = "Cannot preview the stabilization because both the X and Y axis are disabled. Please enable at " \ + "least one stabilized axis and try again." + logger.error(error) + return jsonify({'success': False, 'error': error}) - overridable_printer_profile_settings = current_printer.get_overridable_profile_settings( - self.get_octoprint_g90_influences_extruder(), - self.get_octoprint_printer_profile() - ) - # create a snapshot gcode generator object - gcode_generator = SnapshotGcodeGenerator( - self._octolapse_settings, overridable_printer_profile_settings - ) + if ( + current_trigger.trigger_type == TriggerProfile.TRIGGER_TYPE_SMART and + current_trigger.smart_layer_trigger_type == TriggerProfile.SMART_TRIGGER_TYPE_SNAP_TO_PRINT + ): + error = "Cannot preview the stabilization when using a snap-to-print trigger." + logger.error(error) + return jsonify({'success': False, 'error': error}) + + gcode_array = Commands.string_to_gcode_array(current_printer.home_axis_gcode) + if len(gcode_array) < 1: + error = "Cannot preview stabilization. The home axis gcode script in your printer profile does not " \ + "contain any valid gcodes. Please add a script to home your printer to the printer profile and " \ + "try again." + logger.error(error) + return jsonify({'success': False, 'error': error}) + + + overridable_printer_profile_settings = current_printer.get_overridable_profile_settings( + self.get_octoprint_g90_influences_extruder(), + self.get_octoprint_printer_profile() + ) + # create a snapshot gcode generator object + gcode_generator = SnapshotGcodeGenerator( + self._octolapse_settings, overridable_printer_profile_settings + ) - # get the stabilization point - stabilization_point = gcode_generator.get_snapshot_position(None, None) - if stabilization_point is None and stabilization_point["x"] is None and stabilization_point["y"] is None: - error = "No stabilization points were returned. Cannot preview stabilization." - logger.error(error) - return json.dumps({'success': False, 'error': error}), 200, {'ContentType': 'application/json'} - - # get the movement feedrate from the Octoprint printer profile, or use 6000 as a default - - default_feedrate = 6000 - - feedrate_x = default_feedrate - feedrate_y = default_feedrate - octoprint_printer_profile = self.get_octoprint_printer_profile() - axes = octoprint_printer_profile.get("axes", None) - if axes is not None: - x_axis = axes.get("x", {"speed": default_feedrate}) - feedrate_x = x_axis.get("speed", default_feedrate) - y_axis = axes.get("y", {"speed": default_feedrate}) - feedrate_y = y_axis.get("speed", default_feedrate) - - feedrate = min(feedrate_x, feedrate_y) - # create the gcode necessary to move the extuder to the stabilization position and add it to the gocde array - gcode_array.append( - SnapshotGcodeGenerator.get_gcode_travel(stabilization_point["x"], stabilization_point["y"], feedrate) - ) + # get the stabilization point + stabilization_point = gcode_generator.get_snapshot_position(None, None) + if stabilization_point is None and stabilization_point["x"] is None and stabilization_point["y"] is None: + error = "No stabilization points were returned. Cannot preview stabilization." + logger.error(error) + return jsonify({'success': False, 'error': error}) + + # get the movement feedrate from the Octoprint printer profile, or use 6000 as a default + + default_feedrate = 6000 + + feedrate_x = default_feedrate + feedrate_y = default_feedrate + octoprint_printer_profile = self.get_octoprint_printer_profile() + axes = octoprint_printer_profile.get("axes", None) + if axes is not None: + x_axis = axes.get("x", {"speed": default_feedrate}) + feedrate_x = x_axis.get("speed", default_feedrate) + y_axis = axes.get("y", {"speed": default_feedrate}) + feedrate_y = y_axis.get("speed", default_feedrate) + + feedrate = min(feedrate_x, feedrate_y) + # create the gcode necessary to move the extruder to the stabilization position and add it to the gcode array + gcode_array.append( + SnapshotGcodeGenerator.get_gcode_travel(stabilization_point["x"], stabilization_point["y"], feedrate) + ) - # send the gcode to the printer - self._printer.commands(gcode_array, tags={"preview-stabilization"}) - return json.dumps({'success': True}), 200, {'ContentType': 'application/json'} + # send the gcode to the printer + self._printer.commands(gcode_array, tags={"preview-stabilization"}) + return jsonify({'success': True}) @octoprint.plugin.BlueprintPlugin.route("/saveMainSettings", methods=["POST"]) @restricted_access - @admin_permission.require(403) def save_main_settings_request(self): - request_values = flask.request.get_json() - self._octolapse_settings.main_settings.is_octolapse_enabled = request_values["is_octolapse_enabled"] - self._octolapse_settings.main_settings.auto_reload_latest_snapshot = ( - request_values["auto_reload_latest_snapshot"] - ) - self._octolapse_settings.main_settings.auto_reload_frames = request_values["auto_reload_frames"] - self._octolapse_settings.main_settings.show_navbar_icon = request_values["show_navbar_icon"] - self._octolapse_settings.main_settings.show_navbar_when_not_printing = ( - request_values["show_navbar_when_not_printing"] - ) - self._octolapse_settings.main_settings.show_printer_state_changes = ( - request_values["show_printer_state_changes"] - ) - self._octolapse_settings.main_settings.show_position_changes = request_values["show_position_changes"] - self._octolapse_settings.main_settings.show_extruder_state_changes = ( - request_values["show_extruder_state_changes"] - ) - self._octolapse_settings.main_settings.show_trigger_state_changes = request_values["show_trigger_state_changes"] - self._octolapse_settings.main_settings.show_snapshot_plan_information = request_values["show_snapshot_plan_information"] - self._octolapse_settings.main_settings.cancel_print_on_startup_error = ( - request_values["cancel_print_on_startup_error"] - ) - self._octolapse_settings.main_settings.preview_snapshot_plans = ( - request_values["preview_snapshot_plans"] - ) - self._octolapse_settings.main_settings.automatic_updates_enabled = ( - request_values["automatic_updates_enabled"] - ) - self._octolapse_settings.main_settings.automatic_update_interval_days = ( - request_values["automatic_update_interval_days"] - ) + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + client_id = request_values["client_id"] - # save the updated settings to a file. - self.save_settings() + # make sure to test the provided directories first + main_settings_test = MainSettings(__version__, __git_version__) + main_settings_test.update(request_values) + success, results = main_settings_test.test_directories( + self.get_plugin_data_folder(), + self.get_octoprint_timelapse_location() + ) + if not success: + data = { + 'success': False, + 'error': "The provided directories did not pass testing. Could not save settings." + } + else: + self._octolapse_settings.main_settings.update(request_values) + # save the updated settings to a file. + self.save_settings() + # inform the rendering processor of the temporary directory, since it may have changed + success, results = self._rendering_processor.update_directories() + # inform any clients who are listening that they need to update their state. + self.send_state_changed_message(None, client_id) + if success: + if ( + results["temporary_directory_changed"] or + results["snapshot_archive_directory_changed"] or + results["timelapse_directory_changed"] + ): + self.send_directories_changed_message(results) - self.send_state_loaded_message() - data = {'success': True} - return json.dumps(data), 200, {'ContentType': 'application/json'} + data = { + "success": True + } + else: + data = { + 'success': False, + 'error': "The provided directories did not pass testing. Could not save settings." + } + + return jsonify(data) @octoprint.plugin.BlueprintPlugin.route("/setEnabled", methods=["POST"]) @restricted_access - @admin_permission.require(403) def set_enabled(self): - request_values = flask.request.get_json() - enable_octolapse = request_values["is_octolapse_enabled"] + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + client_id = request_values["client_id"] + enable_octolapse = request_values["is_octolapse_enabled"] + if ( + self._timelapse is not None and + self._timelapse.is_timelapse_active() and + self._octolapse_settings.main_settings.is_octolapse_enabled and + not enable_octolapse + ): + self.send_plugin_message( + "disabled-running", + "Octolapse will remain active until the current print ends." + " If you wish to stop the active timelapse, click 'Stop Timelapse'." + ) + # save the updated settings to a file. + self._octolapse_settings.main_settings.is_octolapse_enabled = enable_octolapse + self.save_settings() + self.send_state_changed_message(None, client_id) + data = { + 'success': True, + 'enabled': enable_octolapse + } + return jsonify(data) - if ( - self._timelapse is not None and - self._timelapse.is_timelapse_active() and - self._octolapse_settings.main_settings.is_octolapse_enabled and - not enable_octolapse - ): - self.send_plugin_message( - "disabled-running", - "Octolapse will remain active until the current print ends." - " If you wish to stop the active timelapse, click 'Stop Timelapse'." - ) + @octoprint.plugin.BlueprintPlugin.route("/setTestMode", methods=["POST"]) + @restricted_access + def set_test_mode_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + client_id = request_values["client_id"] + test_mode_enabled = request_values["test_mode_enabled"] + if ( + self._timelapse is not None and + self._timelapse.is_timelapse_active() + ): + self.send_plugin_message( + "test-mode-changed-running", + "Test mode has been changed, but Octolapse is currently active. The new setting will take effect" + " after the current print ends" + ) + # save the updated settings to a file. + self._octolapse_settings.main_settings.test_mode_enabled = test_mode_enabled + self.save_settings() + self.send_state_changed_message(None, client_id) + data = { + 'success': True, + 'enabled': test_mode_enabled + } + return jsonify(data) - # save the updated settings to a file. - self._octolapse_settings.main_settings.is_octolapse_enabled = enable_octolapse - self.save_settings() - self.send_state_loaded_message() - data = {'success': True} - return json.dumps(data), 200, {'ContentType': 'application/json'} + @octoprint.plugin.BlueprintPlugin.route("/setPreviewSnapshotPlans", methods=["POST"]) + @restricted_access + def set_preview_snapshot_plans_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + client_id = request_values["client_id"] + preview_snapshot_plans_enabled = request_values["preview_snapshot_plans_enabled"] + # save the updated settings to a file. + self._octolapse_settings.main_settings.preview_snapshot_plans = preview_snapshot_plans_enabled + self.save_settings() + self.send_state_changed_message(None, client_id) + data = { + 'success': True, + 'enabled': preview_snapshot_plans_enabled + } + return jsonify(data) @octoprint.plugin.BlueprintPlugin.route("/toggleInfoPanel", methods=["POST"]) @restricted_access - @admin_permission.require(403) def toggle_info_panel(self): - request_values = flask.request.get_json() - panel_type = request_values["panel_type"] - - if panel_type == "show_printer_state_changes": - self._octolapse_settings.main_settings.show_printer_state_changes = ( - not self._octolapse_settings.main_settings.show_printer_state_changes - ) - elif panel_type == "show_position_changes": - self._octolapse_settings.main_settings.show_position_changes = ( - not self._octolapse_settings.main_settings.show_position_changes - ) - elif panel_type == "show_extruder_state_changes": - self._octolapse_settings.main_settings.show_extruder_state_changes = ( - not self._octolapse_settings.main_settings.show_extruder_state_changes - ) - elif panel_type == "show_trigger_state_changes": - self._octolapse_settings.main_settings.show_trigger_state_changes = ( - not self._octolapse_settings.main_settings.show_trigger_state_changes - ) - - elif panel_type == "show_snapshot_plan_information": - self._octolapse_settings.main_settings.show_snapshot_plan_information = ( - not self._octolapse_settings.main_settings.show_snapshot_plan_information - ) - - else: - return json.dumps({'error': "Unknown panel_type."}), 500, {'ContentType': 'application/json'} - - # save the updated settings to a file. - self.save_settings() - - self.send_state_loaded_message() - data = {'success': True} - return json.dumps(data), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + client_id = request_values["client_id"] + panel_type = request_values["panel_type"] + enabled = None + if panel_type == "show_printer_state_changes": + enabled = not self._octolapse_settings.main_settings.show_printer_state_changes + self._octolapse_settings.main_settings.show_printer_state_changes = enabled + elif panel_type == "show_position_changes": + enabled = not self._octolapse_settings.main_settings.show_position_changes + self._octolapse_settings.main_settings.show_position_changes = enabled + elif panel_type == "show_extruder_state_changes": + enabled = not self._octolapse_settings.main_settings.show_extruder_state_changes + self._octolapse_settings.main_settings.show_extruder_state_changes = enabled + elif panel_type == "show_trigger_state_changes": + enabled = not self._octolapse_settings.main_settings.show_trigger_state_changes + self._octolapse_settings.main_settings.show_trigger_state_changes = enabled + elif panel_type == "show_snapshot_plan_information": + enabled = not self._octolapse_settings.main_settings.show_snapshot_plan_information + self._octolapse_settings.main_settings.show_snapshot_plan_information = enabled + else: + return jsonify({'error': "Unknown panel_type."}), 500 - @octoprint.plugin.BlueprintPlugin.route("/loadMainSettings", methods=["POST"]) - def load_main_settings_request(self): - data = {'success': True} - data.update(self._octolapse_settings.main_settings.to_dict()) - return json.dumps(data), 200, {'ContentType': 'application/json'} + # save the updated settings to a file. + self.save_settings() - @octoprint.plugin.BlueprintPlugin.route("/loadState", methods=["POST"]) - def load_state_request(self): - # TODO: add a timer to wait for the settings to load! - if self._octolapse_settings is None: - raise Exception( - "Unable to load values from Octolapse.Settings, it hasn't been initialized yet. Please wait a few " - "minutes and try again. If the problem persists, please check plugin_octolapse.log for exceptions.") - # TODO: add a timer to wait for the timelapse to be initialized - if self._timelapse is None: - raise Exception( - "Unable to load values from Octolapse.Timelapse, it hasn't been initialized yet. Please wait a few " - "minutes and try again. If the problem persists, please check plugin_octolapse.log for exceptions.") - self.send_state_loaded_message() - return json.dumps({'success': True}), 200, {'ContentType': 'application/json'} + # inform any clients who are listening that they need to update their state. + self.send_state_changed_message(None, client_id) + data = { + 'success': True, + 'enabled': enabled + } + return jsonify(data) @octoprint.plugin.BlueprintPlugin.route("/addUpdateProfile", methods=["POST"]) @restricted_access - @admin_permission.require(403) def add_update_profile_request(self): - try: - request_values = flask.request.get_json() + with OctolapsePlugin.admin_permission.require(http_exception=403): + try: + request_values = request.get_json() + profile_type = request_values["profileType"] + profile = request_values["profile"] + client_id = request_values["client_id"] + updated_profile = self._octolapse_settings.profiles.add_update_profile(profile_type, profile) + # save the updated settings to a file. + self.save_settings() + self.send_settings_changed_message(client_id) + return updated_profile.to_json(), 200, {'ContentType': 'application/json'} + except Exception as e: + logger.exception("Error encountered in /addUpdateProfile.") + return jsonify({}), 500 + + @octoprint.plugin.BlueprintPlugin.route("/removeProfile", methods=["POST"]) + @restricted_access + def remove_profile_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() profile_type = request_values["profileType"] - profile = request_values["profile"] + guid = request_values["guid"] client_id = request_values["client_id"] - updated_profile = self._octolapse_settings.profiles.add_update_profile(profile_type, profile) + if not self._octolapse_settings.profiles.remove_profile(profile_type, guid): + return ( + jsonify({ + 'success': False, + 'error': "Cannot delete the default profile." + }) + ) # save the updated settings to a file. self.save_settings() self.send_settings_changed_message(client_id) - return updated_profile.to_json(), 200, {'ContentType': 'application/json'} - except Exception as e: - logger.exception("Error encountered in /addUpdateProfile.") - return {}, 500, {'ContentType': 'application/json'} - - @octoprint.plugin.BlueprintPlugin.route("/removeProfile", methods=["POST"]) - @restricted_access - @admin_permission.require(403) - def remove_profile_request(self): - request_values = flask.request.get_json() - profile_type = request_values["profileType"] - guid = request_values["guid"] - client_id = request_values["client_id"] - if not self._octolapse_settings.profiles.remove_profile(profile_type, guid): - return ( - json.dumps({'success': False, 'error': "Cannot delete the default profile."}), - 200, - {'ContentType': 'application/json'} - ) - # save the updated settings to a file. - self.save_settings() - self.send_settings_changed_message(client_id) - return json.dumps({'success': True}), 200, {'ContentType': 'application/json'} + return jsonify({'success': True}) @octoprint.plugin.BlueprintPlugin.route("/setCurrentProfile", methods=["POST"]) @restricted_access - @admin_permission.require(403) def set_current_profile_request(self): - request_values = flask.request.get_json() - profile_type = request_values["profileType"] - guid = request_values["guid"] - client_id = request_values["client_id"] - self._octolapse_settings.profiles.set_current_profile(profile_type, guid) - self.save_settings() - self.send_settings_changed_message(client_id) - return json.dumps({'success': True, 'guid': request_values["guid"]}), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + profile_type = request_values["profileType"] + guid = request_values["guid"] + client_id = request_values["client_id"] + self._octolapse_settings.profiles.set_current_profile(profile_type, guid) + self.save_settings() + self.send_settings_changed_message(client_id) + return jsonify({'success': True, 'guid': request_values["guid"]}) @octoprint.plugin.BlueprintPlugin.route("/setCurrentCameraProfile", methods=["POST"]) @restricted_access - @admin_permission.require(403) def set_current_camera_profile(self): - # this setting will only determine which profile will be the default within - # the snapshot preview if a new instance is loaded. Save the settings, but - # do not notify other clients - request_values = flask.request.get_json() - guid = request_values["guid"] - self._octolapse_settings.profiles.current_camera_profile_guid = guid - self.save_settings() - return json.dumps({'success': True, 'guid': request_values["guid"]}), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + # this setting will only determine which profile will be the default within + # the snapshot preview if a new instance is loaded. Save the settings, but + # do not notify other clients + request_values = request.get_json() + guid = request_values["guid"] + self._octolapse_settings.profiles.current_camera_profile_guid = guid + self.save_settings() + return jsonify({'success': True, 'guid': request_values["guid"]}) @octoprint.plugin.BlueprintPlugin.route("/restoreDefaults", methods=["POST"]) @restricted_access - @admin_permission.require(403) def restore_defaults_request(self): - request_values = flask.request.get_json() - client_id = request_values["client_id"] - try: - self.load_settings(force_defaults=True) - self.send_settings_changed_message(client_id) - self.check_for_updates(wait_for_lock=True, force_updates=True) - return self._octolapse_settings.to_json(), 200, {'ContentType': 'application/json'} - except Exception as e: - logger.exception("Failed to restore the defaults in /restoreDefaults.") - return json.dumps({'success': False}), 500, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + client_id = request_values["client_id"] + try: + self.load_settings(force_defaults=True) + self.send_settings_changed_message(client_id) + # TODO: Check for updates after settings restore, but send request from client. + return self._octolapse_settings.to_json(), 200, {'ContentType': 'application/json'} + except Exception as e: + logger.exception("Failed to restore the defaults in /restoreDefaults.") + return jsonify({'success': False}), 500 + + @octoprint.plugin.BlueprintPlugin.route("/loadMainSettings", methods=["POST"]) + def load_main_settings_request(self): + data = {'success': True} + data.update(self._octolapse_settings.main_settings.to_dict()) + return jsonify(data) + + @octoprint.plugin.BlueprintPlugin.route("/loadState", methods=["POST"]) + def load_state_request(self): + # TODO: add a timer to wait for the settings to load! + if self._octolapse_settings is None: + message = "Unable to load values from Octolapse.Settings, it hasn't been initialized yet. Please wait a few minutes and try again. If the problem persists, please check plugin_octolapse.log for exceptions." + logger.error(message) + return jsonify({"error": True, "error_message": message}), 500 + # TODO: add a timer to wait for the timelapse to be initialized + if self._timelapse is None: + message = "Unable to load values from Octolapse.Timelapse, it hasn't been initialized yet. Please wait a few minutes and try again. If the problem persists, please check plugin_octolapse.log for exceptions." + return jsonify({"error": True, "error_message": message}), 500 + data = self.get_state_request_dict() + return jsonify(data) + + def get_state_request_dict(self): + state_date = None + if self._timelapse is not None: + state_data = self._timelapse.to_state_dict(include_timelapse_start_data=True) + data = { + "main_settings": self._octolapse_settings.main_settings.to_dict(), + "status": self.get_status_dict(include_profiles=True), + "state": state_data, + "success": True + } + if self.preprocessing_job_guid and self.saved_snapshot_plans: + data["snapshot_plan_preview"] = self.get_snapshot_plan_preview_dict() + return data - @octoprint.plugin.BlueprintPlugin.route("/loadSettings", methods=["POST"]) + @octoprint.plugin.BlueprintPlugin.route("/loadSettingsAndState", methods=["POST"]) @restricted_access - @admin_permission.require(403) - def load_settings_request(self): - return self._octolapse_settings.to_json(), 200, {'ContentType': 'application/json'} + def load_settings_and_state_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + try: + if self._octolapse_settings is None: + message = "Unable to load values from Octolapse.Settings, it hasn't been initialized yet. Please " \ + "wait a few minutes and try again. If the problem persists, please check " \ + "plugin_octolapse.log for exceptions. " + logger.error(message) + return jsonify({"error": True, "error_message": message}), 500 + # TODO: add a timer to wait for the timelapse to be initialized + if self._timelapse is None: + message = "Unable to load values from Octolapse.Timelapse, it hasn't been initialized yet. Please" \ + " wait a few minutes and try again. If the problem persists, please check " \ + "plugin_octolapse.log for exceptions. " + return jsonify({"error": True, "error_message": message}), 500 + data = self.get_state_request_dict() + data["settings"] = self._octolapse_settings + return json.dumps(data, cls=SettingsJsonEncoder), 200, {'ContentType': 'application/json'} + except Exception as e: + logger.exception("An unexpected exception occurred while loading the settings and state.") + raise e @octoprint.plugin.BlueprintPlugin.route("/updateProfileFromServer", methods=["POST"]) @restricted_access - @admin_permission.require(403) def update_profile_from_server(self): - logger.info("Attempting to update the current profile from the server") - request_values = flask.request.get_json() - profile_type = request_values["type"] - key_values = request_values["key_values"] - profile_dict = request_values["profile"] - try: - server_profile_dict = ExternalSettings.get_profile(self._plugin_version, profile_type, key_values) - except ExternalSettingsError as e: - logger.exception(e) - return json.dumps( + with OctolapsePlugin.admin_permission.require(http_exception=403): + logger.info("Attempting to update the current profile from the server") + request_values = request.get_json() + profile_type = request_values["type"] + key_values = request_values["key_values"] + profile_dict = request_values["profile"] + try: + server_profile_dict = ExternalSettings.get_profile(self._plugin_version, profile_type, key_values) + except ExternalSettingsError as e: + logger.exception("An error occurred while retrieving profiles from the profile server.") + return jsonify( + { + "success": False, + "message": e.message + } + ), 500 + + # update the profile + updated_profile = self._octolapse_settings.profiles.update_from_server_profile( + profile_type, profile_dict, server_profile_dict + ) + return jsonify( { - "success": False, - "message": e.message + "success": True, + "profile_json": updated_profile.to_json() } - ), 500, {'ContentType': 'application/json'} + ) - # update the profile - updated_profile = self._octolapse_settings.profiles.update_from_server_profile( - profile_type, profile_dict, server_profile_dict - ) - return json.dumps( - { + @octoprint.plugin.BlueprintPlugin.route("/checkForProfileUpdates", methods=["POST"]) + @restricted_access + def check_for_profile_updates_request(self): + request_values = request.get_json() + is_silent_test = request_values["is_silent_test"] + # if this is a silent update, make sure automatic updates are enabled + if is_silent_test and not self._octolapse_settings.main_settings.automatic_updates_enabled: + return jsonify({ "success": True, - "profile_json": updated_profile.to_json() - } - ), 200, {'ContentType': 'application/json'} + "available_profile_count": 0 + }) + # ignore suppression only if this is not a silent test, which is performed when the client is done + # loading + ignore_suppression = not is_silent_test + with OctolapsePlugin.admin_permission.require(http_exception=403): + logger.info("Attempting to update all current profile from the server") + available_profiles = self.check_for_updates(ignore_suppression=ignore_suppression) + available_profile_count = 0 + if available_profiles: + # remove python 2 support + # for key, profile_type in six.iteritems(available_profiles): + for key, profile_type in available_profiles.items(): + available_profile_count += len(profile_type) + return jsonify({ + "success": True, + "available_profile_count": available_profile_count + }) @octoprint.plugin.BlueprintPlugin.route("/updateProfilesFromServer", methods=["POST"]) @restricted_access - @admin_permission.require(403) def update_profiles_from_server(self): - logger.info("Attempting to update all current profile from the server") - request_values = flask.request.get_json() - client_id = request_values["client_id"] - self.check_for_updates(False, True) - - with self.automatic_update_lock: - if not self.automatic_updates_available: - return json.dumps({ - "num_updated": 0 - }), 200, {'ContentType': 'application/json'} - num_profiles = 0 - for profile_type, updatable_profiles in six.iteritems(self.automatic_updates_available): - # now iterate through the printer profiles - for updatable_profile in updatable_profiles: - current_profile = self._octolapse_settings.profiles.get_profile( - profile_type, updatable_profile["guid"] - ) - try: - server_profile_dict = ExternalSettings.get_profile( - self._plugin_version, profile_type, - updatable_profile["key_values"] + with OctolapsePlugin.admin_permission.require(http_exception=403): + logger.info("Attempting to update all current profile from the server") + request_values = request.get_json() + client_id = request_values["client_id"] + profiles_to_update = self.check_for_updates(True) + + with self.automatic_update_lock: + if not profiles_to_update: + return jsonify({ + "num_updated": 0 + }) + num_profiles = 0 + # remove python 2 support + # for profile_type, updatable_profiles in six.iteritems(profiles_to_update): + for profile_type, updatable_profiles in profiles_to_update.items(): + # now iterate through the printer profiles + for updatable_profile in updatable_profiles: + current_profile = self._octolapse_settings.profiles.get_profile( + profile_type, updatable_profile["guid"] ) - except ExternalSettingsError as e: - logger.exception(e) - return json.dumps( - { - "message": e.message - } - ), 500, {'ContentType': 'application/json'} - - # update the profile - - updated_profile = self._octolapse_settings.profiles.update_from_server_profile( - profile_type, current_profile.to_dict(), server_profile_dict - ) - current_profile.update(updated_profile) - num_profiles += 1 + try: + server_profile_dict = ExternalSettings.get_profile( + self._plugin_version, profile_type, + updatable_profile["key_values"] + ) + except ExternalSettingsError as e: + logger.exception("An error occurred while getting a profile from the profile server.") + return jsonify( + { + "message": e.message + } + ), 500 + + # update the profile + + updated_profile = self._octolapse_settings.profiles.update_from_server_profile( + profile_type, current_profile.to_dict(), server_profile_dict + ) + current_profile.update(updated_profile) + num_profiles += 1 - self.automatic_updates_available = False - self.save_settings() - self.send_settings_changed_message(client_id) - return json.dumps({ - "settings": self._octolapse_settings.to_json(), - "num_updated": num_profiles - }), 200, {'ContentType': 'application/json'} + self.save_settings() + self.send_settings_changed_message(client_id) + return jsonify({ + "settings": self._octolapse_settings.to_json(), + "num_updated": num_profiles + }) @octoprint.plugin.BlueprintPlugin.route("/suppressServerUpdates", methods=["POST"]) @restricted_access - @admin_permission.require(403) def suppress_server_updates(self): - logger.info("Suppressing updates for all current updatable profiles.") - request_values = flask.request.get_json() - client_id = request_values["client_id"] - with self.automatic_update_lock: - if self.automatic_updates_available: - for profile_type, updatable_profiles in six.iteritems(self.automatic_updates_available): + with OctolapsePlugin.admin_permission.require(http_exception=403): + logger.info("Suppressing updates for all current updatable profiles.") + request_values = request.get_json() + client_id = request_values["client_id"] + + with self.automatic_update_lock: + has_updated = False + # remove python 2 support + # for profile_type, updatable_profiles in six.iteritems(self._octolapse_settings.profiles.get_updatable_profiles_dict()): + for profile_type, updatable_profiles in self._octolapse_settings.profiles.get_updatable_profiles_dict().items(): # now iterate through the printer profiles for updatable_profile in updatable_profiles: current_profile = self._octolapse_settings.profiles.get_profile( @@ -635,545 +921,689 @@ def suppress_server_updates(self): self.available_profiles, current_profile, profile_type ) if server_profile is not None: + has_updated = True current_profile.suppress_updates(server_profile) - self.automatic_updates_available = False - self.save_settings() - self.send_settings_changed_message(client_id) + if has_updated: + self.save_settings() + self.send_settings_changed_message(client_id) return self._octolapse_settings.to_json(), 200, {'ContentType': 'application/json'} @octoprint.plugin.BlueprintPlugin.route("/importSettings", methods=["POST"]) @restricted_access - @admin_permission.require(403) def import_settings(self): - logger.info("Importing settings from file") - import_method = 'file' - import_text = '' - client_id = '' - # get the json request values - request_values = flask.request.get_json() - if request_values is not None: - import_method = request_values["import_method"] - import_text = request_values["import_text"] - client_id = request_values["client_id"] - if import_method == "file": - logger.debug("Importing settings from file.") - # Parse the request. - settings_path = flask.request.values['octolapse_settings_import_path_upload.path'] - client_id = flask.request.values['client_id'] - self._octolapse_settings = self._octolapse_settings.import_settings_from_file( - settings_path, - self._plugin_version, - self.get_default_settings_folder(), - self.get_plugin_data_folder(), - self.available_profiles - ) - message = "Your settings have been updated from the supplied file." + with OctolapsePlugin.admin_permission.require(http_exception=403): + logger.info("Importing settings from file") + import_method = 'file' + import_text = '' + client_id = '' + # get the json request values + request_values = request.get_json() + message = "" + + if request_values is not None: + import_method = request_values["import_method"] + import_text = request_values["import_text"] + client_id = request_values["client_id"] - elif import_method == "text": - logger.debug("Importing settings from text.") - # convert the settings json to a python object - self._octolapse_settings = self._octolapse_settings.import_settings_from_text( - import_text, - self._plugin_version, - self.get_default_settings_folder(), - self.get_plugin_data_folder(), - self.available_profiles - ) - message = "Your settings have been updated from the uploaded text." + try: + if import_method == "file": + logger.debug("Importing settings from file.") + # Parse the request. + settings_path = request.values['octolapse_settings_import_path_upload.path'] + client_id = request.values['client_id'] + self._octolapse_settings = self._octolapse_settings.import_settings_from_file( + settings_path, + self._plugin_version, + __git_version__, + self.get_default_settings_folder(), + self.get_plugin_data_folder(), + self.available_profiles + ) + message = "Your settings have been updated from the supplied file." + elif import_method == "text": + logger.debug("Importing settings from text.") + # convert the settings json to a python object + self._octolapse_settings = self._octolapse_settings.import_settings_from_text( + import_text, + self._plugin_version, + __git_version__, + self.get_default_settings_folder(), + self.get_plugin_data_folder(), + self.available_profiles + ) + message = "Your settings have been updated from the uploaded text." + else: + raise Exception("Unknown Import Method") + except Exception as e: + logger.exception("An error occurred in import_settings_request.") + message = "Unable to import the provided settings, see plugin_octolapse.log for more information." + if type(e) is OctolapseSettings.IncorrectSettingsVersionException: + message = e.message + + return jsonify( + { + "success": False, + "msg": message + } + ) - # if we're this far we need to save the settings. - self.save_settings() + # if we're this far we need to save the settings. + self.save_settings() - # send a state changed message - self.send_settings_changed_message(client_id) + # send a state changed message + self.send_settings_changed_message(client_id) - return json.dumps( - { - "settings": self._octolapse_settings.to_json(), "msg": message - } - ), 200, {'ContentType': 'application/json'} + return jsonify( + { + "success": True, + "settings": self._octolapse_settings.to_json(), + "msg": message + } + ) @octoprint.plugin.BlueprintPlugin.route("/getMjpgStreamerControls", methods=["POST"]) @restricted_access - @admin_permission.require(403) def get_mjpg_streamer_controls(self): - request_values = flask.request.get_json() - server_type = request_values["server_type"] - camera_name = request_values["camera_name"] - address = request_values["address"] - username = request_values["username"] - password = request_values["password"] - ignore_ssl_error = request_values["ignore_ssl_error"] - replace = request_values["replace"] - guid = None if "guid" not in request_values else request_values["guid"] - try: - success, errors, webcam_settings = camera.CameraControl.get_webcam_settings( - server_type, - camera_name, - address, - username, - password, - ignore_ssl_error - ) - except Exception as e: - logger.exception(e) - raise e - - if not success: - return json.dumps( - {'success': False, 'error': errors}, 500, - {'ContentType': 'application/json'}) - - if guid and guid in self._octolapse_settings.profiles.cameras: - # Check to see if we need to update our existing camera image settings. We only want to do this - # if the control set has changed (minus the actual value) - - camera_profile = self._octolapse_settings.profiles.cameras[guid].clone() - matches = False - if server_type == MjpgStreamer.server_type: - if camera_profile.webcam_settings.mjpg_streamer.controls_match_server( - webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"] - ): - matches = True - - if replace or len(camera_profile.webcam_settings.mjpg_streamer.controls) == 0: - # get the existing settings since we don't want to use the defaults - camera_profile.webcam_settings.mjpg_streamer.controls = {} - camera_profile.webcam_settings.mjpg_streamer.update( - webcam_settings["webcam_settings"]["mjpg_streamer"] + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + profile = request_values["profile"] + server_type = profile["server_type"] + camera_name = profile["name"] + address = profile["address"] + username = profile["username"] + password = profile["password"] + ignore_ssl_error = profile["ignore_ssl_error"] + + guid = None if "guid" not in profile else profile["guid"] + try: + success, errors, webcam_settings = camera.CameraControl.get_webcam_settings( + server_type, + camera_name, + address, + username, + password, + ignore_ssl_error ) + except Exception as e: + logger.exception("An exception occurred while retrieving the current camera settings.") + raise e - # see if we know about the camera type - webcam_type = camera.CameraControl.get_webcam_type( - self._basefolder, - MjpgStreamer.server_type, - webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"] - ) - # turn the controls into a dict - webcam_settings = { - "webcam_settings": { - "type": webcam_type, - "mjpg_streamer": { - "controls": camera_profile.webcam_settings.mjpg_streamer.controls - } - }, - "new_preferences_available": False - } - else: - webcam_settings = { - "webcam_settings": { - "mjpg_streamer": { - "controls": camera_profile.webcam_settings.mjpg_streamer.controls - } - }, - "new_preferences_available": ( - not matches and len(webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"]) > 0 + if not success: + return jsonify({'success': False, 'error': errors}), 500 + + if guid and guid in self._octolapse_settings.profiles.cameras: + # Create a new camera profile and update from the supplied profile. + camera_profile = CameraProfile() + camera_profile.update(profile) + matches = False + if server_type == MjpgStreamer.server_type: + if camera_profile.webcam_settings.mjpg_streamer.controls_match_server( + webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"] + ): + matches = True + + if len(camera_profile.webcam_settings.mjpg_streamer.controls) == 0: + # get the existing settings since we don't want to use the defaults + camera_profile.webcam_settings.mjpg_streamer.controls = {} + camera_profile.webcam_settings.mjpg_streamer.update( + webcam_settings["webcam_settings"]["mjpg_streamer"] ) - } - - # if we're here, we should be good, extract and return the camera settings - return json.dumps( - { - 'success': True, 'settings': webcam_settings - }, cls=SettingsJsonEncoder), 200, {'ContentType': 'application/json'} + # see if we know about the camera type + webcam_type = camera.CameraControl.get_webcam_type( + self._basefolder, + MjpgStreamer.server_type, + webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"] + ) + # turn the controls into a dict + webcam_settings = { + "webcam_settings": { + "type": webcam_type, + "mjpg_streamer": { + "controls": camera_profile.webcam_settings.mjpg_streamer.controls + } + }, + "new_preferences_available": False + } + else: + webcam_settings = { + "webcam_settings": { + "mjpg_streamer": { + "controls": camera_profile.webcam_settings.mjpg_streamer.controls + } + }, + "new_preferences_available": ( + not matches and len(webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"]) > 0 + ) + } + # if we're here, we should be good, extract and return the camera settings + return json.dumps( + { + 'success': True, 'settings': webcam_settings + }, cls=SettingsJsonEncoder), 200, {'ContentType': 'application/json'} @octoprint.plugin.BlueprintPlugin.route("/getWebcamImagePreferences", methods=["POST"]) @restricted_access - @admin_permission.require(403) def get_webcam_image_preferences(self): - request_values = flask.request.get_json() - guid = request_values["guid"] - replace = request_values["replace"] - if guid not in self._octolapse_settings.profiles.cameras: - return json.dumps({'success': False, 'error': 'The requested camera profile does not exist. Cannot adjust settings.'}, 404, {'ContentType': 'application/json'}) - profile = self._octolapse_settings.profiles.cameras[guid] - if not profile.camera_type == 'webcam': - return json.dumps({'success': False, 'error': 'The selected camera is not a webcam. Cannot adjust settings.'}, 500, - {'ContentType': 'application/json'}) - - camera_profile = self._octolapse_settings.profiles.cameras[guid].clone() - - success, errors, settings = camera.CameraControl.get_webcam_settings( - camera_profile.webcam_settings.server_type, - camera_profile.name, - camera_profile.webcam_settings.address, - camera_profile.webcam_settings.username, - camera_profile.webcam_settings.password, - camera_profile.webcam_settings.ignore_ssl_error - ) + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + guid = request_values["guid"] + replace = request_values["replace"] + if guid not in self._octolapse_settings.profiles.cameras: + return ( + jsonify({ + 'success': False, + 'error': 'The requested camera profile does not exist. Cannot adjust settings.' + }), 404 + ) + profile = self._octolapse_settings.profiles.cameras[guid] + if not profile.camera_type == 'webcam': + return jsonify({ + 'success': False, + 'error': 'The selected camera is not a webcam. Cannot adjust settings.' + }), 500 - if not success: - return json.dumps( - {'success': False, 'error': errors}, 500, - {'ContentType': 'application/json'}) + camera_profile = self._octolapse_settings.profiles.cameras[guid].clone() - # Check to see if we need to update our existing camera image settings. We only want to do this - # if the control set has changed (minus the actual value) + success, errors, webcam_settings = camera.CameraControl.get_webcam_settings( + camera_profile.webcam_settings.server_type, + camera_profile.name, + camera_profile.webcam_settings.address, + camera_profile.webcam_settings.username, + camera_profile.webcam_settings.password, + camera_profile.webcam_settings.ignore_ssl_error + ) - if camera_profile.webcam_settings.server_type == MjpgStreamer.server_type: - matches = False - if camera_profile.webcam_settings.mjpg_streamer.controls_match_server( - settings["webcam_settings"]["mjpg_streamer"]["controls"] - ): - matches = True + if not success: + return jsonify({ + 'success': False, 'error': errors + }), 500 - if replace or len(camera_profile.webcam_settings.mjpg_streamer.controls) == 0: - # get the existing settings since we don't want to use the defaults - camera_profile.webcam_settings.mjpg_streamer.controls = {} - camera_profile.webcam_settings.mjpg_streamer.update( - settings["webcam_settings"]["mjpg_streamer"] - ) - # see if we know about the camera type - webcam_type = camera.CameraControl.get_webcam_type( - self._basefolder, - MjpgStreamer.server_type, - settings["webcam_settings"]["mjpg_streamer"]["controls"] - ) - camera_profile.webcam_settings.type = webcam_type - settings = { - "name": camera_profile.name, - "guid": camera_profile.guid, - "new_preferences_available": False, - "webcam_settings": camera_profile.webcam_settings - } + # Check to see if we need to update our existing camera image settings. We only want to do this + # if the control set has changed (minus the actual value) + + if camera_profile.webcam_settings.server_type == MjpgStreamer.server_type: + matches = False + if camera_profile.webcam_settings.mjpg_streamer.controls_match_server( + webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"] + ): + matches = True + + if replace or len(camera_profile.webcam_settings.mjpg_streamer.controls) == 0: + # get the existing settings since we don't want to use the defaults + camera_profile.webcam_settings.mjpg_streamer.controls = {} + camera_profile.webcam_settings.mjpg_streamer.update( + webcam_settings["webcam_settings"]["mjpg_streamer"] + ) + # see if we know about the camera type + webcam_type = camera.CameraControl.get_webcam_type( + self._basefolder, + MjpgStreamer.server_type, + webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"] + ) + camera_profile.webcam_settings.type = webcam_type + webcam_settings = { + "name": camera_profile.name, + "guid": camera_profile.guid, + "new_preferences_available": False, + "webcam_settings": camera_profile.webcam_settings + } + else: + webcam_settings = { + "name": camera_profile.name, + "guid": camera_profile.guid, + "new_preferences_available": ( + not matches and len(webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"]) > 0 + ), + "webcam_settings": camera_profile.webcam_settings + } else: - settings = { - "name": camera_profile.name, - "guid": camera_profile.guid, - "new_preferences_available": ( - not matches and len(settings["webcam_settings"]["mjpg_streamer"]["controls"]) > 0 - ), - "webcam_settings": camera_profile.webcam_settings - } - else: - return json.dumps( - { + return jsonify({ 'success': False, - 'error': 'The webcam you are using is not streaming via mjpg-streamer, which is the only server supported currently.' - }, 500,{'ContentType': 'application/json'} - ) - # if we're here, we should be good, extract and return the camera settings - return json.dumps({ - 'success': True, 'settings': settings - }, cls=SettingsJsonEncoder), 200, {'ContentType': 'application/json'} + 'error': 'The webcam you are using is not streaming via mjpg-streamer, which is the only server ' + 'supported currently. ' + }), 500 + # if we're here, we should be good, extract and return the camera settings + return json.dumps({ + 'success': True, 'settings': webcam_settings + }, cls=SettingsJsonEncoder), 200, {'ContentType': 'application/json'} @octoprint.plugin.BlueprintPlugin.route("/getWebcamType", methods=["POST"]) @restricted_access - @admin_permission.require(403) def get_webcam_type(self): - request_values = flask.request.get_json() - server_type = request_values["server_type"] - camera_name = request_values["camera_name"] - address = request_values["address"] - username = request_values["username"] - password = request_values["password"] - ignore_ssl_error = request_values["ignore_ssl_error"] - success, errors, webcam_settings = camera.CameraControl.get_webcam_settings( - server_type, - camera_name, - address, - username, - password, - ignore_ssl_error - ) - if not success: - return json.dumps( - {'success': False, 'error': errors}, 500, - {'ContentType': 'application/json'}) - - if server_type == MjpgStreamer.server_type: - - webcam_type = camera.CameraControl.get_webcam_type( - self._basefolder, server_type, webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"] + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + server_type = request_values["server_type"] + camera_name = request_values["camera_name"] + address = request_values["address"] + username = request_values["username"] + password = request_values["password"] + ignore_ssl_error = request_values["ignore_ssl_error"] + success, errors, webcam_settings = camera.CameraControl.get_webcam_settings( + server_type, + camera_name, + address, + username, + password, + ignore_ssl_error ) - else: - webcam_type = False + if not success: + return jsonify({ + 'success': False, + 'error': errors + }), 500, + + if server_type == MjpgStreamer.server_type: + webcam_type = camera.CameraControl.get_webcam_type( + self._basefolder, server_type, webcam_settings["webcam_settings"]["mjpg_streamer"]["controls"] + ) + else: + webcam_type = False - # if we're here, we should be good, extract and return the camera settings - return json.dumps({ - 'success': True, 'type': webcam_type - }, cls=SettingsJsonEncoder), 200, {'ContentType': 'application/json'} + # if we're here, we should be good, extract and return the camera settings + return json.dumps({ + 'success': True, 'type': webcam_type + }, cls=SettingsJsonEncoder), 200, {'ContentType': 'application/json'} @octoprint.plugin.BlueprintPlugin.route("/testCameraSettingsApply", methods=["POST"]) @restricted_access - @admin_permission.require(403) def test_camera_settings_apply(self): - request_values = flask.request.get_json() - profile = request_values["profile"] - camera_profile = CameraProfile.create_from(profile) - success, errors = camera.CameraControl.test_web_camera_image_preferences(camera_profile) - return json.dumps({'success': success, 'error': errors}, 200, {'ContentType': 'application/json'}) + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + profile = request_values["profile"] + camera_profile = CameraProfile.create_from(profile) + success, errors = camera.CameraControl.test_web_camera_image_preferences(camera_profile) + return jsonify({ + 'success': success, + 'error': errors + }) @octoprint.plugin.BlueprintPlugin.route("/applyCameraSettings", methods=["POST"]) @restricted_access - @admin_permission.require(403) def apply_camera_settings_request(self): - request_values = flask.request.get_json() - type = request_values["type"] - # Get the settings we need to run applycamerasettings - if type == "by_guid": - guid = request_values["guid"] - # get the current camera profile - if guid not in self._octolapse_settings.profiles.cameras: - return json.dumps({'success': False, 'error': 'The requested camera profile does not exist. Cannot adjust settings.'}, 404, - {'ContentType': 'application/json'}) - camera_profile = self._octolapse_settings.profiles.cameras[guid] - elif type == "ui-settings-update": - # create a new profile from the profile dict - webcam_settings = request_values["settings"] - camera_profile = CameraProfile() - camera_profile.name = webcam_settings["name"] - camera_profile.guid = webcam_settings["guid"] - camera_profile.webcam_settings.update(webcam_settings) - else: - return json.dumps( - {'success': False, 'error': 'Unknown request type: {0}.'.format(type)}, - 500, - {'ContentType': 'application/json'}) + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + request_type = request_values["type"] + # Get the settings we need to run apply cameras ettings + if request_type == "by_guid": + guid = request_values["guid"] + # get the current camera profile + if guid not in self._octolapse_settings.profiles.cameras: + return jsonify({ + 'success': False, + 'error': 'The requested camera profile does not exist. Cannot adjust settings.' + }), 404 + camera_profile = self._octolapse_settings.profiles.cameras[guid] + elif request_type == "ui-settings-update": + # create a new profile from the profile dict + webcam_settings = request_values["settings"] + camera_profile = CameraProfile() + camera_profile.name = webcam_settings["name"] + camera_profile.guid = webcam_settings["guid"] + camera_profile.webcam_settings.update(webcam_settings) + else: + return jsonify({ + 'success': False, + 'error': 'Unknown request type: {0}.'.format(request_type) + }), 500 - # Make sure the current profile is a webcam - if not camera_profile.camera_type == 'webcam': - return json.dumps({'success': False, 'error': 'The selected camera is not a webcam. Cannot adjust settings.'}, 500, - {'ContentType': 'application/json'}) + # Make sure the current profile is a webcam + if not camera_profile.camera_type == 'webcam': + return jsonify({ + 'success': False, + 'error': 'The selected camera is not a webcam. Cannot adjust settings.' + }), 500 - # Apply the settings - try: - success, error = self.apply_camera_settings([camera_profile]) - except camera.CameraError as e: - logger.exception("Failed to apply webcam settings in /applyCameraSettings.") - return json.dumps( - {'success': False, - 'error': "Octolapse was unable to apply image preferences to your webcam. See plugin_octolapse.log " - "for details. " - }, 200, {'ContentType': 'application/json'}) + # Apply the settings + try: + success, error = self.apply_camera_settings([camera_profile]) + except camera.CameraError as e: + logger.exception("Failed to apply webcam settings in /applyCameraSettings.") + return jsonify({ + 'success': False, + 'error': "Octolapse was unable to apply image preferences to your webcam. See plugin_octolapse.log for details. " + }) - return json.dumps({'success': success, 'error': error}, 200, {'ContentType': 'application/json'}) + return jsonify({ + 'success': success, + 'error': error + }) @octoprint.plugin.BlueprintPlugin.route("/saveWebcamSettings", methods=["POST"]) @restricted_access - @admin_permission.require(403) def save_webcam_settings(self): - request_values = flask.request.get_json() - guid = request_values["guid"] - webcam_settings = request_values["webcam_settings"] - - # get the current camera profile - if guid not in self._octolapse_settings.profiles.cameras: - return json.dumps({'success': False, 'error': 'The requested camera profile does not exist. Cannot adjust settings.'}, 404, - {'ContentType': 'application/json'}) - profile = self._octolapse_settings.profiles.cameras[guid] - if not profile.camera_type == 'webcam': - return json.dumps({'success': False, 'error': 'The selected camera is not a webcam. Cannot adjust settings.'}, 500, - {'ContentType': 'application/json'}) - - camera_profile = self._octolapse_settings.profiles.cameras[guid] - camera_profile.webcam_settings.update(webcam_settings) - self.save_settings() + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + guid = request_values["guid"] + webcam_settings = request_values["webcam_settings"] - try: - success, error = camera.CameraControl.apply_camera_settings([camera_profile]) - except camera.CameraError as e: - logger.exception("Failed to save webcam settings in /saveWebcamSettings.") - return json.dumps({'success': False, 'error': e.message}, 200, {'ContentType': 'application/json'}) + # get the current camera profile + if guid not in self._octolapse_settings.profiles.cameras: + return jsonify({ + 'success': False, + 'error': 'The requested camera profile does not exist. Cannot adjust settings.' + }), 404 + profile = self._octolapse_settings.profiles.cameras[guid] + if not profile.camera_type == 'webcam': + return jsonify({ + 'success': False, + 'error': 'The selected camera is not a webcam. Cannot adjust settings.' + }), 500 - return json.dumps({'success': success, 'error': error}, 200, {'ContentType': 'application/json'}) + camera_profile = self._octolapse_settings.profiles.cameras[guid] + camera_profile.webcam_settings.update(webcam_settings) + self.save_settings() - @octoprint.plugin.BlueprintPlugin.route("/loadWebcamDefaults", methods=["POST"]) - @restricted_access - @admin_permission.require(403) - def load_webcam_defaults(self): - request_values = flask.request.get_json() - server_type = request_values["server_type"] - camera_name = request_values["camera_name"] - address = request_values["address"] - username = request_values["username"] - password = request_values["password"] - ignore_ssl_error = request_values["ignore_ssl_error"] - - success, errors, defaults = camera.CameraControl.load_webcam_defaults( - server_type, - camera_name, - address, - username, - password, - ignore_ssl_error - ) - ret_val = { - 'webcam_settings': { - 'mjpg_streamer': { - 'controls': defaults - } - } - } - return json.dumps({'success': success, 'defaults': ret_val, 'error': errors}, 200, {'ContentType': 'application/json'}) + try: + success, error = camera.CameraControl.apply_camera_settings([camera_profile]) + except camera.CameraError as e: + logger.exception("Failed to save webcam settings in /saveWebcamSettings.") + return jsonify({ + 'success': False, + 'error': e.message + }) + return jsonify({ + 'success': success, + 'error': error + }) - @octoprint.plugin.BlueprintPlugin.route("/applyWebcamSetting", methods=["POST"]) + @octoprint.plugin.BlueprintPlugin.route("/loadWebcamDefaults", methods=["POST"]) @restricted_access - @admin_permission.require(403) - def apply_webcam_setting_request(self): - request_values = flask.request.get_json() - server_type = request_values["server_type"] - camera_name = request_values["camera_name"] - address = request_values["address"] - username = request_values["username"] - password = request_values["password"] - ignore_ssl_error = request_values["ignore_ssl_error"] - setting = request_values["setting"] - - # apply a single setting to the camera - try: - success, error = camera.CameraControl.apply_webcam_setting( + def load_webcam_defaults(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + server_type = request_values["server_type"] + camera_name = request_values["camera_name"] + address = request_values["address"] + username = request_values["username"] + password = request_values["password"] + ignore_ssl_error = request_values["ignore_ssl_error"] + + success, errors, defaults = camera.CameraControl.load_webcam_defaults( server_type, - setting, camera_name, address, username, password, - ignore_ssl_error, + ignore_ssl_error ) + ret_val = { + 'webcam_settings': { + 'mjpg_streamer': { + 'controls': defaults + } + } + } + return jsonify({ + 'success': success, + 'defaults': ret_val, + 'error': errors + }) + + @octoprint.plugin.BlueprintPlugin.route("/applyWebcamSetting", methods=["POST"]) + @restricted_access + def apply_webcam_setting_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + server_type = request_values["server_type"] + camera_name = request_values["camera_name"] + address = request_values["address"] + username = request_values["username"] + password = request_values["password"] + ignore_ssl_error = request_values["ignore_ssl_error"] + setting = request_values["setting"] + + # apply a single setting to the camera + try: + success, error = camera.CameraControl.apply_webcam_setting( + server_type, + setting, + camera_name, + address, + username, + password, + ignore_ssl_error, + ) + if not success: + logger.error(error) + except camera.CameraError as e: + logger.exception("Failed to apply webcam settings in /applyWebcamSetting") + return jsonify({ + 'success': False, + 'error': e.message + }) + error_string = "" if not success: - logger.error(error) - except camera.CameraError as e: - logger.exception("Failed to apply webcam settings in /applyWebcamSetting") - return json.dumps({'success': False, 'error': e.message}, 200, {'ContentType': 'application/json'}) - error_string = "" - if not success: - error_string = error.message - return json.dumps({'success': success, 'error': error_string}, 200, {'ContentType': 'application/json'}) + error_string = error.message + return jsonify({ + 'success': success, + 'error': error_string + }) @octoprint.plugin.BlueprintPlugin.route("/testCamera", methods=["POST"]) @restricted_access - @admin_permission.require(403) def test_camera_request(self): - request_values = flask.request.get_json() - profile = request_values["profile"] - camera_profile = CameraProfile.create_from(profile) - success, errors = camera.CameraControl.test_web_camera(camera_profile) - return json.dumps({'success': success, 'error': errors}), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + profile = request_values["profile"] + camera_profile = CameraProfile.create_from(profile) + success, errors = camera.CameraControl.test_web_camera(camera_profile) + return jsonify({ + 'success': success, + 'error': errors + }) + + @octoprint.plugin.BlueprintPlugin.route("/testCameraScript", methods=["POST"]) + @restricted_access + def test_camera_script_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + success = False + errors = None + snapshot_created = False + try: + request_values = request.get_json() + profile = request_values["profile"] + script_type = request_values["script_type"] + camera_profile = CameraProfile.create_from(profile) + success, errors, snapshot_created = camera.CameraControl.test_script(camera_profile, script_type, self._basefolder) + except Exception as e: + logger.exception("An unexpected error occurred while executing the {0} script.".format(script_type)) + raise e + return jsonify({ + 'success': success, + 'error': errors, + 'snapshot_created': snapshot_created + }) + + @octoprint.plugin.BlueprintPlugin.route("/testDirectory", methods=["POST"]) + @restricted_access + def test_directory_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + directory_type = request_values["type"] + directory = request_values["directory"].strip() + test_profile = MainSettings(self._plugin_version, __git_version__,) + + success = False + errors = "An unknown directory type was requested for testing." + directory_path = None + if directory_type == "snapshot-archive": + if len(directory) == 0: + directory = test_profile.get_snapshot_archive_directory( + self.get_plugin_data_folder() + ) + success, errors = MainSettings.test_directory( + directory + ) + elif directory_type == "temporary": + if len(directory) == 0: + directory = test_profile.get_temporary_directory( + self.get_plugin_data_folder() + ) + success, errors = MainSettings.test_directory( + directory + ) + elif directory_type == "timelapse": + if len(directory) == 0: + directory = test_profile.get_timelapse_directory( + self.get_octoprint_timelapse_location() + ) + success, errors = MainSettings.test_directory( + directory + ) + return jsonify({ + 'success': success, + 'error': errors, + }) @octoprint.plugin.BlueprintPlugin.route("/cancelPreprocessing", methods=["POST"]) @restricted_access - @admin_permission.require(403) def cancel_preprocessing_request(self): - request_values = flask.request.get_json() - preprocessing_job_guid = request_values["preprocessing_job_guid"] - if ( - self.preprocessing_job_guid is None - or preprocessing_job_guid != str(self.preprocessing_job_guid) - ): - # return without doing anything, this job is already over - return json.dumps({'success': True}), 200, {'ContentType': 'application/json'} - - logger.info("Cancelling Preprocessing for /cancelPreprocessing.") - # todo: Check the current printing session and make sure it matches before canceling the print! - self.preprocessing_job_guid = None - self.cancel_preprocessing() - if self._printer.is_printing(): - self._printer.cancel_print(tags={'startup-failed'}) - self.send_snapshot_preview_complete_message() - return json.dumps({'success': True}), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + preprocessing_job_guid = request_values["preprocessing_job_guid"] + if ( + self.preprocessing_job_guid is None + or preprocessing_job_guid != str(self.preprocessing_job_guid) + ): + # return without doing anything, this job is already over + return jsonify({ + 'success': True + }) + + logger.info("Cancelling Preprocessing for /cancelPreprocessing.") + # todo: Check the current printing session and make sure it matches before canceling the print! + self.reset_preprocessing() + self._timelapse.release_job_on_hold_lock(reset=True) + if self._printer.is_printing(): + self._printer.cancel_print(tags={'octolapse-startup-failed'}) + self.send_snapshot_preview_complete_message() + return jsonify({ + 'success': True + }) @octoprint.plugin.BlueprintPlugin.route("/acceptSnapshotPlanPreview", methods=["POST"]) @restricted_access - @admin_permission.require(403) - def accept_snapshot_plan_preview(self): - request_values = flask.request.get_json() - preprocessing_job_guid = request_values["preprocessing_job_guid"] - if ( - self.preprocessing_job_guid is None or - preprocessing_job_guid != str(self.preprocessing_job_guid) or - self.saved_timelapse_settings is None or - self.saved_snapshot_plans is None or - self.saved_parsed_command is None - ): - # return without doing anything, this job is already over - message = "Unable to accept the snapshot plan. Either the printer not operational, is currently printing, " \ - "or this plan has been deleted. " - return json.dumps({'success': False, 'error': message}), 200, {'ContentType': 'application/json'} - - logger.info("Accepting the saved snapshot plan") - self.preprocessing_job_guid = None - self.start_preprocessed_timelapse() - self.send_snapshot_preview_complete_message() - return json.dumps({'success': True}), 200, {'ContentType': 'application/json'} + def accept_snapshot_plan_preview_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + preprocessing_job_guid = request_values["preprocessing_job_guid"] + if ( + preprocessing_job_guid is not None and + str(self.preprocessing_job_guid) == preprocessing_job_guid + ): + self.accept_snapshot_plan_preview(preprocessing_job_guid) + return jsonify({ + 'success': True + }) + return jsonify({ + 'success': False + }) @octoprint.plugin.BlueprintPlugin.route("/toggleCamera", methods=["POST"]) @restricted_access - @admin_permission.require(403) def toggle_camera(self): - request_values = flask.request.get_json() - guid = request_values["guid"] - client_id = request_values["client_id"] - new_value = not self._octolapse_settings.profiles.cameras[guid].enabled - self._octolapse_settings.profiles.cameras[guid].enabled = new_value - self.save_settings() - self.send_settings_changed_message(client_id) - return json.dumps({'success': True, 'enabled': new_value}), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + guid = request_values["guid"] + client_id = request_values["client_id"] + new_value = not self._octolapse_settings.profiles.cameras[guid].enabled + self._octolapse_settings.profiles.cameras[guid].enabled = new_value + self.save_settings() + self.send_settings_changed_message(client_id) + return jsonify({ + 'success': True, + 'enabled': new_value, + }) + + @octoprint.plugin.BlueprintPlugin.route("/validateSnapshotCommand", methods=["POST"]) + @restricted_access + def validate_snapshot_command(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + snapshot_command = request_values["snapshot_command"] + valid = "true" if self._timelapse.validate_snapshot_command(snapshot_command) else "" + return "\"{0}\"".format(valid), 200, {'ContentType': 'application/json'} @octoprint.plugin.BlueprintPlugin.route("/validateRenderingTemplate", methods=["POST"]) + @restricted_access def validate_rendering_template(self): - template = flask.request.form['output_template'] - result = render.is_rendering_template_valid( - template, - self._octolapse_settings.profiles.options.rendering["rendering_file_templates"] - ) - if result[0]: - valid = "true" - else: - valid = result[1] - return "\"{0}\"".format(valid), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + template = request.form['octolapse_rendering_output_template'] + result = render.is_rendering_template_valid( + template, + self._octolapse_settings.profiles.options.rendering["rendering_file_templates"] + ) + if result[0]: + valid = "true" + else: + valid = result[1] + return "\"{0}\"".format(valid), 200, {'ContentType': 'application/json'} @octoprint.plugin.BlueprintPlugin.route("/validateOverlayTextTemplate", methods=["POST"]) + @restricted_access def validate_overlay_text_template(self): - template = flask.request.form['overlay_text_template'] - result = render.is_overlay_text_template_valid( - template, - self._octolapse_settings.profiles.options.rendering["overlay_text_templates"] - ) - if result[0]: - valid = "true" - else: - valid = result[1] - return "\"{0}\"".format(valid), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + template = request.form['octolapse_rendering_overlay_text_template'] + result = render.is_overlay_text_template_valid( + template, + self._octolapse_settings.profiles.options.rendering["overlay_text_templates"] + ) + if result[0]: + valid = "true" + else: + valid = result[1] + return "\"{0}\"".format(valid), 200, {'ContentType': 'application/json'} @octoprint.plugin.BlueprintPlugin.route("/rendering/font", methods=["GET"]) @restricted_access - @admin_permission.require(403) def get_available_fonts(self): - font_list = utility.get_system_fonts(self._basefolder) - return json.dumps(font_list), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + font_list = [] + try: + font_list = utility.get_system_fonts(self._basefolder) + except Exception as e: + logger.exception("Unable to retrieve system fonts.") + raise e + return jsonify({ + "fonts": font_list + }) @octoprint.plugin.BlueprintPlugin.route("/rendering/previewOverlay", methods=["POST"]) def preview_overlay(self): preview_image = None camera_image = None - request_values = flask.request.get_json() + request_values = request.get_json() try: # Take a snapshot from the first active camera. active_cameras = self._octolapse_settings.profiles.active_cameras() - if len(active_cameras) > 0: + for active_camera in active_cameras: + if active_camera.camera_type != 'webcam': + continue try: - camera_image = snapshot.take_in_memory_snapshot(self._octolapse_settings, active_cameras[0]) + camera_image = snapshot.take_in_memory_snapshot(self._octolapse_settings, active_camera) except Exception as e: logger.exception("Failed to take a snapshot. Falling back to solid color.") + # Extract the profile from the request. try: rendering_profile = RenderingProfile().create_from(request_values) except Exception as e: logger.exception('Preview overlay request did not provide valid Rendering profile.') - return json.dumps({ + return jsonify({ 'error': 'Request did not contain valid Rendering profile. Check octolapse log for details.' - }), 400, {} + }), 400 # Render a preview image. preview_image = render.preview_overlay(rendering_profile, image=camera_image) if preview_image is None: - return json.dumps({'success': False}), 404, {'ContentType': 'application/json'} + return jsonify({ + 'success': False + }), 404 # Use a buffer to base64 encode the image. img_io = BytesIO() @@ -1182,7 +1612,9 @@ def preview_overlay(self): base64_encoded_image = base64.b64encode(img_io.getvalue()) # Return a response. We have to return JSON because jQuery only knows how to parse JSON. - return json.dumps({'image': base64_encoded_image}), 200, {'ContentType': 'application/json'} + return jsonify({ + 'image': base64_encoded_image + }) finally: # cleanup if camera_image is not None: @@ -1192,148 +1624,373 @@ def preview_overlay(self): @octoprint.plugin.BlueprintPlugin.route("/rendering/watermark", methods=["GET"]) @restricted_access - @admin_permission.require(403) def get_available_watermarks(self): - # TODO(Shadowen): Retrieve watermarks_directory_name from config.yaml. - watermarks_directory_name = "watermarks" - full_watermarks_dir = os.path.join(self.get_plugin_data_folder(), watermarks_directory_name) - files = [] - if os.path.exists(full_watermarks_dir): - files = [ - os.path.join( - self.get_plugin_data_folder(), watermarks_directory_name, f - ) for f in os.listdir(full_watermarks_dir) - ] - data = {'filepaths': files} - return json.dumps(data), 200, {'ContentType': 'application/json'} + with OctolapsePlugin.admin_permission.require(http_exception=403): + # TODO(Shadowen): Retrieve watermarks_directory_name from config.yaml. + watermarks_directory_name = "watermarks" + full_watermarks_dir = os.path.join(self.get_plugin_data_folder(), watermarks_directory_name) + files = [] + if os.path.exists(full_watermarks_dir): + files = [ + os.path.join( + self.get_plugin_data_folder(), watermarks_directory_name, f + ) for f in os.listdir(full_watermarks_dir) + ] + data = {'filepaths': files} + return jsonify(data) @octoprint.plugin.BlueprintPlugin.route("/rendering/watermark/upload", methods=["POST"]) @restricted_access - @admin_permission.require(403) def upload_watermark(self): # TODO(Shadowen): Receive chunked uploads properly. # It seems like this function is called once PER CHUNK rather than when the entire upload has completed. + with OctolapsePlugin.admin_permission.require(http_exception=403): + # Parse the request. + image_filename = request.values['image.name'] + # The path where the watermark file was saved by the uploader. + watermark_temp_path = request.values['image.path'] + logger.debug("Receiving uploaded watermark %s.", image_filename) + + # Move the watermark from the (temp) upload location to a permanent location. + # Maybe it could be uploaded directly there, but I don't know how to do that. + # TODO(Shadowen): Retrieve watermarks_directory_name from config.yaml. + watermarks_directory_name = "watermarks" + # Ensure the watermarks directory exists. + full_watermarks_dir = os.path.join(self.get_plugin_data_folder(), watermarks_directory_name) + if not os.path.exists(full_watermarks_dir): + logger.info("Creating watermarks directory at %s.".format(full_watermarks_dir)) + try: + os.makedirs(full_watermarks_dir) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + + # Move the image. + watermark_destination_path = os.path.join(full_watermarks_dir, image_filename) + if os.path.exists(watermark_destination_path): + if os.path.isdir(watermark_destination_path): + logger.error( + "Tried to upload watermark to %s but already contains a directory! Aborting!", + watermark_destination_path + ) + return jsonify({ + 'error': 'Bad file name.' + }), 501 + else: + # TODO(Shadowen): Maybe offer a config.yaml option for this. + logger.warning( + "Tried to upload watermark to %s but file already exists! Overwriting...", + watermark_destination_path + ) + utility.remove(watermark_destination_path) + logger.info( + "Moving watermark from %s to %s.", + watermark_temp_path, + watermark_destination_path + ) + utility.move(watermark_temp_path, watermark_destination_path) - # Parse the request. - image_filename = flask.request.values['image.name'] - # The path where the watermark file was saved by the uploader. - watermark_temp_path = flask.request.values['image.path'] - logger.debug("Receiving uploaded watermark %s.", image_filename) - - # Move the watermark from the (temp) upload location to a permanent location. - # Maybe it could be uploaded directly there, but I don't know how to do that. - # TODO(Shadowen): Retrieve watermarks_directory_name from config.yaml. - watermarks_directory_name = "watermarks" - # Ensure the watermarks directory exists. - full_watermarks_dir = os.path.join(self.get_plugin_data_folder(), watermarks_directory_name) - if not os.path.exists(full_watermarks_dir): - logger.info("Creating watermarks directory at %s.".format(full_watermarks_dir)) - os.makedirs(full_watermarks_dir) - - # Move the image. - watermark_destination_path = os.path.join(full_watermarks_dir, image_filename) - if os.path.exists(watermark_destination_path): - if os.path.isdir(watermark_destination_path): + return jsonify({}), 200 + + @octoprint.plugin.BlueprintPlugin.route("/rendering/watermark/delete", methods=["POST"]) + @restricted_access + def delete_watermark(self): + """Delete the watermark given in the HTTP POST name field.""" + with OctolapsePlugin.admin_permission.require(http_exception=403): + # Parse the request. + filepath = request.get_json()['path'] + logger.debug("Deleting watermark %s.", filepath) + if not os.path.exists(filepath): + logger.error("Tried to delete watermark at %s but file doesn't exists!", filepath) + return jsonify({ + 'error': 'No such file.' + }), 501 + + def is_subdirectory(a, b): + """Returns true if a is (or is in) a subdirectory of b.""" + real_a = os.path.join(os.path.realpath(a), '') + real_b = os.path.join(os.path.realpath(b), '') + return os.path.commonprefix([real_a, real_b]) == real_a + + # TODO(Shadowen): Retrieve watermarks_directory_name from config.yaml. + watermarks_directory_name = "watermarks" + # Ensure the file we are trying to delete is in the watermarks folder. + watermarks_directory = os.path.join(self.get_plugin_data_folder(), watermarks_directory_name) + if not is_subdirectory(watermarks_directory, filepath): logger.error( - "Tried to upload watermark to %s but already contains a directory! Aborting!", - watermark_destination_path + "Tried to delete watermark at %s but file doesn't exists!", + filepath ) - return json.dumps({'error': 'Bad file name.'}, 501, {'ContentType': 'application/json'}) - else: - # TODO(Shadowen): Maybe offer a config.yaml option for this. - logger.warning( - "Tried to upload watermark to %s but file already exists! Overwriting...", - watermark_destination_path + return jsonify({ + 'error': "Cannot delete file outside watermarks folder." + }), 400 + utility.remove(filepath) + return jsonify({ + 'success': "Deleted {} successfully.".format(filepath) + }) + + @octoprint.plugin.BlueprintPlugin.route("/loadFailedRenderings", methods=["POST"]) + @restricted_access + def load_failed_renderings_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + failed_jobs = self._rendering_processor.get_failed() + return jsonify({ + "failed": { + "renderings": failed_jobs["failed"], + "size": failed_jobs["failed_size"], + } + }) + + @octoprint.plugin.BlueprintPlugin.route("/loadInProcessRenderings", methods=["POST"]) + @restricted_access + def load_in_process_renderings_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + in_process_jobs = self._rendering_processor.get_in_process() + return jsonify({ + "in_process": { + "renderings": in_process_jobs["in_process"], + "size": in_process_jobs["in_process_size"], + } + }) + + @octoprint.plugin.BlueprintPlugin.route("/deleteAllFailedRenderings", methods=["POST"]) + @restricted_access + def delete_all_failed_renderings_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + failed_jobs = self._rendering_processor.get_failed() + failed_renderings = failed_jobs["failed"] + for failed_rendering in failed_renderings: + self.delete_rendering_job( + failed_rendering["job_guid"], + failed_rendering["camera_guid"] ) - os.remove(watermark_destination_path) - logger.info( - "Moving watermark from %s to %s.", - watermark_temp_path, - watermark_destination_path - ) - shutil.move(watermark_temp_path, watermark_destination_path) + return jsonify({ + "success": True + }) - return json.dumps({}, 200, {'ContentType': 'application/json'}) + @octoprint.plugin.BlueprintPlugin.route("/deleteFailedRendering", methods=["POST"]) + @restricted_access + def delete_failed_rendering_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + job_guid = request_values["job_guid"] + camera_guid = request_values["camera_guid"] + self.delete_rendering_job( + job_guid, + camera_guid + ) + return jsonify({ + "success": True + }) - @octoprint.plugin.BlueprintPlugin.route("/rendering/watermark/delete", methods=["POST"]) + @octoprint.plugin.BlueprintPlugin.route("/renderAllFailedRenderings", methods=["POST"]) @restricted_access - @admin_permission.require(403) - def delete_watermark(self): - """Delete the watermark given in the HTTP POST name field.""" - # Parse the request. - filepath = flask.request.get_json()['path'] - logger.debug("Deleting watermark %s.", filepath) - if not os.path.exists(filepath): - logger.error("Tried to delete watermark at %s but file doesn't exists!", filepath) - return json.dumps({'error': 'No such file.'}, 501, {'ContentType': 'application/json'}) - - def is_subdirectory(a, b): - """Returns true if a is (or is in) a subdirectory of b.""" - real_a = os.path.join(os.path.realpath(a), '') - real_b = os.path.join(os.path.realpath(b), '') - return os.path.commonprefix([real_a, real_b]) == real_a - - # TODO(Shadowen): Retrieve watermarks_directory_name from config.yaml. - watermarks_directory_name = "watermarks" - # Ensure the file we are trying to delete is in the watermarks folder. - watermarks_directory = os.path.join(self.get_plugin_data_folder(), watermarks_directory_name) - if not is_subdirectory(watermarks_directory, filepath): - logger.error( - "Tried to delete watermark at %s but file doesn't exists!", - filepath + def render_all_failed_renderings_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + render_profile_override_guid = request_values["render_profile_override_guid"] + camera_profile_override_guid = request_values["camera_profile_override_guid"] + rendering_profile = None + camera_profile = None + if ( + render_profile_override_guid and + render_profile_override_guid in self._octolapse_settings.profiles.renderings + ): + rendering_profile = self._octolapse_settings.profiles.renderings[render_profile_override_guid] + + if ( + camera_profile_override_guid and + camera_profile_override_guid in self._octolapse_settings.profiles.cameras + ): + camera_profile = self._octolapse_settings.profiles.cameras[camera_profile_override_guid].clone() + + failed_jobs = self._rendering_processor.get_failed() + failed_renderings = failed_jobs["failed"] + for failed_rendering in failed_renderings: + self.add_rendering_job( + failed_rendering["job_guid"], + failed_rendering["camera_guid"], + self._octolapse_settings.main_settings.get_temporary_directory(self.get_plugin_data_folder()), + rendering_profile=rendering_profile, + camera_profile=camera_profile + ) + return jsonify({ + "success": True + }) + + @octoprint.plugin.BlueprintPlugin.route("/renderFailedRendering", methods=["POST"]) + @restricted_access + def render_failed_rendering_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + job_guid = request_values["job_guid"] + camera_guid = request_values["camera_guid"] + render_profile_override_guid = request_values["render_profile_override_guid"] + camera_profile_override_guid = request_values["camera_profile_override_guid"] + rendering_profile = None + camera_profile = None + if ( + render_profile_override_guid and + render_profile_override_guid in self._octolapse_settings.profiles.renderings + ): + rendering_profile = self._octolapse_settings.profiles.renderings[render_profile_override_guid] + + if ( + camera_profile_override_guid and + camera_profile_override_guid in self._octolapse_settings.profiles.cameras + ): + camera_profile = self._octolapse_settings.profiles.cameras[camera_profile_override_guid].clone() + + self.add_rendering_job( + job_guid, + camera_guid, + self._octolapse_settings.main_settings.get_temporary_directory(self.get_plugin_data_folder()), + rendering_profile=rendering_profile, + camera_profile=camera_profile ) - return json.dumps({'error': "Cannot delete file outside watermarks folder."}, 400, - {'ContentType': 'application/json'}) - os.remove(filepath) - return json.dumps({'success': "Deleted {} successfully.".format(filepath)}), 200, { - 'ContentType': 'application/json'} + return jsonify({ + "success": True + }) - # blueprint helpers - @staticmethod - def get_download_file_response(file_path, download_filename, on_complete_callback=None, on_complete_additional_args=None): - if os.path.isfile(file_path): - def single_chunk_generator(file_path): - with open(file_path, 'rb') as file_to_download: - while True: - chunk = file_to_download.read(1024) - if not chunk: - break - yield chunk - if on_complete_callback is not None: - on_complete_callback(file_path, on_complete_additional_args) - - - response = flask.Response(flask.stream_with_context( - single_chunk_generator(file_path))) - response.headers.set('Content-Disposition', - 'attachment', filename=download_filename) - response.headers.set('Content-Type', 'application/octet-stream') - return response - - return json.dumps({'success': False}), 404, {'ContentType': 'application/json'} - - def apply_camera_settings(self, camera_profiles): + @octoprint.plugin.BlueprintPlugin.route("/addArchiveToUnfinishedRenderings", methods=["POST"]) + @restricted_access + def add_archive_to_unfinished_renderings(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + snapshot_archive_name = utility.unquote(request_values["archive_name"]) + + # make sure the extension is correct + if not snapshot_archive_name.lower().endswith(".{0}".format(utility.snapshot_archive_extension)): + return jsonify({ + "success": False, + "error": "The selected archive is not a valid snapshot archive file." + }) + + # get the zip file path + snapshot_archive_directory = self._octolapse_settings.main_settings.get_snapshot_archive_directory( + self.get_plugin_data_folder() + ) + snapshot_archive_path = os.path.join(snapshot_archive_directory, snapshot_archive_name) + # attempt to import the zip file + try: + results = self._rendering_processor.import_snapshot_archive(snapshot_archive_path, prevent_archive=True) + except Exception as e: + logger.exception("Unable to import the snapshot archive.") + raise e + + if not results["success"]: + error = error_messages.get_error(results["error_keys"]) + logger.info(error["description"]) + return jsonify({ + "success": False, + "errors": [error] + }) + + return jsonify({ + "success": True + }) + + @octoprint.plugin.BlueprintPlugin.route("/importSnapshots", methods=["POST"]) + @restricted_access + def import_snapshots_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + # get the zip file path + # example of getting the path. Use the button name + path + # settings_path = request.values['octolapse_settings_import_path_upload.path'] + snapshot_archive_path = request.values['octolapse_snapshot_upload.path'] + snapshot_archive_name = request.values['octolapse_snapshot_upload.name'] + # get the extension + extension = utility.get_extension_from_filename(snapshot_archive_name) + if extension not in RenderingProfile.get_archive_formats(): + return jsonify({ + "success": False, + "error": "The file type is not allowed for upload." + }), 403 + + # get the archive folder + target_folder = self._octolapse_settings.main_settings.get_snapshot_archive_directory( + self.get_plugin_data_folder() + ) + target_path = utility.get_collision_free_filepath(os.path.join(target_folder, snapshot_archive_name)) + utility.move(snapshot_archive_path, target_path) + + file_info = utility.get_file_info(target_path) + file_info["type"] = utility.FILE_TYPE_SNAPSHOT_ARCHIVE + self.send_files_changed_message( + file_info, + 'added', + None + ) + + return jsonify({ + "success": True + }) + + @octoprint.plugin.BlueprintPlugin.route("/getFiles", methods=["POST"]) + @restricted_access + def get_files_request(self): + with OctolapsePlugin.admin_permission.require(http_exception=403): + request_values = request.get_json() + file_type = request_values["type"] + files = [] + + if file_type == utility.FILE_TYPE_SNAPSHOT_ARCHIVE: + def filter_archives(path, name, extension): + return extension in RenderingProfile.get_archive_formats() + + files = [ + file for file in utility.walk_files( + self._octolapse_settings.main_settings.get_snapshot_archive_directory( + self.get_plugin_data_folder() + ), + filter_archives + ) + ] + elif file_type == utility.FILE_TYPE_TIMELAPSE_OCTOLAPSE: + def filter_octolapse_timelapse(path, name, extension): + return extension in set(self.get_timelapse_extensions()) + + files = [ + file for file in utility.walk_files( + self._octolapse_settings.main_settings.get_timelapse_directory( + self.get_octoprint_timelapse_location() + ), + filter_octolapse_timelapse + ) + ] + elif file_type == utility.FILE_TYPE_TIMELAPSE_OCTOPRINT: + def filter_octoprint_timelapse(path, name, extension): + return extension in set(self.get_timelapse_extensions()) + files = [ + file for file in utility.walk_files( + self.get_octoprint_timelapse_location() + ) + ] + + return jsonify({ + "files": files + }) + + def apply_camera_settings(self, camera_profiles, retries=3, backoff_factor=0.3, no_wait=False): if camera_profiles is None: camera_profiles = self._octolapse_settings.profiles.active_cameras() - success, errors = camera.CameraControl.apply_camera_settings(camera_profiles) - if not success: + success, errors = camera.CameraControl.apply_camera_settings( + camera_profiles, retries=retries, backoff_factor=backoff_factor, no_wait=no_wait + ) + if not success and not no_wait: + logger.error(errors) return False, errors else: return True, None - def get_timelapse_folder(self): - return utility.get_rendering_directory_from_data_directory(self.get_plugin_data_folder()) - - def get_default_settings_path(self): - return os.path.join( - self.get_default_settings_folder(), "settings_default_{0}.json".format(self._plugin_version) - ) - - def get_default_settings_filename(self): - return "settings_default_{0}.json".format(self._plugin_version) + @staticmethod + def get_default_settings_filename(): + return "settings_default_current.json" def get_default_settings_folder(self): return os.path.join(self._basefolder, 'data') @@ -1346,7 +2003,7 @@ def get_log_file_path(self): def configure_loggers(self): logging_configurator.configure_loggers( - self.get_log_file_path(), self._octolapse_settings.profiles.current_debug_profile() + self.get_log_file_path(), self._octolapse_settings.profiles.current_logging_profile() ) def load_settings(self, force_defaults=False): @@ -1361,6 +2018,7 @@ def load_settings(self, force_defaults=False): new_settings, defaults_loaded = OctolapseSettings.load( settings_file_path, self._plugin_version, + __git_version__, self.get_default_settings_folder(), self.get_default_settings_filename(), self.get_plugin_data_folder(), @@ -1389,24 +2047,35 @@ def copy_octoprint_default_settings(self, apply_to_current_profile=False): # for all cameras. try: # adjust the snapshot url - o = urlparse(snapshot_url) - camera_address = o.scheme + "://" + o.netloc + o.path - logger.info("Setting octolapse camera address to %s.", camera_address) - snapshot_action = urlparse(snapshot_url).query - snapshot_request_template = "{camera_address}?" + snapshot_action + if snapshot_url: + o = urlparse(snapshot_url) + camera_address = o.scheme + "://" + o.netloc + o.path + logger.info("Setting octolapse camera address to %s.", camera_address) + snapshot_action = urlparse(snapshot_url).query + snapshot_request_template = "{camera_address}?" + snapshot_action + logger.info("Setting octolapse camera snapshot template to %s.", + snapshot_request_template.replace('{', '{{').replace('}', '}}')) # but why??? + self._octolapse_settings.profiles.defaults.camera.webcam_settings.address = camera_address + self._octolapse_settings.profiles.defaults.camera.webcam_settings.snapshot_request_template = snapshot_request_template + if apply_to_current_profile: + logger.info("Applying the default snapshot url to the Octolapse camera profiles.") + for profile in self._octolapse_settings.profiles.cameras.values(): + profile.webcam_settings.address = camera_address + profile.webcam_settings.snapshot_request_template = snapshot_request_template + else: + logger.warning("No snapshot url was set in the Octoprint settings, unable to apply defaults.") # adjust the webcam stream url webcam_stream_template = webcam_stream_url + if webcam_stream_template: + logger.info("Setting octolapse defalt streaming url to %s.", webcam_stream_url) + self._octolapse_settings.profiles.defaults.camera.webcam_settings.stream_template = webcam_stream_template + if apply_to_current_profile: + logger.info("Applying the default streaming url to the Octolapse camera profiles.") + for profile in self._octolapse_settings.profiles.cameras.values(): + profile.webcam_settings.stream_template = webcam_stream_template + else: + logger.warning("No streaming url was set in the Octoprint settings, unable to apply defaults.") - logger.info("Setting octolapse camera snapshot template to %s.", snapshot_request_template.replace('{', '{{').replace('}','}}')) - self._octolapse_settings.profiles.defaults.camera.webcam_settings.address = camera_address - self._octolapse_settings.profiles.defaults.camera.webcam_settings.snapshot_request_template = snapshot_request_template - self._octolapse_settings.profiles.defaults.camera.webcam_settings.stream_template = webcam_stream_template - - if apply_to_current_profile: - for profile in self._octolapse_settings.profiles.cameras.values(): - profile.webcam_settings.address = camera_address - profile.webcam_settings.snapshot_request_template = snapshot_request_template - profile.webcam_settings.stream_template = webcam_stream_template except Exception as e: # cannot send a popup yet,because no clients will be connected. We should write a routine that # checks to make sure Octolapse is correctly configured if it is enabled and send some kind of @@ -1416,17 +2085,24 @@ def copy_octoprint_default_settings(self, apply_to_current_profile=False): logger.exception("Unable to copy the default webcam settings from OctoPrint.") bitrate = self._settings.global_get(["webcam", "bitrate"]) - self._octolapse_settings.profiles.defaults.rendering.bitrate = bitrate - if apply_to_current_profile: - for profile in self._octolapse_settings.profiles.renderings.values(): - profile.bitrate = bitrate + if bitrate: + self._octolapse_settings.profiles.defaults.rendering.bitrate = bitrate + logger.info("Setting the default octolapse rendering bitrate to the Octoprint default of %s.", bitrate) + if apply_to_current_profile and bitrate: + logger.info("Applying default bitrate to the Octolapse rendering profiles.") + for profile in self._octolapse_settings.profiles.renderings.values(): + profile.bitrate = bitrate + else: + logger.warning("No rendering bitrate was set in the Octoprint settings. Unable to apply the defaults.") except Exception as e: logger.exception("Unable to copy default settings from OctoPrint.") def save_settings(self): # Save setting from file try: - settings_dict = self._octolapse_settings.save(self.get_settings_file_path()) + # set the git-version + self._octolapse_settings.main_settings.git_version = __git_version__ + self._octolapse_settings.save(self.get_settings_file_path()) self.configure_loggers() except Exception as e: logger.exception("Failed to save settings.") @@ -1444,51 +2120,81 @@ def queue_plugin_message(self, plugin_message): # EVENTS ######### def get_settings_defaults(self): - return dict(load=None, restore_default_settings=None) + return dict(version=self._plugin_version, load=None, restore_default_settings=None) + + def get_settings_version(self): + # This needs to be incremented for each file migration. What a pain. Currently set for V0.4.0rc1 + return 3 + + def on_settings_migrate(self, target, current): + # If we don't have a current version, look at the current settings file for the most recent version. + if current is None: + current_version = 'unknown' + if os.path.isfile(self.get_settings_file_path()): + current_version = OctolapseSettings.get_plugin_version_from_file( + self.get_settings_file_path() + ) + if current_version == 'unknown': + current_version = None + else: + current_version = get_version_from_settings_index(current) - def on_settings_load(self): - return dict(load=None, restore_default_settings=None) + has_migrated = migrate_files(current_version, self._plugin_version, self.get_plugin_data_folder()) + if has_migrated: + logger.info("Octolapse has migrated files from version index %d to %s", current, self._plugin_version) - def get_status_dict(self): + def get_status_dict(self, include_profiles=False): try: is_timelapse_active = False snapshot_count = 0 + snapshot_failed_count = 0 is_taking_snapshot = False is_rendering = False current_timelapse_state = TimelapseState.Idle is_waiting_to_render = False - profiles_dict = self._octolapse_settings.profiles.get_profiles_dict() - debug_dict = profiles_dict["debug"] + profiles_dict = None + is_test_mode_active = False + unfinished_renderings = None + in_process_renderings = None if self._timelapse is not None: - snapshot_count = self._timelapse.get_snapshot_count() + snapshot_count, snapshot_failed_count = self._timelapse.get_snapshot_count() is_timelapse_active = self._timelapse.is_timelapse_active() if is_timelapse_active: - profiles_dict = self._timelapse.get_current_profiles() - - # Always get the current debug settings, else they won't update from the tab while a timelapse is + is_test_mode_active = self._timelapse.get_is_test_mode_active() + # Always get the current logging settings, else they won't update from the tab while a timelapse is # running. - profiles_dict["current_debug_profile_guid"] = ( - self._octolapse_settings.profiles.current_debug_profile_guid - ) - profiles_dict["debug_profiles"] = debug_dict - # always get the latest current camera profile guid. - profiles_dict["current_camera_profile_guid"] = ( - self._octolapse_settings.profiles.current_camera_profile_guid - ) - - is_rendering = self._timelapse.get_is_rendering() + if include_profiles: + if is_timelapse_active: + profiles_dict = self._timelapse.get_current_settings().profiles.get_profiles_dict() + else: + profiles_dict = self._octolapse_settings.profiles.get_profiles_dict() + profiles_dict["current_logging_profile_guid"] = ( + self._octolapse_settings.profiles.current_logging_profile_guid + ) + profiles_dict["logging_profiles"] = profiles_dict["logging"] + # always get the latest current camera profile guid. + profiles_dict["current_camera_profile_guid"] = ( + self._octolapse_settings.profiles.current_camera_profile_guid + ) + is_rendering = False + if self._rendering_processor: + is_rendering = self._rendering_processor.is_processing() current_timelapse_state = self._timelapse.get_current_state() is_taking_snapshot = TimelapseState.TakingSnapshot == current_timelapse_state is_waiting_to_render = (not is_rendering) and current_timelapse_state == TimelapseState.WaitingToRender return { 'snapshot_count': snapshot_count, + 'snapshot_failed_count': snapshot_failed_count, 'is_timelapse_active': is_timelapse_active, 'is_taking_snapshot': is_taking_snapshot, 'is_rendering': is_rendering, + 'is_test_mode_active': is_test_mode_active, 'waiting_to_render': is_waiting_to_render, 'state': current_timelapse_state, - 'profiles': profiles_dict + 'profiles': profiles_dict, + 'unfinished_renderings': unfinished_renderings, + 'in_process_renderings': in_process_renderings } except Exception as e: logger.exception("Failed to create status dict.") @@ -1503,96 +2209,109 @@ def create_timelapse_object(self): self.get_current_octolapse_settings, self._printer, self.get_plugin_data_folder(), - self._settings.settings.getBaseFolder("timelapse"), + self.get_default_settings_folder(), + self._settings, on_print_started=self.on_print_start, on_print_start_failed=self.on_print_start_failed, - on_render_start=self.on_render_start, - on_render_success=self.on_render_success, - on_render_error=self.on_render_error, - on_render_end=self.on_render_end, on_snapshot_start=self.on_snapshot_start, on_snapshot_end=self.on_snapshot_end, on_new_thumbnail_available=self.on_new_thumbnail_available, + on_post_processing_error_callback=self.on_post_processing_error_callback, on_timelapse_stopping=self.on_timelapse_stopping, on_timelapse_stopped=self.on_timelapse_stopped, on_timelapse_end=self.on_timelapse_end, on_state_changed=self.on_timelapse_state_changed, on_snapshot_position_error=self.on_snapshot_position_error, - on_position_error=self.on_position_error + on_rendering_start=self.add_rendering_job ) def get_available_server_profiles_path(self): return os.path.join(self.get_plugin_data_folder(), "server_profiles.json".format()) - def start_automatic_updates(self): - # create function to load makes and models - def _get_available_profiles(): - # load all available profiles from the server - try: - available_profiles = ExternalSettings.get_available_profiles( - self._plugin_version, self.get_available_server_profiles_path() - ) - # update the profile options within the octolapse settings - self._octolapse_settings.profiles.options.update_server_options(available_profiles) - # notify the clients of the makes and models changes - data = { - 'type': 'server_profiles_updated', - 'external_profiles_list_changed': available_profiles - } - self._plugin_manager.send_plugin_message(self._identifier, data) - return available_profiles - except ExternalSettingsError as e: - logger.exception("Unable to fetch available profiles from the server.") - return None + def _update_available_server_profiles(self): + # load all available profiles from the server + have_profiles_changed = False + + available_profiles = ExternalSettings.get_available_profiles( + self._plugin_version, self.get_available_server_profiles_path() + ) + if available_profiles: + have_profiles_changed = self.available_profiles != available_profiles + self.available_profiles = available_profiles + # update the profile options within the octolapse settings + self._octolapse_settings.profiles.options.update_server_options(available_profiles) + + return have_profiles_changed - # create a function to do all of the updates + def start_automatic_updates(self): + # create a function to do all of the updates def _update(): if not self._octolapse_settings.main_settings.automatic_updates_enabled: return - with self.automatic_update_lock: - logger.info("Checking for profile updates.") - available_profiles = _get_available_profiles() - if available_profiles: - self.available_profiles = available_profiles - self.check_for_updates() - else: - logger.info("There are no automatic profiles to update.") + logger.info("Checking for profile updates.") + self.check_for_updates(notify=True) update_interval = 60 * 60 * 24 * self._octolapse_settings.main_settings.automatic_update_interval_days - self.automatic_update_thread = utility.RecurringTimerThread( - update_interval, _update, self.automatic_update_cancel - ) - # do an initial update now - _update() - self.automatic_update_thread.start() + with self.automatic_update_lock: + if self.automatic_update_thread is not None: + self.automatic_update_cancel.set() + self.automatic_update_cancel.clear() + self.automatic_update_thread.join() - update_interval = 30 # three minutes + self.automatic_update_thread = utility.RecurringTimerThread( + update_interval, _update, self.automatic_update_cancel + ) + self.automatic_update_thread.daemon = True - # create another thread to notify users when new updates are available - self.automatic_updates_notification_thread = utility.RecurringTimerThread( - update_interval, self.notify_updates, self.automatic_update_cancel - ) + # load available server profiles before starting the automatic server profile update + self.load_available_server_profiles() + self.automatic_update_thread.start() - self.automatic_updates_notification_thread.start() + def load_available_server_profiles(self): + with self.automatic_update_lock: + if self._update_available_server_profiles(): + # notify the clients of the makes and models changes + data = { + 'type': 'external_profiles_list_changed', + 'server_profiles': self.available_profiles + } + self._plugin_manager.send_plugin_message(self._identifier, data) + if not self.available_profiles: + logger.warning("No server profiles are available.") + return # create function to update all existing automatic profiles - def check_for_updates(self, wait_for_lock=False, force_updates=False): - try: - if wait_for_lock: - self.automatic_update_lock.acquire() - self.automatic_updates_available = ExternalSettings.check_for_updates( - self.available_profiles, self._octolapse_settings.profiles.get_updatable_profiles_dict(), force_updates + def check_for_updates(self, force_updates=False, notify=False, ignore_suppression=False): + self.load_available_server_profiles() + with self.automatic_update_lock: + if not self.available_profiles: + logger.warning("Can't check for profile updates when no server profiles are available.") + return + + profiles_to_update = ExternalSettings.check_for_updates( + self.available_profiles, + self._octolapse_settings.profiles.get_updatable_profiles_dict(), + force_updates, + ignore_suppression ) - if self.automatic_updates_available: + if profiles_to_update: + if notify: + num_available = 0 + # remove python 2 support + # for key, profile_type in six.iteritems(profiles_to_update): + for key, profile_type in profiles_to_update.items(): + num_available += len(profile_type) + data = { + "type": "updated-profiles-available", + "available_profile_count": num_available + } + self._plugin_manager.send_plugin_message(self._identifier, data) logger.info("Profile updates are available.") else: logger.info("No profile updates are available.") - except ExternalSettingsError as e: - logger.exception(e) - finally: - if wait_for_lock: - self.automatic_update_lock.release() + + return profiles_to_update def notify_updates(self): if not self._octolapse_settings.main_settings.automatic_updates_enabled: @@ -1601,10 +2320,6 @@ def notify_updates(self): if self.automatic_updates_available: logger.info("Profile updates are available from the server.") # create an automatic updates available message - data = { - "type": "updated-profiles-available" - } - self._plugin_manager.send_plugin_message(self._identifier, data) def on_startup(self, host, port): try: @@ -1632,11 +2347,35 @@ def on_startup(self, host, port): # apply camera settings if necessary startup_cameras = self._octolapse_settings.profiles.after_startup_cameras() + # note that errors here will ONLY show up in the log. if len(startup_cameras) > 0: - self.apply_camera_settings(camera_profiles=startup_cameras) + self.apply_camera_settings(camera_profiles=startup_cameras, retries=9, backoff_factor=0.1, no_wait=True) + # start automatic updates self.start_automatic_updates() + + # start the rendering processor + self._rendering_processor = RenderingProcessor( + self._rendering_task_queue, + self._data_folder, + self._octolapse_settings.main_settings.version, + __git_version__, + self.get_default_settings_folder(), + self._settings, + self.get_current_octolapse_settings, + self.on_render_start, + self.on_render_success, + self.on_render_progress, + self.on_render_error, + self.on_render_end, + self.send_failed_renderings_changed_message, + self.send_in_process_renderings_changed_message, + self.send_unfinished_renderings_loaded_message + ) + self._rendering_processor.daemon = True + self._rendering_processor.start() + # log the loaded state logger.info("Octolapse - loaded and active.") except Exception as e: @@ -1648,33 +2387,15 @@ def on_shutdown(self): # Event Mixin Handler def on_event(self, event, payload): - try: # If we haven't loaded our settings yet, return. - if self._octolapse_settings is None or self._timelapse is None: + if self._octolapse_settings is None or self._timelapse is None or self._timelapse.get_current_state() == TimelapseState.Idle: return - - logger.verbose("Printer event received:%s.", event) - - if event == Events.PRINT_STARTED: - # warn and cancel print if not printing locally - if not self._octolapse_settings.main_settings.is_octolapse_enabled: - return - - if ( - payload["origin"] != "local" - ): - self._timelapse.end_timelapse("FAILED") - message = "Octolapse cannot be used when printing from the SD card. Disable Octolapse to print " \ - "from SD. " - logger.info(message) - self.on_print_start_failed(message) - return if event == Events.PRINTER_STATE_CHANGED: self.send_state_changed_message({"status": self.get_status_dict()}) - if event == Events.CONNECTIVITY_CHANGED: + elif event == Events.CONNECTIVITY_CHANGED: self.send_state_changed_message({"status": self.get_status_dict()}) - if event == Events.CLIENT_OPENED: + elif event == Events.CLIENT_OPENED: self.send_state_changed_message({"status": self.get_status_dict()}) elif event == Events.DISCONNECTING: self.on_printer_disconnecting() @@ -1683,18 +2404,25 @@ def on_event(self, event, payload): elif event == Events.PRINT_PAUSED: self.on_print_paused() elif event == Events.HOME: - logger.info("homing to payload:%s.".format(payload)) + logger.info("homing to payload: %s.", payload) elif event == Events.PRINT_RESUMED: logger.info("Print Resumed.") self.on_print_resumed() elif event == Events.PRINT_FAILED: self.on_print_failed() + self.on_print_end() elif event == Events.PRINT_CANCELLING: self.on_print_cancelling() elif event == Events.PRINT_CANCELLED: self.on_print_canceled() elif event == Events.PRINT_DONE: self.on_print_completed() + self.on_print_end() + elif event == Events.SETTINGS_UPDATED: + # See if the ffmpeg directory changed. + ffmpeg_directory = self._settings.global_get(["webcam", "ffmpeg"]) + if self._rendering_processor.set_ffmpeg_directory(ffmpeg_directory): + logger.info("FFMPEG directory changed to %s, updating the rendering processor", ffmpeg_directory) except Exception as e: logger.exception("An error occurred while handling an OctoPrint event.") raise e @@ -1706,7 +2434,6 @@ def on_print_paused(self): self._timelapse.on_print_paused() def on_print_start(self, parsed_command): - """Return True in order to release the printer job lock, False keeps it locked.""" logger.info( "Print start detected, attempting to start timelapse." ) @@ -1714,164 +2441,221 @@ def on_print_start(self, parsed_command): try: results = self.test_timelapse_config() if not results["success"]: - self.on_print_start_failed(results["error-message"]) - return True + self.on_print_start_failed(results["error"], parsed_command=parsed_command) + return # get all of the settings we need timelapse_settings = self.get_timelapse_settings() if not timelapse_settings["success"]: - self.on_print_start_failed(timelapse_settings["error-message"]) - return True + errors = timelapse_settings["errors"] + if len(errors) == 0: + errors = timelapse_settings["warnings"] + self.on_print_start_failed(errors, parsed_command=parsed_command) + return if len(timelapse_settings["warnings"]) > 0: - self.send_plugin_message("warning", "\r\n".join(timelapse_settings["warnings"])) + self.send_plugin_errors("warning", timelapse_settings["warnings"], parsed_command=parsed_command) - gcode_file_path = timelapse_settings["gcode_file_path"] settings_clone = timelapse_settings["settings"] current_trigger_clone = settings_clone.profiles.current_trigger() - preprocessed = False if current_trigger_clone.trigger_type in TriggerProfile.get_precalculated_trigger_types(): - preprocessed = True # pre-process the stabilization # this is done in another process, so we'll have to exit and wait for the results self.pre_process_stabilization( timelapse_settings, parsed_command ) + # exit and allow the timelapse pre-processing routine to complete. + elif self.start_timelapse(timelapse_settings): + self._timelapse.release_job_on_hold_lock(parsed_command=parsed_command) - # return false so the print job lock isn't released - return False - - self.start_timelapse(timelapse_settings) - return True except Exception as e: + error = error_messages.get_error(["init", "timelapse_start_exception"]) logger.exception("Unable to start the timelapse.") - self.on_print_start_failed("Unable to start the timelapse. See plugin_octolapse.log for details") - return True + self.on_print_start_failed([error], parsed_command=parsed_command) def test_timelapse_config(self): + logger.debug("Testing timelapse configuration.") + + logger.verbose("Verify that Octolapse is enabled.") if not self._octolapse_settings.main_settings.is_octolapse_enabled: - logger.info("Octolapse is disabled. Cannot start timelapse.") - return {"success": False, "error": "disabled", "error-message": "Octolapse is disabled."} + logger.error("Octolapse is not enabled. Cannot start timelapse.") + error = error_messages.get_error(["init","octolapse_is_disabled"]) + logger.error(error["description"]) + return {"success": False, "error": error} + + logger.verbose("Ensure that we have at least one printer profile.") + # make sure that at least one profile is available + if len(self._octolapse_settings.profiles.printers) == 0: + logger.error("No printer profiles exist. Cannot start timelapse.") + error = error_messages.get_error(["init", "no_printer_profile_exists"]) + return {"success": False, "error": error} - # make sure we have an active printer + # make sure we have an active printer (a selected profile) + logger.verbose("Ensure one printer profile is active and selected.") if self._octolapse_settings.profiles.current_printer() is None: - message = "You must select a printer before using Octolapse. Either choose a printer profile or disable" \ - " Octolapse via the Octolapse tab." - return {"success": False, "error": "printer-not-configured", "error-message": message} + logger.error("There is no currently selected printer profile. Cannot start timelapse.") + error = error_messages.get_error(["init", "no_printer_profile_selected"]) + logger.error(error["description"]) + return {"success": False, "error": error} # see if the printer profile has been configured + logger.verbose("Ensure the current printer profile has been configured or that the configuration is automatic.") if ( not self._octolapse_settings.profiles.current_printer().has_been_saved_by_user and not self._octolapse_settings.profiles.current_printer().slicer_type == "automatic" ): - message = "Your Octolapse printer profile has not been configured. To fix this error go to the Octolapse" \ - " tab, edit your selected printer via the 'gear' icon, and save your changes." - return {"success": False, "error": "printer-not-configured", "error-message": message} + logger.error("The selected printer profile is not configured, or the slicer type is not automatic. Cannot start timelapse.") + error = error_messages.get_error(["init", "printer_not_configured"]) + logger.error(error["description"]) + return {"success": False, "error": error} # determine the file source + logger.verbose("Ensure data exists about the current job.") printer_data = self._printer.get_current_data() current_job = printer_data.get("job", None) if not current_job: - message = "Octolapse was unable to acquire job start information from Octoprint." \ - " Please see plugin_octolapse.log for details." + logger.error("Data about the current job could not be found. Cannot start timelapse.") + error = error_messages.get_error(["init", "no_current_job_data_found"]) log_message = "Failed to get current job data on_print_start:" \ " Current printer data: {0}".format(printer_data) logger.error(log_message) - return {"success": False, "error": "print-job-info-not-available", "error-message": message} + return {"success": False, "error": error} + logger.verbose("Ensure current job has an associated file.") current_file = current_job.get("file", None) if not current_file: - message = "Octolapse was unable to acquire file information from the current job." \ - " Please see plugin_octolapse.log for details." + logger.error("The current job has no associated file. Cannot start timelapse.") + error = error_messages.get_error(["init", "no_current_job_file_data_found"]) log_message = "Failed to get current file data on_print_start:" \ " Current job data: {0}".format(current_job) logger.error(log_message) - return {"success": False, "error": "print-file-info-not-available", "error-message": message} + # ERROR_REPLACEMENT + return {"success": False, "error": error} + logger.verbose("Ensure origin information is available for the current job.") current_origin = current_file.get("origin", "unknown") if not current_origin: - message = "Octolapse cannot tell if you are printing from an SD card or streaming via Octoprint." \ - " Please see plugin_octolapse.log for details." - self.on_print_start_failed(message) + logger.error("File origin information does not exist within the current job info. Cannot start timelapse.") + error = error_messages.get_error(["init", "unknown_file_origin"]) log_message = "Failed to get current origin data on_print_start:" \ "Current file data: {0}".format(current_file) + # ERROR_REPLACEMENT logger.error(log_message) - return {"success": False, "error": "print-origin-info-unavailable", "error-message": message} + return {"success": False, "error": error} + logger.verbose("Ensure the file is being printed locally (not from SD).") if current_origin != "local": - message = "Octolapse only works when printing locally. The current source ({0}) is incompatible. " \ - "Disable octolapse if you want to print from the SD card.".format(current_origin) + logger.error("Octolapse does not support printing from SD. Cannot start timelapse.") + error = error_messages.get_error(["init", "cant_print_from_sd"]) log_message = "Unable to start Octolapse when printing from {0}.".format(current_origin) - logger.warning(log_message) - - return {"success": False, "error": "incompatible-file-source", "error-message": message} + logger.error(log_message) + # ERROR_REPLACEMENT + return {"success": False, "error": error} + logger.verbose("Ensure that Octolapse is in the correct state (Initializing).") if self._timelapse.get_current_state() != TimelapseState.Initializing: - message = "Unable to start the timelapse when not in the Initializing state." \ - " Please see plugin_octolapse.log for details." + logger.error("Octolapse is not in the correct state. Cannot start timelapse.") + error = error_messages.get_error(["init", "incorrect_printer_state"]) log_message = "Octolapse was in the wrong state at print start. StateId: {0}".format( self._timelapse.get_current_state()) logger.error(log_message) - return {"success": False, "error": "incorrect-timelapse-state", "error-message": message} + # ERROR_REPLACEMENT + return {"success": False, "error": error} # test all cameras and look for at least one enabled camera + logger.verbose("Testing all enabled cameras.") found_camera = False for current_camera in self._octolapse_settings.profiles.active_cameras(): if current_camera.enabled: found_camera = True if current_camera.camera_type == "webcam": # test the camera and see if it works. - success, errors = camera.CameraControl.test_web_camera(current_camera, is_before_print_test=True) + success, camera_test_errors = camera.CameraControl.test_web_camera( + current_camera, is_before_print_test=True) if not success: - return {"success": False, "error": "camera-profile-failed", "error-message": errors} + error = error_messages.get_error(["init", "camera_init_test_failed"], error=camera_test_errors) + return {"success": False, "error": error} if not found_camera: - message = "There are no enabled cameras. Enable at least one camera profile and try again." - return {"success": False, "error": "no-cameras-enabled", "error-message": message} + logger.error("No enabled cameras could be found, please enable at least one camera profile and ensure it passes tests.") + error = error_messages.get_error(["init", "no_enabled_cameras"]) + return {"success": False, "error": error} - success, errors = camera.CameraControl.apply_camera_settings( - self._octolapse_settings.profiles.before_print_start_cameras() + logger.verbose("Attempting to apply any existing custom camera preferences, if any are set.") + success, camera_settings_apply_errors = camera.CameraControl.apply_camera_settings( + self._octolapse_settings.profiles.before_print_start_webcameras() ) if not success: - message = "Octolapse could not apply custom image preferences, or running a camera startup script. Please see this link for assistance with this error. Details:" \ - " {0}".format(errors) - self.on_print_start_failed(message) - return {"success": False, "error": "camera-settings-apply-failed", "error-message": message} - - # check for version 1.3.7 min + logger.error("Unable to apply custom camera preferences, failed to start timelapse.") + error = error_messages.get_error( + ["init", "camera_settings_apply_failed"], + error=camera_settings_apply_errors) + return {"success": False, "error": error} + + # run before print start camera scripts + logger.verbose("Running 'before print' camera scripts, if any exist.") + success, before_print_start_camera_script_errors = camera.CameraControl.run_on_print_start_script( + self._octolapse_settings.profiles.cameras + ) + if not success: + logger.error("Errors were encountered running 'before print' camera scripts. Failed to start timelapse.") + error = error_messages.get_error( + ["init", "before_print_start_camera_script_apply_failed"], + error=before_print_start_camera_script_errors) + return {"success": False, "error": error} + + # check for version 1.3.8 min + logger.verbose("Ensure we are running at least OctoPrint version 1.3.8.") if not (LooseVersion(octoprint.server.VERSION) > LooseVersion("1.3.8")): - message = "Octolapse requires Octoprint v1.3.9 rc3 or above, but version v{0} is installed." \ - " Please update Octoprint to use Octolapse.".format(octoprint.server.DISPLAY_VERSION), - return {"success": False, "error": "octoprint-version-incompatible", "error-message": message} + logger.error("The current octoprint version is older than V1.3.8, and is not supported. Failed to start " + "timelapse.") + error = error_messages.get_error( + ["init", "incorrect_octoprint_version"], installed_version=octoprint.server.DISPLAY_VERSION + ) + return {"success": False, "error": error} # Check the rendering filename template + logger.verbose("Validating the rendering file template.") if not render.is_rendering_template_valid( self._octolapse_settings.profiles.current_rendering().output_template, self._octolapse_settings.profiles.options.rendering['rendering_file_templates'], ): - message = "The rendering file template is invalid. Please correct the template" \ - " within the current rendering profile." - return {"success": False, "error": "rendering-file-template-invalid", "error-message": message} - - # make sure that at least one profile is available - if len(self._octolapse_settings.profiles.printers) == 0: - message = "There are no printer profiles. Cannot start timelapse. " \ - "Please create a printer profile in the octolapse settings pages and " \ - "restart the print." - return {"success": False, "error": "no-printer-profiles-available", "error-message": message} - # check to make sure a printer is selected - if self._octolapse_settings.profiles.current_printer() is None: - message = "No default printer profile was selected. Cannot start timelapse. " \ - "Please select a printer profile in the octolapse settings pages and " \ - "restart the print." - return {"success": False, "error": "no-printer-profile-selected", "error-message": message} + logger.error("The rendering file template is invalid. Cannot start timelapse.") + error = error_messages.get_error(["init", "rendering_file_template_invalid"]) + return {"success": False, "error": error} + # test the main settings directories + logger.verbose("Verify that the folder structure is valid.") + success, errors = self._octolapse_settings.main_settings.test_directories( + self.get_plugin_data_folder(), + self.get_octoprint_timelapse_location() + ) + if not success: + logger.error("The Octolapse folder structure is invalid. Cannot start timelapse.") + error_folders = ",".join([x["name"] for x in errors]) + error = error_messages.get_error(["init", "directory_test_failed"], failed_directories=error_folders) + return {"success": False, "error": error} + + # test rendering overlay font path + logger.verbose("Verify the overlay font path is correct, if rendering overlays are being used.") + current_rendering_profile = self._octolapse_settings.profiles.current_rendering() + if ( + current_rendering_profile.overlay_text_template and + not os.path.isfile(current_rendering_profile.overlay_font_path) + ): + logger.error("The rendering overlay template font path is invalid. Cannot start timelapse.") + error = error_messages.get_error( + ["init", "overlay_font_path_not_found"], + overlay_font_path=current_rendering_profile.overlay_font_path) + return {"success": False, "error": error} + logger.debug("Timelapse configuration is valid!") return {"success": True} + def get_octoprint_timelapse_location(self): + return self._settings.settings.getBaseFolder("timelapse") + def get_octoprint_g90_influences_extruder(self): - return self._settings.global_get(["feature", "g90_influences_extruder"]) + return self._settings.global_get(["feature", "g90InfluencesExtruder"]) def get_octoprint_printer_profile(self): return self._printer_profile_manager.get_current() @@ -1880,55 +2664,64 @@ def get_timelapse_settings(self): # Create a copy of the settings to send to the Timelapse object. # We make this copy here so that editing settings vis the GUI won't affect the # current timelapse. + logger.debug("Getting timelapse settings.") settings_clone = self._octolapse_settings.clone() current_printer_clone = settings_clone.profiles.current_printer() - has_automatic_settings_issue = False - automatic_settings_error = None - warnings = [] + return_value = { + "success": False, + "errors": [], + "warnings": [], + "settings": None, + "overridable_printer_profile_settings": None, + "ffmpeg_path": None, + "gcode_file_path": None, + "error_code": None, + "error_message": None, + "help_link": None + } - gcode_file_path = None path = utility.get_currently_printing_file_path(self._printer) if path is not None: gcode_file_path = self._file_manager.path_on_disk(octoprint.filemanager.FileDestinations.LOCAL, path) else: - message = "Could not find the gcode file path. Cannot start timelapse." - return {"success": False, "error": "no-gcode-file-path-found", "error-message": message} + error = error_messages.get_error(["init", "no_gcode_filepath_found"]) + logger.error(error["description"]) + return_value["errors"].append(error) + return return_value # check the ffmpeg path - ffmpeg_path = None try: ffmpeg_path = self._settings.global_get(["webcam", "ffmpeg"]) if ( self._octolapse_settings.profiles.current_rendering().enabled and (ffmpeg_path == "" or ffmpeg_path is None) ): - log_message = "A timelapse was started, but there is no ffmpeg path set!" - logger.error(log_message) - message = "No ffmpeg path is set. Please configure this setting within the Octoprint settings " \ - "pages located at Features->Webcam & Timelapse under Timelapse Recordings->Path to " \ - "FFMPEG." - return {"success": False, "error": "ffmpeg-path-not-set", "error-message": message} + error = error_messages.get_error(["init", "ffmpeg_path_not_set"]) + logger.error(error["description"]) + return_value["errors"].append(error) + return return_value except Exception as e: - message = "An exception occurred and was logged while trying to acquire the ffmpeg path from Octoprint." \ - "Please configure this setting within the Octoprint settings pages located at Features->Webcam " \ - "& Timelapse under Timelapse Recordings->Path to FFMPEG." - logger.exception(message) - return {"success": False, "error": "exception-receiving-ffmpeg-path", "error-message": message} + logger.exception("An exception occurred while retrieving the ffmpeg/avconv path from octoprint.") + error = error_messages.get_error(["init", "ffmpeg_path_retrieve_exception"]) + return_value["errors"].append(error) + return return_value if not os.path.isfile(ffmpeg_path): - message = "The ffmpeg {0} does not exist. Please configure this setting within the Octoprint " \ - "settings pages located at Features->Webcam & Timelapse under Timelapse Recordings->Path " \ - "to FFMPEG.".format(ffmpeg_path) - return {"success": False, "error": "ffmpeg-not-at-path", "error-message": message} + error = error_messages.get_error(["init", "ffmpeg_not_found_at_path"]) + logger.error(error["description"]) + return_value["errors"].append(error) + return return_value if current_printer_clone.slicer_type == 'automatic': # extract any slicer settings if possible. This must be done before any calls to the printer profile # info that includes slicer setting - success, error_type, error_list = current_printer_clone.get_gcode_settings_from_file(gcode_file_path) + try: + success, error_type, error_list = current_printer_clone.get_gcode_settings_from_file(gcode_file_path) + except error_messages.OctolapseException as e: + logger.error(str(e)) + return_value["errors"].append(e.to_dict()) + return return_value if success: - settings_saved = False - - updated_profile_json = None # Save the profile changes # get the extracted slicer settings extracted_slicer_settings = current_printer_clone.get_current_slicer_settings() @@ -1946,33 +2739,34 @@ def get_timelapse_settings(self): self.send_slicer_settings_detected_message(settings_saved, updated_profile_json) else: if self._octolapse_settings.main_settings.cancel_print_on_startup_error: - # If you are using Cura, see this link: TODO: ADD LINK TO CURA FEATURE TEMPLATE AND INSTRUCTIONS if error_type == "no-settings-detected": - message = "No slicer settings could be extracted from the gcode file. Please check the " \ - "gcode file for issues. If you are using Cura, please see this link: " \ - " https://github.com/FormerLurker/Octolapse/wiki/Automatic-Slicer-Settings#cura-settings-extraction" - return { - "success": False, "error": "no-automatic-slicer-settings-detected", - "error-message": message - } + error = error_messages.get_error(["init", "automatic_slicer_no_settings_found"]) + logger.error(error["description"]) + return_value["errors"].append(error) + return return_value else: - message = "Some required slicer settings are missing from your gcode file. Unable to proceed" \ - ". Missing Settings: {0}".format(",".join(error_list)) - return { - "success": False, "error": "missing-automatic-slicer-settings", "error-message": message - } + error = error_messages.get_error( + ["init", "automatic_slicer_settings_missing"], missing_settings=",".join(error_list) + ) + logger.error(error["description"]) + return_value["errors"].append(error) + return return_value else: - has_automatic_settings_issue = True if error_type == "no-settings-detected": - warning_message = "Automatic settings extraction failed - No settings found in gcode" \ - " file - Continue on failure is enabled so your print will" \ - " continue, but the timelapse has been aborted." + error = error_messages.get_error( + ["init", "automatic_slicer_no_settings_found_continue_printing"] + ) + logger.error(error["description"]) + return_value["warnings"].append(error) + return return_value else: - warning_message = "Automatic settings extraction failed - Required settings could not" \ - " be found. Continue on failure is enabled so your print will continue," \ - " but the timelapse has been aborted. Missing settings: {0}" \ - .format(",".join(error_list)) - warnings.push(warning_message) + error = error_messages.get_error( + ["init", "automatic_slicer_settings_missing_continue_printing"], + missing_settings=",".join(error_list) + ) + logger.error(error["description"]) + return_value["warnings"].append(error) + return return_value else: # see if the current printer profile is missing any required settings # it is important to check here in case automatic slicer settings extraction @@ -1982,31 +2776,71 @@ def get_timelapse_settings(self): slicer_type=settings_clone.profiles.current_printer().slicer_type ) if len(missing_settings) > 0: - message = "Unable to start the print. Some required slicer settings are missing or corrupt: {0}" \ - .format(",".join(missing_settings)) - return {"success": False, "error": "missing-manual-slicer-settings","error-message": message} + error = error_messages.get_error( + ["init", "manual_slicer_settings_missing"], + missing_settings=",".join(missing_settings) + ) + logger.error(error["description"]) + return_value["errors"].append(error) + return return_value + + slicer_settings_clone = settings_clone.profiles.current_printer().get_current_slicer_settings() current_printer_clone = settings_clone.profiles.current_printer() + # Make sure the printer profile has the correct number of extruders defined + if len(slicer_settings_clone.extruders) > current_printer_clone.num_extruders: + error = error_messages.get_error( + ["init", "too_few_extruders_defined"], + printer_num_extruders=current_printer_clone.num_extruders, + gcode_num_extruders=len(slicer_settings_clone.extruders) + ) + logger.error(error["description"]) + return_value["errors"].append(error) + return return_value + + # make sure there are enough extruder offsets + if ( + not current_printer_clone.shared_extruder and + current_printer_clone.num_extruders < len(current_printer_clone.extruder_offsets) + ): + error = error_messages.get_error( + ["init", "too_few_extruder_offsets_defined"], + num_extruders=current_printer_clone.num_extruders, + num_extruder_offsets=len(current_printer_clone.extruder_offsets) + ) + logger.error(error["description"]) + return_value["errors"].append(error) + return return_value + overridable_printer_profile_settings = current_printer_clone.get_overridable_profile_settings( self.get_octoprint_g90_influences_extruder(), self.get_octoprint_printer_profile() ) - return { - 'success': True, - 'warning': warnings, - "settings": settings_clone, - "overridable_printer_profile_settings": overridable_printer_profile_settings, - "ffmpeg_path": ffmpeg_path, - "gcode_file_path": gcode_file_path, - "warnings": warnings - } + return_value["success"] = True + return_value["settings"] = settings_clone + return_value["overridable_printer_profile_settings"] = overridable_printer_profile_settings + return_value["ffmpeg_path"] = ffmpeg_path + return_value["gcode_file_path"] = gcode_file_path + logger.debug("Timelapse settings retrieved.") + return return_value def start_timelapse(self, timelapse_settings, snapshot_plans=None): - self._timelapse.start_timelapse( - timelapse_settings["settings"], - timelapse_settings["overridable_printer_profile_settings"], - timelapse_settings["ffmpeg_path"], - timelapse_settings["gcode_file_path"], - snapshot_plans=snapshot_plans - ) + + try: + self._timelapse.start_timelapse( + timelapse_settings["settings"], + timelapse_settings["overridable_printer_profile_settings"], + timelapse_settings["gcode_file_path"], + snapshot_plans=snapshot_plans + ) + except TimelapseStartException as e: + logger.exception("Unable to start the timelapse. Error Details: %s", e.message) + self.on_print_start_failed([error_messages.get_error(['init', e.type])]) + return False + except Exception as e: + message = "An unexpected exception occurred while starting the timelapse. See plugin_octolapse.log for " \ + "details. " + logger.exception(message) + self.on_print_start_failed([error_messages.get_error(['init', 'unexpected_exception'])]) + return False # send G90/G91 if necessary, note that this must come before M82/M83 because sometimes G90/G91 affects # the extruder. @@ -2027,24 +2861,51 @@ def start_timelapse(self, timelapse_settings, snapshot_plans=None): logger.info("Print Started - Timelapse Started.") self.on_timelapse_start() - - def on_print_start_failed(self, error): - # see if there is a job lock, if you find one release it - self._timelapse.release_job_on_hold_lock() - if self._octolapse_settings.main_settings.cancel_print_on_startup_error: - message = "Unable to start the timelapse. Cancelling print. Error: {0}".format(error) - self._printer.cancel_print(tags={'startup-failed'}) + return True + + def on_print_start_failed(self, errors, parsed_command=None): + if not isinstance(errors, list): + errors = [errors] + + cancel_print = self._octolapse_settings.main_settings.cancel_print_on_startup_error + is_warning = not cancel_print + + if len(errors) == 1: + error = errors[0] + if "options" in error: + options = error["options"] + if "is_warning" in options: + is_warning = options["is_warning"] + if "cancel_print" in options: + cancel_print = options["cancel_print"] + + if cancel_print: + parsed_command = None # don't send any commands, we've cancelled. + self._printer.cancel_print(tags={'octolapse-startup-failed'}) + + if is_warning: + self.send_plugin_errors("print-start-warning", errors=errors) else: - message = "Unable to start the timelapse. Continuing print without Octolapse. Error: {0}".format(error) - logger.error(message) - self.send_plugin_message("print-start-error", message) + self.send_plugin_errors("print-start-error", errors=errors) + + # see if there is a job lock, if you find one release it, and don't wait for signals. + self._timelapse.release_job_on_hold_lock(force=True, reset=True, parsed_command=parsed_command) + + def on_preprocessing_failed(self, errors): + message_type = "gcode-preprocessing-failed" + self.send_plugin_errors(message_type, errors) def pre_process_stabilization( self, timelapse_settings, parsed_command ): - self._preprocessing_progress_queue = queue.Queue() - # create the thread - preprocessor = StabilizationPreprocessingThread( + logger.debug( + "Pre-Processing trigger detected, starting pre-processing thread." + ) + if self._stabilization_preprocessor_thread is not None: + self._stabilization_preprocessor_thread.join() + self._stabilization_preprocessor_thread = None + + self._stabilization_preprocessor_thread = StabilizationPreprocessingThread( timelapse_settings, self.send_pre_processing_progress_message, self.on_pre_processing_start, @@ -2054,13 +2915,15 @@ def pre_process_stabilization( notification_period_seconds=self.PREPROCESSING_NOTIFICATION_PERIOD_SECONDS, ) - preprocessor.start() + self._stabilization_preprocessor_thread.daemon = True + self._stabilization_preprocessor_thread.start() - def pre_preocessing_complete(self, success, errors, is_cancelled, snapshot_plans, seconds_elapsed, - gcodes_processed, lines_processed, timelapse_settings, parsed_command): + def pre_preocessing_complete(self, success, is_cancelled, snapshot_plans, seconds_elapsed, + gcodes_processed, lines_processed, missed_snapshots, quality_issues, + processing_issues, timelapse_settings, parsed_command): if not success: # An error occurred - self.pre_processing_failed(errors) + self.pre_processing_failed(processing_issues) else: if is_cancelled: self._timelapse.preprocessing_finished(None) @@ -2068,16 +2931,22 @@ def pre_preocessing_complete(self, success, errors, is_cancelled, snapshot_plans else: self.pre_processing_success( timelapse_settings, parsed_command, snapshot_plans, seconds_elapsed, - gcodes_processed, lines_processed + gcodes_processed, lines_processed, quality_issues, missed_snapshots ) # complete, exit loop - def cancel_preprocessing(self): - if self._preprocessing_cancel_event.is_set(): + def reset_preprocessing(self): + if self.preprocessing_job_guid is not None: self._preprocessing_cancel_event.clear() + self.preprocessing_job_guid = None self.saved_timelapse_settings = None self.saved_snapshot_plans = None self.saved_parsed_command = None + self.snapshot_plan_preview_autoclose = False + self.snapshot_plan_preview_close_time = 0 + self.saved_preprocessing_quality_issues = "" + self.saved_missed_snapshots = 0 + self._timelapse.release_job_on_hold_lock(reset=True) def pre_processing_cancelled(self): # signal complete to the UI (will close the progress popup @@ -2085,33 +2954,61 @@ def pre_processing_cancelled(self): 100, 0, 0, 0, 0) self.preprocessing_job_guid = None - def pre_processing_failed(self, errors): + def pre_processing_failed(self, preprocessing_issues): + if self._printer.is_printing(): - if len(errors) > 0: - # display error messages if there are any - error_message = "\r\n".join(errors) - self.on_print_start_failed(error_message) + self.on_preprocessing_failed( + errors=preprocessing_issues + ) # cancel the print - self._printer.cancel_print(tags={'octolapse-preprocessing-cancelled'}) + self._printer.cancel_print(tags={'preprocessing-cancelled'}) # inform the timelapse object that preprocessing has failed self._timelapse.preprocessing_finished(None) # close the UI progress popup self.send_pre_processing_progress_message( 100, 0, 0, 0, 0) - self.preprocessing_job_guid = None def pre_processing_success( self, timelapse_settings, parsed_command, snapshot_plans, total_seconds, - gcodes_processed, lines_processed + gcodes_processed, lines_processed, quality_issues, missed_snapshots ): # inform the timelapse object that preprocessing is complete and successful by sending it the first gcode # which was saved when pring start was detected self.send_pre_processing_progress_message(100, total_seconds, 0, gcodes_processed, lines_processed) - self.saved_timelapse_settings = timelapse_settings self.saved_snapshot_plans = snapshot_plans + self.saved_preprocessing_quality_issues = quality_issues + self.saved_missed_snapshots = missed_snapshots self.saved_parsed_command = parsed_command + if timelapse_settings["settings"].main_settings.preview_snapshot_plan_autoclose: + self.snapshot_plan_preview_autoclose = True + self.snapshot_plan_preview_close_time = ( + time.time() + timelapse_settings["settings"].main_settings.preview_snapshot_plan_seconds + ) + # start a timer thread to automatically close the preview + def autoclose_snapshot_preview(preprocessing_job_guid): + while True: + # if we aren't autoclosing any longer, return + if ( + str(self.preprocessing_job_guid) != str(preprocessing_job_guid) or + self.snapshot_plan_preview_close_time == 0 + ): + return + + elif time.time() > self.snapshot_plan_preview_close_time: + # close all of the snapshot preview popups + self.send_snapshot_preview_complete_message() + # start the print + self.accept_snapshot_plan_preview(self.preprocessing_job_guid) + return + time.sleep(1) + autoclose_thread = threading.Thread(target=autoclose_snapshot_preview, args=[self.preprocessing_job_guid]) + autoclose_thread.daemon = True + autoclose_thread.start() + else: + self.snapshot_plan_preview_autoclose = False + self.snapshot_plan_preview_close_time = 0 if timelapse_settings["settings"].main_settings.preview_snapshot_plans: self.send_snapshot_plan_preview() @@ -2119,7 +3016,21 @@ def pre_processing_success( self.start_preprocessed_timelapse() def send_snapshot_plan_preview(self): + data = { + "type": "snapshot-plan-preview", + "snapshot_plan_preview": self.get_snapshot_plan_preview_dict() + } + + self._plugin_manager.send_plugin_message(self._identifier, data) + + def get_snapshot_plan_preview_dict(self): # set a print job guid so we know if we should cancel this print later or not + autoclose = self.snapshot_plan_preview_autoclose + autoclose_seconds = int(self.snapshot_plan_preview_close_time - time.time()) + + if autoclose_seconds < 0: + autoclose_seconds = 0 + snapshot_plans = [] total_travel_distance = 0 total_saved_travel_distance = 0 @@ -2136,18 +3047,45 @@ def send_snapshot_plan_preview(self): total_saved_travel_distance += plan.saved_travel_distance data = { - "type": "snapshot-plan-preview", - "preprocessing_job_guid": str(self.preprocessing_job_guid), + "preprocessing_job_guid": str(self.preprocessing_job_guid), "snapshot_plans": { - "snapshot_plans": snapshot_plans, - "printer_volume": printer_volume, - "total_travel_distance": total_travel_distance, - "total_saved_travel_distance": total_saved_travel_distance, - "current_plan_index": 0, - "current_file_line": 0, + "snapshot_plans": snapshot_plans, + "printer_volume": printer_volume, + "total_travel_distance": total_travel_distance, + "total_saved_travel_distance": total_saved_travel_distance, + "current_plan_index": 0, + "current_file_line": 0, + "autoclose": autoclose, + "autoclose_seconds": autoclose_seconds, + "quality_issues": self.saved_preprocessing_quality_issues, + "missed_snapshots": self.saved_missed_snapshots } } - self._plugin_manager.send_plugin_message(self._identifier, data) + + return data + + def accept_snapshot_plan_preview(self, preprocessing_job_guid=None): + # use a lock + with self.autoclose_snapshot_preview_thread_lock: + if preprocessing_job_guid is not None and self.preprocessing_job_guid is not None: + preprocessing_job_guid = str(self.preprocessing_job_guid) + + if ( + self.preprocessing_job_guid is None or + preprocessing_job_guid != str(self.preprocessing_job_guid) or + self.saved_timelapse_settings is None or + self.saved_snapshot_plans is None or + self.saved_parsed_command is None + ): + self.on_print_start_failed(error_messages.get_error(["init", "unable_to_accept_snapshot_plan"])) + return + + logger.info("Accepting the saved snapshot plan") + self.preprocessing_job_guid = None + if not self.start_preprocessed_timelapse(): + # an error message will have been returned to the client, just return. + return + self.send_snapshot_preview_complete_message() def start_preprocessed_timelapse(self): # initialize the timelapse obeject @@ -2158,11 +3096,14 @@ def start_preprocessed_timelapse(self): self.saved_parsed_command is None ): logger.error("Unable to start the preprocessed timelapse, some required items could not be found.") - self.cancel_preprocessing() + self.reset_preprocessing() + self._timelapse.release_job_on_hold_lock(reset=True) return + if not self.start_timelapse(self.saved_timelapse_settings, self.saved_snapshot_plans): + return False - self.start_timelapse(self.saved_timelapse_settings, self.saved_snapshot_plans) self._timelapse.preprocessing_finished(self.saved_parsed_command) + return True def on_pre_processing_start(self): # set a print job guid so we know if we should cancel this print later or not @@ -2196,10 +3137,20 @@ def send_popup_message(self, msg): def send_popup_error(self, msg): self.send_plugin_message("popup-error", msg) - def send_state_changed_message(self, state): + def send_state_changed_message(self, state, client_id=None): data = { "type": "state-changed", - "state": state + "state": state, + "client_id": client_id + } + self._plugin_manager.send_plugin_message(self._identifier, data) + + def send_files_changed_message(self, file_info, action, client_id): + data = { + "type": "file-changed", + "file": file_info, + "action": action, + "client_id": client_id } self._plugin_manager.send_plugin_message(self._identifier, data) @@ -2207,12 +3158,12 @@ def send_snapshot_preview_complete_message(self): data = {"type": "snapshot-plan-preview-complete"} self._plugin_manager.send_plugin_message(self._identifier, data) - def send_settings_changed_message(self, client_id=""): + def send_settings_changed_message(self, client_id): data = { "type": "settings-changed", - "client_id": client_id, - "status": self.get_status_dict(), - "main_settings": self._octolapse_settings.main_settings.to_dict() + "client_id": client_id + #"status": self.get_status_dict(), + #"main_settings": self._octolapse_settings.main_settings.to_dict() } self._plugin_manager.send_plugin_message(self._identifier, data) @@ -2224,21 +3175,85 @@ def send_slicer_settings_detected_message(self, settings_saved, printer_profile_ } self._plugin_manager.send_plugin_message(self._identifier, data) - def send_plugin_message(self, message_type, msg): + def send_plugin_errors(self, message_type, errors): + self._plugin_manager.send_plugin_message( + self._identifier, dict(type=message_type, errors=errors)) + + def send_plugin_message(self, message_type, msg ): self._plugin_manager.send_plugin_message( self._identifier, dict(type=message_type, msg=msg)) - def send_prerender_start_message(self, payload): + def send_directories_changed_message(self, changed_directories): data = { - "type": "prerender-start", "payload": payload, "status": self.get_status_dict(), - "main_settings": self._octolapse_settings.main_settings.to_dict() + "type": "directories-changed", + "directories": changed_directories } self._plugin_manager.send_plugin_message(self._identifier, data) - def send_render_start_message(self, msg): + def send_unfinished_renderings_loaded_message(self): + failed_jobs = self._rendering_processor.get_failed() + in_process = self._rendering_processor.get_in_process() data = { - "type": "render-start", "msg": msg, "status": self.get_status_dict(), - "main_settings": self._octolapse_settings.main_settings.to_dict() + "type": "unfinished-renderings-loaded", + "status": { + "unfinished_renderings": { + "failed": { + "renderings": failed_jobs["failed"], + "size": failed_jobs["failed_size"], + }, + "in_process": { + "renderings": in_process["in_process"], + "size": in_process["in_process_size"], + } + } + } + } + + self._plugin_manager.send_plugin_message(self._identifier, data) + + def send_failed_renderings_changed_message(self, rendering, change_type): + data = { + "type": "unfinished-renderings-changed", + "status": { + "unfinished_renderings": { + "failed": + { + "rendering": rendering, + "change_type": change_type + } + } + } + } + self._plugin_manager.send_plugin_message(self._identifier, data) + + def send_in_process_renderings_changed_message(self, rendering, change_type): + data = { + "type": "unfinished-renderings-changed", + "status": { + "unfinished_renderings": { + "in_process": + { + "rendering": rendering, + "change_type": change_type + } + } + } + } + self._plugin_manager.send_plugin_message(self._identifier, data) + + def send_render_start_message(self, msg, job): + status = self.get_status_dict() + status["unfinished_renderings"] = { + 'in_process': { + "rendering": job, + "change_type": "changed" + } + } + data = { + "type": "render-start", + "msg": msg, + "status": status, + "main_settings": self._octolapse_settings.main_settings.to_dict(), } self._plugin_manager.send_plugin_message(self._identifier, data) @@ -2247,23 +3262,30 @@ def send_post_render_failed_message(self, msg): "type": "post-render-failed", "msg": msg, "status": self.get_status_dict(), - "main_settings": self._octolapse_settings.main_settings.to_dict(), - "status": self.get_status_dict() + "main_settings": self._octolapse_settings.main_settings.to_dict() } self._plugin_manager.send_plugin_message(self._identifier, data) - def send_render_failed_message(self, msg): + def send_render_failed_message(self, msg, job): + status = self.get_status_dict() + status["unfinished_renderings"] = { + 'in_process': { + "rendering": job, + "change_type": "removed" + } + } + data = { "type": "render-failed", "msg": msg, - "status": self.get_status_dict(), - "main_settings": self._octolapse_settings.main_settings.to_dict(), - "status": self.get_status_dict() + "status": status, + "main_settings": self._octolapse_settings.main_settings.to_dict() } + + self._plugin_manager.send_plugin_message(self._identifier, data) def send_render_end_message(self): - data = { "type": "render-end", "status": self.get_status_dict(), @@ -2271,22 +3293,42 @@ def send_render_end_message(self): } self._plugin_manager.send_plugin_message(self._identifier, data) - def send_render_success_message(self, message, synchronized): + def send_render_success_message(self, message, job): + status = self.get_status_dict() + status["unfinished_renderings"] = { + 'in_process': { + "rendering": job, + "change_type": "removed" + } + } data = { "type": "render-complete", "msg": message, - "is_synchronized": synchronized, - "status": self.get_status_dict(), + "status": status, "main_settings": self._octolapse_settings.main_settings.to_dict() } self._plugin_manager.send_plugin_message(self._identifier, data) + def send_render_progress_message(self, progress, job): + data = { + "type": "render-progress", + "status": { + "unfinished_renderings": { + 'in_process': { + "rendering": job, + "progress_percent": progress, + "change_type": "progress" + } + } + } + } + self._plugin_manager.send_plugin_message(self._identifier, data) def on_timelapse_start(self): data = { "type": "timelapse-start", "msg": "Octolapse has started a timelapse.", - "status": self.get_status_dict(), + "status": self.get_status_dict(include_profiles=True), "main_settings": self._octolapse_settings.main_settings.to_dict(), "state": self._timelapse.to_state_dict(include_timelapse_start_data=True) } @@ -2296,16 +3338,7 @@ def on_timelapse_end(self): state_data = self._timelapse.to_state_dict() data = { "type": "timelapse-complete", "msg": "Octolapse has completed a timelapse.", - "status": self.get_status_dict(), - "main_settings": self._octolapse_settings.main_settings.to_dict() - } - data.update(state_data) - self._plugin_manager.send_plugin_message(self._identifier, data) - - def on_position_error(self, message): - state_data = self._timelapse.to_state_dict() - data = { - "type": "position-error", "msg": message, "status": self.get_status_dict(), + "status": self.get_status_dict(include_profiles=True), "main_settings": self._octolapse_settings.main_settings.to_dict() } data.update(state_data) @@ -2320,35 +3353,9 @@ def on_snapshot_position_error(self, message): data.update(state_data) self._plugin_manager.send_plugin_message(self._identifier, data) - def send_state_loaded_message(self): - state_date = None - if self._timelapse is not None: - state_data = self._timelapse.to_state_dict(include_timelapse_start_data=True) - data = { - "msg": "The current state has been loaded.", - "main_settings": self._octolapse_settings.main_settings.to_dict(), - "status": self.get_status_dict(), - "state": state_data - } - try: - self.queue_plugin_message(PluginMessage(data, "state-loaded")) - if self.preprocessing_job_guid and self.saved_snapshot_plans: - self.send_snapshot_plan_preview() - except Exception as e: - raise e - - def on_timelapse_complete(self): - state_data = self._timelapse.to_state_dict() - data = { - "type": "timelapse-complete", "msg": "Octolapse has completed the timelapse.", - "status": self.get_status_dict(), "main_settings": self._octolapse_settings.main_settings.to_dict() - } - data.update(state_data) - self._plugin_manager.send_plugin_message(self._identifier, data) - def on_snapshot_start(self): data = { - #"type": "snapshot-start", + "type": "snapshot-start", "msg": "Octolapse is taking a snapshot.", "status": self.get_status_dict(), "main_settings": self._octolapse_settings.main_settings.to_dict(), @@ -2370,7 +3377,7 @@ def on_snapshot_end(self, *args): snapshot_error = snapshot_payload["error"] data = { - #"type": "snapshot-complete", + "type": "snapshot-complete", "msg": "Octolapse has completed the current snapshot.", "status": status_dict, "state": self._timelapse.to_state_dict(), @@ -2388,35 +3395,61 @@ def on_new_thumbnail_available(self, guid): } self.queue_plugin_message(PluginMessage(data, "new-thumbnail-available")) + def on_post_processing_error_callback(self, guid, exc): + status_dict = self.get_status_dict() + error = str(exc) + data = { + # "type": "snapshot-complete", + "msg": error, + "status": status_dict, + "state": self._timelapse.to_state_dict(), + "main_settings": self._octolapse_settings.main_settings.to_dict(), + } + self.queue_plugin_message(PluginMessage(data, "snapshot-post-proocessing-failed")) + def on_print_failed(self): self._timelapse.on_print_failed() logger.info("Print failed.") def on_printer_disconnecting(self): + self.reset_preprocessing() + # stop any preprocessing scripts if they are called + self._timelapse.release_job_on_hold_lock() + # tell the timelapse object that we are cancelling self._timelapse.on_print_disconnecting() logger.info("Printer disconnecting.") def on_printer_disconnected(self): self._timelapse.on_print_disconnected() + self._timelapse.release_job_on_hold_lock(reset=True) logger.info("Printer disconnected.") def on_print_cancelling(self): logger.info("Print cancelling.") + + self.reset_preprocessing() # stop any preprocessing scripts if they are called - self.cancel_preprocessing() + self._timelapse.release_job_on_hold_lock() # tell the timelapse object that we are cancelling self._timelapse.on_print_cancelling() + def on_print_canceled(self): logger.info("Print cancelled.") self._timelapse.on_print_canceled() + self._timelapse.release_job_on_hold_lock(reset=True) def on_print_completed(self): self._timelapse.on_print_completed() - logger.info("Print completed.") + # run on print start scripts + logger.info("Print completed successfullly.") - def on_timelapse_stopping(self): + def on_print_end(self): + # run on print start scripts + camera.CameraControl.run_on_print_end_script(self._octolapse_settings.profiles.cameras) + logger.info("The print has ended.") + def on_timelapse_stopping(self): self.send_plugin_message( "timelapse-stopping", "Waiting for a snapshot to complete before stopping the timelapse.") @@ -2434,7 +3467,7 @@ def on_timelapse_stopped(self, message, error): data = { "type": message_type, "msg": message, - "status": self.get_status_dict(), + "status": self.get_status_dict(include_profiles=True), "main_settings": self._octolapse_settings.main_settings.to_dict() } data.update(state_data) @@ -2443,11 +3476,7 @@ def on_timelapse_stopped(self, message, error): # noinspection PyUnusedLocal def on_gcode_queuing(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): try: - # only handle commands sent while printing - #if self._timelapse is not None and self._octolapse_settings.profiles.current_printer() is not None: if self._timelapse is not None: - # needed to handle non utf-8 characters - #cmd = cmd.encode('ascii', 'ignore') return self._timelapse.on_gcode_queuing(cmd, cmd_type, gcode, kwargs["tags"]) except Exception as e: logger.exception("on_gcode_queuing failed..") @@ -2455,8 +3484,7 @@ def on_gcode_queuing(self, comm_instance, phase, cmd, cmd_type, gcode, *args, ** # noinspection PyUnusedLocal def on_gcode_sending(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): try: - if self._timelapse is not None and self._octolapse_settings.profiles.current_printer() is not None: - # we always want to send this event, else we may get stuck waiting for a position request! + if self._timelapse is not None: self._timelapse.on_gcode_sending(cmd, kwargs["tags"]) except Exception as e: logger.exception("on_gcode_sending failed.") @@ -2464,7 +3492,7 @@ def on_gcode_sending(self, comm_instance, phase, cmd, cmd_type, gcode, *args, ** # noinspection PyUnusedLocal def on_gcode_sent(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): try: - if self._timelapse is not None and self._timelapse.is_timelapse_active(): + if self._timelapse is not None: self._timelapse.on_gcode_sent(cmd, cmd_type, gcode, kwargs["tags"]) except Exception as e: logger.exception("on_gcode_sent failed.") @@ -2472,7 +3500,7 @@ def on_gcode_sent(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwa # noinspection PyUnusedLocal def on_gcode_received(self, comm, line, *args, **kwargs): try: - if self._timelapse is not None and self._timelapse.is_timelapse_active(): + if self._timelapse is not None: self._timelapse.on_gcode_received(line) except Exception as e: logger.exception("on_gcode_received failed.") @@ -2484,73 +3512,133 @@ def on_timelapse_state_changed(self, *args): } self.queue_plugin_message(PluginMessage(state_change_dict, "state-changed", rate_limit_seconds=1)) - def on_prerender_start(self, payload): - self.send_prerender_start_message(payload) + def _get_rendering_settings(self): + return { + "ffmpeg_path": self._settings.global_get(["webcam", "ffmpeg"]), + "timelapse_directory": self.get_octoprint_timelapse_location(), + } + + def _get_current_rendering_profile(self): + return self.get_current_octolapse_settings().profiles.current_rendering().clone() + + def add_rendering_job(self, job_guid, camera_guid, temporary_folder, rendering_profile=None, camera_profile=None): + logger.info("Adding new rendering job. JobGuid: %s, CameraGuid: %s", job_guid, camera_guid) + parameters = { + "job_guid": job_guid, + "camera_guid": camera_guid, + "action": "add", + "rendering_profile": rendering_profile, + "camera_profile": camera_profile, + "temporary_directory": temporary_folder + } + self._rendering_task_queue.put(parameters) + + def delete_rendering_job(self, job_guid, camera_guid): + logger.info("Deleting unfinished rendering job. JobGuid: %s, CameraGuid: %s", job_guid, camera_guid) + parameters = { + "job_guid": job_guid, + "camera_guid": camera_guid, + "action": "remove_unfinished", + "delete": True + } + self._rendering_task_queue.put(parameters) - def on_render_start(self, payload): + def on_render_start(self, payload, job): """Called when a timelapse has started being rendered. Calls any callbacks OnRenderStart callback set in the constructor. """ assert (isinstance(payload, RenderingCallbackArgs)) - # Set a flag marking that we have not yet synchronized with the default Octoprint plugin, in case we do this - # later. # Generate a notification message - job_message = "" + msg = "Octolapse is rendering {0} frames for the {1} camera.".format( + payload.SnapshotCount, payload.CameraName) if payload.JobsRemaining > 1: - job_message = "Rendering for camera '{0}'. {1} jobs remaining.".format( - payload.CameraName, payload.JobsRemaining - ) - - msg = "Octolapse captured {0} frames and has started rendering your timelapse file.".format( - payload.SnapshotCount) + msg += " {0} jobs remaining. ".format(payload.JobsRemaining) - if payload.Synchronize: - will_sync_message = "This timelapse will synchronized with the default timelapse module, and will be " \ - "available within the default timelapse plugin as '{0}' after rendering is " \ - "complete.".format(payload.get_synchronization_filename()) - else: - will_sync_message = "Due to your rendering settings, this timelapse will NOT be synchronized with the " \ - "default timelapse module. You will be able to find on your octoprint server" \ - " here:
{0}".format(payload.get_rendering_path()) + msg += "After rendering is completed, the finished video will be available both within the stock timelapse " \ + "tab, and within the Octolapse tab. " - message = "{0}{1}{2}".format(job_message, msg, will_sync_message) - # send a message to the client - self.send_render_start_message(message) + self.send_render_start_message(msg, job) - def on_render_success(self, payload): - """Called after all rendering and synchronization attempts are complete.""" + def on_render_success(self, payload, job): + """Called after all rendering is complete.""" assert (isinstance(payload, RenderingCallbackArgs)) - message = "Rendering completed and was successful." if payload.BeforeRenderError or payload.AfterRenderError: - pre_post_render_message = "Rendering completed and was successful, but there were some script errors: " + pre_post_render_message = "Rendering completed and was successful for the '{0}' camera, but there were " \ + "some script errors: ".format(payload.CameraName) if payload.BeforeRenderError: - pre_post_render_message += " The before script failed with the following error:" \ - " {0}".format(payload.BeforeRenderError) + pre_post_render_message += "{0}Before Render Script - {1}".format(os.linesep, payload.BeforeRenderError.message) if payload.AfterRenderError: - pre_post_render_message += " The after script failed with the following error:" \ - " {0}".format(payload.AfterRenderError) + pre_post_render_message += "{0}After Render Script - {1}".format(os.linesep, payload.AfterRenderError.message) self.send_post_render_failed_message(pre_post_render_message) - if payload.Synchronize: - # If we are synchronizing with the Octoprint timelapse plugin, we will send a tailored message - message = "Octolapse has completed rendering a timelapse for camera '{0}'. Your video is now available " \ - "within the default timelapse plugin tab as '{1}'. Octolapse ".format( - payload.CameraName, - payload.get_synchronization_filename() - ) - - else: + if payload.RenderingEnabled: # This timelapse won't be moved into the octoprint timelapse plugin folder. - message = "Octolapse has completed rendering a timelapse for camera '{0}'. Due to your rendering " \ - "settings, the timelapse was not synchronized with the OctoPrint plugin. You should be able" \ - " to find your video within your octoprint server here:
'{1}'".format( - payload.CameraName, - payload.get_rendering_path() - ) - - self.send_render_success_message(message, payload.Synchronize) + message = "Octolapse has completed rendering a timelapse for camera '{0}'. Your video can be found by " \ + "clicking 'Videos and Images' within the Octolapse tab.".format(payload.CameraName) + if payload.ArchivePath and os.path.isfile(payload.ArchivePath): + message += " An archive of your snapshots can be found within the 'Saved Snapshots' tab of the " \ + "'Videos and Images' dialog." + else: + message = ( + "Octolapse has completed creating an archive of your snapshots for camera '{0}'. Your archive " + "is available within the 'Saved Snapshots' tab of the 'Videos and Images' dialog." + .format(payload.CameraName) + ) + self.send_render_success_message(message, job) + + output_file_name = "{0}.{1}".format(payload.RenderingFilename, payload.RenderingExtension) + gcode_filename = "{0}.{1}".format(payload.GcodeFilename, payload.GcodeFileExtension) + # Todo: Make sure this path is correct + output_file_path = payload.get_rendering_path() + + # Notify the plugin that a timelapse has been added, if it exists + if os.path.isfile(output_file_path): + file_info = utility.get_file_info(output_file_path) + file_info["type"] = utility.FILE_TYPE_TIMELAPSE_OCTOLAPSE + self.send_files_changed_message( + file_info, + 'added', + None + ) + # Notify anyone who cares that Octolapse has finished rendering a movie + self.send_movie_done_event(gcode_filename, output_file_path, output_file_name) + + archive_path = payload.ArchivePath + # we need to make sure the archive path exists again, since some plugin might have deleted it. + if archive_path and os.path.isfile(archive_path): + # Notify the plugin that an archive was created + file_info = utility.get_file_info(archive_path) + file_info["type"] = utility.FILE_TYPE_SNAPSHOT_ARCHIVE + self.send_files_changed_message( + file_info, + 'added', + None + ) + # Notify anyone who cares that Octolapse has finished creating a snapshot archive + self.send_snapshot_archive_done_event(gcode_filename, archive_path, os.path.basename(archive_path)) + + def on_render_progress(self, payload, job): + self.send_render_progress_message(payload, job) + + def send_movie_done_event(self, gcode_filename, movie_path, movie_basename): + event = Events.PLUGIN_OCTOLAPSE_MOVIE_DONE + custom_payload = dict( + gcode=gcode_filename, + movie=movie_path, + movie_basename=movie_basename + ) + self._event_bus.fire(event, payload=custom_payload) + + def send_snapshot_archive_done_event(self, gcode_filename, archive_path, archive_basename): + event = Events.PLUGIN_OCTOLAPSE_SNAPSHOT_ARCHIVE_DONE + custom_payload = dict( + gcode=gcode_filename, + archive=archive_path, + archive_basename=archive_basename + ) + self._event_bus.fire(event, payload=custom_payload) - def on_render_error(self, payload, error): - """Called after all rendering and synchronization attempts are complete.""" + def on_render_error(self, payload, error, job): + """Called after all rendering is complete.""" if payload: assert (isinstance(payload, RenderingCallbackArgs)) if payload.BeforeRenderError or payload.AfterRenderError: @@ -2561,14 +3649,13 @@ def on_render_error(self, payload, error): if payload.AfterRenderError: pre_post_render_message += " The after script failed with the following error:" \ " {0}".format(payload.AfterRenderError) - self.send_render_failed_message(pre_post_render_message) - - if error != None: + self.send_render_failed_message(pre_post_render_message, job) + if error is not None: message = "Rendering failed for camera '{0}'. {1}".format(payload.CameraName, error) - self.send_render_failed_message(message) + self.send_render_failed_message(message, job) else: message = "Rendering failed. {0}".format(error) - self.send_render_failed_message(message) + self.send_render_failed_message(message, job) def on_render_end(self): self.send_render_end_message() @@ -2599,96 +3686,141 @@ def get_assets(self): "js/octolapse.profiles.camera.js", "js/octolapse.profiles.camera.webcam.js", "js/octolapse.profiles.camera.webcam.mjpg_streamer.js", - "js/octolapse.profiles.debug.js", + "js/octolapse.profiles.logging.js", "js/octolapse.status.js", "js/octolapse.status.snapshotplan.js", "js/octolapse.status.snapshotplan_preview.js", "js/octolapse.help.js", "js/octolapse.profiles.library.js", - "js/webcams/mjpg_streamer/raspi_cam_v2.js" + "js/webcams/mjpg_streamer/raspi_cam_v2.js", + "js/octolapse.dialog.js", + "js/octolapse.dialog.renderings.unfinished.js", + "js/octolapse.dialog.renderings.in_process.js", + "js/octolapse.file_browser.js", + "js/octolapse.helpers.js", + "js/octolapse.dialog.timelapse_files.js" ], css=["css/jquery.minicolors.css", "css/octolapse.css"], less=["less/octolapse.less"] ) - # ~~ software update hook - def get_update_information(self): - # get the checkout type from the software updater - prerelease_channel = None - is_prerelease = False - # get this for reference. Eventually I'll have to use it! - # is the software update set to prerelease? - - if self._settings.global_get(["plugins", "softwareupdate", "checks", "octoprint", "prerelease"]): - # If it's a prerelease, look at the channel and configure the proper branch for Octolapse - prerelease_channel = self._settings.global_get( - ["plugins", "softwareupdate", "checks", "octoprint", "prerelease_channel"] + octolapse_update_info = dict( + displayName="Octolapse", + # version check: github repository + type="github_release", + user="FormerLurker", + repo="Octolapse", + pip="https://github.com/FormerLurker/Octolapse/archive/{target_version}.zip", + stable_branch=dict(branch="master", commitish=["master"], name="Stable"), + release_compare='custom', + prerelease_branches=[ + dict( + branch="rc/maintenance", + commitish=["master", "rc/maintenance"], # maintenance RCs + name="Maintenance RCs" + ), + dict( + branch="rc/devel", + commitish=["master", "rc/maintenance", "rc/devel"], # devel & maintenance RCs + name="Devel RCs" ) - if prerelease_channel == "rc/maintenance": - is_prerelease = True - prerelease_channel = "rc/maintenance" - elif prerelease_channel == "rc/devel": - is_prerelease = True - prerelease_channel = "rc/devel" - - octolapse_info = dict( - displayName="Octolapse", - displayVersion=self._plugin_version, - # version check: github repository - type="github_release", - user="FormerLurker", - repo="Octolapse", - current=self._plugin_version, - prerelease=is_prerelease, - pip="https://github.com/FormerLurker/Octolapse/archive/{target_version}.zip", - stable_branch=dict(branch="master", commitish=["master"], name="Stable"), - release_compare='semantic_version', - prerelease_branches=[ - dict( - branch="rc/maintenance", - commitish=["rc/maintenance"], # maintenance RCs - name="Maintenance RCs" - ), - dict( - branch="rc/devel", - commitish=["rc/maintenance", "rc/devel"], # devel & maintenance RCs - name="Devel RCs" + ], + ) + + def get_release_info(self): + # Starting with V1.5.0 prerelease branches are supported! + if LooseVersion(octoprint.server.VERSION) < LooseVersion("1.5.0"): + # get the checkout type from the software updater + prerelease_channel = None + is_prerelease = False + # get this for reference. Eventually I'll have to use it! + # is the software update set to prerelease? + if self._settings.global_get(["plugins", "softwareupdate", "checks", "octoprint", "prerelease"]): + # If it's a prerelease, look at the channel and configure the proper branch for Arc Welder + prerelease_channel = self._settings.global_get( + ["plugins", "softwareupdate", "checks", "octoprint", "prerelease_channel"] ) - ], - ) - - if prerelease_channel is not None: - octolapse_info["prerelease_channel"] = prerelease_channel - # return the update config + if prerelease_channel == "rc/maintenance": + is_prerelease = True + prerelease_channel = "rc/maintenance" + elif prerelease_channel == "rc/devel": + is_prerelease = True + prerelease_channel = "rc/devel" + OctolapsePlugin.octolapse_update_info["prerelease"] = is_prerelease + if prerelease_channel is not None: + OctolapsePlugin.octolapse_update_info["prerelease_channel"] = prerelease_channel + + OctolapsePlugin.octolapse_update_info["displayVersion"] = self._plugin_version + OctolapsePlugin.octolapse_update_info["current"] = self._plugin_version return dict( - octolapse=octolapse_info + octolapse=OctolapsePlugin.octolapse_update_info ) + # ~~ software update hook + def get_update_information(self): + return self.get_release_info() + # noinspection PyUnusedLocal def get_timelapse_extensions(self, *args, **kwargs): allowed_extensions = ["mpg", "mpeg", "mp4", "m4v", "mkv", "gif", "avi", "flv", "vob"] - if sys.version_info < (3,0): return [i.encode('ascii', 'replace') for i in allowed_extensions] return allowed_extensions - def bodysize_hook(self, current_max_body_sizes, *args, **kwargs): - max_upload_size_mb = 5 # 5mb bytes - return [("POST", "/importSettings", 1024*1024*max_upload_size_mb)] - -# If you want your plugin to be registered within OctoPrin#t under a different -# name than what you defined in setup.py -# ("OctoPrint-PluginSkeleton"), you may define that here. Same goes for the -# other metadata derived from setup.py that -# can be overwritten via __plugin_xyz__ control properties. See the -# documentation for that. + max_settings_upload_mb = 5 + # TODO - Add max_snapshot_upload_mb setting to config.yaml + max_snapshot_upload_mb = 1024 # 1GB + return [ + ("POST", "/importSettings", 1024*1024*max_settings_upload_mb), + ("POST", "/importSnapshots", 1024*1024*max_snapshot_upload_mb) + + ] + + def register_custom_events(*args, **kwargs): + return ["movie_done", "snapshot_archive_done"] + + def register_custom_routes(self, server_routes, *args, **kwargs): + # version specific permission validator + if LooseVersion(octoprint.server.VERSION) >= LooseVersion("1.4"): + admin_validation_chain = [ + util.tornado.access_validation_factory(app, util.flask.admin_validator), + ] + else: + # the concept of granular permissions does not exist in this version of Octoprint. Fallback to the + # admin role + def admin_permission_validator(flask_request): + user = util.flask.get_flask_user_from_request(flask_request) + if user is None or not user.is_authenticated() or not user.is_admin(): + raise tornado.web.HTTPError(403) + permission_validator = admin_permission_validator + admin_validation_chain = [util.tornado.access_validation_factory(app, permission_validator), ] + + + return [ + ( + r"/downloadFile", + OctolapseLargeResponseHandler, + dict( + request_callback=self.download_file_request, + as_attachment=True, + access_validation=util.tornado.validation_chain(*admin_validation_chain) + ) + ), + ( + r"/getSnapshot", + OctolapseLargeResponseHandler, + dict( + request_callback=self.get_snapshot_request, + as_attachment=False + ) + ) + ] __plugin_name__ = "Octolapse" -__plugin_pythoncompat__ = ">=2.7,<4" - +__plugin_pythoncompat__ = ">=3.7,<4" def __plugin_load__(): global __plugin_implementation__ @@ -2701,5 +3833,62 @@ def __plugin_load__(): "octoprint.comm.protocol.gcode.sending": (__plugin_implementation__.on_gcode_sending, -1), "octoprint.comm.protocol.gcode.received": (__plugin_implementation__.on_gcode_received, -1), "octoprint.timelapse.extensions": __plugin_implementation__.get_timelapse_extensions, - "octoprint.server.http.bodysize": __plugin_implementation__.bodysize_hook + "octoprint.server.http.bodysize": __plugin_implementation__.bodysize_hook, + "octoprint.events.register_custom_events": __plugin_implementation__.register_custom_events, + "octoprint.server.http.routes": __plugin_implementation__.register_custom_routes } + + +class OctolapseLargeResponseHandler(LargeResponseHandler): + def initialize( + self, request_callback, as_attachment=False, access_validation=None, default_filename=None, + on_before_request=None, on_after_request=None + ): + super().initialize( + '', default_filename=default_filename, as_attachment=as_attachment, allow_client_caching=False, + access_validation=access_validation, path_validation=None, etag_generator=None, + name_generator=self.name_generator, mime_type_guesser=None) + self.download_file_name = None + self._before_request_callback = on_before_request + self._request_callback = request_callback + self._after_request_callback = on_after_request + self.after_request_internal = None + self.after_request_internal_args = None + + def name_generator(self, path): + if self.download_file_name is not None: + return self.download_file_name + + def prepare(self): + if self._before_request_callback: + self._before_request_callback() + + def get(self, include_body=True): + if self._access_validation is not None: + self._access_validation(self.request) + + if "cookie" in self.request.arguments: + self.set_cookie(self.request.arguments["cookie"][0], "true", path="/") + full_path = self._request_callback(self) + self.root = utility.get_directory_from_full_path(full_path) + + # if the file does not exist, return a 404 + if not os.path.isfile(full_path): + raise tornado.web.HTTPError(404) + + # return the file + return tornado.web.StaticFileHandler.get(self, full_path, include_body=include_body) + + def on_finish(self): + if self.after_request_internal: + self.after_request_internal(**self.after_request_internal_args) + + if self._after_request_callback: + self._after_request_callback() + + + +from ._version import get_versions +__version__ = get_versions()['version'] +__git_version__ = get_versions()['full-revisionid'] +del get_versions diff --git a/octoprint_octolapse/_version.py b/octoprint_octolapse/_version.py new file mode 100644 index 00000000..aec13b46 --- /dev/null +++ b/octoprint_octolapse/_version.py @@ -0,0 +1,520 @@ + +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.18 (https://github.com/warner/python-versioneer) + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "$Format:%d$" + git_full = "$Format:%H$" + git_date = "$Format:%ci$" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "" + cfg.parentdir_prefix = "" + cfg.versionfile_source = "octoprint_octolapse/_version.py" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + print("stdout was %s" % stdout) + return None, p.returncode + return stdout, p.returncode + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs - tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +def get_versions(): + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", "date": None} diff --git a/octoprint_octolapse/camera.py b/octoprint_octolapse/camera.py index bebf3730..811cb195 100644 --- a/octoprint_octolapse/camera.py +++ b/octoprint_octolapse/camera.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -22,18 +22,25 @@ ################################################################################## from __future__ import unicode_literals import json -import six +# remove unused imports +# import six import octoprint_octolapse.utility as utility +import octoprint_octolapse.script as script from threading import Thread import time +import uuid +import shutil # This file is subject to the terms and conditions defined in # file called 'LICENSE', which is part of this source code package. import requests import os +import errno # Todo: Do we need to add this to setup.py? -from requests.auth import HTTPBasicAuth +# remove unused import +# from requests.auth import HTTPBasicAuth from requests.exceptions import SSLError from octoprint_octolapse.settings import CameraProfile, MjpgStreamerControl, MjpgStreamer +from tempfile import mkdtemp # create the module level logger from octoprint_octolapse.log import LoggingConfigurator @@ -73,7 +80,9 @@ def _load_camera_types(data_path): server_types = camera_files["server_types"] # loop through each server type - for key, server_type in six.iteritems(server_types): + # remove python 2 compatibility + # for key, server_type in six.iteritems(server_types): + for key, server_type in server_types.items(): camera_types["server_types"][key] = {'cameras': {}} # load all of the individual camera type info files for the current server for file_name in server_type["file_names"]: @@ -120,7 +129,9 @@ def get_webcam_type(data_path, server_type, data): control_setting.update(control) controls[control_setting.id] = control_setting elif isinstance(data, dict): - for key, control in six.iteritems(data): + # remove python 2 compatibility + # for key, control in six.iteritems(data): + for key, control in data.items(): control_setting = MjpgStreamerControl() control_setting.update(control) controls[key] = control_setting @@ -136,7 +147,9 @@ def get_webcam_type(data_path, server_type, data): cameras = CameraControl.camera_types["server_types"][server_type]["cameras"] # iterate the cameras dictionary for the given server type if possible - for key, camera in six.iteritems(cameras): + # remove python 2 compatibility + # for key, camera in six.iteritems(cameras): + for key, camera in cameras.items(): # see if the current camera type matches the given data if CameraControl._is_webcam_type(server_type, camera, data): return { @@ -166,45 +179,126 @@ def _is_webcam_type(server_type, camera, data): return False @staticmethod - def apply_camera_settings(cameras, timeout_seconds=5): + def apply_camera_settings(cameras, retries=3, backoff_factor=0.1, no_wait=False): errors = [] # make sure the supplied cameras are enabled and either webcams or script cameras cameras = [x for x in cameras if x.camera_type in [ - CameraProfile.camera_type_webcam, - CameraProfile.camera_type_script + CameraProfile.camera_type_webcam ]] # create the threads threads = [] for current_camera in cameras: thread = None - if current_camera.camera_type == CameraProfile.camera_type_webcam: - if current_camera.webcam_settings.server_type == MjpgStreamer.server_type: - thread = MjpgStreamerSettingsThread(profile=current_camera) - elif current_camera.camera_type == CameraProfile.camera_type_script: - thread = CameraSettingScriptThread(current_camera) + + if current_camera.webcam_settings.server_type == MjpgStreamer.server_type: + thread = MjpgStreamerSettingsThread(profile=current_camera, retries=retries, backoff_factor=backoff_factor) + thread.daemon = True if thread: threads.append(thread) + # start the threads + for thread in threads: + thread.start() + + if not no_wait: + # join the threads, but timeout in a reasonable way + for thread in threads: + thread.join(requests.packages.urllib3.util.retry.Retry.BACKOFF_MAX) + if not thread.success: + for error in thread.errors: + errors.append(error) + return len(errors) == 0, CameraControl._get_errors_string(errors) + return True, "" + + @staticmethod + def run_on_print_start_script(cameras): + # build the arg list + args_list = [] + # remove python 2 compatibility + # for key, camera in six.iteritems(cameras): + for key, camera in cameras.items(): + script_path = camera.on_print_start_script.strip() + if not script_path or not camera.enabled: + # skip any cameras where there is no on print start script or the camera is disabled + continue + script_args = [camera.name] + script_type = 'on print start' + + args = { + 'camera': camera, + 'script_path': script_path, + 'script_args': script_args, + 'script_type': script_type + } + args_list.append(args) + + if len(args_list) > 0: + return CameraControl._run_camera_scripts(args_list) + + return True, None + + @staticmethod + def run_on_print_end_script(cameras): + # build the arg list + args_list = [] + # remove python 2 compatibility + # for key, camera in six.iteritems(cameras): + for key, camera in cameras.items(): + script_path = camera.on_print_end_script.strip() + if not script_path or not camera.enabled: + # skip any cameras where there is no on print start script or the camera is disabled + continue + script_args = [camera.name] + script_type = 'on print end' + + args = { + 'camera': camera, + 'script_path': script_path, + 'script_args': script_args, + 'script_type': script_type + } + args_list.append(args) + + if len(args_list) > 0: + return CameraControl._run_camera_scripts(args_list) + return True, None + + @staticmethod + def _run_camera_scripts(args_list, timeout_seconds=5): + errors = [] + + # create the threads + threads = [] + for args in args_list: + thread = CameraSettingScriptThread( + args['camera'], + args['script_path'], + args['script_args'], + args['script_type'] + ) + thread.daemon = True + threads.append(thread) + # start the threads for thread in threads: thread.start() start_time = time.time() - timeout_time = start_time + 5 + timeout_time = start_time + timeout_seconds # join the threads, but timeout in a reasonable way for thread in threads: timeout_sec = timeout_time - time.time() if timeout_sec < 0: timeout_sec = 0.001 thread.join(timeout_sec) - if not thread.success: - for error in thread.errors: - errors.append(error) + if thread.error: + errors.append(thread.error) return len(errors) == 0, CameraControl._get_errors_string(errors) + @staticmethod def _get_errors_string(errors): if len(errors) == 0: @@ -223,7 +317,6 @@ def _get_errors_string(errors): unknown_errors = "{} unknown errors, check plugin.octolapse.log for details.".format(unknown_errors_count) return "{}{}".format(camera_errors, unknown_errors) - @staticmethod def apply_webcam_setting( server_type, @@ -232,19 +325,18 @@ def apply_webcam_setting( address, username, password, - ignore_ssl_error, - timeout_seconds=5 + ignore_ssl_error ): if server_type == 'mjpg-streamer': thread = MjpgStreamerSettingThread( setting, - camera_name=camera_name, - address=address, - username=username, - password=password, - ignore_ssl_error=ignore_ssl_error, - timeout_seconds=timeout_seconds + address, + username, + password, + ignore_ssl_error, + camera_name ) + thread.daemon = True thread.start() thread.join() return thread.success, thread.errors @@ -261,7 +353,8 @@ def get_webcam_settings( username, password, ignore_ssl_error, - timeout_seconds=5 + retries=3, + backoff_factor=0.1 ): try: errors = [] @@ -274,8 +367,10 @@ def get_webcam_settings( password=password, ignore_ssl_error=ignore_ssl_error, camera_name=camera_name, - timeout_seconds=timeout_seconds + retries=retries, + backoff_factor=backoff_factor ) + thread.daemon = True thread.start() thread.join() if thread.errors: @@ -332,14 +427,24 @@ def get_webcam_server_type_from_request(r): def _test_mjpg_streamer_control(camera_profile, timeout_seconds=2): url = camera_profile.webcam_settings.address + "?action=command&id=-1" try: + s = requests.Session() + s.verify = not camera_profile.webcam_settings.ignore_ssl_error if len(camera_profile.webcam_settings.username) > 0: - r = requests.get(url, auth=HTTPBasicAuth(camera_profile.webcam_settings.username, - camera_profile.webcam_settings.password), - verify=not camera_profile.webcam_settings.ignore_ssl_error, - timeout=float(timeout_seconds)) - else: - r = requests.get( - url, verify=not camera_profile.webcam_settings.ignore_ssl_error, timeout=float(timeout_seconds)) + s.auth = ( + camera_profile.webcam_settings.username, + camera_profile.webcam_settings.password + ) + + r = utility.requests_retry_session(session=s).request("GET", url, timeout=timeout_seconds) + + #if len(camera_profile.webcam_settings.username) > 0: + # r = requests.get(url, auth=HTTPBasicAuth(camera_profile.webcam_settings.username, + # camera_profile.webcam_settings.password), + # verify=not camera_profile.webcam_settings.ignore_ssl_error, + # timeout=float(timeout_seconds)) + #else: + # r = requests.get( + # url, verify=not camera_profile.webcam_settings.ignore_ssl_error, timeout=float(timeout_seconds)) webcam_server_type = CameraControl.get_webcam_server_type_from_request(r) if webcam_server_type != "mjpg-streamer": @@ -350,15 +455,14 @@ def _test_mjpg_streamer_control(camera_profile, timeout_seconds=2): raise CameraError('unknown-server-type', message) if r.status_code == 501: message = "The server denied access to the mjpg-streamer control.htm for the '{0}' camera profile. Please see this link to correct this " \ + "href=\"https://github.com/FormerLurker/Octolapse/wiki/V0.4---Enabling-Camera-Controls\" target = \"_blank\">Please see this link to correct this " \ "error., or disable the 'Enable And Apply Preferences at Startup' and " \ "'Enable And Apply Preferences Before Print' options.".format(camera_profile.name) logger.error(message) - raise CameraError("mjpg_streamer-control-error", message) + raise CameraError("mjpg_streamer_control_error", message) if r.status_code != requests.codes.ok: message = "Status code received ({0}) was not OK. Double check your webcam 'Base Addresss' address and " \ - "your 'Snapshot Address Template'. Or, disable the 'Enable And Apply Preferences at Startup' " \ + "your 'Snapshot Address'. Or, disable the 'Enable And Apply Preferences at Startup' " \ "and 'Enable And Apply Preferences Before Print' options for the {1} camera " \ "profile and try again.".format(r.status_code, camera_profile.name) logger.error(message) @@ -392,15 +496,27 @@ def _test_web_camera_image_preferences(camera_profile, timeout_seconds=2): # first see what kind of server we have url = format_request_template( camera_profile.webcam_settings.address, camera_profile.webcam_settings.snapshot_request_template, "") + try: + s = requests.Session() + s.verify = not camera_profile.webcam_settings.ignore_ssl_error if len(camera_profile.webcam_settings.username) > 0: - r = requests.get(url, auth=HTTPBasicAuth(camera_profile.webcam_settings.username, - camera_profile.webcam_settings.password), - verify=not camera_profile.webcam_settings.ignore_ssl_error, - timeout=float(timeout_seconds)) - else: - r = requests.get(url, verify=not camera_profile.webcam_settings.ignore_ssl_error, - timeout=float(timeout_seconds)) + s.auth = ( + camera_profile.webcam_settings.username, + camera_profile.webcam_settings.password + ) + + r = utility.requests_retry_session(session=s).request("GET", url, timeout=timeout_seconds) + + + # if len(camera_profile.webcam_settings.username) > 0: + # r = requests.get(url, auth=HTTPBasicAuth(camera_profile.webcam_settings.username, + # camera_profile.webcam_settings.password), + # verify=not camera_profile.webcam_settings.ignore_ssl_error, + # timeout=float(timeout_seconds)) + # else: + # r = requests.get(url, verify=not camera_profile.webcam_settings.ignore_ssl_error, + # timeout=float(timeout_seconds)) webcam_server_type = CameraControl.get_webcam_server_type_from_request(r) @@ -413,7 +529,7 @@ def _test_web_camera_image_preferences(camera_profile, timeout_seconds=2): raise CameraError('ssl-error', message, cause=e) except requests.ConnectionError as e: message = "Unable to connect to '{0}' for the '{1}' camera profile. Please double check your 'Base Address' " \ - "and 'Snapshot Address Template' settings.".format(url, camera_profile.name) + "and 'Snapshot Address' settings.".format(url, camera_profile.name) logger.exception(message) raise CameraError('connection-error', message, cause=e) except Exception as e: @@ -451,14 +567,35 @@ def _test_web_camera(camera_profile, timeout_seconds=2, is_before_print_test=Fal url = format_request_template( camera_profile.webcam_settings.address, camera_profile.webcam_settings.snapshot_request_template, "") try: + s = requests.Session() + s.verify = not camera_profile.webcam_settings.ignore_ssl_error if len(camera_profile.webcam_settings.username) > 0: - r = requests.get(url, auth=HTTPBasicAuth(camera_profile.webcam_settings.username, - camera_profile.webcam_settings.password), - verify=not camera_profile.webcam_settings.ignore_ssl_error, - timeout=float(timeout_seconds)) - else: - r = requests.get( - url, verify=not camera_profile.webcam_settings.ignore_ssl_error, timeout=float(timeout_seconds)) + s.auth = ( + camera_profile.webcam_settings.username, + camera_profile.webcam_settings.password + ) + retry_session = utility.requests_retry_session(session=s) + r = retry_session.get(url, stream=True, timeout=timeout_seconds) + + body = [] + start_time = time.time() + for chunk in r.iter_content(1024): + body.append(chunk) + if time.time() > (start_time + timeout_seconds): + message = ( + "A read timeout occurred while connecting to {0} for the '{1}' camera profile. Are you using the video stream URL by mistake?" + .format(url, camera_profile.name) + ) + logger.exception(message) + raise CameraError('read-timeout', message) + #if len(camera_profile.webcam_settings.username) > 0: + # r = requests.get(url, auth=HTTPBasicAuth(camera_profile.webcam_settings.username, + # camera_profile.webcam_settings.password), + # verify=not camera_profile.webcam_settings.ignore_ssl_error, + # timeout=float(timeout_seconds)) + #else: + # r = requests.get( + # url, verify=not camera_profile.webcam_settings.ignore_ssl_error, timeout=float(timeout_seconds)) if r.status_code == requests.codes.ok: if 'content-length' in r.headers and r.headers["content-length"] == 0: message = "The request contained no data for the '{0}' camera profile.".format(camera_profile.name) @@ -466,16 +603,20 @@ def _test_web_camera(camera_profile, timeout_seconds=2, is_before_print_test=Fal raise CameraError('request-contained-no-data', message) elif "image/jpeg" not in r.headers["content-type"].lower(): message = ( - "The returned daata was not an image for the '{0}' camera profile.".format(camera_profile.name) + "The returned data was not an image for the '{0}' camera profile.".format(camera_profile.name) ) logger.error(message) raise CameraError('not-an-image', message) - elif not is_before_print_test or camera_profile.apply_settings_before_print: + elif ( + is_before_print_test and + camera_profile.enable_custom_image_preferences and + camera_profile.apply_settings_before_print + ): CameraControl._test_web_camera_image_preferences(camera_profile, timeout_seconds) else: message = ( "An invalid status code or {0} was returned from the '{1}' camera profile." - .format(r.status_code, camera_profile.name) + .format(r.status_code, camera_profile.name) ) logger.error(message) raise CameraError('invalid-status-code', message) @@ -520,6 +661,283 @@ def _test_web_camera(camera_profile, timeout_seconds=2, is_before_print_test=Fal logger.exception(message) raise CameraError('missing-schema', message, cause=e) + @staticmethod + def test_script(camera_profile, script_type, base_folder): + snapshot_created = None + if script_type == 'snapshot': + success, error, snapshot_created = CameraControl._test_camera_snapshot_script(camera_profile, script_type, base_folder) + elif script_type in ['before-snapshot', 'after-snapshot']: + success, error = CameraControl._test_camera_before_after_snapshot_script( + camera_profile, script_type, base_folder + ) + elif script_type in ['before-print', 'after-print']: + success, error = CameraControl._test_print_script(camera_profile, script_type, base_folder) + elif script_type == 'before-render': + success, error = CameraControl._test_before_render_script(camera_profile, script_type, base_folder) + elif script_type == 'after-render': + success, error = CameraControl._test_after_render_script(camera_profile, script_type, base_folder) + else: + return False, "An unknown script type of {0} was sent. Cannot test script".format(script), None + + return success, error, snapshot_created + + @staticmethod + def _test_camera_snapshot_script(camera_profile, script_type, base_folder): + """Test a script camera.""" + # create a temp directory for use with this test + temp_directory = mkdtemp() + + try: + camera_name = camera_profile.name + snapshot_number = 0 + delay_seconds = camera_profile.delay + data_directory = temp_directory + snapshot_directory = os.path.join( + data_directory, "{}".format(uuid.uuid4()), "{}".format(uuid.uuid4()) + ) + snapshot_filename = utility.get_snapshot_filename( + "test_snapshot", snapshot_number + ) + snapshot_full_path = os.path.join(snapshot_directory, snapshot_filename) + timeout_seconds = camera_profile.timeout_ms / 1000.0 + + script_path = camera_profile.external_camera_snapshot_script + + cmd = script_job = script.CameraScriptSnapshot( + script_path, + camera_name, + snapshot_number, + delay_seconds, + data_directory, + snapshot_directory, + snapshot_filename, + snapshot_full_path, + timeout_seconds=timeout_seconds + ) + cmd.run() + # check to see if the snapshot image was created + success = cmd.success() + snapshot_created = os.path.exists(snapshot_full_path) + return cmd.success(), cmd.error_message, snapshot_created + finally: + utility.rmtree(temp_directory) + + @staticmethod + def _test_camera_before_after_snapshot_script(camera_profile, script_type, base_folder): + """Test a script camera.""" + # create a temp directory for use with this test + temp_directory = mkdtemp() + + try: + camera_name = camera_profile.name + snapshot_number = 0 + delay_seconds = camera_profile.delay + data_directory = temp_directory + snapshot_directory = os.path.join( + data_directory, "{}".format(uuid.uuid4()), "{}".format(uuid.uuid4()) + ) + snapshot_filename = utility.get_snapshot_filename( + "test_snapshot", snapshot_number + ) + snapshot_full_path = os.path.join(snapshot_directory, snapshot_filename) + timeout_seconds = camera_profile.timeout_ms / 1000.0 + + # setup test depending on the script_type + script_path = "" + if script_type == 'before-snapshot': + script_path = camera_profile.on_before_snapshot_script + elif script_type == 'after-snapshot': + script_path = camera_profile.on_after_snapshot_script + # we need to add an image to the temp folder, in case the script operates on the image + test_image_path = os.path.join(base_folder, "data", "Images", "test-snapshot-image.jpg") + target_directory = os.path.dirname(snapshot_full_path) + if not os.path.exists(target_directory): + try: + os.makedirs(target_directory) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + shutil.copy(test_image_path, snapshot_full_path) + + if script_type == 'before-snapshot': + cmd = script.CameraScriptBeforeSnapshot( + script_path, + camera_name, + snapshot_number, + delay_seconds, + data_directory, + snapshot_directory, + snapshot_filename, + snapshot_full_path, + timeout_seconds=timeout_seconds + ) + else: + cmd = script.CameraScriptAfterSnapshot( + script_path, + camera_name, + snapshot_number, + delay_seconds, + data_directory, + snapshot_directory, + snapshot_filename, + snapshot_full_path, + timeout_seconds=timeout_seconds + ) + cmd.run() + return cmd.success(), cmd.error_message + finally: + utility.rmtree(temp_directory) + + @staticmethod + def _test_print_script(camera_profile, script_type, base_folder): + timeout_seconds = camera_profile.timeout_ms / 1000.0 + # build the arg list + if script_type == "before-print": + script_path = camera_profile.on_print_start_script.strip() + else: + script_path = camera_profile.on_print_end_script.strip() + + if script_type == "before-print": + cmd = script.CameraScriptBeforePrint( + script_path, + camera_profile.name, + timeout_seconds=timeout_seconds + ) + else: + cmd = script.CameraScriptAfterPrint( + script_path, + camera_profile.name, + timeout_seconds=timeout_seconds + ) + cmd.run() + return cmd.success(), cmd.error_message + + @staticmethod + def _test_before_render_script(camera_profile, script_type, base_folder): + timeout_seconds = camera_profile.timeout_ms / 1000.0 + # create a temp folder for the rendered file and the snapshots + temp_directory = mkdtemp() + + try: + # create 10 snapshots + test_image_path = os.path.join(base_folder, "data", "Images", "test-snapshot-image.jpg") + snapshot_directory = os.path.join( + temp_directory, "{}".format(uuid.uuid4()), "{}".format(uuid.uuid4()) + ) + if not os.path.exists(snapshot_directory): + try: + os.makedirs(snapshot_directory) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + + for snapshot_number in range(10): + snapshot_file_name = utility.get_snapshot_filename( + 'test', snapshot_number + ) + target_snapshot_path = os.path.join(snapshot_directory, snapshot_file_name) + shutil.copy(test_image_path, target_snapshot_path) + + snapshot_filename_format = os.path.basename( + utility.get_snapshot_filename( + "render_script_test", utility.SnapshotNumberFormat + ) + ) + script_path = camera_profile.on_before_render_script.strip() + + if script_path is None or len(script_path) == 0: + return False, "No script path was provided. Please enter a script path and try again." + + if not os.path.isfile(script_path): + return False, "The script path '{0}' does not exist. Please enter a valid script path and try again.".format(script_path) + + console_output = "" + error_message = "" + + cmd = script.CameraScriptBeforeRender( + script_path, + camera_profile.name, + snapshot_directory, + snapshot_filename_format, + os.path.join(snapshot_directory, snapshot_filename_format), + timeout_seconds=timeout_seconds + ) + cmd.run() + return cmd.success(), cmd.error_message + finally: + utility.rmtree(temp_directory) + + @staticmethod + def _test_after_render_script(camera_profile, script_type, base_folder): + + # create a temp folder for the rendered file and the snapshots + temp_directory = mkdtemp() + timeout_seconds = camera_profile.timeout_ms / 1000.0 + try: + # create 10 snapshots + test_image_path = os.path.join(base_folder, "data", "Images", "test-snapshot-image.jpg") + snapshot_directory = os.path.join( + temp_directory, "{}".format(uuid.uuid4()), "{}".format(uuid.uuid4()) + ) + if not os.path.exists(snapshot_directory): + try: + os.makedirs(snapshot_directory) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + + for snapshot_number in range(10): + snapshot_file_name = utility.get_snapshot_filename( + 'test', snapshot_number + ) + target_snapshot_path = os.path.join(snapshot_directory, snapshot_file_name) + shutil.copy(test_image_path, target_snapshot_path) + + snapshot_filename_format = os.path.basename( + utility.get_snapshot_filename( + "render_script_test", utility.SnapshotNumberFormat + ) + ) + + + script_path = camera_profile.on_after_render_script.strip() + rendering_path = os.path.join(temp_directory, "timelapse", "test_rendering.mp4") + + output_filepath = utility.get_collision_free_filepath(rendering_path) + output_filename = utility.get_filename_from_full_path(rendering_path) + output_directory = utility.get_directory_from_full_path(rendering_path) + output_extension = utility.get_extension_from_full_path(rendering_path) + + if script_path is None or len(script_path) == 0: + return False, "No script path was provided. Please enter a script path and try again." + + if not os.path.exists(script_path): + return False, "The script path '{0}' does not exist. Please enter a valid script path and try again.".format( + script_path) + + cmd = script.CameraScriptAfterRender( + script_path, + camera_profile.name, + snapshot_directory, + snapshot_filename_format, + os.path.join(snapshot_directory, snapshot_filename_format), + output_directory, + output_filename, + output_extension, + rendering_path, + timeout_seconds=timeout_seconds + ) + cmd.run() + return cmd.success(), cmd.error_message + finally: + utility.rmtree(temp_directory) + @staticmethod def load_webcam_defaults( server_type, @@ -528,7 +946,8 @@ def load_webcam_defaults( username, password, ignore_ssl_error, - timeout_seconds=5): + retries=3, + backoff_factor=0.1): if server_type == MjpgStreamer.server_type: @@ -540,14 +959,16 @@ def load_webcam_defaults( username, password, ignore_ssl_error, - timeout_seconds=timeout_seconds + retries=retries, + backoff_factor=backoff_factor ) if not success: return False, errors # set the control values to the defaults controls = settings["webcam_settings"]["mjpg_streamer"]["controls"] - for key, control in six.iteritems(controls): - + # remove python 2 compatibility + # for key, control in six.iteritems(controls): + for key, control in controls.items(): control.value = control.default controls[key] = control.to_dict() @@ -559,8 +980,10 @@ def load_webcam_defaults( password=password, ignore_ssl_error=ignore_ssl_error, camera_name=camera_name, - timeout_seconds=timeout_seconds + retries=retries, + backoff_factor=backoff_factor ) + thread.daemon = True thread.start() thread.join() @@ -581,7 +1004,8 @@ def __init__( password=None, ignore_ssl_error=False, camera_name=None, - timeout_seconds=4 + retries=3, + backoff_factor=0.1 ): super(MjpgStreamerThread, self).__init__() if profile: @@ -589,18 +1013,19 @@ def __init__( self.username = profile.webcam_settings.username self.password = profile.webcam_settings.password self.ignore_ssl_error = profile.webcam_settings.ignore_ssl_error - self.timeout_seconds = timeout_seconds self.camera_name = profile.name else: self.address = address self.username = username self.password = password self.ignore_ssl_error = ignore_ssl_error - self.timeout_seconds = timeout_seconds self.camera_name = camera_name - if isinstance(self.address, six.string_types): + # remove python 2 compatibility + # if isinstance(self.address, six.string_types): + if isinstance(self.address, str): self.address = self.address.strip() - + self.retries = retries + self.backoff_factor=backoff_factor self.errors = [] self.success = False @@ -614,7 +1039,8 @@ def __init__( password=None, ignore_ssl_error=False, camera_name=None, - timeout_seconds=4 + retries=3, + backoff_factor=0.1 ): super(MjpgStreamerControlThread, self).__init__( profile=profile, @@ -623,7 +1049,8 @@ def __init__( password=password, ignore_ssl_error=ignore_ssl_error, camera_name=camera_name, - timeout_seconds=timeout_seconds + retries=retries, + backoff_factor=backoff_factor ) self.controls = None @@ -631,7 +1058,7 @@ def run(self): try: self.controls = self.get_controls_from_server() except CameraError as e: - logger.exception(e) + logger.exception("An unexpected error occurred while running the MjpgStreamerControlThread.") self.errors.append(e) def get_controls_from_server(self): @@ -648,32 +1075,22 @@ def get_controls_from_server(self): return control_settings def get_file(self, file_name): + + url = "{camera_address}{file_name}".format(camera_address=self.address, file_name=file_name) try: - url = "{camera_address}{file_name}".format(camera_address=self.address, file_name=file_name) + s = requests.Session() + s.verify = not self.ignore_ssl_error if len(self.username) > 0: - r = requests.get(url, auth=HTTPBasicAuth(self.username, self.password), - verify=not self.ignore_ssl_error, timeout=float(self.timeout_seconds)) - else: - r = requests.get(url, verify=not self.ignore_ssl_error, - timeout=float(self.timeout_seconds)) - - if r.status_code == 501: - message = "Access was denied to the mjpg-streamer {0} file for the " \ - "'{1}' camera profile.".format(file_name, self.camera_name) - raise CameraError("mjpg_streamer-control-error", message) - if r.status_code != requests.codes.ok: - message = ( - "Recived a status code of ({0}) while retrieving the {1} file from the {2} camera profile. Double " - "check your 'Base Address' and 'Snapshot Address Template' within your camera profile settings." - .format(r.status_code, file_name, self.camera_name) - ) - raise CameraError('webcam_settings_apply_error', message) - try: - data = json.loads(r.text) - except ValueError as e: - raise CameraError('json_error', "Unable to read the input.json file from mjpg-streamer. Please chack " - "your base address and try again.", cause=e) - return data + s.auth = (self.username, self.password) + + r = utility.requests_retry_session(session=s, retries=self.retries, + backoff_factor=self.backoff_factor).request("GET", url) + #if len(self.username) > 0: + # r = requests.get(url, auth=HTTPBasicAuth(self.username, self.password), + # verify=not self.ignore_ssl_error, timeout=float(self.timeout_seconds)) + #else: + # r = requests.get(url, verify=not self.ignore_ssl_error, + # timeout=float(self.timeout_seconds)) except SSLError as e: message = ( "An SSL error was raised while retrieving the {0} file for the '{1}' camera profile." @@ -683,15 +1100,36 @@ def get_file(self, file_name): except requests.ConnectionError as e: message = ( "Unable to connect to the camera to '{0}' to retrieve controls for the {1} camera profile." - .format(url, self.camera_name) + .format(url, self.camera_name) ) - raise CameraError("connection-error", message, cause=e) + raise CameraError("connection_error", message, cause=e) except Exception as e: message = ( "An unexpected error was raised while retrieving the {0} file for the '{1}' camera profile." - .format(file_name, self.camera_name) + .format(file_name, self.camera_name) + ) + raise CameraError('unexpected_error', message, cause=e) + + if r.status_code == 501: + message = "Access was denied to the mjpg-streamer {0} file for the " \ + "'{1}' camera profile.".format(file_name, self.camera_name) + raise CameraError("access_denied", message) + if r.status_code == 404: + message = "Unable to find the camera at the supplied base address for the " \ + "'{0}' camera profile.".format(self.camera_name) + raise CameraError("file_not_found", message) + if r.status_code != requests.codes.ok: + message = ( + "Recived a status code of ({0}) while retrieving the {1} file from the {2} camera profile." + .format(r.status_code, file_name, self.camera_name) ) - raise CameraError('unexpected-error', message, cause=e) + raise CameraError('unexpected_status_code', message) + try: + data = json.loads(r.text, strict=False) + except ValueError as e: + raise CameraError('json_error', "Unable to read the input.json file from mjpg-streamer. Please chack " + "your base address and try again.", cause=e) + return data class MjpgStreamerSettingsThread(MjpgStreamerThread): @@ -704,7 +1142,8 @@ def __init__( password=None, ignore_ssl_error=False, camera_name=None, - timeout_seconds=4 + retries=3, + backoff_factor=0.1 ): super(MjpgStreamerSettingsThread, self).__init__( profile=profile, @@ -713,7 +1152,8 @@ def __init__( password=password, ignore_ssl_error=ignore_ssl_error, camera_name=camera_name, - timeout_seconds=timeout_seconds + retries=retries, + backoff_factor=backoff_factor ) if profile: mjpg_streamer = profile.webcam_settings.mjpg_streamer.clone() @@ -734,8 +1174,9 @@ def apply_mjpg_streamer_settings(self): controls_list = [] - - for key, control in six.iteritems(self.controls): + # remove python 2 compatibility + # for key, control in six.iteritems(self.controls): + for key, control in self.controls.items(): controls_list.append(control) # Sort the controls. They need to be sent in order. @@ -754,8 +1195,10 @@ def apply_mjpg_streamer_settings(self): self.password, self.ignore_ssl_error, self.camera_name, - timeout_seconds=self.timeout_seconds + retries=self.retries, + backoff_factor=self.backoff_factor ) + thread.daemon = True threads.append(thread) if len(threads) > 0: @@ -770,6 +1213,7 @@ def apply_mjpg_streamer_settings(self): if not self.success: for error in errors: self.errors.append(error) + break # Do not continue applying settings if one fails. self.success = len(self.errors) == 0 @@ -782,7 +1226,8 @@ def __init__( password, ignore_ssl_error, camera_name, - timeout_seconds=4 + retries=3, + backoff_factor=0.1 ): super(MjpgStreamerSettingThread, self).__init__( address=address, @@ -790,7 +1235,8 @@ def __init__( password=password, ignore_ssl_error=ignore_ssl_error, camera_name=camera_name, - timeout_seconds=timeout_seconds + retries=retries, + backoff_factor=backoff_factor ) self.control = control @@ -830,14 +1276,22 @@ def apply_mjpg_streamer_setting(self): address=self.address, querystring=querystring ) - logger.debug("Changing %s to %s for %s. Querystring: %s", name, str(value), self.camera_name, querystring) + logger.debug("Changing %s to %s for %s. Request: %s", name, str(value), self.camera_name, url) try: + # old version without session + #if len(self.username) > 0: + # r = requests.get(url, auth=HTTPBasicAuth(self.username, self.password), + # verify=not self.ignore_ssl_error, timeout=float(self.timeout_seconds)) + #else: + # r = requests.get(url, verify=not self.ignore_ssl_error, + # timeout=float(self.timeout_seconds)) + + s = requests.Session() + s.verify = not self.ignore_ssl_error if len(self.username) > 0: - r = requests.get(url, auth=HTTPBasicAuth(self.username, self.password), - verify=not self.ignore_ssl_error, timeout=float(self.timeout_seconds)) - else: - r = requests.get(url, verify=not self.ignore_ssl_error, - timeout=float(self.timeout_seconds)) + s.auth = (self.username, self.password) + + r = utility.requests_retry_session(session=s, retries=self.retries, backoff_factor=self.backoff_factor).request("GET", url) webcam_server_type = CameraControl.get_webcam_server_type_from_request(r) if webcam_server_type != "mjpg-streamer": @@ -849,17 +1303,16 @@ def apply_mjpg_streamer_setting(self): if r.status_code == 501: message = "Access was denied to the mjpg-streamer control.html while applying the {0} setting to the '{" \ "1}' camera profile. Please see this link to correct this " \ + "href=\"https://github.com/FormerLurker/Octolapse/wiki/V0.4---Enabling-Camera-Controls\" target = \"_blank\">Please see this link to correct this " \ "error., disable the 'Enable And Apply Preferences at Startup' " \ "and 'Enable And Apply Preferences Before Print' options '.".format(name, self.camera_name) logger.error(message) - raise CameraError("mjpg_streamer-control-error", message) + raise CameraError("mjpg_streamer_control_error", message) if r.status_code != requests.codes.ok: message = ( "Recived a status code of ({0}) while applying the {1} settings to the {2} camera profile. " - "Double check your 'Base Address' and 'Snapshot Address Template' within your camera profile " + "Double check your 'Base Address' and 'Snapshot Address' within your camera profile " "settings. Or disable 'Custom Image Preferences' for this profile and try again.".format( r.status_code, name, self.camera_name ) @@ -873,73 +1326,42 @@ def apply_mjpg_streamer_setting(self): "An SSL error was raised while applying the {0} setting to the '{1}' camera profile." .format(name, self.camera_name) ) - logger.error(message) + logger.exception(message) raise CameraError('ssl_error', message, cause=e) except Exception as e: message = ( "An unexpected error was raised while applying the {0} setting to the '{1}' camera profile." .format(name, self.camera_name) ) - logger.error(message) + logger.exception(message) raise CameraError('webcam_settings_apply_error', message, cause=e) class CameraSettingScriptThread(Thread): - def __init__(self, camera): + def __init__(self, camera, script_path, script_args, script_type): super(CameraSettingScriptThread, self).__init__() self.Camera = camera self.camera_name = camera.name - self.errors = [] + self.script_path = script_path.strip() + self.script_type = script_type + self.script_args = script_args + self.console_output = "" + self.error = None def run(self): - try: - script = self.Camera.on_print_start_script.strip() - logger.info( - "Executing the 'Before Print Starts' script at %s for the '%s' camera.", script, self.camera_name + if self.script_type == "before-print": + cmd = script.CameraScriptBeforePrint( + self.script_path, + self.camera_name ) - if not script: - message = "The Camera Initialization script is empty" - logger.error(message) - raise CameraError('no_camera_script_path', message) - try: - script_args = [ - script, - self.Camera.name - ] - cmd = utility.POpenWithTimeout() - return_code = cmd.run(script_args, None) - console_output = cmd.stdout - error_message = cmd.stderr - except utility.POpenWithTimeout.ProcessError as e: - message = "An OS Error error occurred while executing the custom camera initialization script" - logger.exception(message) - raise CameraError('camera_initialization_error', message, cause=e) - - if error_message is not None: - if error_message.endswith("\r\n"): - error_message = error_message[:-2] - if not return_code == 0: - if error_message is not None: - error_message = "The custom camera initialization script failed with the following error message: {0}" \ - .format(error_message) - else: - error_message = ( - "The custom camera initialization script returned {0}," - " which indicates an error.".format(return_code) - ) - logger.exception(error_message) - elif error_message is not None: - logger.warn( - "The console returned an error while running the script at %s for the '%s' camera. Details:%s", - script, self.camera_name, error_message - ) - else: - logger.info( - "The 'Before Print Starts' script for the %s camera completed successfully.", self.camera_name - ) - raise CameraError('camera_initialization_error', error_message) - except CameraError as e: - self.errors.append(e) + else: + cmd = script.CameraScriptAfterPrint( + self.script_path, + self.camera_name + ) + cmd.run() + if not cmd.success(): + self.error = CameraError('error_message_returned', cmd.error_message) class CameraError(Exception): diff --git a/octoprint_octolapse/data/Images/no-camera-selected.png b/octoprint_octolapse/data/Images/no-camera-selected.png index a47f15d5..a25823f1 100644 Binary files a/octoprint_octolapse/data/Images/no-camera-selected.png and b/octoprint_octolapse/data/Images/no-camera-selected.png differ diff --git a/octoprint_octolapse/data/Images/no-image-available.png b/octoprint_octolapse/data/Images/no-image-available.png index a47f15d5..be147974 100644 Binary files a/octoprint_octolapse/data/Images/no-image-available.png and b/octoprint_octolapse/data/Images/no-image-available.png differ diff --git a/octoprint_octolapse/data/Images/no_snapshot.png b/octoprint_octolapse/data/Images/no_snapshot.png index 53a72bd8..32234ed1 100644 Binary files a/octoprint_octolapse/data/Images/no_snapshot.png and b/octoprint_octolapse/data/Images/no_snapshot.png differ diff --git a/octoprint_octolapse/data/Images/test-snapshot-image.jpg b/octoprint_octolapse/data/Images/test-snapshot-image.jpg new file mode 100644 index 00000000..d65f2e91 Binary files /dev/null and b/octoprint_octolapse/data/Images/test-snapshot-image.jpg differ diff --git a/octoprint_octolapse/data/lib/c/extruder.cpp b/octoprint_octolapse/data/lib/c/extruder.cpp new file mode 100644 index 00000000..9f1a6e5d --- /dev/null +++ b/octoprint_octolapse/data/lib/c/extruder.cpp @@ -0,0 +1,160 @@ +#include "extruder.h" +#include "logging.h" + +extruder::extruder() +{ + x_firmware_offset = 0; + y_firmware_offset = 0; + z_firmware_offset = 0; + e = 0; + e_offset = 0; + e_relative = 0; + extrusion_length = 0; + extrusion_length_total = 0; + retraction_length = 0; + deretraction_length = 0; + is_extruding_start = false; + is_extruding = false; + is_primed = false; + is_retracting_start = false; + is_retracting = false; + is_retracted = false; + is_partially_retracted = false; + is_deretracting_start = false; + is_deretracting = false; + is_deretracted = false; +} + +double extruder::get_offset_e() const +{ + return e - e_offset; +} + +PyObject* extruder::to_py_tuple() const +{ + //std::cout << "Building extruder py_tuple.\r\n"; + PyObject* py_extruder = Py_BuildValue( + // ReSharper disable once StringLiteralTypo + "ddddddddddllllllllll", + // Floats + x_firmware_offset, // 0 + y_firmware_offset, // 1 + z_firmware_offset, // 2 + e, // 3 + e_offset, // 4 + e_relative, // 5 + extrusion_length, // 6 + extrusion_length_total, // 7 + retraction_length, // 8 + deretraction_length, // 9 + // Bool (represented as an integer) + (long int)(is_extruding_start ? 1 : 0), // 10 + (long int)(is_extruding ? 1 : 0), // 11 + (long int)(is_primed ? 1 : 0), // 12 + (long int)(is_retracting_start ? 1 : 0), // 13 + (long int)(is_retracting ? 1 : 0), // 14 + (long int)(is_retracted ? 1 : 0), // 15 + (long int)(is_partially_retracted ? 1 : 0), // 16 + (long int)(is_deretracting_start ? 1 : 0), // 17 + (long int)(is_deretracting ? 1 : 0), // 18 + (long int)(is_deretracted ? 1 : 0) // 19 + ); + if (py_extruder == NULL) + { + std::string message = + "extruder.to_py_tuple: Unable to convert extruder value to a PyObject tuple via Py_BuildValue."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return NULL; + } + return py_extruder; +} + +PyObject* extruder::to_py_dict() const +{ + PyObject* p_extruder = Py_BuildValue( + "{s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i}", + // FLOATS + "x_firmware_offset", + x_firmware_offset, + "y_firmware_offset", + y_firmware_offset, + "z_firmware_offset", + z_firmware_offset, + "e", + e, + "e_offset", + e_offset, + "e_relative", + e_relative, + "extrusion_length", + extrusion_length, + "extrusion_length_total", + extrusion_length_total, + "retraction_length", + retraction_length, + "deretraction_length", + deretraction_length, + // Bool (represented as an integer) + "is_extruding_start", + (long int)(is_extruding_start ? 1 : 0), + "is_extruding", + (long int)(is_extruding ? 1 : 0), + "is_primed", + (long int)(is_primed ? 1 : 0), + "is_retracting_start", + (long int)(is_retracting_start ? 1 : 0), + "is_retracting", + (long int)(is_retracting ? 1 : 0), + "is_retracted", + (long int)(is_retracted ? 1 : 0), + "is_partially_retracted", + (long int)(is_partially_retracted ? 1 : 0), + "is_deretracting_start", + (long int)(is_deretracting_start ? 1 : 0), + "is_deretracting", + (long int)(is_deretracting ? 1 : 0), + "is_deretracted", + (long int)(is_deretracted ? 1 : 0) + ); + if (p_extruder == NULL) + { + std::string message = "extruder.to_py_dict: Unable to convert extruder value to a dict PyObject via Py_BuildValue."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return NULL; + } + return p_extruder; +} + +PyObject* extruder::build_py_object(extruder* p_extruders, const unsigned int num_extruders) +{ + //std::cout << "Building extruders py_object.\r\n"; + PyObject* py_extruders = PyList_New(0); + if (py_extruders == NULL) + { + std::string message = "Error executing Extruder.build_py_object: Unable to create Extruder PyList object."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return NULL; + } + + // Create each snapshot plan + for (unsigned int index = 0; index < num_extruders; index++) + { + PyObject* py_extruder = p_extruders[index].to_py_tuple(); + if (py_extruder == NULL) + { + return NULL; + } + bool success = !(PyList_Append(py_extruders, py_extruder) < 0); // reference to pSnapshotPlan stolen + if (!success) + { + std::string message = + "Error executing Extruder.build_py_object Unable to append the extruder to the extruders list."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return NULL; + } + // Need to decref after PyList_Append, since it increfs the PyObject + Py_DECREF(py_extruder); + } + + return py_extruders; +} diff --git a/octoprint_octolapse/data/lib/c/extruder.h b/octoprint_octolapse/data/lib/c/extruder.h new file mode 100644 index 00000000..74537376 --- /dev/null +++ b/octoprint_octolapse/data/lib/c/extruder.h @@ -0,0 +1,36 @@ +#pragma once +#ifdef _DEBUG +//#undef _DEBUG +#include +//python311_d.lib +#else +#include +#endif +struct extruder +{ + extruder(); + double x_firmware_offset; + double y_firmware_offset; + double z_firmware_offset; + double e; + double e_offset; + double e_relative; + double extrusion_length; + double extrusion_length_total; + double retraction_length; + double deretraction_length; + bool is_extruding_start; + bool is_extruding; + bool is_primed; + bool is_retracting_start; + bool is_retracting; + bool is_retracted; + bool is_partially_retracted; + bool is_deretracting_start; + bool is_deretracting; + bool is_deretracted; + double get_offset_e() const; + PyObject* to_py_tuple() const; + PyObject* to_py_dict() const; + static PyObject* build_py_object(extruder* p_extruders, unsigned int num_extruders); +}; diff --git a/octoprint_octolapse/data/lib/c/gcode_comment_processor.cpp b/octoprint_octolapse/data/lib/c/gcode_comment_processor.cpp new file mode 100644 index 00000000..4b9906cc --- /dev/null +++ b/octoprint_octolapse/data/lib/c/gcode_comment_processor.cpp @@ -0,0 +1,412 @@ +#include "gcode_comment_processor.h" +#include "utilities.h" +gcode_comment_processor::gcode_comment_processor() +{ + current_section_ = section_type_no_section; + current_subsection_ = subsection_type_none; + processing_type_ = comment_process_type_unknown; + +} + +gcode_comment_processor::~gcode_comment_processor() +{ +} + +comment_process_type gcode_comment_processor::get_comment_process_type() +{ + return processing_type_; +} + +void gcode_comment_processor::update(position& pos) +{ + if (processing_type_ == comment_process_type_off) + return; + + if (current_section_ != section_type_no_section) + { + update_feature_from_section(pos); + return; + } + + if (processing_type_ == comment_process_type_unknown || processing_type_ == comment_process_type_slic3r_pe) + { + if (update_feature_for_slic3r_pe_comment(pos, pos.command.comment)) + processing_type_ = comment_process_type_slic3r_pe; + } +} + +bool gcode_comment_processor::update_feature_for_slic3r_pe_comment(position& pos, std::string& comment) const +{ + // External Perimeter - SuperSlicer + // Also, adding overhang perimeter here too. + if (utilities::is_in_caseless_trim(comment, SLICER_PE_OUTER_PERIMETER_COMMENTS)) + { + pos.feature_type_tag = feature_type_outer_perimeter_feature; + return true; + } + // Internal Perimeter - SuperSlicer + if (utilities::is_in_caseless_trim(comment, SLICER_PE_INNER_PERIMETER_COMMENTS)) + { + pos.feature_type_tag = feature_type_inner_perimeter_feature; + return true; + } + if (utilities::is_in_caseless_trim(comment, SLICER_PE_UNKNOWN_PERIMETER_COMMENTS)) + { + pos.feature_type_tag = feature_type_unknown_perimeter_feature; + return true; + } + + if (utilities::is_in_caseless_trim(comment, SLICER_PE_INFILL_COMMENTS)) + { + pos.feature_type_tag = feature_type_infill_feature; + return true; + } + // Solid Infill/Top Solid Infill - SuperSlicer + if (utilities::is_in_caseless_trim(comment, SLICER_PE_SOLID_INFILL_COMMENTS)) + { + pos.feature_type_tag = feature_type_solid_infill_feature; + return true; + } + + // Gap Fill - SuperSlicer + if (utilities::is_in_caseless_trim(comment, SLICER_PE_GAP_FILL_COMMENTS)) + { + pos.feature_type_tag = feature_type_gap_fill_feature; + return true; + } + + // bridge infill - SuperSlicer + if (utilities::is_in_caseless_trim(comment, SLICER_PE_BRIDGE_COMMENTS)) + { + pos.feature_type_tag = feature_type_bridge_feature; + return true; + } + + if (utilities::is_in_caseless_trim(comment, SLICER_PE_SKIRT_COMMENTS)) + { + pos.feature_type_tag = feature_type_skirt_feature; + return true; + } + return false; +} + +void gcode_comment_processor::update_feature_from_section(position& pos) const +{ + // Don't update if we're not processing comments, if we have no section, or if we are in a wipe subsection. + if (processing_type_ == comment_process_type_off || current_section_ == section_type_no_section || current_subsection_ == subsection_type_wipe) + return; + + switch (current_section_) + { + case(section_type_outer_perimeter_section): + pos.feature_type_tag = feature_type_outer_perimeter_feature; + break; + case(section_type_inner_perimeter_section): + pos.feature_type_tag = feature_type_inner_perimeter_feature; + break; + case(section_type_skirt_section): + pos.feature_type_tag = feature_type_skirt_feature; + break; + case(section_type_solid_infill_section): + pos.feature_type_tag = feature_type_solid_infill_feature; + break; + case(section_type_ooze_shield_section): + pos.feature_type_tag = feature_type_ooze_shield_feature; + break; + case(section_type_infill_section): + pos.feature_type_tag = feature_type_infill_feature; + break; + case(section_type_prime_pillar_section): + pos.feature_type_tag = feature_type_prime_pillar_feature; + break; + case(section_type_gap_fill_section): + pos.feature_type_tag = feature_type_gap_fill_feature; + break; + case (section_type_bridge_section): + pos.feature_type_tag = feature_type_bridge_feature; + break; + case (section_type_support_section): + pos.feature_type_tag = feature_type_support_feature; + } +} + +void gcode_comment_processor::update(std::string& comment) +{ + switch (processing_type_) + { + case comment_process_type_off: + break; + case comment_process_type_unknown: + update_unknown_section(comment); + break; + case comment_process_type_cura: + update_cura_section(comment); + break; + case comment_process_type_slic3r_pe: + update_slic3r_pe_section(comment); + break; + case comment_process_type_simplify_3d: + update_simplify_3d_section(comment); + break; + } +} + +void gcode_comment_processor::update_unknown_section(std::string& comment) +{ + if (comment.length() == 0) + return; + + if (update_cura_section(comment)) + { + processing_type_ = comment_process_type_cura; + return; + } + + if (update_simplify_3d_section(comment)) + { + processing_type_ = comment_process_type_simplify_3d; + return; + } + if (update_slic3r_pe_section(comment)) + { + processing_type_ = comment_process_type_slic3r_pe; + return; + } +} + +bool gcode_comment_processor::update_cura_section(std::string& comment) +{ + if (comment == "TYPE:WALL-OUTER") + { + current_section_ = section_type_outer_perimeter_section; + return true; + } + else if (comment == "TYPE:WALL-INNER") + { + current_section_ = section_type_inner_perimeter_section; + return true; + } + if (comment == "TYPE:FILL") + { + current_section_ = section_type_infill_section; + return true; + } + if (comment == "TYPE:SKIN") + { + current_section_ = section_type_solid_infill_section; + return true; + } + if (comment.rfind("LAYER:", 0) != std::string::npos || comment.rfind(";MESH:NONMESH", 0) != std::string::npos) + { + current_section_ = section_type_no_section; + return false; + } + if (comment == "TYPE:SKIRT") + { + current_section_ = section_type_skirt_section; + return true; + } + return false; +} + +bool gcode_comment_processor::update_simplify_3d_section(std::string& comment) +{ + // Apparently simplify 3d added the word 'feature' to the their feature comments + // at some point to make my life more difficult :P + if (comment.rfind("feature", 0) != std::string::npos) + { + if (comment == "feature outer perimeter") + { + current_section_ = section_type_outer_perimeter_section; + return true; + } + if (comment == "feature inner perimeter") + { + current_section_ = section_type_inner_perimeter_section; + return true; + } + if (comment == "feature infill") + { + current_section_ = section_type_infill_section; + return true; + } + if (comment == "feature solid layer") + { + current_section_ = section_type_solid_infill_section; + return true; + } + if (comment == "feature skirt") + { + current_section_ = section_type_skirt_section; + return true; + } + if (comment == "feature ooze shield") + { + current_section_ = section_type_ooze_shield_section; + return true; + } + if (comment == "feature prime pillar") + { + current_section_ = section_type_prime_pillar_section; + return true; + } + if (comment == "feature gap fill") + { + current_section_ = section_type_gap_fill_section; + return true; + } + } + else + { + if (comment == "outer perimeter") + { + current_section_ = section_type_outer_perimeter_section; + return true; + } + if (comment == "inner perimeter") + { + current_section_ = section_type_inner_perimeter_section; + return true; + } + if (comment == "infill") + { + current_section_ = section_type_infill_section; + return true; + } + if (comment == "solid layer") + { + current_section_ = section_type_solid_infill_section; + return true; + } + if (comment == "skirt") + { + current_section_ = section_type_skirt_section; + return true; + } + if (comment == "ooze shield") + { + current_section_ = section_type_ooze_shield_section; + return true; + } + + if (comment == "prime pillar") + { + current_section_ = section_type_prime_pillar_section; + return true; + } + + if (comment == "gap fill") + { + current_section_ = section_type_gap_fill_section; + return true; + } + } + + + return false; +} + +bool gcode_comment_processor::update_slic3r_pe_section(std::string& comment) +{ + // PrusaSlicer / SuperSlicer tags + if (comment == "TYPE:Internal perimeter" + || comment == "TYPE:Perimeter") + { + current_section_ = section_type_inner_perimeter_section; + return true; + } + if (comment == "TYPE:External perimeter" + || comment == "TYPE:Thin wall") + { + current_section_ = section_type_outer_perimeter_section; + return true; + } + if (comment == "TYPE:Internal infill") + { + current_section_ = section_type_infill_section; + return true; + } + if (comment == "TYPE:Solid infill" + || comment == "TYPE:Top solid infill") + { + current_section_ = section_type_solid_infill_section; + + return true; + } + if (comment == "TYPE:Gap fill") + { + current_section_ = section_type_gap_fill_section; + return true; + } + if (comment == "TYPE:Bridge infill" + || comment == "TYPE:Internal bridge infill" + || comment == "TYPE:Overhang perimeter") + { + current_section_ = section_type_bridge_section; + return true; + } + if (comment == "TYPE:Skirt") + { + current_section_ = section_type_skirt_section; + return true; + } + if (comment == "TYPE:Support material" + || comment == "TYPE:Support material interface") + { + // no "support" feature on octolapse. Maybe feature_type_prime_pillar_feature ? + // If we lable this feature as unknown, it will be absolutely last on the 'quality' order, which is what we want. + current_section_ = section_type_support_section; + return true; + } + if (comment == "TYPE:Wipe tower") + { + current_section_ = section_type_prime_pillar_section; + return true; + } + + // Here are some features we want to just ignore. Might want to think about ironing. + if (comment == "TYPE:Mill" + || comment == "TYPE:Unknown" + || comment == "TYPE:Custom" + || comment == "TYPE:Mixed" + || comment == "TYPE:Ironing") + { + current_section_ = section_type_no_section; + return true; + } + + + if (comment == "CP TOOLCHANGE WIPE") + { + current_section_ = section_type_prime_pillar_section; + return true; + } + + // End section codes (we don't want to apply ANY features if we hit these comments) + + if (comment == "CP TOOLCHANGE END" // Reset section on end of tool change + || comment == "LAYER_CHANGE" // Reset section on layer change + || comment == "BEFORE_LAYER_CHANGE" // Reset section on layer change + || comment == "AFTER_LAYER_CHANGE" // Reset section on layer change + ) + { + current_section_ = section_type_no_section; + return true; + } + + // Now look for subsection codes (wipe so far) + if (comment == "WIPE_START") + { + current_subsection_ = subsection_type_wipe; + return true; + } + + if (comment == "WIPE_END") + { + current_subsection_ = subsection_type_none; + return true; + } + + + return false; +} diff --git a/octoprint_octolapse/data/lib/c/gcode_comment_processor.h b/octoprint_octolapse/data/lib/c/gcode_comment_processor.h new file mode 100644 index 00000000..b437c66c --- /dev/null +++ b/octoprint_octolapse/data/lib/c/gcode_comment_processor.h @@ -0,0 +1,106 @@ +#pragma once +#include "position.h" +#define NUM_FEATURE_TYPES 12 + +static const std::string feature_type_name[NUM_FEATURE_TYPES] = { + "unknown_feature", "bridge_feature", "support_feature", "outer_perimeter_feature", "unknown_perimeter_feature", + "inner_perimeter_feature", "skirt_feature", "gap_fill_feature", "solid_infill_feature", "ooze_shield_feature", + "infill_feature", "prime_pillar_feature" +}; + +enum feature_type +{ + feature_type_unknown_feature, + feature_type_bridge_feature, + feature_type_support_feature, + feature_type_outer_perimeter_feature, + feature_type_unknown_perimeter_feature, + feature_type_inner_perimeter_feature, + feature_type_skirt_feature, + feature_type_gap_fill_feature, + feature_type_solid_infill_feature, + feature_type_ooze_shield_feature, + feature_type_infill_feature, + feature_type_prime_pillar_feature, +}; + +enum comment_process_type +{ + comment_process_type_off, + comment_process_type_unknown, + comment_process_type_slic3r_pe, + comment_process_type_cura, + comment_process_type_simplify_3d +}; + +// used for marking slicer sections for cura and simplify 3d +enum section_type +{ + section_type_no_section, + section_type_outer_perimeter_section, + section_type_inner_perimeter_section, + section_type_infill_section, + section_type_gap_fill_section, + section_type_skirt_section, + section_type_solid_infill_section, + section_type_ooze_shield_section, + section_type_prime_pillar_section, + section_type_bridge_section, + section_type_support_section +}; + +// Used for sections inside of sections (depth = 1) +enum subsection_type +{ + subsection_type_none, + subsection_type_wipe +}; + +// Static strings for slicer comment extraction +// Note these must all be lower case, no beginning or trailing whitespace, and the arrays must be null terminated. +static const char* SLICER_PE_OUTER_PERIMETER_COMMENTS[] = { + "external perimeter", "overhang perimeter", "move to first external perimeter point", NULL +}; +static const char* SLICER_PE_INNER_PERIMETER_COMMENTS[] = { + "internal perimeter", "move to first internal perimeter point", NULL +}; +static const char* SLICER_PE_UNKNOWN_PERIMETER_COMMENTS[] = {"perimeter", "move to first perimeter point", NULL}; +static const char* SLICER_PE_INFILL_COMMENTS[] = {"infill", "move to first infill point", NULL}; +static const char* SLICER_PE_SOLID_INFILL_COMMENTS[] = { + "solid infill", "move to first solid infill point", "top solid infill", "move to first top solid infill point", NULL +}; +static const char* SLICER_PE_GAP_FILL_COMMENTS[] = {"gap fill", "move to first gap fill point", NULL}; +static const char* SLICER_PE_BRIDGE_COMMENTS[] = { + "infill(bridge)", "move to first infill(bridge) point", "internal bridge infill", + "move to first internal bridge infill point", NULL +}; +static const char* SLICER_PE_SKIRT_COMMENTS[] = {"skirt", "move to first skirt point", NULL}; + +class gcode_comment_processor +{ +public: + + gcode_comment_processor(); + ~gcode_comment_processor(); + void update(position& pos); + void update(std::string& comment); + comment_process_type get_comment_process_type(); + +private: + section_type current_section_; + subsection_type current_subsection_; + + comment_process_type processing_type_; + + void update_feature_from_section(position& pos) const; + bool update_feature_from_section_from_section(position& pos) const; + bool update_feature_from_section_for_cura(position& pos) const; + bool update_feature_from_section_for_simplify_3d(position& pos) const; + bool update_feature_from_section_for_slice3r_pe(position& pos) const; + void update_feature_for_unknown_slicer_comment(position& pos, std::string& comment); + bool update_feature_for_slic3r_pe_comment(position& pos, std::string& comment) const; + void update_unknown_section(std::string& comment); + bool update_cura_section(std::string& comment); + bool update_simplify_3d_section(std::string& comment); + bool update_slic3r_pe_section(std::string& comment); +}; diff --git a/octoprint_octolapse/data/lib/c/gcode_parser.cpp b/octoprint_octolapse/data/lib/c/gcode_parser.cpp index 6123c74a..f57fdb26 100644 --- a/octoprint_octolapse/data/lib/c/gcode_parser.cpp +++ b/octoprint_octolapse/data/lib/c/gcode_parser.cpp @@ -21,422 +21,671 @@ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "gcode_parser.h" #include "logging.h" -#include -#include +#include "utilities.h" + gcode_parser::gcode_parser() { - // doesn't work in the ancient version of c++ I am forced to use :( - // or at least I don't know how to us a newer one with python 2.7 - // help... - /* - std::vector text_only_function_names = { "M117" }; // "M117" is an example of a command that would work here. - - std::vector parsable_command_names = { - "G0","G1","G2","G3","G10","G11","G20","G21","G28","G29","G80","G90","G91","G92","M82","M83","M104","M105","M106","M109","M114","M116","M140","M141","M190","M191","M207","M208","M240","M400","T" - }; - */ - // Have to resort to barbarity. - // Text only function names - std::vector text_only_function_names; - text_only_function_names.push_back(("M117")); - // parsable_command_names - std::vector parsable_command_names; - parsable_command_names.push_back("G0"); - parsable_command_names.push_back("G1"); - parsable_command_names.push_back("G2"); - parsable_command_names.push_back("G3"); - parsable_command_names.push_back("G10"); - parsable_command_names.push_back("G11"); - parsable_command_names.push_back("G20"); - parsable_command_names.push_back("G21"); - parsable_command_names.push_back("G28"); - parsable_command_names.push_back("G29"); - parsable_command_names.push_back("G80"); - parsable_command_names.push_back("G90"); - parsable_command_names.push_back("G91"); - parsable_command_names.push_back("G92"); - parsable_command_names.push_back("M82"); - parsable_command_names.push_back("M83"); - parsable_command_names.push_back("M104"); - parsable_command_names.push_back("M105"); - parsable_command_names.push_back("M106"); - parsable_command_names.push_back("M109"); - parsable_command_names.push_back("M114"); - parsable_command_names.push_back("M116"); - parsable_command_names.push_back("M140"); - parsable_command_names.push_back("M141"); - parsable_command_names.push_back("M190"); - parsable_command_names.push_back("M191"); - parsable_command_names.push_back("M207"); - parsable_command_names.push_back("M208"); - parsable_command_names.push_back("M240"); - parsable_command_names.push_back("M400"); - parsable_command_names.push_back("T"); - - for (unsigned int index = 0; index < text_only_function_names.size(); index++) - { - std::string functionName = text_only_function_names[index]; - text_only_functions_.insert(functionName); - } - - for (unsigned int index = 0; index < parsable_command_names.size(); index++) - { - std::string commandName = parsable_command_names[index]; - parsable_commands_.insert(commandName); - } - + // doesn't work in the ancient version of c++ I am forced to use :( + // or at least I don't know how to us a newer one with python 2.7 + // help... + /* + std::vector text_only_function_names = { "M117" }; // "M117" is an example of a command that would work here. + + std::vector parsable_command_names = { + "G0","G1","G2","G3","G10","G11","G20","G21","G28","G29","G80","G90","G91","G92","M82","M83","M104","M105","M106","M109","M114","M116","M140","M141","M190","M191","M207","M208","M240","M400","T" + }; + */ + // Have to resort to barbarity. + // Text only function names + std::vector text_only_function_names; + text_only_function_names.push_back(("M117")); + // parsable_command_names + std::vector parsable_command_names; + parsable_command_names.push_back("G0"); + parsable_command_names.push_back("G1"); + parsable_command_names.push_back("G2"); + parsable_command_names.push_back("G3"); + parsable_command_names.push_back("G10"); + parsable_command_names.push_back("G11"); + parsable_command_names.push_back("G20"); + parsable_command_names.push_back("G21"); + parsable_command_names.push_back("G28"); + parsable_command_names.push_back("G29"); + parsable_command_names.push_back("G80"); + parsable_command_names.push_back("G90"); + parsable_command_names.push_back("G91"); + parsable_command_names.push_back("G92"); + parsable_command_names.push_back("M82"); + parsable_command_names.push_back("M83"); + parsable_command_names.push_back("M104"); + parsable_command_names.push_back("M105"); + parsable_command_names.push_back("M106"); + parsable_command_names.push_back("M109"); + parsable_command_names.push_back("M114"); + parsable_command_names.push_back("M116"); + parsable_command_names.push_back("M140"); + parsable_command_names.push_back("M141"); + parsable_command_names.push_back("M190"); + parsable_command_names.push_back("M191"); + parsable_command_names.push_back("M207"); + parsable_command_names.push_back("M208"); + parsable_command_names.push_back("M218"); + parsable_command_names.push_back("M240"); + parsable_command_names.push_back("M400"); + parsable_command_names.push_back("M563"); + parsable_command_names.push_back("T"); + parsable_command_names.push_back("@OCTOLAPSE"); + + for (unsigned int index = 0; index < text_only_function_names.size(); index++) + { + std::string functionName = text_only_function_names[index]; + text_only_functions_.insert(functionName); + } + + for (unsigned int index = 0; index < parsable_command_names.size(); index++) + { + std::string commandName = parsable_command_names[index]; + parsable_commands_.insert(commandName); + } } -gcode_parser::gcode_parser(const gcode_parser &source) +gcode_parser::gcode_parser(const gcode_parser& source) { - // Private copy constructor - you can't copy this class + // Private copy constructor - you can't copy this class } gcode_parser::~gcode_parser() { - text_only_functions_.clear(); - parsable_commands_.clear(); + text_only_functions_.clear(); + parsable_commands_.clear(); +} + +parsed_command gcode_parser::parse_gcode(const char* gcode) +{ + parsed_command p_cmd; + try_parse_gcode(gcode, p_cmd); + return p_cmd; } // Superfast gcode parser - v2 -bool gcode_parser::try_parse_gcode(const char * gcode, parsed_command * command) +bool gcode_parser::try_parse_gcode(const char* gcode, parsed_command& command) +{ + // Create a command + //octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::VERBOSE, gcode); + char* p_gcode = const_cast(gcode); + char* p = const_cast(gcode); + command.is_empty = true; + command.is_known_command = try_extract_gcode_command(&p, &(command.command)); + if (!command.is_known_command) + { + while (true) + { + char c = *p_gcode; + if (c == '\0' || c == ';' || c == ' ' || c == '\t') + break; + else if (c > 31) + { + command.is_empty = false; + break; + } + p_gcode++; + } + // Don't bother logging this... Not worth it + //std::string message = "No gcode command was found: "; + //message += gcode; + //octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::DEBUG, message); + command.command = ""; + } + else + command.is_empty = false; + + bool has_seen_character = false; + while (true) + { + char cur_char = *p_gcode; + if (cur_char == '\0' || cur_char == ';') + break; + else if (cur_char > 32 || cur_char == ' ' && has_seen_character) + { + if (cur_char >= 'a' && cur_char <= 'z') + command.gcode.push_back(cur_char - 32); + else + command.gcode.push_back(cur_char); + has_seen_character = true; + } + p_gcode++; + } + command.gcode = utilities::rtrim(command.gcode); + + if (command.is_known_command) + { + //command->gcode_ = gcode; + //std::vector parameters; + + if (parsable_commands_.find(command.command) == parsable_commands_.end()) + { + // Don't bother logging this. Too much logging. + //std::string message = "The gcode command is not in the parsable commands set: "; + //message += gcode; + //octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::VERBOSE, message); + return true; + } + if (command.command.length() > 0 && command.command == "@OCTOLAPSE") + { + parsed_command_parameter octolapse_parameter; + + if (!try_extract_octolapse_parameter(&p, &octolapse_parameter)) + { + std::string message = "Unable to extract an octolapse parameter from: "; + message += p; + octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::WARNING, message); + return true; + } + command.parameters.push_back(octolapse_parameter); + // Extract any additional parameters the old way + while (true) + { + //std::cout << "GcodeParser.try_parse_gcode - Trying to extract parameters.\r\n"; + parsed_command_parameter param; + if (try_extract_parameter(&p, ¶m)) + command.parameters.push_back(param); + else + { + //std::cout << "GcodeParser.try_parse_gcode - No parameters found.\r\n"; + break; + } + } + } + else if ( + text_only_functions_.find(command.command) != text_only_functions_.end() || + ( + command.command.length() > 0 && command.command[0] == '@' + ) + ) + { + //std::cout << "GcodeParser.try_parse_gcode - Text only parameter found.\r\n"; + parsed_command_parameter text_command; + if (!try_extract_text_parameter(&p, &(text_command.string_value))) + { + std::string message = "Unable to extract a text parameter from: "; + message += p; + octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::WARNING, message); + return true; + } + text_command.name = '\0'; + command.parameters.push_back(text_command); + } + else + { + if (command.command[0] == 'T') + { + //std::cout << "GcodeParser.try_parse_gcode - T parameter found.\r\n"; + parsed_command_parameter param; + + if (!try_extract_t_parameter(&p, ¶m)) + { + std::string message = "Unable to extract a parameter from the T command: "; + message += gcode; + octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); + } + else + command.parameters.push_back(param); + } + else + { + while (true) + { + //std::cout << "GcodeParser.try_parse_gcode - Trying to extract parameters.\r\n"; + parsed_command_parameter param; + if (try_extract_parameter(&p, ¶m)) + command.parameters.push_back(param); + else + { + //std::cout << "GcodeParser.try_parse_gcode - No parameters found.\r\n"; + break; + } + } + } + } + } + try_extract_comment(&p_gcode, &(command.comment)); + + + return command.is_known_command; +} + +bool gcode_parser::try_extract_gcode_command(char** p_p_gcode, std::string* p_command) { - // Create a command - //octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::VERBOSE, gcode); - char * p = const_cast(gcode); - if(!try_extract_gcode_command(&p, &(command->cmd_))) - { - std::string message = "No gcode command was found: "; - message += gcode; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::WARNING, message); - return false; - } - command->gcode_ = gcode; - std::vector parameters; - - if (parsable_commands_.find(command->cmd_) == parsable_commands_.end()) - { - //std::cout << "GcodeParser.try_parse_gcode - Not in command list, exiting.\r\n"; - std::string message = "The gcode command is not in the parsable commands set: "; - message += gcode; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::VERBOSE, message); - return true; - } - - if (text_only_functions_.find(command->cmd_) != text_only_functions_.end()) - { - //std::cout << "GcodeParser.try_parse_gcode - Text only parameter found.\r\n"; - parsed_command_parameter * p_text_command = new parsed_command_parameter(); - - if(!try_extract_text_parameter(&p, &(p_text_command->string_value_))) - { - std::string message = "Unable to extract a text parameter from: "; - message += p; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::WARNING, message); - return true; - } - p_text_command->name_ = "TEXT"; - } - else - { - if (command->cmd_[0] == 'T') - { - //std::cout << "GcodeParser.try_parse_gcode - T parameter found.\r\n"; - parsed_command_parameter* param = new parsed_command_parameter(); - - if(!try_extract_t_parameter(&p,param)) - { - std::string message = "Unable to extract a parameter from the T command: "; - message += gcode; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - delete param; - param = NULL; - } - else - command->parameters_.push_back(param); - } - else - { - while (true) - { - //std::cout << "GcodeParser.try_parse_gcode - Trying to extract parameters.\r\n"; - parsed_command_parameter* param = new parsed_command_parameter(); - if (try_extract_parameter(&p, param)) - command->parameters_.push_back(param); - else - { - //std::cout << "GcodeParser.try_parse_gcode - No parameters found.\r\n"; - delete param; - param = NULL; - break; - } - } - } - } - - return true; - + char* p = *p_p_gcode; + char gcode_word; + bool found_command = false; + + // Ignore Leading Spaces + while (*p == ' ') + { + p++; + } + // See if this is an @ command, which can be used in octoprint for controlling octolapse + if (*p == '@') + { + found_command = gcode_parser::try_extract_at_command(&p, p_command); + } + else + { + } + // Deal with case sensitivity + if (*p >= 'a' && *p <= 'z') + gcode_word = *p - 32; + else + gcode_word = *p; + if (gcode_word == 'G' || gcode_word == 'M' || gcode_word == 'T') + { + // Set the gcode word of the new command to the current pointer's location and increment both + (*p_command).push_back(gcode_word); + p++; + + if (gcode_word != 'T') + { + // the T command is special, it has no address + + // Now look for a command address + while ((*p >= '0' && *p <= '9') || *p == ' ') + { + if (*p != ' ') + { + found_command = true; + (*p_command).push_back(*p++); + } + else if (found_command) + { + // Previously we just ignored all spaces, + // but for the command itself, it might be a good idea + // to assume the space is important. + // instead, keep moving forward until no spaces are found + while (*p == ' ') + { + p++; + } + break; + } + else + { + // a space was encountered, but no command was found. + // increment the pointer and continue to search + // for an address + ++p; + } + } + if (*p == '.') + { + (*p_command).push_back(*p++); + found_command = false; + while ((*p >= '0' && *p <= '9') || *p == ' ') + { + if (*p != ' ') + { + found_command = true; + (*p_command).push_back(*p++); + } + else + ++p; + } + } + } + else + { + // peek at the next character and see if it is either a number, a question mark, a c, x, or integer. + // Use a different pointer so as not to mess up parameter parsing + char* p_t = p; + // skip any whitespace + // Ignore Leading Spaces + while (*p_t == ' ' || *p_t == '\t') + { + p_t++; + } + // create a char to hold the t parameter + char t_param = '\0'; + // + if (*p_t >= 'a' && *p_t <= 'z') + t_param = *p_t - 32; + else + t_param = *p_t; + + + if (t_param == 'C' || t_param == 'X' || t_param == '?') + { + p_t++; + // The next letter looks good! Now see if there are any other characters before the end of the line (excluding comments) + while (*p_t == ' ' || *p_t == '\t') + { + p_t++; + } + if (*p_t == ';' || *p_t == '\0') + found_command = true; + } + else if (t_param >= '0' && t_param <= '9') + { + found_command = true; + } + } + } + *p_p_gcode = p; + return found_command; } -bool gcode_parser::try_extract_gcode_command(char ** p_p_gcode, std::string * p_command) +bool gcode_parser::try_extract_at_command(char** p_p_gcode, std::string* p_command) { - char * p = *p_p_gcode; - char gcode_word; - bool found_command = false; - - // Ignore Leading Spaces - while (*p == ' ') - { - p++; - } - // Deal with case sensitivity - if (*p >= 'a' && *p <= 'z') - gcode_word = *p - 32; - else - gcode_word = *p; - if (gcode_word == 'G' || gcode_word == 'M' || gcode_word == 'T') - { - // Set the gcode word of the new command to the current pointer's location and increment both - (*p_command) += gcode_word; - p++; - - if (gcode_word != 'T') - { - // the T command is special, it has no address - - // Now look for a command address - while ((*p >= '0' && *p <= '9') || *p == ' ') { - if (*p != ' ') - { - found_command = true; - (*p_command) += *p++; - } - else - ++p; - } - if (*p == '.') { - (*p_command) += *p++; - found_command = false; - while ((*p >= '0' && *p <= '9') || *p == ' ') { - if (*p != ' ') - { - found_command = true; - (*p_command) += *p++; - } - else - ++p; - } - } - } - else - { - found_command = true; - } - } - *p_p_gcode = p; - return found_command; + char* p = *p_p_gcode; + bool found_command = false; + while (*p != '\0' && *p != ';' && *p != ' ') + { + if (!found_command) + { + found_command = true; + } + if (*p >= 'a' && *p <= 'z') + (*p_command).push_back(*p++ - 32); + else + (*p_command).push_back(*p++); + } + *p_p_gcode = p; + return found_command; } -bool gcode_parser::try_extract_unsigned_long(char ** p_p_gcode, unsigned long * p_value) { - char * p = *p_p_gcode; - unsigned int r = 0; - bool found_numbers = false; - // skip any leading whitespace - while (*p == ' ') - ++p; - - while ((*p >= '0' && *p <= '9') || *p == ' ') { - if (*p != ' ') - { - found_numbers = true; - r = static_cast((r * 10.0) + (*p - '0')); - } - ++p; - } - if (found_numbers) - { - *p_value = r; - *p_p_gcode = p; - } - - return found_numbers; +bool gcode_parser::try_extract_unsigned_long(char** p_p_gcode, unsigned long* p_value) +{ + char* p = *p_p_gcode; + unsigned int r = 0; + bool found_numbers = false; + // skip any leading whitespace + while (*p == ' ') + ++p; + + while ((*p >= '0' && *p <= '9') || *p == ' ') + { + if (*p != ' ') + { + found_numbers = true; + r = static_cast((r * 10.0) + (*p - '0')); + } + ++p; + } + if (found_numbers) + { + *p_value = r; + *p_p_gcode = p; + } + + return found_numbers; } -double gcode_parser::ten_pow(size_t n) { - double r = 1.0; +double gcode_parser::ten_pow(unsigned short n) +{ + double r = 1.0; - while (n > 0) { - r *= 10; - --n; - } + while (n > 0) + { + r *= 10; + --n; + } - return r; + return r; } -bool gcode_parser::try_extract_double(char ** p_p_gcode, double * p_double) const +bool gcode_parser::try_extract_double(char** p_p_gcode, double* p_double) const { - char * p = *p_p_gcode; - bool neg = false; - double r = 0; - bool found_numbers = false; - // skip any leading whitespace - while (*p == ' ') - ++p; - // Check for negative sign - if (*p == '-') { - neg = true; - ++p; - while (*p == ' ') - ++p; - } - else if (*p == '+') { - // Positive sign doesn't affect anything since we assume positive - ++p; - while (*p == ' ') - ++p; - } - // skip any additional whitespace - - - while ((*p >= '0' && *p <= '9') || *p == ' ') { - if (*p != ' ') - { - found_numbers = true; - r = (r*10.0) + (*p - '0'); - } - ++p; - } - if (*p == '.') { - double f = 0.0; - int n = 0; - ++p; - while ((*p >= '0' && *p <= '9') || *p == ' ') { - if (*p != ' ') - { - found_numbers = true; - f = (f*10.0) + (*p - '0'); - ++n; - } - ++p; - } - //r += f / std::pow(10.0, n); - r += f / ten_pow(n); - } - if (neg) { - r = -r; - } - if (found_numbers) - { - *p_double = r; - *p_p_gcode = p; - } - - return found_numbers; + char* p = *p_p_gcode; + bool neg = false; + double r = 0; + bool found_numbers = false; + // skip any leading whitespace + while (*p == ' ') + ++p; + // Check for negative sign + if (*p == '-') + { + neg = true; + ++p; + while (*p == ' ') + ++p; + } + else if (*p == '+') + { + // Positive sign doesn't affect anything since we assume positive + ++p; + while (*p == ' ') + ++p; + } + // skip any additional whitespace + + + while ((*p >= '0' && *p <= '9') || *p == ' ') + { + if (*p != ' ') + { + found_numbers = true; + r = (r * 10.0) + (*p - '0'); + } + ++p; + } + if (*p == '.') + { + double f = 0.0; + unsigned short n = 0; + ++p; + while ((*p >= '0' && *p <= '9') || *p == ' ') + { + if (*p != ' ') + { + found_numbers = true; + f = (f * 10.0) + (*p - '0'); + ++n; + } + ++p; + } + //r += f / std::pow(10.0, n); + r += f / ten_pow(n); + } + if (neg) + { + r = -r; + } + if (found_numbers) + { + *p_double = r; + *p_p_gcode = p; + } + + return found_numbers; } -bool gcode_parser::try_extract_text_parameter(char ** p_p_gcode, std::string * p_parameter) +bool gcode_parser::try_extract_text_parameter(char** p_p_gcode, std::string* p_parameter) { - // Skip initial whitespace - //std::cout << "GcodeParser.try_extract_parameter - Trying to extract a text parameter from " << *p_p_gcode << "\r\n"; - char * p = *p_p_gcode; - - // Ignore Leading Spaces - while (*p == ' ') - { - p++; - } - // Add all values, stop at end of string or when we hit a ';' - - while (*p != '\0' && *p != ';') - { - (*p_parameter) += *p++; - } - *p_p_gcode = p; - return true; + // Skip initial whitespace + //std::cout << "GcodeParser.try_extract_parameter - Trying to extract a text parameter from " << *p_p_gcode << "\r\n"; + char* p = *p_p_gcode; + + // Ignore Leading Spaces + while (*p == ' ') + { + p++; + } + // Add all values, stop at end of string or when we hit a ';' + + while (*p != '\0' && *p != ';') + { + (*p_parameter).push_back(*p++); + } + *p_p_gcode = p; + return true; +} +bool gcode_parser::try_extract_octolapse_parameter(char** p_p_gcode, parsed_command_parameter* p_parameter) +{ + p_parameter->name = ""; + p_parameter->value_type = 'N'; + // Skip initial whitespace + //std::cout << "GcodeParser.try_extract_parameter - Trying to extract a text parameter from " << *p_p_gcode << "\r\n"; + char* p = *p_p_gcode; + bool has_found_parameter = false; + // Ignore Leading Spaces + while (*p == ' ') + { + p++; + } + // extract name, make all caps. + while (*p != '\0' && *p != ';' && *p != ' ') + { + if (!has_found_parameter) + { + has_found_parameter = true; + } + + if (*p >= 'a' && *p <= 'z') + { + p_parameter->name.push_back(*p++ - 32); + } + else + { + p_parameter->name.push_back(*p++); + } + } + // Todo: Handle any otolapse commands require a string parameter + /* + // Ignore spaces after the command name + while (*p == ' ') + { + p++; + } + // Extract the value (we may do this per command in the future). This will output mixed case. + bool has_parameter_value = false; + while (*p != '\0' && *p != ';') + { + if (!has_parameter_value) + { + p_parameter->value_type = 'S'; + has_parameter_value = true; + } + p_parameter->string_value.push_back(*p++); + } + if (has_parameter_value) + { + p_parameter->string_value = utilities::rtrim(p_parameter->string_value); + } + */ + *p_p_gcode = p; + return has_found_parameter; } -bool gcode_parser::try_extract_parameter(char ** p_p_gcode, parsed_command_parameter * parameter) const +bool gcode_parser::try_extract_parameter(char** p_p_gcode, parsed_command_parameter* parameter) const { - //std::cout << "GcodeParser.try_extract_parameter - Trying to extract a parameter from " << *p_p_gcode << "\r\n"; - char * p = *p_p_gcode; - - // Ignore Leading Spaces - while (*p == ' ') - { - p++; - } - - // Deal with case sensitivity - if (*p >= 'a' && *p <= 'z') - parameter->name_ += *p++ - 32; - else if (*p >= 'A' && *p <= 'Z') - parameter->name_ += *p++; - else - return false; - - // Add all values, stop at end of string or when we hit a ';' - if (try_extract_double(&p,&(parameter->double_value_))) - { - parameter->value_type_ = 'F'; - } - else - { - if(try_extract_text_parameter(&p, &(parameter->string_value_))) - { - parameter->value_type_ = 'S'; - } - else - { - return false; - } - } - - *p_p_gcode = p; - return true; + //std::cout << "GcodeParser.try_extract_parameter - Trying to extract a parameter from " << *p_p_gcode << "\r\n"; + char* p = *p_p_gcode; + + // Ignore Leading Spaces + while (*p == ' ') + { + p++; + } + + // Deal with case sensitivity + if (*p >= 'a' && *p <= 'z') + parameter->name = *p++ - 32; + else if (*p >= 'A' && *p <= 'Z') + parameter->name = *p++; + else + return false; + // TODO: See if unsigned long works.... + + // Add all values, stop at end of string or when we hit a ';' + if (try_extract_double(&p, &(parameter->double_value))) + { + parameter->value_type = 'F'; + } + else + { + if (try_extract_text_parameter(&p, &(parameter->string_value))) + { + parameter->value_type = 'S'; + } + else + { + return false; + } + } + + *p_p_gcode = p; + return true; +} +bool gcode_parser::try_extract_t_parameter(char** p_p_gcode, parsed_command_parameter* parameter) +{ + //std::cout << "Trying to extract a T parameter from " << *p_p_gcode << "\r\n"; + char* p = *p_p_gcode; + parameter->name = 'T'; + // Ignore Leading Spaces + while (*p == L' ') + { + p++; + } + + if (*p == L'c' || *p == L'C') + { + //std::cout << "Found C value for T parameter\r\n"; + parameter->string_value = "C"; + parameter->value_type = 'S'; + } + else if (*p == L'x' || *p == L'X') + { + //std::cout << "Found X value for T parameter\r\n"; + parameter->string_value = "X"; + parameter->value_type = 'S'; + } + else if (*p == L'?') + { + //std::cout << "Found ? value for T parameter\r\n"; + parameter->string_value = "?"; + parameter->value_type = 'S'; + } + else + { + //std::cout << "No char t parameter found, looking for unsigned int values.\r\n"; + if (!try_extract_unsigned_long(&p, &(parameter->unsigned_long_value))) + { + std::string message = "GcodeParser.try_extract_t_parameter: Unable to extract parameters from the T command."; + octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::WARNING, message); + //std::cout << "No parameter for the T command.\r\n"; + return false; + } + parameter->value_type = 'U'; + } + return true; } -bool gcode_parser::try_extract_t_parameter(char ** p_p_gcode, parsed_command_parameter * parameter) +bool gcode_parser::try_extract_comment(char** p_p_gcode, std::string* p_comment) { - //std::cout << "Trying to extract a T parameter from " << *p_p_gcode << "\r\n"; - char * p = *p_p_gcode; - parameter->name_ = "T"; - // Ignore Leading Spaces - while (*p == L' ') - { - p++; - } - - if (*p == L'c' || *p == L'C') - { - //std::cout << "Found C value for T parameter\r\n"; - parameter->string_value_ = "C"; - parameter->value_type_ = 'S'; - } - else if (*p == L'x' || *p == L'X') - { - //std::cout << "Found X value for T parameter\r\n"; - parameter->string_value_ = "X"; - parameter->value_type_ = 'S'; - } - else if (*p == L'?') - { - //std::cout << "Found ? value for T parameter\r\n"; - parameter->string_value_ = "?"; - parameter->value_type_ = 'S'; - } - else - { - //std::cout << "No char t parameter found, looking for unsigned int values.\r\n"; - if(!try_extract_unsigned_long(&p,&(parameter->unsigned_long_value_))) - { - std::string message = "GcodeParser.try_extract_t_parameter: Unable to extract parameters from the T command."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::WARNING, message); - //std::cout << "No parameter for the T command.\r\n"; - return false; - } - parameter->value_type_ = 'U'; - } - return true; + // Skip initial whitespace + //std::cout << "GcodeParser.try_extract_parameter - Trying to extract a text parameter from " << *p_p_gcode << "\r\n"; + char* p = *p_p_gcode; + + // Ignore Leading Spaces + while (*p != '\0' && *p != ';') + { + p++; + } + + // Add all values, stop at end of string or when we hit a ';' + while (*p == ';' || *p == ' ') + { + p++; + } + while (*p != '\0') + { + if (*p != '\r' && *p != '\n') + (*p_comment).push_back(*p++); + else + p++; + } + *p_p_gcode = p; + return p_comment->length() != 0; } diff --git a/octoprint_octolapse/data/lib/c/gcode_parser.h b/octoprint_octolapse/data/lib/c/gcode_parser.h index 729f85b1..2da8900b 100644 --- a/octoprint_octolapse/data/lib/c/gcode_parser.h +++ b/octoprint_octolapse/data/lib/c/gcode_parser.h @@ -32,22 +32,25 @@ static const std::string GCODE_WORDS = "GMT"; class gcode_parser { public: - gcode_parser(); - ~gcode_parser(); - bool try_parse_gcode(const char * gcode, parsed_command * command); + gcode_parser(); + ~gcode_parser(); + bool try_parse_gcode(const char* gcode, parsed_command& command); + parsed_command parse_gcode(const char* gcode); private: - gcode_parser(const gcode_parser &source); - // Variables and lookups - std::set text_only_functions_; - std::set parsable_commands_; - // Functions - bool try_extract_double(char ** p_p_gcode, double * p_double) const; - static bool try_extract_gcode_command(char ** p_p_gcode, std::string * p_command); - static bool try_extract_text_parameter(char ** p_p_gcode, std::string * p_parameter); - bool try_extract_parameter(char ** p_p_gcode, parsed_command_parameter * parameter) const; - static bool try_extract_t_parameter(char ** p_p_gcode, parsed_command_parameter * parameter); - static bool try_extract_unsigned_long(char ** p_p_gcode, unsigned long * p_value); - double static ten_pow(size_t n); - + gcode_parser(const gcode_parser& source); + // Variables and lookups + std::set text_only_functions_; + std::set parsable_commands_; + // Functions + bool try_extract_double(char** p_p_gcode, double* p_double) const; + static bool try_extract_gcode_command(char** p_p_gcode, std::string* p_command); + static bool try_extract_text_parameter(char** p_p_gcode, std::string* p_parameter); + bool try_extract_parameter(char** p_p_gcode, parsed_command_parameter* parameter) const; + static bool try_extract_t_parameter(char** p_p_gcode, parsed_command_parameter* parameter); + static bool try_extract_unsigned_long(char** p_p_gcode, unsigned long* p_value); + double static ten_pow(unsigned short n); + bool try_extract_comment(char** p_p_gcode, std::string* p_comment); + static bool try_extract_at_command(char** p_p_gcode, std::string* p_command); + bool try_extract_octolapse_parameter(char** p_p_gcode, parsed_command_parameter* p_parameter); }; #endif diff --git a/octoprint_octolapse/data/lib/c/gcode_position.cpp b/octoprint_octolapse/data/lib/c/gcode_position.cpp index b39067f7..8712e184 100644 --- a/octoprint_octolapse/data/lib/c/gcode_position.cpp +++ b/octoprint_octolapse/data/lib/c/gcode_position.cpp @@ -22,780 +22,1390 @@ #include "gcode_position.h" #include "utilities.h" #include "logging.h" +#include +#include + +gcode_position_args::gcode_position_args(const gcode_position_args& pos_args) +{ + shared_extruder = pos_args.shared_extruder; + autodetect_position = pos_args.autodetect_position; + is_circular_bed = pos_args.is_circular_bed; + home_x = pos_args.home_x; + home_y = pos_args.home_y; + home_z = pos_args.home_z; + home_x_none = pos_args.home_x_none; + home_y_none = pos_args.home_y_none; + home_z_none = pos_args.home_z_none; + + priming_height = pos_args.priming_height; + minimum_layer_height = pos_args.minimum_layer_height; + height_increment = pos_args.height_increment; + g90_influences_extruder = pos_args.g90_influences_extruder; + xyz_axis_default_mode = pos_args.xyz_axis_default_mode; + e_axis_default_mode = pos_args.e_axis_default_mode; + units_default = pos_args.units_default; + is_bound_ = pos_args.is_bound_; + x_min = pos_args.x_min; + x_max = pos_args.x_max; + y_min = pos_args.y_min; + y_max = pos_args.y_max; + z_min = pos_args.z_min; + z_max = pos_args.z_max; + snapshot_x_min = pos_args.snapshot_x_min; + snapshot_x_max = pos_args.snapshot_x_max; + snapshot_y_min = pos_args.snapshot_y_min; + snapshot_y_max = pos_args.snapshot_y_max; + snapshot_z_min = pos_args.snapshot_z_min; + snapshot_z_max = pos_args.snapshot_z_max; + + default_extruder = pos_args.default_extruder; + zero_based_extruder = pos_args.zero_based_extruder; + num_extruders = pos_args.num_extruders; + retraction_lengths = NULL; + z_lift_heights = NULL; + x_firmware_offsets = NULL; + y_firmware_offsets = NULL; + set_num_extruders(pos_args.num_extruders); + // copy extruder specific members + for (int index = 0; index < pos_args.num_extruders; index++) + { + retraction_lengths[index] = pos_args.retraction_lengths[index]; + z_lift_heights[index] = pos_args.z_lift_heights[index]; + if (!pos_args.shared_extruder) + { + x_firmware_offsets[index] = pos_args.x_firmware_offsets[index]; + y_firmware_offsets[index] = pos_args.y_firmware_offsets[index]; + } + else + { + x_firmware_offsets[index] = 0; + y_firmware_offsets[index] = 0; + } + } + std::vector location_detection_commands; // Final list of location detection commands +} + +gcode_position_args& gcode_position_args::operator=(const gcode_position_args& pos_args) +{ + shared_extruder = pos_args.shared_extruder; + autodetect_position = pos_args.autodetect_position; + is_circular_bed = pos_args.is_circular_bed; + home_x = pos_args.home_x; + home_y = pos_args.home_y; + home_z = pos_args.home_z; + home_x_none = pos_args.home_x_none; + home_y_none = pos_args.home_y_none; + home_z_none = pos_args.home_z_none; + + priming_height = pos_args.priming_height; + minimum_layer_height = pos_args.minimum_layer_height; + height_increment = pos_args.height_increment; + g90_influences_extruder = pos_args.g90_influences_extruder; + xyz_axis_default_mode = pos_args.xyz_axis_default_mode; + e_axis_default_mode = pos_args.e_axis_default_mode; + units_default = pos_args.units_default; + is_bound_ = pos_args.is_bound_; + x_min = pos_args.x_min; + x_max = pos_args.x_max; + y_min = pos_args.y_min; + y_max = pos_args.y_max; + z_min = pos_args.z_min; + z_max = pos_args.z_max; + snapshot_x_min = pos_args.snapshot_x_min; + snapshot_x_max = pos_args.snapshot_x_max; + snapshot_y_min = pos_args.snapshot_y_min; + snapshot_y_max = pos_args.snapshot_y_max; + snapshot_z_min = pos_args.snapshot_z_min; + snapshot_z_max = pos_args.snapshot_z_max; + + default_extruder = pos_args.default_extruder; + zero_based_extruder = pos_args.zero_based_extruder; + num_extruders = pos_args.num_extruders; + delete_retraction_lengths(); + delete_x_firmware_offsets(); + delete_y_firmware_offsets(); + delete_z_lift_heights(); + set_num_extruders(pos_args.num_extruders); + // copy extruder specific members + for (int index = 0; index < pos_args.num_extruders; index++) + { + retraction_lengths[index] = pos_args.retraction_lengths[index]; + z_lift_heights[index] = pos_args.z_lift_heights[index]; + if (!pos_args.shared_extruder) + { + x_firmware_offsets[index] = pos_args.x_firmware_offsets[index]; + y_firmware_offsets[index] = pos_args.y_firmware_offsets[index]; + } + else + { + x_firmware_offsets[index] = 0; + y_firmware_offsets[index] = 0; + } + } + std::vector location_detection_commands; // Final list of location detection commands + return *this; +} + +void gcode_position_args::set_num_extruders(int num_extruders_) +{ + delete_retraction_lengths(); + delete_z_lift_heights(); + delete_x_firmware_offsets(); + delete_y_firmware_offsets(); + num_extruders = num_extruders_; + retraction_lengths = new double[num_extruders_]; + z_lift_heights = new double[num_extruders_]; + x_firmware_offsets = new double[num_extruders_]; + y_firmware_offsets = new double[num_extruders_]; + // initialize arrays + for (int index = 0; index < num_extruders; index++) + { + retraction_lengths[index] = 0.0; + z_lift_heights[index] = 0.0; + x_firmware_offsets[index] = 0.0; + y_firmware_offsets[index] = 0.0; + } +} + +void gcode_position_args::delete_retraction_lengths() +{ + if (retraction_lengths != NULL) + { + delete[] retraction_lengths; + retraction_lengths = NULL; + } +} + +void gcode_position_args::delete_z_lift_heights() +{ + if (z_lift_heights != NULL) + { + delete[] z_lift_heights; + z_lift_heights = NULL; + } +} + +void gcode_position_args::delete_x_firmware_offsets() +{ + if (x_firmware_offsets != NULL) + { + delete[] x_firmware_offsets; + x_firmware_offsets = NULL; + } +} + +void gcode_position_args::delete_y_firmware_offsets() +{ + if (y_firmware_offsets != NULL) + { + delete[] y_firmware_offsets; + y_firmware_offsets = NULL; + } +} gcode_position::gcode_position() { - autodetect_position_ = false; - home_x_ = 0; - home_y_ = 0; - home_z_ = 0; - home_x_none_ = true; - home_y_none_ = true; - home_z_none_ = true; - - retraction_length_ = 0; - z_lift_height_ = 0; - priming_height_ = 0; - minimum_layer_height_ = 0; - g90_influences_extruder_ = false; - e_axis_default_mode_ = "absolute"; - xyz_axis_default_mode_ = "absolute"; - units_default_ = "millimeters"; - gcode_functions_ = get_gcode_functions(); - - is_bound_ = false; - snapshot_x_min_ = 0; - snapshot_x_max_ = 0; - snapshot_y_min_ = 0; - snapshot_y_max_ = 0; - snapshot_z_min_ = 0; - snapshot_z_max_ = 0; - - x_min_ = 0; - x_max_ = 0; - y_min_ = 0; - y_max_ = 0; - z_min_ = 0; - z_max_ = 0; - is_circular_bed_ = false; - - p_previous_pos_ = new position(xyz_axis_default_mode_, e_axis_default_mode_, units_default_); - p_current_pos_ = new position(xyz_axis_default_mode_, e_axis_default_mode_, units_default_); - p_undo_pos_ = new position(xyz_axis_default_mode_, e_axis_default_mode_, units_default_); -} - -gcode_position::gcode_position(gcode_position_args* args) -{ - autodetect_position_ = args->autodetect_position; - home_x_ = args->home_x; - home_y_ = args->home_y; - home_z_ = args->home_z; - home_x_none_ = args->home_x_none; - home_y_none_ = args->home_y_none; - home_z_none_ = args->home_z_none; - - retraction_length_ = args->retraction_length; - z_lift_height_ = args->z_lift_height; - priming_height_ = args->priming_height; - minimum_layer_height_ = args->minimum_layer_height; - g90_influences_extruder_ = args->g90_influences_extruder; - e_axis_default_mode_ = args->e_axis_default_mode; - xyz_axis_default_mode_ = args->xyz_axis_default_mode; - units_default_ = args->units_default; - gcode_functions_ = get_gcode_functions(); - - is_bound_ = args->is_bound_; - snapshot_x_min_ = args->snapshot_x_min; - snapshot_x_max_ = args->snapshot_x_max; - snapshot_y_min_ = args->snapshot_y_min; - snapshot_y_max_ = args->snapshot_y_max; - snapshot_z_min_ = args->snapshot_z_min; - snapshot_z_max_ = args->snapshot_z_max; - - x_min_ = args->x_min; - x_max_ = args->x_max; - y_min_ = args->y_min; - y_max_ = args->y_max; - z_min_ = args->z_min; - z_max_ = args->z_max; - - is_circular_bed_ = args->is_circular_bed; - - p_previous_pos_ = new position(xyz_axis_default_mode_, e_axis_default_mode_, units_default_); - p_current_pos_ = new position(xyz_axis_default_mode_, e_axis_default_mode_, units_default_); - p_undo_pos_ = new position(xyz_axis_default_mode_, e_axis_default_mode_, units_default_); - -} - -gcode_position::gcode_position(const gcode_position &source) -{ - // Private copy constructor - you can't copy this class + autodetect_position_ = false; + home_x_ = 0; + home_y_ = 0; + home_z_ = 0; + home_x_none_ = true; + home_y_none_ = true; + home_z_none_ = true; + retraction_lengths_ = NULL; + z_lift_heights_ = NULL; + shared_extruder_ = false; + set_num_extruders(0); + zero_based_extruder_ = true; + priming_height_ = 0; + minimum_layer_height_ = 0; + height_increment_ = 0; + g90_influences_extruder_ = false; + e_axis_default_mode_ = "absolute"; + xyz_axis_default_mode_ = "absolute"; + units_default_ = "millimeters"; + gcode_functions_ = get_gcode_functions(); + + is_bound_ = false; + snapshot_x_min_ = 0; + snapshot_x_max_ = 0; + snapshot_y_min_ = 0; + snapshot_y_max_ = 0; + snapshot_z_min_ = 0; + snapshot_z_max_ = 0; + + x_min_ = 0; + x_max_ = 0; + y_min_ = 0; + y_max_ = 0; + z_min_ = 0; + z_max_ = 0; + is_circular_bed_ = false; + + cur_pos_ = 0; + + for (int index = 0; index < NUM_POSITIONS; index ++) + { + position initial_pos(num_extruders_); + initial_pos.set_xyz_axis_mode(xyz_axis_default_mode_); + initial_pos.set_e_axis_mode(e_axis_default_mode_); + initial_pos.set_units_default(units_default_); + add_position(initial_pos); + } +} + + +gcode_position::gcode_position(gcode_position_args args) +{ + autodetect_position_ = args.autodetect_position; + home_x_ = args.home_x; + home_y_ = args.home_y; + home_z_ = args.home_z; + home_x_none_ = args.home_x_none; + home_y_none_ = args.home_y_none; + home_z_none_ = args.home_z_none; + retraction_lengths_ = NULL; + z_lift_heights_ = NULL; + // Configure Extruders + shared_extruder_ = args.shared_extruder; + set_num_extruders(args.num_extruders); + zero_based_extruder_ = args.zero_based_extruder; + // Set the current extruder to the default extruder (0 based) + int current_extruder = args.default_extruder; + // make sure our current extruder is between 0 and num_extruders - 1 + if (current_extruder < 0) + { + current_extruder = 0; + } + else if (current_extruder > args.num_extruders - 1) + { + current_extruder = args.num_extruders - 1; + } + + // copy the retraction lengths array + for (int index = 0; index < args.num_extruders; index++) + { + retraction_lengths_[index] = args.retraction_lengths[index]; + } + // Copy the z_lift_heights array from the arguments + for (int index = 0; index < args.num_extruders; index++) + { + z_lift_heights_[index] = args.z_lift_heights[index]; + } + // Copy the firmware offsets + for (int index = 0; index < args.num_extruders; index++) + { + retraction_lengths_[index] = args.retraction_lengths[index]; + } + + priming_height_ = args.priming_height; + minimum_layer_height_ = args.minimum_layer_height; + height_increment_ = args.height_increment; + g90_influences_extruder_ = args.g90_influences_extruder; + e_axis_default_mode_ = args.e_axis_default_mode; + xyz_axis_default_mode_ = args.xyz_axis_default_mode; + units_default_ = args.units_default; + gcode_functions_ = get_gcode_functions(); + + is_bound_ = args.is_bound_; + snapshot_x_min_ = args.snapshot_x_min; + snapshot_x_max_ = args.snapshot_x_max; + snapshot_y_min_ = args.snapshot_y_min; + snapshot_y_max_ = args.snapshot_y_max; + snapshot_z_min_ = args.snapshot_z_min; + snapshot_z_max_ = args.snapshot_z_max; + + x_min_ = args.x_min; + x_max_ = args.x_max; + y_min_ = args.y_min; + y_max_ = args.y_max; + z_min_ = args.z_min; + z_max_ = args.z_max; + + is_circular_bed_ = args.is_circular_bed; + + cur_pos_ = -1; + num_extruders_ = args.num_extruders; + + // Configure the initial position + position initial_pos(num_extruders_); + initial_pos.set_xyz_axis_mode(xyz_axis_default_mode_); + initial_pos.set_e_axis_mode(e_axis_default_mode_); + initial_pos.set_units_default(units_default_); + initial_pos.current_tool = current_extruder; + for (int index = 0; index < args.num_extruders; index++) + { + initial_pos.p_extruders[index].x_firmware_offset = args.x_firmware_offsets[index]; + initial_pos.p_extruders[index].y_firmware_offset = args.y_firmware_offsets[index]; + } + + for (int index = 0; index < NUM_POSITIONS; index++) + { + add_position(initial_pos); + } +} + +gcode_position::gcode_position(const gcode_position& source) +{ + // Private copy constructor - you can't copy this class } gcode_position::~gcode_position() { - if (p_previous_pos_ != NULL) - { - delete p_previous_pos_; - p_previous_pos_ = NULL; - } - if (p_current_pos_ != NULL) - { - delete p_current_pos_; - p_current_pos_ = NULL; - } - if (p_undo_pos_ != NULL) - { - delete p_undo_pos_; - p_undo_pos_ = NULL; - } -} - -position * gcode_position::get_current_position() -{ - return p_current_pos_; -} - -position * gcode_position::get_previous_position() -{ - return p_previous_pos_; -} - -void gcode_position::update(parsed_command *command, const int file_line_number, const int gcode_number) -{ - if (command->cmd_.empty()) - return; - // Move the current position to the previous and the previous to the undo position - // then copy previous to current - - position * old_undo_pos = p_undo_pos_; - p_undo_pos_ = p_previous_pos_; - p_previous_pos_ = p_current_pos_; - p_current_pos_ = old_undo_pos; - // Copy the previous position to the current, but don't copy the parsed command - position::copy(p_previous_pos_, command, p_current_pos_); - //position::copy(*p_previous_pos_, p_current_pos_); - p_current_pos_->reset_state(); - - // add our parsed command to the current position - /*if (p_current_pos_->p_command != NULL) - { - delete p_current_pos_->p_command; - p_current_pos_->p_command = NULL; - } - p_current_pos_->p_command = new parsed_command(*command); - */ - p_current_pos_->file_line_number_ = file_line_number; - p_current_pos_->gcode_number_ = gcode_number; - // Does our function exist in our functions map? - gcode_functions_iterator_ = gcode_functions_.find(command->cmd_); - - if (gcode_functions_iterator_ != gcode_functions_.end()) - { - p_current_pos_->gcode_ignored_ = false; - // Execute the function to process this gcode - const pos_function_type func = gcode_functions_iterator_->second; - (this->*func)(p_current_pos_, command); - // calculate z and e relative distances - p_current_pos_->e_relative_ = (p_current_pos_->e_ - p_previous_pos_->e_); - p_current_pos_->z_relative_ = (p_current_pos_->z_ - p_previous_pos_->z_); - // Have the XYZ positions changed after processing a command ? - - p_current_pos_->has_xy_position_changed_ = ( - !utilities::is_equal(p_current_pos_->x_, p_previous_pos_->x_) || - !utilities::is_equal(p_current_pos_->y_, p_previous_pos_->y_) - ); - p_current_pos_->has_position_changed_ = ( - p_current_pos_->has_xy_position_changed_ || - !utilities::is_equal(p_current_pos_->z_, p_previous_pos_->z_) || - !utilities::is_zero(p_current_pos_->e_relative_) || - p_current_pos_->x_null_ != p_previous_pos_->x_null_ || - p_current_pos_->y_null_ != p_previous_pos_->y_null_ || - p_current_pos_->z_null_ != p_previous_pos_->z_null_); - - // see if our position is homed - if (!p_current_pos_->has_homed_position_) - { - p_current_pos_->has_homed_position_ = ( - p_current_pos_->x_homed_ && - p_current_pos_->y_homed_ && - p_current_pos_->z_homed_ && - p_current_pos_->is_metric_ && - !p_current_pos_->is_metric_null_ && - !p_current_pos_->x_null_ && - !p_current_pos_->y_null_ && - !p_current_pos_->z_null_ && - !p_current_pos_->is_relative_null_ && - !p_current_pos_->is_extruder_relative_null_); - } - } - - if (p_current_pos_->has_position_changed_) - { - p_current_pos_->extrusion_length_total_ += p_current_pos_->e_relative_; - - if (utilities::greater_than(p_current_pos_->e_relative_, 0) && p_previous_pos_->is_extruding_ && !p_previous_pos_->is_extruding_start_) - { - // A little shortcut if we know we were extruding (not starting extruding) in the previous command - // This lets us skip a lot of the calculations for the extruder, including the state calculation - p_current_pos_->extrusion_length_ = p_current_pos_->e_relative_; - } - else - { - - // Update retraction_length and extrusion_length - p_current_pos_->retraction_length_ = p_current_pos_->retraction_length_ - p_current_pos_->e_relative_; - if (utilities::less_than_or_equal(p_current_pos_->retraction_length_, 0)) - { - // we can use the negative retraction length to calculate our extrusion length! - p_current_pos_->extrusion_length_ = -1.0 * p_current_pos_->retraction_length_; - // set the retraction length to 0 since we are extruding - p_current_pos_->retraction_length_ = 0; - } - else - p_current_pos_->extrusion_length_ = 0; - - // calculate deretraction length - if (utilities::greater_than(p_previous_pos_->retraction_length_, p_current_pos_->retraction_length_)) - { - p_current_pos_->deretraction_length_ = p_previous_pos_->retraction_length_ - p_current_pos_->retraction_length_; - } - else - p_current_pos_->deretraction_length_ = 0; - - // *************Calculate extruder state************* - // rounding should all be done by now - p_current_pos_->is_extruding_start_ = utilities::greater_than(p_current_pos_->extrusion_length_, 0) && !p_previous_pos_->is_extruding_; - p_current_pos_->is_extruding_ = utilities::greater_than(p_current_pos_->extrusion_length_, 0); - p_current_pos_->is_primed_ = utilities::is_zero(p_current_pos_->extrusion_length_) && utilities::is_zero(p_current_pos_->retraction_length_); - p_current_pos_->is_retracting_start_ = !p_previous_pos_->is_retracting_ && utilities::greater_than(p_current_pos_->retraction_length_, 0); - p_current_pos_->is_retracting_ = utilities::greater_than(p_current_pos_->retraction_length_, p_previous_pos_->retraction_length_); - p_current_pos_->is_partially_retracted_ = utilities::greater_than(p_current_pos_->retraction_length_, 0) && utilities::less_than(p_current_pos_->retraction_length_, retraction_length_); - p_current_pos_->is_retracted_ = utilities::greater_than_or_equal(p_current_pos_->retraction_length_, retraction_length_); - p_current_pos_->is_deretracting_start_ = utilities::greater_than(p_current_pos_->deretraction_length_, 0) && !p_previous_pos_->is_deretracting_; - p_current_pos_->is_deretracting_ = utilities::greater_than(p_current_pos_->deretraction_length_, p_previous_pos_->deretraction_length_); - p_current_pos_->is_deretracted_ = utilities::greater_than(p_previous_pos_->retraction_length_, 0) && utilities::is_zero(p_current_pos_->retraction_length_); - // *************End Calculate extruder state************* - } - // calculate last_extrusion_height and height - // If we are extruding on a higher level, or if retract is enabled and the nozzle is primed - // adjust the last extrusion height - if (!utilities::is_equal(p_current_pos_->z_, p_current_pos_->last_extrusion_height_)) - { - if (!p_current_pos_->z_null_) - { - if (p_current_pos_->is_extruding_) - { - p_current_pos_->last_extrusion_height_ = p_current_pos_->z_; - p_current_pos_->last_extrusion_height_null_ = false; - // Is Primed - if (!p_current_pos_->is_printer_primed_) - { - // We haven't primed yet, check to see if we have priming height restrictions - if (utilities::greater_than(priming_height_, 0)) - { - // if a priming height is configured, see if we've extruded below the height - if (utilities::less_than(p_current_pos_->last_extrusion_height_, priming_height_)) - p_current_pos_->is_printer_primed_ = true; - } - else - // if we have no priming height set, just set is_printer_primed = true. - p_current_pos_->is_printer_primed_ = true; - } - - if (p_current_pos_->is_printer_primed_) - { - // Calculate current height - if (utilities::greater_than_or_equal(p_current_pos_->z_, p_previous_pos_->height_ + minimum_layer_height_)) - { - p_current_pos_->height_ = p_current_pos_->z_; - p_current_pos_->is_layer_change_ = true; - p_current_pos_->layer_++; - } - } - } - - // calculate is_zhop - if (p_current_pos_->is_extruding_ || p_current_pos_->z_null_ || p_current_pos_->last_extrusion_height_null_) - p_current_pos_->is_zhop_ = false; - else - p_current_pos_->is_zhop_ = utilities::greater_than_or_equal(p_current_pos_->z_ - p_current_pos_->last_extrusion_height_, z_lift_height_); - } - - } - - // Calcluate position restructions - // TODO: INCLUDE POSITION RESTRICTION CALCULATIONS! - // Set is_in_bounds_ to false if we're not in bounds, it will be true at this point - if (is_bound_) - { - bool is_in_bounds = true; - if (!is_circular_bed_) - { - is_in_bounds = !( - p_current_pos_->x_ < snapshot_x_min_ || - p_current_pos_->x_ > snapshot_x_max_ || - p_current_pos_->y_ < snapshot_y_min_ || - p_current_pos_->y_ > snapshot_y_max_ || - p_current_pos_->z_ < snapshot_z_min_ || - p_current_pos_->z_ > snapshot_z_max_ - ); - - } - else - { - double r; - r = snapshot_x_max_; // good stand in for radius - const double dist = sqrt(p_current_pos_->x_*p_current_pos_->x_ + p_current_pos_->y_*p_current_pos_->y_); - is_in_bounds = utilities::less_than_or_equal(dist, r); - - } - p_current_pos_->is_in_bounds_ = is_in_bounds; - } - - } + delete_retraction_lengths_(); + delete_z_lift_heights_(); +} + +void gcode_position::set_num_extruders(int num_extruders) +{ + delete_retraction_lengths_(); + delete_z_lift_heights_(); + if (shared_extruder_) + { + num_extruders_ = 1; + } + else + { + num_extruders_ = num_extruders; + } + retraction_lengths_ = new double[num_extruders]; + z_lift_heights_ = new double[num_extruders]; +} + +void gcode_position::delete_retraction_lengths_() +{ + if (retraction_lengths_ != NULL) + { + delete[] retraction_lengths_; + retraction_lengths_ = NULL; + } +} + +void gcode_position::delete_z_lift_heights_() +{ + if (z_lift_heights_ != NULL) + { + delete[] z_lift_heights_; + z_lift_heights_ = NULL; + } +} + +void gcode_position::add_position(position& pos) +{ + cur_pos_ = (++cur_pos_) % NUM_POSITIONS; + positions_[cur_pos_] = pos; +} + +void gcode_position::add_position(parsed_command& cmd) +{ + const int prev_pos = cur_pos_; + cur_pos_ = (++cur_pos_) % NUM_POSITIONS; + positions_[cur_pos_] = positions_[prev_pos]; + positions_[cur_pos_].reset_state(); + positions_[cur_pos_].command = cmd; + positions_[cur_pos_].is_empty = false; +} + +position gcode_position::get_current_position() const +{ + return positions_[cur_pos_]; +} + +position gcode_position::get_previous_position() const +{ + return positions_[(cur_pos_ - 1 + NUM_POSITIONS) % NUM_POSITIONS]; +} + +position* gcode_position::get_current_position_ptr() +{ + return &positions_[cur_pos_]; +} + +position* gcode_position::get_previous_position_ptr() +{ + return &positions_[(cur_pos_ - 1 + NUM_POSITIONS) % NUM_POSITIONS]; +} + +void gcode_position::update(parsed_command& command, const long file_line_number, const long gcode_number, + const long file_position) +{ + if (command.is_empty) + { + // process any comment sections + comment_processor_.update(command.comment); + return; + } + + add_position(command); + position* p_current_pos = get_current_position_ptr(); + position* p_previous_pos = get_previous_position_ptr(); + p_current_pos->file_line_number = file_line_number; + p_current_pos->gcode_number = gcode_number; + p_current_pos->file_position = file_position; + comment_processor_.update(*p_current_pos); + + if (!command.is_known_command) + return; + + // Does our function exist in our functions map? + gcode_functions_iterator_ = gcode_functions_.find(command.command); + + if (gcode_functions_iterator_ != gcode_functions_.end()) + { + p_current_pos->gcode_ignored = false; + // Execute the function to process this gcode + const pos_function_type func = gcode_functions_iterator_->second; + (this->*func)(p_current_pos, command); + // calculate z and e relative distances + p_current_pos->get_current_extruder().e_relative = (p_current_pos->get_current_extruder().e - p_previous_pos + ->get_extruder( + p_current_pos-> + current_tool).e); + p_current_pos->z_relative = (p_current_pos->z - p_previous_pos->z); + // Have the XYZ positions changed after processing a command ? + + p_current_pos->has_xy_position_changed = ( + !utilities::is_equal(p_current_pos->x, p_previous_pos->x) || + !utilities::is_equal(p_current_pos->y, p_previous_pos->y) + ); + p_current_pos->has_position_changed = ( + p_current_pos->has_xy_position_changed || + !utilities::is_equal(p_current_pos->z, p_previous_pos->z) || + !utilities::is_zero(p_current_pos->get_current_extruder().e_relative) || + p_current_pos->x_null != p_previous_pos->x_null || + p_current_pos->y_null != p_previous_pos->y_null || + p_current_pos->z_null != p_previous_pos->z_null); + + // see if our position is homed + if (!p_current_pos->has_definite_position) + { + p_current_pos->has_definite_position = ( + //p_current_pos->x_homed_ && + //p_current_pos->y_homed_ && + //p_current_pos->z_homed_ && + p_current_pos->is_metric && + !p_current_pos->is_metric_null && + !p_current_pos->x_null && + !p_current_pos->y_null && + !p_current_pos->z_null && + !p_current_pos->is_relative_null && + !p_current_pos->is_extruder_relative_null); + } + } + + if (p_current_pos->has_position_changed) + { + p_current_pos->get_current_extruder().extrusion_length_total += p_current_pos->get_current_extruder().e_relative; + + if ( + utilities::greater_than(p_current_pos->get_current_extruder().e_relative, 0) && + p_previous_pos->current_tool == p_current_pos->current_tool && + // notice we can use the previous position's current extruder since we've made sure they are using the same tool + p_previous_pos->get_current_extruder().is_extruding && + !p_previous_pos->get_current_extruder().is_extruding_start) + { + // A little shortcut if we know we were extruding (not starting extruding) in the previous command + // This lets us skip a lot of the calculations for the extruder, including the state calculation + p_current_pos->get_current_extruder().extrusion_length = p_current_pos->get_current_extruder().e_relative; + } + else + { + // Update retraction_length and extrusion_length + p_current_pos->get_current_extruder().retraction_length = p_current_pos->get_current_extruder().retraction_length + - p_current_pos->get_current_extruder().e_relative; + if (utilities::less_than_or_equal(p_current_pos->get_current_extruder().retraction_length, 0)) + { + // we can use the negative retraction length to calculate our extrusion length! + p_current_pos->get_current_extruder().extrusion_length = -1.0 * p_current_pos + ->get_current_extruder().retraction_length; + // set the retraction length to 0 since we are extruding + p_current_pos->get_current_extruder().retraction_length = 0; + } + else + p_current_pos->get_current_extruder().extrusion_length = 0; + + // calculate deretraction length + if (utilities::greater_than(p_previous_pos->get_extruder(p_current_pos->current_tool).retraction_length, + p_current_pos->get_current_extruder().retraction_length)) + { + p_current_pos->get_current_extruder().deretraction_length = p_previous_pos + ->get_extruder(p_current_pos->current_tool). + retraction_length - p_current_pos + ->get_current_extruder(). + retraction_length; + } + else + p_current_pos->get_current_extruder().deretraction_length = 0; + + // *************Calculate extruder state************* + // rounding should all be done by now + if (p_current_pos->current_tool == p_previous_pos->current_tool) + { + // On a toolchange some flags are not possible, so don't change them. + // these flags include like is_extruding, is_extruding_start, is_retracting_start, is_retracting, is_deretracting_start and is_deretracting + // Note that it's ok to use the previous pos current extruder since we've made sure the current tool is identical + p_current_pos->get_current_extruder().is_extruding_start = utilities::greater_than( + p_current_pos->get_current_extruder().extrusion_length, 0) && !p_previous_pos + ->get_current_extruder().is_extruding; + p_current_pos->get_current_extruder().is_extruding = utilities::greater_than( + p_current_pos->get_current_extruder().extrusion_length, 0); + p_current_pos->get_current_extruder().is_retracting_start = !p_previous_pos + ->get_current_extruder().is_retracting && utilities + ::greater_than(p_current_pos->get_current_extruder().retraction_length, 0); + p_current_pos->get_current_extruder().is_retracting = utilities::greater_than( + p_current_pos->get_current_extruder().retraction_length, + p_previous_pos->get_current_extruder().retraction_length); + p_current_pos->get_current_extruder().is_deretracting = utilities::greater_than( + p_current_pos->get_current_extruder().deretraction_length, + p_previous_pos->get_current_extruder().deretraction_length); + p_current_pos->get_current_extruder().is_deretracting_start = utilities::greater_than( + p_current_pos->get_current_extruder().deretraction_length, 0) && !p_previous_pos + ->get_current_extruder().is_deretracting; + } + else + { + p_current_pos->get_current_extruder().is_extruding_start = false; + p_current_pos->get_current_extruder().is_extruding = false; + p_current_pos->get_current_extruder().is_retracting_start = false; + p_current_pos->get_current_extruder().is_retracting = false; + p_current_pos->get_current_extruder().is_deretracting = false; + p_current_pos->get_current_extruder().is_deretracting_start = false; + } + p_current_pos->get_current_extruder().is_primed = utilities:: + is_zero(p_current_pos->get_current_extruder().extrusion_length) && utilities::is_zero( + p_current_pos->get_current_extruder().retraction_length); + p_current_pos->get_current_extruder().is_partially_retracted = utilities:: + greater_than(p_current_pos->get_current_extruder().retraction_length, 0) && utilities::less_than( + p_current_pos->get_current_extruder().retraction_length, retraction_lengths_[p_current_pos->current_tool]); + p_current_pos->get_current_extruder().is_retracted = utilities::greater_than_or_equal( + p_current_pos->get_current_extruder().retraction_length, retraction_lengths_[p_current_pos->current_tool]); + p_current_pos->get_current_extruder().is_deretracted = utilities::greater_than( + p_previous_pos->get_extruder(p_current_pos->current_tool).retraction_length, 0) && utilities::is_zero( + p_current_pos->get_current_extruder().retraction_length); + // *************End Calculate extruder state************* + } + + // Calcluate position restructions + // TODO: INCLUDE POSITION RESTRICTION CALCULATIONS! + // Set is_in_bounds_ to false if we're not in bounds, it will be true at this point + bool is_in_bounds = true; + if (is_bound_) + { + if (!is_circular_bed_) + { + is_in_bounds = !( + utilities::less_than(p_current_pos->x, snapshot_x_min_) || + utilities::greater_than(p_current_pos->x, snapshot_x_max_) || + utilities::less_than(p_current_pos->y, snapshot_y_min_) || + utilities::greater_than(p_current_pos->y, snapshot_y_max_) || + utilities::less_than(p_current_pos->z, snapshot_z_min_) || + utilities::greater_than(p_current_pos->z, snapshot_z_max_) + ); + } + else + { + double r; + r = snapshot_x_max_; // good stand in for radius + const double dist = sqrt(p_current_pos->x * p_current_pos->x + p_current_pos->y * p_current_pos->y); + is_in_bounds = utilities::less_than_or_equal(dist, r); + } + p_current_pos->is_in_bounds = is_in_bounds; + } + + // calculate last_extrusion_height and height + // If we are extruding on a higher level, or if retract is enabled and the nozzle is primed + // adjust the last extrusion height + if (utilities::greater_than(p_current_pos->z, p_current_pos->last_extrusion_height)) + { + if (!p_current_pos->z_null) + { + // detect layer changes/ printer priming/last extrusion height and height + // Normally we would only want to use is_extruding, but we can also use is_deretracted if the layer is greater than 0 + if (p_current_pos->get_current_extruder().is_extruding || (p_current_pos->layer > 0 && p_current_pos + ->get_current_extruder(). + is_deretracted)) + { + // Is Primed + if (!p_current_pos->is_printer_primed) + { + // We haven't primed yet, check to see if we have priming height restrictions + if (utilities::greater_than(priming_height_, 0)) + { + // if a priming height is configured, see if we've extruded below the height + if (utilities::less_than(p_current_pos->z, priming_height_)) + p_current_pos->is_printer_primed = true; + } + else + // if we have no priming height set, just set is_printer_primed = true. + p_current_pos->is_printer_primed = true; + } + + if (p_current_pos->is_printer_primed && is_in_bounds) + { + // Update the last extrusion height + p_current_pos->last_extrusion_height = p_current_pos->z; + p_current_pos->last_extrusion_height_null = false; + + // Calculate current height + if (utilities::greater_than_or_equal(p_current_pos->z, p_previous_pos->height + minimum_layer_height_)) + { + p_current_pos->height = p_current_pos->z; + p_current_pos->is_layer_change = true; + p_current_pos->layer++; + if (height_increment_ != 0) + { + const double increment_double = p_current_pos->height / height_increment_; + unsigned const int increment = utilities::round_up_to_int(increment_double); + if (increment > p_current_pos->height_increment && increment > 1) + { + p_current_pos->height_increment = increment; + p_current_pos->is_height_increment_change = true; + p_current_pos->height_increment_change_count++; + } + } + } + } + } + + // calculate is_zhop + if (p_current_pos->get_current_extruder().is_extruding || p_current_pos->z_null || p_current_pos-> + last_extrusion_height_null) + p_current_pos->is_zhop = false; + else + p_current_pos->is_zhop = utilities::greater_than_or_equal( + p_current_pos->z - p_current_pos->last_extrusion_height, z_lift_heights_[p_current_pos->current_tool]); + } + } + } } void gcode_position::undo_update() { - position* temp = p_current_pos_; - p_current_pos_ = p_previous_pos_; - p_previous_pos_ = p_undo_pos_; - p_undo_pos_ = temp; + cur_pos_ = (cur_pos_ - 1 + NUM_POSITIONS) % NUM_POSITIONS; } // Private Members std::map gcode_position::get_gcode_functions() { - std::map newMap; - newMap.insert(std::make_pair("G0", &gcode_position::process_g0_g1)); - newMap.insert(std::make_pair("G1", &gcode_position::process_g0_g1)); - newMap.insert(std::make_pair("G2", &gcode_position::process_g2)); - newMap.insert(std::make_pair("G3", &gcode_position::process_g3)); - newMap.insert(std::make_pair("G10", &gcode_position::process_g10)); - newMap.insert(std::make_pair("G11", &gcode_position::process_g11)); - newMap.insert(std::make_pair("G20", &gcode_position::process_g20)); - newMap.insert(std::make_pair("G21", &gcode_position::process_g21)); - newMap.insert(std::make_pair("G28", &gcode_position::process_g28)); - newMap.insert(std::make_pair("G90", &gcode_position::process_g90)); - newMap.insert(std::make_pair("G91", &gcode_position::process_g91)); - newMap.insert(std::make_pair("G92", &gcode_position::process_g92)); - newMap.insert(std::make_pair("M82", &gcode_position::process_m82)); - newMap.insert(std::make_pair("M83", &gcode_position::process_m83)); - newMap.insert(std::make_pair("M207", &gcode_position::process_m207)); - newMap.insert(std::make_pair("M208", &gcode_position::process_m208)); - return newMap; -} - -void gcode_position::update_position(position* pos, double x, bool update_x, double y, bool update_y, double z, bool update_z, double e, bool update_e, double f, bool update_f, bool force, bool is_g1_g0) -{ - if (is_g1_g0) - { - if (!update_e) - { - if (update_z) - { - pos->is_xyz_travel_ = (update_x || update_y); - } - else - { - pos->is_xy_travel_ = (update_x || update_y); - } - } - - } - if (update_f) - { - pos->f_ = f; - pos->f_null_ = false; - } - - if (force) - { - if (update_x) - { - pos->x_ = x + pos->x_offset_; - pos->x_null_ = false; - } - if (update_y) - { - pos->y_ = y + pos->y_offset_; - pos->y_null_ = false; - } - if (update_z) - { - pos->z_ = z + pos->z_offset_; - pos->z_null_ = false; - } - // note that e cannot be null and starts at 0 - if (update_e) - pos->e_ = e + pos->e_offset_; - return; - } - - if (!pos->is_relative_null_) - { - if (pos->is_relative_) { - if (update_x) - { - if (!pos->x_null_) - pos->x_ = x + pos->x_; - else - { - octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, "GcodePosition.update_position: Cannot update X because the XYZ axis mode is relative and X is null."); - } - } - if (update_y) - { - if (!pos->y_null_) - pos->y_ = y + pos->y_; - else - { - octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, "GcodePosition.update_position: Cannot update Y because the XYZ axis mode is relative and Y is null."); - } - } - if (update_z) - { - if (!pos->z_null_) - pos->z_ = z + pos->z_; - else - { - octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, "GcodePosition.update_position: Cannot update Z because the XYZ axis mode is relative and Z is null."); - } - } - } - else - { - if (update_x) - { - pos->x_ = x + pos->x_offset_; - pos->x_null_ = false; - } - if (update_y) - { - pos->y_ = y + pos->y_offset_; - pos->y_null_ = false; - } - if (update_z) - { - pos->z_ = z + pos->z_offset_; - pos->z_null_ = false; - } - } - } - else - { - std::string message = "The XYZ axis mode is not set, cannot update position."; - octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, message); - } - - if (update_e) - { - if (!pos->is_extruder_relative_null_) - { - if (pos->is_extruder_relative_) - { - pos->e_ = e + pos->e_; - } - else - { - pos->e_ = e + pos->e_offset_; - } - } - else - { - std::string message = "The E axis mode is not set, cannot update position."; - octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, message); - } - } - -} - -void gcode_position::process_g0_g1(position* posPtr, parsed_command* parsedCommandPtr) -{ - bool update_x = false; - bool update_y = false; - bool update_z = false; - bool update_e = false; - bool update_f = false; - double x = 0; - double y = 0; - double z = 0; - double e = 0; - double f = 0; - - for (unsigned int index = 0; index < parsedCommandPtr->parameters_.size(); index++) - { - parsed_command_parameter * p_cur_param = parsedCommandPtr->parameters_[index]; - std::string cmdName = p_cur_param->name_; - if (cmdName == "X") - { - update_x = true; - x = p_cur_param->double_value_; - } - else if (cmdName == "Y") - { - update_y = true; - y = p_cur_param->double_value_; - } - else if (cmdName == "E") - { - update_e = true; - e = p_cur_param->double_value_; - } - else if (cmdName == "Z") - { - update_z = true; - z = p_cur_param->double_value_; - } - else if (cmdName == "F") - { - update_f = true; - f = p_cur_param->double_value_; - } - } - update_position(posPtr, x, update_x, y, update_y, z, update_z, e, update_e, f, update_f, false, true); -} - -void gcode_position::process_g2(position* posPtr, parsed_command* parsedCommandPtr) -{ - // ToDo: Fix G2 -} - -void gcode_position::process_g3(position* posPtr, parsed_command* parsedCommandPtr) -{ - // Todo: Fix G3 -} - -void gcode_position::process_g10(position* posPtr, parsed_command* parsedCommandPtr) -{ - // Todo: Fix G10 -} - -void gcode_position::process_g11(position* posPtr, parsed_command* parsedCommandPtr) -{ - // Todo: Fix G11 -} - -void gcode_position::process_g20(position* posPtr, parsed_command* parsedCommandPtr) -{ - -} - -void gcode_position::process_g21(position* posPtr, parsed_command* parsedCommandPtr) -{ - -} - -void gcode_position::process_g28(position* p_position, parsed_command* p_parsed_command) -{ - bool has_x = false; - bool has_y = false; - bool has_z = false; - bool set_x_home = false; - bool set_y_home = false; - bool set_z_home = false; - - for (unsigned int index = 0; index < p_parsed_command->parameters_.size(); index++) - { - parsed_command_parameter* p_cur_param = p_parsed_command->parameters_[index]; - if (p_cur_param->name_ == "X") - has_x = true; - else if (p_cur_param->name_ == "Y") - has_y = true; - else if (p_cur_param->name_ == "Z") - has_z = true; - } - if (has_x) - { - p_position->x_homed_ = true; - set_x_home = true; - } - if (has_y) - { - p_position->y_homed_ = true; - set_y_home = true; - } - if (has_z) - { - p_position->z_homed_ = true; - set_z_home = true; - } - if (!has_x && !has_y && !has_z) - { - p_position->x_homed_ = true; - p_position->y_homed_ = true; - p_position->z_homed_ = true; - set_x_home = true; - set_y_home = true; - set_z_home = true; - } - - if (set_x_home && !home_x_none_) - { - p_position->x_ = home_x_; - p_position->x_null_ = false; - } - // todo: set error flag on else - if (set_y_home && !home_y_none_) - { - p_position->y_ = home_y_; - p_position->y_null_ = false; - } - // todo: set error flag on else - if (set_z_home && !home_z_none_) - { - p_position->z_ = home_z_; - p_position->z_null_ = false; - } - // todo: set error flag on else -} - -void gcode_position::process_g90(position* posPtr, parsed_command* parsedCommandPtr) -{ - // Set xyz to absolute mode - if (posPtr->is_relative_null_) - posPtr->is_relative_null_ = false; - - posPtr->is_relative_ = false; - - if (g90_influences_extruder_) - { - // If g90/g91 influences the extruder, set the extruder to absolute mode too - if (posPtr->is_extruder_relative_null_) - posPtr->is_extruder_relative_null_ = false; - - posPtr->is_extruder_relative_ = false; - } - -} - -void gcode_position::process_g91(position* posPtr, parsed_command* parsedCommandPtr) -{ - // Set XYZ axis to relative mode - if (posPtr->is_relative_null_) - posPtr->is_relative_null_ = false; - - posPtr->is_relative_ = true; - - if (g90_influences_extruder_) - { - // If g90/g91 influences the extruder, set the extruder to relative mode too - if (posPtr->is_extruder_relative_null_) - posPtr->is_extruder_relative_null_ = false; - - posPtr->is_extruder_relative_ = true; - } -} - -void gcode_position::process_g92(position* p_position, parsed_command* p_parsed_command) -{ - // Set position offset - bool update_x = false; - bool update_y = false; - bool update_z = false; - bool update_e = false; - bool o_exists = false; - double x = 0; - double y = 0; - double z = 0; - double e = 0; - for (unsigned int index = 0; index < p_parsed_command->parameters_.size(); index++) - { - parsed_command_parameter * p_cur_param = p_parsed_command->parameters_[index]; - std::string cmdName = p_cur_param->name_; - if (cmdName == "X") - { - update_x = true; - x = p_cur_param->double_value_; - } - else if (cmdName == "Y") - { - update_y = true; - y = p_cur_param->double_value_; - } - else if (cmdName == "E") - { - update_e = true; - e = p_cur_param->double_value_; - } - else if (cmdName == "Z") - { - update_z = true; - z = p_cur_param->double_value_; - } - else if (cmdName == "O") - { - o_exists = true; - } - } - - if (o_exists) - { - // Our fake O parameter exists, set axis to homed! - // This is a workaround to allow folks to use octolapse without homing (for shame, lol!) - p_position->x_homed_ = true; - p_position->y_homed_ = true; - p_position->z_homed_ = true; - } - - if (!o_exists && !update_x && !update_y && !update_z && !update_e) - { - if (!p_position->x_null_) - p_position->x_offset_ = p_position->x_; - if (!p_position->y_null_) - p_position->y_offset_ = p_position->y_; - if (!p_position->z_null_) - p_position->z_offset_ = p_position->z_; - // Todo: Does this reset E too? Figure that $#$$ out Formerlurker! - p_position->e_offset_ = p_position->e_; - } - else - { - if (update_x) - { - if (!p_position->x_null_ && p_position->x_homed_) - p_position->x_offset_ = p_position->x_ - x; - else - { - p_position->x_ = x; - p_position->x_offset_ = 0; - p_position->x_null_ = false; - } - } - if (update_y) - { - if (!p_position->y_null_ && p_position->y_homed_) - p_position->y_offset_ = p_position->y_ - y; - else - { - p_position->y_ = y; - p_position->y_offset_ = 0; - p_position->y_null_ = false; - } - } - if (update_z) - { - if (!p_position->z_null_ && p_position->z_homed_) - p_position->z_offset_ = p_position->z_ - z; - else - { - p_position->z_ = z; - p_position->z_offset_ = 0; - p_position->z_null_ = false; - } - } - if (update_e) - { - p_position->e_offset_ = p_position->e_ - e; - } - } -} - -void gcode_position::process_m82(position* posPtr, parsed_command* parsedCommandPtr) -{ - // Set extrder mode to absolute - if (posPtr->is_extruder_relative_null_) - posPtr->is_extruder_relative_null_ = false; - - posPtr->is_extruder_relative_ = false; -} - -void gcode_position::process_m83(position* posPtr, parsed_command* parsedCommandPtr) -{ - // Set extrder mode to relative - if (posPtr->is_extruder_relative_null_) - posPtr->is_extruder_relative_null_ = false; - - posPtr->is_extruder_relative_ = true; -} - -void gcode_position::process_m207(position* posPtr, parsed_command* parsedCommandPtr) -{ - // Todo: impemente firmware retract -} - -void gcode_position::process_m208(position* posPtr, parsed_command* parsedCommandPtr) -{ - // Todo: implement firmware retract + std::map newMap; + newMap.insert(std::make_pair("G0", &gcode_position::process_g0_g1)); + newMap.insert(std::make_pair("G1", &gcode_position::process_g0_g1)); + newMap.insert(std::make_pair("G2", &gcode_position::process_g2)); + newMap.insert(std::make_pair("G3", &gcode_position::process_g3)); + newMap.insert(std::make_pair("G10", &gcode_position::process_g10)); + newMap.insert(std::make_pair("G11", &gcode_position::process_g11)); + newMap.insert(std::make_pair("G20", &gcode_position::process_g20)); + newMap.insert(std::make_pair("G21", &gcode_position::process_g21)); + newMap.insert(std::make_pair("G28", &gcode_position::process_g28)); + newMap.insert(std::make_pair("G90", &gcode_position::process_g90)); + newMap.insert(std::make_pair("G91", &gcode_position::process_g91)); + newMap.insert(std::make_pair("G92", &gcode_position::process_g92)); + newMap.insert(std::make_pair("M82", &gcode_position::process_m82)); + newMap.insert(std::make_pair("M83", &gcode_position::process_m83)); + newMap.insert(std::make_pair("M207", &gcode_position::process_m207)); + newMap.insert(std::make_pair("M208", &gcode_position::process_m208)); + newMap.insert(std::make_pair("M218", &gcode_position::process_m218)); + newMap.insert(std::make_pair("M563", &gcode_position::process_m563)); + newMap.insert(std::make_pair("T", &gcode_position::process_t)); + return newMap; +} + +void gcode_position::update_position( + position* pos, + const double x, + const bool update_x, + const double y, + const bool update_y, + const double z, + const bool update_z, + const double e, + const bool update_e, + const double f, + const bool update_f, + const bool force, + const bool is_g1_g0) const +{ + if (is_g1_g0) + { + if (!update_e) + { + if (update_z) + { + pos->is_xyz_travel = (update_x || update_y); + } + else + { + pos->is_xy_travel = (update_x || update_y); + } + } + } + if (update_f) + { + pos->f = f; + pos->f_null = false; + } + + if (force) + { + if (update_x) + { + pos->x = x + pos->x_offset - pos->x_firmware_offset; + pos->x_null = false; + } + if (update_y) + { + pos->y = y + pos->y_offset - pos->y_firmware_offset; + pos->y_null = false; + } + if (update_z) + { + pos->z = z + pos->z_offset - pos->z_firmware_offset; + pos->z_null = false; + } + // note that e cannot be null and starts at 0 + if (update_e) + pos->get_current_extruder().e = e + pos->get_current_extruder().e_offset; + return; + } + + if (!pos->is_relative_null) + { + if (pos->is_relative) + { + if (update_x) + { + if (!pos->x_null) + pos->x = x + pos->x; + else + { + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePosition.update_position: Cannot update X because the XYZ axis mode is relative and X is null."); + } + } + if (update_y) + { + if (!pos->y_null) + pos->y = y + pos->y; + else + { + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePosition.update_position: Cannot update Y because the XYZ axis mode is relative and Y is null."); + } + } + if (update_z) + { + if (!pos->z_null) + pos->z = z + pos->z; + else + { + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePosition.update_position: Cannot update Z because the XYZ axis mode is relative and Z is null."); + } + } + } + else + { + if (update_x) + { + pos->x_firmware_offset = pos->get_current_extruder().x_firmware_offset; + pos->x = x + pos->x_offset - pos->x_firmware_offset; + pos->x_null = false; + } + if (update_y) + { + pos->y_firmware_offset = pos->get_current_extruder().y_firmware_offset; + pos->y = y + pos->y_offset - pos->y_firmware_offset; + pos->y_null = false; + } + if (update_z) + { + pos->z_firmware_offset = pos->get_current_extruder().z_firmware_offset; + pos->z = z + pos->z_offset - pos->z_firmware_offset; + pos->z_null = false; + } + } + } + else + { + std::string message = "The XYZ axis mode is not set, cannot update position."; + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, message); + } + + if (update_e) + { + if (!pos->is_extruder_relative_null) + { + if (pos->is_extruder_relative) + { + pos->get_current_extruder().e = e + pos->get_current_extruder().e; + } + else + { + pos->get_current_extruder().e = e + pos->get_current_extruder().e_offset; + } + } + else + { + std::string message = "The E axis mode is not set, cannot update position."; + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, message); + } + } +} + +void gcode_position::process_g0_g1(position* pos, parsed_command& cmd) +{ + bool update_x = false; + bool update_y = false; + bool update_z = false; + bool update_e = false; + bool update_f = false; + double x = 0; + double y = 0; + double z = 0; + double e = 0; + double f = 0; + for (unsigned int index = 0; index < cmd.parameters.size(); index++) + { + const parsed_command_parameter p_cur_param = cmd.parameters[index]; + if (p_cur_param.name == "X") + { + update_x = true; + x = p_cur_param.double_value; + } + else if (p_cur_param.name == "Y") + { + update_y = true; + y = p_cur_param.double_value; + } + else if (p_cur_param.name == "E") + { + update_e = true; + e = p_cur_param.double_value; + } + else if (p_cur_param.name == "Z") + { + update_z = true; + z = p_cur_param.double_value; + } + else if (p_cur_param.name == "F") + { + update_f = true; + f = p_cur_param.double_value; + } + } + update_position(pos, x, update_x, y, update_y, z, update_z, e, update_e, f, update_f, false, true); +} + +void gcode_position::process_g2(position* pos, parsed_command& cmd) +{ + bool update_x = false; + bool update_y = false; + bool update_e = false; + bool update_f = false; + double x = 0; + double y = 0; + double e = 0; + double f = 0; + for (unsigned int index = 0; index < cmd.parameters.size(); index++) + { + const parsed_command_parameter p_cur_param = cmd.parameters[index]; + if (p_cur_param.name == "X") + { + update_x = true; + x = p_cur_param.double_value; + } + else if (p_cur_param.name == "Y") + { + update_y = true; + y = p_cur_param.double_value; + } + else if (p_cur_param.name == "E") + { + update_e = true; + e = p_cur_param.double_value; + } + else if (p_cur_param.name == "F") + { + update_f = true; + f = p_cur_param.double_value; + } + } + update_position(pos, x, update_x, y, update_y, 0, false, e, update_e, f, update_f, false, true); +} + +void gcode_position::process_g3(position* pos, parsed_command& cmd) +{ + return process_g2(pos, cmd); +} + +void gcode_position::process_g10(position* pos, parsed_command& cmd) +{ + // Take 0 based extruder parameter in account + int p = 0; + bool has_p = false; + double x = 0; + bool has_x = false; + double y = 0; + bool has_y = false; + double z = 0; + bool has_z = false; + double s = 0; + bool has_s = false; + // Handle extruder offset commands + for (unsigned int index = 0; index < cmd.parameters.size(); index++) + { + parsed_command_parameter p_cur_param = cmd.parameters[index]; + if (p_cur_param.name == "S") + { + has_s = true; + if (p_cur_param.value_type == 'F') + s = p_cur_param.double_value; + else + has_s = false; + } + else if (p_cur_param.name == "P") + { + has_p = true; + if (p_cur_param.value_type == 'L') + { + p = static_cast(p_cur_param.unsigned_long_value); + } + else if (p_cur_param.value_type == 'F') + { + double val = p_cur_param.double_value; + val = val + 0.5 - (val < 0); + p = static_cast(val); + } + else + has_p = false; + } + else if (p_cur_param.name == "X") + { + has_x = true; + if (p_cur_param.value_type == 'F') + x = p_cur_param.double_value; + else + has_x = false; + } + else if (p_cur_param.name == "Y") + { + has_y = true; + if (p_cur_param.value_type == 'F') + y = p_cur_param.double_value; + else + has_y = false; + } + else if (p_cur_param.name == "Z") + { + has_z = true; + if (p_cur_param.value_type == 'F') + z = p_cur_param.double_value; + else + has_z = false; + } + } + // apply offsets + if (has_p) + { + // Take 0 based extruder parameter in account before setting offsets + if (!zero_based_extruder_) + { + p--; + } + if (p < 0) + { + p = 0; + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePosition.process_g10: Selected tool was less than 0. Setting offset for tool index 0 instead."); + } + else if (p > num_extruders_ - 1) + { + p = num_extruders_ - 1; + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePosition.process_g10: Selected tool was greater than the number of configured tools. Setting offset for the maximum tool index instead."); + } + if (has_x) + pos->get_extruder(p).x_firmware_offset = x; + if (has_y) + pos->get_extruder(p).y_firmware_offset = y; + if (has_z) + pos->get_extruder(p).z_firmware_offset = z; + return; + } + + // Todo: add firmware retract here +} + +void gcode_position::process_g11(position* pos, parsed_command& cmd) +{ + // Todo: Fix G11 +} + +void gcode_position::process_g20(position* pos, parsed_command& cmd) +{ +} + +void gcode_position::process_g21(position* pos, parsed_command& cmd) +{ +} + +void gcode_position::process_g28(position* pos, parsed_command& cmd) +{ + bool has_x = false; + bool has_y = false; + bool has_z = false; + bool set_x_home = false; + bool set_y_home = false; + bool set_z_home = false; + + for (unsigned int index = 0; index < cmd.parameters.size(); index++) + { + parsed_command_parameter p_cur_param = cmd.parameters[index]; + if (p_cur_param.name == "X") + has_x = true; + else if (p_cur_param.name == "Y") + has_y = true; + else if (p_cur_param.name == "Z") + has_z = true; + } + if (has_x) + { + pos->x_homed = true; + set_x_home = true; + } + if (has_y) + { + pos->y_homed = true; + set_y_home = true; + } + if (has_z) + { + pos->z_homed = true; + set_z_home = true; + } + if (!has_x && !has_y && !has_z) + { + pos->x_homed = true; + pos->y_homed = true; + pos->z_homed = true; + set_x_home = true; + set_y_home = true; + set_z_home = true; + } + + if (set_x_home && !home_x_none_) + { + pos->x = home_x_; + pos->x_null = false; + } + // todo: set error flag on else + if (set_y_home && !home_y_none_) + { + pos->y = home_y_; + pos->y_null = false; + } + // todo: set error flag on else + if (set_z_home && !home_z_none_) + { + pos->z = home_z_; + pos->z_null = false; + } + // todo: set error flag on else +} + +void gcode_position::process_g90(position* pos, parsed_command& cmd) +{ + // Set xyz to absolute mode + if (pos->is_relative_null) + pos->is_relative_null = false; + + pos->is_relative = false; + + if (g90_influences_extruder_) + { + // If g90/g91 influences the extruder, set the extruder to absolute mode too + if (pos->is_extruder_relative_null) + pos->is_extruder_relative_null = false; + + pos->is_extruder_relative = false; + } +} + +void gcode_position::process_g91(position* pos, parsed_command& cmd) +{ + // Set XYZ axis to relative mode + if (pos->is_relative_null) + pos->is_relative_null = false; + + pos->is_relative = true; + + if (g90_influences_extruder_) + { + // If g90/g91 influences the extruder, set the extruder to relative mode too + if (pos->is_extruder_relative_null) + pos->is_extruder_relative_null = false; + + pos->is_extruder_relative = true; + } +} + +void gcode_position::process_g92(position* pos, parsed_command& cmd) +{ + // Set position offset + bool update_x = false; + bool update_y = false; + bool update_z = false; + bool update_e = false; + bool o_exists = false; + double x = 0; + double y = 0; + double z = 0; + double e = 0; + for (unsigned int index = 0; index < cmd.parameters.size(); index++) + { + parsed_command_parameter p_cur_param = cmd.parameters[index]; + if (p_cur_param.name == "X") + { + update_x = true; + x = p_cur_param.double_value; + } + else if (p_cur_param.name == "Y") + { + update_y = true; + y = p_cur_param.double_value; + } + else if (p_cur_param.name == "E") + { + update_e = true; + e = p_cur_param.double_value; + } + else if (p_cur_param.name == "Z") + { + update_z = true; + z = p_cur_param.double_value; + } + else if (p_cur_param.name == "O") + { + o_exists = true; + } + } + + if (o_exists) + { + // Our fake O parameter exists, set axis to homed! + // This is a workaround to allow folks to use octolapse without homing (for shame, lol!) + pos->x_homed = true; + pos->y_homed = true; + pos->z_homed = true; + } + + if (!o_exists && !update_x && !update_y && !update_z && !update_e) + { + if (!pos->x_null) + pos->x_offset = pos->x + pos->x_firmware_offset; + if (!pos->y_null) + pos->y_offset = pos->y + pos->y_firmware_offset; + if (!pos->z_null) + pos->z_offset = pos->z + pos->z_firmware_offset; + // Todo: Does this reset E too? Figure that $#$$ out Formerlurker! + pos->get_current_extruder().e_offset = pos->get_current_extruder().e; + } + else + { + if (update_x) + { + if (!pos->x_null && pos->x_homed) + pos->x_offset = pos->x - x + pos->x_firmware_offset; + else + { + pos->x = x; + pos->x_offset = 0; + pos->x_null = false; + } + } + if (update_y) + { + if (!pos->y_null && pos->y_homed) + pos->y_offset = pos->y - y + pos->y_firmware_offset; + else + { + pos->y = y; + pos->y_offset = 0; + pos->y_null = false; + } + } + if (update_z) + { + if (!pos->z_null && pos->z_homed) + pos->z_offset = pos->z - z + pos->z_firmware_offset; + else + { + pos->z = z; + pos->z_offset = 0; + pos->z_null = false; + } + } + if (update_e) + { + pos->get_current_extruder().e_offset = pos->get_current_extruder().e - e; + } + } +} + +void gcode_position::process_m82(position* pos, parsed_command& cmd) +{ + // Set extrder mode to absolute + if (pos->is_extruder_relative_null) + pos->is_extruder_relative_null = false; + + pos->is_extruder_relative = false; +} + +void gcode_position::process_m83(position* pos, parsed_command& cmd) +{ + // Set extrder mode to relative + if (pos->is_extruder_relative_null) + pos->is_extruder_relative_null = false; + + pos->is_extruder_relative = true; +} + +void gcode_position::process_m207(position* pos, parsed_command& cmd) +{ + // Todo: impemente firmware retract +} + +void gcode_position::process_m208(position* pos, parsed_command& cmd) +{ + // Todo: implement firmware retract +} + +void gcode_position::process_m218(position* pos, parsed_command& cmd) +{ + // Set hotend offsets + int t = 0; + bool has_t = false; + double x = 0; + bool has_x = false; + double y = 0; + bool has_y = false; + double z = 0; + bool has_z = false; + // Handle extruder offset commands + for (unsigned int index = 0; index < cmd.parameters.size(); index++) + { + parsed_command_parameter p_cur_param = cmd.parameters[index]; + + if (p_cur_param.name == "T") + { + has_t = true; + if (p_cur_param.value_type == 'L') + { + t = static_cast(p_cur_param.unsigned_long_value); + } + else if (p_cur_param.value_type == 'F') + { + double val = p_cur_param.double_value; + val = val + 0.5 - (val < 0); + t = static_cast(val); + } + else + has_t = false; + } + else if (p_cur_param.name == "X") + { + has_x = true; + if (p_cur_param.value_type == 'F') + x = p_cur_param.double_value; + else + has_x = false; + } + else if (p_cur_param.name == "Y") + { + has_y = true; + if (p_cur_param.value_type == 'F') + y = p_cur_param.double_value; + else + has_y = false; + } + else if (p_cur_param.name == "Z") + { + has_z = true; + if (p_cur_param.value_type == 'F') + z = p_cur_param.double_value; + else + has_z = false; + } + } + // apply offsets + if (has_t) + { + // Take 0 based extruder parameter in account before setting offsets + if (!zero_based_extruder_) + { + t--; + } + if (t < 0) + { + t = 0; + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePosition.process_m218: Selected tool was less than 0. Setting offset for tool index 0 instead."); + } + else if (t > num_extruders_ - 1) + { + t = num_extruders_ - 1; + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePosition.process_m218: Selected tool was greater than the number of configured tools. Setting offset for the maximum tool index instead."); + } + + if (has_x) + pos->get_extruder(t).x_firmware_offset = x; + if (has_y) + pos->get_extruder(t).y_firmware_offset = y; + if (has_z) + pos->get_extruder(t).z_firmware_offset = z; + return; + } +} + +void gcode_position::process_m563(position* pos, parsed_command& cmd) +{ + // Todo: Work on this command, which defines tools and will affect which tool is selected. +} + +void gcode_position::process_t(position* pos, parsed_command& cmd) +{ + for (unsigned int index = 0; index < cmd.parameters.size(); index++) + { + parsed_command_parameter p_cur_param = cmd.parameters[index]; + if (p_cur_param.name == "T" && p_cur_param.value_type == 'U') + { + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::DEBUG, + "GcodePosition.process_t: Tool change Detected."); + pos->current_tool = static_cast(p_cur_param.unsigned_long_value); + if (!zero_based_extruder_) + { + pos->current_tool--; + } + if (pos->current_tool < 0) + { + pos->current_tool = 0; + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePosition.process_t: The tool index was less than 0. Setting tool index to 0 instead."); + } + else if (pos->current_tool > num_extruders_ - 1) + { + pos->current_tool = num_extruders_ - 1; + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePosition.process_t: The tool index was greater than the number of available tools. Setting tool index to the max tool index instead."); + } + + break; + } + } +} + +gcode_comment_processor* gcode_position::get_gcode_comment_processor() +{ + return &comment_processor_; } diff --git a/octoprint_octolapse/data/lib/c/gcode_position.h b/octoprint_octolapse/data/lib/c/gcode_position.h index 41a38cfe..06e474ca 100644 --- a/octoprint_octolapse/data/lib/c/gcode_position.h +++ b/octoprint_octolapse/data/lib/c/gcode_position.h @@ -26,143 +26,194 @@ #include #include "gcode_parser.h" #include "position.h" -struct gcode_position_args { - gcode_position_args() { - // Wipe Variables - autodetect_position = true; - is_circular_bed = false; - home_x = 0; - home_y = 0; - home_z = 0; - home_x_none = false; - home_y_none = false; - home_z_none = false; - retraction_length = 0; - z_lift_height = 0; - priming_height = 0; - minimum_layer_height = 0; - g90_influences_extruder = false; - xyz_axis_default_mode = "absolute"; - e_axis_default_mode = "absolute"; - units_default = "millimeters"; - is_bound_ = false; - x_min = 0; - x_max = 0; - y_min = 0; - y_max = 0; - z_min = 0; - z_max = 0; - snapshot_x_min = 0; - snapshot_x_max = 0; - snapshot_y_min = 0; - snapshot_y_max = 0; - snapshot_z_min = 0; - snapshot_z_max = 0; - std::vector location_detection_commands; // Final list of location detection commands - } - bool autodetect_position; - bool is_circular_bed; - // Wipe variables - double home_x; - double home_y; - double home_z; - bool home_x_none; - bool home_y_none; - bool home_z_none; - double retraction_length; - double z_lift_height; - double priming_height; - double minimum_layer_height; - bool g90_influences_extruder; - bool is_bound_; - double snapshot_x_min; - double snapshot_x_max; - double snapshot_y_min; - double snapshot_y_max; - double snapshot_z_min; - double snapshot_z_max; - double x_min; - double x_max; - double y_min; - double y_max; - double z_min; - double z_max; - std::string xyz_axis_default_mode; - std::string e_axis_default_mode; - std::string units_default; - std::vector location_detection_commands; // Final list of location detection commands +#include "gcode_comment_processor.h" +#define NUM_POSITIONS 10 + +struct gcode_position_args +{ + gcode_position_args() + { + // Wipe Variables + shared_extruder = true; + autodetect_position = true; + is_circular_bed = false; + home_x = 0; + home_y = 0; + home_z = 0; + home_x_none = false; + home_y_none = false; + home_z_none = false; + retraction_lengths = NULL; + z_lift_heights = NULL; + x_firmware_offsets = NULL; + y_firmware_offsets = NULL; + priming_height = 0; + minimum_layer_height = 0; + height_increment = 0; + g90_influences_extruder = false; + xyz_axis_default_mode = "absolute"; + e_axis_default_mode = "absolute"; + units_default = "millimeters"; + is_bound_ = false; + x_min = 0; + x_max = 0; + y_min = 0; + y_max = 0; + z_min = 0; + z_max = 0; + snapshot_x_min = 0; + snapshot_x_max = 0; + snapshot_y_min = 0; + snapshot_y_max = 0; + snapshot_z_min = 0; + snapshot_z_max = 0; + num_extruders = 1; + default_extruder = 0; + zero_based_extruder = true; + std::vector location_detection_commands; // Final list of location detection commands + set_num_extruders(num_extruders); + } + + gcode_position_args(const gcode_position_args& pos); // Copy Constructor + ~gcode_position_args() + { + delete_retraction_lengths(); + delete_z_lift_heights(); + delete_x_firmware_offsets(); + delete_y_firmware_offsets(); + } + + bool autodetect_position; + bool is_circular_bed; + // Wipe variables + double home_x; + double home_y; + double home_z; + bool home_x_none; + bool home_y_none; + bool home_z_none; + double* retraction_lengths; + double* z_lift_heights; + double* x_firmware_offsets; + double* y_firmware_offsets; + double priming_height; + double minimum_layer_height; + double height_increment; + bool g90_influences_extruder; + bool is_bound_; + double snapshot_x_min; + double snapshot_x_max; + double snapshot_y_min; + double snapshot_y_max; + double snapshot_z_min; + double snapshot_z_max; + double x_min; + double x_max; + double y_min; + double y_max; + double z_min; + double z_max; + bool shared_extruder; + bool zero_based_extruder; + int num_extruders; + int default_extruder; + std::string xyz_axis_default_mode; + std::string e_axis_default_mode; + std::string units_default; + std::vector location_detection_commands; // Final list of location detection commands + gcode_position_args& operator=(const gcode_position_args& pos_args); + void set_num_extruders(int num_extruders); + void delete_retraction_lengths(); + void delete_z_lift_heights(); + void delete_x_firmware_offsets(); + void delete_y_firmware_offsets(); }; class gcode_position { public: - typedef void(gcode_position::*pos_function_type)(position*, parsed_command*); - gcode_position(gcode_position_args* args); - gcode_position(); - ~gcode_position(); + typedef void (gcode_position::*pos_function_type)(position*, parsed_command&); + gcode_position(gcode_position_args args); + gcode_position(); + virtual ~gcode_position(); - void update(parsed_command* command, int file_line_number, int gcode_number); - void update_position(position*, double x, bool update_x, double y, bool update_y, double z, bool update_z, double e, bool update_e, double f, bool update_f, bool force, bool is_g1_g0); - void undo_update(); - position * get_current_position(); - position * get_previous_position(); + void update(parsed_command& command, long file_line_number, long gcode_number, const long file_position); + void update_position(position* position, double x, bool update_x, double y, bool update_y, double z, bool update_z, + double e, bool update_e, double f, bool update_f, bool force, bool is_g1_g0) const; + void undo_update(); + position get_current_position() const; + position get_previous_position() const; + position* get_current_position_ptr(); + position* get_previous_position_ptr(); + gcode_comment_processor* get_gcode_comment_processor(); private: - gcode_position(const gcode_position & source); - - position* p_previous_pos_; - position* p_current_pos_; - position* p_undo_pos_; - bool autodetect_position_; - double priming_height_; - double home_x_; - double home_y_; - double home_z_; - bool home_x_none_; - bool home_y_none_; - bool home_z_none_; - double retraction_length_; - double z_lift_height_; - double minimum_layer_height_; - bool g90_influences_extruder_; - std::string e_axis_default_mode_; - std::string xyz_axis_default_mode_; - std::string units_default_; - bool is_bound_; - double x_min_; - double x_max_; - double y_min_; - double y_max_; - double z_min_; - double z_max_; - bool is_circular_bed_; - double snapshot_x_min_; - double snapshot_x_max_; - double snapshot_y_min_; - double snapshot_y_max_; - double snapshot_z_min_; - double snapshot_z_max_; + gcode_position(const gcode_position& source); + position positions_[static_cast(NUM_POSITIONS)]; + int cur_pos_; + void add_position(parsed_command&); + void add_position(position&); + bool autodetect_position_; + double priming_height_; + double home_x_; + double home_y_; + double home_z_; + bool home_x_none_; + bool home_y_none_; + bool home_z_none_; + double* retraction_lengths_; + double* z_lift_heights_; + double minimum_layer_height_; + double height_increment_; + bool g90_influences_extruder_; + std::string e_axis_default_mode_; + std::string xyz_axis_default_mode_; + std::string units_default_; + bool is_bound_; + double x_min_; + double x_max_; + double y_min_; + double y_max_; + double z_min_; + double z_max_; + bool is_circular_bed_; + double snapshot_x_min_; + double snapshot_x_max_; + double snapshot_y_min_; + double snapshot_y_max_; + double snapshot_z_min_; + double snapshot_z_max_; + int num_extruders_; + bool shared_extruder_; + bool zero_based_extruder_; + + std::map gcode_functions_; + std::map::iterator gcode_functions_iterator_; - std::map gcode_functions_; - std::map::iterator gcode_functions_iterator_; - - std::map get_gcode_functions(); - /// Process Gcode Command Functions - void process_g0_g1(position*, parsed_command*); - void process_g2(position*, parsed_command*); - void process_g3(position*, parsed_command*); - void process_g10(position*, parsed_command*); - void process_g11(position*, parsed_command*); - void process_g20(position*, parsed_command*); - void process_g21(position*, parsed_command*); - void process_g28(position*, parsed_command*); - void process_g90(position*, parsed_command*); - void process_g91(position*, parsed_command*); - void process_g92(position*, parsed_command*); - void process_m82(position*, parsed_command*); - void process_m83(position*, parsed_command*); - void process_m207(position*, parsed_command*); - void process_m208(position*, parsed_command*); + std::map get_gcode_functions(); + /// Process Gcode Command Functions + void process_g0_g1(position*, parsed_command&); + void process_g2(position*, parsed_command&); + void process_g3(position*, parsed_command&); + void process_g10(position*, parsed_command&); + void process_g11(position*, parsed_command&); + void process_g20(position*, parsed_command&); + void process_g21(position*, parsed_command&); + void process_g28(position*, parsed_command&); + void process_g90(position*, parsed_command&); + void process_g91(position*, parsed_command&); + void process_g92(position*, parsed_command&); + void process_m82(position*, parsed_command&); + void process_m83(position*, parsed_command&); + void process_m207(position*, parsed_command&); + void process_m208(position*, parsed_command&); + void process_m218(position*, parsed_command&); + void process_m563(position*, parsed_command&); + void process_t(position*, parsed_command&); + gcode_comment_processor comment_processor_; + void delete_retraction_lengths_(); + void delete_z_lift_heights_(); + void set_num_extruders(int num_extruders); }; #endif diff --git a/octoprint_octolapse/data/lib/c/gcode_position_processor.cpp b/octoprint_octolapse/data/lib/c/gcode_position_processor.cpp index f99e6e88..b7020044 100644 --- a/octoprint_octolapse/data/lib/c/gcode_position_processor.cpp +++ b/octoprint_octolapse/data/lib/c/gcode_position_processor.cpp @@ -29,16 +29,16 @@ #include "stabilization.h" #include "logging.h" #include "python_helpers.h" + #ifdef _DEBUG #include "test.h" #endif // Sometimes used to test performance in release mode. //#include "test.h" -#if PY_MAJOR_VERSION >= 3 -int main(int argc, char *argv[]) +int main(int argc, wchar_t* argv[]) { - wchar_t *program = Py_DecodeLocale(argv[0], NULL); + wchar_t* program = argv[0]; if (program == NULL) { fprintf(stderr, "Fatal error: cannot decode argv[0]\n"); exit(1); @@ -47,151 +47,158 @@ int main(int argc, char *argv[]) // Add a built-in module, before Py_Initialize PyImport_AppendInittab("GcodePositionProcessor", PyInit_GcodePositionProcessor); - // Pass argv[0] to the Python interpreter +#if PY_VERSION_HEX < 0x03080000 Py_SetProgramName(program); - // Initialize the Python interpreter. Required. Py_Initialize(); +#else + { + PyStatus status; + + PyConfig config; + PyConfig_InitPythonConfig(&config); + + if (argc && argv) { + status = PyConfig_SetString(&config, &config.program_name, program); + if (PyStatus_Exception(status)) { + PyConfig_Clear(&config); + return 1; + } + + status = PyConfig_SetArgv(&config, argc, argv); + if (PyStatus_Exception(status)) { + PyConfig_Clear(&config); + return 1; + } + } + + status = Py_InitializeFromConfig(&config); + if (PyStatus_Exception(status)) { + PyConfig_Clear(&config); + return 1; + } + + PyConfig_Clear(&config); + } +#endif + + + + + +#if PY_VERSION_HEX < 0x03090000 std::cout << "Initializing threads..."; - PyEval_InitThreads(); + Py_SetProgramName(program); +#endif // Optionally import the module; alternatively, import can be deferred until the embedded script imports it. PyImport_ImportModule("GcodePositionProcessor"); PyMem_RawFree(program); return 0; } -#else - -int main(int argc, char *argv[]) +struct module_state { -#ifdef _DEBUG - run_tests(argc, argv); - return 0; -#endif - // I use this sometimes to test performance in release mode - //run_tests(argc, argv); - //return 0; - Py_SetProgramName(argv[0]); - Py_Initialize(); - PyEval_InitThreads(); - initGcodePositionProcessor(); - return 0; - -} -#endif - -struct module_state { - PyObject *error; + PyObject* error; }; -#if PY_MAJOR_VERSION >= 3 #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) -#else -#define GETSTATE(m) (&_state) -static struct module_state _state; -#endif // Python 2 module method definition static PyMethodDef GcodePositionProcessorMethods[] = { - { "Initialize", (PyCFunction)Initialize, METH_VARARGS ,"Initialize the internal shared position processor." }, - { "Undo", (PyCFunction)Undo, METH_VARARGS ,"Undo an update made to the current position. You can only undo once." }, - { "Update", (PyCFunction)Update, METH_VARARGS ,"Undo an update made to the current position. You can only undo once." }, - { "UpdatePosition", (PyCFunction)UpdatePosition, METH_VARARGS ,"Update x,y,z,e and f for the given position key." }, - { "Parse", (PyCFunction)Parse, METH_VARARGS ,"Parse gcode text into a ParsedCommand." }, - { "GetCurrentPositionTuple", (PyCFunction)GetCurrentPositionTuple, METH_VARARGS ,"Returns the current position of the global GcodePosition tracker in a faster but harder to handle tuple form." }, - { "GetCurrentPositionDict", (PyCFunction)GetCurrentPositionDict, METH_VARARGS ,"Returns the current position of the global GcodePosition tracker in a slower but easier to deal with dict form." }, - { "GetPreviousPositionTuple", (PyCFunction)GetPreviousPositionTuple, METH_VARARGS ,"Returns the previous position of the global GcodePosition tracker in a faster but harder to handle tuple form." }, - { "GetPreviousPositionDict", (PyCFunction)GetPreviousPositionDict, METH_VARARGS ,"Returns the previous position of the global GcodePosition tracker in a slower but easier to deal with dict form." }, - { "GetSnapshotPlans_SmartLayer", (PyCFunction)GetSnapshotPlans_SmartLayer, METH_VARARGS, "Parses a gcode file and returns snapshot plans for a 'SmartLayer' stabilization." }, - { NULL, NULL, 0, NULL } + {"Initialize", (PyCFunction)Initialize, METH_VARARGS, "Initialize the internal shared position processor."}, + {"Undo", (PyCFunction)Undo, METH_VARARGS, "Undo an update made to the current position. You can only undo once."}, + { + "Update", (PyCFunction)Update, METH_VARARGS, "Undo an update made to the current position. You can only undo once." + }, + {"UpdatePosition", (PyCFunction)UpdatePosition, METH_VARARGS, "Update x,y,z,e and f for the given position key."}, + {"Parse", (PyCFunction)Parse, METH_VARARGS, "Parse gcode text into a ParsedCommand."}, + { + "GetCurrentPositionTuple", (PyCFunction)GetCurrentPositionTuple, METH_VARARGS, + "Returns the current position of the global GcodePosition tracker in a faster but harder to handle tuple form." + }, + { + "GetCurrentPositionDict", (PyCFunction)GetCurrentPositionDict, METH_VARARGS, + "Returns the current position of the global GcodePosition tracker in a slower but easier to deal with dict form." + }, + { + "GetPreviousPositionTuple", (PyCFunction)GetPreviousPositionTuple, METH_VARARGS, + "Returns the previous position of the global GcodePosition tracker in a faster but harder to handle tuple form." + }, + { + "GetPreviousPositionDict", (PyCFunction)GetPreviousPositionDict, METH_VARARGS, + "Returns the previous position of the global GcodePosition tracker in a slower but easier to deal with dict form." + }, + { + "GetSnapshotPlans_SmartLayer", (PyCFunction)GetSnapshotPlans_SmartLayer, METH_VARARGS, + "Parses a gcode file and returns snapshot plans for a 'SmartLayer' stabilization." + }, + { + "GetSnapshotPlans_SmartGcode", (PyCFunction)GetSnapshotPlans_SmartGcode, METH_VARARGS, + "Parses a gcode file and returns snapshot plans for a 'SmartGcode' stabilization." + }, + {NULL, NULL, 0, NULL} }; // Python 3 module method definition -#if PY_MAJOR_VERSION >= 3 -static int GcodePositionProcessor_traverse(PyObject *m, visitproc visit, void *arg) { +static int GcodePositionProcessor_traverse(PyObject* m, visitproc visit, void* arg) { Py_VISIT(GETSTATE(m)->error); return 0; } -static int GcodePositionProcessor_clear(PyObject *m) { +static int GcodePositionProcessor_clear(PyObject* m) { Py_CLEAR(GETSTATE(m)->error); return 0; } static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "GcodePositionProcessor", - NULL, - sizeof(struct module_state), - GcodePositionProcessorMethods, - NULL, - GcodePositionProcessor_traverse, - GcodePositionProcessor_clear, - NULL + PyModuleDef_HEAD_INIT, + "GcodePositionProcessor", + NULL, + sizeof(struct module_state), + GcodePositionProcessorMethods, + NULL, + GcodePositionProcessor_traverse, + GcodePositionProcessor_clear, + NULL }; #define INITERROR return NULL PyMODINIT_FUNC PyInit_GcodePositionProcessor(void) - -#else -#define INITERROR return - -extern "C" void initGcodePositionProcessor(void) -#endif { - std::cout << "Initializing GcodePositionProcessor V1.0.0 - Copyright (C) 2019 Brad Hochgesang..."; - -#if PY_MAJOR_VERSION >= 3 - std::cout << "Python 3+ Detected..."; - PyObject *module = PyModule_Create(&moduledef); -#else - std::cout << "Python 2 Detected..."; - PyObject *module = Py_InitModule("GcodePositionProcessor", GcodePositionProcessorMethods); -#endif + std::cout << "Initializing GcodePositionProcessor V1.0.1 - Copyright (C) 2019 Brad Hochgesang..."; - if (module == NULL) - INITERROR; - struct module_state *st = GETSTATE(module); + std::cout << "Python 3+ Detected..."; + PyObject* module = PyModule_Create(&moduledef); - st->error = PyErr_NewException((char*)"GcodePositionProcessor.Error", NULL, NULL); - if (st->error == NULL) { - Py_DECREF(module); - INITERROR; - } - octolapse_initialize_loggers(); - gpp::parser = new gcode_parser(); - std::cout << "complete\r\n"; + if (module == NULL) + INITERROR; + struct module_state* st = GETSTATE(module); -#if PY_MAJOR_VERSION >= 3 - return module; -#endif + st->error = PyErr_NewException((char*)"GcodePositionProcessor.Error", NULL, NULL); + if (st->error == NULL) + { + Py_DECREF(module); + INITERROR; + } + octolapse_initialize_loggers(); + gpp::parser = new gcode_parser(); + + std::cout << "complete\r\n"; + return module; } -extern "C" -{ - /* - void initGcodePositionProcessor(void) - { - - Py_Initialize(); - //PyEval_InitThreads(); +extern "C" { - PyObject *m = Py_InitModule("GcodePositionProcessor", GcodePositionProcessorMethods); - octolapse_initialize_loggers(); - gpp::parser = new gcode_parser(); - std::cout << "complete\r\n"; - } - */ - static PyObject * GetSnapshotPlans_SmartLayer(PyObject *self, PyObject *args) + static PyObject* GetSnapshotPlans_SmartLayer(PyObject* self, PyObject* args) { set_internal_log_levels(true); octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Running smart layer stabilization preprocessing."); // TODO: add error reporting and logging - PyObject *py_position_args; - PyObject *py_stabilization_args; - PyObject *py_stabilization_type_args; + PyObject* py_position_args; + PyObject* py_stabilization_args; + PyObject* py_stabilization_type_args; //std::cout << "Parsing Arguments\r\n"; if (!PyArg_ParseTuple( args, @@ -200,14 +207,11 @@ extern "C" &py_stabilization_args, &py_stabilization_type_args)) { - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::ERROR, "Error parsing parameters for GcodePositionProcessor.GetSnapshotPlans_LockToPrint."); - PyErr_SetString(PyExc_ValueError, "Error parsing parameters for GcodePositionProcessor.GetSnapshotPlans_LockToPrint."); + std::string message = "GcodePositionProcessor.GetSnapshotPlans_SmartLayer - Error parsing parameters."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return NULL; } // Removed by BH on 4-28-2019 - //Py_INCREF(py_position_args); - //Py_INCREF(py_stabilization_args); - //Py_INCREF(py_stabilization_type_args); // Extract the position args gcode_position_args p_args; //std::cout << "Parsing position arguments\r\n"; @@ -219,7 +223,10 @@ extern "C" // Extract the stabilization args stabilization_args s_args; //std::cout << "Parsing stabilization arguments\r\n"; - if (!ParseStabilizationArgs(py_stabilization_args, &s_args)) + PyObject* py_progress_received_callback = NULL; + PyObject* py_snapshot_position_callback = NULL; + if (!ParseStabilizationArgs(py_stabilization_args, &s_args, &py_progress_received_callback, + &py_snapshot_position_callback)) { return NULL; } @@ -233,39 +240,23 @@ extern "C" // Create our stabilization object set_internal_log_levels(false); stabilization_smart_layer stabilization( - &p_args, - &s_args, - &mt_args, + p_args, + s_args, + mt_args, pythonGetCoordinatesCallback(ExecuteGetSnapshotPositionCallback), - pythonProgressCallback(ExecuteStabilizationProgressCallback) + py_snapshot_position_callback, + pythonProgressCallback(ExecuteStabilizationProgressCallback), + py_progress_received_callback ); - - stabilization_results results; - //std::cout << "Processing gcode file.\r\n"; - - stabilization.process_file(&results); - - + stabilization_results results = stabilization.process_file(); set_internal_log_levels(true); - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Building snapshot plans."); - PyObject * py_snapshot_plans = snapshot_plan::build_py_object(results.snapshot_plans_); - if (py_snapshot_plans == NULL) - { - //octolapse_log(SNAPSHOT_PLAN, ERROR, "GcodePositionProcessor.ExecuteStabilizationCompleteCallback - Snapshot_plan::build_py_object returned Null"); - //PyErr_SetString(PyExc_ValueError, "GcodePositionProcessor.ExecuteStabilizationCompleteCallback - Snapshot_plan::build_py_object returned Null - Terminating"); - return NULL; - } - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Creating return values."); - PyObject * py_results = Py_BuildValue("(l,s,O,d,l,l)", results.success_, results.errors_.c_str(), py_snapshot_plans, results.seconds_elapsed_, results.gcodes_processed_, results.lines_processed_); + + PyObject* py_results = results.to_py_object(); if (py_results == NULL) { - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::ERROR, "Unable to create a Tuple from the snapshot plan list."); - PyErr_SetString(PyExc_ValueError, "GcodePositionProcessor.ExecuteStabilizationCompleteCallback - Error building callback arguments - Terminating"); return NULL; } - // Bring the snapshot plan refcount to 1 - Py_DECREF(py_snapshot_plans); //Py_DECREF(py_position_args); //Py_DECREF(py_stabilization_args); //Py_DECREF(py_stabilization_type_args); @@ -275,12 +266,82 @@ extern "C" return py_results; } - static PyObject* Initialize(PyObject* self, PyObject *args) + static PyObject* GetSnapshotPlans_SmartGcode(PyObject* self, PyObject* args) + { + set_internal_log_levels(true); + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Running smart gcode stabilization preprocessing."); + // TODO: add error reporting and logging + PyObject* py_position_args; + PyObject* py_stabilization_args; + PyObject* py_stabilization_type_args; + //std::cout << "Parsing Arguments\r\n"; + if (!PyArg_ParseTuple( + args, + "OOO", + &py_position_args, + &py_stabilization_args, + &py_stabilization_type_args)) + { + std::string message = "GcodePositionProcessor.GetSnapshotPlans_SmartGcode - Error parsing parameters."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + // Removed by BH on 4-28-2019 + // Extract the position args + gcode_position_args p_args; + //std::cout << "Parsing position arguments\r\n"; + if (!ParsePositionArgs(py_position_args, &p_args)) + { + return NULL; + } + + // Extract the stabilization args + stabilization_args s_args; + //std::cout << "Parsing stabilization arguments\r\n"; + PyObject* py_progress_received_callback = NULL; + PyObject* py_snapshot_position_callback = NULL; + if (!ParseStabilizationArgs(py_stabilization_args, &s_args, &py_progress_received_callback, + &py_snapshot_position_callback)) + { + return NULL; + } + //std::cout << "Parsing smart layer arguments\r\n"; + smart_gcode_args mt_args; + if (!ParseStabilizationArgs_SmartGcode(py_stabilization_type_args, &mt_args)) + { + return NULL; + } + //std::cout << "Creating Stabilization.\r\n"; + // Create our stabilization object + set_internal_log_levels(false); + stabilization_smart_gcode stabilization( + p_args, + s_args, + mt_args, + pythonGetCoordinatesCallback(ExecuteGetSnapshotPositionCallback), + py_snapshot_position_callback, + pythonProgressCallback(ExecuteStabilizationProgressCallback), + py_progress_received_callback + ); + stabilization_results results = stabilization.process_file(); + set_internal_log_levels(true); + + + PyObject* py_results = results.to_py_object(); + if (py_results == NULL) + { + return NULL; + } + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Snapshot plan creation complete, returning plans."); + return py_results; + } + + static PyObject* Initialize(PyObject* self, PyObject* args) { set_internal_log_levels(true); // Create the gcode position object octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::INFO, "Initializing gcode position processor."); - const char * pKey; + const char* pKey; PyObject* py_position_args; if (!PyArg_ParseTuple( args, "sO", @@ -288,30 +349,27 @@ extern "C" &py_position_args )) { - std::string message = "GcodePositionProcessor.Initialize failed: unable to parse the initialization parameters."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.Initialize - Error parsing parameters."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return NULL; } gcode_position_args positionArgs; // Create the gcode position object - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Parsing initialization position args."); + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::INFO, "Parsing initialization position args."); if (!ParsePositionArgs(py_position_args, &positionArgs)) { - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::ERROR, "Error parsing initialization position args."); return NULL; // The call failed, ParseInitializationArgs has taken care of the error message } - + // see if we already have a gcode_position object for the given key std::map::iterator gcode_position_iterator = gpp::gcode_positions.find(pKey); gcode_position* p_gcode_position = NULL; if (gcode_position_iterator != gpp::gcode_positions.end()) { - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Existing processor found, deleting."); + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::INFO, "Existing processor found, deleting."); delete gcode_position_iterator->second; } // Separate delete step. Something is going on here. @@ -323,25 +381,27 @@ extern "C" std::string message = "Adding processor with key:"; message.append(pKey).append("\r\n"); - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, message); + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::INFO, message); // Create the new position object - gcode_position * p_new_position = new gcode_position(&positionArgs); + gcode_position* p_new_position = new gcode_position(positionArgs); // add the new gcode position to our list of objects gpp::gcode_positions.insert(std::pair(pKey, p_new_position)); + // Return True return Py_BuildValue("O", Py_True); } - static PyObject* Undo(PyObject* self, PyObject *args) + static PyObject* Undo(PyObject* self, PyObject* args) { set_internal_log_levels(true); octolapse_log( - octolapse_log::GCODE_POSITION, octolapse_log::VERBOSE, + octolapse_log::GCODE_POSITION, octolapse_log::DEBUG, "Undoing the last gcode position update." ); - const char * key; + const char* key; if (!PyArg_ParseTuple(args, "s", &key)) { - PyErr_SetString(PyExc_ValueError, "Undo requires at least one parameter: the gcode_position key"); + std::string message = "GcodePositionProcessor.Undo - Error parsing parameters."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return NULL; } // Get the parser @@ -353,11 +413,11 @@ extern "C" gcode_position* p_gcode_position = gcode_position_iterator->second; p_gcode_position->undo_update(); - + return Py_BuildValue("O", Py_True); } - static PyObject* Update(PyObject* self, PyObject *args) + static PyObject* Update(PyObject* self, PyObject* args) { set_internal_log_levels(true); octolapse_log( @@ -368,39 +428,30 @@ extern "C" const char* gcode; if (!PyArg_ParseTuple(args, "ss", &key, &gcode)) { - std::string message = "GcodePositionProcessor.Update - requires at least two parameters: the key and the gcode string"; - PyErr_Print(); - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.Update - Error parsing parameters."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return NULL; - } - + // Get the parser std::map::iterator gcode_position_iterator = gpp::gcode_positions.find(key); if (gcode_position_iterator == gpp::gcode_positions.end()) { + std::string message = "GcodePositionProcessor.Update - No position processor was found for the given key: "; + message += key; + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, message); return Py_BuildValue("O", Py_False); } gcode_position* p_gcode_position = gcode_position_iterator->second; parsed_command command; - if(gpp::parser->try_parse_gcode(gcode, &command)) - p_gcode_position->update(&command, -1, -1); + gpp::parser->try_parse_gcode(gcode, command); + p_gcode_position->update(command, -1, -1, -1); - PyObject * py_position = p_gcode_position->get_current_position()->to_py_tuple(); - if (py_position == NULL) - { - std::string message = "GcodePositionProcessor.Update - Unable to convert the position to a tuple."; - PyErr_Print(); - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - return py_position; + return p_gcode_position->get_current_position_ptr()->to_py_tuple(); } - static PyObject* UpdatePosition(PyObject* self, PyObject *args) + static PyObject* UpdatePosition(PyObject* self, PyObject* args) { set_internal_log_levels(true); octolapse_log( @@ -434,25 +485,23 @@ extern "C" &update_f )) { - std::string message = "Unable to parse the UpdatePosition argument list."; - PyErr_Print(); - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.UpdatePosition - Error parsing parameters."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return NULL; } // Get the parser std::map::iterator gcode_position_iterator = gpp::gcode_positions.find(key); if (gcode_position_iterator == gpp::gcode_positions.end()) { - std::string message = "No parser was found for the given key: "; + std::string message = "GcodePositionProcessor.UpdatePosition - No position processor was found for the given key: "; message += key; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, message); return Py_BuildValue("O", Py_False); } gcode_position* p_gcode_position = gcode_position_iterator->second; - + position* pos = p_gcode_position->get_current_position_ptr(); p_gcode_position->update_position( - p_gcode_position->get_current_position(), + pos, x, update_x > 0, y, @@ -466,72 +515,55 @@ extern "C" true, false); - PyObject * py_position = p_gcode_position->get_current_position()->to_py_tuple(); - if (py_position == NULL) - { - return NULL; - } - - return py_position; + return p_gcode_position->get_current_position_ptr()->to_py_tuple(); } - static PyObject* Parse(PyObject* self, PyObject *args) + static PyObject* Parse(PyObject* self, PyObject* args) { set_internal_log_levels(true); octolapse_log( - octolapse_log::GCODE_PARSER, octolapse_log::INFO, + octolapse_log::GCODE_PARSER, octolapse_log::VERBOSE, "Parsing gcode." ); const char* gcode; if (!PyArg_ParseTuple(args, "s", &gcode)) { - std::string message = "Parse requires at least one parameter: the gcode string. Either this parameter is missing or it is not a unicode string."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.Parse - Error parsing parameters."; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); return NULL; } parsed_command command; - bool success = gpp::parser->try_parse_gcode(gcode, &command); - if (!success) - return Py_BuildValue("O", Py_False); - // Convert ParsedCommand to python object - // note that all error handling will be done within the - // to_py_object function + gpp::parser->try_parse_gcode(gcode, command); return command.to_py_object(); - } - static PyObject* GetCurrentPositionTuple(PyObject* self, PyObject *args) + static PyObject* GetCurrentPositionTuple(PyObject* self, PyObject* args) { set_internal_log_levels(true); octolapse_log( octolapse_log::GCODE_POSITION, octolapse_log::VERBOSE, "Getting current position tuple." ); - const char * key; + const char* key; if (!PyArg_ParseTuple(args, "s", &key)) { - std::string message = "GetCurrentPositionTuple requires at least one parameter: the gcode_position key"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.Parse - Error parsing parameters."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return NULL; } // Get the parser - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Retrieving the current position processor for the supplied key."); std::map::iterator gcode_position_iterator = gpp::gcode_positions.find(key); if (gcode_position_iterator == gpp::gcode_positions.end()) { - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Could not find a position processor with the given key."); + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "Could not find a position processor with the given key."); return Py_BuildValue("O", Py_False); } gcode_position* p_gcode_position = gcode_position_iterator->second; - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Creating and returning the current position tuple."); - return p_gcode_position->get_current_position()->to_py_tuple(); + return p_gcode_position->get_current_position().to_py_tuple(); } - static PyObject* GetCurrentPositionDict(PyObject* self, PyObject *args) + static PyObject* GetCurrentPositionDict(PyObject* self, PyObject* args) { set_internal_log_levels(true); octolapse_log( @@ -541,10 +573,8 @@ extern "C" char* key; if (!PyArg_ParseTuple(args, "s", &key)) { - std::string message = "GetCurrentPositionTuple requires at least one parameter: the gcode_position key"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.GetCurrentPositionDict - Error parsing parameters."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return NULL; } @@ -552,43 +582,42 @@ extern "C" std::map::iterator gcode_position_iterator = gpp::gcode_positions.find(key); if (gcode_position_iterator == gpp::gcode_positions.end()) { + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "Could not find a position processor with the given key."); return Py_BuildValue("O", Py_False); } gcode_position* p_gcode_position = gcode_position_iterator->second; - return p_gcode_position->get_current_position()->to_py_dict(); + return p_gcode_position->get_current_position().to_py_dict(); } - static PyObject* GetPreviousPositionTuple(PyObject* self, PyObject *args) + static PyObject* GetPreviousPositionTuple(PyObject* self, PyObject* args) { set_internal_log_levels(true); octolapse_log( octolapse_log::GCODE_POSITION, octolapse_log::VERBOSE, "Getting previous position tuple." ); - const char * key; + const char* key; if (!PyArg_ParseTuple(args, "s", &key)) { - std::string message = "GetCurrentPositionTuple requires at least one parameter: the gcode_position key"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.GetPreviousPositionTuple - Error parsing parameters."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return NULL; } // Get the position processor by key - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Retrieving the current position processor for the supplied key."); std::map::iterator gcode_position_iterator = gpp::gcode_positions.find(key); if (gcode_position_iterator == gpp::gcode_positions.end()) { - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Could not find a position processor with the given key."); + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePositionProcessor.GetPreviousPositionTuple - Could not find a position processor with the given key."); return Py_BuildValue("O", Py_False); } gcode_position* p_gcode_position = gcode_position_iterator->second; - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Creating and returning the previous position tuple."); - return p_gcode_position->get_previous_position()->to_py_tuple(); + return p_gcode_position->get_previous_position().to_py_tuple(); } - static PyObject* GetPreviousPositionDict(PyObject* self, PyObject *args) + static PyObject* GetPreviousPositionDict(PyObject* self, PyObject* args) { set_internal_log_levels(true); octolapse_log( @@ -598,10 +627,8 @@ extern "C" char* key; if (!PyArg_ParseTuple(args, "s", &key)) { - std::string message = "GetCurrentPositionTuple requires at least one parameter: the gcode_position key"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.GetPreviousPositionDict - Error parsing parameters."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return NULL; } @@ -609,216 +636,203 @@ extern "C" std::map::iterator gcode_position_iterator = gpp::gcode_positions.find(key); if (gcode_position_iterator == gpp::gcode_positions.end()) { + octolapse_log(octolapse_log::GCODE_POSITION, octolapse_log::ERROR, + "GcodePositionProcessor.GetPreviousPositionDict - Could not find a position processor with the given key."); return Py_BuildValue("O", Py_False); } gcode_position* p_gcode_position = gcode_position_iterator->second; - return p_gcode_position->get_previous_position()->to_py_dict(); + return p_gcode_position->get_previous_position().to_py_dict(); } } -static bool ExecuteStabilizationProgressCallback(PyObject* progress_callback, const double percent_complete, const double seconds_elapsed, const double estimated_seconds_remaining, const long gcodes_processed, const long lines_processed) +static bool ExecuteStabilizationProgressCallback(PyObject* progress_callback, const double percent_complete, + const double seconds_elapsed, const double estimated_seconds_remaining, + const int gcodes_processed, const int lines_processed) { - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::VERBOSE, "Executing the stabilization progress callback."); - PyObject * funcArgs = Py_BuildValue("(d,d,d,i,i)", percent_complete, seconds_elapsed, estimated_seconds_remaining, gcodes_processed, lines_processed); + //octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::VERBOSE, "Executing the stabilization progress callback."); + PyObject* funcArgs = Py_BuildValue("(d,d,d,i,i)", percent_complete, seconds_elapsed, estimated_seconds_remaining, + gcodes_processed, lines_processed); if (funcArgs == NULL) { - std::string message = "GcodePositionProcessor.ExecuteStabilizationProgressCallback - Error building callback arguments - Terminating"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.ExecuteStabilizationProgressCallback - Error parsing parameters."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject * pContinueProcessing = PyObject_CallObject(progress_callback, funcArgs); + PyObject* pContinueProcessing = PyObject_CallObject(progress_callback, funcArgs); PyGILState_Release(gstate); Py_DECREF(funcArgs); if (pContinueProcessing == NULL) { - std::string message = "GcodePositionProcessor.ExecuteStabilizationProgressCallback - Failed to call python - Terminating"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = + "GcodePositionProcessor.ExecuteStabilizationProgressCallback - Failed to call python progress callback."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } bool continue_processing = PyLong_AsLong(pContinueProcessing) > 0; Py_DECREF(pContinueProcessing); return continue_processing; - } -static bool ExecuteGetSnapshotPositionCallback(PyObject* py_get_snapshot_position_callback, double x_initial, double y_initial, double* x_result, double* y_result ) +static bool ExecuteGetSnapshotPositionCallback(PyObject* py_get_snapshot_position_callback, double x_initial, + double y_initial, double& x_result, double& y_result) { - //std::cout << "Executing get_snapshot_position callback.\r\n"; - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::VERBOSE, "Executing the get_snapshot_position callback."); - PyObject * funcArgs = Py_BuildValue("(d,d)", x_initial, y_initial); + //octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::VERBOSE, "Executing the get_snapshot_position callback."); + PyObject* funcArgs = Py_BuildValue("(d,d)", x_initial, y_initial); if (funcArgs == NULL) { - std::string message = "GcodePositionProcessor.ExecuteGetSnapshotPositionCallback - Error building callback arguments - Terminating"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = "GcodePositionProcessor.ExecuteGetSnapshotPositionCallback - Error parsing parameters."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject * pyCoordinates = PyObject_CallObject(py_get_snapshot_position_callback, funcArgs); + PyObject* pyCoordinates = PyObject_CallObject(py_get_snapshot_position_callback, funcArgs); PyGILState_Release(gstate); Py_DECREF(funcArgs); if (pyCoordinates == NULL) { - std::string message = "GcodePositionProcessor.ExecuteGetSnapshotPositionCallback - Failed to call python - Terminating"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = + "GcodePositionProcessor.ExecuteGetSnapshotPositionCallback - Failed to call python get stabilization position callback."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } - //std::cout << "Extracting X coordinate.\r\n"; - PyObject * pyX = PyDict_GetItemString(pyCoordinates,"x"); + PyObject* pyX = PyDict_GetItemString(pyCoordinates, "x"); if (pyX == NULL) { - std::string message = "GcodePositionProcessor.ExecuteGetSnapshotPositionCallback - Failed to parse the return x value - Terminating"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = + "GcodePositionProcessor.ExecuteGetSnapshotPositionCallback - Failed to parse the return x value."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } - *x_result = PyFloatOrInt_AsDouble(pyX); - //std::cout << "Extracting Y coordinate.\r\n"; - PyObject * pyY = PyDict_GetItemString(pyCoordinates, "y"); + x_result = PyFloatOrInt_AsDouble(pyX); + PyObject* pyY = PyDict_GetItemString(pyCoordinates, "y"); if (pyY == NULL) { - std::string message = "GcodePositionProcessor.ExecuteGetSnapshotPositionCallback - Failed to parse the return y value - Terminating"; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = + "GcodePositionProcessor.ExecuteGetSnapshotPositionCallback - Failed to parse the return y value."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } - *y_result = PyFloatOrInt_AsDouble(pyY); - //std::cout << "Next Stabilization Coordinates: X" << *x_result << " Y"<< *y_result <<"\r\n"; + y_result = PyFloatOrInt_AsDouble(pyY); Py_DECREF(pyCoordinates); return true; - } /// Argument Parsing -static bool ParsePositionArgs(PyObject *py_args, gcode_position_args *args) +static bool ParsePositionArgs(PyObject* py_args, gcode_position_args* args) { octolapse_log( - octolapse_log::GCODE_POSITION, octolapse_log::INFO, + octolapse_log::GCODE_POSITION, octolapse_log::DEBUG, "Parsing Position Args." ); // Here is the full structure of the position args: // get the volume py object - PyObject * py_volume = PyDict_GetItemString(py_args, "volume"); + PyObject* py_volume = PyDict_GetItemString(py_args, "volume"); if (py_volume == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve py_volume from the position args dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve volume from the position args dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } // Here is the full structure of the position args: // get the volume py object - PyObject * py_bed_type = PyDict_GetItemString(py_volume, "bed_type"); + PyObject* py_bed_type = PyDict_GetItemString(py_volume, "bed_type"); if (py_bed_type == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve py_bed_type from the position args dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve bed_type from the position args dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } // Extract the bed type string args->is_circular_bed = strcmp(PyUnicode_SafeAsString(py_bed_type), "circular") == 0; - if (args->is_circular_bed) - { - octolapse_log( - octolapse_log::GCODE_PARSER, octolapse_log::INFO, - "Circular bed set." - ); - } - else - { - octolapse_log( - octolapse_log::GCODE_PARSER, octolapse_log::INFO, - "Rectangular bed set" - ); - } // Get Build Plate Area - PyObject * py_x_min = PyDict_GetItemString(py_volume, "min_x"); + PyObject* py_x_min = PyDict_GetItemString(py_volume, "min_x"); if (py_x_min == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve min_x from the volume dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve min_x from the position args dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->x_min = PyFloatOrInt_AsDouble(py_x_min); - PyObject * py_x_max = PyDict_GetItemString(py_volume, "max_x"); + PyObject* py_x_max = PyDict_GetItemString(py_volume, "max_x"); if (py_x_max == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve max_x from the volume dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve max_x from the position args dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->x_max = PyFloatOrInt_AsDouble(py_x_max); - PyObject * py_y_min = PyDict_GetItemString(py_volume, "min_y"); + PyObject* py_y_min = PyDict_GetItemString(py_volume, "min_y"); if (py_y_min == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve min_y from the volume dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve min_y from the position args dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->y_min = PyFloatOrInt_AsDouble(py_y_min); - PyObject * py_y_max = PyDict_GetItemString(py_volume, "max_y"); + PyObject* py_y_max = PyDict_GetItemString(py_volume, "max_y"); if (py_y_max == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve max_y from the volume dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve max_y from the position args dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->y_max = PyFloatOrInt_AsDouble(py_y_max); - PyObject * py_z_min = PyDict_GetItemString(py_volume, "min_z"); + PyObject* py_z_min = PyDict_GetItemString(py_volume, "min_z"); if (py_z_min == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve min_z from the volume dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve min_z from the position args dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->z_min = PyFloatOrInt_AsDouble(py_z_min); - PyObject * py_z_max = PyDict_GetItemString(py_volume, "max_z"); + PyObject* py_z_max = PyDict_GetItemString(py_volume, "max_z"); if (py_z_max == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve max_z from the volume dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve max_z from the position args dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->z_max = PyFloatOrInt_AsDouble(py_z_max); // Get Bounds - PyObject * py_bounds = PyDict_GetItemString(py_volume, "bounds"); + PyObject* py_bounds = PyDict_GetItemString(py_volume, "bounds"); if (py_bounds == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve bounds from the position args dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve bounds from the position args dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } // If py_bounds is a dict, we have no snapshot boundaries other than the printer volume - if (PyDict_Check(py_bounds)<1) + if (PyDict_Check(py_bounds) < 1) { octolapse_log( - octolapse_log::GCODE_PARSER, octolapse_log::INFO, + octolapse_log::GCODE_PARSER, octolapse_log::DEBUG, "No snapshot restrictions set." ); args->is_bound_ = false; @@ -827,60 +841,60 @@ static bool ParsePositionArgs(PyObject *py_args, gcode_position_args *args) { args->is_bound_ = true; octolapse_log( - octolapse_log::GCODE_PARSER, octolapse_log::INFO, + octolapse_log::GCODE_PARSER, octolapse_log::INFO, "Snapshot restrictions set." ); - PyObject * py_snapshot_x_min = PyDict_GetItemString(py_bounds, "min_x"); + PyObject* py_snapshot_x_min = PyDict_GetItemString(py_bounds, "min_x"); if (py_snapshot_x_min == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve min_x from the bounds dict."); + std::string message = "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve min_x from the bounds dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->snapshot_x_min = PyFloatOrInt_AsDouble(py_snapshot_x_min); - PyObject * py_snapshot_x_max = PyDict_GetItemString(py_bounds, "max_x"); + PyObject* py_snapshot_x_max = PyDict_GetItemString(py_bounds, "max_x"); if (py_snapshot_x_max == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve max_x from the bounds dict."); + std::string message = "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve max_x from the bounds dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->snapshot_x_max = PyFloatOrInt_AsDouble(py_snapshot_x_max); - PyObject * py_snapshot_y_min = PyDict_GetItemString(py_bounds, "min_y"); + PyObject* py_snapshot_y_min = PyDict_GetItemString(py_bounds, "min_y"); if (py_snapshot_y_min == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve min_y from the bounds dict."); + std::string message = "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve min_y from the bounds dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->snapshot_y_min = PyFloatOrInt_AsDouble(py_snapshot_y_min); - PyObject * py_snapshot_y_max = PyDict_GetItemString(py_bounds, "max_y"); + PyObject* py_snapshot_y_max = PyDict_GetItemString(py_bounds, "max_y"); if (py_snapshot_y_max == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve max_y from the bounds dict."); + std::string message = "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve max_y from the bounds dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->snapshot_y_max = PyFloatOrInt_AsDouble(py_snapshot_y_max); - PyObject * py_snapshot_z_min = PyDict_GetItemString(py_bounds, "min_z"); + PyObject* py_snapshot_z_min = PyDict_GetItemString(py_bounds, "min_z"); if (py_snapshot_z_min == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve min_z from the bounds dict."); + std::string message = "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve min_z from the bounds dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->snapshot_z_min = PyFloatOrInt_AsDouble(py_snapshot_z_min); - PyObject * py_snapshot_z_max = PyDict_GetItemString(py_bounds, "max_z"); + PyObject* py_snapshot_z_max = PyDict_GetItemString(py_bounds, "max_z"); if (py_snapshot_z_max == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve max_z from the bounds dict."); + std::string message = "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve max_z from the bounds dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->snapshot_z_max = PyFloatOrInt_AsDouble(py_snapshot_z_max); @@ -888,109 +902,118 @@ static bool ParsePositionArgs(PyObject *py_args, gcode_position_args *args) std::stringstream stream; stream << "Bounds - " << - "X:(" << utilities::to_string(args->snapshot_x_min) << "," << utilities::to_string(args->snapshot_x_max) << ") " << - "Y:(" << utilities::to_string(args->snapshot_y_min) << "," << utilities::to_string(args->snapshot_y_max) << ") " << + "X:(" << utilities::to_string(args->snapshot_x_min) << "," << utilities::to_string(args->snapshot_x_max) << ") " + << + "Y:(" << utilities::to_string(args->snapshot_y_min) << "," << utilities::to_string(args->snapshot_y_max) << ") " + << "Z:(" << utilities::to_string(args->snapshot_z_min) << "," << utilities::to_string(args->snapshot_z_max) << ") "; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::INFO, stream.str()); + octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::DEBUG, stream.str()); } +#pragma region location_detection_commands // Get LocationDetection Commands - PyObject * py_location_detection_commands = PyDict_GetItemString(py_args, "location_detection_commands"); + PyObject* py_location_detection_commands = PyDict_GetItemString(py_args, "location_detection_commands"); if (py_location_detection_commands == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve py_location_detection_commands from the position args dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve location_detection_commands from the bounds dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } // Extract the elements from the location detection command list pyobject const int listSize = PyList_Size(py_location_detection_commands); if (listSize < 0) { - std::string message = "Unable to build position arguments, LocationDetectionCommands is not a list."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to build position arguments, LocationDetectionCommands is not a list."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } - for (int index = 0; index < listSize; index++) { - PyObject *pyListItem = PyList_GetItem(py_location_detection_commands, index); + for (int index = 0; index < listSize; index++) + { + PyObject* pyListItem = PyList_GetItem(py_location_detection_commands, index); if (pyListItem == NULL) { - std::string message = "Could not extract a list item from index from the location detection commands."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Could not extract a list item from index from the location detection commands."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } - if (!PyUnicode_SafeCheck(pyListItem)) { - std::string message = "Argument 16 (location_detection_commands) must be a list of strings."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, message.c_str()); - + if (!PyUnicode_SafeCheck(pyListItem)) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Argument 16 (location_detection_commands) must be a list of strings."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } std::string command = PyUnicode_SafeAsString(pyListItem); args->location_detection_commands.push_back(command); } - +#pragma endregion Parse the list of location detection commands + // xyz_axis_default_mode - PyObject * py_xyz_axis_default_mode = PyDict_GetItemString(py_args, "xyz_axis_default_mode"); + PyObject* py_xyz_axis_default_mode = PyDict_GetItemString(py_args, "xyz_axis_default_mode"); if (py_xyz_axis_default_mode == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve xyz_axis_default_mode from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve xyz_axis_default_mode from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->xyz_axis_default_mode = PyUnicode_SafeAsString(py_xyz_axis_default_mode); // e_axis_default_mode - PyObject * py_e_axis_default_mode = PyDict_GetItemString(py_args, "e_axis_default_mode"); + PyObject* py_e_axis_default_mode = PyDict_GetItemString(py_args, "e_axis_default_mode"); if (py_e_axis_default_mode == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve e_axis_default_mode from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve e_axis_default_mode from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->e_axis_default_mode = PyUnicode_SafeAsString(py_e_axis_default_mode); // units_default - PyObject * py_units_default = PyDict_GetItemString(py_args, "units_default"); + PyObject* py_units_default = PyDict_GetItemString(py_args, "units_default"); if (py_units_default == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve units_default from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve units_default from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->units_default = PyUnicode_SafeAsString(py_units_default); // autodetect_position - PyObject * py_autodetect_position = PyDict_GetItemString(py_args, "autodetect_position"); + PyObject* py_autodetect_position = PyDict_GetItemString(py_args, "autodetect_position"); if (py_autodetect_position == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve autodetect_position from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve autodetect_position from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->autodetect_position = PyLong_AsLong(py_autodetect_position) > 0; // Here is the full structure of the position args: // get the volume py object - PyObject * py_home = PyDict_GetItemString(py_args, "home_position"); + PyObject* py_home = PyDict_GetItemString(py_args, "home_position"); if (py_home == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve py_home from the position args dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve home_position from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } // home_x - PyObject * py_home_x = PyDict_GetItemString(py_home, "home_x"); + PyObject* py_home_x = PyDict_GetItemString(py_home, "home_x"); if (py_home_x == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve home_x from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve home_x from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } if (py_home_x == Py_None) @@ -1001,13 +1024,14 @@ static bool ParsePositionArgs(PyObject *py_args, gcode_position_args *args) { args->home_x = PyFloatOrInt_AsDouble(py_home_x); } - + // home_y - PyObject * py_home_y = PyDict_GetItemString(py_home, "home_y"); + PyObject* py_home_y = PyDict_GetItemString(py_home, "home_y"); if (py_home_y == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve home_y from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve home_y from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } if (py_home_y == Py_None) @@ -1020,11 +1044,12 @@ static bool ParsePositionArgs(PyObject *py_args, gcode_position_args *args) } // home_z - PyObject * py_home_z = PyDict_GetItemString(py_home, "home_z"); + PyObject* py_home_z = PyDict_GetItemString(py_home, "home_z"); if (py_home_z == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve home_z from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve home_z from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } if (py_home_z == Py_None) @@ -1035,217 +1060,526 @@ static bool ParsePositionArgs(PyObject *py_args, gcode_position_args *args) { args->home_z = PyFloatOrInt_AsDouble(py_home_z); } - // get the slicer settings dictionary - PyObject * py_slicer_settings_dict = PyDict_GetItemString(py_args, "slicer_settings"); - if (py_slicer_settings_dict == NULL) + // num_extruders + PyObject* py_num_extruders = PyDict_GetItemString(py_args, "num_extruders"); + if (py_num_extruders == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve num_extruders from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + args->set_num_extruders(PyLong_AsLong(py_num_extruders)); + + // py_shared_extruder + PyObject* py_shared_extruder = PyDict_GetItemString(py_args, "shared_extruder"); + if (py_shared_extruder == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve shared_extruder from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + args->shared_extruder = PyLong_AsLong(py_shared_extruder) > 0; + + // zero_based_extruder + PyObject* py_zero_based_extruder = PyDict_GetItemString(py_args, "zero_based_extruder"); + if (py_zero_based_extruder == NULL) { - PyErr_Print(); - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::ERROR, "Unable to retrieve slicer settings from the position dict."); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve slicer settings from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve zero_based_extruder from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } - - // retraction_length - PyObject * py_retraction_length = PyDict_GetItemString(py_slicer_settings_dict, "retraction_length"); - if (py_retraction_length == NULL) + args->zero_based_extruder = PyLong_AsLong(py_zero_based_extruder) > 0; + + // default extruder + PyObject* py_default_extruder_index = PyDict_GetItemString(py_args, "default_extruder_index"); + if (py_default_extruder_index == NULL) { - PyErr_Print(); - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::ERROR, "Unable to retrieve retraction_length from the slicer settings dict."); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve retraction_length from the slicer settings dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve default_extruder_index from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } - args->retraction_length = PyFloatOrInt_AsDouble(py_retraction_length); + // The zero based default extruder + args->default_extruder = PyLong_AsLong(py_default_extruder_index); - // z_lift_height - PyObject * py_z_lift_height = PyDict_GetItemString(py_slicer_settings_dict, "z_lift_height"); - if (py_z_lift_height == NULL) + // get the slicer settings dictionary + PyObject* py_slicer_settings_dict = PyDict_GetItemString(py_args, "slicer_settings"); + if (py_slicer_settings_dict == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve z_lift_height from the slicer settings dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve slicer_settings from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } - args->z_lift_height = PyFloatOrInt_AsDouble(py_z_lift_height); + +#pragma region Extract extruder objects + PyObject* py_extruders = PyDict_GetItemString(py_slicer_settings_dict, "extruders"); + if (py_extruders == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve extruders list from the slicer settings dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + if (!PyList_Check(py_extruders)) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - The extruders object in the slicer settings dict is not a list."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + const int extruder_list_size = PyList_Size(py_extruders); + //std::cout << "Found " << extruder_list_size << " extruders.\r\n"; + // make sure there is at lest one item in the list + if (extruder_list_size < 1) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to build a list of extruders from the slicer settings dict arguments. There are no extruders in the list."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + if (extruder_list_size < args->num_extruders) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Too few extruders were detected. There must be at least as many extruders in the list the num_extruders variable."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + for (int index = 0; index < extruder_list_size; index++) + { + //std::cout << "Extracting the current extruder #" << index << ".\r\n"; + PyObject* py_extruder = PyList_GetItem(py_extruders, index); + if (py_extruder == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Could not extract an extruder from index from the extruders list."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + // Extract the z_lift_height from the current extruder + PyObject* py_z_lift_height = PyDict_GetItemString(py_extruder, "z_lift_height"); + if (py_z_lift_height == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve z_lift_height list from the current extruder."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + + if (!(PyFloatLongOrInt_Check(py_z_lift_height) || py_z_lift_height == Py_None)) + { + std::string message = "GcodePositionProcessor.ParsePositionArgs - The z_lift_height object must a float or int."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + + if (py_z_lift_height == Py_None) + { + args->z_lift_heights[index] = 0; + } + else + { + double height = PyFloatOrInt_AsDouble(py_z_lift_height); + args->z_lift_heights[index] = height; + } + + // Extract the retraction_length from the current extruder + PyObject* py_retraction_length = PyDict_GetItemString(py_extruder, "retraction_length"); + if (py_retraction_length == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve retraction_length list from the current extruder."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + if (!(PyFloatLongOrInt_Check(py_retraction_length) || py_retraction_length == Py_None)) + { + std::string message = "GcodePositionProcessor.ParsePositionArgs - The z_lift_height object must a float or int."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + if (py_z_lift_height == Py_None) + { + args->retraction_lengths[index] = 0; + } + else + { + double length = PyFloatOrInt_AsDouble(py_retraction_length); + args->retraction_lengths[index] = length; + } + } + +#pragma endregion Parse extruder objects + +#pragma region Extract firmware extruder offsets from the printer settings + // Only extract extruder offsets if there is more than one extruder. + if (args->num_extruders > 1) + { + PyObject* py_extruder_offsets = PyDict_GetItemString(py_args, "extruder_offsets"); + if (py_extruder_offsets == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve extruder_offsets from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + if (!PyList_Check(py_extruder_offsets)) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - The extruder_offsets object in the position dict is not a list."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + + int extruder_offsets_list_size = PyList_Size(py_extruder_offsets); + //std::cout << "Found " << extruder_list_size << " extruders.\r\n"; + // make sure there is at lest one item in the list + if (extruder_offsets_list_size < args->num_extruders && !args->shared_extruder) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Too few extruder offsets were detected. There must be at least as many extruder offsets in the list as the num_extruders variable when not using a shared extruder."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + if (extruder_offsets_list_size > args->num_extruders && !args->shared_extruder) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Too many extruder offsets were detected. There can only be as many extruder offsets in the list as the num_extruders variable when not using a shared extruder."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + if (extruder_offsets_list_size > 0 && args->shared_extruder) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Firmware extruder offset values are not allowed when using shared extruders. Setting offsets to 0."; + octolapse_log(octolapse_log::WARNING, octolapse_log::GCODE_POSITION, message); + extruder_offsets_list_size = 0; + } + + for (int index = 0; index < extruder_offsets_list_size; index++) + { + std::cout << "Extracting the current extruder offset#" << index << ".\r\n"; + PyObject* py_extruder_offset = PyList_GetItem(py_extruder_offsets, index); + if (py_extruder_offset == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Could not extract an extruder offset by index from the extruder_offsets list."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + // Extract the x_firmware_offset from the current extruder (called x) + PyObject* py_extruder_offset_x = PyDict_GetItemString(py_extruder_offset, "x"); + if (py_extruder_offset_x == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve an x offset from a list item in the the extruder_offsets list."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + std::cout << "Checking x offset value.\r\n"; + if (!(PyFloatLongOrInt_Check(py_extruder_offset_x) || py_extruder_offset_x == Py_None)) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - The extruder_offset.x object must a float or int."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + std::cout << "Assigning x offset value.\r\n"; + if (py_extruder_offset_x == Py_None) + { + args->x_firmware_offsets[index] = 0; + } + else + { + double x = PyFloatOrInt_AsDouble(py_extruder_offset_x); + args->x_firmware_offsets[index] = x; + } + // Extract the x_firmware_offset from the current extruder (called x) + PyObject* py_extruder_offset_y = PyDict_GetItemString(py_extruder_offset, "y"); + if (py_extruder_offset_y == NULL) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve an y offset from a list item in the the extruder_offsets list."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + std::cout << "Checking y offset value.\r\n"; + if (!(PyFloatLongOrInt_Check(py_extruder_offset_y) || py_extruder_offset_y == Py_None)) + { + std::string message = + "GcodePositionProcessor.ParsePositionArgs - The extruder_offset.y object must a float or int."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return false; + } + std::cout << "Assigning y offset value.\r\n"; + if (py_extruder_offset_y == Py_None) + { + args->y_firmware_offsets[index] = 0; + } + else + { + double y = PyFloatOrInt_AsDouble(py_extruder_offset_y); + args->y_firmware_offsets[index] = y; + } + } + } + +#pragma endregion Extract firmware extruder offsets from the printer settings // priming_height - PyObject * py_priming_height = PyDict_GetItemString(py_args, "priming_height"); + PyObject* py_priming_height = PyDict_GetItemString(py_args, "priming_height"); if (py_priming_height == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve priming_height from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve priming_height from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->priming_height = PyFloatOrInt_AsDouble(py_priming_height); // minimum_layer_height - PyObject * py_minimum_layer_height = PyDict_GetItemString(py_args, "minimum_layer_height"); + PyObject* py_minimum_layer_height = PyDict_GetItemString(py_args, "minimum_layer_height"); if (py_minimum_layer_height == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve minimum_layer_height from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve minimum_layer_height from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->minimum_layer_height = PyFloatOrInt_AsDouble(py_minimum_layer_height); // g90_influences_extruder - PyObject * py_g90_influences_extruder = PyDict_GetItemString(py_args, "g90_influences_extruder"); + PyObject* py_g90_influences_extruder = PyDict_GetItemString(py_args, "g90_influences_extruder"); if (py_g90_influences_extruder == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve g90_influences_extruder from the position dict."); + std::string message = + "GcodePositionProcessor.ParsePositionArgs - Unable to retrieve g90_influences_extruder from the position dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); return false; } args->g90_influences_extruder = PyLong_AsLong(py_g90_influences_extruder) > 0; - + return true; } -static bool ParseStabilizationArgs(PyObject *py_args, stabilization_args* args) +static bool ParseStabilizationArgs(PyObject* py_args, stabilization_args* args, PyObject** py_progress_callback, + PyObject** py_snapshot_position_callback) { octolapse_log( - octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, + octolapse_log::SNAPSHOT_PLAN, octolapse_log::DEBUG, "Parsing Stabilization Args." ); //std::cout << "Parsing Stabilization Args.\r\n"; // gcode_generator - PyObject * py_gcode_generator = PyDict_GetItemString(py_args, "gcode_generator"); + PyObject* py_gcode_generator = PyDict_GetItemString(py_args, "gcode_generator"); if (py_gcode_generator == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve gcode_generator from the smart layer stabilization args."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve gcode_generator from the smart layer stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } // Need to incref py_gcode_generator, borrowed ref and we're holding it! - Py_INCREF(py_gcode_generator); - args->py_gcode_generator = py_gcode_generator; + // This should no longer be true... + //Py_INCREF(py_gcode_generator); // extract the get_snapshot_position callback - PyObject * py_get_snapshot_position_callback = PyObject_GetAttrString(py_gcode_generator, "get_snapshot_position"); + PyObject* py_get_snapshot_position_callback = PyObject_GetAttrString(py_gcode_generator, "get_snapshot_position"); if (py_get_snapshot_position_callback == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve get_snapshot_position function from the gcode_generator object."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve get_snapshot_position function from the gcode_generator object."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } // make sure it is callable - if (!PyCallable_Check(py_get_snapshot_position_callback)) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "The get_snapshot_position attribute must be callable."); - return NULL; + if (!PyCallable_Check(py_get_snapshot_position_callback)) + { + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve get_snapshot_position function from the gcode_generator object."; + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::ERROR, message); + return false; } // py_get_snapshot_position_callback is a new reference, no reason to incref - args->py_get_snapshot_position_callback = py_get_snapshot_position_callback; + *py_snapshot_position_callback = py_get_snapshot_position_callback; + // on_progress_received + PyObject* py_on_progress_received = PyDict_GetItemString(py_args, "on_progress_received"); + if (py_on_progress_received == NULL) + { + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve on_progress_received from the stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return false; + } + // need to incref this so it doesn't vanish later (borrowed reference we are saving) + Py_IncRef(py_on_progress_received); + *py_progress_callback = py_on_progress_received; + // height_increment - PyObject * py_height_increment = PyDict_GetItemString(py_args, "height_increment"); + PyObject* py_height_increment = PyDict_GetItemString(py_args, "height_increment"); if (py_height_increment == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve height_increment from the stabilization args."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve height_increment from the stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } args->height_increment = PyFloatOrInt_AsDouble(py_height_increment); // x_axis_stabilization_disabled - PyObject * py_x_stabilization_disabled = PyDict_GetItemString(py_args, "x_stabilization_disabled"); + PyObject* py_x_stabilization_disabled = PyDict_GetItemString(py_args, "x_stabilization_disabled"); if (py_x_stabilization_disabled == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve x_stabilization_disabled from the stabilization args."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve x_stabilization_disabled from the stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } args->x_stabilization_disabled = PyLong_AsLong(py_x_stabilization_disabled) > 0; // x_axis_stabilization_disabled - PyObject * py_y_stabilization_disabled = PyDict_GetItemString(py_args, "y_stabilization_disabled"); + PyObject* py_y_stabilization_disabled = PyDict_GetItemString(py_args, "y_stabilization_disabled"); if (py_x_stabilization_disabled == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve y_stabilization_disabled from the stabilization args."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve y_stabilization_disabled from the stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } args->y_stabilization_disabled = PyLong_AsLong(py_y_stabilization_disabled) > 0; + // allow_snapshot_commands + PyObject* py_allow_snapshot_commands = PyDict_GetItemString(py_args, "allow_snapshot_commands"); + if (py_allow_snapshot_commands == NULL) + { + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve allow_snapshot_commands from the stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return false; + } + args->allow_snapshot_commands = PyLong_AsLong(py_allow_snapshot_commands) > 0; + // notification_period_seconds - PyObject * py_notification_period_seconds = PyDict_GetItemString(py_args, "notification_period_seconds"); + PyObject* py_notification_period_seconds = PyDict_GetItemString(py_args, "notification_period_seconds"); if (py_notification_period_seconds == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve notification_period_seconds from the stabilization args."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve notification_period_seconds from the stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } args->notification_period_seconds = PyFloatOrInt_AsDouble(py_notification_period_seconds); - // on_progress_received - PyObject * py_on_progress_received = PyDict_GetItemString(py_args, "on_progress_received"); - if (py_on_progress_received == NULL) + + // file_path + PyObject* py_dict_key = PyString_SafeFromString("file_path"); + PyObject* py_dict_item = PyDict_GetItem(py_args, py_dict_key); + if (py_dict_item == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve on_progress_received from the stabilization args."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to retrieve the file_path from the stabilization args dict."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } - // need to incref this so it doesn't vanish later (borrowed reference we are saving) - Py_IncRef(py_on_progress_received); - args->py_on_progress_received = py_on_progress_received; + Py_DecRef(py_dict_key); + /* + Py_UNICODE* py_dict_item_unicode = PyUnicode_AsUnicode(py_dict_item); + //PyObject* py_file_path = PyDict_GetItemString(py_args, "file_path"); + if (py_dict_item_unicode == NULL) + { + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs - Unable to convert the file_path dict item to a Py_UNICODE ."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return false; + }*/ - // file_path - PyObject * py_file_path = PyDict_GetItemString(py_args, "file_path"); - if (py_file_path == NULL) + args->file_path = PyUnicode_SafeAsString(py_dict_item); + + args->snapshot_command.clear(); + + // Extract the snapshot_command + PyObject* py_snapshot_command = PyDict_GetItemString(py_args, "snapshot_command"); + if (py_snapshot_command == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve file_path from the stabilization args."); + std::string message = + "ParseStabilizationArgs - Unable to retrieve snapshot_command from the stabilization args dict."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } - args->file_path = PyUnicode_SafeAsString(py_file_path); + args->snapshot_command_text = PyUnicode_SafeAsString(py_snapshot_command); + + gcode_parser parser; + parser.try_parse_gcode(args->snapshot_command_text.c_str(), args->snapshot_command); + if (args->snapshot_command.gcode.empty()) + { + std::string message = + "ParseStabilizationArgs - No alternative snapshot command was provided, using default command only."; + message += args->snapshot_command_text; + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, message); + } + else + { + std::string message = "ParseStabilizationArgs - Alternative snapshot gcode ("; + message += args->snapshot_command.gcode; + message += ") parsed successfully."; + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, message); + } + //std::cout << "Stabilization Args parsed successfully.\r\n"; return true; } -static bool ParseStabilizationArgs_SmartLayer(PyObject *py_args, smart_layer_args* args) +static bool ParseStabilizationArgs_SmartLayer(PyObject* py_args, smart_layer_args* args) { octolapse_log( octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, - "Parsing Stabilization Args." + "Parsing Smart Layer Stabilization Args." ); //std::cout << "Parsing smart layer args.\r\n"; // Extract trigger_on_extrude - PyObject * py_trigger_type = PyDict_GetItemString(py_args, "trigger_type"); + PyObject* py_trigger_type = PyDict_GetItemString(py_args, "trigger_type"); if (py_trigger_type == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve trigger_type from the smart layer trigger stabilization args."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs_SmartLayer - Unable to retrieve trigger_type from the smart layer trigger stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } - args->smart_layer_trigger_type = static_cast(PyLong_AsLong(py_trigger_type)); + args->smart_layer_trigger_type = static_cast(PyLong_AsLong(py_trigger_type)); - // Extract speed_threshold - PyObject * py_speed_threshold = PyDict_GetItemString(py_args, "speed_threshold"); - if (py_speed_threshold == NULL) + PyObject* py_snap_to_print_high_quality = PyDict_GetItemString(py_args, "snap_to_print_high_quality"); + if (py_snap_to_print_high_quality == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve speed_threshold from the smart layer trigger stabilization args."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs_SmartLayer - Unable to retrieve snap_to_print_high_quality from the smart layer trigger stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } - args->speed_threshold = PyFloatOrInt_AsDouble(py_speed_threshold); + args->snap_to_print_high_quality = PyLong_AsLong(py_snap_to_print_high_quality) > 0; - // Extract speed_threshold - PyObject * py_distance_threshold_percent = PyDict_GetItemString(py_args, "distance_threshold_percent"); - if (py_distance_threshold_percent == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve distance_threshold_percent from the smart layer trigger stabilization args."); - return false; - } - args->distance_threshold_percent = PyFloatOrInt_AsDouble(py_distance_threshold_percent); - //std::cout << "Smart layer args parsed successfully.\r\n"; - // snap_to_print - PyObject * py_snap_to_print = PyDict_GetItemString(py_args, "snap_to_print"); - if (py_snap_to_print == NULL) + PyObject* py_snap_to_print_smooth = PyDict_GetItemString(py_args, "snap_to_print_smooth"); + if (py_snap_to_print_smooth == NULL) { - PyErr_Print(); - PyErr_SetString(PyExc_TypeError, "Unable to retrieve snap_to_print from the position args dict."); + std::string message = + "GcodePositionProcessor.ParseStabilizationArgs_SmartLayer - Unable to retrieve snap_to_print_smooth from the smart layer trigger stabilization args."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); return false; } - args->snap_to_print = PyLong_AsLong(py_snap_to_print) > 0; + args->snap_to_print_smooth = PyLong_AsLong(py_snap_to_print_smooth) > 0; + + return true; +} + +static bool ParseStabilizationArgs_SmartGcode(PyObject* py_args, smart_gcode_args* args) +{ + octolapse_log( + octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, + "Parsing Smart Gcode Stabilization Args." + ); diff --git a/octoprint_octolapse/data/lib/c/gcode_position_processor.h b/octoprint_octolapse/data/lib/c/gcode_position_processor.h index d7762236..ef356fdb 100644 --- a/octoprint_octolapse/data/lib/c/gcode_position_processor.h +++ b/octoprint_octolapse/data/lib/c/gcode_position_processor.h @@ -23,9 +23,9 @@ #ifndef GcodePositionProcessor_H #define GcodePositionProcessor_H #ifdef _DEBUG -#undef _DEBUG +//#undef _DEBUG #include -#define _DEBUG +//python311_d.lib #else #include #endif @@ -34,33 +34,39 @@ #include "gcode_parser.h" #include "stabilization.h" #include "stabilization_smart_layer.h" -namespace gpp { +#include "stabilization_smart_gcode.h" + +namespace gpp +{ static std::map gcode_positions; static gcode_parser* parser; } -extern "C" -{ -#if PY_MAJOR_VERSION >= 3 +extern "C" { + PyMODINIT_FUNC PyInit_GcodePositionProcessor(void); -#else - extern "C" void initGcodePositionProcessor(void); -#endif - static PyObject* Initialize(PyObject* self, PyObject *args); - static PyObject* Undo(PyObject* self, PyObject *args); - static PyObject* Update(PyObject* self, PyObject *args); - static PyObject* UpdatePosition(PyObject* self, PyObject *args); - static PyObject* Parse(PyObject* self, PyObject *args); - static PyObject* GetCurrentPositionTuple(PyObject* self, PyObject *args); - static PyObject* GetCurrentPositionDict(PyObject* self, PyObject *args); - static PyObject* GetPreviousPositionTuple(PyObject* self, PyObject *args); - static PyObject* GetPreviousPositionDict(PyObject* self, PyObject *args); - static PyObject* GetSnapshotPlans_SmartLayer(PyObject *self, PyObject *args); + + static PyObject* Initialize(PyObject* self, PyObject* args); + static PyObject* Undo(PyObject* self, PyObject* args); + static PyObject* Update(PyObject* self, PyObject* args); + static PyObject* UpdatePosition(PyObject* self, PyObject* args); + static PyObject* Parse(PyObject* self, PyObject* args); + static PyObject* GetCurrentPositionTuple(PyObject* self, PyObject* args); + static PyObject* GetCurrentPositionDict(PyObject* self, PyObject* args); + static PyObject* GetPreviousPositionTuple(PyObject* self, PyObject* args); + static PyObject* GetPreviousPositionDict(PyObject* self, PyObject* args); + static PyObject* GetSnapshotPlans_SmartLayer(PyObject* self, PyObject* args); + static PyObject* GetSnapshotPlans_SmartGcode(PyObject* self, PyObject* args); } -static bool ParsePositionArgs(PyObject *py_args, gcode_position_args *args); -static bool ParseStabilizationArgs(PyObject *py_args, stabilization_args* args); -static bool ParseStabilizationArgs_SmartLayer(PyObject *py_args, smart_layer_args* args); -static bool ExecuteStabilizationProgressCallback(PyObject* progress_callback, const double percent_complete, const double seconds_elapsed, const double estimated_seconds_remaining, const long gcodes_processed, const long lines_processed); -static bool ExecuteGetSnapshotPositionCallback(PyObject* py_get_snapshot_position_callback, double x_initial, double y_initial, double* x_result, double* y_result); -#endif +static bool ParsePositionArgs(PyObject* py_args, gcode_position_args* args); +static bool ParseStabilizationArgs(PyObject* py_args, stabilization_args* args, PyObject** p_py_progress_callback, + PyObject** p_py_snapshot_position_callback); +static bool ParseStabilizationArgs_SmartLayer(PyObject* py_args, smart_layer_args* args); +static bool ParseStabilizationArgs_SmartGcode(PyObject* py_args, smart_gcode_args* args); +static bool ExecuteStabilizationProgressCallback(PyObject* progress_callback, const double percent_complete, + const double seconds_elapsed, const double estimated_seconds_remaining, + const int gcodes_processed, const int lines_processed); +static bool ExecuteGetSnapshotPositionCallback(PyObject* py_get_snapshot_position_callback, double x_initial, + double y_initial, double& x_result, double& y_result); +#endif diff --git a/octoprint_octolapse/data/lib/c/logging.cpp b/octoprint_octolapse/data/lib/c/logging.cpp index 82360e0d..8b5956ca 100644 --- a/octoprint_octolapse/data/lib/c/logging.cpp +++ b/octoprint_octolapse/data/lib/c/logging.cpp @@ -1,8 +1,8 @@ // Todo: Convert to C++ #ifdef _DEBUG -#undef _DEBUG +//#undef _DEBUG #include -#define _DEBUG +//python311_d.lib #else #include #endif @@ -12,214 +12,287 @@ static bool octolapse_loggers_created = false; static bool check_log_levels_real_time = true; -static PyObject *py_logging_module = NULL; -static PyObject *py_logging_configurator_name = NULL; -static PyObject *py_logging_configurator = NULL; -static PyObject *py_octolapse_gcode_parser_logger = NULL; +static PyObject* py_logging_module = NULL; +static PyObject* py_logging_configurator_name = NULL; +static PyObject* py_logging_configurator = NULL; +static PyObject* py_octolapse_gcode_parser_logger = NULL; static long gcode_parser_log_level = 0; -static PyObject *py_octolapse_gcode_position_logger = NULL; +static PyObject* py_octolapse_gcode_position_logger = NULL; static long gcode_position_log_level = 0; -static PyObject *py_octolapse_snapshot_plan_logger = NULL; +static PyObject* py_octolapse_snapshot_plan_logger = NULL; static long snapshot_plan_log_level = 0; -static PyObject *py_info_function_name = NULL; -static PyObject *py_warn_function_name = NULL; -static PyObject *py_error_function_name = NULL; -static PyObject *py_debug_function_name = NULL; -static PyObject *py_verbose_function_name = NULL; -static PyObject *py_critical_function_name = NULL; -static PyObject *py_get_effective_level_function_name = NULL; +static PyObject* py_info_function_name = NULL; +static PyObject* py_warn_function_name = NULL; +static PyObject* py_error_function_name = NULL; +static PyObject* py_debug_function_name = NULL; +static PyObject* py_verbose_function_name = NULL; +static PyObject* py_critical_function_name = NULL; +static PyObject* py_get_effective_level_function_name = NULL; void octolapse_initialize_loggers() { - // Create all of the objects necessary for logging - // Import the octolapse.log module - py_logging_module = PyImport_ImportModuleNoBlock("octoprint_octolapse.log"); - if (py_logging_module == NULL) - { - PyErr_SetString(PyExc_ImportError, "Could not import module 'octolapse.log'."); - return; - } - - // Get the logging configurator attribute string - py_logging_configurator_name = PyObject_GetAttrString(py_logging_module, "LoggingConfigurator"); - if (py_logging_configurator_name == NULL) - { - PyErr_SetString(PyExc_ImportError, "Could not acquire the LoggingConfigurator attribute string."); - return; - } - - // Create a logging configurator - PyGILState_STATE gstate = PyGILState_Ensure(); - py_logging_configurator = PyObject_CallObject(py_logging_configurator_name, NULL); - PyGILState_Release(gstate); - - if (py_logging_configurator == NULL) - { - PyErr_SetString(PyExc_ImportError, "Could not create a new instance of LoggingConfigurator."); - return; - } - - // Create the gcode_parser logging object - py_octolapse_gcode_parser_logger = PyObject_CallMethod(py_logging_configurator, (char*)"get_logger", (char *)"s", "octoprint_octolapse.gcode_parser"); - if (py_octolapse_gcode_parser_logger == NULL) - { - PyErr_SetString(PyExc_ImportError, "Could not create the octolapse.gcode_parser child logger."); - return; - } - - // Create the gcode_position logging object - py_octolapse_gcode_position_logger = PyObject_CallMethod(py_logging_configurator, (char*)"get_logger", (char *)"s", "octoprint_octolapse.gcode_position"); - if (py_octolapse_gcode_position_logger == NULL) - { - PyErr_SetString(PyExc_ImportError, "Could not create the octolapse.gcode_position child logger."); - return; - } - - // Create the stabilization logging object - py_octolapse_snapshot_plan_logger = PyObject_CallMethod(py_logging_configurator, (char*)"get_logger", (char *)"s", "octoprint_octolapse.snapshot_plan"); - if (py_octolapse_snapshot_plan_logger == NULL) - { - PyErr_SetString(PyExc_ImportError, "Could not create the octolapse.snapshot_plan child logger."); - return; - } - - // create the function name py objects - py_info_function_name = PyString_SafeFromString("info"); - py_warn_function_name = PyString_SafeFromString("warn"); - py_error_function_name = PyString_SafeFromString("error"); - py_debug_function_name = PyString_SafeFromString("debug"); - py_verbose_function_name = PyString_SafeFromString("verbose"); - py_critical_function_name = PyString_SafeFromString("critical"); - py_get_effective_level_function_name = PyString_SafeFromString("getEffectiveLevel"); - octolapse_loggers_created = true; + // Create all of the objects necessary for logging + // Import the octolapse.log module + py_logging_module = PyImport_ImportModuleNoBlock("octoprint_octolapse.log"); + if (py_logging_module == NULL) + { + PyErr_SetString(PyExc_ImportError, "Could not import module 'octolapse.log'."); + return; + } + // Get the logging configurator attribute string + py_logging_configurator_name = PyObject_GetAttrString(py_logging_module, "LoggingConfigurator"); + if (py_logging_configurator_name == NULL) + { + PyErr_SetString(PyExc_ImportError, "Could not acquire the LoggingConfigurator attribute string."); + return; + } + + // Create a logging configurator + PyGILState_STATE gstate = PyGILState_Ensure(); + py_logging_configurator = PyObject_CallObject(py_logging_configurator_name, NULL); + PyGILState_Release(gstate); + + if (py_logging_configurator == NULL) + { + PyErr_SetString(PyExc_ImportError, "Could not create a new instance of LoggingConfigurator."); + return; + } + + // Create the gcode_parser logging object + py_octolapse_gcode_parser_logger = PyObject_CallMethod(py_logging_configurator, (char*)"get_logger", (char *)"s", + "octoprint_octolapse.gcode_parser"); + if (py_octolapse_gcode_parser_logger == NULL) + { + PyErr_SetString(PyExc_ImportError, "Could not create the octolapse.gcode_parser child logger."); + return; + } + + // Create the gcode_position logging object + py_octolapse_gcode_position_logger = PyObject_CallMethod(py_logging_configurator, (char*)"get_logger", (char *)"s", + "octoprint_octolapse.gcode_position"); + if (py_octolapse_gcode_position_logger == NULL) + { + PyErr_SetString(PyExc_ImportError, "Could not create the octolapse.gcode_position child logger."); + return; + } + + // Create the stabilization logging object + py_octolapse_snapshot_plan_logger = PyObject_CallMethod(py_logging_configurator, (char*)"get_logger", (char *)"s", + "octoprint_octolapse.snapshot_plan"); + if (py_octolapse_snapshot_plan_logger == NULL) + { + PyErr_SetString(PyExc_ImportError, "Could not create the octolapse.snapshot_plan child logger."); + return; + } + + // create the function name py objects + py_info_function_name = PyString_SafeFromString("info"); + py_warn_function_name = PyString_SafeFromString("warn"); + py_error_function_name = PyString_SafeFromString("error"); + py_debug_function_name = PyString_SafeFromString("debug"); + py_verbose_function_name = PyString_SafeFromString("verbose"); + py_critical_function_name = PyString_SafeFromString("critical"); + py_get_effective_level_function_name = PyString_SafeFromString("getEffectiveLevel"); + octolapse_loggers_created = true; } void set_internal_log_levels(bool check_real_time) { - check_log_levels_real_time = check_real_time; - if(!check_log_levels_real_time) - { - - PyObject* py_gcode_parser_log_level = PyObject_CallMethodObjArgs(py_octolapse_gcode_parser_logger, py_get_effective_level_function_name, NULL); - if (py_gcode_parser_log_level == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Logging.octolapse_log - Could not retrieve the log level for the gcode parser logger."); - } - gcode_parser_log_level = PyInt_AsLong(py_gcode_parser_log_level); - - PyObject* py_gcode_position_log_level = PyObject_CallMethodObjArgs(py_octolapse_gcode_position_logger, py_get_effective_level_function_name, NULL); - if (py_gcode_position_log_level == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Logging.octolapse_log - Could not retrieve the log level for the gcode position logger."); - } - gcode_position_log_level = PyInt_AsLong(py_gcode_position_log_level); - - PyObject* py_snapshot_plan_log_level = PyObject_CallMethodObjArgs(py_octolapse_snapshot_plan_logger, py_get_effective_level_function_name, NULL); - if (py_snapshot_plan_log_level == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Logging.octolapse_log - Could not retrieve the log level for the snapshot plan logger."); - } - snapshot_plan_log_level = PyInt_AsLong(py_snapshot_plan_log_level); - - Py_XDECREF(py_gcode_parser_log_level); - Py_XDECREF(py_gcode_position_log_level); - Py_XDECREF(py_snapshot_plan_log_level); - } + check_log_levels_real_time = check_real_time; + if (!check_log_levels_real_time) + { + PyObject* py_gcode_parser_log_level = PyObject_CallMethodObjArgs(py_octolapse_gcode_parser_logger, + py_get_effective_level_function_name, NULL); + if (py_gcode_parser_log_level == NULL) + { + PyErr_Print(); + PyErr_SetString(PyExc_ValueError, + "Logging.octolapse_log - Could not retrieve the log level for the gcode parser logger."); + } + gcode_parser_log_level = PyIntOrLong_AsLong(py_gcode_parser_log_level); + + PyObject* py_gcode_position_log_level = PyObject_CallMethodObjArgs(py_octolapse_gcode_position_logger, + py_get_effective_level_function_name, NULL); + if (py_gcode_position_log_level == NULL) + { + PyErr_Print(); + PyErr_SetString(PyExc_ValueError, + "Logging.octolapse_log - Could not retrieve the log level for the gcode position logger."); + } + gcode_position_log_level = PyIntOrLong_AsLong(py_gcode_position_log_level); + + PyObject* py_snapshot_plan_log_level = PyObject_CallMethodObjArgs(py_octolapse_snapshot_plan_logger, + py_get_effective_level_function_name, NULL); + if (py_snapshot_plan_log_level == NULL) + { + PyErr_Print(); + PyErr_SetString(PyExc_ValueError, + "Logging.octolapse_log - Could not retrieve the log level for the snapshot plan logger."); + } + snapshot_plan_log_level = PyIntOrLong_AsLong(py_snapshot_plan_log_level); + + Py_XDECREF(py_gcode_parser_log_level); + Py_XDECREF(py_gcode_position_log_level); + Py_XDECREF(py_snapshot_plan_log_level); + } +} + +bool octolapse_may_be_logged(const int logger_type, const int log_level) +{ + int current_log_level; + switch (logger_type) + { + case octolapse_log::GCODE_PARSER: + current_log_level = gcode_parser_log_level; + break; + case octolapse_log::GCODE_POSITION: + current_log_level = gcode_position_log_level; + break; + case octolapse_log::SNAPSHOT_PLAN: + current_log_level = snapshot_plan_log_level; + break; + default: + return false; + } + + if (!check_log_levels_real_time) + { + //std::cout << "Current Log Level: " << current_log_level << " requested:" << log_level; + // For speed we are going to check the log levels here before attempting to send any logging info to Python. + if (current_log_level > log_level) + { + return false; + } + } + return true; +} + +void octolapse_log_exception(const int logger_type, const std::string& message) +{ + octolapse_log(logger_type, octolapse_log::ERROR, message, true); } void octolapse_log(const int logger_type, const int log_level, const std::string& message) { + octolapse_log(logger_type, log_level, message, false); +} - if (!octolapse_loggers_created) - return; - - // Get the appropriate logger - PyObject * py_logger; - long current_log_level = 0; - switch (logger_type) - { - case octolapse_log::GCODE_PARSER: - py_logger = py_octolapse_gcode_parser_logger; - current_log_level = gcode_parser_log_level; - break; - case octolapse_log::GCODE_POSITION: - py_logger = py_octolapse_gcode_position_logger; - current_log_level = gcode_position_log_level; - break; - case octolapse_log::SNAPSHOT_PLAN: - py_logger = py_octolapse_snapshot_plan_logger; - current_log_level = snapshot_plan_log_level; - break; - default: - PyErr_SetString(PyExc_ValueError, "Logging.octolapse_log - unknown logger_type."); - return; - } - - if (!check_log_levels_real_time) - { - //std::cout << "Current Log Level: " << current_log_level << " requested:" << log_level; - // For speed we are going to check the log levels here before attempting to send any logging info to Python. - if (current_log_level > log_level) - { - return; - } - } - - PyObject * pyFunctionName; - switch (log_level) - { - case octolapse_log::INFO: - pyFunctionName = py_info_function_name; - break; - case octolapse_log::WARNING: - pyFunctionName = py_warn_function_name; - break; - case octolapse_log::ERROR: - pyFunctionName = py_error_function_name; - break; - case octolapse_log::DEBUG: - pyFunctionName = py_debug_function_name; - break; - case octolapse_log::VERBOSE: - pyFunctionName = py_verbose_function_name; - break; - case octolapse_log::CRITICAL: - pyFunctionName = py_critical_function_name; - break; - default: - return; - } - PyObject * pyMessage = PyUnicode_SafeFromString(message); - if (pyMessage == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, ""); - PyErr_Format(PyExc_ValueError, - "Unable to convert the log message '%s' to a PyString/Unicode message.", message.c_str()); - // Todo: What should I do if this fails?? - return; - } - PyGILState_STATE state = PyGILState_Ensure(); - PyObject * ret_val = PyObject_CallMethodObjArgs(py_logger, pyFunctionName, pyMessage, NULL); - // We need to decref our message so that the GC can remove it. Maybe? - Py_DECREF(pyMessage); - PyGILState_Release(state); - if (ret_val == NULL) - { - if (!PyErr_Occurred()) - PyErr_SetString(PyExc_ValueError, "Logging.octolapse_log - unknown logger_type."); - else - { - // I'm not sure what else to do here since I can't log the error. I will print it - // so that it shows up in the console, but I can't log it, and there is no way to - // return an error. - PyErr_Print(); - PyErr_Clear(); - } - } - Py_XDECREF(ret_val); -} \ No newline at end of file +void octolapse_log(const int logger_type, const int log_level, const std::string& message, bool is_exception) +{ + if (!octolapse_loggers_created) + return; + + // Get the appropriate logger + PyObject* py_logger; + long current_log_level = 0; + switch (logger_type) + { + case octolapse_log::GCODE_PARSER: + py_logger = py_octolapse_gcode_parser_logger; + current_log_level = gcode_parser_log_level; + break; + case octolapse_log::GCODE_POSITION: + py_logger = py_octolapse_gcode_position_logger; + current_log_level = gcode_position_log_level; + break; + case octolapse_log::SNAPSHOT_PLAN: + py_logger = py_octolapse_snapshot_plan_logger; + current_log_level = snapshot_plan_log_level; + break; + default: + PyErr_SetString(PyExc_ValueError, "Logging.octolapse_log - unknown logger_type."); + return; + } + + if (!check_log_levels_real_time) + { + //std::cout << "Current Log Level: " << current_log_level << " requested:" << log_level; + // For speed we are going to check the log levels here before attempting to send any logging info to Python. + if (current_log_level > log_level) + { + return; + } + } + + PyObject* pyFunctionName = NULL; + + PyObject* error_type = NULL; + PyObject* error_value = NULL; + PyObject* error_traceback = NULL; + bool error_occurred = false; + if (is_exception) + { + // if an error has occurred, use the exception function to log the entire error + pyFunctionName = py_error_function_name; + if (PyErr_Occurred()) + { + error_occurred = true; + PyErr_Fetch(&error_type, &error_value, &error_traceback); + PyErr_NormalizeException(&error_type, &error_value, &error_traceback); + } + } + else + { + switch (log_level) + { + case octolapse_log::INFO: + pyFunctionName = py_info_function_name; + break; + case octolapse_log::WARNING: + pyFunctionName = py_warn_function_name; + break; + case octolapse_log::ERROR: + pyFunctionName = py_error_function_name; + break; + case octolapse_log::DEBUG: + pyFunctionName = py_debug_function_name; + break; + case octolapse_log::VERBOSE: + pyFunctionName = py_verbose_function_name; + break; + case octolapse_log::CRITICAL: + pyFunctionName = py_critical_function_name; + break; + default: + return; + } + } + PyObject* pyMessage = PyUnicode_SafeFromString(message); + if (pyMessage == NULL) + { + PyErr_Format(PyExc_ValueError, + "Unable to convert the log message '%s' to a PyString/Unicode message.", message.c_str()); + return; + } + PyGILState_STATE state = PyGILState_Ensure(); + PyObject* ret_val = PyObject_CallMethodObjArgs(py_logger, pyFunctionName, pyMessage, NULL); + // We need to decref our message so that the GC can remove it. Maybe? + Py_DECREF(pyMessage); + PyGILState_Release(state); + if (ret_val == NULL) + { + if (!PyErr_Occurred()) + PyErr_SetString(PyExc_ValueError, "Logging.octolapse_log - unknown logger_type."); + else + { + // I'm not sure what else to do here since I can't log the error. I will print it + // so that it shows up in the console, but I can't log it, and there is no way to + // return an error. + PyErr_Print(); + PyErr_Clear(); + } + } + else + { + // Set the exception if we are doing exception logging. + if (is_exception) + { + if (error_occurred) + PyErr_Restore(error_type, error_value, error_traceback); + else + PyErr_SetString(PyExc_Exception, message.c_str()); + } + } + Py_XDECREF(ret_val); +} diff --git a/octoprint_octolapse/data/lib/c/logging.h b/octoprint_octolapse/data/lib/c/logging.h index 4ef11995..4de81d7e 100644 --- a/octoprint_octolapse/data/lib/c/logging.h +++ b/octoprint_octolapse/data/lib/c/logging.h @@ -1,15 +1,18 @@ #pragma once + #include #include struct octolapse_log { - enum octolapse_loggers { GCODE_PARSER, GCODE_POSITION, SNAPSHOT_PLAN }; - enum octolapse_log_levels {NOSET = 0, VERBOSE = 5, DEBUG = 10, INFO=20, WARNING=30, ERROR=40, CRITICAL=50}; + enum octolapse_loggers { GCODE_PARSER, GCODE_POSITION, SNAPSHOT_PLAN }; + + enum octolapse_log_levels { NOSET = 0, VERBOSE = 5, DEBUG = 10, INFO=20, WARNING=30, ERROR=40, CRITICAL=50 }; }; void octolapse_initialize_loggers(); -void octolapse_log(const int logger_type, const int log_level, const std::string &message); +bool octolapse_may_be_logged(const int logger_type, const int log_level); +void octolapse_log(const int logger_type, const int log_level, const std::string& message); +void octolapse_log(const int logger_type, const int log_level, const std::string& message, bool is_exception); +void octolapse_log_exception(const int logger_type, const std::string& message); void set_internal_log_levels(bool check_real_time); - - diff --git a/octoprint_octolapse/data/lib/c/parsed_command.cpp b/octoprint_octolapse/data/lib/c/parsed_command.cpp index b57d22f8..d9ee2c66 100644 --- a/octoprint_octolapse/data/lib/c/parsed_command.cpp +++ b/octoprint_octolapse/data/lib/c/parsed_command.cpp @@ -24,170 +24,155 @@ #include "python_helpers.h" #include "logging.h" #include + parsed_command::parsed_command() { - //cmd_ = ""; - //gcode_ = ""; -} -parsed_command::parsed_command(parsed_command & source) -{ - cmd_ = source.cmd_; - gcode_ = source.gcode_; - for (unsigned int index = 0; index < source.parameters_.size(); index++) - parameters_.push_back(new parsed_command_parameter(*source.parameters_[index])); + command.reserve(8); + gcode.reserve(128); + comment.reserve(128); + parameters.reserve(6); + is_known_command = false; + is_empty = true; } -parsed_command::~parsed_command() -{ - for (std::vector< parsed_command_parameter * >::iterator it = parameters_.begin(); it != parameters_.end(); ++it) - { - delete (*it); - } - parameters_.clear(); -} void parsed_command::clear() { - cmd_ = ""; - gcode_ = ""; - for (std::vector< parsed_command_parameter * >::iterator it = parameters_.begin(); it != parameters_.end(); ++it) - { - delete (*it); - } - parameters_.clear(); + command.clear(); + gcode.clear(); + comment.clear(); + parameters.clear(); + is_known_command = false; + is_empty = true; } -PyObject * parsed_command::to_py_object() + +PyObject* parsed_command::to_py_object() { - PyObject *ret_val; - PyObject * pyCommandName = PyUnicode_SafeFromString(cmd_.c_str()); - - if (pyCommandName == NULL) - { - PyErr_Print(); - std::string message = "Unable to convert the parameter name to unicode: "; - message += cmd_; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - PyObject * pyGcode = PyUnicode_SafeFromString(gcode_.c_str()); - if (pyGcode == NULL) - { - PyErr_Print(); - std::string message = "Unable to convert the gcode to unicode: "; - message += gcode_; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - - if (parameters_.empty()) - { - ret_val = PyTuple_Pack(3, pyCommandName, Py_None, pyGcode); - if (ret_val == NULL) - { - PyErr_Print(); - std::string message = "Unable to convert the parsed_command (no parameters) to a tuple. Command: "; - message += cmd_; - message += " Gcode: "; - message += gcode_; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - // We will need to decref pyCommandName and pyGcode later - } - else - { - PyObject * pyParametersDict = PyDict_New(); + PyObject* ret_val; + PyObject* pyCommandName = PyUnicode_SafeFromString(command.c_str()); + + if (pyCommandName == NULL) + { + std::string message = "Unable to convert the parameter name to unicode: "; + message += command; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + PyObject* pyGcode = PyUnicode_SafeFromString(gcode.c_str()); + if (pyGcode == NULL) + { + std::string message = "Unable to convert the gcode to unicode: "; + message += gcode; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + + PyObject* pyComment = PyUnicode_SafeFromString(comment.c_str()); + if (pyComment == NULL) + { + std::string message = "Unable to convert the gocde comment to unicode: "; + message += comment; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + + if (parameters.empty()) + { + ret_val = PyTuple_Pack(4, pyCommandName, Py_None, pyGcode, pyComment); + if (ret_val == NULL) + { + std::string message = "Unable to convert the parsed_command (no parameters) to a tuple. Command: "; + message += command; + message += " Gcode: "; + message += gcode; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + // We will need to decref pyCommandName and pyGcode later + } + else + { + PyObject* pyParametersDict = PyDict_New(); - // Create the parameters dictionary - if (pyParametersDict == NULL) - { - PyErr_Print(); - std::string message = "ParsedCommand.to_py_object: Unable to create the parameters dict."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - // Loop through our parameters vector and create and add PyDict items - for (unsigned int index = 0; index < parameters_.size(); index++) - { - parsed_command_parameter* param = parameters_[index]; - PyObject * param_value = param->value_to_py_object(); - // Errors here will be handled by value_to_py_object, just return NULL - if (param_value == NULL) - return NULL; - - if (PyDict_SetItemString(pyParametersDict, param->name_.c_str(), param_value) != 0) - { - PyErr_Print(); - // Handle error here, display detailed message - std::string message = "Unable to add the command parameter to the parameters dictionary. Parameter Name: "; - message += param->name_; - message += " Value Type: "; - message += param->value_type_; - message += " Value: "; + // Create the parameters dictionary + if (pyParametersDict == NULL) + { + std::string message = "ParsedCommand.to_py_object: Unable to create the parameters dict."; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + // Loop through our parameters vector and create and add PyDict items + for (unsigned int index = 0; index < parameters.size(); index++) + { + parsed_command_parameter param = parameters[index]; + PyObject* param_value = param.value_to_py_object(); + // Errors here will be handled by value_to_py_object, just return NULL + if (param_value == NULL) + { + return NULL; + } + if (PyDict_SetItemString(pyParametersDict, param.name.c_str(), param_value) != 0) + { + // Handle error here, display detailed message + std::string message = "Unable to add the command parameter to the parameters dictionary. Parameter Name: "; + message += param.name; + message += " Value Type: "; + message += param.value_type; + message += " Value: "; - switch (param->value_type_) - { - case 'S': - message += param->string_value_; - break; - case 'N': - message += "None"; - break; - case 'F': - { - std::ostringstream doubld_str; - doubld_str << param->double_value_; - message += doubld_str.str(); - message += param->string_value_; - } - break; - case 'U': - { - std::ostringstream unsigned_strs; - unsigned_strs << param->unsigned_long_value_; - message += unsigned_strs.str(); - message += param->string_value_; - } - break; - default: - break; - } - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - // Todo: evaluate the effects of this - Py_DECREF(param_value); - //std::cout << "param_value refcount = " << param_value->ob_refcnt << "\r\n"; - } + switch (param.value_type) + { + case 'S': + message += param.string_value; + break; + case 'N': + message += "None"; + break; + case 'F': + { + std::ostringstream doubld_str; + doubld_str << param.double_value; + message += doubld_str.str(); + message += param.string_value; + } + break; + case 'U': + { + std::ostringstream unsigned_strs; + unsigned_strs << param.unsigned_long_value; + message += unsigned_strs.str(); + message += param.string_value; + } + break; + default: + break; + } + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + // Todo: evaluate the effects of this + Py_DECREF(param_value); + } - ret_val = PyTuple_Pack(3, pyCommandName, pyParametersDict, pyGcode); - if (ret_val == NULL) - { - PyErr_Print(); - std::string message = "Unable to convert the parsed_command (with parameters) to a tuple. Command: "; - message += cmd_; - message += " Gcode: "; - message += gcode_; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - // PyTuple_Pack makes a reference of its own, decref pyParametersDict. - // We will need to decref pyCommandName and pyGcode later - // Todo: evaluate the effects of this - Py_DECREF(pyParametersDict); - //std::cout << "pyParametersDict refcount = " << pyParametersDict->ob_refcnt << "\r\n"; - } - // If we're here, we need to decref pyCommandName and pyGcode. - // Todo: evaluate the effects of this - Py_DECREF(pyCommandName); - // Todo: evaluate the effects of this - Py_DECREF(pyGcode); - //std::cout << "pyCommandName refcount = " << pyCommandName->ob_refcnt << "\r\n"; - //std::cout << "pyGcode refcount = " << pyGcode->ob_refcnt << "\r\n"; - //std::cout << "ret_val refcount = " << ret_val->ob_refcnt << "\r\n"; - return ret_val; + ret_val = PyTuple_Pack(4, pyCommandName, pyParametersDict, pyGcode, pyComment); + if (ret_val == NULL) + { + std::string message = "Unable to convert the parsed_command (with parameters) to a tuple. Command: "; + message += command; + message += " Gcode: "; + message += gcode; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + // PyTuple_Pack makes a reference of its own, decref pyParametersDict. + // We will need to decref pyCommandName and pyGcode later + // Todo: evaluate the effects of this + Py_DECREF(pyParametersDict); + } + // If we're here, we need to decref pyCommandName and pyGcode. + // Todo: evaluate the effects of this + Py_DECREF(pyCommandName); + // Todo: evaluate the effects of this + Py_DECREF(pyGcode); + Py_DECREF(pyComment); + return ret_val; } diff --git a/octoprint_octolapse/data/lib/c/parsed_command.h b/octoprint_octolapse/data/lib/c/parsed_command.h index 18fac9a9..cf627b5f 100644 --- a/octoprint_octolapse/data/lib/c/parsed_command.h +++ b/octoprint_octolapse/data/lib/c/parsed_command.h @@ -23,9 +23,9 @@ #ifndef PARSED_COMMAND_H #define PARSED_COMMAND_H #ifdef _DEBUG -#undef _DEBUG +//#undef _DEBUG #include -#define _DEBUG +//python311_d.lib #else #include #endif @@ -33,19 +33,18 @@ #include #include "parsed_command_parameter.h" -class parsed_command +struct parsed_command { public: - parsed_command(); - parsed_command(parsed_command & source); - ~parsed_command(); - std::string cmd_; - std::string gcode_; - std::vector parameters_; - PyObject * to_py_object(); - void clear(); -private: - + parsed_command(); + std::string command; + std::string gcode; + std::string comment; + bool is_empty; + bool is_known_command; + std::vector parameters; + PyObject* to_py_object(); + void clear(); }; -#endif \ No newline at end of file +#endif diff --git a/octoprint_octolapse/data/lib/c/parsed_command_parameter.cpp b/octoprint_octolapse/data/lib/c/parsed_command_parameter.cpp index 94fca467..e482dce0 100644 --- a/octoprint_octolapse/data/lib/c/parsed_command_parameter.cpp +++ b/octoprint_octolapse/data/lib/c/parsed_command_parameter.cpp @@ -24,103 +24,84 @@ #include "parsed_command.h" #include "logging.h" #include "python_helpers.h" + parsed_command_parameter::parsed_command_parameter() { - //name_ = ""; - value_type_ = 'N'; - //double_value_ = 0.0; - //string_value_ = ""; - //unsigned_long_value_ = 0; + value_type = 'N'; + name.reserve(1); } -parsed_command_parameter::parsed_command_parameter(parsed_command_parameter & source) +parsed_command_parameter:: +parsed_command_parameter(const std::string name, double value) : name(name), double_value(value) { - name_ = source.name_; - value_type_ = source.value_type_; - double_value_ = source.double_value_; - string_value_ = source.string_value_; - unsigned_long_value_ = source.unsigned_long_value_; + value_type = 'F'; } -parsed_command_parameter::parsed_command_parameter(std::string name, double double_value) : name_(name), double_value_(double_value) +parsed_command_parameter:: +parsed_command_parameter(const std::string name, const std::string value) : name(name), string_value(value) { - value_type_ = 'F'; - //string_value_ = ""; - //unsigned_long_value_ = 0; + value_type = 'S'; } -parsed_command_parameter::parsed_command_parameter(std::string name, std::string string_value) : name_(name), string_value_(string_value) +parsed_command_parameter:: +parsed_command_parameter(const std::string name, const unsigned long value) : name(name), unsigned_long_value(value) { - value_type_ = 'S'; - //double_value_ = 0.0; - //unsigned_long_value_ = 0; + value_type = 'U'; } -parsed_command_parameter::parsed_command_parameter(std::string name, unsigned int unsigned_int_value) : name_(name), string_value_(string_value_), unsigned_long_value_(unsigned_int_value) -{ - value_type_ = 'U'; - //double_value_ = 0.0; - //string_value_ = ""; -} parsed_command_parameter::~parsed_command_parameter() { - } -PyObject * parsed_command_parameter::value_to_py_object() +PyObject* parsed_command_parameter::value_to_py_object() { - PyObject * ret_val; - // check the parameter type - if (value_type_ == 'F') - { - ret_val = PyFloat_FromDouble(double_value_); - if (ret_val == NULL) - { - std::string message = "parsedCommandParameter.value_to_py_object: Unable to convert double value to a PyObject."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - } - else if (value_type_ == 'N') - { - // None Type - Py_INCREF(Py_None); - ret_val = Py_None; - } - else if (value_type_ == 'S') - { - ret_val = PyUnicode_SafeFromString(string_value_.c_str()); - if (ret_val == NULL) - { - std::string message = "parsedCommandParameter.value_to_py_object: Unable to convert string value to a PyObject."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - - } - else if (value_type_ == 'U') - { - ret_val = PyLong_FromUnsignedLong(unsigned_long_value_); - if (ret_val == NULL) - { - std::string message = "parsedCommandParameter.value_to_py_object: Unable to convert unsigned long value to a PyObject."; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - } - else - { - std::string message = "The command parameter value type does not exist. Value Type: "; - message += value_type_; - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - // There has been an error, we don't support this value_type! - PyErr_SetString(PyExc_ValueError, "Error creating ParsedCommand: Unknown value_type"); - return NULL; - } - - return ret_val; + PyObject* ret_val; + // check the parameter type + if (value_type == 'F') + { + ret_val = PyFloat_FromDouble(double_value); + if (ret_val == NULL) + { + std::string message = "parsedCommandParameter.value_to_py_object: Unable to convert double value to a PyObject."; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + } + else if (value_type == 'N') + { + // None Type + Py_INCREF(Py_None); + ret_val = Py_None; + } + else if (value_type == 'S') + { + ret_val = PyUnicode_SafeFromString(string_value.c_str()); + if (ret_val == NULL) + { + std::string message = "parsedCommandParameter.value_to_py_object: Unable to convert string value to a PyObject."; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + } + else if (value_type == 'U') + { + ret_val = PyLong_FromUnsignedLong(unsigned_long_value); + if (ret_val == NULL) + { + std::string message = + "parsedCommandParameter.value_to_py_object: Unable to convert unsigned long value to a PyObject."; + octolapse_log_exception(octolapse_log::GCODE_PARSER, message); + return NULL; + } + } + else + { + std::string message = "The command parameter value type does not exist. Value Type: "; + message += value_type; + octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); + // There has been an error, we don't support this value_type! + return NULL; + } -} \ No newline at end of file + return ret_val; +} diff --git a/octoprint_octolapse/data/lib/c/parsed_command_parameter.h b/octoprint_octolapse/data/lib/c/parsed_command_parameter.h index faf322e5..e4c1c09e 100644 --- a/octoprint_octolapse/data/lib/c/parsed_command_parameter.h +++ b/octoprint_octolapse/data/lib/c/parsed_command_parameter.h @@ -24,29 +24,27 @@ #define PARSED_COMMAND_PARAMETER_H #include #ifdef _DEBUG -#undef _DEBUG +//#undef _DEBUG #include -#define _DEBUG +//python311_d.lib #else #include #endif -class parsed_command_parameter +struct parsed_command_parameter { public: - parsed_command_parameter(); - ~parsed_command_parameter(); - parsed_command_parameter(parsed_command_parameter & source); - parsed_command_parameter(std::string name, double double_value); - parsed_command_parameter(std::string name, std::string string_value); - parsed_command_parameter(std::string name, unsigned int unsigned_int_value); + parsed_command_parameter(); + ~parsed_command_parameter(); + parsed_command_parameter(std::string name, double value); + parsed_command_parameter(std::string name, std::string value); + parsed_command_parameter(std::string name, unsigned long value); + PyObject* value_to_py_object(); - PyObject * value_to_py_object(); - - std::string name_; - char value_type_; - double double_value_; - unsigned long unsigned_long_value_; - std::string string_value_; + std::string name; + char value_type; + double double_value; + unsigned long unsigned_long_value; + std::string string_value; }; #endif diff --git a/octoprint_octolapse/data/lib/c/position.cpp b/octoprint_octolapse/data/lib/c/position.cpp index 13410edb..7ea61e4b 100644 --- a/octoprint_octolapse/data/lib/c/position.cpp +++ b/octoprint_octolapse/data/lib/c/position.cpp @@ -22,610 +22,641 @@ #include "position.h" #include "logging.h" +#include -void position::initialize() +void position::set_xyz_axis_mode(const std::string& xyz_axis_default_mode) { - p_command = NULL; - f_ = 0; - f_null_ = true; - x_ = 0; - x_null_ = true; - x_offset_ = 0; - x_homed_ = false; - y_ = 0; - y_null_ = true; - y_offset_ = 0; - y_homed_ = false; - z_ = 0; - z_null_ = true; - z_offset_ = 0; - z_homed_ = false; - e_ = 0; - e_offset_ = 0; - is_relative_ = false; - is_relative_null_ = true; - is_extruder_relative_ = false; - is_extruder_relative_null_ = true; - is_metric_ = true; - is_metric_null_ = true; - last_extrusion_height_ = 0; - last_extrusion_height_null_ = true; - layer_ = 0; - height_ = 0; - current_height_increment_ = 0; - is_printer_primed_ = false; - firmware_retraction_length_ = 0; - firmware_retraction_length_null_ = true; - firmware_unretraction_additional_length_ = 0; - firmware_unretraction_additional_length_null_ = true; - firmware_retraction_feedrate_ = 0; - firmware_retraction_feedrate_null_ = true; - firmware_unretraction_feedrate_ = 0; - firmware_unretraction_feedrate_null_ = true; - firmware_z_lift_ = 0; - firmware_z_lift_null_ = true; - has_position_error_ = false; - position_error_ = ""; - has_homed_position_ = false; - e_relative_ = 0; - z_relative_ = 0; - extrusion_length_ = 0; - extrusion_length_total_ = 0; - retraction_length_ = 0; - deretraction_length_ = 0; - is_extruding_start_ = false; - is_extruding_ = false; - is_primed_ = false; - is_retracting_start_ = false; - is_retracting_ = false; - is_retracted_ = false; - is_partially_retracted_ = false; - is_deretracting_start_ = false; - is_deretracting_ = false; - is_deretracted_ = false; - is_in_position_ = false; - in_path_position_ = false; - is_zhop_ = false; - is_layer_change_ = false; - is_height_change_ = false; - is_xy_travel_ = false; - is_xyz_travel_ = false; - has_xy_position_changed_ = false; - has_position_changed_ = false; - has_state_changed_ = false; - has_received_home_command_ = false; - file_line_number_ = -1; - gcode_number_ = -1; - gcode_ignored_ = true; - is_in_bounds_ = true; + if (xyz_axis_default_mode == "relative" || xyz_axis_default_mode == "force-relative") + { + is_relative = true; + is_relative_null = false; + } + else if (xyz_axis_default_mode == "absolute" || xyz_axis_default_mode == "force-absolute") + { + is_relative = false; + is_relative_null = false; + } } -position::position() -{ - initialize(); +void position::set_e_axis_mode(const std::string& e_axis_default_mode) +{ + if (e_axis_default_mode == "relative" || e_axis_default_mode == "force-relative") + { + is_extruder_relative = true; + is_extruder_relative_null = false; + } + else if (e_axis_default_mode == "absolute" || e_axis_default_mode == "force-absolute") + { + is_extruder_relative = false; + is_extruder_relative_null = false; + } } -position::position(position & source) +void position::set_units_default(const std::string& units_default) { - if(source.p_command != NULL) - p_command = new parsed_command(*source.p_command); - f_ = source.f_; - f_null_ = source.f_null_; - x_ = source.x_; - x_null_ = source.x_null_; - x_offset_ = source.x_offset_; - x_homed_ = source.x_homed_; - y_ = source.y_; - y_null_ = source.y_null_; - y_offset_ = source.y_offset_; - y_homed_ = source.y_homed_; - z_ = source.z_; - z_null_ = source.z_null_; - z_offset_ = source.z_offset_; - z_homed_ = source.z_homed_; - e_ = source.e_; - e_offset_ = source.e_offset_; - is_relative_ = source.is_relative_; - is_relative_null_ = source.is_relative_null_; - is_extruder_relative_ = source.is_extruder_relative_; - is_extruder_relative_null_ = source.is_extruder_relative_null_; - is_metric_ = source.is_metric_; - is_metric_null_ = source.is_metric_null_; - last_extrusion_height_ = source.last_extrusion_height_ ; - last_extrusion_height_null_ = source.last_extrusion_height_null_; - layer_ = source.layer_; - height_ = source.height_; - current_height_increment_ = source.current_height_increment_; - is_printer_primed_ = source.is_printer_primed_; - firmware_retraction_length_ = source.firmware_retraction_length_; - firmware_retraction_length_null_ = source.firmware_retraction_length_null_; - firmware_unretraction_additional_length_ = source.firmware_unretraction_additional_length_; - firmware_unretraction_additional_length_null_ = source.firmware_unretraction_additional_length_null_; - firmware_retraction_feedrate_ = source.firmware_retraction_feedrate_; - firmware_retraction_feedrate_null_ = source.firmware_retraction_feedrate_null_; - firmware_unretraction_feedrate_ = source.firmware_unretraction_feedrate_; - firmware_unretraction_feedrate_null_ = source.firmware_unretraction_feedrate_null_; - firmware_z_lift_ = source.firmware_z_lift_; - firmware_z_lift_null_ = source.firmware_z_lift_null_; - has_position_error_ = source.has_position_error_; - position_error_ = source.position_error_; - has_homed_position_ = source.has_homed_position_; - e_relative_ = source.e_relative_; - z_relative_ = source.z_relative_; - extrusion_length_ = source.extrusion_length_; - extrusion_length_total_ = source.extrusion_length_total_; - retraction_length_ = source.retraction_length_; - deretraction_length_ = source.deretraction_length_; - is_extruding_start_ = source.is_extruding_start_; - is_extruding_ = source.is_extruding_; - is_primed_ = source.is_primed_; - is_retracting_start_ = source.is_retracting_start_; - is_retracting_ = source.is_retracting_; - is_retracted_ = source.is_retracted_; - is_partially_retracted_ = source.is_partially_retracted_; - is_deretracting_start_ = source.is_deretracting_start_; - is_deretracting_ = source.is_deretracting_; - is_deretracted_ = source.is_deretracted_; - is_layer_change_ = source.is_layer_change_; - is_height_change_ = source.is_height_change_; - is_xy_travel_ = source.is_xy_travel_; - is_xyz_travel_ = source.is_xyz_travel_; - is_zhop_ = source.is_zhop_; - has_xy_position_changed_ = source.has_xy_position_changed_; - has_position_changed_ = source.has_position_changed_; - has_state_changed_ = source.has_state_changed_; - has_received_home_command_ = source.has_received_home_command_; - is_in_position_ = source.is_in_position_; - in_path_position_ = source.in_path_position_; - file_line_number_ = source.file_line_number_; - gcode_number_ = source.gcode_number_; - gcode_ignored_ = source.gcode_ignored_; - is_in_bounds_ = source.is_in_bounds_; + if (units_default == "inches") + { + is_metric = false; + is_metric_null = false; + } + else if (units_default == "millimeters") + { + is_metric = true; + is_metric_null = false; + } } -position::position(const std::string& xyz_axis_default_mode, const std::string& e_axis_default_mode, const std::string& - units_default) +bool position::can_take_snapshot() { - initialize(); + return ( + !is_relative_null && + !is_extruder_relative_null && + has_definite_position && + is_printer_primed && + !is_metric_null + ); +} - if (xyz_axis_default_mode == "relative" || xyz_axis_default_mode == "force-relative") - { - is_relative_ = true; - is_relative_null_ = false; - } - else if (xyz_axis_default_mode == "absolute" || xyz_axis_default_mode == "force-absolute") - { - is_relative_ = false; - is_relative_null_ = false; - } +position::position() +{ + is_empty = true; + feature_type_tag = 0; + f = 0; + f_null = true; + x = 0; + x_null = true; + x_offset = 0; + x_firmware_offset = 0; + x_homed = false; + y = 0; + y_null = true; + y_offset = 0; + y_firmware_offset = 0; + y_homed = false; + z = 0; + z_null = true; + z_offset = 0; + z_firmware_offset = 0; + z_homed = false; + is_relative = false; + is_relative_null = true; + is_extruder_relative = false; + is_extruder_relative_null = true; + is_metric = true; + is_metric_null = true; + last_extrusion_height = 0; + last_extrusion_height_null = true; + layer = 0; + height = 0; + height_increment = 0; + height_increment_change_count = 0; + is_printer_primed = false; + has_definite_position = false; + z_relative = 0; + is_in_position = false; + in_path_position = false; + is_zhop = false; + is_layer_change = false; + is_height_change = false; + is_height_increment_change = false; + is_xy_travel = false; + is_xyz_travel = false; + has_xy_position_changed = false; + has_position_changed = false; + has_received_home_command = false; + file_line_number = -1; + gcode_number = -1; + file_position = -1; + gcode_ignored = true; + is_in_bounds = true; + current_tool = -1; + p_extruders = NULL; + set_num_extruders(0); +} - if (e_axis_default_mode == "relative" || e_axis_default_mode == "force-relative") - { - is_extruder_relative_ = true; - is_extruder_relative_null_ = false; - } - else if (e_axis_default_mode == "absolute" || e_axis_default_mode == "force-absolute") - { - is_extruder_relative_ = false; - is_extruder_relative_null_ = false; - } +position::position(int extruder_count) +{ + is_empty = true; + feature_type_tag = 0; + f = 0; + f_null = true; + x = 0; + x_null = true; + x_offset = 0; + x_firmware_offset = 0; + x_homed = false; + y = 0; + y_null = true; + y_offset = 0; + y_firmware_offset = 0; + y_homed = false; + z = 0; + z_null = true; + z_offset = 0; + z_firmware_offset = 0; + z_homed = false; + is_relative = false; + is_relative_null = true; + is_extruder_relative = false; + is_extruder_relative_null = true; + is_metric = true; + is_metric_null = true; + last_extrusion_height = 0; + last_extrusion_height_null = true; + layer = 0; + height = 0; + height_increment = 0; + height_increment_change_count = 0; + is_printer_primed = false; + has_definite_position = false; + z_relative = 0; + is_in_position = false; + in_path_position = false; + is_zhop = false; + is_layer_change = false; + is_height_change = false; + is_height_increment_change = false; + is_xy_travel = false; + is_xyz_travel = false; + has_xy_position_changed = false; + has_position_changed = false; + has_received_home_command = false; + file_line_number = -1; + gcode_number = -1; + file_position = -1; + gcode_ignored = true; + is_in_bounds = true; + current_tool = 0; + p_extruders = NULL; + set_num_extruders(extruder_count); +} - if (units_default == "inches") - { - is_metric_ = false; - is_metric_null_ = false; - } - else if (units_default == "millimeters") - { - is_metric_ = true; - is_metric_null_ = false; - } +position::position(const position& pos) +{ + is_empty = pos.is_empty; + feature_type_tag = pos.feature_type_tag; + f = pos.f; + f_null = pos.f_null; + x = pos.x; + x_null = pos.x_null; + x_offset = pos.x_offset; + x_firmware_offset = pos.x_firmware_offset; + x_homed = pos.x_homed; + y = pos.y; + y_null = pos.y_null; + y_offset = pos.y_offset; + y_firmware_offset = pos.y_firmware_offset; + y_homed = pos.y_homed; + z = pos.z; + z_null = pos.z_null; + z_offset = pos.z_offset; + z_firmware_offset = pos.z_firmware_offset; + z_homed = pos.z_homed; + is_relative = pos.is_relative; + is_relative_null = pos.is_relative_null; + is_extruder_relative = pos.is_extruder_relative; + is_extruder_relative_null = pos.is_extruder_relative_null; + is_metric = pos.is_metric; + is_metric_null = pos.is_metric_null; + last_extrusion_height = pos.last_extrusion_height; + last_extrusion_height_null = pos.last_extrusion_height_null; + layer = pos.layer; + height = pos.height; + height_increment = pos.height_increment; + height_increment_change_count = pos.height_increment_change_count; + is_printer_primed = pos.is_printer_primed; + has_definite_position = pos.has_definite_position; + z_relative = pos.z_relative; + is_in_position = pos.is_in_position; + in_path_position = pos.in_path_position; + is_zhop = pos.is_zhop; + is_layer_change = pos.is_layer_change; + is_height_change = pos.is_height_change; + is_height_increment_change = pos.is_height_increment_change; + is_xy_travel = pos.is_xy_travel; + is_xyz_travel = pos.is_xyz_travel; + has_xy_position_changed = pos.has_xy_position_changed; + has_position_changed = pos.has_position_changed; + has_received_home_command = pos.has_received_home_command; + file_line_number = pos.file_line_number; + gcode_number = pos.gcode_number; + file_position = pos.file_position; + gcode_ignored = pos.gcode_ignored; + is_in_bounds = pos.is_in_bounds; + current_tool = pos.current_tool; + p_extruders = NULL; + command = pos.command; + set_num_extruders(pos.num_extruders); + for (int index = 0; index < pos.num_extruders; index++) + { + p_extruders[index] = pos.p_extruders[index]; + } } position::~position() { - if (p_command != NULL) - { - delete p_command; - p_command = NULL; - } + delete_extruders(); } -void position::_copy_parsed_command(parsed_command* source_command, position* target) + +position& position::operator=(const position& pos) { - if (target->p_command != NULL) - { - delete target->p_command; - target->p_command = NULL; - } - if (source_command != NULL) - target->p_command = new parsed_command(*source_command); + is_empty = pos.is_empty; + feature_type_tag = pos.feature_type_tag; + f = pos.f; + f_null = pos.f_null; + x = pos.x; + x_null = pos.x_null; + x_offset = pos.x_offset; + x_firmware_offset = pos.x_firmware_offset; + x_homed = pos.x_homed; + y = pos.y; + y_null = pos.y_null; + y_offset = pos.y_offset; + y_firmware_offset = pos.y_firmware_offset; + y_homed = pos.y_homed; + z = pos.z; + z_null = pos.z_null; + z_offset = pos.z_offset; + z_firmware_offset = pos.z_firmware_offset; + z_homed = pos.z_homed; + is_relative = pos.is_relative; + is_relative_null = pos.is_relative_null; + is_extruder_relative = pos.is_extruder_relative; + is_extruder_relative_null = pos.is_extruder_relative_null; + is_metric = pos.is_metric; + is_metric_null = pos.is_metric_null; + last_extrusion_height = pos.last_extrusion_height; + last_extrusion_height_null = pos.last_extrusion_height_null; + layer = pos.layer; + height = pos.height; + height_increment = pos.height_increment; + height_increment_change_count = pos.height_increment_change_count; + is_printer_primed = pos.is_printer_primed; + has_definite_position = pos.has_definite_position; + z_relative = pos.z_relative; + is_in_position = pos.is_in_position; + in_path_position = pos.in_path_position; + is_zhop = pos.is_zhop; + is_layer_change = pos.is_layer_change; + is_height_change = pos.is_height_change; + is_height_increment_change = pos.is_height_increment_change; + is_xy_travel = pos.is_xy_travel; + is_xyz_travel = pos.is_xyz_travel; + has_xy_position_changed = pos.has_xy_position_changed; + has_position_changed = pos.has_position_changed; + has_received_home_command = pos.has_received_home_command; + file_line_number = pos.file_line_number; + file_position = pos.file_position; + gcode_number = pos.gcode_number; + gcode_ignored = pos.gcode_ignored; + is_in_bounds = pos.is_in_bounds; + current_tool = pos.current_tool; + command = pos.command; + if (num_extruders != pos.num_extruders) + { + set_num_extruders(pos.num_extruders); + } + for (int index = 0; index < pos.num_extruders; index++) + { + p_extruders[index] = pos.p_extruders[index]; + } + return *this; } -void position::copy(position *source, position* target) + +void position::set_num_extruders(int num_extruders_) { - position::_copy_parsed_command(source->p_command, target); - position::_copy_position(source, target); + delete_extruders(); + num_extruders = num_extruders_; + if (num_extruders_ > 0) + { + p_extruders = new extruder[num_extruders_]; + } } -void position::_copy_position(position* source, position* target) +void position::delete_extruders() { - target->f_ = source->f_; - target->f_null_ = source->f_null_; - target->x_ = source->x_; - target->x_null_ = source->x_null_; - target->x_offset_ = source->x_offset_; - target->x_homed_ = source->x_homed_; - target->y_ = source->y_; - target->y_null_ = source->y_null_; - target->y_offset_ = source->y_offset_; - target->y_homed_ = source->y_homed_; - target->z_ = source->z_; - target->z_null_ = source->z_null_; - target->z_offset_ = source->z_offset_; - target->z_homed_ = source->z_homed_; - target->e_ = source->e_; - target->e_offset_ = source->e_offset_; - target->is_relative_ = source->is_relative_; - target->is_relative_null_ = source->is_relative_null_; - target->is_extruder_relative_ = source->is_extruder_relative_; - target->is_extruder_relative_null_ = source->is_extruder_relative_null_; - target->is_metric_ = source->is_metric_; - target->is_metric_null_ = source->is_metric_null_; - target->last_extrusion_height_ = source->last_extrusion_height_; - target->last_extrusion_height_null_ = source->last_extrusion_height_null_; - target->layer_ = source->layer_; - target->height_ = source->height_; - target->current_height_increment_ = source->current_height_increment_; - target->is_printer_primed_ = source->is_printer_primed_; - target->firmware_retraction_length_ = source->firmware_retraction_length_; - target->firmware_retraction_length_null_ = source->firmware_retraction_length_null_; - target->firmware_unretraction_additional_length_ = source->firmware_unretraction_additional_length_; - target->firmware_unretraction_additional_length_null_ = source->firmware_unretraction_additional_length_null_; - target->firmware_retraction_feedrate_ = source->firmware_retraction_feedrate_; - target->firmware_retraction_feedrate_null_ = source->firmware_retraction_feedrate_null_; - target->firmware_unretraction_feedrate_ = source->firmware_unretraction_feedrate_; - target->firmware_unretraction_feedrate_null_ = source->firmware_unretraction_feedrate_null_; - target->firmware_z_lift_ = source->firmware_z_lift_; - target->firmware_z_lift_null_ = source->firmware_z_lift_null_; - target->has_position_error_ = source->has_position_error_; - target->position_error_ = source->position_error_; - target->has_homed_position_ = source->has_homed_position_; - target->e_relative_ = source->e_relative_; - target->z_relative_ = source->z_relative_; - target->extrusion_length_ = source->extrusion_length_; - target->extrusion_length_total_ = source->extrusion_length_total_; - target->retraction_length_ = source->retraction_length_; - target->deretraction_length_ = source->deretraction_length_; - target->is_extruding_start_ = source->is_extruding_start_; - target->is_extruding_ = source->is_extruding_; - target->is_primed_ = source->is_primed_; - target->is_retracting_start_ = source->is_retracting_start_; - target->is_retracting_ = source->is_retracting_; - target->is_retracted_ = source->is_retracted_; - target->is_partially_retracted_ = source->is_partially_retracted_; - target->is_deretracting_start_ = source->is_deretracting_start_; - target->is_deretracting_ = source->is_deretracting_; - target->is_deretracted_ = source->is_deretracted_; - target->is_layer_change_ = source->is_layer_change_; - target->is_height_change_ = source->is_height_change_; - target->is_xy_travel_ = source->is_xy_travel_; - target->is_xyz_travel_ = source->is_xyz_travel_; - target->is_zhop_ = source->is_zhop_; - target->has_xy_position_changed_ = source->has_xy_position_changed_; - target->has_position_changed_ = source->has_position_changed_; - target->has_state_changed_ = source->has_state_changed_; - target->has_received_home_command_ = source->has_received_home_command_; - target->is_in_position_ = source->is_in_position_; - target->in_path_position_ = source->in_path_position_; - target->file_line_number_ = source->file_line_number_; - target->gcode_number_ = source->gcode_number_; - target->gcode_ignored_ = source->gcode_ignored_; - target->is_in_bounds_ = source->is_in_bounds_; + if (p_extruders != NULL) + { + delete[] p_extruders; + p_extruders = NULL; + } } -void position::copy(position *source, parsed_command* source_command, position* target) +double position::get_gcode_x() const { - position::_copy_parsed_command(source_command, target); - position::_copy_position(source, target); + return x - x_offset + x_firmware_offset; } -PyObject* position::to_py_tuple() +double position::get_gcode_y() const { - PyObject * py_command; - if(p_command == NULL) - { - py_command = Py_None; - } - else - { - py_command = p_command->to_py_object(); - } - PyObject* pyPosition = Py_BuildValue( - // ReSharper disable once StringLiteralTypo - "ddddddddddddddddddddddlllllllllllllllllllllllllllllllllllllllllllllllO", - // Floats - x_, // 0 - y_, // 1 - z_, // 2 - f_, // 3 - e_, // 4 - x_offset_, // 5 - y_offset_, // 6 - z_offset_, // 7 - e_offset_, // 8 - e_relative_, // 9 - z_relative_, // 10 - extrusion_length_, // 11 - extrusion_length_total_, // 12 - retraction_length_, // 13 - deretraction_length_, // 14 - last_extrusion_height_, // 15 - height_, // 16 - firmware_retraction_length_, // 17 - firmware_unretraction_additional_length_, // 18 - firmware_retraction_feedrate_, // 19 - firmware_unretraction_feedrate_, // 20 - firmware_z_lift_, // 21 - // Int - layer_, // 22 - // Bool (represented as an integer) - x_homed_, // 23 - y_homed_, // 24 - z_homed_, // 25 - is_relative_, // 26 - is_extruder_relative_, // 27 - is_metric_, // 28 - is_printer_primed_, // 29 - has_position_error_, // 30 - has_homed_position_, // 31 - is_extruding_start_, // 32 - is_extruding_, // 33 - is_primed_, // 34 - is_retracting_start_, // 35 - is_retracting_, // 36 - is_retracted_, // 37 - is_partially_retracted_, // 38 - is_deretracting_start_, // 39 - is_deretracting_, // 40 - is_deretracted_, // 41 - is_layer_change_, // 42 - is_height_change_, // 43 - is_xy_travel_, // 44 - is_xyz_travel_, // 45 - is_zhop_, // 46 - has_xy_position_changed_, // 47 - has_position_changed_, // 48 - has_state_changed_, // 49 - has_received_home_command_, // 50 - is_in_position_, // 51 - in_path_position_, // 52 - // Null bool, represented as integers - x_null_, // 53 - y_null_, // 54 - z_null_, // 55 - f_null_, // 56 - is_relative_null_, // 57 - is_extruder_relative_null_, // 58 - last_extrusion_height_null_, // 59 - is_metric_null_, // 60 - firmware_retraction_length_null_, // 61 - firmware_unretraction_additional_length_null_, // 62 - firmware_retraction_feedrate_null_, // 63 - firmware_unretraction_feedrate_null_, // 64 - firmware_z_lift_null_, // 65 - // file statistics - file_line_number_, // 66 - gcode_number_, // 67 - // is in bounds - is_in_bounds_, // 68 - // Objects - py_command // 69 - ); - if (pyPosition == NULL) - { - std::string message = "Position.to_py_tuple - Unable to create the position."; - PyErr_Print(); - octolapse_log(octolapse_log::GCODE_PARSER, octolapse_log::ERROR, message); - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - Py_DECREF(py_command); - return pyPosition; - + return y - y_offset + y_firmware_offset; } -double position::get_offset_x() +double position::get_gcode_z() const { - return x_ - x_offset_; + return z - z_offset + z_firmware_offset; } -double position::get_offset_y() + +extruder& position::get_current_extruder() const { - return y_ - y_offset_; + int tool_number = current_tool; + if (current_tool >= num_extruders) + tool_number = num_extruders - 1; + else if (current_tool < 0) + tool_number = 0; + return p_extruders[tool_number]; } -double position::get_offset_z() + +extruder& position::get_extruder(int index) const { - return z_ - z_offset_; + if (index >= num_extruders) + index = num_extruders - 1; + else if (index < 0) + index = 0; + return p_extruders[index]; } -double position::get_offset_e() + +void position::reset_state() { - return e_ - e_offset_; + is_layer_change = false; + is_height_change = false; + is_height_increment_change = false; + is_xy_travel = false; + is_xyz_travel = false; + has_position_changed = false; + has_received_home_command = false; + gcode_ignored = true; + + //is_in_bounds = true; // I dont' think we want to reset this every time since it's only calculated if the current position + // changes. + p_extruders[current_tool].e_relative = 0; + z_relative = 0; + feature_type_tag = 0; } -void position::reset_state() +PyObject* position::to_py_tuple() { - is_layer_change_ = false; - is_height_change_ = false; - is_xy_travel_ = false; - is_xyz_travel_ = false; - has_position_changed_ = false; - has_state_changed_ = false; - has_received_home_command_ = false; - gcode_ignored_ = true; - is_in_bounds_ = true; - e_relative_ = 0; - z_relative_ = 0; + //std::cout << "Building position py_object.\r\n"; + PyObject* py_command; + if (command.is_empty) + { + py_command = Py_None; + } + else + { + py_command = command.to_py_object(); + if (py_command == NULL) + { + return NULL; + } + } + PyObject* py_extruders = extruder::build_py_object(p_extruders, num_extruders); + if (py_extruders == NULL) + { + return NULL; + } + //std::cout << "Building position py_tuple.\r\n"; + PyObject* pyPosition = Py_BuildValue( + // ReSharper disable once StringLiteralTypo + "ddddddddddddddddddlllllllllllllllllllllllllllllllllllllllllOO", + // Floats + x, // 0 + y, // 1 + z, // 2 + f, // 3 + x_offset, // 4 + y_offset, // 5 + z_offset, // 6 + x_firmware_offset, // 7 + y_firmware_offset, // 8 + z_firmware_offset, // 9 + z_relative, // 10 + last_extrusion_height, // 11 + height, // 12 + 0.0, // 13 - Firmware Retraction Length + 0.0, // 14 - Firmware Unretraction Additional Length + 0.0, // 15 - Firmware Retraction Feedrate + 0.0, // 16 - Firmware Unretraction Feedrate + 0.0, // 17 - Firmware Unretraction ZLift + // Int + layer, // 18 + height_increment, // 19 !!!!!!!!! + height_increment_change_count, // 20 !!!!!! + current_tool, // 21 + num_extruders, // 22 + // Bool (represented as an integer) + (long int)(x_homed ? 1 : 0), // 23 + (long int)(y_homed ? 1 : 0), // 24 + (long int)(z_homed ? 1 : 0), // 25 + (long int)(is_relative ? 1 : 0), // 26 + (long int)(is_extruder_relative ? 1 : 0), // 27 + (long int)(is_metric ? 1 : 0), // 28 + (long int)(is_printer_primed ? 1 : 0), // 29 + (long int)(has_definite_position ? 1 : 0), // 30 + (long int)(is_layer_change ? 1 : 0), // 31 + (long int)(is_height_change ? 1 : 0), // 32 + (long int)(is_height_increment_change ? 1 : 0), // 33 + (long int)(is_xy_travel ? 1 : 0), // 34 + (long int)(is_xyz_travel ? 1 : 0), // 35 + (long int)(is_zhop ? 1 : 0), // 36 + (long int)(has_xy_position_changed ? 1 : 0), // 37 + (long int)(has_position_changed ? 1 : 0), // 38 + (long int)(has_received_home_command ? 1 : 0), // 39 + (long int)(is_in_position ? 1 : 0), // 40 + (long int)(in_path_position ? 1 : 0), // 41 + (long int)(is_in_bounds ? 1 : 0), // 42 + // Null bool, represented as integers + (long int)(x_null ? 1 : 0), // 43 + (long int)(y_null ? 1 : 0), // 44 + (long int)(z_null ? 1 : 0), // 45 + (long int)(f_null ? 1 : 0), // 46 + (long int)(is_relative_null ? 1 : 0), // 47 + (long int)(is_extruder_relative_null ? 1 : 0), // 48 + (long int)(last_extrusion_height_null ? 1 : 0), // 49 + (long int)(is_metric_null ? 1 : 0), // 50 + (long int)(true ? 1 : 0), // 51 - Firmware retraction length null + (long int)(true ? 1 : 0), // 52 - Firmware unretraction additional length null + (long int)(true ? 1 : 0), // 53 - Firmware retraction feedrate null + (long int)(true ? 1 : 0), // 54 - Firmware unretraction feedrate null + (long int)(true ? 1 : 0), // 55 - Firmware ZLift Null + // file statistics + file_line_number, // 56 + gcode_number, // 57 + file_position, // 58 + // Objects + py_command, // 59 + py_extruders // 60 + + ); + if (pyPosition == NULL) + { + //std::cout << "No py_object returned for position!\r\n"; + std::string message = + "position.to_py_tuple: Unable to convert position value to a PyObject tuple via Py_BuildValue."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return NULL; + } + //std::cout << "Finished building pyPosition.\r\n"; + Py_DECREF(py_command); + Py_DECREF(py_extruders); + //std::cout << "Returning pyPosition.\r\n"; + return pyPosition; } PyObject* position::to_py_dict() { - PyObject * py_command; - if (p_command == NULL) - { - py_command = Py_None; - } - else - { - py_command = p_command->to_py_object(); - } + PyObject* py_command; + if (command.command.length() == 0) + { + py_command = Py_None; + } + else + { + py_command = command.to_py_object(); + } + PyObject* py_extruders = extruder::build_py_object(p_extruders, num_extruders); + if (py_extruders == NULL) + { + return NULL; + } + PyObject* p_position = Py_BuildValue( + "{s:O,s:O,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i}", + "parsed_command", + py_command, + "extruders", + py_extruders, + // FLOATS + "x_firmware_offset", + x_firmware_offset, + "y_firmware_offset", + y_firmware_offset, + "z_firmware_offset", + z_firmware_offset, + "x", + x, + "y", + y, + "z", + z, + "f", + f, + "e", + x_offset, + "y_offset", + y_offset, + "z_offset", + z_offset, + "last_extrusion_height", + last_extrusion_height, + "height", + height, + "firmware_retraction_length", + 0.0, + "firmware_unretraction_additional_length", + 0.0, + "firmware_retraction_feedrate", + 0.0, + "firmware_unretraction_feedrate", + 0.0, + "firmware_z_lift", + 0.0, + "z_relative", + z_relative, + // Ints + "layer", + layer, + "height_increment", + height_increment, + "height_increment_change_count", + height_increment_change_count, + "current_tool", + current_tool, + "num_extruders", + num_extruders, + // Bools + "x_null", + (long int)(x_null ? 1 : 0), + "y_null", + (long int)(y_null ? 1 : 0), + "z_null", + (long int)(z_null ? 1 : 0), + "f_null", + (long int)(f_null ? 1 : 0), + "x_homed", + (long int)(x_homed ? 1 : 0), + "y_homed", + (long int)(y_homed ? 1 : 0), + "z_homed", + (long int)(z_homed ? 1 : 0), + "is_relative", + (long int)(is_relative ? 1 : 0), + "is_relative_null", + (long int)(is_relative_null ? 1 : 0), + "is_extruder_relative", + (long int)(is_extruder_relative ? 1 : 0), + "is_extruder_relative_null", + (long int)(is_extruder_relative_null ? 1 : 0), + "is_metric", + (long int)(is_metric ? 1 : 0), + "is_metric_null", + (long int)(is_metric_null ? 1 : 0), + "is_printer_primed", + (long int)(is_printer_primed ? 1 : 0), + "last_extrusion_height_null", + (long int)(last_extrusion_height_null ? 1 : 0), + "firmware_retraction_length_null", + (long int)(false ? 1 : 0), + "firmware_unretraction_additional_length_null", + (long int)(false ? 1 : 0), + "firmware_retraction_feedrate_null", + (long int)(false ? 1 : 0), + "firmware_unretraction_feedrate_null", + (long int)(false ? 1 : 0), + "firmware_z_lift_null", + (long int)(false ? 1 : 0), + "has_position_error", + (long int)(false ? 1 : 0), + "has_definite_position", + (long int)(has_definite_position ? 1 : 0), + "is_layer_change", + (long int)(is_layer_change ? 1 : 0), + "is_height_change", + (long int)(is_height_change ? 1 : 0), + "is_height_increment_change", + (long int)(is_height_increment_change ? 1 : 0), + "is_xy_travel", + (long int)(is_xy_travel ? 1 : 0), + "is_xyz_travel", + (long int)(is_xyz_travel ? 1 : 0), + "is_zhop", + (long int)(is_zhop ? 1 : 0), + "has_xy_position_changed", + (long int)(has_xy_position_changed ? 1 : 0), + "has_position_changed", + (long int)(has_position_changed ? 1 : 0), + "has_received_home_command", + (long int)(has_received_home_command ? 1 : 0), + "is_in_position", + (long int)(is_in_position ? 1 : 0), + "in_path_position", + (long int)(in_path_position ? 1 : 0), + "file_line_number", + file_line_number, + "file_position", + file_position, + "gcode_number", + gcode_number, + "is_in_bounds", + (long int)(is_in_bounds ? 1 : 0) + ); + if (p_position == NULL) + { + std::string message = "position.to_py_dict: Unable to convert position value to a dict PyObject via Py_BuildValue."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return NULL; + } + Py_DECREF(py_command); + Py_DECREF(py_extruders); - PyObject * p_position = Py_BuildValue( - "{s:O,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:d,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i,s:i}", - "parsed_command", - py_command, - // FLOATS - "x", - x_, - "y", - y_, - "z", - z_, - "f", - f_, - "e", - e_, - "x_offset", - x_offset_, - "y_offset", - y_offset_, - "z_offset", - z_offset_, - "e_offset", - e_offset_, - "last_extrusion_height", - last_extrusion_height_, - "height", - height_, - "current_height_increment_", - current_height_increment_, - "firmware_retraction_length", - firmware_retraction_length_, - "firmware_unretraction_additional_length", - firmware_unretraction_additional_length_, - "firmware_retraction_feedrate", - firmware_retraction_feedrate_, - "firmware_unretraction_feedrate", - firmware_unretraction_feedrate_, - "firmware_z_lift", - firmware_z_lift_, - "e_relative", - e_relative_, - "z_relative", - z_relative_, - "extrusion_length", - extrusion_length_, - "extrusion_length_total", - extrusion_length_total_, - "retraction_length", - retraction_length_, - "deretraction_length", - deretraction_length_, - "layer", - layer_, - "x_null", - x_null_, - "y_null", - y_null_, - "z_null", - z_null_, - "f_null", - f_null_, - "x_homed", - x_homed_, - "y_homed", - y_homed_, - "z_homed", - z_homed_, - "is_relative", - is_relative_, - "is_relative_null", - is_relative_null_, - "is_extruder_relative", - is_extruder_relative_, - "is_extruder_relative_null", - is_extruder_relative_null_, - "is_metric", - is_metric_, - "is_metric_null", - is_metric_null_, - "is_printer_primed", - is_printer_primed_, - "last_extrusion_height_null", - last_extrusion_height_null_, - "firmware_retraction_length_null", - firmware_retraction_length_null_, - "firmware_unretraction_additional_length_null", - firmware_unretraction_additional_length_null_, - "firmware_retraction_feedrate_null", - firmware_retraction_feedrate_null_, - "firmware_unretraction_feedrate_null", - firmware_unretraction_feedrate_null_, - "firmware_z_lift_null", - firmware_z_lift_null_, - "has_position_error", - has_position_error_, - "has_homed_position", - has_homed_position_, - "is_extruding_start", - is_extruding_start_, - "is_extruding", - is_extruding_, - "is_primed", - is_primed_, - "is_retracting_start", - is_retracting_start_, - "is_retracting", - is_retracting_, - "is_retracted", - is_retracted_, - "is_partially_retracted", - is_partially_retracted_, - "is_deretracting_start", - is_deretracting_start_, - "is_deretracting", - is_deretracting_, - "is_deretracted", - is_deretracted_, - "is_layer_change", - is_layer_change_, - "is_height_change", - is_height_change_, - "is_xy_travel", - is_xy_travel_, - "is_xyz_travel", - is_xyz_travel_, - "is_zhop", - is_zhop_, - "has_xy_position_changed", - has_xy_position_changed_, - "has_position_changed", - has_position_changed_, - "has_state_changed", - has_state_changed_, - "has_received_home_command", - has_received_home_command_, - "is_in_position", - is_in_position_, - "in_path_position", - in_path_position_, - "file_line_number", - file_line_number_, - "gcode_number", - gcode_number_, - "is_in_bounds", - is_in_bounds_ - ); - if (p_position == NULL) - { - return NULL; - } - return p_position; + return p_position; } diff --git a/octoprint_octolapse/data/lib/c/position.h b/octoprint_octolapse/data/lib/c/position.h index b7431f80..61e2e817 100644 --- a/octoprint_octolapse/data/lib/c/position.h +++ b/octoprint_octolapse/data/lib/c/position.h @@ -24,107 +24,89 @@ #define POSITION_H #include #include "parsed_command.h" +#include "extruder.h" #ifdef _DEBUG -#undef _DEBUG +//#undef _DEBUG #include -#define _DEBUG +//python311_d.lib #else #include #endif -class position +struct position { -public: - position(); - position(position &source); - position(const std::string& xyz_axis_default_mode, const std::string& e_axis_default_mode, const std::string& - units_default); - ~position(); - static void copy(position *source, position* target); - static void copy(position *source, parsed_command* source_command, position* target); - void reset_state(); - PyObject * to_py_tuple(); - PyObject * to_py_dict(); - parsed_command* p_command; - double f_; - bool f_null_; - double x_; - bool x_null_; - double x_offset_; - bool x_homed_; - double y_; - bool y_null_; - double y_offset_; - bool y_homed_; - double z_; - bool z_null_; - double z_offset_; - bool z_homed_; - double e_; - double e_offset_; - bool is_relative_; - bool is_relative_null_; - bool is_extruder_relative_; - bool is_extruder_relative_null_; - bool is_metric_; - bool is_metric_null_; - double last_extrusion_height_; - bool last_extrusion_height_null_; - long layer_; - double height_; - int current_height_increment_; - bool is_printer_primed_; - double firmware_retraction_length_; - bool firmware_retraction_length_null_; - double firmware_unretraction_additional_length_; - bool firmware_unretraction_additional_length_null_; - double firmware_retraction_feedrate_; - bool firmware_retraction_feedrate_null_; - double firmware_unretraction_feedrate_; - bool firmware_unretraction_feedrate_null_; - double firmware_z_lift_; - bool firmware_z_lift_null_; - bool has_position_error_; - std::string position_error_; - bool has_homed_position_; - double e_relative_; - double z_relative_; - double extrusion_length_; - double extrusion_length_total_; - double retraction_length_; - double deretraction_length_; - bool is_extruding_start_; - bool is_extruding_; - bool is_primed_; - bool is_retracting_start_; - bool is_retracting_; - bool is_retracted_; - bool is_partially_retracted_; - bool is_deretracting_start_; - bool is_deretracting_; - bool is_deretracted_; - bool is_layer_change_; - bool is_height_change_; - bool is_xy_travel_; - bool is_xyz_travel_; - bool is_zhop_; - bool has_position_changed_; - bool has_xy_position_changed_; - bool has_state_changed_; - bool has_received_home_command_; - bool is_in_position_; - bool in_path_position_; - int file_line_number_; - int gcode_number_; - bool gcode_ignored_; - bool is_in_bounds_; - double get_offset_x(); - double get_offset_y(); - double get_offset_z(); - double get_offset_e(); -private: - void initialize(); - static void _copy_parsed_command(parsed_command* source_command, position* target); - static void _copy_position(position* source, position* target); + position(); + position(int extruder_count); + position(const position& pos); // Copy Constructor + virtual ~position(); + position& operator=(const position& pos); + void reset_state(); + PyObject* to_py_tuple(); + PyObject* to_py_dict(); + parsed_command command; + int feature_type_tag; + double f; + bool f_null; + double x; + bool x_null; + double x_offset; + double x_firmware_offset; + bool x_homed; + double y; + bool y_null; + double y_offset; + double y_firmware_offset; + bool y_homed; + double z; + bool z_null; + double z_offset; + double z_firmware_offset; + bool z_homed; + bool is_metric; + bool is_metric_null; + double last_extrusion_height; + bool last_extrusion_height_null; + long layer; + double height; + int height_increment; + int height_increment_change_count; + bool is_printer_primed; + bool has_definite_position; + double z_relative; + bool is_relative; + bool is_relative_null; + bool is_extruder_relative; + bool is_extruder_relative_null; + bool is_layer_change; + bool is_height_change; + bool is_height_increment_change; + bool is_xy_travel; + bool is_xyz_travel; + bool is_zhop; + bool has_position_changed; + bool has_xy_position_changed; + bool has_received_home_command; + bool is_in_position; + bool in_path_position; + long file_line_number; + long gcode_number; + long file_position; + bool gcode_ignored; + bool is_in_bounds; + bool is_empty; + int current_tool; + int num_extruders; + extruder* p_extruders; + extruder& get_current_extruder() const; + extruder& get_extruder(int index) const; + void set_num_extruders(int num_extruders_); + void delete_extruders(); + double get_gcode_x() const; + double get_gcode_y() const; + double get_gcode_z() const; + void set_xyz_axis_mode(const std::string& xyz_axis_default_mode); + void set_e_axis_mode(const std::string& e_axis_default_mode); + void set_units_default(const std::string& units_default); + bool can_take_snapshot(); }; -#endif \ No newline at end of file +#endif diff --git a/octoprint_octolapse/data/lib/c/python_helpers.cpp b/octoprint_octolapse/data/lib/c/python_helpers.cpp index 23d72b35..fe8cad13 100644 --- a/octoprint_octolapse/data/lib/c/python_helpers.cpp +++ b/octoprint_octolapse/data/lib/c/python_helpers.cpp @@ -1,59 +1,45 @@ #include "python_helpers.h" #include "logging.h" -int PyUnicode_SafeCheck(PyObject * py) + +int PyUnicode_SafeCheck(PyObject* py) { -#if PY_MAJOR_VERSION >= 3 - return PyUnicode_Check(py); -#else return PyUnicode_Check(py); -#endif } -const char* PyUnicode_SafeAsString(PyObject * py) +const char* PyUnicode_SafeAsString(PyObject* py) { -#if PY_MAJOR_VERSION >= 3 return PyUnicode_AsUTF8(py); -#else - return (char *)PyString_AsString(py); -#endif } -PyObject * PyString_SafeFromString(const char * str) +PyObject* PyString_SafeFromString(const char* str) { -#if PY_MAJOR_VERSION >= 3 - return PyUnicode_FromString(str); -#else - return PyString_FromString(str); -#endif + return PyUnicode_FromString(str); } -PyObject * PyUnicode_SafeFromString(std::string str) +PyObject* PyUnicode_SafeFromString(std::string str) { -#if PY_MAJOR_VERSION >= 3 return PyUnicode_FromString(str.c_str()); -#else - // TODO: try PyUnicode_DecodeUnicodeEscape maybe? - //return PyUnicode_DecodeUTF8(str.c_str(), NULL, "replace"); - PyObject * pyString = PyString_FromString(str.c_str()); - if (pyString == NULL) - { - PyErr_Print(); - std::string message = "Unable to convert the c_str to a python string: "; - message += str; - PyErr_SetString(PyExc_ValueError, message.c_str()); - return NULL; - } - PyObject * pyUnicode = PyUnicode_FromEncodedObject(pyString, NULL, "replace"); - Py_DECREF(pyString); - return pyUnicode; -#endif } double PyFloatOrInt_AsDouble(PyObject* py_double_or_int) { - if (PyFloat_CheckExact(py_double_or_int)) - return PyFloat_AsDouble(py_double_or_int); - else if (PyInt_CheckExact(py_double_or_int)) - return static_cast(PyInt_AsLong(py_double_or_int)); - return NULL; + if (PyFloat_CheckExact(py_double_or_int)) + return PyFloat_AsDouble(py_double_or_int); + else if (PyLong_CheckExact(py_double_or_int)) + return static_cast(PyLong_AsLong(py_double_or_int)); + return 0; +} + +long PyIntOrLong_AsLong(PyObject* value) +{ + long ret_val; + ret_val = PyLong_AsLong(value); + return ret_val; +} + +bool PyFloatLongOrInt_Check(PyObject* py_object) +{ + return ( + PyFloat_Check(py_object) || PyLong_Check(py_object) + ); } diff --git a/octoprint_octolapse/data/lib/c/python_helpers.h b/octoprint_octolapse/data/lib/c/python_helpers.h index e26a8feb..a422f5c2 100644 --- a/octoprint_octolapse/data/lib/c/python_helpers.h +++ b/octoprint_octolapse/data/lib/c/python_helpers.h @@ -1,14 +1,17 @@ #pragma once #ifdef _DEBUG -#undef _DEBUG +//#undef _DEBUG #include -#define _DEBUG +//python311_d.lib #else #include #endif #include -int PyUnicode_SafeCheck(PyObject * py); -const char* PyUnicode_SafeAsString(PyObject * py); -PyObject * PyString_SafeFromString(const char * str); -PyObject * PyUnicode_SafeFromString(std::string str); -double PyFloatOrInt_AsDouble(PyObject* py_double_or_int); \ No newline at end of file +int PyUnicode_SafeCheck(PyObject* py); +const char* PyUnicode_SafeAsString(PyObject* py); +PyObject* PyString_SafeFromString(const char* str); +PyObject* PyUnicode_SafeFromString(std::string str); +std::wstring PyObject_SafeFileNameAsWstring(PyObject* py); +double PyFloatOrInt_AsDouble(PyObject* py_double_or_int); +long PyIntOrLong_AsLong(PyObject* value); +bool PyFloatLongOrInt_Check(PyObject* value); diff --git a/octoprint_octolapse/data/lib/c/snapshot_plan.cpp b/octoprint_octolapse/data/lib/c/snapshot_plan.cpp index 6563f8b8..ea0d3260 100644 --- a/octoprint_octolapse/data/lib/c/snapshot_plan.cpp +++ b/octoprint_octolapse/data/lib/c/snapshot_plan.cpp @@ -20,252 +20,187 @@ // following email address : FormerLurker@pm.me //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "snapshot_plan.h" -#include +#include "logging.h" snapshot_plan::snapshot_plan() { - file_line = 0; - file_gcode_number = 0; - p_triggering_command = NULL; - p_initial_position = NULL; - p_return_position = NULL; - p_start_command = NULL; - p_end_command = NULL; - total_travel_distance = 0; - saved_travel_distance = 0; - triggering_command_type = trigger_position::unknown; // unknown + file_line = 0; + file_gcode_number = 0; + file_position = 0; + total_travel_distance = 0; + saved_travel_distance = 0; + distance_from_stabilization_point = 0; + triggering_command_type = position_type_unknown; // unknown + has_initial_position = false; } -snapshot_plan::snapshot_plan(const snapshot_plan & source) -{ - file_line = source.file_line; - file_gcode_number = source.file_gcode_number; - p_triggering_command = new parsed_command(*source.p_triggering_command); - p_initial_position = new position(*source.p_initial_position); - - for (unsigned int index = 0; index < source.steps.size(); index++) - { - steps.push_back(new snapshot_plan_step(*source.steps[index])); - } - - p_return_position = new position(*source.p_return_position); - p_start_command = new parsed_command(*source.p_start_command); - p_end_command = new parsed_command(*source.p_end_command); -} -snapshot_plan::~snapshot_plan() +PyObject* snapshot_plan::build_py_object(std::vector& p_plans) { - if(p_triggering_command != NULL) - { - delete p_triggering_command; - p_triggering_command = NULL; - } - if (p_initial_position != NULL) - { - delete p_initial_position; - p_initial_position = NULL; - } - if (p_return_position != NULL) - { - delete p_return_position; - p_return_position = NULL; - } - if (p_start_command != NULL) - { - delete p_start_command; - p_start_command = NULL; - } + PyObject* py_snapshot_plans = PyList_New(0); + if (py_snapshot_plans == NULL) + { + PyErr_SetString(PyExc_ValueError, + "Error executing SnapshotPlan.build_py_object: Unable to create SnapshotPlans PyList object."); + return NULL; + } - if (p_end_command != NULL) - { - delete p_end_command; - p_end_command = NULL; - } + // Create each snapshot plan + for (unsigned int plan_index = 0; plan_index < p_plans.size(); plan_index++) + { + PyObject* py_snapshot_plan = p_plans[plan_index].to_py_object(); + if (py_snapshot_plan == NULL) + { + //PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.build_py_object: Unable to convert the snapshot plan to a PyObject."); + return NULL; + } + bool success = !(PyList_Append(py_snapshot_plans, py_snapshot_plan) < 0); // reference to pSnapshotPlan stolen + if (!success) + { + PyErr_SetString(PyExc_ValueError, + "Error executing SnapshotPlan.build_py_object: Unable to append the snapshot plan to the snapshot plan list."); + return NULL; + } + // Need to decref after PyList_Append, since it increfs the PyObject + Py_DECREF(py_snapshot_plan); + //std::cout << "py_snapshot_plan refcount = " << py_snapshot_plan->ob_refcnt << "\r\n"; + } - for (std::vector::iterator step = steps.begin(); step != steps.end(); ++step) { - delete *step; - } - steps.clear(); - + return py_snapshot_plans; } -PyObject * snapshot_plan::build_py_object(std::vector p_plans) +PyObject* snapshot_plan::to_py_object() { - PyObject *py_snapshot_plans = PyList_New(0); - if (py_snapshot_plans == NULL) - { - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.build_py_object: Unable to create SnapshotPlans PyList object."); - return NULL; - } - - // Create each snapshot plan - for (unsigned int plan_index = 0; plan_index < p_plans.size(); plan_index++) - { - PyObject * py_snapshot_plan = p_plans[plan_index]->to_py_object(); - if (py_snapshot_plan == NULL) - { - //PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.build_py_object: Unable to convert the snapshot plan to a PyObject."); - return NULL; - } - bool success = !(PyList_Append(py_snapshot_plans, py_snapshot_plan) < 0); // reference to pSnapshotPlan stolen - if (!success) - { - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.build_py_object: Unable to append the snapshot plan to the snapshot plan list."); - return NULL; - } - // Need to decref after PyList_Append, since it increfs the PyObject - Py_DECREF(py_snapshot_plan); - //std::cout << "py_snapshot_plan refcount = " << py_snapshot_plan->ob_refcnt << "\r\n"; - } - - return py_snapshot_plans; -} + //std::cout << "Building Snapshot Plan Pyobject.\r\n"; + PyObject* py_triggering_command; -PyObject * snapshot_plan::to_py_object() -{ - PyObject* py_triggering_command; - if (p_triggering_command == NULL) - { - py_triggering_command = Py_None; - Py_IncRef(py_triggering_command); - } - else - { - py_triggering_command = p_triggering_command->to_py_object(); - if (py_triggering_command == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.to_py_object: Unable to convert the triggering_command to a PyObject."); - return NULL; - } - } - + if (triggering_command.is_empty) + { + py_triggering_command = Py_None; + Py_IncRef(py_triggering_command); + } + else + { + py_triggering_command = triggering_command.to_py_object(); + if (py_triggering_command == NULL) + { + return NULL; + } + } - PyObject* py_start_command = NULL; - if (p_start_command == NULL) - { - py_start_command = Py_None; - Py_IncRef(Py_None); - } - else - { - py_start_command = p_start_command->to_py_object(); - if (py_start_command == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.to_py_object: Unable to convert the start_command to a PyObject."); - return NULL; - } - } - PyObject * py_initial_position; - if (p_initial_position == NULL) - { - py_initial_position = Py_None; - Py_IncRef(py_initial_position); - } - else - { - py_initial_position = p_initial_position->to_py_tuple(); - if (py_initial_position == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.to_py_object: Unable to create InitialPosition PyObject."); - return NULL; - } - } + PyObject* py_start_command = NULL; + if (start_command.is_empty) + { + py_start_command = Py_None; + Py_IncRef(Py_None); + } + else + { + py_start_command = start_command.to_py_object(); + if (py_start_command == NULL) + { + return NULL; + } + } + //std::cout << "Building initial position..\r\n"; + PyObject* py_initial_position; + if (!has_initial_position) + { + py_initial_position = Py_None; + Py_IncRef(py_initial_position); + } + else + { + py_initial_position = initial_position.to_py_tuple(); + if (py_initial_position == NULL) + { + return NULL; + } + } + //std::cout << "Building snapshot plan steps.\r\n"; + PyObject* py_steps = PyList_New(0); + if (py_steps == NULL) + { + return NULL; + } + for (unsigned int step_index = 0; step_index < steps.size(); step_index++) + { + // create the snapshot step object with build + PyObject* py_step = steps[step_index].to_py_object(); + if (py_step == NULL) + { + return NULL; + } + bool success = !(PyList_Append(py_steps, py_step) < 0); // reference to pSnapshotPlan stolen + if (!success) + { + std::string message = + "Error executing SnapshotPlan.to_py_object: Unable to append the snapshot plan step to the snapshot plan step list via the PyList_Append method."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + // Need to decref after PyList_Append, since it increfs the PyObject + Py_DECREF(py_step); + } - PyObject * py_steps = PyList_New(0); - if (py_steps == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.to_py_object: Unable to create a PyList object to hold the snapshot plan steps."); - return NULL; - } - for (unsigned int step_index = 0; step_index < steps.size(); step_index++) - { + PyObject* py_return_position; + if (return_position.is_empty) + { + py_return_position = Py_None; + Py_IncRef(py_return_position); + } + else + { + py_return_position = return_position.to_py_tuple(); + if (py_return_position == NULL) + { + return NULL; + } + } - // create the snapshot step object with build - PyObject * py_step = steps[step_index]->to_py_object(); - if (py_step == NULL) - { - PyErr_Print(); - return NULL; - } - bool success = !(PyList_Append(py_steps, py_step) < 0); // reference to pSnapshotPlan stolen - if (!success) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.to_py_object: Unable to append the snapshot plan step to the snapshot plan step list."); - return NULL; - } - // Need to decref after PyList_Append, since it increfs the PyObject - // Todo: evaluate the effect of this - Py_DECREF(py_step); - //std::cout << "py_step refcount = " << py_step->ob_refcnt << "\r\n"; + PyObject* py_end_command; + if (end_command.is_empty) + { + py_end_command = Py_None; + Py_IncRef(py_end_command); + } + else + { + py_end_command = end_command.to_py_object(); + if (py_end_command == NULL) + { + return NULL; + } + } + PyObject* py_snapshot_plan = Py_BuildValue( + "lllddOOOOOO", + file_line, + file_gcode_number, + file_position, + total_travel_distance, + saved_travel_distance, + py_triggering_command, + py_start_command, + py_initial_position, + py_steps, + py_return_position, + py_end_command + ); + if (py_snapshot_plan == NULL) + { + std::string message = + "Error executing SnapshotPlan.to_py_object: Unable to create SnapshotPlan PyObject with the Py_BuildValue function."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } - } + Py_DECREF(py_triggering_command); + Py_DECREF(py_initial_position); + Py_DECREF(py_return_position); + Py_DECREF(py_steps); + Py_DECREF(py_start_command); + Py_DECREF(py_end_command); - PyObject * py_return_position; - if (p_return_position == NULL) - { - py_return_position = Py_None; - Py_IncRef(py_return_position); - } - else - { - py_return_position = p_return_position->to_py_tuple(); - if (py_return_position == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.to_py_object: Unable to convert the return position to a tuple."); - return NULL; - } - } - - PyObject* py_end_command; - if (p_end_command == NULL) - { - py_end_command = Py_None; - Py_IncRef(py_end_command); - } - else - { - py_end_command = p_end_command->to_py_object(); - if (py_end_command == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.to_py_object: Unable to convert the end_command to a PyObject."); - return NULL; - } - } - - PyObject *py_snapshot_plan = Py_BuildValue( - "llddOOOOOO", - file_line, - file_gcode_number, - total_travel_distance, - saved_travel_distance, - py_triggering_command, - py_start_command, - py_initial_position, - py_steps, - py_return_position, - py_end_command - ); - if (py_snapshot_plan == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlan.to_py_object: Unable to create SnapshotPlan PyObject."); - return NULL; - } - - Py_DECREF(py_triggering_command); - Py_DECREF(py_initial_position); - Py_DECREF(py_return_position); - Py_DECREF(py_steps); - Py_DECREF(py_start_command); - Py_DECREF(py_end_command); - - return py_snapshot_plan; -} \ No newline at end of file + return py_snapshot_plan; +} diff --git a/octoprint_octolapse/data/lib/c/snapshot_plan.h b/octoprint_octolapse/data/lib/c/snapshot_plan.h index fcce79f0..95aba80c 100644 --- a/octoprint_octolapse/data/lib/c/snapshot_plan.h +++ b/octoprint_octolapse/data/lib/c/snapshot_plan.h @@ -27,24 +27,27 @@ #include "trigger_position.h" #include #include + struct snapshot_plan { - snapshot_plan(); - snapshot_plan(const snapshot_plan & source); - ~snapshot_plan(); - PyObject * to_py_object(); - static PyObject * build_py_object(std::vector plans); - long file_line; - long file_gcode_number; - trigger_position::position_type triggering_command_type; - parsed_command * p_triggering_command; - parsed_command * p_start_command; - position * p_initial_position; - std::vector steps; - position * p_return_position; - parsed_command * p_end_command; - double total_travel_distance; - double saved_travel_distance; + snapshot_plan(); + PyObject* to_py_object(); + static PyObject* build_py_object(std::vector& plans); + long file_line; + long file_gcode_number; + long file_position; + position_type triggering_command_type; + feature_type triggering_command_feature_type; + parsed_command triggering_command; + parsed_command start_command; + position initial_position; + bool has_initial_position; + std::vector steps; + position return_position; + parsed_command end_command; + double distance_from_stabilization_point; + double total_travel_distance; + double saved_travel_distance; }; #endif diff --git a/octoprint_octolapse/data/lib/c/snapshot_plan_step.cpp b/octoprint_octolapse/data/lib/c/snapshot_plan_step.cpp index 0855ff40..7a1f5b3b 100644 --- a/octoprint_octolapse/data/lib/c/snapshot_plan_step.cpp +++ b/octoprint_octolapse/data/lib/c/snapshot_plan_step.cpp @@ -21,256 +21,257 @@ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "snapshot_plan_step.h" #include "python_helpers.h" -#include +#include "logging.h" + snapshot_plan_step::snapshot_plan_step() { - p_x_ = NULL; - p_y_ = NULL; - p_z_ = NULL; - p_e_ = NULL; - p_f_ = NULL; - action_ = ""; + p_x = NULL; + p_y = NULL; + p_z = NULL; + p_e = NULL; + p_f = NULL; + action = ""; } -snapshot_plan_step::snapshot_plan_step(const snapshot_plan_step & source) +snapshot_plan_step::snapshot_plan_step(const snapshot_plan_step& source) { - if (source.p_x_ != NULL) - { - p_x_ = new double; - *p_x_ = *source.p_x_; - } - else - { - p_x_ = NULL; - } + if (source.p_x != NULL) + { + p_x = new double; + *p_x = *source.p_x; + } + else + { + p_x = NULL; + } - if (source.p_y_ != NULL) - { - p_y_ = new double; - *p_y_ = *source.p_y_; - } - else - { - p_y_ = NULL; - } + if (source.p_y != NULL) + { + p_y = new double; + *p_y = *source.p_y; + } + else + { + p_y = NULL; + } - if (source.p_z_ != NULL) - { - p_z_ = new double; - *p_z_ = *source.p_z_; - } - else - { - p_z_ = NULL; - } + if (source.p_z != NULL) + { + p_z = new double; + *p_z = *source.p_z; + } + else + { + p_z = NULL; + } - if (source.p_e_ != NULL) - { - p_e_ = new double; - *p_e_ = *source.p_e_; - } - else - { - p_e_ = NULL; - } + if (source.p_e != NULL) + { + p_e = new double; + *p_e = *source.p_e; + } + else + { + p_e = NULL; + } - if (source.p_f_ != NULL) - { - p_f_ = new double; - *p_f_ = *source.p_f_; - } - else - { - p_f_ = NULL; - } + if (source.p_f != NULL) + { + p_f = new double; + *p_f = *source.p_f; + } + else + { + p_f = NULL; + } - action_ = source.action_; + action = source.action; } -snapshot_plan_step::snapshot_plan_step(double* x, double* y, double* z, double* e, double* f, std::string action) + +snapshot_plan_step::snapshot_plan_step(double* x, double* y, double* z, double* e, double* f, std::string action_type) { - if (x != NULL) - { - p_x_ = new double; - *p_x_ = *x; - } - else - { - p_x_ = NULL; - } + if (x != NULL) + { + p_x = new double; + *p_x = *x; + } + else + { + p_x = NULL; + } - if (y != NULL) - { - p_y_ = new double; - *p_y_ = *y; - } - else - { - p_y_ = NULL; - } + if (y != NULL) + { + p_y = new double; + *p_y = *y; + } + else + { + p_y = NULL; + } - if (z != NULL) - { - p_z_ = new double; - *p_z_ = *z; - } - else - { - p_z_ = NULL; - } + if (z != NULL) + { + p_z = new double; + *p_z = *z; + } + else + { + p_z = NULL; + } - if (e != NULL) - { - p_e_ = new double; - *p_e_ = *e; - } - else - { - p_e_ = NULL; - } + if (e != NULL) + { + p_e = new double; + *p_e = *e; + } + else + { + p_e = NULL; + } - if (f != NULL) - { - p_f_ = new double; - *p_f_ = *f; - } - else - { - p_f_ = NULL; - } + if (f != NULL) + { + p_f = new double; + *p_f = *f; + } + else + { + p_f = NULL; + } - action_ = action; + action = action_type; } snapshot_plan_step::~snapshot_plan_step() { - if (p_x_ != NULL) - { - delete p_x_; - p_x_ = NULL; - } - if(p_y_ != NULL) - { - delete p_y_; - p_y_ = NULL; - } - if (p_z_ != NULL) - { - delete p_z_; - p_z_ = NULL; - } - if (p_e_ != NULL) - { - delete p_e_; - p_e_ = NULL; - } - if (p_f_ != NULL) - { - delete p_f_; - p_f_ = NULL; - } - + if (p_x != NULL) + { + delete p_x; + p_x = NULL; + } + if (p_y != NULL) + { + delete p_y; + p_y = NULL; + } + if (p_z != NULL) + { + delete p_z; + p_z = NULL; + } + if (p_e != NULL) + { + delete p_e; + p_e = NULL; + } + if (p_f != NULL) + { + delete p_f; + p_f = NULL; + } } -PyObject * snapshot_plan_step::to_py_object() +PyObject* snapshot_plan_step::to_py_object() const { - PyObject * py_x; - if(p_x_ == NULL) - { - py_x = Py_None; - Py_IncRef(py_x); - } - else - { - py_x = PyFloat_FromDouble(*p_x_); - } - if (py_x == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlanStep.to_py_object: Unable to convert the X value to a python object."); - return NULL; - } - - PyObject * py_y; - if (p_y_ == NULL) - { - py_y = Py_None; - Py_IncRef(py_y); - } - else - { - py_y = PyFloat_FromDouble(*p_y_); - } - if (py_y == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlanStep.to_py_object: Unable to convert the Y value to a python object."); - return NULL; - } + PyObject* py_x; + if (p_x == NULL) + { + py_x = Py_None; + Py_IncRef(py_x); + } + else + { + py_x = PyFloat_FromDouble(*p_x); + } + if (py_x == NULL) + { + std::string message = "snapshot_plan_step.to_py_object: Unable to convert the X value to a python object."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } - PyObject * py_z; - if (p_z_ == NULL) - { - py_z = Py_None; - Py_IncRef(py_z); - } - else - { - py_z = PyFloat_FromDouble(*p_z_); - } - if (py_z == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlanStep.to_py_object: Unable to convert the Z value to a python object."); - return NULL; - } + PyObject* py_y; + if (p_y == NULL) + { + py_y = Py_None; + Py_IncRef(py_y); + } + else + { + py_y = PyFloat_FromDouble(*p_y); + } + if (py_y == NULL) + { + std::string message = "snapshot_plan_step.to_py_object: Unable to convert the Y value to a python object."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } - PyObject * py_e; - if (p_e_ == NULL) - { - py_e = Py_None; - Py_IncRef(py_e); - } - else - { - py_e = PyFloat_FromDouble(*p_e_); - } - if (py_e == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlanStep.to_py_object: Unable to convert the E value to a python object."); - return NULL; - } + PyObject* py_z; + if (p_z == NULL) + { + py_z = Py_None; + Py_IncRef(py_z); + } + else + { + py_z = PyFloat_FromDouble(*p_z); + } + if (py_z == NULL) + { + std::string message = "snapshot_plan_step.to_py_object: Unable to convert the Z value to a python object."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } - PyObject * py_f; - if (p_f_ == NULL) - { - py_f = Py_None; - Py_IncRef(py_f); - } - else - { - py_f = PyFloat_FromDouble(*p_f_); - } - if (py_f == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlanStep.to_py_object: Unable to convert the F value to a python object."); - return NULL; - } + PyObject* py_e; + if (p_e == NULL) + { + py_e = Py_None; + Py_IncRef(py_e); + } + else + { + py_e = PyFloat_FromDouble(*p_e); + } + if (py_e == NULL) + { + std::string message = "snapshot_plan_step.to_py_object: Unable to convert the E value to a python object."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } - PyObject * py_step = Py_BuildValue("sOOOOO", action_.c_str(), py_x, py_y, py_z, py_e, py_f); - if (py_step == NULL) - { - PyErr_Print(); - PyErr_SetString(PyExc_ValueError, "Error executing SnapshotPlanStep.to_py_object: Unable to create the snapshot plan step PyObject."); - return NULL; - } - Py_DecRef(py_x); - Py_DecRef(py_y); - Py_DecRef(py_z); - Py_DecRef(py_e); - Py_DecRef(py_f); - return py_step; -} \ No newline at end of file + PyObject* py_f; + if (p_f == NULL) + { + py_f = Py_None; + Py_IncRef(py_f); + } + else + { + py_f = PyFloat_FromDouble(*p_f); + } + if (py_f == NULL) + { + std::string message = "snapshot_plan_step.to_py_object: Unable to convert the F value to a python object."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + + PyObject* py_step = Py_BuildValue("sOOOOO", action.c_str(), py_x, py_y, py_z, py_e, py_f); + if (py_step == NULL) + { + std::string message = "snapshot_plan_step.to_py_object: Unable to create the snapshot plan step PyObject."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + Py_DecRef(py_x); + Py_DecRef(py_y); + Py_DecRef(py_z); + Py_DecRef(py_e); + Py_DecRef(py_f); + return py_step; +} diff --git a/octoprint_octolapse/data/lib/c/snapshot_plan_step.h b/octoprint_octolapse/data/lib/c/snapshot_plan_step.h index a7c1eb65..1d20e9fb 100644 --- a/octoprint_octolapse/data/lib/c/snapshot_plan_step.h +++ b/octoprint_octolapse/data/lib/c/snapshot_plan_step.h @@ -23,26 +23,25 @@ #define SNAPSHOT_PLAN_STEP_H #include #ifdef _DEBUG -#undef _DEBUG +//#undef _DEBUG #include -#define _DEBUG +//python311_d.lib #else #include #endif -class snapshot_plan_step +struct snapshot_plan_step { -public: - snapshot_plan_step(); - snapshot_plan_step(double* x, double* y, double* z, double* e, double* f, std::string action); - snapshot_plan_step(const snapshot_plan_step & source); - ~snapshot_plan_step(); - PyObject * to_py_object(); - double *p_x_; - double *p_y_; - double *p_z_; - double *p_e_; - double *p_f_; - std::string action_; + snapshot_plan_step(); + snapshot_plan_step(double* x, double* y, double* z, double* e, double* f, std::string action_type); + snapshot_plan_step(const snapshot_plan_step& source); + ~snapshot_plan_step(); + PyObject* to_py_object() const; + double* p_x; + double* p_y; + double* p_z; + double* p_e; + double* p_f; + std::string action; }; #endif diff --git a/octoprint_octolapse/data/lib/c/stabilization.cpp b/octoprint_octolapse/data/lib/c/stabilization.cpp index cff16289..8fbbf46c 100644 --- a/octoprint_octolapse/data/lib/c/stabilization.cpp +++ b/octoprint_octolapse/data/lib/c/stabilization.cpp @@ -20,18 +20,21 @@ // following email address : FormerLurker@pm.me //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "stabilization.h" -#include -#include -#include +#include #include +#include #include "logging.h" +#include "utilities.h" +#include +#include -stabilization::stabilization( - gcode_position_args* position_args, stabilization_args* stab_args, pythonGetCoordinatesCallback get_coordinates_callback, pythonProgressCallback progress -) +stabilization::stabilization(gcode_position_args position_args, stabilization_args stab_args, + pythonGetCoordinatesCallback get_coordinates_callback, + PyObject* py_get_coordinates_callback, pythonProgressCallback progress_callback, + PyObject* py_progress_callback) { std::string errors_; - if (stab_args->py_on_progress_received != NULL && stab_args->py_get_snapshot_position_callback != NULL) + if (py_get_coordinates_callback != NULL && py_progress_callback != NULL) { has_python_callbacks_ = true; } @@ -39,16 +42,25 @@ stabilization::stabilization( { has_python_callbacks_ = false; } - progress_callback_ = progress; + progress_callback_ = progress_callback; _get_coordinates_callback = get_coordinates_callback; + py_on_progress_received = py_progress_callback; + py_get_snapshot_position_callback = py_get_coordinates_callback; native_progress_callback_ = NULL; - p_stabilization_args_ = stab_args; + stabilization_args_ = stab_args; + gcode_position_args_ = position_args; is_running_ = true; - gcode_parser_ = new gcode_parser(); - gcode_position_ = new gcode_position(position_args); + gcode_parser_ = NULL; + gcode_position_ = NULL; file_size_ = 0; lines_processed_ = 0; gcodes_processed_ = 0; + file_position_ = 0; + missed_snapshots_ = 0; + snapshots_enabled_ = true; + stabilization_x_ = 0; + stabilization_y_ = 0; + update_stabilization_coordinates(); } stabilization::stabilization() @@ -57,42 +69,84 @@ stabilization::stabilization() has_python_callbacks_ = false; native_progress_callback_ = NULL; progress_callback_ = NULL; - p_stabilization_args_ = new stabilization_args(); + stabilization_args_ = stabilization_args(); + gcode_position_args_ = gcode_position_args(); is_running_ = true; gcode_parser_ = NULL; gcode_position_ = NULL; file_size_ = 0; lines_processed_ = 0; gcodes_processed_ = 0; + file_position_ = 0; + missed_snapshots_ = 0; + stabilization_x_ = 0; + stabilization_y_ = 0; + _get_coordinates_callback = NULL; + py_on_progress_received = NULL; + py_get_snapshot_position_callback = NULL; + snapshots_enabled_ = true; + stabilization_x_ = 0; + stabilization_y_ = 0; + update_stabilization_coordinates(); } -stabilization::stabilization(gcode_position_args* position_args, stabilization_args* args, progressCallback progress) +stabilization::stabilization(gcode_position_args position_args, stabilization_args args, progressCallback progress) { - std::string errors_; + std::string errors_; has_python_callbacks_ = false; native_progress_callback_ = progress; progress_callback_ = NULL; - p_stabilization_args_ = args; + stabilization_args_ = args; + gcode_position_args_ = position_args; is_running_ = true; - gcode_parser_ = new gcode_parser(); - gcode_position_ = new gcode_position(position_args); + gcode_parser_ = NULL; + gcode_position_ = NULL; file_size_ = 0; lines_processed_ = 0; gcodes_processed_ = 0; + file_position_ = 0; + missed_snapshots_ = 0; + stabilization_x_ = 0; + stabilization_y_ = 0; + _get_coordinates_callback = NULL; + py_on_progress_received = NULL; + py_get_snapshot_position_callback = NULL; + snapshots_enabled_ = true; } -stabilization::stabilization(const stabilization &source) +stabilization::stabilization(const stabilization& source) { // Private copy constructor, don't copy me! + throw std::exception(); } stabilization::~stabilization() +{ + delete_gcode_parser(); + delete_gcode_position(); + + if (gcode_position_ != NULL) + { + delete gcode_position_; + gcode_position_ = NULL; + } + if (py_on_progress_received != NULL) + Py_XDECREF(py_on_progress_received); + if (py_get_snapshot_position_callback != NULL) + Py_XDECREF(py_get_snapshot_position_callback); +} + +void stabilization::delete_gcode_parser() { if (gcode_parser_ != NULL) { delete gcode_parser_; gcode_parser_ = NULL; } +} + +void stabilization::delete_gcode_position() +{ if (gcode_position_ != NULL) { delete gcode_position_; @@ -113,7 +167,7 @@ long stabilization::get_file_size(const std::string& file_path) double stabilization::get_next_update_time() const { - return clock() + (p_stabilization_args_->notification_period_seconds * CLOCKS_PER_SEC); + return clock() + (stabilization_args_.notification_period_seconds * CLOCKS_PER_SEC); } double stabilization::get_time_elapsed(double start_clock, double end_clock) @@ -121,126 +175,473 @@ double stabilization::get_time_elapsed(double start_clock, double end_clock) return static_cast(end_clock - start_clock) / CLOCKS_PER_SEC; } -void stabilization::process_file(stabilization_results* results) +stabilization_results stabilization::process_file() { - - p_snapshot_plans_ = &results->snapshot_plans_; - //std::cout << "stabilization::process_file - Processing file.\r\n"; - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Processing File."); + if (gcode_parser_ != NULL) + { + delete gcode_parser_; + gcode_parser_ = NULL; + } + if (gcode_position_ != NULL) + { + delete gcode_position_; + gcode_position_ = NULL; + } + on_processing_start(); + // Construct the gcode_parser and gcode_position objects. + gcode_parser_ = new gcode_parser(); + gcode_position_ = new gcode_position(gcode_position_args_); + // Create a stringstream we can use for messaging. + std::stringstream stream; + // Make sure snapshots are enabled at the start of the process. + snapshots_enabled_ = true; + int read_lines_before_clock_check = 2000; + std::cout << "stabilization::process_file - Processing file.\r\n"; + stream << "Stabilizing file at: " << stabilization_args_.file_path; + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, stream.str()); is_running_ = true; - + double next_update_time = get_next_update_time(); const clock_t start_clock = clock(); + file_size_ = get_file_size(stabilization_args_.file_path); + + std::string path = stabilization_args_.file_path; + + +#ifdef _MSC_VER + - // todo : clear out everything for a fresh go! - file_size_ = get_file_size(p_stabilization_args_->file_path); + std::wstring wpath = utilities::ToUtf16(path); + stream << "Windows detected, encoding file as UTF16"; + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, stream.str()); + std::ifstream gcodeFile(wpath.c_str()); - //std::ifstream gcodeFile(p_stabilization_args_->file_path.c_str()); - FILE *gcodeFile = fopen(p_stabilization_args_->file_path.c_str(), "r"); - char line[9999]; - if (gcodeFile != NULL) +#else + std::ifstream gcodeFile(path.c_str()); +#endif + + + + std::string line; + int lines_with_no_commands = 0; + if (gcodeFile.is_open()) { + stream.clear(); + stream.str(""); + stream << "Opened file for reading. File Size: " << utilities::to_string(file_size_); + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, stream.str()); + parsed_command cmd; // Communicate every second - parsed_command* cmd = new parsed_command(); - while (fgets(line, 9999, gcodeFile) && is_running_) + while (std::getline(gcodeFile, line) && is_running_) { + file_position_ = static_cast(gcodeFile.tellg()); lines_processed_++; - cmd->clear(); - //std::cout << "stabilization::process_file - parsing gcode: " << line << "..."; - gcode_parser_->try_parse_gcode(line, cmd); - //std::cout << "Complete.\r\n"; - if (!cmd->cmd_.empty()) - { + cmd.clear(); + bool found_command = gcode_parser_->try_parse_gcode(line.c_str(), cmd); + bool has_gcode = false; + if (cmd.gcode.length() > 0) + { + has_gcode = true; gcodes_processed_++; - //std::cout << "stabilization::process_file - updating position..."; - gcode_position_->update(cmd, lines_processed_, gcodes_processed_); - //std::cout << "Complete.\r\n"; - process_pos(gcode_position_->get_current_position(), gcode_position_->get_previous_position()); - if (next_update_time < clock()) + } + else + { + lines_with_no_commands++; + } + // If the current command is an @Octolapse command, check the paramaters and update any state as necessary + if (cmd.command == "@OCTOLAPSE") + { + if (cmd.parameters.size() == 1) + { + parsed_command_parameter param = cmd.parameters[0]; + if (param.name == "STOP-SNAPSHOTS") + { + if (snapshots_enabled_) + { + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, + "@Octolapse command detected - STOP-SNAPSHOTS - snapshots stopped."); + snapshots_enabled_ = false; + } + else + { + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, + "@Octolapse command detected - STOP-SNAPSHOTS - snapshots already stopped, command ignored."); + } + } + else if (param.name == "START-SNAPSHOTS") + { + if (!snapshots_enabled_) + { + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, + "@Octolapse command detected - START-SNAPSHOTS - snapshots started."); + snapshots_enabled_ = true; + } + else + { + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, + "@Octolapse command detected - START-SNAPSHOTS - snapshots already started, command ignored."); + } + } + } + } + + // Always process the command through the printer, even if no command is found + // This is important so that comments can be analyzed + //std::cout << "stabilization::process_file - updating position..."; + gcode_position_->update(cmd, lines_processed_, gcodes_processed_, file_position_); + + // Only continue to process if we've found a command. + if (has_gcode) + { + if (snapshots_enabled_) + { + position* currentPositionPtr = gcode_position_->get_current_position_ptr(); + if (stabilization_args_.allow_snapshot_commands && process_snapshot_command(currentPositionPtr) && currentPositionPtr->can_take_snapshot()) + { + // If we've received a snapshot command, and this isn't the smart gcode stabilizastion, add the snapshot + add_plan_plan_from_snapshot_command(currentPositionPtr); + } + else + { + // process the position as usual + process_pos(currentPositionPtr, gcode_position_->get_previous_position_ptr(), found_command); + } + + } + + if ((lines_processed_ % read_lines_before_clock_check) == 0 && next_update_time < clock()) { // ToDo: tellg does not do what I think it does, but why? - long currentPosition = ftell (gcodeFile); - long bytesRemaining = file_size_ - currentPosition; - double percentProgress = (double)currentPosition / (double)file_size_*100.0; + long bytesRemaining = file_size_ - file_position_; + double percentProgress = static_cast(file_position_) / static_cast(file_size_) * 100.0; double secondsElapsed = get_time_elapsed(start_clock, clock()); - double bytesPerSecond = (double)currentPosition / secondsElapsed; + double bytesPerSecond = static_cast(file_position_) / secondsElapsed; double secondsToComplete = bytesRemaining / bytesPerSecond; //std::cout << "stabilization::process_file - notifying progress..."; + + stream.clear(); + stream.str(""); + stream << "Stabilization Progress - Bytes Remaining: " << bytesRemaining << + ", Seconds Elapsed: " << utilities::to_string(secondsElapsed) << ", Percent Progress:" << utilities:: + to_string(percentProgress); + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::DEBUG, stream.str()); notify_progress(percentProgress, secondsElapsed, secondsToComplete, gcodes_processed_, lines_processed_); //std::cout << "Complete.\r\n"; next_update_time = get_next_update_time(); } - } } // deallocate the parsed_command object - - fclose(gcodeFile); + + gcodeFile.close(); on_processing_complete(); - delete cmd; //std::cout << "stabilization::process_file - Completed Processing file.\r\n"; } - //else - //{ - // octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::ERROR, "Unable to open the gcode file."); - //} + else + { + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::ERROR, "Unable to open the gcode file for processing."); + } const clock_t end_clock = clock(); const double total_seconds = static_cast(end_clock - start_clock) / CLOCKS_PER_SEC; - results->success_ = errors_.empty(); - results->errors_ = errors_; - results->seconds_elapsed_ = total_seconds; - results->gcodes_processed_ = gcodes_processed_; - results->lines_processed_ = lines_processed_; - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Completed file processing."); - p_snapshot_plans_ = NULL; + stabilization_results results; + results.seconds_elapsed = total_seconds; + results.gcodes_processed = gcodes_processed_; + results.lines_processed = lines_processed_; + results.quality_issues = get_quality_issues(); + results.snapshot_plans = p_snapshot_plans_; + results.processing_issues = get_processing_issues(); + // Calculate number of missed layers + results.missed_layer_count = missed_snapshots_; + stream.clear(); + stream.str(""); + stream << "Completed file processing\r\n"; + stream << "\tBytes Processed : " << file_position_ << "\r\n"; + stream << "\tLines Processed : " << lines_processed_ << "\r\n"; + stream << "\tSnapshots Found : " << results.snapshot_plans.size() << "\r\n"; + stream << "\tTotal Seconds : " << total_seconds << "\r\n"; + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, stream.str()); + // Try to avoid logging the snapshot plan if it definitely won't be logged + if (octolapse_may_be_logged(octolapse_log::SNAPSHOT_PLAN, octolapse_log::DEBUG)) + { + stream.clear(); + stream.str(""); + stream << "Snapshot Plan Details:"; + for (unsigned int index = 0; index < results.snapshot_plans.size(); index++) + { + snapshot_plan pPlan = results.snapshot_plans[index]; + std::string gcode = pPlan.start_command.gcode; + std::string feature_type_description = "unknown"; + if (pPlan.triggering_command_feature_type != feature_type_unknown_feature) + { + feature_type_description = "feature-"; + feature_type_description += feature_type_name[pPlan.triggering_command_feature_type]; + } + else + feature_type_description = position_type_name[pPlan.triggering_command_type]; + stream << "\r\n"; + stream << "\tPlan# " << index + 1; + stream << ", Layer:" << pPlan.initial_position.layer; + stream << ", Line:" << pPlan.file_line; + stream << ", Position:" << pPlan.file_position; + stream << ", StartX:" << pPlan.initial_position.x; + stream << ", StartY:" << pPlan.initial_position.y; + stream << ", StartZ:" << pPlan.initial_position.z; + stream << ", Tool:" << pPlan.initial_position.current_tool; + stream << ", Speed:" << pPlan.initial_position.f; + stream << ", Distance:" << pPlan.distance_from_stabilization_point; + stream << ", Travel Distance:" << pPlan.total_travel_distance; + stream << ", Type:" << feature_type_description; + stream << ", Gcode:" << pPlan.start_command.gcode; + if (pPlan.start_command.comment.length() > 0) + stream << ", Comment: " << pPlan.start_command.comment; + } + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::DEBUG, stream.str()); + } + return results; } -void stabilization::notify_progress(const double percent_progress, const double seconds_elapsed, const double seconds_to_complete, - const long gcodes_processed, const long lines_processed) +void stabilization::notify_progress(const double percent_progress, const double seconds_elapsed, + const double seconds_to_complete, + const int gcodes_processed, const int lines_processed) { if (has_python_callbacks_) { - is_running_ = progress_callback_(p_stabilization_args_->py_on_progress_received, percent_progress, seconds_elapsed, seconds_to_complete, gcodes_processed, lines_processed); + is_running_ = progress_callback_(py_on_progress_received, percent_progress, seconds_elapsed, seconds_to_complete, + gcodes_processed, lines_processed); } - else if(native_progress_callback_ != NULL) + else if (native_progress_callback_ != NULL) { - is_running_ = native_progress_callback_(percent_progress, seconds_elapsed, seconds_to_complete, gcodes_processed, lines_processed); + is_running_ = native_progress_callback_(percent_progress, seconds_elapsed, seconds_to_complete, gcodes_processed, + lines_processed); } - } -void stabilization::process_pos(position* current_pos, position* previous_pos) +void stabilization::process_pos(position* current_pos, position* previous_pos, bool found_command) { throw std::exception(); } +void stabilization::on_processing_start() +{ + // empty by default +} + void stabilization::on_processing_complete() { throw std::exception(); } -void stabilization::get_next_xy_coordinates(double *x, double*y) +std::vector stabilization::get_quality_issues() +{ + throw std::exception(); +} + +std::vector stabilization::get_internal_processing_issues() { + return std::vector(); +} + +std::vector stabilization::get_processing_issues() +{ + // Create a vector to hold the processing issue + std::vector issues; + // retrieve the current position object; + position pos = gcode_position_->get_current_position(); + + if (pos.is_relative_null) + { + // Add issue for missing xyz axis type + stabilization_processing_issue issue; + issue.description = "The XYZ axis mode was not set in the gcode file."; + issue.issue_type = stabilization_processing_issue_type_xyz_axis_mode_unknown; + issues.push_back(issue); + } + + if (pos.is_extruder_relative_null) + { + // Add issue for missing e axis type + stabilization_processing_issue issue; + issue.description = "The E axis mode was not set in the gcode file."; + issue.issue_type = stabilization_processing_issue_type_e_axis_mode_unknown; + issues.push_back(issue); + } + + if (!pos.has_definite_position) + { + // Add issue for no definite position + stabilization_processing_issue issue; + issue.description = "No definite position found."; + issue.issue_type = stabilization_processing_issue_type_no_definite_position; + issues.push_back(issue); + } + + if (!pos.is_printer_primed) + { + // Add issue for no prime detected + stabilization_processing_issue issue; + issue.description = "Priming was not detected."; + issue.issue_type = stabilization_processing_issue_type_printer_not_primed; + issues.push_back(issue); + } + + if (pos.is_metric_null) + { + // Add issue for metri null + stabilization_processing_issue issue; + issue.description = "Units are not metric."; + issue.issue_type = stabilization_processing_issue_type_no_metric_units; + issues.push_back(issue); + } + + // Get all internal issues of any override classes + std::vector internal_issues = get_internal_processing_issues(); + // Add these issues to the master list + for (std::vector::iterator it = internal_issues.begin(); it != internal_issues.end(); + ++it) + { + issues.push_back(*it); + } + return issues; +} + + +void stabilization::get_next_xy_coordinates(double& x, double& y) const +{ + //octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Getting stabilization coordinates."); //std::cout << "Getting XY stabilization coordinates..."; + double x_ret, y_ret; if (has_python_callbacks_) { //std::cout << "calling python..."; - if (!_get_coordinates_callback(p_stabilization_args_->py_get_snapshot_position_callback, p_stabilization_args_->x_coordinate, p_stabilization_args_->y_coordinate, x, y)) + if (!_get_coordinates_callback(py_get_snapshot_position_callback, stabilization_args_.x_coordinate, + stabilization_args_.y_coordinate, x_ret, y_ret)) octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::INFO, "Failed dto get snapshot coordinates."); } else { //std::cout << "extracting from args..."; - *x = p_stabilization_args_->x_coordinate; - *y = p_stabilization_args_->y_coordinate; + x_ret = stabilization_args_.x_coordinate; + y_ret = stabilization_args_.y_coordinate; } + x = x_ret; + y = y_ret; //std::cout << " - X coord: " << x; //std::cout << " - Y coord: " << y << "\r\n"; } +void stabilization::process_snapshot_command_parameters(position* p_cur_pos) +{ + double x = stabilization_x_; + double y = stabilization_y_; + for (std::vector::const_iterator it = p_cur_pos->command.parameters.begin(); it != p_cur_pos + ->command + .parameters + .end(); + ++it) + { + if ((*it).name == "X") + { + x = (*it).double_value; + } + else if ((*it).name == "Y") + { + y = (*it).double_value; + } + } +} + +bool stabilization::process_snapshot_command(position* p_cur_pos) +{ + if (p_cur_pos->command.command == "@OCTOLAPSE") + { + bool ret_val = false; + for (std::vector::const_iterator it = p_cur_pos->command.parameters.begin(); it != + p_cur_pos->command.parameters.end(); ++it) + { + if ((*it).name == "TAKE-SNAPSHOT") + { + // Todo: Figure out what to do here + //process_snapshot_command_parameters(p_cur_pos); + ret_val = true; + break; + } + } + return ret_val; + } + + else if ( + stabilization_args_.snapshot_command.gcode.size() > 0 && + ( + stabilization_args_.snapshot_command.gcode == p_cur_pos->command.gcode + ) + ){ + return true; + } + else if (p_cur_pos->command.gcode == "SNAP") // Backwards Compatibility + { + return true; + } + return false; +} + +void stabilization::add_plan_plan_from_snapshot_command(position* p_position) +{ + //std::cout << "Adding saved plan to plans... F Speed" << p_saved_position_->f_ << " \r\n"; + snapshot_plan p_plan; + double total_travel_distance; + total_travel_distance = utilities::get_cartesian_distance(p_position->x, p_position->y, stabilization_x_, + stabilization_y_); + p_plan.total_travel_distance = total_travel_distance * 2; + p_plan.saved_travel_distance = 0; + p_plan.distance_from_stabilization_point = total_travel_distance; + p_plan.triggering_command_type = position_type_unknown; + p_plan.triggering_command_feature_type = static_cast(p_position->feature_type_tag); + // create the initial position + p_plan.triggering_command = p_position->command; + p_plan.start_command = p_position->command; + p_plan.initial_position = *p_position; + p_plan.has_initial_position = true; + const bool all_stabilizations_disabled = stabilization_args_.x_stabilization_disabled && stabilization_args_. + y_stabilization_disabled; + if (!all_stabilizations_disabled) + { + double x_stabilization, y_stabilization; + if (stabilization_args_.x_stabilization_disabled) + x_stabilization = p_position->x; + else + x_stabilization = stabilization_x_; + + if (stabilization_args_.y_stabilization_disabled) + y_stabilization = p_position->y; + else + y_stabilization = stabilization_y_; + + const snapshot_plan_step p_travel_step(&x_stabilization, &y_stabilization, NULL, NULL, NULL, travel_action); + p_plan.steps.push_back(p_travel_step); + } + + const snapshot_plan_step p_snapshot_step(NULL, NULL, NULL, NULL, NULL, snapshot_action); + p_plan.steps.push_back(p_snapshot_step); + + p_plan.return_position = *p_position; + + p_plan.file_line = p_position->file_line_number; + p_plan.file_gcode_number = p_position->gcode_number; + p_plan.file_position = p_position->file_position; + + // Add the plan + p_snapshot_plans_.push_back(p_plan); + // get the next coordinates + update_stabilization_coordinates(); +} + +void stabilization::update_stabilization_coordinates() +{ + get_next_xy_coordinates(stabilization_x_, stabilization_y_); +} diff --git a/octoprint_octolapse/data/lib/c/stabilization.h b/octoprint_octolapse/data/lib/c/stabilization.h index 17df1c84..ed08595e 100644 --- a/octoprint_octolapse/data/lib/c/stabilization.h +++ b/octoprint_octolapse/data/lib/c/stabilization.h @@ -28,118 +28,140 @@ #include "gcode_position.h" #include "snapshot_plan.h" #include "stabilization_results.h" - +#include #ifdef _DEBUG -#undef _DEBUG +//#undef _DEBUG #include -#define _DEBUG +//python311_d.lib #else #include #endif -#include + static const char* travel_action = "travel"; static const char* snapshot_action = "snapshot"; static const char* send_parsed_command_first = "first"; static const char* send_parsed_command_last = "last"; static const char* send_parsed_command_never = "never"; -class stabilization_args { +class stabilization_args +{ public: - stabilization_args() - { - stabilization_type = ""; - height_increment = 0.0; - notification_period_seconds = 0.25; - file_path = ""; - py_get_snapshot_position_callback = NULL; - py_gcode_generator = NULL; - py_on_progress_received = NULL; - x_coordinate = 0; - y_coordinate = 0; - x_stabilization_disabled = false; - y_stabilization_disabled = false; - } - ~stabilization_args() - { - if (py_get_snapshot_position_callback != NULL) - Py_XDECREF(py_get_snapshot_position_callback); - if (py_gcode_generator != NULL) - Py_XDECREF(py_gcode_generator); - if (py_on_progress_received != NULL) - Py_XDECREF(py_on_progress_received); - } - PyObject* py_on_progress_received; - PyObject * py_get_snapshot_position_callback; - PyObject * py_gcode_generator; - std::string stabilization_type; - std::string file_path; - double height_increment; - double notification_period_seconds; - - /** - * \brief If true, the x axis will stabilize at the layer change point. - */ - bool x_stabilization_disabled; - /** - * \brief If true, the y axis will stabilize at the layer change point. - */ - bool y_stabilization_disabled; + stabilization_args() + { + height_increment = 0.0; + notification_period_seconds = 0.25; + file_path = ""; + x_coordinate = 0; + y_coordinate = 0; + x_stabilization_disabled = false; + y_stabilization_disabled = false; + allow_snapshot_commands = true; + snapshot_command_text = "@OCTOLAPSE TAKE-SNAPSHOT"; + snapshot_command.command = "@OCTOLAPSE"; + parsed_command_parameter parameter; + parameter.name = "TAKE-SNAPSHOT"; + snapshot_command.gcode = "@OCTOLAPSE TAKE-SNAPSHOT"; + snapshot_command.parameters.push_back(parameter); + } + + ~stabilization_args() + { + } + + std::string file_path; + double height_increment; + double notification_period_seconds; - double x_coordinate; - double y_coordinate; + /** + * \brief If true, only @Octolapse commands will be processed. + */ + bool allow_snapshot_commands; + + /** + * \brief If true, the x axis will stabilize at the layer change point. + */ + bool x_stabilization_disabled; + /** + * \brief If true, the y axis will stabilize at the layer change point. + */ + bool y_stabilization_disabled; + + double x_coordinate; + double y_coordinate; + parsed_command snapshot_command; + std::string snapshot_command_text; }; -typedef bool(*progressCallback)(double percentComplete, double seconds_elapsed, double estimatedSecondsRemaining, long gcodesProcessed, long linesProcessed); -typedef bool(*pythonProgressCallback)(PyObject* python_progress_callback, double percentComplete, double seconds_elapsed, double estimatedSecondsRemaining, long gcodesProcessed, long linesProcessed); -typedef bool(*pythonGetCoordinatesCallback)(PyObject* py_get_snapshot_position_callback, double x_initial, double y_initial, double* x_result, double* y_result); + +typedef bool (*progressCallback)(double percentComplete, double seconds_elapsed, double estimatedSecondsRemaining, + long gcodesProcessed, long linesProcessed); +typedef bool (*pythonProgressCallback)(PyObject* python_progress_callback, double percentComplete, + double seconds_elapsed, double estimatedSecondsRemaining, int gcodesProcessed, + int linesProcessed); +typedef bool (*pythonGetCoordinatesCallback)(PyObject* py_get_snapshot_position_callback, double x_initial, + double y_initial, double& x_result, double& y_result); class stabilization { public: - stabilization(); - // constructor for use when running natively - stabilization(gcode_position_args* position_args, stabilization_args* args, progressCallback progress); - // constructor for use when being called from python - stabilization(gcode_position_args* position_args, stabilization_args* args, pythonGetCoordinatesCallback get_coordinates, pythonProgressCallback progress); - virtual ~stabilization(); - void process_file(stabilization_results* results); - + stabilization(); + // constructor for use when running natively + stabilization(gcode_position_args position_args, stabilization_args args, progressCallback progress); + // constructor for use when being called from python + stabilization(gcode_position_args position_args, stabilization_args args, + pythonGetCoordinatesCallback get_coordinates, PyObject* py_get_coordinates_callback, + pythonProgressCallback progress, PyObject* py_progress_callback); + virtual ~stabilization(); + stabilization_results process_file(); + private: - stabilization(const stabilization &source); // don't copy me! - double get_next_update_time() const; - static double get_time_elapsed(double start_clock, double end_clock); - bool has_python_callbacks_; - // False if return < 0, else true - pythonGetCoordinatesCallback _get_coordinates_callback; - void notify_progress(double percent_progress, double seconds_elapsed, double seconds_to_complete, - long gcodes_processed, long lines_processed); - gcode_position_args* p_args_; - // current stabilization point + stabilization(const stabilization& source); // don't copy me! + double get_next_update_time() const; + static double get_time_elapsed(double start_clock, double end_clock); + bool has_python_callbacks_; + // False if return < 0, else true + pythonGetCoordinatesCallback _get_coordinates_callback; + void notify_progress(double percent_progress, double seconds_elapsed, double seconds_to_complete, + int gcodes_processed, int lines_processed); - double stabilization_x_; - double stabilization_y_; -protected: - /** - * \brief Gets the next xy stabilization point - * \param x The current x stabilization point, will be replaced with the next x point. - * \param y The current y stabilization point, will be replaced with the next y point - */ - void get_next_xy_coordinates(double *x, double *y); - virtual void process_pos(position* p_current_pos, position* p_previous_pos); - virtual void on_processing_complete(); - std::vector* p_snapshot_plans_; - bool is_running_; - std::string errors_; - stabilization_args* p_stabilization_args_; - progressCallback native_progress_callback_; - pythonProgressCallback progress_callback_; - gcode_position* gcode_position_; - gcode_parser* gcode_parser_; - long get_file_size(const std::string& file_path); - long file_size_; - long lines_processed_; - long gcodes_processed_; + PyObject* py_on_progress_received; + PyObject* py_get_snapshot_position_callback; - +protected: + /** + * \brief Gets the next xy stabilization point + * \param x The current x stabilization point, will be replaced with the next x point. + * \param y The current y stabilization point, will be replaced with the next y point + */ + void delete_gcode_parser(); + void delete_gcode_position(); + void get_next_xy_coordinates(double& x, double& y) const; + virtual void process_pos(position* p_current_pos, position* p_previous_pos, bool found_command); + virtual void on_processing_start(); + virtual void on_processing_complete(); + virtual std::vector get_internal_processing_issues(); + virtual std::vector get_quality_issues(); + virtual std::vector get_processing_issues(); + bool process_snapshot_command(position* p_cur_pos); + void process_snapshot_command_parameters(position* p_cur_pos); + void add_plan_plan_from_snapshot_command(position* p_position); + void update_stabilization_coordinates(); + std::vector p_snapshot_plans_; + bool is_running_; + gcode_position_args gcode_position_args_; + stabilization_args stabilization_args_; + progressCallback native_progress_callback_; + pythonProgressCallback progress_callback_; + gcode_position* gcode_position_; + gcode_parser* gcode_parser_; + long get_file_size(const std::string& file_path); + long file_size_; + int lines_processed_; + int gcodes_processed_; + long file_position_; + int missed_snapshots_; + bool snapshots_enabled_; + double stabilization_x_; + double stabilization_y_; }; -#endif \ No newline at end of file +#endif diff --git a/octoprint_octolapse/data/lib/c/stabilization_results.cpp b/octoprint_octolapse/data/lib/c/stabilization_results.cpp index 3a9c831f..c0b9142d 100644 --- a/octoprint_octolapse/data/lib/c/stabilization_results.cpp +++ b/octoprint_octolapse/data/lib/c/stabilization_results.cpp @@ -20,19 +20,147 @@ // following email address : FormerLurker@pm.me //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "stabilization_results.h" +#include "logging.h" +#include "python_helpers.h" stabilization_results::stabilization_results() { - bool success = false; - double seconds_elapsed = 0; - long gcodes_processed = 0; - long lines_processed = 0; + gcodes_processed = 0; + lines_processed = 0; + missed_layer_count = 0; + seconds_elapsed = 0; +} + +PyObject* stabilization_results::to_py_object() +{ + PyObject* py_snapshot_plans = snapshot_plan::build_py_object(snapshot_plans); + if (py_snapshot_plans == NULL) + { + return NULL; + } + + // Create stabilization quality issues list + PyObject* py_quality_issues = PyList_New(0); + if (py_quality_issues == NULL) + { + std::string message = "stabilization_results.to_py_object - Unable to create py_issues PyList object."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + + // Create each quality issue and append to list + for (unsigned int issue_index = 0; issue_index < quality_issues.size(); issue_index++) + { + PyObject* py_issue = quality_issues[issue_index].to_py_object(); + if (py_issue == NULL) + { + return NULL; + } + bool success = !(PyList_Append(py_quality_issues, py_issue) < 0); // reference to pSnapshotPlan stolen + if (!success) + { + std::string message = + "stabilization_results.to_py_object - Unable to append the quality issue to the quality issues list."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + // Need to decref after PyList_Append, since it increfs the PyObject + Py_DECREF(py_issue); + } + + // Create each processing issue and append to list + PyObject* py_processing_issues = PyList_New(0); + for (unsigned int issue_index = 0; issue_index < processing_issues.size(); issue_index++) + { + PyObject* py_issue = processing_issues[issue_index].to_py_object(); + if (py_issue == NULL) + { + return NULL; + } + bool success = !(PyList_Append(py_processing_issues, py_issue) < 0); // reference to pSnapshotPlan stolen + if (!success) + { + std::string message = + "stabilization_results.to_py_object - Unable to append the processing issue to the quality issues list."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + // Need to decref after PyList_Append, since it increfs the PyObject + Py_DECREF(py_issue); + } + + PyObject* py_results = Py_BuildValue("(O,d,l,l,l,O,O)", py_snapshot_plans, seconds_elapsed, gcodes_processed, + lines_processed, missed_layer_count, py_quality_issues, py_processing_issues); + if (py_results == NULL) + { + std::string message = "stabilization_results.to_py_object - Unable to create a Tuple from the snapshot plan list."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + Py_DECREF(py_snapshot_plans); + Py_DECREF(py_quality_issues); + Py_DECREF(py_processing_issues); + + return py_results; +} + + +PyObject* stabilization_quality_issue::to_py_object() const +{ + PyObject* py_results = Py_BuildValue("(l,s)", static_cast(issue_type), description.c_str()); + if (py_results == NULL) + { + std::string message = + "stabilization_quality_issue.to_py_object - Unable to create a Tuple from a stabilization_quality_issue."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + return py_results; +} + +PyObject* stabilization_processing_issue::to_py_object() const +{ + PyObject* py_replacement_tokens_dict = PyDict_New(); + if (py_replacement_tokens_dict == NULL) + { + std::string message = + "Error executing stabilization_processing_issue.build_py_object: Unable to create issue replacement token PyDict object."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + // Create each replacement token + for (std::vector::const_iterator it = replacement_tokens.begin(); it != replacement_tokens.end(); + ++it) + { + PyObject* py_replacement_value = PyString_SafeFromString((*it).value.c_str()); + if (py_replacement_value == NULL) + { + std::string message = + "Error executing stabilization_processing_issue.build_py_object: Unable to create issue replacement token value PyString object from the token value string."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + + bool success = !(PyDict_SetItemString(py_replacement_tokens_dict, (*it).key.c_str(), py_replacement_value) < 0); + // reference to pSnapshotPlan stolen + if (!success) + { + std::string message = + "Error executing stabilization_processing_issue.build_py_object: Unable to append the issue replacement token to the to the replacement tokens dict."; + octolapse_log_exception(octolapse_log::GCODE_POSITION, message); + return NULL; + } + // Need to decref after PyList_Append, since it increfs the PyObject + Py_DECREF(py_replacement_value); + } + PyObject* py_results = Py_BuildValue("(l,s,O)", static_cast(issue_type), description.c_str(), + py_replacement_tokens_dict); + if (py_results == NULL) + { + std::string message = + "stabilization_processing_issue.to_py_object - Unable to create a Tuple from a stabilization_processing_issue."; + octolapse_log_exception(octolapse_log::SNAPSHOT_PLAN, message); + return NULL; + } + return py_results; } -stabilization_results::~stabilization_results() -{ - for (std::vector::iterator plan = snapshot_plans_.begin(); plan != snapshot_plans_.end(); ++plan) - { - delete *plan; - } - snapshot_plans_.clear(); -} \ No newline at end of file diff --git a/octoprint_octolapse/data/lib/c/stabilization_results.h b/octoprint_octolapse/data/lib/c/stabilization_results.h index 05b2b630..4d1a473e 100644 --- a/octoprint_octolapse/data/lib/c/stabilization_results.h +++ b/octoprint_octolapse/data/lib/c/stabilization_results.h @@ -24,18 +24,57 @@ #include #include #include "snapshot_plan.h" -class stabilization_results + +enum stabilization_quality_issue_type { -public: - stabilization_results(); - virtual ~stabilization_results(); - - bool success_; - std::string errors_; - std::vector snapshot_plans_; - double seconds_elapsed_; - long gcodes_processed_; - long lines_processed_; + stabilization_quality_issue_fast_trigger = 1, + stabilization_quality_issue_snap_to_print_low_quality = 2, + stabilization_quality_issue_no_print_features = 3 }; +enum stabilization_processing_issue_type +{ + stabilization_processing_issue_type_xyz_axis_mode_unknown = 1, + stabilization_processing_issue_type_e_axis_mode_unknown = 2, + stabilization_processing_issue_type_no_definite_position = 3, + stabilization_processing_issue_type_printer_not_primed = 4, + stabilization_processing_issue_type_no_metric_units = 5, + stabilization_processing_issue_type_no_snapshot_commands_found = 6 +}; + +struct stabilization_quality_issue +{ + std::string description; + stabilization_quality_issue_type issue_type; + PyObject* to_py_object() const; +}; + +struct replacement_token +{ + std::string key; + std::string value; +}; + +struct stabilization_processing_issue +{ + std::string description; + stabilization_processing_issue_type issue_type; + std::vector replacement_tokens; + PyObject* to_py_object() const; +}; + +struct stabilization_results +{ + stabilization_results(); + PyObject* to_py_object(); + std::vector snapshot_plans; + double seconds_elapsed; + long gcodes_processed; + long lines_processed; + int missed_layer_count; + std::vector quality_issues; + std::vector processing_issues; +}; + + #endif diff --git a/octoprint_octolapse/data/lib/c/stabilization_smart_gcode.cpp b/octoprint_octolapse/data/lib/c/stabilization_smart_gcode.cpp new file mode 100644 index 00000000..bd8c328b --- /dev/null +++ b/octoprint_octolapse/data/lib/c/stabilization_smart_gcode.cpp @@ -0,0 +1,118 @@ +#include "stabilization_smart_gcode.h" +#include "utilities.h" +#include "trigger_position.h" + +stabilization_smart_gcode::stabilization_smart_gcode() +{ + // Initialize travel args + smart_gcode_args_ = smart_gcode_args(); + // initialize layer/height tracking variables + snapshot_commands_found_ = 0; + // Make sure the default smart gocde processing is skipped so we can track errors + stabilization_args_.allow_snapshot_commands = false; +} + +stabilization_smart_gcode::stabilization_smart_gcode(gcode_position_args position_args, stabilization_args stab_args, + smart_gcode_args mt_args, progressCallback progress) : + stabilization(position_args, stab_args, progress) +{ + // Initialize travel args + smart_gcode_args_ = mt_args; + // initialize layer/height tracking variables + snapshot_commands_found_ = 0; + // Make sure the default smart gocde processing is skipped so we can track errors + stabilization_args_.allow_snapshot_commands = false; + update_stabilization_coordinates(); +} + +stabilization_smart_gcode::stabilization_smart_gcode(gcode_position_args position_args, stabilization_args stab_args, + smart_gcode_args mt_args, + pythonGetCoordinatesCallback get_coordinates, + PyObject* py_get_coordinates_callback, + pythonProgressCallback progress, PyObject* py_progress_callback) : + stabilization(position_args, stab_args, get_coordinates, py_get_coordinates_callback, progress, py_progress_callback) +{ + // Initialize travel args + smart_gcode_args_ = mt_args; + // initialize layer/height tracking variables + snapshot_commands_found_ = 0; + // Make sure the default smart gocde processing is skipped so we can track errors + stabilization_args_.allow_snapshot_commands = false; + update_stabilization_coordinates(); +} + +stabilization_smart_gcode::~stabilization_smart_gcode() +{ +} + +stabilization_smart_gcode::stabilization_smart_gcode(const stabilization_smart_gcode& source) +{ +} + +void stabilization_smart_gcode::process_pos(position* p_current_pos, position* p_previous_pos, bool found_command) +{ + if (process_snapshot_command(p_current_pos)) + { + snapshot_commands_found_++; + if (!p_current_pos->can_take_snapshot()) + { + missed_snapshots_++; + // ToDo: Record more details about missed snapshot + } + else + { + add_plan_plan_from_snapshot_command(p_current_pos); + } + } +} + +void stabilization_smart_gcode::on_processing_complete() +{ +} + +std::vector stabilization_smart_gcode::get_quality_issues() +{ + return std::vector(); +} + +std::vector stabilization_smart_gcode::get_internal_processing_issues() +{ + std::vector issues; + if (snapshot_commands_found_ == 0) + { + stabilization_processing_issue issue; + issue.description = "No snapshot commands were found."; + replacement_token snapshot_command_text_token; + snapshot_command_text_token.key = "snapshot_command_text"; + if (stabilization_args_.snapshot_command.gcode.size() > 0) + { + snapshot_command_text_token.value = ", "; + snapshot_command_text_token.value += stabilization_args_.snapshot_command_text; + } + else + { + snapshot_command_text_token.value = ""; + } + issue.replacement_tokens.push_back(snapshot_command_text_token); + + replacement_token snapshot_command_token; + snapshot_command_token.key = "snapshot_command_gcode"; + if (stabilization_args_.snapshot_command.gcode.size() > 0) + { + snapshot_command_token.value = ", "; + snapshot_command_token.value += stabilization_args_.snapshot_command.gcode; + } + else + { + snapshot_command_token.value = ""; + } + issue.replacement_tokens.push_back(snapshot_command_token); + + + issue.issue_type = stabilization_processing_issue_type_no_snapshot_commands_found; + issues.push_back(issue); + } + return issues; +} + + diff --git a/octoprint_octolapse/data/lib/c/stabilization_smart_gcode.h b/octoprint_octolapse/data/lib/c/stabilization_smart_gcode.h new file mode 100644 index 00000000..a34c5940 --- /dev/null +++ b/octoprint_octolapse/data/lib/c/stabilization_smart_gcode.h @@ -0,0 +1,35 @@ +#pragma once +#include "stabilization.h" +#include "parsed_command.h" +#include "parsed_command_parameter.h" +static const char* SMART_GCODE_STABILIZATION = "smart_gcode"; + +struct smart_gcode_args +{ + smart_gcode_args() + { + } +}; + +class stabilization_smart_gcode : + public stabilization +{ +public: + stabilization_smart_gcode(); + stabilization_smart_gcode(gcode_position_args position_args, stabilization_args stab_args, smart_gcode_args mt_args, + progressCallback progress); + stabilization_smart_gcode(gcode_position_args position_args, stabilization_args stab_args, smart_gcode_args mt_args, + pythonGetCoordinatesCallback get_coordinates, PyObject* py_get_coordinates_callback, + pythonProgressCallback progress, PyObject* py_progress_callback); + virtual ~stabilization_smart_gcode(); +private: + static std::string default_snapshot_gcode_; + stabilization_smart_gcode(const stabilization_smart_gcode& source); // don't copy me + void process_pos(position* p_current_pos, position* p_previous_pos, bool found_command) override; + void on_processing_complete() override; + std::vector get_quality_issues() override; + std::vector get_internal_processing_issues() override; + smart_gcode_args smart_gcode_args_; + int snapshot_commands_found_; + +}; diff --git a/octoprint_octolapse/data/lib/c/stabilization_smart_layer.cpp b/octoprint_octolapse/data/lib/c/stabilization_smart_layer.cpp index 1811402c..5cdd5e26 100644 --- a/octoprint_octolapse/data/lib/c/stabilization_smart_layer.cpp +++ b/octoprint_octolapse/data/lib/c/stabilization_smart_layer.cpp @@ -1,364 +1,315 @@ #include "stabilization_smart_layer.h" #include "utilities.h" #include "logging.h" -#include -stabilization_smart_layer::stabilization_smart_layer() : closest_positions_(0) +stabilization_smart_layer::stabilization_smart_layer() { - // Initialize travel args - p_smart_layer_args_ = new smart_layer_args(); - // initialize layer/height tracking variables - is_layer_change_wait_ = false; - current_layer_ = 0; - current_height_increment_ = 0; - has_one_extrusion_speed_ = true; - last_tested_gcode_number_ = -1; - stabilization_x_ = 0; - stabilization_y_ = 0; - current_layer_saved_extrusion_speed_ = -1; - standard_layer_trigger_distance_ = 0.0; + // Initialize travel args + smart_layer_args_ = smart_layer_args(); + // initialize layer/height tracking variables + is_layer_change_wait_ = false; + has_one_extrusion_speed_ = true; + last_tested_gcode_number_ = -1; + current_layer_saved_extrusion_speed_ = -1; + standard_layer_trigger_distance_ = 0.0; + fastest_extrusion_speed_ = -1; + slowest_extrusion_speed_ = -1; + last_snapshot_layer_ = 0; + last_snapshot_height_increment_change_count_ = 0; + trigger_position_args default_args; + closest_positions_.initialize(default_args); } stabilization_smart_layer::stabilization_smart_layer( - gcode_position_args* position_args, stabilization_args* stab_args, smart_layer_args* mt_args, progressCallback progress -) :stabilization(position_args, stab_args, progress), closest_positions_(mt_args->distance_threshold_percent) + gcode_position_args position_args, stabilization_args stab_args, smart_layer_args mt_args, progressCallback progress +) : stabilization(position_args, stab_args, progress) { - is_layer_change_wait_ = false; - current_layer_ = 0; - current_height_increment_ = 0; - has_one_extrusion_speed_ = true; - last_tested_gcode_number_ = -1; - - p_smart_layer_args_ = mt_args; - - // initialize closest extrusion/travel tracking structs - stabilization_x_ = 0; - stabilization_y_ = 0; - current_layer_saved_extrusion_speed_ = -1; - standard_layer_trigger_distance_ = 0.0; - // Get the initial stabilization coordinates - get_next_xy_coordinates(&stabilization_x_, &stabilization_y_); + is_layer_change_wait_ = false; + last_snapshot_layer_ = 0; + last_snapshot_height_increment_change_count_ = 0; + has_one_extrusion_speed_ = true; + last_tested_gcode_number_ = -1; + fastest_extrusion_speed_ = -1; + slowest_extrusion_speed_ = -1; + + smart_layer_args_ = mt_args; + // initialize closest extrusion/travel tracking structs + current_layer_saved_extrusion_speed_ = -1; + standard_layer_trigger_distance_ = 0.0; + + trigger_position_args default_args; + default_args.type = mt_args.smart_layer_trigger_type; + default_args.minimum_speed = mt_args.speed_threshold; + default_args.snap_to_print_high_quality = mt_args.snap_to_print_high_quality; + default_args.x_stabilization_disabled = stab_args.x_stabilization_disabled; + default_args.y_stabilization_disabled = stab_args.y_stabilization_disabled; + closest_positions_.initialize(default_args); + last_snapshot_initial_position_.is_empty = true; + update_stabilization_coordinates(); } stabilization_smart_layer::stabilization_smart_layer( - gcode_position_args* position_args, stabilization_args* stab_args, smart_layer_args* mt_args, pythonGetCoordinatesCallback get_coordinates, pythonProgressCallback progress -) : stabilization(position_args, stab_args, get_coordinates, progress), closest_positions_(mt_args->distance_threshold_percent) + gcode_position_args position_args, stabilization_args stab_args, smart_layer_args mt_args, + pythonGetCoordinatesCallback get_coordinates, PyObject* py_get_coordinates_callback, pythonProgressCallback progress, + PyObject* py_progress_callback +) : stabilization(position_args, stab_args, get_coordinates, py_get_coordinates_callback, progress, + py_progress_callback) { - is_layer_change_wait_ = false; - current_layer_ = 0; - current_height_increment_ = 0; - has_one_extrusion_speed_ = true; - last_tested_gcode_number_ = -1; - p_smart_layer_args_ = mt_args; - // Get the initial stabilization coordinates - stabilization_x_ = 0; - stabilization_y_ = 0; - current_layer_saved_extrusion_speed_ = -1; - standard_layer_trigger_distance_ = 0.0; - get_next_xy_coordinates(&stabilization_x_, &stabilization_y_); + is_layer_change_wait_ = false; + last_snapshot_layer_ = 0; + last_snapshot_height_increment_change_count_ = 0; + has_one_extrusion_speed_ = true; + last_tested_gcode_number_ = -1; + fastest_extrusion_speed_ = -1; + slowest_extrusion_speed_ = -1; + + smart_layer_args_ = mt_args; + current_layer_saved_extrusion_speed_ = -1; + standard_layer_trigger_distance_ = 0.0; + + trigger_position_args default_args; + default_args.type = mt_args.smart_layer_trigger_type; + default_args.minimum_speed = mt_args.speed_threshold; + default_args.snap_to_print_high_quality = mt_args.snap_to_print_high_quality; + default_args.x_stabilization_disabled = stab_args.x_stabilization_disabled; + default_args.y_stabilization_disabled = stab_args.y_stabilization_disabled; + closest_positions_.initialize(default_args); + last_snapshot_initial_position_.is_empty = true; + update_stabilization_coordinates(); } -stabilization_smart_layer::stabilization_smart_layer(const stabilization_smart_layer &source) +stabilization_smart_layer::stabilization_smart_layer(const stabilization_smart_layer& source) { - } stabilization_smart_layer::~stabilization_smart_layer() { - } -smart_layer_args::trigger_type stabilization_smart_layer::get_trigger_type() +void stabilization_smart_layer::on_processing_start() { - if (p_smart_layer_args_->snap_to_print) - return smart_layer_args::fastest; - return p_smart_layer_args_->smart_layer_trigger_type; + gcode_position_args_.height_increment = stabilization_args_.height_increment; } -bool stabilization_smart_layer::can_process_position(position* p_position, trigger_position::position_type type) +void stabilization_smart_layer::update_stabilization_coordinates() { - - if (type == trigger_position::unknown) - return false; - if (p_smart_layer_args_->snap_to_print && type != trigger_position::extrusion) - return false; - - if (type == trigger_position::extrusion) - { - if (get_trigger_type() > smart_layer_args::compatibility) - return false; - } - - // check for errors in position, layer, or height - if (p_position->layer_ == 0 || p_position->x_null_ || p_position->y_null_ || p_position->z_null_) - { - return false; - } - // See if we should ignore the current position because it is not in bounds, or because it wasn't processed - if (p_position->gcode_ignored_ || !p_position->is_in_bounds_) - return false; - - return true; + const bool snap_to_print_smooth = smart_layer_args_.smart_layer_trigger_type == trigger_type_snap_to_print && + smart_layer_args_.snap_to_print_smooth; + const bool stabilization_disabled = stabilization_args_.x_stabilization_disabled && stabilization_args_. + y_stabilization_disabled; + if ( + (stabilization_disabled || snap_to_print_smooth) + && !last_snapshot_initial_position_.is_empty + ) + { + stabilization_x_ = last_snapshot_initial_position_.x; + stabilization_y_ = last_snapshot_initial_position_.y; + } + else + { + // Get the next stabilization point + get_next_xy_coordinates(stabilization_x_, stabilization_y_); + } + closest_positions_.set_stabilization_coordinates(stabilization_x_, stabilization_y_); } -void stabilization_smart_layer::process_pos(position* p_current_pos, position* p_previous_pos) +void stabilization_smart_layer::process_pos(position* p_current_pos, position* p_previous_pos, bool found_command) { - //std::cout << "StabilizationSmartLayer::process_pos - Processing Position..."; - // if we're at a layer change, add the current saved plan - if (p_current_pos->is_layer_change_ && p_current_pos->layer_ > 1) - { - is_layer_change_wait_ = true; - // get distance from current point to the stabilization point - standard_layer_trigger_distance_ = utilities::get_cartesian_distance( - p_current_pos->x_, p_current_pos->y_, - stabilization_x_, stabilization_y_ - ); - } - - if (is_layer_change_wait_ && !closest_positions_.is_empty()) - { - bool can_add_saved_plan = true; - if (p_stabilization_args_->height_increment != 0) - { - can_add_saved_plan = false; - const double increment_double = p_current_pos->height_ / p_stabilization_args_->height_increment; - unsigned const int increment = utilities::round_up_to_int(increment_double); - if (increment > current_height_increment_) - { - // We only update the height increment if we've detected extrusion on a layer - if (increment > 1.0 && !closest_positions_.is_empty()) - { - current_height_increment_ = increment; - can_add_saved_plan = true; - } - else - { - octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::WARNING, "Octolapse missed a layer while creating a snapshot plan due to a height restriction."); - } - } - } - if (can_add_saved_plan) - { - add_plan(); - } - - } - - double distance = -1; - // Get the current position type and see if we can process this type - trigger_position::position_type current_type = trigger_position::get_type(p_current_pos); - if (can_process_position(p_current_pos, current_type)) - { - // Is the endpoint of the current command closer - // Note that we need to save the position immediately - // so that the IsCloser check for the previous_pos will - // have a saved command to check. - if (is_closer(p_current_pos, current_type, distance)) - { - closest_positions_.add(current_type, distance, p_current_pos); - } - } - // If this is the first command on a new layer, the previous command is usually also a valid position - // If the last command was not examined, test it IF we are at the same z height. - if ( - last_tested_gcode_number_ != p_previous_pos->gcode_number_ && - utilities::is_equal(p_current_pos->z_, p_previous_pos->z_)) - { - trigger_position::position_type previous_type = trigger_position::get_type(p_previous_pos); - if (can_process_position(p_previous_pos, previous_type)) - { - // Calculate the distance to the previous extrusion - if(is_closer(p_previous_pos, previous_type, distance)) - closest_positions_.add(previous_type, distance, p_previous_pos); - } - } - last_tested_gcode_number_ = p_current_pos->gcode_number_; -} + if (!found_command) + return; + //std::cout << "StabilizationSmartLayer::process_pos - Processing Position..."; + if ( + p_current_pos->is_layer_change && + p_current_pos->layer > 1 -bool stabilization_smart_layer::is_closer(position * p_position, trigger_position::position_type type, double &distance) -{ - // Fist check the speed threshold if we are running a fast trigger type - // We want to ignore any extrusions that are below the speed threshold - const bool filter_extrusion_speeds = get_trigger_type() == smart_layer_args::fast && type == trigger_position::extrusion; - if (filter_extrusion_speeds) - { - // Initialize our previous extrusion speed if it's not been initialized - if (current_layer_saved_extrusion_speed_ == -1) - current_layer_saved_extrusion_speed_ = p_position->f_; - // see if we have found more than one extrusion speed - if (has_one_extrusion_speed_) - { - if ( - current_layer_saved_extrusion_speed_ != p_position->f_ || - ( - utilities::greater_than(p_smart_layer_args_->speed_threshold, 0) && - utilities::greater_than(p_position->f_, p_smart_layer_args_->speed_threshold) - ) - ) - { - has_one_extrusion_speed_ = false; - } - } - // see if we should filter out this position due to the feedrate - if (utilities::less_than_or_equal(p_position->f_, p_smart_layer_args_->speed_threshold)) - return false; - - } - - // Get the current closest position - trigger_position* p_current_closest = closest_positions_.get(type); - // Calculate the distance between the current point and the stabilization point - double x2, y2; - if (p_stabilization_args_->x_stabilization_disabled) - x2 = p_position->x_; - else - x2 = stabilization_x_; - if (p_stabilization_args_->y_stabilization_disabled) - y2 = p_position->y_; - else - y2 = stabilization_y_; - - distance = utilities::get_cartesian_distance(p_position->x_, p_position->y_, x2,y2); - // If we don't have any closest position, this is the closest - if (p_current_closest == NULL) - { - return true; - } - - if(filter_extrusion_speeds) - { - // Check to see if the current extrusion feedrate is faster than the current closest extrusion feedrate. If it is, it's our new closest - // extrusion position - if (utilities::greater_than(p_position->f_, p_current_closest->p_position->f_)) - { - return true; - } - } - // See if we have a closer position - if (utilities::greater_than(p_current_closest->distance, distance)) - { - //std::cout << " - IsCloser Complete, closer.\r\n"; - return true; - } - else if (utilities::is_equal(p_current_closest->distance, distance) && !p_snapshot_plans_->empty()) - { - //std::cout << "Closest position tie detected, "; - // get the last snapshot plan position - position* last_position = (*p_snapshot_plans_)[p_snapshot_plans_->size() - 1]->p_initial_position; - const double old_distance_from_previous = utilities::get_cartesian_distance(p_current_closest->p_position->x_, p_current_closest->p_position->y_, last_position->x_, last_position->y_); - const double new_distance_from_previous = utilities::get_cartesian_distance(p_position->x_, p_position->y_, last_position->x_, last_position->y_); - if (utilities::less_than(new_distance_from_previous, old_distance_from_previous)) - { - //std::cout << "new is closer to the last initial snapshot position.\r\n"; - return true; - } - //std::cout << "old position is closer to the last initial snapshot position.\r\n"; - } - //std::cout << " - IsCloser Complete, not closer.\r\n"; - return false; -} + ) + { + if ( + stabilization_args_.height_increment != 0 && + !(p_current_pos->is_height_increment_change && p_current_pos->height_increment > 1) + ) + { + // We need to clear all of the closest positions here, since there was not + //height increment change. Else our snapshot height incrementation may not be stable. + closest_positions_.clear(); + } + else + { + // This is either a layer change or a height increment change. + //octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::VERBOSE, "Layer change detected."); + is_layer_change_wait_ = true; -trigger_position* stabilization_smart_layer::get_closest_position() -{ - switch (get_trigger_type()) - { - case smart_layer_args::fastest: - return closest_positions_.get_fastest_position(); - case smart_layer_args::fast: - if (has_one_extrusion_speed_) - return closest_positions_.get_compatibility_position(); - return closest_positions_.get_fastest_position(); - case smart_layer_args::compatibility: - return closest_positions_.get_compatibility_position(); - case smart_layer_args::normal_quality: - return closest_positions_.get_normal_quality_position(); - case smart_layer_args::high_quality: - return closest_positions_.get_high_quality_position(); - case smart_layer_args::best_quality: - return closest_positions_.get_best_quality_position(); - default: - return NULL; - } + // Determine if we've missed a snapshot layer or height increment + if (stabilization_args_.height_increment != 0) + { + if (p_current_pos->height_increment_change_count > 2 && p_current_pos->height_increment_change_count - 2 > + last_snapshot_height_increment_change_count_) + missed_snapshots_++; + } + else + { + if (p_current_pos->layer - 2 > last_snapshot_layer_) + missed_snapshots_++; + } + + // get distance from current point to the stabilization point + standard_layer_trigger_distance_ = utilities::get_cartesian_distance( + p_current_pos->x, p_current_pos->y, + stabilization_x_, stabilization_y_ + ); + } + } + + if (is_layer_change_wait_ && !closest_positions_.is_empty()) + { + add_plan(); + } + //octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::VERBOSE, "Adding closest position."); + closest_positions_.try_add(p_current_pos, p_previous_pos); + last_tested_gcode_number_ = p_current_pos->gcode_number; } void stabilization_smart_layer::add_plan() { - trigger_position * p_closest = get_closest_position(); - if (p_closest != NULL) - { - //std::cout << "Adding saved plan to plans... F Speed" << p_saved_position_->f_ << " \r\n"; - snapshot_plan* p_plan = new snapshot_plan(); - double total_travel_distance = p_closest->distance * 2; - p_plan->total_travel_distance = total_travel_distance; - p_plan->saved_travel_distance = (standard_layer_trigger_distance_ * 2) - total_travel_distance; - p_plan->triggering_command_type = p_closest->type; - // create the initial position - p_plan->p_triggering_command = new parsed_command(*p_closest->p_position->p_command); - p_plan->p_start_command = new parsed_command(*p_closest->p_position->p_command); - p_plan->p_initial_position = new position(*p_closest->p_position); - - bool all_stabilizations_disabled = p_stabilization_args_->x_stabilization_disabled && p_stabilization_args_->y_stabilization_disabled; - - if (!(all_stabilizations_disabled || p_smart_layer_args_->snap_to_print)) - { - double x_stabilization, y_stabilization; - if (p_stabilization_args_->x_stabilization_disabled) - x_stabilization = p_closest->p_position->x_; - else - x_stabilization = stabilization_x_; - - if (p_stabilization_args_->y_stabilization_disabled) - y_stabilization = p_closest->p_position->y_; - else - y_stabilization = stabilization_y_; - - snapshot_plan_step* p_travel_step = new snapshot_plan_step(&x_stabilization, &y_stabilization, NULL, NULL, NULL, travel_action); - p_plan->steps.push_back(p_travel_step); - } - - snapshot_plan_step* p_snapshot_step = new snapshot_plan_step(NULL, NULL, NULL, NULL, NULL, snapshot_action); - p_plan->steps.push_back(p_snapshot_step); - - p_plan->p_return_position = new position(*p_closest->p_position); - p_plan->p_end_command = NULL; - - p_plan->file_line = p_closest->p_position->file_line_number_; - p_plan->file_gcode_number = p_closest->p_position->gcode_number_; - - // Add the plan - p_snapshot_plans_->push_back(p_plan); - current_layer_ = p_closest->p_position->layer_; - // only get the next coordinates if we've actually added a plan. - get_next_xy_coordinates(&stabilization_x_, &stabilization_y_); - } - else - { - std::cout << "No saved position available to add snapshot plan!"; - } - // always reset the saved positions - reset_saved_positions(); - - //std::cout << "Complete.\r\n"; + trigger_position p_closest; + if (closest_positions_.get_position(p_closest)) + { + //std::cout << "Adding saved plan to plans... F Speed" << p_saved_position_->f_ << " \r\n"; + snapshot_plan p_plan; + double total_travel_distance; + if (smart_layer_args_.smart_layer_trigger_type == trigger_type_snap_to_print) + { + total_travel_distance = 0; + } + else + { + total_travel_distance = p_closest.distance * 2; + } + + p_plan.total_travel_distance = total_travel_distance; + p_plan.saved_travel_distance = (standard_layer_trigger_distance_ * 2) - total_travel_distance; + p_plan.distance_from_stabilization_point = p_closest.distance; + p_plan.triggering_command_type = p_closest.type_position; + p_plan.triggering_command_feature_type = p_closest.type_feature; + // create the initial position + p_plan.triggering_command = p_closest.pos.command; + p_plan.start_command = p_closest.pos.command; + p_plan.initial_position = p_closest.pos; + p_plan.has_initial_position = true; + const bool all_stabilizations_disabled = stabilization_args_.x_stabilization_disabled && stabilization_args_. + y_stabilization_disabled; + + if (!(all_stabilizations_disabled || smart_layer_args_.smart_layer_trigger_type == trigger_type_snap_to_print)) + { + double x_stabilization, y_stabilization; + if (stabilization_args_.x_stabilization_disabled) + x_stabilization = p_closest.pos.x; + else + x_stabilization = stabilization_x_; + + if (stabilization_args_.y_stabilization_disabled) + y_stabilization = p_closest.pos.y; + else + y_stabilization = stabilization_y_; + + const snapshot_plan_step p_travel_step(&x_stabilization, &y_stabilization, NULL, NULL, NULL, travel_action); + p_plan.steps.push_back(p_travel_step); + } + + const snapshot_plan_step p_snapshot_step(NULL, NULL, NULL, NULL, NULL, snapshot_action); + p_plan.steps.push_back(p_snapshot_step); + + // Only add a return position if we're not using snap to print + if (smart_layer_args_.smart_layer_trigger_type != trigger_type_snap_to_print) + p_plan.return_position = p_closest.pos; + + p_plan.file_line = p_closest.pos.file_line_number; + p_plan.file_gcode_number = p_closest.pos.gcode_number; + p_plan.file_position = p_closest.pos.file_position; + + // Add the plan + p_snapshot_plans_.push_back(p_plan); + last_snapshot_initial_position_ = p_plan.initial_position; + // only get the next coordinates if we've actually added a plan. + update_stabilization_coordinates(); + + // reset the saved positions + reset_saved_positions(); + // Need to set the initial position after resetting the saved positions + closest_positions_.set_previous_initial_position(last_snapshot_initial_position_); + + // update the last snapshot layer and increment + last_snapshot_layer_ = p_closest.pos.layer; + last_snapshot_height_increment_change_count_ = p_closest.pos.height_increment_change_count; + } + else + { + octolapse_log(octolapse_log::SNAPSHOT_PLAN, octolapse_log::WARNING, "No point snapshot position found for layer."); + // reset the saved positions + reset_saved_positions(); + } + + + //std::cout << "Complete.\r\n"; } void stabilization_smart_layer::reset_saved_positions() { - // Clear the saved closest positions - closest_positions_.clear(); - // set the state for the next layer - is_layer_change_wait_ = false; - has_one_extrusion_speed_ = true; - current_layer_saved_extrusion_speed_ = -1; - standard_layer_trigger_distance_ = 0.0; + // Clear the saved closest positions + closest_positions_.clear(); + // set the state for the next layer + is_layer_change_wait_ = false; + has_one_extrusion_speed_ = true; + current_layer_saved_extrusion_speed_ = -1; + standard_layer_trigger_distance_ = 0.0; } void stabilization_smart_layer::on_processing_complete() { - //std::cout << "Running on_process_complete..."; - if (!closest_positions_.is_empty()) - { - add_plan(); - } - //std::cout << "Complete.\r\n"; -} \ No newline at end of file + // If we were on + if (!closest_positions_.is_empty()) + { + add_plan(); + } + //std::cout << "Complete.\r\n"; +} + +std::vector stabilization_smart_layer::get_quality_issues() +{ + std::vector issues; + + // Detect quality issues and return as a human readable string. + gcode_comment_processor* p_comment_processor = gcode_position_->get_gcode_comment_processor(); + if (this->smart_layer_args_.smart_layer_trigger_type == trigger_type_fast) + { + stabilization_quality_issue issue; + issue.description = + "You are using the 'Fast' smart trigger. This could lead to quality issues. If you are having print quality issues, consider using a 'high quality' or 'snap to print' smart trigger."; + issue.issue_type = stabilization_quality_issue_fast_trigger; + issues.push_back(issue); + } + else + { + if (this->smart_layer_args_.smart_layer_trigger_type == trigger_type_snap_to_print && !smart_layer_args_. + snap_to_print_high_quality) + { + stabilization_quality_issue issue; + issue.description = + "In most cases using the 'High Quality' snap to print option will improve print quality, unless you are printing with vase mode enabled."; + issue.issue_type = stabilization_quality_issue_snap_to_print_low_quality; + issues.push_back(issue); + } + + else if (p_comment_processor->get_comment_process_type() == comment_process_type_unknown) + { + stabilization_quality_issue issue; + issue.description = + "No print features were found in your gcode file. This can reduce print quality significantly. If you are using Slic3r or PrusaSlicer, please enable 'Verbose G-code' in 'Print Settings'->'Output Options'->'Output File'."; + issue.issue_type = stabilization_quality_issue_no_print_features; + issues.push_back(issue); + } + } + + return issues; +} diff --git a/octoprint_octolapse/data/lib/c/stabilization_smart_layer.h b/octoprint_octolapse/data/lib/c/stabilization_smart_layer.h index f4014bfb..3f9eba44 100644 --- a/octoprint_octolapse/data/lib/c/stabilization_smart_layer.h +++ b/octoprint_octolapse/data/lib/c/stabilization_smart_layer.h @@ -4,82 +4,70 @@ #include "position.h" #include "trigger_position.h" #ifdef _DEBUG -#undef _DEBUG +//#undef _DEBUG #include -#define _DEBUG +//python311_d.lib #endif static const char* SMART_LAYER_STABILIZATION = "smart_layer"; -/** - * \brief The type of trigger position to use when creating snapshot plans\n - * fastest - Gets the closest position\n - * fast - Gets the closest position, including the fastest extrusion movement on the current - * layer, optionally excluding extrusions with feedrates that are below or equal to an - * speed threshold. If only one extrusion speed is detected on a given layer, - * and no speed threshold is provided, use a non-extrusion position\n - * compatibility - Gets the closest non-extrusion position if possible, else returns the closest available position\n - * normal_quality - Gets a close non-extrusion position. Returns a lesser quality position if no good quality position is found.\n - * high_quality - Gets a close non-extrusion position and automatically balance time and quality Returns a lesser quality position if no good quality position is found.\n - * best_quality - gets the best non-extrusion position available. Skips snapshots if no quality position can be found.\n - */ - - struct smart_layer_args { - enum trigger_type { fastest, fast, compatibility, normal_quality, high_quality, best_quality }; - - smart_layer_args() - { - smart_layer_trigger_type = smart_layer_args::compatibility; - speed_threshold = 0; - distance_threshold_percent = 0; - snap_to_print = false; - } - smart_layer_args::trigger_type smart_layer_trigger_type; - double speed_threshold; - double distance_threshold_percent; - bool snap_to_print; + smart_layer_args() + { + smart_layer_trigger_type = trigger_type::trigger_type_compatibility; + speed_threshold = 0; + snap_to_print_high_quality = false; + snap_to_print_smooth = false; + } + + trigger_type smart_layer_trigger_type; + double speed_threshold; + bool snap_to_print_high_quality; + bool snap_to_print_smooth; }; class stabilization_smart_layer : public stabilization { public: - stabilization_smart_layer(); - stabilization_smart_layer(gcode_position_args* position_args, stabilization_args* stab_args, smart_layer_args* mt_args, progressCallback progress); - stabilization_smart_layer(gcode_position_args* position_args, stabilization_args* stab_args, smart_layer_args* mt_args, pythonGetCoordinatesCallback get_coordinates, pythonProgressCallback progress); - ~stabilization_smart_layer(); + stabilization_smart_layer(); + stabilization_smart_layer(gcode_position_args position_args, stabilization_args stab_args, smart_layer_args mt_args, + progressCallback progress); + stabilization_smart_layer(gcode_position_args position_args, stabilization_args stab_args, smart_layer_args mt_args, + pythonGetCoordinatesCallback get_coordinates, PyObject* py_get_coordinates_callback, + pythonProgressCallback progress, PyObject* py_progress_callback); + ~stabilization_smart_layer(); private: - stabilization_smart_layer(const stabilization_smart_layer &source); // don't copy me - void process_pos(position* p_current_pos, position* p_previous_pos) override; - void on_processing_complete() override; - void add_plan(); - void reset_saved_positions(); - bool can_process_position(position* p_position, trigger_position::position_type type); - smart_layer_args::trigger_type get_trigger_type(); - trigger_position* get_closest_position(); - /** - * \brief Determine if a position is closer. If necessary, filter based on speed, and also detect - * if there are multiple extrusion speeds if necessary. - * previous points, or -1 (less than 0) if it is not. - * \param p_position the position to test - * \param type_ the type of position we are comparing, either extrusion or retracted travel - * \param distance the distance between the supplied position and the stabilization point. Is set to -1 if there are errors - * \return true if the position is closer, false if it is not or if it is filtered - */ - bool is_closer(position* p_position, trigger_position::position_type type_, double &distance); - - // Layer/height tracking variables - bool is_layer_change_wait_; - int current_layer_; - int last_tested_gcode_number_; - bool has_one_extrusion_speed_; - unsigned int current_height_increment_; - double stabilization_x_; - double stabilization_y_; - double current_layer_saved_extrusion_speed_; - double standard_layer_trigger_distance_; - smart_layer_args *p_smart_layer_args_; - // closest extrusion/travel position tracking variables - trigger_positions closest_positions_; + stabilization_smart_layer(const stabilization_smart_layer& source); // don't copy me + void process_pos(position* p_current_pos, position* p_previous_pos, bool found_command) override; + void on_processing_start() override; + void on_processing_complete() override; + std::vector get_quality_issues() override; + void add_plan(); + void reset_saved_positions(); + /** + * \brief Determine if a position is closer. If necessary, filter based on speed, and also detect + * if there are multiple extrusion speeds if necessary. + * previous points, or -1 (less than 0) if it is not. + * \param p_position the position to test + * \param type_ the type of position we are comparing, either extrusion or retracted travel + * \param distance the distance between the supplied position and the stabilization point. Is set to -1 if there are errors + * \return true if the position is closer, false if it is not or if it is filtered + */ + bool is_closer(position* p_position, position_type type_, double& distance); + void update_stabilization_coordinates(); + // Layer/height tracking variables + bool is_layer_change_wait_; + int last_snapshot_layer_; + unsigned int last_snapshot_height_increment_change_count_; + int last_tested_gcode_number_; + double fastest_extrusion_speed_; + double slowest_extrusion_speed_; + bool has_one_extrusion_speed_; + double current_layer_saved_extrusion_speed_; + double standard_layer_trigger_distance_; + smart_layer_args smart_layer_args_; + position last_snapshot_initial_position_; + // closest extrusion/travel position tracking variables + trigger_positions closest_positions_; }; -#endif \ No newline at end of file +#endif diff --git a/octoprint_octolapse/data/lib/c/stabilization_snap_to_print.h b/octoprint_octolapse/data/lib/c/stabilization_snap_to_print.h index 9e9f5498..c5f02809 100644 --- a/octoprint_octolapse/data/lib/c/stabilization_snap_to_print.h +++ b/octoprint_octolapse/data/lib/c/stabilization_snap_to_print.h @@ -30,7 +30,6 @@ #include #define _DEBUG #endif -static const char* LOCK_TO_PRINT_CORNER_STABILIZATION = "lock-to-print-corner"; class stabilization_snap_to_print : public stabilization { @@ -48,7 +47,7 @@ class stabilization_snap_to_print : private: stabilization_snap_to_print(const stabilization_snap_to_print &source); // don't copy me - void process_pos(position* p_current_pos, position* p_previous_pos) override; + void process_pos(position& p_current_pos, position& p_previous_pos) override; void on_processing_complete() override; void add_saved_plan(); bool has_saved_position(); diff --git a/octoprint_octolapse/data/lib/c/trigger_position.cpp b/octoprint_octolapse/data/lib/c/trigger_position.cpp index 6d41e79c..c82869d8 100644 --- a/octoprint_octolapse/data/lib/c/trigger_position.cpp +++ b/octoprint_octolapse/data/lib/c/trigger_position.cpp @@ -1,266 +1,681 @@ #include "trigger_position.h" #include "utilities.h" -#include +#include "stabilization_smart_layer.h" -trigger_position::position_type trigger_position::get_type(position* p_position) +position_type trigger_position::get_type(position* p_pos) { - if (p_position->is_partially_retracted_ || p_position->is_deretracted_) - return trigger_position::unknown; - - if (p_position->is_extruding_ && utilities::greater_than(p_position->e_relative_, 0)) - { - return trigger_position::extrusion; - } - else if(p_position->is_xy_travel_) - { - if (p_position->is_retracted_) - { - if (p_position->is_zhop_) - return trigger_position::lifted_retracted_travel; - else - return trigger_position::retracted_travel; - } - else - { - if (p_position->is_zhop_) - return trigger_position::lifted_travel; - else - return trigger_position::travel; - } - } - else if(utilities::greater_than(p_position->z_relative_, 0)) - { - if (p_position->is_retracted_) - { - if(p_position->is_xyz_travel_) - { - if (p_position->is_zhop_) - return trigger_position::lifted_retracted_travel; - else - return trigger_position::lifting_retracted_travel; - } - else - { - if (p_position->is_zhop_) - return trigger_position::retracted_lifted; - else - return trigger_position::retracted_lifting; - } - } - else - { - if (p_position->is_xyz_travel_) - { - if (p_position->is_zhop_) - return trigger_position::lifted_travel; - else - return trigger_position::lifting_travel; - } - else - { - if (p_position->is_zhop_) - return trigger_position::lifted; - else - return trigger_position::lifting; - } - } - - } - else if(utilities::less_than(p_position->e_relative_ , 0) && p_position->is_retracted_) - { - return trigger_position::retraction; - } - else - { - return trigger_position::unknown; - } + if (p_pos->get_current_extruder().is_partially_retracted || p_pos->get_current_extruder().is_deretracted) + return position_type_unknown; + + if (p_pos->get_current_extruder().is_extruding && utilities::greater_than(p_pos->get_current_extruder().e_relative, 0) + ) + { + return position_type_extrusion; + } + else if (p_pos->is_xy_travel) + { + if (p_pos->get_current_extruder().is_retracted) + { + if (p_pos->is_zhop) + return position_type_lifted_retracted_travel; + else + return position_type_retracted_travel; + } + else + { + if (p_pos->is_zhop) + return position_type_lifted_travel; + else + return position_type_travel; + } + } + else if (utilities::greater_than(p_pos->z_relative, 0)) + { + if (p_pos->get_current_extruder().is_retracted) + { + if (p_pos->is_xyz_travel) + { + if (p_pos->is_zhop) + return position_type_lifted_retracted_travel; + else + return position_type_lifting_retracted_travel; + } + else + { + if (p_pos->is_zhop) + return position_type_retracted_lifted; + else + return position_type_retracted_lifting; + } + } + else + { + if (p_pos->is_xyz_travel) + { + if (p_pos->is_zhop) + return position_type_lifted_travel; + else + return position_type_lifting_travel; + } + else + { + if (p_pos->is_zhop) + return position_type_lifted; + else + return position_type_lifting; + } + } + } + else if (utilities::less_than(p_pos->get_current_extruder().e_relative, 0) && p_pos + ->get_current_extruder().is_retracted) + { + return position_type_retraction; + } + else + { + return position_type_unknown; + } } -trigger_positions::trigger_positions(double distance_threshold_percent) +trigger_positions::trigger_positions() { - distance_threshold_percent_ = distance_threshold_percent; - initialize_position_list(); + fastest_extrusion_speed_ = -1; + slowest_extrusion_speed_ = -1; + stabilization_x_ = 0; + stabilization_y_ = 0; } -trigger_positions::trigger_positions() +trigger_positions::~trigger_positions() { - distance_threshold_percent_ = 0; - initialize_position_list(); + clear(); } -void trigger_positions::initialize_position_list() +void trigger_positions::initialize(trigger_position_args args) { - for (int index = trigger_position::num_position_types - 1; index > -1; index--) - { - position_list_[index] = NULL; - } + clear(); + args_ = args; } -void trigger_positions::set_distance_threshold_percent(double distance_threshold_percent) +void trigger_positions::set_stabilization_coordinates(double x, double y) { - distance_threshold_percent_ = distance_threshold_percent; + stabilization_x_ = x; + stabilization_y_ = y; } -trigger_positions::~trigger_positions() + +void trigger_positions::set_previous_initial_position(position& pos) { - clear(); + previous_initial_pos_ = pos; + previous_initial_pos_.is_empty = false; } -bool trigger_positions::is_empty() +bool trigger_positions::is_empty() const { - for (unsigned int index = 0; index < trigger_position::num_position_types; index++) - { - if (position_list_[index] != NULL) - return false; - } - return true; + for (unsigned int index = 0; index < trigger_position::num_position_types; index++) + { + if (!position_list_[index].is_empty) + return false; + } + return true; } -trigger_position* trigger_positions::get_fastest_position() +bool trigger_positions::get_position(trigger_position& pos) { - trigger_position* current_closest = NULL; - // Loop backwards so that in the case of ties, the best match (the one with the higher enum value) is selected - for (int index = trigger_position::num_position_types - 1; index > -1; index--) - { - trigger_position* current_position = position_list_[index]; - - if (current_position != NULL) - { - if (current_closest == NULL || utilities::less_than(current_position->distance, current_closest->distance)) - current_closest = current_position; - } - } - return current_closest; + switch (args_.type) + { + case trigger_type_snap_to_print: + return get_snap_to_print_position(pos); + case trigger_type_fast: + return get_fast_position(pos); + case trigger_type_compatibility: + return get_compatibility_position(pos); + case trigger_type_high_quality: + return get_high_quality_position(pos); + } + return false; } -trigger_position* trigger_positions::get_compatibility_position() +// Returns the fastest extrusion position, or NULL if there is not one (including any speed requirements) +bool trigger_positions::has_fastest_extrusion_position() const { - trigger_position* current_closest = NULL; - double closest_distance; - // Loop backwards so that in the case of ties, the best match (the one with the higher enum value) is selected - for (int index = trigger_position::num_position_types - 1; index > -1; index--) - { - if (index < (int)trigger_position::quality_cutoff && current_closest != NULL) - return current_closest; - - trigger_position* current_position = position_list_[index]; - - if (current_position != NULL) - { - if (current_closest == NULL || utilities::less_than(current_position->distance, current_closest->distance)) - current_closest = current_position; - } - } - return current_closest; + // If there are no fastest speeds return null + if (slowest_extrusion_speed_ == -1 || fastest_extrusion_speed_ == -1) + { + return false; + } + + // the fastest_extrusion_speed_ must be greater than 0, else we haven't found any extrusions! + if (utilities::greater_than(fastest_extrusion_speed_, 0)) + { + // if we have a minimum speed or more than one extrusion speed was detected + if (position_list_[position_type_fastest_extrusion].is_empty) + return false; + + if (utilities::greater_than(args_.minimum_speed, 0) && utilities::greater_than_or_equal( + position_list_[position_type_fastest_extrusion].pos.f, args_.minimum_speed)) + { + return true; + } + if (utilities::less_than_or_equal(args_.minimum_speed, 0) && utilities::greater_than( + fastest_extrusion_speed_, slowest_extrusion_speed_)) + { + return true; + } + } + return false; } -trigger_position* trigger_positions::get_normal_quality_position() +// Gets the snap to print position from the position list +bool trigger_positions::get_snap_to_print_position(trigger_position& pos) { - trigger_position* current_closest = NULL; - double closest_distance; - // Loop backwards so that in the case of ties, the best match (the one with the higher enum value) is selected - for (int index = trigger_position::num_position_types - 1; index > trigger_position::extrusion; index--) - { - if (index < trigger_position::quality_cutoff && current_closest != NULL) - return current_closest; - - trigger_position* current_position = position_list_[index]; - - if (current_position != NULL) - { - if (current_closest == NULL || utilities::less_than(current_position->distance, current_closest->distance)) - current_closest = current_position; - } - } - return current_closest; + pos.is_empty = true; + const bool has_fastest_position = has_fastest_extrusion_position(); + // If we are snapping to the closest and fastest point, return that if it exists. + if (args_.snap_to_print_high_quality) + { + int current_closest_index = -1; + // First try to get the closest known high quality feature position if one exists + for (int index = NUM_FEATURE_TYPES - 1; index > feature_type::feature_type_inner_perimeter_feature - 1; index--) + { + if (!feature_position_list_[index].is_empty) + { + if (current_closest_index < 0 || utilities::less_than(feature_position_list_[index].distance, + feature_position_list_[current_closest_index].distance)) + current_closest_index = index; + } + } + if (current_closest_index > -1) + { + pos = feature_position_list_[current_closest_index]; + return true; + } + + if (has_fastest_position) + { + pos = position_list_[position_type_fastest_extrusion]; + } + else + { + pos = position_list_[position_type_extrusion]; + } + return !pos.is_empty; + } + + // If extrusion position is empty return the fastest position if it exists + if (position_list_[position_type_extrusion].is_empty && !has_fastest_position) + { + return false; + } + + if (position_list_[position_type_extrusion].is_empty) + { + pos = position_list_[position_type_fastest_extrusion]; + return true; + } + + // if the p_extrusion distance is less than or equal to the p_fastest_extrusion distance, return that. + if (utilities::less_than_or_equal(position_list_[position_type_extrusion].distance, + position_list_[position_type_fastest_extrusion].distance)) + { + pos = position_list_[position_type_extrusion]; + } + else + pos = position_list_[position_type_fastest_extrusion]; + + // return p_fastest_extrusion, which is equal to or less than the travel distance of p_extrusion + return true; } -trigger_position* trigger_positions::get_high_quality_position() +bool trigger_positions::get_fast_position(trigger_position& pos) { - trigger_position* current_closest = NULL; - for (int index = trigger_position::num_position_types - 1; index > trigger_position::extrusion; index--) - { - if (index < trigger_position::quality_cutoff && current_closest != NULL) - return current_closest; - - trigger_position* current_position = position_list_[index]; - - if (current_position != NULL) - { - if (current_closest == NULL) - { - current_closest = current_position; - } - else - { - if (!utilities::is_zero(current_position->distance)) - { - const double difference_percent = 100.0 * (1.0 - (current_position->distance / current_closest->distance)); - // If our current position is closer to the previous distance by at least the set distance threshold, - // record the current position as the closest position - if (utilities::greater_than(difference_percent, distance_threshold_percent_)) - { - current_closest = current_position; - } - } - } - } - } - return current_closest; + pos.is_empty = true; + int current_closest_index = -1; + // Loop backwards so that in the case of ties, the best match (the one with the higher enum value) is selected + for (int index = trigger_position::num_position_types - 1; index > -1; index--) + { + if (!position_list_[index].is_empty) + { + if (current_closest_index < 0 || utilities::less_than(position_list_[index].distance, + position_list_[current_closest_index].distance)) + current_closest_index = index; + } + } + if (current_closest_index > -1) + { + pos = position_list_[current_closest_index]; + return true; + } + return false; } -trigger_position* trigger_positions::get_best_quality_position() +bool trigger_positions::get_compatibility_position(trigger_position& pos) { - for (int index = trigger_position::num_position_types - 1; index > trigger_position::quality_cutoff - 1; index--) - { - trigger_position* current_position = position_list_[index]; - if (current_position != NULL) - return current_position; - } - return NULL; + for (int index = NUM_FEATURE_TYPES - 1; index > feature_type::feature_type_inner_perimeter_feature - 1; index--) + { + if (!feature_position_list_[index].is_empty) + { + pos = feature_position_list_[index]; + return true; + } + } + int current_best_index = -1; + for (int index = trigger_position::num_position_types - 1; index > -1; index--) + { + if (index == position_type_fastest_extrusion && has_fastest_extrusion_position()) + { + pos = position_list_[index]; + return true; + } + else if (!position_list_[index].is_empty) + { + pos = position_list_[index]; + return true; + } + } + return false; } -trigger_position** trigger_positions::get_all() +bool trigger_positions::get_high_quality_position(trigger_position& pos) { - return position_list_; + for (int index = NUM_FEATURE_TYPES - 1; index > feature_type_inner_perimeter_feature - 1; index--) + { + if (!feature_position_list_[index].is_empty) + { + pos = feature_position_list_[index]; + return true; + } + } + for (int index = trigger_position::num_position_types - 1; index > trigger_position::quality_cutoff - 1; index--) + { + if (index == position_type_fastest_extrusion) + { + if (has_fastest_extrusion_position()) + { + pos = position_list_[index]; + return true; + } + continue; + } + else if (!position_list_[index].is_empty) + { + pos = position_list_[index]; + return true; + } + } + return false; +} + +void trigger_positions::try_save_retracted_position(position* p_current_pos) +{ + if (p_current_pos->get_current_extruder().is_retracted) + previous_retracted_pos_ = *p_current_pos; + else if (p_current_pos->get_current_extruder().is_extruding && !p_current_pos + ->get_current_extruder().is_extruding_start) + previous_retracted_pos_.is_empty = true; +} + +void trigger_positions::try_save_primed_position(position* p_current_pos) +{ + if (p_current_pos->get_current_extruder().is_primed) + previous_primed_pos_ = *p_current_pos; + else if (p_current_pos->get_current_extruder().is_extruding && !p_current_pos + ->get_current_extruder().is_extruding_start) + previous_primed_pos_.is_empty = true; } void trigger_positions::clear() { - for (unsigned int index = 0; index < trigger_position::num_position_types; index++) - { - trigger_position* current_position = position_list_[index]; - if (current_position != NULL) - { - delete current_position; - position_list_[index] = NULL; - } - } + // reset all tracking variables + fastest_extrusion_speed_ = -1; + slowest_extrusion_speed_ = -1; + previous_initial_pos_.is_empty = true; + previous_retracted_pos_.is_empty = true; + previous_primed_pos_.is_empty = true; + + // clear out any saved positions + for (unsigned int index = 0; index < trigger_position::num_position_types; index++) + { + position_list_[index].is_empty = true; + } + + // clear out any saved feature positions + for (unsigned int index = 0; index < NUM_FEATURE_TYPES; index++) + { + feature_position_list_[index].is_empty = true; + } } -void trigger_positions::add(trigger_position::position_type type, double distance, position *p_position) +trigger_position trigger_positions::get(const position_type type) { - trigger_position* current_position = position_list_[type]; - if(current_position != NULL) - { - delete current_position; - } - position_list_[type] = new trigger_position(type, distance, p_position); + return position_list_[type]; } -void trigger_positions::add(double distance, position *p_position) + +bool trigger_positions::can_process_position(position* pos, const position_type type) { - trigger_position::position_type type = trigger_position::get_type(p_position); - trigger_position* current_position = position_list_[type]; - if (current_position != NULL) - { - delete current_position; - } - position_list_[type] = new trigger_position(type, distance, p_position); + if (type == position_type_unknown || pos->is_empty) + return false; + + + // check for errors in position, layer, or height + if (pos->layer == 0 || pos->x_null || pos->y_null || pos->z_null) + { + return false; + } + // See if we should ignore the current position because it is not in bounds, or because it wasn't processed + if (pos->gcode_ignored || !pos->is_in_bounds) + return false; + + // Never save any positions that are below the highest extrusion point. + if (utilities::less_than(pos->z, pos->last_extrusion_height)) + { + // if the current z height is less than the maximum extrusion height! + // Do not add this point else we might ram into the printed part! + // Note: This is even a problem for snap to print, since the extruder will appear to drop, which makes for a bad timelapse + return false; + } + return true; } -trigger_position* trigger_positions::get(trigger_position::position_type type) + +double trigger_positions::get_stabilization_distance(position* p_pos) const { - return position_list_[type]; + double x, y; + if (args_.x_stabilization_disabled && previous_initial_pos_.is_empty) + { + x = p_pos->x; + } + else + { + x = stabilization_x_; + } + if (args_.y_stabilization_disabled && previous_initial_pos_.is_empty) + { + y = p_pos->y; + } + else + { + y = stabilization_y_; + } + + return utilities::get_cartesian_distance(p_pos->x, p_pos->y, x, y); } +/// Try to add a position to the position list. Returns false if no position can be added. +void trigger_positions::try_add(position* p_current_pos, position* p_previous_pos) +{ + // Get the position type + const position_type type = trigger_position::get_type(p_current_pos); + + if (!can_process_position(p_current_pos, type)) + { + return; + } + + // add any feature positions if a feature tag exists, and if we are in high quality or compatibility mode + if ( + p_current_pos->feature_type_tag != feature_type::feature_type_unknown_feature && + ( + args_.type == trigger_type_high_quality || + args_.type == trigger_type_compatibility || + (args_.type == trigger_type_snap_to_print && type == position_type_extrusion && args_.snap_to_print_high_quality) + ) + ) + { + // only add features if we are extruding. + if (p_current_pos->get_current_extruder().is_extruding) + { + try_add_feature_position_internal(p_current_pos); + if (p_current_pos->get_current_extruder().is_extruding_start) + { + // if this is an extrusion_stat (also an extrusion), we will want to add the + // starting point of the extrusion as well , which would not have been marked as an extrusion + position* start_pos = NULL; + // set the latest saved retracted (preferred) or primed position + if (!previous_retracted_pos_.is_empty) + start_pos = &previous_retracted_pos_; + else + start_pos = &previous_primed_pos_; + + if ( + start_pos != NULL && + start_pos->x == p_previous_pos->x && + start_pos->y == p_previous_pos->y && + start_pos->z == p_previous_pos->z + ) + { + // If we have a starting position that matches the previous position (retracted or primed) + // try to add the previous position to the feature position list + try_add_feature_position_internal(p_previous_pos); + } + } + } + } + + if (args_.type == trigger_type_snap_to_print) + { + // Do special things for snap to print trigger + + + // If this isn't an extrusion, we might need to save some of the positions for future reference + try_save_retracted_position(p_current_pos); + try_save_primed_position(p_current_pos); + } + const double distance = get_stabilization_distance(p_current_pos); + //std::cout << "Distance:" << distance << "\r\n"; + try_add_internal(p_current_pos, distance, type); + + // If we are using snap to print, and the current position is = is_extruding_start + if (args_.type == trigger_type_snap_to_print) + { + if (p_current_pos->get_current_extruder().is_extruding_start) + { + // try to add the snap_to_print starting position + try_add_extrusion_start_positions(p_previous_pos); + } + else if (p_current_pos->get_current_extruder().is_extruding) + { + previous_retracted_pos_.is_empty = true; + previous_primed_pos_.is_empty = true; + } + } +} + +void trigger_positions::try_add_feature_position_internal(position* p_pos) +{ + bool add_position = false; + const double distance = get_stabilization_distance(p_pos); + const feature_type type = static_cast(p_pos->feature_type_tag); + + if (feature_position_list_[type].is_empty) + { + add_position = true; + } + else if (utilities::less_than(distance, feature_position_list_[type].distance)) + { + add_position = true; + } + else if (utilities::is_equal(feature_position_list_[type].distance, distance) && !previous_initial_pos_.is_empty) + { + //std::cout << "Closest position tie detected, "; + const double old_distance_from_previous = utilities::get_cartesian_distance( + feature_position_list_[type].pos.x, feature_position_list_[type].pos.y, previous_initial_pos_.x, + previous_initial_pos_.y); + const double new_distance_from_previous = utilities::get_cartesian_distance( + p_pos->x, p_pos->y, previous_initial_pos_.x, previous_initial_pos_.y); + if (utilities::less_than(new_distance_from_previous, old_distance_from_previous)) + { + //std::cout << "new is closer to the last initial snapshot position.\r\n"; + add_position = true; + } + //std::cout << "old position is closer to the last initial snapshot position.\r\n"; + } + + if (add_position) + { + // add the current position as the fastest extrusion speed + add_feature_position_internal(p_pos, distance, static_cast(type)); + } +} + +void trigger_positions::add_feature_position_internal(position* p_pos, double distance, feature_type type) +{ + feature_position_list_[p_pos->feature_type_tag].pos = *p_pos; + feature_position_list_[p_pos->feature_type_tag].distance = distance; + feature_position_list_[p_pos->feature_type_tag].type_feature = type; + feature_position_list_[p_pos->feature_type_tag].is_empty = false; +} + +// Adds a position to the internal position list. +void trigger_positions::add_internal(position* p_pos, double distance, position_type type) +{ + position_list_[type].pos = *p_pos; + position_list_[type].distance = distance; + position_list_[type].type_position = type; + position_list_[type].is_empty = false; +} + +void trigger_positions::try_add_extrusion_start_positions(position* p_extrusion_start_pos) +{ + // Try to add the start of the extrusion to the snap to print stabilization + if (!previous_retracted_pos_.is_empty) + try_add_extrusion_start_position(p_extrusion_start_pos, previous_retracted_pos_); + else if (!previous_primed_pos_.is_empty) + try_add_extrusion_start_position(p_extrusion_start_pos, previous_primed_pos_); +} + +void trigger_positions::try_add_extrusion_start_position(position* p_extrusion_start_pos, position& saved_pos) +{ + // A special case where we are trying to add a snap to print position from the start of an extrusion. + // Note that we do not need to add any checks for max speed or thresholds, since that will have been taken care of + if ( + saved_pos.x != p_extrusion_start_pos->x || + saved_pos.y != p_extrusion_start_pos->y || + saved_pos.z != p_extrusion_start_pos->z + ) + { + return; + } + + const double distance = get_stabilization_distance(&saved_pos); + + // See if we need to update the fastest extrusion position + if ( + utilities::is_equal(fastest_extrusion_speed_, p_extrusion_start_pos->f) + && utilities::less_than(distance, position_list_[position_type_fastest_extrusion].distance)) + { + // add the current position as the fastest extrusion speed + add_internal(&saved_pos, distance, position_type_fastest_extrusion); + } + + + bool add_position = false; + if (position_list_[position_type_extrusion].is_empty) + { + add_position = true; + } + else if (utilities::less_than(distance, position_list_[position_type_extrusion].distance)) + { + add_position = true; + } + else if (utilities::is_equal(position_list_[position_type_extrusion].distance, distance) && !previous_initial_pos_. + is_empty) + { + //std::cout << "Closest position tie detected, "; + const double old_distance_from_previous = utilities::get_cartesian_distance( + position_list_[position_type_extrusion].pos.x, + position_list_[position_type_extrusion].pos.y, + previous_initial_pos_.x, + previous_initial_pos_.y + ); + const double new_distance_from_previous = utilities::get_cartesian_distance( + saved_pos.x, saved_pos.y, previous_initial_pos_.x, previous_initial_pos_.y); + if (utilities::less_than(new_distance_from_previous, old_distance_from_previous)) + { + //std::cout << "new is closer to the last initial snapshot position.\r\n"; + add_position = true; + } + //std::cout << "old position is closer to the last initial snapshot position.\r\n"; + } + if (add_position) + { + // add the current position as the fastest extrusion speed + add_internal(&saved_pos, distance, position_type_extrusion); + } +} + +// Try to add a position to the internal position list. +void trigger_positions::try_add_internal(position* p_pos, double distance, position_type type) +{ + // If this is an extrusion type position, we need to handle it with care since we want to track both the closest + // extrusion and the closest extrusion at the fastest speed (inluding any speed filters that are supplied. + if (type == position_type_extrusion) + { + // First make sure to update the fastest and slowest extrusion speeds. + // important for implementing any 'min_extrusion_speed_difference_' rules. + if (slowest_extrusion_speed_ == -1 || utilities::less_than(p_pos->f, slowest_extrusion_speed_)) + { + slowest_extrusion_speed_ = p_pos->f; + } + + // See if the feedrate is faster than our minimum speed. + if (args_.minimum_speed > 0) + { + // see if we should filter out this position due to the feedrate + if (utilities::less_than_or_equal(p_pos->f, args_.minimum_speed)) + return; + } + + // Now that we've filtered any feed rates below the minimum speed, let's let's see if we've set a new speed record + bool add_fastest = false; + if (utilities::greater_than(p_pos->f, fastest_extrusion_speed_)) + { + fastest_extrusion_speed_ = p_pos->f; + add_fastest = true; + } + else if ( + utilities::is_equal(fastest_extrusion_speed_, p_pos->f) + && utilities::less_than(distance, position_list_[position_type_fastest_extrusion].distance)) + { + add_fastest = true; + } + + if (add_fastest) + { + // add the current position as the fastest extrusion speed + add_internal(p_pos, distance, position_type_fastest_extrusion); + } + } + + // See if we have a closer position for any but the 'fastest_extrusion' position (it will have been dealt with by now) + // First get the current closest position by type + + bool add_position = false; + if (position_list_[type].is_empty) + { + add_position = true; + } + else if (utilities::less_than(distance, position_list_[type].distance)) + { + add_position = true; + } + else if (utilities::is_equal(position_list_[type].distance, distance) && !previous_initial_pos_.is_empty) + { + //std::cout << "Closest position tie detected, "; + const double old_distance_from_previous = utilities::get_cartesian_distance( + position_list_[type].pos.x, position_list_[type].pos.y, previous_initial_pos_.x, previous_initial_pos_.y); + const double new_distance_from_previous = utilities::get_cartesian_distance( + p_pos->x, p_pos->y, previous_initial_pos_.x, previous_initial_pos_.y); + if (utilities::less_than(new_distance_from_previous, old_distance_from_previous)) + { + //std::cout << "new is closer to the last initial snapshot position.\r\n"; + add_position = true; + } + //std::cout << "old position is closer to the last initial snapshot position.\r\n"; + } + if (add_position) + { + // add the current position as the fastest extrusion speed + add_internal(p_pos, distance, type); + } +} diff --git a/octoprint_octolapse/data/lib/c/trigger_position.h b/octoprint_octolapse/data/lib/c/trigger_position.h index 0da80a6c..0652d728 100644 --- a/octoprint_octolapse/data/lib/c/trigger_position.h +++ b/octoprint_octolapse/data/lib/c/trigger_position.h @@ -1,62 +1,162 @@ #pragma once #include "position.h" +#include "gcode_comment_processor.h" /** * \brief A struct to hold the closest position, which is used by the stabilization preprocessors. */ -static const std::string position_type_name[13] = { - "unknown", "extrusion", "lifting", "lifted", "travel", "lifting_travel", "lifted_travel", "retraction", "retracted_lifting", "retracted_lifted", "retracted_travel", "lifting_retracted_travel"," lifted_retracted_travel" +static const std::string position_type_name[14] = { + "unknown", + "extrusion", + "lifting", + "lifted", + "travel", + "lifting_travel", + "lifted_travel", + "retraction", + "retracted_lifting", + "retracted_lifted", + "retracted_travel", + "lifting_retracted_travel", + "lifted_retracted_travel", + "fastest_extrusion" }; + +enum trigger_type +{ + trigger_type_snap_to_print, + trigger_type_fast, + trigger_type_compatibility, + trigger_type_high_quality +}; + +enum position_type +{ + position_type_unknown, + position_type_extrusion, + position_type_lifting, + position_type_lifted, + position_type_travel, + position_type_lifting_travel, + position_type_lifted_travel, + position_type_retraction, + position_type_retracted_lifting, + position_type_retracted_lifted, + position_type_retracted_travel, + position_type_lifting_retracted_travel, + position_type_lifted_retracted_travel, + position_type_fastest_extrusion +}; + struct trigger_position { - enum position_type { unknown, extrusion, lifting, lifted, travel, lifting_travel, lifted_travel, retraction, retracted_lifting, retracted_lifted, retracted_travel, lifting_retracted_travel, lifted_retracted_travel }; - static const unsigned int num_position_types = 13; - static const position_type quality_cutoff = trigger_position::retraction; - - trigger_position() - { - type = trigger_position::unknown; - distance = -1; - p_position = NULL; - } - trigger_position(position_type type_, double distance_, position* p_position_) - { - type = type_; - distance = distance_; - p_position = new position(*p_position_); - } - ~trigger_position() - { - if (p_position != NULL) - delete p_position; - } - static position_type get_type(position* p_position); - position_type type; - double distance; - position * p_position; + /** + * \brief The type of trigger position to use when creating snapshot plans\n + * fastest - Gets the closest position\n + * compatibility - Gets the best quality position available. + * high_quality - Gets the best quality position availiable, but stops searching after the quality_cutoff (retraction) + */ + + static const unsigned int num_position_types = 14; + static const position_type quality_cutoff = position_type_retraction; + + trigger_position() + { + type_position = position_type_unknown; + distance = -1; + is_empty = true; + type_feature = feature_type_unknown_feature; + } + + trigger_position(position_type type_, double distance_, position pos_) + { + type_position = type_; + distance = distance_; + pos = pos_; + is_empty = false; + type_feature = feature_type_unknown_feature; + } + + trigger_position(feature_type feature_, double distance_, position pos_) + { + type_position = position_type_unknown; + distance = distance_; + pos = pos_; + is_empty = false; + type_feature = feature_; + } + + static position_type get_type(position* p_pos); + position_type type_position; + feature_type type_feature; + double distance; + position pos; + bool is_empty; +}; + +struct trigger_position_args +{ +public: + trigger_position_args() + { + type = trigger_type_compatibility; + minimum_speed = 0; + snap_to_print_high_quality = false; + x_stabilization_disabled = true; + y_stabilization_disabled = true; + } + + trigger_type type; + double minimum_speed; + bool snap_to_print_high_quality; + bool x_stabilization_disabled; + bool y_stabilization_disabled; }; class trigger_positions { public: - trigger_positions(); - trigger_positions(double distance_threshold); - ~trigger_positions(); - trigger_position* get_fastest_position(); - trigger_position* get_compatibility_position(); - trigger_position* get_normal_quality_position(); - trigger_position* get_high_quality_position(); - trigger_position* get_best_quality_position(); - trigger_position** get_all(); - void set_distance_threshold_percent(double distance_threshold_percent); - void clear(); - void add(trigger_position::position_type type, double distance, position *p_position); - void add(double distance, position *p_position); - bool is_empty(); - trigger_position* get(trigger_position::position_type type); + trigger_positions(); + ~trigger_positions(); + bool get_position(trigger_position& pos); + + void initialize(trigger_position_args args); + void clear(); + void try_add(position* p_current_pos, position* p_previous_pos); + bool is_empty() const; + trigger_position get(position_type type); + void set_stabilization_coordinates(double x, double y); + void set_previous_initial_position(position& pos); private: - void initialize_position_list(); - trigger_position* position_list_[trigger_position::num_position_types]; - double distance_threshold_percent_; -}; + bool has_fastest_extrusion_position() const; + bool get_snap_to_print_position(trigger_position& pos); + bool get_fast_position(trigger_position& pos); + bool get_compatibility_position(trigger_position& pos); + bool get_high_quality_position(trigger_position& pos); + + double get_stabilization_distance(position* p_pos) const; + //trigger_position* get_normal_quality_position(); + void try_save_retracted_position(position* p_current_pos); + void try_save_primed_position(position* p_current_pos); + static bool can_process_position(position* pos, position_type type); + void add_internal(position* p_pos, double distance, position_type type); + void try_add_feature_position_internal(position* p_pos); + void add_feature_position_internal(position* p_pos, double distance, feature_type type); + void try_add_internal(position* p_pos, double distance, position_type type); + void try_add_extrusion_start_positions(position* p_extrusion_start_pos); + void try_add_extrusion_start_position(position* p_extrusion_start_pos, position& saved_pos); + + trigger_position position_list_[trigger_position::num_position_types]; + trigger_position feature_position_list_[NUM_FEATURE_TYPES]; + // arguments + trigger_position_args args_; + double stabilization_x_; + double stabilization_y_; + // Tracking variables + double fastest_extrusion_speed_; + double slowest_extrusion_speed_; + position previous_initial_pos_; + position previous_retracted_pos_; + position previous_primed_pos_; +}; diff --git a/octoprint_octolapse/data/lib/c/utilities.cpp b/octoprint_octolapse/data/lib/c/utilities.cpp index b23a1aeb..9f9f539e 100644 --- a/octoprint_octolapse/data/lib/c/utilities.cpp +++ b/octoprint_octolapse/data/lib/c/utilities.cpp @@ -1,57 +1,187 @@ #include "utilities.h" -#include +#include #include +#include +#include +#include + + +#ifdef _MSC_VER + #include + #include + + std::wstring utilities::ToUtf16(std::string str) + { + UINT codepage = 65001; + #ifdef IS_PYTHON_EXTENSION + codepage = 65001; // CP_UTF8 + #else + codepage = 65000; // CP_UTF7 + #endif + std::wstring ret; + int len = MultiByteToWideChar(codepage, 0, str.c_str(), str.length(), NULL, 0); + if (len > 0) + { + ret.resize(len); + MultiByteToWideChar(codepage, 0, str.c_str(), str.length(), &ret[0], len); + } + return ret; + } +#endif + + // Had to increase the zero tolerance because prusa slicer doesn't always retract enough while wiping. const double ZERO_TOLERANCE = 0.00005; +const std::string utilities::WHITESPACE_ = " \n\r\t\f\v"; int utilities::round_up_to_int(double x) { - return int(x + ZERO_TOLERANCE); + return int(x + ZERO_TOLERANCE); } bool utilities::is_equal(double x, double y) { - double abs_difference = fabs(x - y); - return abs_difference < ZERO_TOLERANCE; + double abs_difference = std::fabs(x - y); + return abs_difference < ZERO_TOLERANCE; } bool utilities::greater_than(double x, double y) { - return x > y && !is_equal(x, y); + return x > y && !is_equal(x, y); } bool utilities::greater_than_or_equal(double x, double y) { - return x > y || is_equal(x, y); + return x > y || is_equal(x, y); } bool utilities::less_than(double x, double y) { - return x < y && !is_equal(x, y); + return x < y && !is_equal(x, y); } bool utilities::less_than_or_equal(double x, double y) { - return x < y || is_equal(x, y); + return x < y || is_equal(x, y); } bool utilities::is_zero(double x) { - return fabs(x) < ZERO_TOLERANCE; + return std::fabs(x) < ZERO_TOLERANCE; } double utilities::get_cartesian_distance(double x1, double y1, double x2, double y2) { - // Compare the saved points cartesian distance from the current point - double xdif = x1 - x2; - double ydif = y1 - y2; - double dist_squared = xdif * xdif + ydif * ydif; - return sqrt(xdif*xdif + ydif * ydif); + // Compare the saved points cartesian distance from the current point + double xdif = x1 - x2; + double ydif = y1 - y2; + double dist_squared = xdif * xdif + ydif * ydif; + return sqrt(xdif * xdif + ydif * ydif); } std::string utilities::to_string(double value) { - std::ostringstream os; - os << value; - return os.str(); -} \ No newline at end of file + std::ostringstream os; + os << value; + return os.str(); +} + +std::string utilities::ltrim(const std::string& s) +{ + size_t start = s.find_first_not_of(WHITESPACE_); + return (start == std::string::npos) ? "" : s.substr(start); +} + +std::string utilities::rtrim(const std::string& s) +{ + size_t end = s.find_last_not_of(WHITESPACE_); + return (end == std::string::npos) ? "" : s.substr(0, end + 1); +} + +std::string utilities::trim(const std::string& s) +{ + return rtrim(ltrim(s)); +} + +std::istream& utilities::safe_get_line(std::istream& is, std::string& t) +{ + t.clear(); + // The characters in the stream are read one-by-one using a std::streambuf. + // That is faster than reading them one-by-one using the std::istream. + // Code that uses streambuf this way must be guarded by a sentry object. + // The sentry object performs various tasks, + // such as thread synchronization and updating the stream state. + + std::istream::sentry se(is, true); + std::streambuf* sb = is.rdbuf(); + + for (;;) + { + const int c = sb->sbumpc(); + switch (c) + { + case '\n': + return is; + case '\r': + if (sb->sgetc() == '\n') + sb->sbumpc(); + return is; + case EOF: + // Also handle the case when the last line has no line ending + if (t.empty()) + is.setstate(std::ios::eofbit); + return is; + default: + t += static_cast(c); + } + } +} + +/// +/// Checks to see if the lhs is in the rhs. +/// +/// The string to search for in the array. Case will be ignored, as will beginning and ending whitespace +/// A null terminated LOWERCASE array of const char * that is already trimmed. +/// +bool utilities::is_in_caseless_trim(const std::string& lhs, const char** rhs) +{ + unsigned int lhend = lhs.find_last_not_of(WHITESPACE_); + unsigned int lhstart = lhs.find_first_not_of(WHITESPACE_); + unsigned int size = lhend - lhstart + 1; + int index = 0; + + while (rhs[index] != NULL) + { + const char* rhString = rhs[index++]; + if (rhString == NULL) + { + break; + } + // If the sizes minus the whitespace doesn't match, the strings can't match + if (size != strlen(rhString)) + { + continue; + } + + // The sizes match, loop through and compare + bool failed = false; + for (unsigned int i = 0; i < size; ++i) + { + if (std::tolower(lhs[i + lhstart]) != rhString[i]) + { + // Something didn't match, return false + failed = true; + break; + } + } + if (!failed) + { + return true; + } + } + + // If we are here, this string does not appear + return false; +} + + diff --git a/octoprint_octolapse/data/lib/c/utilities.h b/octoprint_octolapse/data/lib/c/utilities.h index f67e70e3..4dd7ac51 100644 --- a/octoprint_octolapse/data/lib/c/utilities.h +++ b/octoprint_octolapse/data/lib/c/utilities.h @@ -1,18 +1,29 @@ #pragma once #include -class utilities{ + +class utilities +{ public: - static int round_up_to_int(double x); - static bool is_equal(double x, double y); - static bool greater_than(double x, double y); - static bool greater_than_or_equal(double x, double y); - static bool less_than(double x, double y); - static bool less_than_or_equal(double x, double y); - static bool is_zero(double x); - static double get_cartesian_distance(double x1, double y1, double x2, double y2); - static std::string to_string(double value); + static int round_up_to_int(double x); + static bool is_equal(double x, double y); + static bool greater_than(double x, double y); + static bool greater_than_or_equal(double x, double y); + static bool less_than(double x, double y); + static bool less_than_or_equal(double x, double y); + static bool is_zero(double x); + static double get_cartesian_distance(double x1, double y1, double x2, double y2); + static std::string to_string(double value); + static std::string ltrim(const std::string& s); + static std::string rtrim(const std::string& s); + static std::string trim(const std::string& s); + static std::istream& safe_get_line(std::istream& is, std::string& t); + static bool is_in_caseless_trim(const std::string& lhs, const char** rhs); +#ifdef _MSC_VER + static std::wstring ToUtf16(std::string str); + +#endif +protected: + static const std::string WHITESPACE_; private: - utilities(); - + utilities(); }; - diff --git a/octoprint_octolapse/data/scripts/gcode/cura/settings_output_v4.2.gcode b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output.gcode similarity index 87% rename from octoprint_octolapse/data/scripts/gcode/cura/settings_output_v4.2.gcode rename to octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output.gcode index 6f2d117f..09d2b0e3 100644 --- a/octoprint_octolapse/data/scripts/gcode/cura/settings_output_v4.2.gcode +++ b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output.gcode @@ -1,17 +1,19 @@ - ; Script based on an original created by tjjfvi (https://github.com/tjjfvi) ; An up-to-date version of the tjjfvi's original script can be found ; here: https://csi.t6.fyi/ ; Note - This script will only work in Cura V4.2 and above! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; magic_mesh_surface_mode = {magic_mesh_surface_mode} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings ; speed_z_hop = {speed_z_hop} ; retraction_amount = {retraction_amount} ; retraction_hop = {retraction_hop} ; retraction_hop_enabled = {retraction_hop_enabled} -; retraction_prime_speed = {retraction_prime_speed} -; retraction_retract_speed = {retraction_retract_speed} +; retraction_enable = {retraction_enable} ; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} ; speed_travel = {speed_travel} -; retraction_enable = {retraction_enable} -; layer_height = {layer_height} -; smooth_spiralized_contours = {smooth_spiralized_contours} -; magic_mesh_surface_mode = {magic_mesh_surface_mode} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_02_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_02_extruders.gcode new file mode 100644 index 00000000..e6d6a671 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_02_extruders.gcode @@ -0,0 +1,38 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.2 and above! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; magic_mesh_surface_mode = {magic_mesh_surface_mode} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; speed_z_hop = {speed_z_hop} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; speed_z_hop_0 = {speed_z_hop, 0} +; speed_z_hop_1 = {speed_z_hop, 1} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_03_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_03_extruders.gcode new file mode 100644 index 00000000..eee25614 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_03_extruders.gcode @@ -0,0 +1,48 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.2 and above! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; magic_mesh_surface_mode = {magic_mesh_surface_mode} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; speed_z_hop = {speed_z_hop} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; speed_z_hop_0 = {speed_z_hop, 0} +; speed_z_hop_1 = {speed_z_hop, 1} +; speed_z_hop_2 = {speed_z_hop, 2} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} + diff --git a/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_04_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_04_extruders.gcode new file mode 100644 index 00000000..208542aa --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_04_extruders.gcode @@ -0,0 +1,58 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.2 and above! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; magic_mesh_surface_mode = {magic_mesh_surface_mode} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; speed_z_hop = {speed_z_hop} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; speed_z_hop_0 = {speed_z_hop, 0} +; speed_z_hop_1 = {speed_z_hop, 1} +; speed_z_hop_2 = {speed_z_hop, 2} +; speed_z_hop_3 = {speed_z_hop, 3} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} + + diff --git a/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_05_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_05_extruders.gcode new file mode 100644 index 00000000..5298e105 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_05_extruders.gcode @@ -0,0 +1,66 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.2 and above! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; magic_mesh_surface_mode = {magic_mesh_surface_mode} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; speed_z_hop = {speed_z_hop} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; speed_z_hop_0 = {speed_z_hop, 0} +; speed_z_hop_1 = {speed_z_hop, 1} +; speed_z_hop_2 = {speed_z_hop, 2} +; speed_z_hop_3 = {speed_z_hop, 3} +; speed_z_hop_4 = {speed_z_hop, 4} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_amount_4 = {retraction_amount, 4} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_4 = {retraction_hop, 4} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_hop_enabled_4 = {retraction_hop_enabled, 4} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_prime_speed_4 = {retraction_prime_speed, 4} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_retract_speed_4 = {retraction_retract_speed, 4} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_speed_4 = {retraction_speed, 4} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; retraction_enable_4 = {retraction_enable, 4} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} +; speed_travel_4 = {speed_travel, 4} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_06_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_06_extruders.gcode new file mode 100644 index 00000000..c04d23a5 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_06_extruders.gcode @@ -0,0 +1,74 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.2 and above! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; magic_mesh_surface_mode = {magic_mesh_surface_mode} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; speed_z_hop = {speed_z_hop} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; speed_z_hop_0 = {speed_z_hop, 0} +; speed_z_hop_1 = {speed_z_hop, 1} +; speed_z_hop_2 = {speed_z_hop, 2} +; speed_z_hop_3 = {speed_z_hop, 3} +; speed_z_hop_4 = {speed_z_hop, 4} +; speed_z_hop_5 = {speed_z_hop, 5} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_amount_4 = {retraction_amount, 4} +; retraction_amount_5 = {retraction_amount, 5} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_4 = {retraction_hop, 4} +; retraction_hop_5 = {retraction_hop, 5} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_hop_enabled_4 = {retraction_hop_enabled, 4} +; retraction_hop_enabled_5 = {retraction_hop_enabled, 5} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_prime_speed_4 = {retraction_prime_speed, 4} +; retraction_prime_speed_5 = {retraction_prime_speed, 5} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_retract_speed_4 = {retraction_retract_speed, 4} +; retraction_retract_speed_5 = {retraction_retract_speed, 5} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_speed_4 = {retraction_speed, 4} +; retraction_speed_5 = {retraction_speed, 5} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; retraction_enable_4 = {retraction_enable, 4} +; retraction_enable_5 = {retraction_enable, 5} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} +; speed_travel_4 = {speed_travel, 4} +; speed_travel_5 = {speed_travel, 5} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_07_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_07_extruders.gcode new file mode 100644 index 00000000..8af9ab99 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_07_extruders.gcode @@ -0,0 +1,85 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.2 and above! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; magic_mesh_surface_mode = {magic_mesh_surface_mode} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; speed_z_hop = {speed_z_hop} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; speed_z_hop_0 = {speed_z_hop, 0} +; speed_z_hop_1 = {speed_z_hop, 1} +; speed_z_hop_2 = {speed_z_hop, 2} +; speed_z_hop_3 = {speed_z_hop, 3} +; speed_z_hop_4 = {speed_z_hop, 4} +; speed_z_hop_5 = {speed_z_hop, 5} +; speed_z_hop_6 = {speed_z_hop, 6} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_amount_4 = {retraction_amount, 4} +; retraction_amount_5 = {retraction_amount, 5} +; retraction_amount_6 = {retraction_amount, 6} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_4 = {retraction_hop, 4} +; retraction_hop_5 = {retraction_hop, 5} +; retraction_hop_6 = {retraction_hop, 6} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_hop_enabled_4 = {retraction_hop_enabled, 4} +; retraction_hop_enabled_5 = {retraction_hop_enabled, 5} +; retraction_hop_enabled_6 = {retraction_hop_enabled, 6} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_prime_speed_4 = {retraction_prime_speed, 4} +; retraction_prime_speed_5 = {retraction_prime_speed, 5} +; retraction_prime_speed_6 = {retraction_prime_speed, 6} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_retract_speed_4 = {retraction_retract_speed, 4} +; retraction_retract_speed_5 = {retraction_retract_speed, 5} +; retraction_retract_speed_6 = {retraction_retract_speed, 6} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_speed_4 = {retraction_speed, 4} +; retraction_speed_5 = {retraction_speed, 5} +; retraction_speed_6 = {retraction_speed, 6} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; retraction_enable_4 = {retraction_enable, 4} +; retraction_enable_5 = {retraction_enable, 5} +; retraction_enable_6 = {retraction_enable, 6} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} +; speed_travel_4 = {speed_travel, 4} +; speed_travel_5 = {speed_travel, 5} +; speed_travel_6 = {speed_travel, 6} + + diff --git a/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_08_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_08_extruders.gcode new file mode 100644 index 00000000..dc216a17 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/4.2/settings_output_08_extruders.gcode @@ -0,0 +1,94 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.2 and above! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; magic_mesh_surface_mode = {magic_mesh_surface_mode} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; speed_z_hop = {speed_z_hop} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; speed_z_hop_0 = {speed_z_hop, 0} +; speed_z_hop_1 = {speed_z_hop, 1} +; speed_z_hop_2 = {speed_z_hop, 2} +; speed_z_hop_3 = {speed_z_hop, 3} +; speed_z_hop_4 = {speed_z_hop, 4} +; speed_z_hop_5 = {speed_z_hop, 5} +; speed_z_hop_6 = {speed_z_hop, 6} +; speed_z_hop_7 = {speed_z_hop, 7} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_amount_4 = {retraction_amount, 4} +; retraction_amount_5 = {retraction_amount, 5} +; retraction_amount_6 = {retraction_amount, 6} +; retraction_amount_7 = {retraction_amount, 7} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_4 = {retraction_hop, 4} +; retraction_hop_5 = {retraction_hop, 5} +; retraction_hop_6 = {retraction_hop, 6} +; retraction_hop_7 = {retraction_hop, 7} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_hop_enabled_4 = {retraction_hop_enabled, 4} +; retraction_hop_enabled_5 = {retraction_hop_enabled, 5} +; retraction_hop_enabled_6 = {retraction_hop_enabled, 6} +; retraction_hop_enabled_7 = {retraction_hop_enabled, 7} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_prime_speed_4 = {retraction_prime_speed, 4} +; retraction_prime_speed_5 = {retraction_prime_speed, 5} +; retraction_prime_speed_6 = {retraction_prime_speed, 6} +; retraction_prime_speed_7 = {retraction_prime_speed, 7} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_retract_speed_4 = {retraction_retract_speed, 4} +; retraction_retract_speed_5 = {retraction_retract_speed, 5} +; retraction_retract_speed_6 = {retraction_retract_speed, 6} +; retraction_retract_speed_7 = {retraction_retract_speed, 7} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_speed_4 = {retraction_speed, 4} +; retraction_speed_5 = {retraction_speed, 5} +; retraction_speed_6 = {retraction_speed, 6} +; retraction_speed_7 = {retraction_speed, 7} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; retraction_enable_4 = {retraction_enable, 4} +; retraction_enable_5 = {retraction_enable, 5} +; retraction_enable_6 = {retraction_enable, 6} +; retraction_enable_7 = {retraction_enable, 7} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} +; speed_travel_4 = {speed_travel, 4} +; speed_travel_5 = {speed_travel, 5} +; speed_travel_6 = {speed_travel, 6} +; speed_travel_7 = {speed_travel, 7} + + diff --git a/octoprint_octolapse/data/scripts/gcode/cura/settings_output.gcode b/octoprint_octolapse/data/scripts/gcode/cura/settings_output.gcode index 9a5cf189..98b9c5ef 100644 --- a/octoprint_octolapse/data/scripts/gcode/cura/settings_output.gcode +++ b/octoprint_octolapse/data/scripts/gcode/cura/settings_output.gcode @@ -14,4 +14,4 @@ ; retraction_enable = {retraction_enable} ; layer_height = {layer_height} ; smooth_spiralized_contours = {smooth_spiralized_contours} -; magic_mesh_surface_mode = {magic_mesh_surface_mode} + diff --git a/octoprint_octolapse/data/scripts/gcode/cura/settings_output_02_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_02_extruders.gcode new file mode 100644 index 00000000..1a0b9f79 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_02_extruders.gcode @@ -0,0 +1,37 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.1 and below! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; max_feedrate_z_override = {max_feedrate_z_override} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; max_feedrate_z_override_0 = {max_feedrate_z_override, 0} +; max_feedrate_z_override_1 = {max_feedrate_z_override, 1} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/settings_output_03_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_03_extruders.gcode new file mode 100644 index 00000000..371106e7 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_03_extruders.gcode @@ -0,0 +1,46 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.1 and below! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; max_feedrate_z_override = {max_feedrate_z_override} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; max_feedrate_z_override_0 = {max_feedrate_z_override, 0} +; max_feedrate_z_override_1 = {max_feedrate_z_override, 1} +; max_feedrate_z_override_2 = {max_feedrate_z_override, 2} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/settings_output_04_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_04_extruders.gcode new file mode 100644 index 00000000..cd68ec94 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_04_extruders.gcode @@ -0,0 +1,55 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.1 and below! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; max_feedrate_z_override = {max_feedrate_z_override} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; max_feedrate_z_override_0 = {max_feedrate_z_override, 0} +; max_feedrate_z_override_1 = {max_feedrate_z_override, 1} +; max_feedrate_z_override_2 = {max_feedrate_z_override, 2} +; max_feedrate_z_override_3 = {max_feedrate_z_override, 3} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/settings_output_05_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_05_extruders.gcode new file mode 100644 index 00000000..af08961c --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_05_extruders.gcode @@ -0,0 +1,64 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.1 and below! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; max_feedrate_z_override = {max_feedrate_z_override} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; max_feedrate_z_override_0 = {max_feedrate_z_override, 0} +; max_feedrate_z_override_1 = {max_feedrate_z_override, 1} +; max_feedrate_z_override_2 = {max_feedrate_z_override, 2} +; max_feedrate_z_override_3 = {max_feedrate_z_override, 3} +; max_feedrate_z_override_4 = {max_feedrate_z_override, 4} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_amount_4 = {retraction_amount, 4} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_4 = {retraction_hop, 4} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_hop_enabled_4 = {retraction_hop_enabled, 4} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_prime_speed_4 = {retraction_prime_speed, 4} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_retract_speed_4 = {retraction_retract_speed, 4} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_speed_4 = {retraction_speed, 4} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; retraction_enable_4 = {retraction_enable, 4} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} +; speed_travel_4 = {speed_travel, 4} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/settings_output_06_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_06_extruders.gcode new file mode 100644 index 00000000..b36fd0e7 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_06_extruders.gcode @@ -0,0 +1,73 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.1 and below! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; max_feedrate_z_override = {max_feedrate_z_override} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; max_feedrate_z_override_0 = {max_feedrate_z_override, 0} +; max_feedrate_z_override_1 = {max_feedrate_z_override, 1} +; max_feedrate_z_override_2 = {max_feedrate_z_override, 2} +; max_feedrate_z_override_3 = {max_feedrate_z_override, 3} +; max_feedrate_z_override_4 = {max_feedrate_z_override, 4} +; max_feedrate_z_override_5 = {max_feedrate_z_override, 5} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_amount_4 = {retraction_amount, 4} +; retraction_amount_5 = {retraction_amount, 5} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_4 = {retraction_hop, 4} +; retraction_hop_5 = {retraction_hop, 5} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_hop_enabled_4 = {retraction_hop_enabled, 4} +; retraction_hop_enabled_5 = {retraction_hop_enabled, 5} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_prime_speed_4 = {retraction_prime_speed, 4} +; retraction_prime_speed_5 = {retraction_prime_speed, 5} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_retract_speed_4 = {retraction_retract_speed, 4} +; retraction_retract_speed_5 = {retraction_retract_speed, 5} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_speed_4 = {retraction_speed, 4} +; retraction_speed_5 = {retraction_speed, 5} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; retraction_enable_4 = {retraction_enable, 4} +; retraction_enable_5 = {retraction_enable, 5} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} +; speed_travel_4 = {speed_travel, 4} +; speed_travel_5 = {speed_travel, 5} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/settings_output_07_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_07_extruders.gcode new file mode 100644 index 00000000..5015b308 --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_07_extruders.gcode @@ -0,0 +1,82 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.1 and below! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; max_feedrate_z_override = {max_feedrate_z_override} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; max_feedrate_z_override_0 = {max_feedrate_z_override, 0} +; max_feedrate_z_override_1 = {max_feedrate_z_override, 1} +; max_feedrate_z_override_2 = {max_feedrate_z_override, 2} +; max_feedrate_z_override_3 = {max_feedrate_z_override, 3} +; max_feedrate_z_override_4 = {max_feedrate_z_override, 4} +; max_feedrate_z_override_5 = {max_feedrate_z_override, 5} +; max_feedrate_z_override_6 = {max_feedrate_z_override, 6} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_amount_4 = {retraction_amount, 4} +; retraction_amount_5 = {retraction_amount, 5} +; retraction_amount_6 = {retraction_amount, 6} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_4 = {retraction_hop, 4} +; retraction_hop_5 = {retraction_hop, 5} +; retraction_hop_6 = {retraction_hop, 6} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_hop_enabled_4 = {retraction_hop_enabled, 4} +; retraction_hop_enabled_5 = {retraction_hop_enabled, 5} +; retraction_hop_enabled_6 = {retraction_hop_enabled, 6} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_prime_speed_4 = {retraction_prime_speed, 4} +; retraction_prime_speed_5 = {retraction_prime_speed, 5} +; retraction_prime_speed_6 = {retraction_prime_speed, 6} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_retract_speed_4 = {retraction_retract_speed, 4} +; retraction_retract_speed_5 = {retraction_retract_speed, 5} +; retraction_retract_speed_6 = {retraction_retract_speed, 6} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_speed_4 = {retraction_speed, 4} +; retraction_speed_5 = {retraction_speed, 5} +; retraction_speed_6 = {retraction_speed, 6} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; retraction_enable_4 = {retraction_enable, 4} +; retraction_enable_5 = {retraction_enable, 5} +; retraction_enable_6 = {retraction_enable, 6} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} +; speed_travel_4 = {speed_travel, 4} +; speed_travel_5 = {speed_travel, 5} +; speed_travel_6 = {speed_travel, 6} diff --git a/octoprint_octolapse/data/scripts/gcode/cura/settings_output_08_extruders.gcode b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_08_extruders.gcode new file mode 100644 index 00000000..16bfd09b --- /dev/null +++ b/octoprint_octolapse/data/scripts/gcode/cura/settings_output_08_extruders.gcode @@ -0,0 +1,93 @@ +; Script based on an original created by tjjfvi (https://github.com/tjjfvi) +; An up-to-date version of the tjjfvi's original script can be found +; here: https://csi.t6.fyi/ +; Note - This script will only work in Cura V4.1 and below! +; --- Global Settings +; layer_height = {layer_height} +; smooth_spiralized_contours = {smooth_spiralized_contours} +; machine_extruder_count = {machine_extruder_count} +; --- Single Extruder Settings +; max_feedrate_z_override = {max_feedrate_z_override} +; retraction_amount = {retraction_amount} +; retraction_hop = {retraction_hop} +; retraction_hop_enabled = {retraction_hop_enabled} +; retraction_enable = {retraction_enable} +; retraction_speed = {retraction_speed} +; retraction_retract_speed = {retraction_retract_speed} +; retraction_prime_speed = {retraction_prime_speed} +; speed_travel = {speed_travel} +; --- Multi-Extruder Settings +; max_feedrate_z_override_0 = {max_feedrate_z_override, 0} +; max_feedrate_z_override_1 = {max_feedrate_z_override, 1} +; max_feedrate_z_override_2 = {max_feedrate_z_override, 2} +; max_feedrate_z_override_3 = {max_feedrate_z_override, 3} +; max_feedrate_z_override_4 = {max_feedrate_z_override, 4} +; max_feedrate_z_override_5 = {max_feedrate_z_override, 5} +; max_feedrate_z_override_6 = {max_feedrate_z_override, 6} +; max_feedrate_z_override_7 = {max_feedrate_z_override, 7} +; retraction_amount_0 = {retraction_amount, 0} +; retraction_amount_1 = {retraction_amount, 1} +; retraction_amount_2 = {retraction_amount, 2} +; retraction_amount_3 = {retraction_amount, 3} +; retraction_amount_4 = {retraction_amount, 4} +; retraction_amount_5 = {retraction_amount, 5} +; retraction_amount_6 = {retraction_amount, 6} +; retraction_amount_7 = {retraction_amount, 7} +; retraction_hop_0 = {retraction_hop, 0} +; retraction_hop_1 = {retraction_hop, 1} +; retraction_hop_2 = {retraction_hop, 2} +; retraction_hop_3 = {retraction_hop, 3} +; retraction_hop_4 = {retraction_hop, 4} +; retraction_hop_5 = {retraction_hop, 5} +; retraction_hop_6 = {retraction_hop, 6} +; retraction_hop_7 = {retraction_hop, 7} +; retraction_hop_enabled_0 = {retraction_hop_enabled, 0} +; retraction_hop_enabled_1 = {retraction_hop_enabled, 1} +; retraction_hop_enabled_2 = {retraction_hop_enabled, 2} +; retraction_hop_enabled_3 = {retraction_hop_enabled, 3} +; retraction_hop_enabled_4 = {retraction_hop_enabled, 4} +; retraction_hop_enabled_5 = {retraction_hop_enabled, 5} +; retraction_hop_enabled_6 = {retraction_hop_enabled, 6} +; retraction_hop_enabled_7 = {retraction_hop_enabled, 7} +; retraction_prime_speed_0 = {retraction_prime_speed, 0} +; retraction_prime_speed_1 = {retraction_prime_speed, 1} +; retraction_prime_speed_2 = {retraction_prime_speed, 2} +; retraction_prime_speed_3 = {retraction_prime_speed, 3} +; retraction_prime_speed_4 = {retraction_prime_speed, 4} +; retraction_prime_speed_5 = {retraction_prime_speed, 5} +; retraction_prime_speed_6 = {retraction_prime_speed, 6} +; retraction_prime_speed_7 = {retraction_prime_speed, 7} +; retraction_retract_speed_0 = {retraction_retract_speed, 0} +; retraction_retract_speed_1 = {retraction_retract_speed, 1} +; retraction_retract_speed_2 = {retraction_retract_speed, 2} +; retraction_retract_speed_3 = {retraction_retract_speed, 3} +; retraction_retract_speed_4 = {retraction_retract_speed, 4} +; retraction_retract_speed_5 = {retraction_retract_speed, 5} +; retraction_retract_speed_6 = {retraction_retract_speed, 6} +; retraction_retract_speed_7 = {retraction_retract_speed, 7} +; retraction_speed_0 = {retraction_speed, 0} +; retraction_speed_1 = {retraction_speed, 1} +; retraction_speed_2 = {retraction_speed, 2} +; retraction_speed_3 = {retraction_speed, 3} +; retraction_speed_4 = {retraction_speed, 4} +; retraction_speed_5 = {retraction_speed, 5} +; retraction_speed_6 = {retraction_speed, 6} +; retraction_speed_7 = {retraction_speed, 7} +; retraction_enable_0 = {retraction_enable, 0} +; retraction_enable_1 = {retraction_enable, 1} +; retraction_enable_2 = {retraction_enable, 2} +; retraction_enable_3 = {retraction_enable, 3} +; retraction_enable_4 = {retraction_enable, 4} +; retraction_enable_5 = {retraction_enable, 5} +; retraction_enable_6 = {retraction_enable, 6} +; retraction_enable_7 = {retraction_enable, 7} +; speed_travel_0 = {speed_travel, 0} +; speed_travel_1 = {speed_travel, 1} +; speed_travel_2 = {speed_travel, 2} +; speed_travel_3 = {speed_travel, 3} +; speed_travel_4 = {speed_travel, 4} +; speed_travel_5 = {speed_travel, 5} +; speed_travel_6 = {speed_travel, 6} +; speed_travel_7 = {speed_travel, 7} + + diff --git a/octoprint_octolapse/data/scripts/linux/download_from_camera.sh b/octoprint_octolapse/data/scripts/linux/download-from-camera-and-rename.sh similarity index 51% rename from octoprint_octolapse/data/scripts/linux/download_from_camera.sh rename to octoprint_octolapse/data/scripts/linux/download-from-camera-and-rename.sh index 37818195..2598553d 100644 --- a/octoprint_octolapse/data/scripts/linux/download_from_camera.sh +++ b/octoprint_octolapse/data/scripts/linux/download-from-camera-and-rename.sh @@ -19,13 +19,29 @@ fi cd "${SNAPSHOT_DIRECTORY}" # download all of the images on the camera -gphoto2 --get-all-files --force-overwrite +gphoto2 --auto-detect --get-all-files --force-overwrite -# rename images according to the supplied file template +# Count the number of images downloaded. If we find 0, report an error. a=0 for i in *.JPG *.jpg *.JPEG *.jpeg; do - new=$(printf "${SNAPSHOT_FILENAME_TEMPLATE}" "${a}") - mv -- "${i}" "${new}" a=$((a+1)) done +# If no snapshots were found, report an error and exit. +if [ "$a" = "0" ]; then + echo "No snapshot were found with extensions .JPG, .JPEG, .jpg or .jpeg. " 1>&2; + exit 1; +fi + +# rename images according to the supplied file template +a=0 +for i in *.JPG *.jpg *.JPEG *.jpeg; do + num_files=0 + num_files=$(ls -lq "${i}" 2>/dev/null | wc -l ) + if [ "$num_files" != "0" ]; then + new=$(printf "${SNAPSHOT_FILENAME_TEMPLATE}" "${a}") + echo "Moving file ${i} to ${new}" + mv -- "${i}" "${new}" 2>/dev/null + a=$((a+1)) + fi +done diff --git a/octoprint_octolapse/data/scripts/linux/move_snapshots_to_target.sh b/octoprint_octolapse/data/scripts/linux/move_snapshots_to_target.sh new file mode 100644 index 00000000..a200dda1 --- /dev/null +++ b/octoprint_octolapse/data/scripts/linux/move_snapshots_to_target.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Camera Post-Render Script - Move Snapshots to Target Folder +# Written by: Formerlurker@pm.me + +# 1. This script should be created within the '/home/pi/scripts/' +# folder on your pi and called move_snapshots_to_target.sh. +# You can use the following command to create the file +# sudo nano move_snapshots_to_target.sh +# 2. Copy and paste this script into nano via a single right click. +# 3. ** IMPORTANT - Change the TARGET_SNAPSHOT_DIRECTORY below +TARGET_SNAPSHOT_DIRECTORY="/home/pi/snapshot_target_folder" +# 4. Save the file via ctrl+o and exit via ctrl+x +# 5. Add execute permissions to the file via: +# sudo chmod +x /home/pi/scripts/move_snapshots_to_target.sh +# 6. In your Octolapse camera profile add the following to the 'After Render Script': +# /home/pi/scripts/move_snapshots_to_target.sh +# 7. Enjoy! + +# Put the arguments sent by Octolapse into variables for easy use +CAMERA_NAME=$1 +SNAPSHOT_DIRECTORY=$2 +SNAPSHOT_FILENAME_TEMPLATE=$3 +SNAPSHOT_FULL_PATH_TEMPLATE=$4 +TIMELAPSE_OUTPUT_DIRECTORY=$5 +TIMELAPSE_OUTPUT_FILENAME=$6 +TIMELAPSE_EXTENSION=$7 +TIMELAPSE_FULL_PATH=$8 +SYNCHRONIZATION_DIRECTORY=$9 +SYNCHRONIZATION_FULL_PATH=$10 +# ------------------------------------------------------- + +# Create the directory if it doesn't exist +if [ ! -d "${TARGET_SNAPSHOT_DIRECTORY}" ]; +then + echo "Creating directory: ${TARGET_SNAPSHOT_DIRECTORY}" + mkdir -p "${TARGET_SNAPSHOT_DIRECTORY}" +fi +# ------------------------------------------------------- + +# switch to the snapshot directory +cd "${SNAPSHOT_DIRECTORY}" +# ------------------------------------------------------- + +# Copy all images in the snapshot_directory to the target directory +for snapshot in *.JPG *.jpg *.JPEG *.jpeg; do + new="$TARGET_SNAPSHOT_DIRECTORY/${snapshot##*/}" + cp -- "${snapshot}" "${new}" +done +# ------------------------------------------------------- diff --git a/octoprint_octolapse/data/settings_default_0.4.0.json b/octoprint_octolapse/data/settings_default_0.4.0.json new file mode 100644 index 00000000..94b6caba --- /dev/null +++ b/octoprint_octolapse/data/settings_default_0.4.0.json @@ -0,0 +1,1646 @@ +{ + "main_settings": { + "show_navbar_icon": true, + "show_navbar_when_not_printing": true, + "is_octolapse_enabled": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": 30, + "show_printer_state_changes": true, + "show_position_changes": true, + "show_extruder_state_changes": true, + "show_trigger_state_changes": true, + "show_snapshot_plan_information": true, + "cancel_print_on_startup_error": true, + "platform": "unknown", + "version": "0.4.0", + "settings_version": "0.4.0", + "preview_snapshot_plans": true, + "preview_snapshot_plan_autoclose": false, + "preview_snapshot_plan_seconds": 30, + "automatic_updates_enabled": true, + "automatic_update_interval_days": 30, + "test_mode_enabled": false + }, + "profiles": { + "options": null, + "defaults": null, + "printers": {}, + "current_printer_profile_guid": null, + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": false, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": false, + "name": "Animated - Orbit", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative_path" + }, + "a581ea51-096b-4852-8aa6-28456bf9237e": { + "automatic_configuration": { + "key_values": [ + { + "name": "Center Right", + "value": "center_right" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "a581ea51-096b-4852-8aa6-28456bf9237e", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the right center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Center Right", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Centered", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Disabled", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "disabled" + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Animated - Printing", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative_path" + }, + "18b35adf-543e-406e-83b8-d8a404961339": { + "automatic_configuration": { + "key_values": [ + { + "name": "Front Right", + "value": "front_right" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 1.0, + "x_relative_path": "100.0", + "guid": "18b35adf-543e-406e-83b8-d8a404961339", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the front right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Front Right", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "100.0", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Right", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "b510ae51-b7c5-43c8-8611-6bfc63a05037": { + "automatic_configuration": { + "key_values": [ + { + "name": "Front Left", + "value": "front_left" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 1.0, + "x_relative_path": "100.0", + "guid": "b510ae51-b7c5-43c8-8611-6bfc63a05037", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the front left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Front Left", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "50.0", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Left", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "5c8b02ba-095d-45f9-b7f7-f2a935f34a70": { + "automatic_configuration": { + "key_values": [ + { + "name": "Center Left", + "value": "center_left" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "5c8b02ba-095d-45f9-b7f7-f2a935f34a70", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the left center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Center Left", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "69a0c73c-907c-4a0e-9e96-02d8ae730ac3": { + "automatic_configuration": { + "key_values": [ + { + "name": "Front Center", + "value": "front_center" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 1.0, + "x_relative_path": "100.0", + "guid": "69a0c73c-907c-4a0e-9e96-02d8ae730ac3", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the front center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Front Center", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "100.0", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Center", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "triggers": { + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "name": "Classic - Every 0.5mm", + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.5, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "name": "Classic - Every Layer", + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "name": "Classic - Gcode", + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "name": "Smart - Compatibility", + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "322d69ef-e7cb-454a-916a-15856682932c": { + "name": "Smart - Fast (low quality)", + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "322d69ef-e7cb-454a-916a-15856682932c", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b838fe36-1459-4867-8243-ab7604cf0e2d": { + "name": "Smart - Gcode", + "description": "Takes a snapshot each time the snapshot command is encountered. The snapshot command is @OCTOLAPSE TAKE-SNAPSHOT, but you can specify an alternative snapshot command within your printer profile if you desire.", + "guid": "b838fe36-1459-4867-8243-ab7604cf0e2d", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Gcode", + "value": "smart_gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "f8e5e1a2-a54c-489b-961b-2530648e1625": { + "name": "Smart - High Quality", + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "guid": "f8e5e1a2-a54c-489b-961b-2530648e1625", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 3, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "name": "Smart - Snap To Print", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "name": "Smart - Snap To Print - High Quality", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. It will NOT work with vase mode! As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\n If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": true, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "name": "Smart - Snap To Print - Smooth", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": true, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "name": "Timer - Every 1:00", + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 60, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "name": "Timer - Every 0:30", + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "name": "GIF - Fixed Length - 00:05", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "gif", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[0,0]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "name": "MP4 - 30 FPS", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "name": "MP4 - Fixed Length - 00:05", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10,10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "name": "MP4 - Fixed Length - 00:10", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 10, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "name": "MP4 - 60 FPS", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 60, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "35aababf-0ecf-46d5-b142-6290d38c8fea": { + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled (for manual rendering)", + "value": "disabled" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.1" + }, + "overlay_text_template": "", + "overlay_text_valign": "top", + "thread_count": 1, + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "min_fps": 2.0, + "guid": "35aababf-0ecf-46d5-b142-6290d38c8fea", + "overlay_font_path": "", + "sync_with_timelapse": true, + "constant_rate_factor": 28, + "overlay_text_pos": "[0,0]", + "overlay_outline_width": 1, + "fps": 30, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "selected_watermark": "", + "output_format": "mp4", + "description": "No timelapse will be generated, but all snapshots will be in the Octolapse data directory and can be downloaded and manually rendered.", + "overlay_text_alignment": "left", + "archive_snapshots": false, + "fps_calculation_type": "duration", + "post_roll_seconds": 0, + "bitrate": "6000k", + "name": "Disabled", + "overlay_text_halign": "left", + "enabled": false, + "enable_watermark": false, + "pre_roll_seconds": 0, + "overlay_font_size": 10, + "run_length_seconds": 5, + "max_fps": 120.0, + "overlay_outline_color": [ + 0, + 0, + 0, + 1 + ] + } + }, + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "name": "Webcam - Default OctoPi 0.16.0", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras.", + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "enabled": true, + "camera_type": "webcam", + "gcode_camera_script": "", + "on_print_start_script": "", + "on_before_snapshot_script": "", + "external_camera_snapshot_script": "", + "on_after_snapshot_script": "", + "on_before_render_script": "", + "on_after_render_script": "", + "on_print_end_script": "", + "delay": 125, + "timeout_ms": 5000, + "snapshot_transpose": "", + "webcam_settings": { + "address": "http://tako3.prefecture/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "stream_template": "http://tako3.prefecture:8081/?action=stream", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "username": "", + "password": "", + "type": null, + "use_custom_webcam_settings_page": true, + "mjpg_streamer": { + "name": "mjpg-streamer", + "server_type": "mjpg-streamer", + "controls": {} + }, + "stream_download": false + }, + "enable_custom_image_preferences": false, + "apply_settings_before_print": true, + "apply_settings_at_startup": true + } + }, + "current_logging_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "logging": { + "4d7411e9-f97e-41c0-921c-02e262660e83": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Parsing/Preprocessing/Position Tracking", + "value": "c++" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Parsing/Preprocessing/Position Tracking", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "4d7411e9-f97e-41c0-921c-02e262660e83", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + } + ], + "description": "This logging profile is useful for debugging problems parsing gcode, tracking the printer's position and state, or preprocessing gcode to create snapshot plans using the new smart layer trigger. This profile will create very large log files and will make preprocessing VERY VERY slow, so use this only when necessary to solve a specific issue." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log All Errors - Recommended", + "value": "errors" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log All Errors", + "log_to_console": false, + "enabled": false, + "default_log_level": 10, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "enabled_loggers": [], + "description": "Logs only errors and exceptions. This is the recommended default profile, logs a minimal amount of data, and has a low impact on performance." + }, + "a417b5bf-9d26-4456-81ec-ba2f04f57084": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug Logging", + "value": "debug" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Debug", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "a417b5bf-9d26-4456-81ec-ba2f04f57084", + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.script" + }, + { + "log_level": 10, + "name": "octolapse.migration" + } + ], + "description": "Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "fa759ab9-bc02-4fd0-8d12-a7c5c89591c1": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug - Script Cameras (DSLR)", + "value": "debug_script_cameras" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": 1.0 + }, + "name": "Debug - Script Cameras (DSLR)", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "fa759ab9-bc02-4fd0-8d12-a7c5c89591c1", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.render" + } + ], + "description": "This profile is useful for debugging script based cameras, like DSLRs." + }, + "02717525-3684-4bf7-ac53-037cdfbd4e66": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Everything", + "value": "verbose" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Everything", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "02717525-3684-4bf7-ac53-037cdfbd4e66", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.__init__" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 5, + "name": "octolapse.position" + }, + { + "log_level": 5, + "name": "octolapse.render" + }, + { + "log_level": 5, + "name": "octolapse.settings" + }, + { + "log_level": 5, + "name": "octolapse.settings_external" + }, + { + "log_level": 5, + "name": "octolapse.settings_migration" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 5, + "name": "octolapse.timelapse" + }, + { + "log_level": 5, + "name": "octolapse.trigger" + }, + { + "log_level": 5, + "name": "octolapse.utility" + }, + { + "log_level": 5, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.migration" + } + ], + "description": "This will log absolutely everything that Octolapse is capable of logging. It will severely impact performance, and is only recommended for very difficult to debug or uncommon errors. I do not recommend you use this profile unless you are asked for a verbose log after creating an issue in the Octolapse Github repository." + } + } + }, + "global_options": null, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/settings_default_0.4.0rc1.dev2.json b/octoprint_octolapse/data/settings_default_0.4.0rc1.dev2.json new file mode 100644 index 00000000..9133b3b5 --- /dev/null +++ b/octoprint_octolapse/data/settings_default_0.4.0rc1.dev2.json @@ -0,0 +1,1639 @@ +{ + "main_settings": { + "show_extruder_state_changes": true, + "show_navbar_icon": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": "30", + "preview_snapshot_plans": true, + "show_navbar_when_not_printing": true, + "automatic_update_interval_days": "30", + "show_snapshot_plan_information": true, + "is_octolapse_enabled": true, + "show_printer_state_changes": true, + "platform": "win32", + "version": "0.4.0rc1.dev2", + "show_position_changes": true, + "automatic_updates_enabled": true, + "cancel_print_on_startup_error": true, + "show_trigger_state_changes": true + }, + "global_options": null, + "profiles": { + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": false, + "x_type": "relative_path", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": false, + "name": "Animated - Orbit", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000" + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "x_type": "relative", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Centered", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "y_relative_path": "50" + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "x_type": "disabled", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Disabled", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "y_relative_path": "50" + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "x_type": "relative_path", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Animated - Printing", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "y_relative_path": "50" + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "100.0", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "x_type": "relative", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Right", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "y_relative_path": "100" + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "50.0", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "x_type": "relative", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Left", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "y_relative_path": "50" + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "100.0", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "x_type": "relative", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Center", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "y_relative_path": "100" + } + }, + "triggers": { + "c8f810ef-56b9-4343-8ec3-e35896af1a5c": { + "automatic_configuration": { + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0", + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ] + }, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.0, + "guid": "c8f810ef-56b9-4343-8ec3-e35896af1a5c", + "smart_layer_trigger_type": 3, + "trigger_type": "smart-layer", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "require_zhop": false, + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "trigger_on_primed": "trigger_on", + "name": "Smart - High Quality", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "layer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "fbd4a42d-c266-4418-a7af-58b02787b6f5": { + "automatic_configuration": { + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0", + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ] + }, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.0, + "guid": "fbd4a42d-c266-4418-a7af-58b02787b6f5", + "smart_layer_trigger_type": 1, + "trigger_type": "smart-layer", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "require_zhop": false, + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "trigger_on_primed": "trigger_on", + "name": "Smart - Fast (low quality)", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "layer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "smart_layer_trigger_speed_threshold": 0, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.0, + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "smart_layer_trigger_type": 2, + "trigger_type": "real-time", + "timer_trigger_seconds": 60, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "smart_layer_snap_to_print": false, + "require_zhop": false, + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "smart_layer_trigger_distance_threshold_percent": 10, + "trigger_on_primed": "trigger_on", + "name": "Timer - Every 1:00", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "timer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "smart_layer_trigger_speed_threshold": 0, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.0, + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "smart_layer_trigger_type": 2, + "trigger_type": "real-time", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "smart_layer_snap_to_print": false, + "require_zhop": false, + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "smart_layer_trigger_distance_threshold_percent": 10, + "trigger_on_primed": "trigger_on", + "name": "Timer - Every 0:30", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "timer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "smart_layer_trigger_speed_threshold": 0, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.5, + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "smart_layer_trigger_type": 2, + "trigger_type": "real-time", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "smart_layer_snap_to_print": false, + "require_zhop": false, + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "smart_layer_trigger_distance_threshold_percent": 10, + "trigger_on_primed": "trigger_on", + "name": "Classic - Every 0.5mm", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "layer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "automatic_configuration": { + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0", + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ] + }, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.0, + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "smart_layer_trigger_type": 0, + "trigger_type": "smart-layer", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "require_zhop": false, + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "trigger_on_primed": "trigger_on", + "name": "Smart - Snap To Print", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "layer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "is_custom": true, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "trigger_on_retracted": "trigger_on", + "smart_layer_snap_to_print_high_quality": true, + "layer_trigger_height": 0.0, + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "smart_layer_trigger_type": 0, + "trigger_type": "smart-layer", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "require_zhop": false, + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "smart_layer_snap_to_print_smooth": false, + "trigger_on_primed": "trigger_on", + "name": "Smart - Snap To Print - High Quality", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "layer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "trigger_on_retracted": "trigger_on", + "smart_layer_snap_to_print_high_quality": false, + "layer_trigger_height": 0.0, + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "smart_layer_trigger_type": 0, + "trigger_type": "smart-layer", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "require_zhop": false, + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "smart_layer_snap_to_print_smooth": true, + "trigger_on_primed": "trigger_on", + "name": "Smart - Snap To Print - Smooth", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "layer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "smart_layer_trigger_speed_threshold": 0, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.0, + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "smart_layer_trigger_type": 2, + "trigger_type": "real-time", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "smart_layer_snap_to_print": false, + "require_zhop": false, + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "smart_layer_trigger_distance_threshold_percent": 10, + "trigger_on_primed": "trigger_on", + "name": "Classic - Every Layer", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "layer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "smart_layer_trigger_speed_threshold": 0, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.0, + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "smart_layer_trigger_type": 2, + "trigger_type": "real-time", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "smart_layer_snap_to_print": false, + "require_zhop": false, + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": false, + "smart_layer_trigger_distance_threshold_percent": 10, + "trigger_on_primed": "trigger_on", + "name": "Classic - Gcode", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "gcode", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "automatic_configuration": { + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0", + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ] + }, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.0, + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "smart_layer_trigger_type": 2, + "trigger_type": "smart-layer", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "require_zhop": false, + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "trigger_on_primed": "trigger_on", + "name": "Smart - Compatibility", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "layer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + } + }, + "current_printer_profile_guid": null, + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "is_custom": true, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "timeout_ms": 5000, + "apply_settings_at_startup": false, + "on_after_render_script": "", + "name": "Webcam - Default OctoPi 0.16.0", + "on_before_snapshot_script": "", + "gcode_camera_script": "", + "enabled": true, + "external_camera_snapshot_script": "", + "snapshot_transpose": "", + "on_before_render_script": "", + "delay": 125, + "camera_type": "webcam", + "enable_custom_image_preferences": false, + "webcam_settings": { + "username": "", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "mjpg_streamer": { + "name": "mjpg-streamer", + "controls": {}, + "server_type": "mjpg-streamer" + }, + "use_custom_webcam_settings_page": true, + "address": "http://127.0.0.1:8080/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "password": "", + "type": null, + "stream_template": "/?action=stream" + }, + "on_after_snapshot_script": "", + "on_print_start_script": "", + "apply_settings_before_print": false, + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras." + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "overlay_text_template": "", + "overlay_text_valign": "top", + "thread_count": 1, + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "min_fps": 2.0, + "cleanup_after_render_complete": true, + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "overlay_font_path": "", + "sync_with_timelapse": true, + "max_fps": 120.0, + "fps": 30, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "selected_watermark": "", + "overlay_text_alignment": "left", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "output_format": "gif", + "fps_calculation_type": "duration", + "post_roll_seconds": 2, + "bitrate": "10000k", + "overlay_text_halign": "left", + "cleanup_after_render_fail": true, + "name": "GIF - Fixed Length - 00:05", + "enabled": true, + "enable_watermark": false, + "pre_roll_seconds": 2, + "overlay_font_size": 10, + "run_length_seconds": 5, + "overlay_text_pos": "[0,0]" + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "overlay_text_template": "", + "overlay_text_valign": "top", + "thread_count": 1, + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "min_fps": 2.0, + "cleanup_after_render_complete": true, + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "overlay_font_path": "", + "sync_with_timelapse": true, + "max_fps": 120.0, + "fps": 30, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "selected_watermark": "", + "overlay_text_alignment": "left", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "output_format": "mp4", + "fps_calculation_type": "static", + "post_roll_seconds": 2, + "bitrate": "8000k", + "overlay_text_halign": "left", + "cleanup_after_render_fail": false, + "name": "MP4 - 30 FPS", + "enabled": true, + "enable_watermark": false, + "pre_roll_seconds": 2, + "overlay_font_size": 10, + "run_length_seconds": 5, + "overlay_text_pos": "[10,10]" + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "overlay_text_template": "", + "overlay_text_valign": "top", + "thread_count": 1, + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "min_fps": 2.0, + "cleanup_after_render_complete": true, + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "overlay_font_path": "", + "sync_with_timelapse": true, + "max_fps": 120.0, + "fps": 30, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "selected_watermark": "", + "overlay_text_alignment": "left", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "output_format": "mp4", + "fps_calculation_type": "duration", + "post_roll_seconds": 2, + "bitrate": "10000k", + "overlay_text_halign": "left", + "cleanup_after_render_fail": true, + "name": "MP4 - Fixed Length - 00:05", + "enabled": true, + "enable_watermark": false, + "pre_roll_seconds": 2, + "overlay_font_size": 10, + "run_length_seconds": 5, + "overlay_text_pos": "[0,0]" + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "overlay_text_template": "", + "overlay_text_valign": "top", + "thread_count": 1, + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "min_fps": 2.0, + "cleanup_after_render_complete": true, + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "overlay_font_path": "", + "sync_with_timelapse": true, + "max_fps": 120.0, + "fps": 30, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "selected_watermark": "", + "overlay_text_alignment": "left", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "output_format": "mp4", + "fps_calculation_type": "duration", + "post_roll_seconds": 2, + "bitrate": "10000k", + "overlay_text_halign": "left", + "cleanup_after_render_fail": false, + "name": "MP4 - Fixed Length - 00:10", + "enabled": true, + "enable_watermark": false, + "pre_roll_seconds": 2, + "overlay_font_size": 10, + "run_length_seconds": 10, + "overlay_text_pos": "[0,0]" + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "overlay_text_template": "", + "overlay_text_valign": "top", + "thread_count": 1, + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "min_fps": 2.0, + "cleanup_after_render_complete": true, + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "overlay_font_path": "", + "sync_with_timelapse": true, + "max_fps": 120.0, + "fps": 60, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "selected_watermark": "", + "overlay_text_alignment": "left", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "output_format": "mp4", + "fps_calculation_type": "static", + "post_roll_seconds": 2, + "bitrate": "10000k", + "overlay_text_halign": "left", + "cleanup_after_render_fail": false, + "name": "MP4 - 60 FPS", + "enabled": true, + "enable_watermark": false, + "pre_roll_seconds": 2, + "overlay_font_size": 10, + "run_length_seconds": 5, + "overlay_text_pos": "[0,0]" + } + }, + "current_debug_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "defaults": { + "printer": { + "automatic_configuration": { + "key_values": [ + { + "name": "Select a value", + "value": "null" + }, + { + "name": "Select a value", + "value": "null" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": null + }, + "gocde_axis_compatibility_mode_enabled": true, + "home_axis_gcode": "G90; Switch to Absolute XYZ\r\nG28 X Y; Home XY Axis", + "height": 0.0, + "custom_bounding_box": false, + "restrict_snapshot_area": false, + "snapshot_min_z": 0.0, + "snapshot_min_x": 0.0, + "guid": "c94c01b9-eea9-4295-84cd-f4a96da1abe6", + "description": "", + "snapshot_min_y": 0.0, + "home_z": null, + "min_y": 0.0, + "home_x": null, + "home_y": null, + "override_octoprint_profile_settings": false, + "suppress_snapshot_command_always": true, + "min_x": 0.0, + "default_firmware_retractions": false, + "diameter_xy": 0.0, + "width": 0.0, + "max_z": 0.0, + "max_x": 0.0, + "max_y": 0.0, + "has_been_saved_by_user": false, + "snapshot_diameter_xy": 0.0, + "e_axis_default_mode": "require-explicit", + "slicer_type": "automatic", + "auto_position_detection_commands": "", + "g90_influences_extruder": "use-octoprint-settings", + "priming_height": 0.75, + "abort_out_of_bounds": true, + "units_default": "millimeters", + "default_firmware_retractions_zhop": false, + "axis_speed_display_units": "mm-min", + "gcode_generation_settings": { + "vase_mode": null, + "retraction_speed": null, + "deretraction_speed": null, + "z_lift_speed": null, + "x_y_travel_speed": null, + "lift_when_retracted": null, + "retract_before_move": null, + "first_layer_travel_speed": null, + "z_lift_height": null, + "layer_height": null, + "retraction_length": null + }, + "name": "Default Printer Profile", + "min_z": 0.0, + "bed_type": "rectangular", + "minimum_layer_height": 0.05, + "snapshot_command": "snap", + "depth": 0.0, + "slicers": { + "simplify_3d": { + "extruder_use_retract": null, + "z_axis_movement_speed": null, + "retraction_vertical_lift": null, + "retraction_speed": null, + "slicer_type": "simplify-3d", + "x_y_axis_movement_speed": null, + "version": "unknown", + "retraction_distance": null, + "axis_speed_display_settings": "mm-min", + "spiral_vase_mode": null, + "layer_height": null + }, + "other": { + "axis_speed_display_units": "mm-min", + "vase_mode": null, + "layer_height": null, + "retract_length": null, + "lift_when_retracted": false, + "deretract_speed": null, + "slicer_type": "other", + "retract_before_move": false, + "z_hop": null, + "version": "unknown", + "speed_tolerance": 1, + "travel_speed": null, + "print_speed": null, + "retract_speed": null, + "z_travel_speed": null + }, + "slic3r_pe": { + "axis_speed_display_units": "mm-sec", + "layer_height": null, + "retract_length": null, + "deretract_speed": null, + "slicer_type": "slic3r-pe", + "retract_lift": null, + "version": "unknown", + "travel_speed": null, + "spiral_vase": null, + "retract_speed": null, + "retract_before_travel": false + }, + "cura": { + "smooth_spiralized_contours": null, + "retraction_hop": null, + "max_feedrate_z_override": null, + "retraction_prime_speed": null, + "speed_travel": null, + "layer_height": null, + "slicer_type": "cura", + "retraction_enable": null, + "axis_speed_display_settings": "mm-sec", + "version": "unknown", + "retraction_retract_speed": null, + "retraction_amount": null, + "magic_mesh_surface_mode": "normal", + "retraction_hop_enabled": null + } + }, + "snapshot_max_x": 0.0, + "snapshot_max_y": 0.0, + "snapshot_max_z": 0.0, + "xyz_axes_default_mode": "require-explicit", + "auto_detect_position": true, + "origin_type": "front_left" + }, + "stabilization": { + "automatic_configuration": { + "key_values": [ + { + "name": "Select a value", + "value": "null" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": null + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "59833e86-ab58-4f72-98cf-1d6cd055409e", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "x_type": "relative", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "", + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Default Stabilization Profile", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "y_relative_path": "50" + }, + "trigger": { + "automatic_configuration": { + "key_values": [ + { + "name": "Select a value", + "value": "null" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": null + }, + "smart_layer_trigger_speed_threshold": 0, + "trigger_on_retracted": "trigger_on", + "layer_trigger_height": 0.0, + "guid": "6c06fd0f-e115-4526-8dcb-788137174e85", + "smart_layer_trigger_type": 2, + "trigger_type": "smart-layer", + "timer_trigger_seconds": 30, + "position_restrictions": [], + "trigger_on_retracting_start": "", + "smart_layer_snap_to_print": false, + "require_zhop": false, + "description": "", + "trigger_on_extruding": "trigger_on", + "is_default": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_partially_retracted": "forbidden", + "position_restrictions_enabled": false, + "smart_layer_disable_z_lift": true, + "extruder_state_requirements_enabled": true, + "smart_layer_trigger_distance_threshold_percent": 10, + "trigger_on_primed": "trigger_on", + "name": "Default Trigger Profile", + "trigger_on_deretracting": "forbidden", + "trigger_on_retracting": "", + "trigger_subtype": "layer", + "trigger_on_deretracted": "forbidden", + "trigger_on_deretracting_start": "" + }, + "rendering": { + "automatic_configuration": { + "key_values": [ + { + "name": "Select a value", + "value": "null" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": null + }, + "overlay_text_template": "", + "overlay_text_valign": "top", + "thread_count": 1, + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "min_fps": 2.0, + "cleanup_after_render_complete": true, + "guid": "53763fb2-6bb2-4aef-a597-4455e446dfca", + "overlay_font_path": "", + "sync_with_timelapse": true, + "max_fps": 120.0, + "snapshots_to_skip_beginning": 0, + "fps": 30, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "selected_watermark": "", + "overlay_text_alignment": "left", + "snapshot_to_skip_end": 0, + "description": "", + "output_format": "mp4", + "fps_calculation_type": "duration", + "post_roll_seconds": 0, + "bitrate": "6000k", + "overlay_text_halign": "left", + "cleanup_after_render_fail": false, + "name": "Default Rendering Profile", + "enabled": true, + "enable_watermark": false, + "pre_roll_seconds": 0, + "overlay_font_size": 10, + "run_length_seconds": 5, + "overlay_text_pos": "[0,0]" + }, + "camera": { + "automatic_configuration": { + "key_values": [ + { + "name": "Select a value", + "value": "null" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": null + }, + "timeout_ms": 5000, + "apply_settings_at_startup": false, + "on_after_render_script": "", + "name": "Default Camera Profile", + "on_before_snapshot_script": "", + "gcode_camera_script": "", + "enabled": true, + "external_camera_snapshot_script": "", + "snapshot_transpose": "", + "on_before_render_script": "", + "delay": 125, + "camera_type": "webcam", + "enable_custom_image_preferences": false, + "webcam_settings": { + "username": "", + "ignore_ssl_error": false, + "server_type": "mjpg-streamer", + "mjpg_streamer": { + "name": "mjpg-streamer", + "controls": {}, + "server_type": "mjpg-streamer" + }, + "use_custom_webcam_settings_page": true, + "address": "http://127.0.0.1/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "password": "", + "type": null, + "stream_template": "/webcam/?action=stream" + }, + "on_after_snapshot_script": "", + "on_print_start_script": "", + "apply_settings_before_print": false, + "guid": "9c9e89a0-c969-4772-9dfe-20c9957a134b", + "description": "" + }, + "debug": { + "automatic_configuration": { + "key_values": [ + { + "name": "Select a value", + "value": "null" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": null + }, + "name": "Default Debug Profile", + "log_to_console": false, + "is_test_mode": false, + "enabled": false, + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.gcode_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + } + ], + "log_all_errors": true, + "guid": "891d5f82-866f-44c4-8fbc-2b73a2c23a49", + "default_log_level": 10, + "description": "" + } + }, + "printers": {}, + "debug": { + "3811da8f-182d-44bf-8fcb-95614e4daf1b": { + "automatic_configuration": { + "key_values": [ + { + "name": "Test Mode - Log Errors", + "value": "test_log_errors" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "name": "Test Mode", + "log_to_console": false, + "is_test_mode": true, + "enabled": false, + "enabled_loggers": [], + "log_all_errors": true, + "guid": "3811da8f-182d-44bf-8fcb-95614e4daf1b", + "default_log_level": 10, + "description": "This test mode profile is useful for testing Octolapse without waiting for warm-up or wasting filament. Test mode will prevent extrusion, bed and extruder heating, fan controls, and a few other commands. Only errors are logged. This profile is recommended for your first Octolapse, or for performing a pre-flight check for a new model." + }, + "68d000d6-0ae3-40f4-9b16-bdcb882604a6": { + "automatic_configuration": { + "key_values": [ + { + "name": "Live Print - Debug Issues", + "value": "live_log_debug" + } + ], + "is_custom": true, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "name": "Live Print - Debug Issues", + "log_to_console": false, + "is_test_mode": false, + "enabled": true, + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.gcode_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + } + ], + "log_all_errors": true, + "guid": "68d000d6-0ae3-40f4-9b16-bdcb882604a6", + "default_log_level": 10, + "description": "Logs all but verbose messages during a live print. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "072f506e-3e5e-4e84-922b-fb4e01690398": { + "automatic_configuration": { + "key_values": [ + { + "name": "Test Mode - Debug Issues", + "value": "test_log_debug" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "name": "Test Mode - Debug Issues", + "log_to_console": false, + "is_test_mode": true, + "enabled": true, + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.gcode_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + } + ], + "log_all_errors": true, + "guid": "072f506e-3e5e-4e84-922b-fb4e01690398", + "default_log_level": 10, + "description": "This test mode profile is useful for debugging issues during a print without waiting for warm-up or wasting filament. Test mode will prevent extrusion, bed and extruder heating, fan controls, and a few other commands. Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Live Print - Log Errors", + "value": "live_log_errors" + } + ], + "is_custom": false, + "suppress_update_notification_version": false, + "version": "1.0" + }, + "name": "Live Print", + "log_to_console": false, + "is_test_mode": false, + "enabled": false, + "enabled_loggers": [], + "log_all_errors": true, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "default_log_level": 10, + "description": "Logs only errors and exceptions. This is the recommended live (not test mode) profile to use unless you are trying to solve some additional problem." + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "options": null + }, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/settings_default_0.4.0rc1.dev3.json b/octoprint_octolapse/data/settings_default_0.4.0rc1.dev3.json new file mode 100644 index 00000000..219ea29d --- /dev/null +++ b/octoprint_octolapse/data/settings_default_0.4.0rc1.dev3.json @@ -0,0 +1,1313 @@ +{ + "main_settings": { + "show_navbar_icon": true, + "show_navbar_when_not_printing": true, + "is_octolapse_enabled": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": 30, + "show_printer_state_changes": true, + "show_position_changes": true, + "show_extruder_state_changes": true, + "show_trigger_state_changes": true, + "show_snapshot_plan_information": true, + "cancel_print_on_startup_error": true, + "platform": "unknown", + "version": "0.4.0rc1.dev3", + "preview_snapshot_plans": true, + "preview_snapshot_plan_autoclose": false, + "preview_snapshot_plan_seconds": 30, + "automatic_updates_enabled": true, + "automatic_update_interval_days": 30, + "test_mode_enabled": false + }, + "profiles": { + "options": null, + "defaults": null, + "printers": {}, + "current_printer_profile_guid": null, + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "name": "Animated - Orbit", + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": false, + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": false + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "name": "Centered", + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "name": "Disabled", + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "disabled", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "name": "Animated - Printing", + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "name": "Back Right", + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 99.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "name": "Back Left", + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 1.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "name": "Back Center", + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "triggers": { + "f8e5e1a2-a54c-489b-961b-2530648e1625": { + "name": "Smart - High Quality", + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "guid": "f8e5e1a2-a54c-489b-961b-2530648e1625", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 3, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "322d69ef-e7cb-454a-916a-15856682932c": { + "name": "Smart - Fast (low quality)", + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "322d69ef-e7cb-454a-916a-15856682932c", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "name": "Timer - Every 1:00", + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 60, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "name": "Timer - Every 0:30", + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "name": "Classic - Every 0.5mm", + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.5, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "name": "Smart - Snap To Print", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "name": "Smart - Snap To Print - High Quality", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": true, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "name": "Smart - Snap To Print - Smooth", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": true, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "name": "Classic - Every Layer", + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "name": "Classic - Gcode", + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "name": "Smart - Compatibility", + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b838fe36-1459-4867-8243-ab7604cf0e2d": { + "name": "Smart - Gcode", + "description": "Takes a snapshot each time the snapshot command is encountered. The snapshot command is @OCTOLAPSE TAKE-SNAPSHOT, but you can specify an alternative snapshot command within your printer profile if you desire.", + "guid": "b838fe36-1459-4867-8243-ab7604cf0e2d", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Gcode", + "value": "smart_gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "name": "GIF - Fixed Length - 00:05", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "gif", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[0,0]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "name": "MP4 - 30 FPS", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "name": "MP4 - Fixed Length - 00:05", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10,10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "name": "MP4 - Fixed Length - 00:10", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 10, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "name": "MP4 - 60 FPS", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 60, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + } + }, + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "name": "Webcam - Default OctoPi 0.16.0", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras.", + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "enabled": true, + "camera_type": "webcam", + "gcode_camera_script": "", + "on_print_start_script": "", + "on_before_snapshot_script": "", + "external_camera_snapshot_script": "", + "on_after_snapshot_script": "", + "on_before_render_script": "", + "on_after_render_script": "", + "on_print_end_script": "", + "delay": 125, + "timeout_ms": 5000, + "snapshot_transpose": "", + "webcam_settings": { + "address": "http://tako3.prefecture/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "stream_template": "http://tako3.prefecture:8081/?action=stream", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "username": "", + "password": "", + "type": null, + "use_custom_webcam_settings_page": true, + "mjpg_streamer": { + "name": "mjpg-streamer", + "server_type": "mjpg-streamer", + "controls": {} + }, + "stream_download": false + }, + "enable_custom_image_preferences": false, + "apply_settings_before_print": true, + "apply_settings_at_startup": true + } + }, + "current_logging_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "logging": { + "4d7411e9-f97e-41c0-921c-02e262660e83": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Parsing/Preprocessing/Position Tracking", + "value": "c++" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Parsing/Preprocessing/Position Tracking", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "4d7411e9-f97e-41c0-921c-02e262660e83", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + } + ], + "description": "This logging profile is useful for debugging problems parsing gcode, tracking the printer's position and state, or preprocessing gcode to create snapshot plans using the new smart layer trigger. This profile will create very large log files and will make preprocessing VERY VERY slow, so use this only when necessary to solve a specific issue." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log All Errors - Recommended", + "value": "errors" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log All Errors", + "log_to_console": false, + "enabled": false, + "default_log_level": 10, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "enabled_loggers": [], + "description": "Logs only errors and exceptions. This is the recommended default profile, logs a minimal amount of data, and has a low impact on performance." + }, + "a417b5bf-9d26-4456-81ec-ba2f04f57084": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug Logging", + "value": "debug" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Debug", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "a417b5bf-9d26-4456-81ec-ba2f04f57084", + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.script" + }, + { + "log_level": 10, + "name": "octolapse.migration" + } + ], + "description": "Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "02717525-3684-4bf7-ac53-037cdfbd4e66": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Everything", + "value": "verbose" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Everything", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "02717525-3684-4bf7-ac53-037cdfbd4e66", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.__init__" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 5, + "name": "octolapse.position" + }, + { + "log_level": 5, + "name": "octolapse.render" + }, + { + "log_level": 5, + "name": "octolapse.settings" + }, + { + "log_level": 5, + "name": "octolapse.settings_external" + }, + { + "log_level": 5, + "name": "octolapse.settings_migration" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 5, + "name": "octolapse.timelapse" + }, + { + "log_level": 5, + "name": "octolapse.trigger" + }, + { + "log_level": 5, + "name": "octolapse.utility" + }, + { + "log_level": 5, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.migration" + } + ], + "description": "This will log absolutely everything that Octolapse is capable of logging. It will severely impact performance, and is only recommended for very difficult to debug or uncommon errors. I do not recommend you use this profile unless you are asked for a verbose log after creating an issue in the Octolapse Github repository." + } + } + }, + "global_options": null, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/settings_default_0.4.0rc1.dev4.json b/octoprint_octolapse/data/settings_default_0.4.0rc1.dev4.json new file mode 100644 index 00000000..e60f0ded --- /dev/null +++ b/octoprint_octolapse/data/settings_default_0.4.0rc1.dev4.json @@ -0,0 +1,1313 @@ +{ + "main_settings": { + "show_navbar_icon": true, + "show_navbar_when_not_printing": true, + "is_octolapse_enabled": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": 30, + "show_printer_state_changes": true, + "show_position_changes": true, + "show_extruder_state_changes": true, + "show_trigger_state_changes": true, + "show_snapshot_plan_information": true, + "cancel_print_on_startup_error": true, + "platform": "unknown", + "version": "0.4.0rc1.dev4", + "preview_snapshot_plans": true, + "preview_snapshot_plan_autoclose": false, + "preview_snapshot_plan_seconds": 30, + "automatic_updates_enabled": true, + "automatic_update_interval_days": 30, + "test_mode_enabled": false + }, + "profiles": { + "options": null, + "defaults": null, + "printers": {}, + "current_printer_profile_guid": null, + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "name": "Animated - Orbit", + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": false, + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": false + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "name": "Centered", + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "name": "Disabled", + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "disabled", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "name": "Animated - Printing", + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "name": "Back Right", + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 99.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "name": "Back Left", + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 1.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "name": "Back Center", + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "triggers": { + "f8e5e1a2-a54c-489b-961b-2530648e1625": { + "name": "Smart - High Quality", + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "guid": "f8e5e1a2-a54c-489b-961b-2530648e1625", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 3, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "322d69ef-e7cb-454a-916a-15856682932c": { + "name": "Smart - Fast (low quality)", + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "322d69ef-e7cb-454a-916a-15856682932c", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "name": "Timer - Every 1:00", + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 60, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "name": "Timer - Every 0:30", + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "name": "Classic - Every 0.5mm", + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.5, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "name": "Smart - Snap To Print", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "name": "Smart - Snap To Print - High Quality", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": true, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "name": "Smart - Snap To Print - Smooth", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": true, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "name": "Classic - Every Layer", + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "name": "Classic - Gcode", + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "name": "Smart - Compatibility", + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b838fe36-1459-4867-8243-ab7604cf0e2d": { + "name": "Smart - Gcode", + "description": "Takes a snapshot each time the snapshot command is encountered. The snapshot command is @OCTOLAPSE TAKE-SNAPSHOT, but you can specify an alternative snapshot command within your printer profile if you desire.", + "guid": "b838fe36-1459-4867-8243-ab7604cf0e2d", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Gcode", + "value": "smart_gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "name": "GIF - Fixed Length - 00:05", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "gif", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[0,0]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "name": "MP4 - 30 FPS", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "name": "MP4 - Fixed Length - 00:05", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10,10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "name": "MP4 - Fixed Length - 00:10", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 10, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "name": "MP4 - 60 FPS", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 60, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + } + }, + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "name": "Webcam - Default OctoPi 0.16.0", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras.", + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "enabled": true, + "camera_type": "webcam", + "gcode_camera_script": "", + "on_print_start_script": "", + "on_before_snapshot_script": "", + "external_camera_snapshot_script": "", + "on_after_snapshot_script": "", + "on_before_render_script": "", + "on_after_render_script": "", + "on_print_end_script": "", + "delay": 125, + "timeout_ms": 5000, + "snapshot_transpose": "", + "webcam_settings": { + "address": "http://tako3.prefecture/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "stream_template": "http://tako3.prefecture:8081/?action=stream", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "username": "", + "password": "", + "type": null, + "use_custom_webcam_settings_page": true, + "mjpg_streamer": { + "name": "mjpg-streamer", + "server_type": "mjpg-streamer", + "controls": {} + }, + "stream_download": false + }, + "enable_custom_image_preferences": false, + "apply_settings_before_print": true, + "apply_settings_at_startup": true + } + }, + "current_logging_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "logging": { + "4d7411e9-f97e-41c0-921c-02e262660e83": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Parsing/Preprocessing/Position Tracking", + "value": "c++" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Parsing/Preprocessing/Position Tracking", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "4d7411e9-f97e-41c0-921c-02e262660e83", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + } + ], + "description": "This logging profile is useful for debugging problems parsing gcode, tracking the printer's position and state, or preprocessing gcode to create snapshot plans using the new smart layer trigger. This profile will create very large log files and will make preprocessing VERY VERY slow, so use this only when necessary to solve a specific issue." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log All Errors - Recommended", + "value": "errors" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log All Errors", + "log_to_console": false, + "enabled": false, + "default_log_level": 10, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "enabled_loggers": [], + "description": "Logs only errors and exceptions. This is the recommended default profile, logs a minimal amount of data, and has a low impact on performance." + }, + "a417b5bf-9d26-4456-81ec-ba2f04f57084": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug Logging", + "value": "debug" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Debug", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "a417b5bf-9d26-4456-81ec-ba2f04f57084", + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.script" + }, + { + "log_level": 10, + "name": "octolapse.migration" + } + ], + "description": "Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "02717525-3684-4bf7-ac53-037cdfbd4e66": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Everything", + "value": "verbose" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Everything", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "02717525-3684-4bf7-ac53-037cdfbd4e66", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.__init__" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 5, + "name": "octolapse.position" + }, + { + "log_level": 5, + "name": "octolapse.render" + }, + { + "log_level": 5, + "name": "octolapse.settings" + }, + { + "log_level": 5, + "name": "octolapse.settings_external" + }, + { + "log_level": 5, + "name": "octolapse.settings_migration" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 5, + "name": "octolapse.timelapse" + }, + { + "log_level": 5, + "name": "octolapse.trigger" + }, + { + "log_level": 5, + "name": "octolapse.utility" + }, + { + "log_level": 5, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.migration" + } + ], + "description": "This will log absolutely everything that Octolapse is capable of logging. It will severely impact performance, and is only recommended for very difficult to debug or uncommon errors. I do not recommend you use this profile unless you are asked for a verbose log after creating an issue in the Octolapse Github repository." + } + } + }, + "global_options": null, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/settings_default_0.4.0rc1.dev5.json b/octoprint_octolapse/data/settings_default_0.4.0rc1.dev5.json new file mode 100644 index 00000000..b303ff0f --- /dev/null +++ b/octoprint_octolapse/data/settings_default_0.4.0rc1.dev5.json @@ -0,0 +1,1313 @@ +{ + "main_settings": { + "show_navbar_icon": true, + "show_navbar_when_not_printing": true, + "is_octolapse_enabled": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": 30, + "show_printer_state_changes": true, + "show_position_changes": true, + "show_extruder_state_changes": true, + "show_trigger_state_changes": true, + "show_snapshot_plan_information": true, + "cancel_print_on_startup_error": true, + "platform": "unknown", + "version": "0.4.0rc1.dev5", + "preview_snapshot_plans": true, + "preview_snapshot_plan_autoclose": false, + "preview_snapshot_plan_seconds": 30, + "automatic_updates_enabled": true, + "automatic_update_interval_days": 30, + "test_mode_enabled": false + }, + "profiles": { + "options": null, + "defaults": null, + "printers": {}, + "current_printer_profile_guid": null, + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "name": "Animated - Orbit", + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": false, + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": false + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "name": "Centered", + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "name": "Disabled", + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "disabled", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "name": "Animated - Printing", + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "name": "Back Right", + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 99.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "name": "Back Left", + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 1.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "name": "Back Center", + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "triggers": { + "f8e5e1a2-a54c-489b-961b-2530648e1625": { + "name": "Smart - High Quality", + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "guid": "f8e5e1a2-a54c-489b-961b-2530648e1625", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 3, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "322d69ef-e7cb-454a-916a-15856682932c": { + "name": "Smart - Fast (low quality)", + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "322d69ef-e7cb-454a-916a-15856682932c", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "name": "Timer - Every 1:00", + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 60, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "name": "Timer - Every 0:30", + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "name": "Classic - Every 0.5mm", + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.5, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "name": "Smart - Snap To Print", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "name": "Smart - Snap To Print - High Quality", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": true, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "name": "Smart - Snap To Print - Smooth", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": true, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "name": "Classic - Every Layer", + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "name": "Classic - Gcode", + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "name": "Smart - Compatibility", + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b838fe36-1459-4867-8243-ab7604cf0e2d": { + "name": "Smart - Gcode", + "description": "Takes a snapshot each time the snapshot command is encountered. The snapshot command is @OCTOLAPSE TAKE-SNAPSHOT, but you can specify an alternative snapshot command within your printer profile if you desire.", + "guid": "b838fe36-1459-4867-8243-ab7604cf0e2d", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Gcode", + "value": "smart_gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "name": "GIF - Fixed Length - 00:05", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "gif", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[0,0]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "name": "MP4 - 30 FPS", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "name": "MP4 - Fixed Length - 00:05", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10,10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "name": "MP4 - Fixed Length - 00:10", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 10, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "name": "MP4 - 60 FPS", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 60, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + } + }, + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "name": "Webcam - Default OctoPi 0.16.0", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras.", + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "enabled": true, + "camera_type": "webcam", + "gcode_camera_script": "", + "on_print_start_script": "", + "on_before_snapshot_script": "", + "external_camera_snapshot_script": "", + "on_after_snapshot_script": "", + "on_before_render_script": "", + "on_after_render_script": "", + "on_print_end_script": "", + "delay": 125, + "timeout_ms": 5000, + "snapshot_transpose": "", + "webcam_settings": { + "address": "http://tako3.prefecture/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "stream_template": "http://tako3.prefecture:8081/?action=stream", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "username": "", + "password": "", + "type": null, + "use_custom_webcam_settings_page": true, + "mjpg_streamer": { + "name": "mjpg-streamer", + "server_type": "mjpg-streamer", + "controls": {} + }, + "stream_download": false + }, + "enable_custom_image_preferences": false, + "apply_settings_before_print": true, + "apply_settings_at_startup": true + } + }, + "current_logging_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "logging": { + "4d7411e9-f97e-41c0-921c-02e262660e83": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Parsing/Preprocessing/Position Tracking", + "value": "c++" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Parsing/Preprocessing/Position Tracking", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "4d7411e9-f97e-41c0-921c-02e262660e83", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + } + ], + "description": "This logging profile is useful for debugging problems parsing gcode, tracking the printer's position and state, or preprocessing gcode to create snapshot plans using the new smart layer trigger. This profile will create very large log files and will make preprocessing VERY VERY slow, so use this only when necessary to solve a specific issue." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log All Errors - Recommended", + "value": "errors" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log All Errors", + "log_to_console": false, + "enabled": false, + "default_log_level": 10, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "enabled_loggers": [], + "description": "Logs only errors and exceptions. This is the recommended default profile, logs a minimal amount of data, and has a low impact on performance." + }, + "a417b5bf-9d26-4456-81ec-ba2f04f57084": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug Logging", + "value": "debug" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Debug", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "a417b5bf-9d26-4456-81ec-ba2f04f57084", + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.script" + }, + { + "log_level": 10, + "name": "octolapse.migration" + } + ], + "description": "Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "02717525-3684-4bf7-ac53-037cdfbd4e66": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Everything", + "value": "verbose" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Everything", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "02717525-3684-4bf7-ac53-037cdfbd4e66", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.__init__" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 5, + "name": "octolapse.position" + }, + { + "log_level": 5, + "name": "octolapse.render" + }, + { + "log_level": 5, + "name": "octolapse.settings" + }, + { + "log_level": 5, + "name": "octolapse.settings_external" + }, + { + "log_level": 5, + "name": "octolapse.settings_migration" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 5, + "name": "octolapse.timelapse" + }, + { + "log_level": 5, + "name": "octolapse.trigger" + }, + { + "log_level": 5, + "name": "octolapse.utility" + }, + { + "log_level": 5, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.migration" + } + ], + "description": "This will log absolutely everything that Octolapse is capable of logging. It will severely impact performance, and is only recommended for very difficult to debug or uncommon errors. I do not recommend you use this profile unless you are asked for a verbose log after creating an issue in the Octolapse Github repository." + } + } + }, + "global_options": null, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/settings_default_0.4.0rc1.json b/octoprint_octolapse/data/settings_default_0.4.0rc1.json new file mode 100644 index 00000000..e5e94707 --- /dev/null +++ b/octoprint_octolapse/data/settings_default_0.4.0rc1.json @@ -0,0 +1,1313 @@ +{ + "main_settings": { + "show_navbar_icon": true, + "show_navbar_when_not_printing": true, + "is_octolapse_enabled": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": 30, + "show_printer_state_changes": true, + "show_position_changes": true, + "show_extruder_state_changes": true, + "show_trigger_state_changes": true, + "show_snapshot_plan_information": true, + "cancel_print_on_startup_error": true, + "platform": "unknown", + "version": "0.4.0rc1", + "preview_snapshot_plans": true, + "preview_snapshot_plan_autoclose": false, + "preview_snapshot_plan_seconds": 30, + "automatic_updates_enabled": true, + "automatic_update_interval_days": 30, + "test_mode_enabled": false + }, + "profiles": { + "options": null, + "defaults": null, + "printers": {}, + "current_printer_profile_guid": null, + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "name": "Animated - Orbit", + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": false, + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": false + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "name": "Centered", + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "name": "Disabled", + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "disabled", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "name": "Animated - Printing", + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "name": "Back Right", + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 99.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "name": "Back Left", + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 1.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "name": "Back Center", + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "triggers": { + "f8e5e1a2-a54c-489b-961b-2530648e1625": { + "name": "Smart - High Quality", + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "guid": "f8e5e1a2-a54c-489b-961b-2530648e1625", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 3, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "322d69ef-e7cb-454a-916a-15856682932c": { + "name": "Smart - Fast (low quality)", + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "322d69ef-e7cb-454a-916a-15856682932c", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "name": "Timer - Every 1:00", + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 60, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "name": "Timer - Every 0:30", + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "name": "Classic - Every 0.5mm", + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.5, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "name": "Smart - Snap To Print", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "name": "Smart - Snap To Print - High Quality", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": true, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "name": "Smart - Snap To Print - Smooth", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": true, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "name": "Classic - Every Layer", + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "name": "Classic - Gcode", + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "name": "Smart - Compatibility", + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b838fe36-1459-4867-8243-ab7604cf0e2d": { + "name": "Smart - Gcode", + "description": "Takes a snapshot each time the snapshot command is encountered. The snapshot command is @OCTOLAPSE TAKE-SNAPSHOT, but you can specify an alternative snapshot command within your printer profile if you desire.", + "guid": "b838fe36-1459-4867-8243-ab7604cf0e2d", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Gcode", + "value": "smart_gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "name": "GIF - Fixed Length - 00:05", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "gif", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[0,0]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "name": "MP4 - 30 FPS", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "name": "MP4 - Fixed Length - 00:05", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10,10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "name": "MP4 - Fixed Length - 00:10", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 10, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "name": "MP4 - 60 FPS", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 60, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + } + }, + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "name": "Webcam - Default OctoPi 0.16.0", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras.", + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "enabled": true, + "camera_type": "webcam", + "gcode_camera_script": "", + "on_print_start_script": "", + "on_before_snapshot_script": "", + "external_camera_snapshot_script": "", + "on_after_snapshot_script": "", + "on_before_render_script": "", + "on_after_render_script": "", + "on_print_end_script": "", + "delay": 125, + "timeout_ms": 5000, + "snapshot_transpose": "", + "webcam_settings": { + "address": "http://tako3.prefecture/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "stream_template": "http://tako3.prefecture:8081/?action=stream", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "username": "", + "password": "", + "type": null, + "use_custom_webcam_settings_page": true, + "mjpg_streamer": { + "name": "mjpg-streamer", + "server_type": "mjpg-streamer", + "controls": {} + }, + "stream_download": false + }, + "enable_custom_image_preferences": false, + "apply_settings_before_print": true, + "apply_settings_at_startup": true + } + }, + "current_logging_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "logging": { + "4d7411e9-f97e-41c0-921c-02e262660e83": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Parsing/Preprocessing/Position Tracking", + "value": "c++" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Parsing/Preprocessing/Position Tracking", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "4d7411e9-f97e-41c0-921c-02e262660e83", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + } + ], + "description": "This logging profile is useful for debugging problems parsing gcode, tracking the printer's position and state, or preprocessing gcode to create snapshot plans using the new smart layer trigger. This profile will create very large log files and will make preprocessing VERY VERY slow, so use this only when necessary to solve a specific issue." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log All Errors - Recommended", + "value": "errors" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log All Errors", + "log_to_console": false, + "enabled": false, + "default_log_level": 10, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "enabled_loggers": [], + "description": "Logs only errors and exceptions. This is the recommended default profile, logs a minimal amount of data, and has a low impact on performance." + }, + "a417b5bf-9d26-4456-81ec-ba2f04f57084": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug Logging", + "value": "debug" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Debug", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "a417b5bf-9d26-4456-81ec-ba2f04f57084", + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.script" + }, + { + "log_level": 10, + "name": "octolapse.migration" + } + ], + "description": "Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "02717525-3684-4bf7-ac53-037cdfbd4e66": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Everything", + "value": "verbose" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Everything", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "02717525-3684-4bf7-ac53-037cdfbd4e66", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.__init__" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 5, + "name": "octolapse.position" + }, + { + "log_level": 5, + "name": "octolapse.render" + }, + { + "log_level": 5, + "name": "octolapse.settings" + }, + { + "log_level": 5, + "name": "octolapse.settings_external" + }, + { + "log_level": 5, + "name": "octolapse.settings_migration" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 5, + "name": "octolapse.timelapse" + }, + { + "log_level": 5, + "name": "octolapse.trigger" + }, + { + "log_level": 5, + "name": "octolapse.utility" + }, + { + "log_level": 5, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.migration" + } + ], + "description": "This will log absolutely everything that Octolapse is capable of logging. It will severely impact performance, and is only recommended for very difficult to debug or uncommon errors. I do not recommend you use this profile unless you are asked for a verbose log after creating an issue in the Octolapse Github repository." + } + } + }, + "global_options": null, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/settings_default_0.4.0rc2.json b/octoprint_octolapse/data/settings_default_0.4.0rc2.json new file mode 100644 index 00000000..64bb94b0 --- /dev/null +++ b/octoprint_octolapse/data/settings_default_0.4.0rc2.json @@ -0,0 +1,1313 @@ +{ + "main_settings": { + "show_navbar_icon": true, + "show_navbar_when_not_printing": true, + "is_octolapse_enabled": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": 30, + "show_printer_state_changes": true, + "show_position_changes": true, + "show_extruder_state_changes": true, + "show_trigger_state_changes": true, + "show_snapshot_plan_information": true, + "cancel_print_on_startup_error": true, + "platform": "unknown", + "version": "0.4.0rc2", + "preview_snapshot_plans": true, + "preview_snapshot_plan_autoclose": false, + "preview_snapshot_plan_seconds": 30, + "automatic_updates_enabled": true, + "automatic_update_interval_days": 30, + "test_mode_enabled": false + }, + "profiles": { + "options": null, + "defaults": null, + "printers": {}, + "current_printer_profile_guid": null, + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "name": "Animated - Orbit", + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": false, + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": false + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "name": "Centered", + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "name": "Disabled", + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "disabled", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "name": "Animated - Printing", + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "name": "Back Right", + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 99.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "name": "Back Left", + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 1.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "name": "Back Center", + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "triggers": { + "f8e5e1a2-a54c-489b-961b-2530648e1625": { + "name": "Smart - High Quality", + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "guid": "f8e5e1a2-a54c-489b-961b-2530648e1625", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 3, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "322d69ef-e7cb-454a-916a-15856682932c": { + "name": "Smart - Fast (low quality)", + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "322d69ef-e7cb-454a-916a-15856682932c", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "name": "Timer - Every 1:00", + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 60, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "name": "Timer - Every 0:30", + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "name": "Classic - Every 0.5mm", + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.5, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "name": "Smart - Snap To Print", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "name": "Smart - Snap To Print - High Quality", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. It will NOT work with vase mode! As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\n If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": true, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "name": "Smart - Snap To Print - Smooth", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": true, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "name": "Classic - Every Layer", + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "name": "Classic - Gcode", + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "name": "Smart - Compatibility", + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b838fe36-1459-4867-8243-ab7604cf0e2d": { + "name": "Smart - Gcode", + "description": "Takes a snapshot each time the snapshot command is encountered. The snapshot command is @OCTOLAPSE TAKE-SNAPSHOT, but you can specify an alternative snapshot command within your printer profile if you desire.", + "guid": "b838fe36-1459-4867-8243-ab7604cf0e2d", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Gcode", + "value": "smart_gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "name": "GIF - Fixed Length - 00:05", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "gif", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[0,0]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "name": "MP4 - 30 FPS", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "name": "MP4 - Fixed Length - 00:05", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10,10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "name": "MP4 - Fixed Length - 00:10", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 10, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "name": "MP4 - 60 FPS", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 60, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + } + }, + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "name": "Webcam - Default OctoPi 0.16.0", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras.", + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "enabled": true, + "camera_type": "webcam", + "gcode_camera_script": "", + "on_print_start_script": "", + "on_before_snapshot_script": "", + "external_camera_snapshot_script": "", + "on_after_snapshot_script": "", + "on_before_render_script": "", + "on_after_render_script": "", + "on_print_end_script": "", + "delay": 125, + "timeout_ms": 5000, + "snapshot_transpose": "", + "webcam_settings": { + "address": "http://tako3.prefecture/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "stream_template": "http://tako3.prefecture:8081/?action=stream", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "username": "", + "password": "", + "type": null, + "use_custom_webcam_settings_page": true, + "mjpg_streamer": { + "name": "mjpg-streamer", + "server_type": "mjpg-streamer", + "controls": {} + }, + "stream_download": false + }, + "enable_custom_image_preferences": false, + "apply_settings_before_print": true, + "apply_settings_at_startup": true + } + }, + "current_logging_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "logging": { + "4d7411e9-f97e-41c0-921c-02e262660e83": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Parsing/Preprocessing/Position Tracking", + "value": "c++" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Parsing/Preprocessing/Position Tracking", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "4d7411e9-f97e-41c0-921c-02e262660e83", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + } + ], + "description": "This logging profile is useful for debugging problems parsing gcode, tracking the printer's position and state, or preprocessing gcode to create snapshot plans using the new smart layer trigger. This profile will create very large log files and will make preprocessing VERY VERY slow, so use this only when necessary to solve a specific issue." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log All Errors - Recommended", + "value": "errors" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log All Errors", + "log_to_console": false, + "enabled": false, + "default_log_level": 10, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "enabled_loggers": [], + "description": "Logs only errors and exceptions. This is the recommended default profile, logs a minimal amount of data, and has a low impact on performance." + }, + "a417b5bf-9d26-4456-81ec-ba2f04f57084": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug Logging", + "value": "debug" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Debug", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "a417b5bf-9d26-4456-81ec-ba2f04f57084", + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.script" + }, + { + "log_level": 10, + "name": "octolapse.migration" + } + ], + "description": "Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "02717525-3684-4bf7-ac53-037cdfbd4e66": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Everything", + "value": "verbose" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Everything", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "02717525-3684-4bf7-ac53-037cdfbd4e66", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.__init__" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 5, + "name": "octolapse.position" + }, + { + "log_level": 5, + "name": "octolapse.render" + }, + { + "log_level": 5, + "name": "octolapse.settings" + }, + { + "log_level": 5, + "name": "octolapse.settings_external" + }, + { + "log_level": 5, + "name": "octolapse.settings_migration" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 5, + "name": "octolapse.timelapse" + }, + { + "log_level": 5, + "name": "octolapse.trigger" + }, + { + "log_level": 5, + "name": "octolapse.utility" + }, + { + "log_level": 5, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.migration" + } + ], + "description": "This will log absolutely everything that Octolapse is capable of logging. It will severely impact performance, and is only recommended for very difficult to debug or uncommon errors. I do not recommend you use this profile unless you are asked for a verbose log after creating an issue in the Octolapse Github repository." + } + } + }, + "global_options": null, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/settings_default_0.4.0rc3.json b/octoprint_octolapse/data/settings_default_0.4.0rc3.json new file mode 100644 index 00000000..3fa74739 --- /dev/null +++ b/octoprint_octolapse/data/settings_default_0.4.0rc3.json @@ -0,0 +1,1313 @@ +{ + "main_settings": { + "show_navbar_icon": true, + "show_navbar_when_not_printing": true, + "is_octolapse_enabled": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": 30, + "show_printer_state_changes": true, + "show_position_changes": true, + "show_extruder_state_changes": true, + "show_trigger_state_changes": true, + "show_snapshot_plan_information": true, + "cancel_print_on_startup_error": true, + "platform": "unknown", + "version": "0.4.0rc3", + "preview_snapshot_plans": true, + "preview_snapshot_plan_autoclose": false, + "preview_snapshot_plan_seconds": 30, + "automatic_updates_enabled": true, + "automatic_update_interval_days": 30, + "test_mode_enabled": false + }, + "profiles": { + "options": null, + "defaults": null, + "printers": {}, + "current_printer_profile_guid": null, + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "name": "Animated - Orbit", + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": false, + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": false + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "name": "Centered", + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "name": "Disabled", + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "disabled", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "name": "Animated - Printing", + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative_path", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 50.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "name": "Back Right", + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 99.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "name": "Back Left", + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 1.0, + "x_relative_print": 50.0, + "x_relative_path": "50.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 50.0, + "y_relative_path": "50", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "name": "Back Center", + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "x_type": "relative", + "x_fixed_coordinate": 0.0, + "x_fixed_path": "0", + "x_fixed_path_loop": true, + "x_fixed_path_invert_loop": true, + "x_relative": 50.0, + "x_relative_print": 100.0, + "x_relative_path": "100.0", + "x_relative_path_loop": true, + "x_relative_path_invert_loop": true, + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "y_fixed_path": "0", + "y_fixed_path_loop": true, + "y_fixed_path_invert_loop": true, + "y_relative": 99.0, + "y_relative_print": 100.0, + "y_relative_path": "100", + "y_relative_path_loop": true, + "y_relative_path_invert_loop": true + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "triggers": { + "f8e5e1a2-a54c-489b-961b-2530648e1625": { + "name": "Smart - High Quality", + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "guid": "f8e5e1a2-a54c-489b-961b-2530648e1625", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 3, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "322d69ef-e7cb-454a-916a-15856682932c": { + "name": "Smart - Fast (low quality)", + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "322d69ef-e7cb-454a-916a-15856682932c", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "name": "Timer - Every 1:00", + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 60, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "name": "Timer - Every 0:30", + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "name": "Classic - Every 0.5mm", + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.5, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "name": "Smart - Snap To Print", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "name": "Smart - Snap To Print - High Quality", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. It will NOT work with vase mode! As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\n If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": true, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "name": "Smart - Snap To Print - Smooth", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": true, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "name": "Classic - Every Layer", + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "name": "Classic - Gcode", + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "name": "Smart - Compatibility", + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b838fe36-1459-4867-8243-ab7604cf0e2d": { + "name": "Smart - Gcode", + "description": "Takes a snapshot each time the snapshot command is encountered. The snapshot command is @OCTOLAPSE TAKE-SNAPSHOT, but you can specify an alternative snapshot command within your printer profile if you desire.", + "guid": "b838fe36-1459-4867-8243-ab7604cf0e2d", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Gcode", + "value": "smart_gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "name": "GIF - Fixed Length - 00:05", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "gif", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[0,0]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "name": "MP4 - 30 FPS", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "name": "MP4 - Fixed Length - 00:05", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10,10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "name": "MP4 - Fixed Length - 00:10", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 10, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "name": "MP4 - 60 FPS", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 60, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ 255, 255, 255, 1 ], + "overlay_outline_color": [ 0, 0, 0, 1.0 ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + } + }, + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "name": "Webcam - Default OctoPi 0.16.0", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras.", + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "enabled": true, + "camera_type": "webcam", + "gcode_camera_script": "", + "on_print_start_script": "", + "on_before_snapshot_script": "", + "external_camera_snapshot_script": "", + "on_after_snapshot_script": "", + "on_before_render_script": "", + "on_after_render_script": "", + "on_print_end_script": "", + "delay": 125, + "timeout_ms": 5000, + "snapshot_transpose": "", + "webcam_settings": { + "address": "http://tako3.prefecture/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "stream_template": "http://tako3.prefecture:8081/?action=stream", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "username": "", + "password": "", + "type": null, + "use_custom_webcam_settings_page": true, + "mjpg_streamer": { + "name": "mjpg-streamer", + "server_type": "mjpg-streamer", + "controls": {} + }, + "stream_download": false + }, + "enable_custom_image_preferences": false, + "apply_settings_before_print": true, + "apply_settings_at_startup": true + } + }, + "current_logging_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "logging": { + "4d7411e9-f97e-41c0-921c-02e262660e83": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Parsing/Preprocessing/Position Tracking", + "value": "c++" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Parsing/Preprocessing/Position Tracking", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "4d7411e9-f97e-41c0-921c-02e262660e83", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + } + ], + "description": "This logging profile is useful for debugging problems parsing gcode, tracking the printer's position and state, or preprocessing gcode to create snapshot plans using the new smart layer trigger. This profile will create very large log files and will make preprocessing VERY VERY slow, so use this only when necessary to solve a specific issue." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log All Errors - Recommended", + "value": "errors" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log All Errors", + "log_to_console": false, + "enabled": false, + "default_log_level": 10, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "enabled_loggers": [], + "description": "Logs only errors and exceptions. This is the recommended default profile, logs a minimal amount of data, and has a low impact on performance." + }, + "a417b5bf-9d26-4456-81ec-ba2f04f57084": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug Logging", + "value": "debug" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Debug", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "a417b5bf-9d26-4456-81ec-ba2f04f57084", + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.script" + }, + { + "log_level": 10, + "name": "octolapse.migration" + } + ], + "description": "Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "02717525-3684-4bf7-ac53-037cdfbd4e66": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Everything", + "value": "verbose" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Everything", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "02717525-3684-4bf7-ac53-037cdfbd4e66", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.__init__" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 5, + "name": "octolapse.position" + }, + { + "log_level": 5, + "name": "octolapse.render" + }, + { + "log_level": 5, + "name": "octolapse.settings" + }, + { + "log_level": 5, + "name": "octolapse.settings_external" + }, + { + "log_level": 5, + "name": "octolapse.settings_migration" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 5, + "name": "octolapse.timelapse" + }, + { + "log_level": 5, + "name": "octolapse.trigger" + }, + { + "log_level": 5, + "name": "octolapse.utility" + }, + { + "log_level": 5, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.migration" + } + ], + "description": "This will log absolutely everything that Octolapse is capable of logging. It will severely impact performance, and is only recommended for very difficult to debug or uncommon errors. I do not recommend you use this profile unless you are asked for a verbose log after creating an issue in the Octolapse Github repository." + } + } + }, + "global_options": null, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/settings_default_0.4.3.json b/octoprint_octolapse/data/settings_default_0.4.3.json new file mode 100644 index 00000000..1fdab981 --- /dev/null +++ b/octoprint_octolapse/data/settings_default_0.4.3.json @@ -0,0 +1,1658 @@ +{ + "main_settings": { + "show_navbar_icon": true, + "show_navbar_when_not_printing": true, + "is_octolapse_enabled": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": 30, + "show_printer_state_changes": true, + "show_position_changes": true, + "show_extruder_state_changes": true, + "show_trigger_state_changes": true, + "show_snapshot_plan_information": true, + "cancel_print_on_startup_error": true, + "platform": "unknown", + "version": "0.4.0", + "settings_version": "0.4.0", + "preview_snapshot_plans": true, + "preview_snapshot_plan_autoclose": false, + "preview_snapshot_plan_seconds": 30, + "automatic_updates_enabled": true, + "automatic_update_interval_days": 30, + "test_mode_enabled": false + }, + "profiles": { + "options": null, + "defaults": null, + "printers": {}, + "current_printer_profile_guid": null, + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": false, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": false, + "name": "Animated - Orbit", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative_path" + }, + "a581ea51-096b-4852-8aa6-28456bf9237e": { + "automatic_configuration": { + "key_values": [ + { + "name": "Center Right", + "value": "center_right" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "a581ea51-096b-4852-8aa6-28456bf9237e", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the right center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Center Right", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Centered", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Disabled", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "disabled" + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Animated - Printing", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative_path" + }, + "18b35adf-543e-406e-83b8-d8a404961339": { + "automatic_configuration": { + "key_values": [ + { + "name": "Front Right", + "value": "front_right" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 1.0, + "x_relative_path": "100.0", + "guid": "18b35adf-543e-406e-83b8-d8a404961339", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the front right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Front Right", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "100.0", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Right", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "b510ae51-b7c5-43c8-8611-6bfc63a05037": { + "automatic_configuration": { + "key_values": [ + { + "name": "Front Left", + "value": "front_left" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 1.0, + "x_relative_path": "100.0", + "guid": "b510ae51-b7c5-43c8-8611-6bfc63a05037", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the front left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Front Left", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "50.0", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Left", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "5c8b02ba-095d-45f9-b7f7-f2a935f34a70": { + "automatic_configuration": { + "key_values": [ + { + "name": "Center Left", + "value": "center_left" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "5c8b02ba-095d-45f9-b7f7-f2a935f34a70", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the left center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Center Left", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "69a0c73c-907c-4a0e-9e96-02d8ae730ac3": { + "automatic_configuration": { + "key_values": [ + { + "name": "Front Center", + "value": "front_center" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 1.0, + "x_relative_path": "100.0", + "guid": "69a0c73c-907c-4a0e-9e96-02d8ae730ac3", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the front center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Front Center", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "100.0", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Center", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "triggers": { + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "name": "Classic - Every 0.5mm", + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.5, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "name": "Classic - Every Layer", + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "name": "Classic - Gcode", + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "name": "Smart - Compatibility", + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "322d69ef-e7cb-454a-916a-15856682932c": { + "name": "Smart - Fast (low quality)", + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "322d69ef-e7cb-454a-916a-15856682932c", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b838fe36-1459-4867-8243-ab7604cf0e2d": { + "name": "Smart - Gcode", + "description": "Takes a snapshot each time the snapshot command is encountered. The snapshot command is @OCTOLAPSE TAKE-SNAPSHOT, but you can specify an alternative snapshot command within your printer profile if you desire.", + "guid": "b838fe36-1459-4867-8243-ab7604cf0e2d", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Gcode", + "value": "smart_gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "f8e5e1a2-a54c-489b-961b-2530648e1625": { + "name": "Smart - High Quality", + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "guid": "f8e5e1a2-a54c-489b-961b-2530648e1625", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 3, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "name": "Smart - Snap To Print", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "name": "Smart - Snap To Print - High Quality", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. It will NOT work with vase mode! As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\n If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": true, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "name": "Smart - Snap To Print - Smooth", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": true, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "name": "Timer - Every 1:00", + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 60, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "name": "Timer - Every 0:30", + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "name": "GIF - Fixed Length - 00:05", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "gif", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[0,0]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "name": "MP4 - 30 FPS", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "name": "MP4 - Fixed Length - 00:05", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10,10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "name": "MP4 - Fixed Length - 00:10", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 10, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "name": "MP4 - 60 FPS", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 60, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "35aababf-0ecf-46d5-b142-6290d38c8fea": { + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled (for manual rendering)", + "value": "disabled" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.1" + }, + "overlay_text_template": "", + "overlay_text_valign": "top", + "thread_count": 1, + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "min_fps": 2.0, + "guid": "35aababf-0ecf-46d5-b142-6290d38c8fea", + "overlay_font_path": "", + "sync_with_timelapse": true, + "constant_rate_factor": 28, + "overlay_text_pos": "[0,0]", + "overlay_outline_width": 1, + "fps": 30, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "selected_watermark": "", + "output_format": "mp4", + "description": "No timelapse will be generated, but all snapshots will be in the Octolapse data directory and can be downloaded and manually rendered.", + "overlay_text_alignment": "left", + "archive_snapshots": false, + "fps_calculation_type": "duration", + "post_roll_seconds": 0, + "bitrate": "6000k", + "name": "Disabled", + "overlay_text_halign": "left", + "enabled": false, + "enable_watermark": false, + "pre_roll_seconds": 0, + "overlay_font_size": 10, + "run_length_seconds": 5, + "max_fps": 120.0, + "overlay_outline_color": [ + 0, + 0, + 0, + 1 + ] + } + }, + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "name": "Webcam - Default OctoPi 0.16.0", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras.", + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "enabled": true, + "camera_type": "webcam", + "gcode_camera_script": "", + "on_print_start_script": "", + "on_before_snapshot_script": "", + "external_camera_snapshot_script": "", + "on_after_snapshot_script": "", + "on_before_render_script": "", + "on_after_render_script": "", + "on_print_end_script": "", + "delay": 125, + "timeout_ms": 5000, + "snapshot_transpose": "", + "webcam_settings": { + "address": "http://tako3.prefecture/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "stream_template": "http://tako3.prefecture:8081/?action=stream", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "username": "", + "password": "", + "type": null, + "use_custom_webcam_settings_page": true, + "mjpg_streamer": { + "name": "mjpg-streamer", + "server_type": "mjpg-streamer", + "controls": {} + }, + "stream_download": false + }, + "enable_custom_image_preferences": false, + "apply_settings_before_print": true, + "apply_settings_at_startup": true + } + }, + "current_logging_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "logging": { + "4d7411e9-f97e-41c0-921c-02e262660e83": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Parsing/Preprocessing/Position Tracking", + "value": "c++" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Parsing/Preprocessing/Position Tracking", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "4d7411e9-f97e-41c0-921c-02e262660e83", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + } + ], + "description": "This logging profile is useful for debugging problems parsing gcode, tracking the printer's position and state, or preprocessing gcode to create snapshot plans using the new smart layer trigger. This profile will create very large log files and will make preprocessing VERY VERY slow, so use this only when necessary to solve a specific issue." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log All Errors - Recommended", + "value": "errors" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log All Errors", + "log_to_console": false, + "enabled": false, + "default_log_level": 10, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "enabled_loggers": [], + "description": "Logs only errors and exceptions. This is the recommended default profile, logs a minimal amount of data, and has a low impact on performance." + }, + "a417b5bf-9d26-4456-81ec-ba2f04f57084": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug Logging", + "value": "debug" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Debug", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "a417b5bf-9d26-4456-81ec-ba2f04f57084", + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.script" + }, + { + "log_level": 10, + "name": "octolapse.migration" + } + ], + "description": "Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "fa759ab9-bc02-4fd0-8d12-a7c5c89591c1": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug - Script Cameras (DSLR)", + "value": "debug_script_cameras" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": 1.0 + }, + "name": "Debug - Script Cameras (DSLR)", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "fa759ab9-bc02-4fd0-8d12-a7c5c89591c1", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.render" + } + ], + "description": "This profile is useful for debugging script based cameras, like DSLRs." + }, + "02717525-3684-4bf7-ac53-037cdfbd4e66": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Everything", + "value": "verbose" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Everything", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "02717525-3684-4bf7-ac53-037cdfbd4e66", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.__init__" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 5, + "name": "octolapse.position" + }, + { + "log_level": 5, + "name": "octolapse.render" + }, + { + "log_level": 5, + "name": "octolapse.settings" + }, + { + "log_level": 5, + "name": "octolapse.settings_external" + }, + { + "log_level": 5, + "name": "octolapse.settings_migration" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 5, + "name": "octolapse.timelapse" + }, + { + "log_level": 5, + "name": "octolapse.trigger" + }, + { + "log_level": 5, + "name": "octolapse.utility" + }, + { + "log_level": 5, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.migration" + } + ], + "description": "This will log absolutely everything that Octolapse is capable of logging. It will severely impact performance, and is only recommended for very difficult to debug or uncommon errors. I do not recommend you use this profile unless you are asked for a verbose log after creating an issue in the Octolapse Github repository." + } + } + }, + "global_options": null, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/settings_default_current.json b/octoprint_octolapse/data/settings_default_current.json new file mode 100644 index 00000000..1fdab981 --- /dev/null +++ b/octoprint_octolapse/data/settings_default_current.json @@ -0,0 +1,1658 @@ +{ + "main_settings": { + "show_navbar_icon": true, + "show_navbar_when_not_printing": true, + "is_octolapse_enabled": true, + "auto_reload_latest_snapshot": true, + "auto_reload_frames": 30, + "show_printer_state_changes": true, + "show_position_changes": true, + "show_extruder_state_changes": true, + "show_trigger_state_changes": true, + "show_snapshot_plan_information": true, + "cancel_print_on_startup_error": true, + "platform": "unknown", + "version": "0.4.0", + "settings_version": "0.4.0", + "preview_snapshot_plans": true, + "preview_snapshot_plan_autoclose": false, + "preview_snapshot_plan_seconds": 30, + "automatic_updates_enabled": true, + "automatic_update_interval_days": 30, + "test_mode_enabled": false + }, + "profiles": { + "options": null, + "defaults": null, + "printers": {}, + "current_printer_profile_guid": null, + "current_stabilization_profile_guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "stabilizations": { + "801846db-3889-4d90-8496-9a473ee99cc4": { + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Orbit", + "value": "animated_orbit" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000,50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000", + "guid": "801846db-3889-4d90-8496-9a473ee99cc4", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": false, + "y_relative_path": "50.975,51.913,52.778,53.536,54.157,54.619,54.904,55.000,54.904,54.619,54.157,53.536,52.778,51.913,50.975,50.000,49.025,48.087,47.222,46.464,45.843,45.381,45.096,45.000,45.096,45.381,45.843,46.464,47.222,48.087,49.025,50.000", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative_path", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Moves print in a circular pattern around the center. Best for prints with lower framerates, else the spiraling will be too fast. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": false, + "name": "Animated - Orbit", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative_path" + }, + "a581ea51-096b-4852-8aa6-28456bf9237e": { + "automatic_configuration": { + "key_values": [ + { + "name": "Center Right", + "value": "center_right" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "a581ea51-096b-4852-8aa6-28456bf9237e", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the right center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Center Right", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "d35dbbe6-949e-4f97-b16f-c575d94ad062": { + "automatic_configuration": { + "key_values": [ + { + "name": "Centered", + "value": "centered" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "d35dbbe6-949e-4f97-b16f-c575d94ad062", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Centered", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "a1137d9a-70b6-4941-95fb-74c49e0ae9b8": { + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled", + "value": "disabled" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "a1137d9a-70b6-4941-95fb-74c49e0ae9b8", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "disabled", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "No stabilization will be performed. This has an interesting effect if you are using the 'snap to print' option with the new smart layer trigger. For other profiles it replicates the look of the stock Octoprint timelapse but without the motion blur.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Disabled", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "disabled" + }, + "0d97b0ec-57ee-425b-a202-ea037910337d": { + "automatic_configuration": { + "key_values": [ + { + "name": "Animated - Printing", + "value": "animated_printing" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "45, 45.5, 46, 46.5, 47, 47.5, 48, 48.5, 49, 49.5, 50, 50.5, 51, 51.5, 52, 52.5, 53, 53.5, 54, 54.5, 55", + "guid": "0d97b0ec-57ee-425b-a202-ea037910337d", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Animate the X carriage with a back-and-forth motion while keeping the Bed stable. I like this one! For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Animated - Printing", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative_path" + }, + "18b35adf-543e-406e-83b8-d8a404961339": { + "automatic_configuration": { + "key_values": [ + { + "name": "Front Right", + "value": "front_right" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 1.0, + "x_relative_path": "100.0", + "guid": "18b35adf-543e-406e-83b8-d8a404961339", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the front right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Front Right", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "48413ad7-8345-4da5-af0b-a4f7a4350fc3": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Right", + "value": "back_right" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "100.0", + "guid": "48413ad7-8345-4da5-af0b-a4f7a4350fc3", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back right for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 99.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Right", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "b510ae51-b7c5-43c8-8611-6bfc63a05037": { + "automatic_configuration": { + "key_values": [ + { + "name": "Front Left", + "value": "front_left" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 1.0, + "x_relative_path": "100.0", + "guid": "b510ae51-b7c5-43c8-8611-6bfc63a05037", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the front left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Front Left", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "93036d8c-de74-4fc9-96aa-d9bb410df9b0": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Left", + "value": "back_left" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "50.0", + "guid": "93036d8c-de74-4fc9-96aa-d9bb410df9b0", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back left for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Left", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "5c8b02ba-095d-45f9-b7f7-f2a935f34a70": { + "automatic_configuration": { + "key_values": [ + { + "name": "Center Left", + "value": "center_left" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 50.0, + "x_relative_path": "50.0", + "guid": "5c8b02ba-095d-45f9-b7f7-f2a935f34a70", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "50", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the left center of the bed for most triggers. For snap to print triggers, this will stabilize the extruder as close as possible the stabilization position depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 50.0, + "x_relative": 1.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Center Left", + "y_fixed_path": "0", + "y_relative_print": 50.0, + "x_type": "relative" + }, + "69a0c73c-907c-4a0e-9e96-02d8ae730ac3": { + "automatic_configuration": { + "key_values": [ + { + "name": "Front Center", + "value": "front_center" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 1.0, + "x_relative_path": "100.0", + "guid": "69a0c73c-907c-4a0e-9e96-02d8ae730ac3", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the front center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Front Center", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + }, + "40b1aeba-af5c-4042-88d6-c571412e3fcf": { + "automatic_configuration": { + "key_values": [ + { + "name": "Back Center", + "value": "back_center" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "y_relative_path_loop": true, + "y_relative": 99.0, + "x_relative_path": "100.0", + "guid": "40b1aeba-af5c-4042-88d6-c571412e3fcf", + "y_fixed_path_loop": true, + "x_relative_path_invert_loop": true, + "y_relative_path": "100", + "x_relative_path_loop": true, + "x_fixed_path": "0", + "y_type": "relative", + "y_fixed_coordinate": 0.0, + "x_fixed_coordinate": 0.0, + "y_fixed_path_invert_loop": true, + "description": "Stabilized the extruder to the back center for most triggers. For snap to print triggers, the extruder will be moved as closely as possible to the stabilization positions depending on the trigger options.", + "wait_for_moves_to_finish": true, + "x_fixed_path_loop": true, + "x_relative_print": 100.0, + "x_relative": 50.0, + "x_fixed_path_invert_loop": true, + "y_relative_path_invert_loop": true, + "name": "Back Center", + "y_fixed_path": "0", + "y_relative_print": 100.0, + "x_type": "relative" + } + }, + "current_trigger_profile_guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "triggers": { + "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1": { + "name": "Classic - Every 0.5mm", + "description": "This is the classic, real-time layer trigger that will take snapshots at most once every 0.5mm. This can be used to reduce snapshot time for very tall prints or with very low layer heights. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "e93e644a-cc3f-42bc-81ad-c1c87ef92ad1", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer 0.5mm", + "value": "layer_0.5mm" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.5, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d15086ca-f785-4368-ab37-ce2a3e45d3f4": { + "name": "Classic - Every Layer", + "description": "This is the classic, real-time layer trigger. Triggers as soon as possible after a layer change. Depending on the quality settings, the snapshot might not be taken exactly on the layer change, and it's possible that snapshots can be missed on some layers.", + "guid": "d15086ca-f785-4368-ab37-ce2a3e45d3f4", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Layer", + "value": "layer" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "11db06c4-b80d-4e68-87d4-41729864e181": { + "name": "Classic - Gcode", + "description": "The classic real-time gcode trigger. It will trigger any time the snapshot gocde (see your current Octolapse printer profile for the snapshot gcode, but it is 'snap' by defualt)", + "guid": "11db06c4-b80d-4e68-87d4-41729864e181", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Gcode", + "value": "gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": false, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "c44de27e-053e-4a89-900f-c89502fec7ee": { + "name": "Smart - Compatibility", + "description": "Takes a snapshot on every layer with an emphasis on compatibility. This trigger will return the closest high quality position if one is available. If no high quality point can be found, it will return the next best points, including extrusions. Because of this it will usually take a snapshot on every layer, regardless of slicer settings. This trigger will not work with vase mode prints, but will leave substantial artifacts and is not recommended. Use one of the 'Snap To Print' stabilizations for vase mode prints.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "c44de27e-053e-4a89-900f-c89502fec7ee", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Compatibility", + "value": "smart_compatibility" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "322d69ef-e7cb-454a-916a-15856682932c": { + "name": "Smart - Fast (low quality)", + "description": "Takes a snapshot on every layer with an emphasis on speed. This trigger always chooses the closest position, which is usually an extrusion, and can cause significant artifacts/quality issues. However, this trigger can be useful if your print contains a wipe tower that is the closest object on your print to the selected stabilization point, or if there is an ooze shield around the print.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "322d69ef-e7cb-454a-916a-15856682932c", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Fast (low quality)", + "value": "smart_fast" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b838fe36-1459-4867-8243-ab7604cf0e2d": { + "name": "Smart - Gcode", + "description": "Takes a snapshot each time the snapshot command is encountered. The snapshot command is @OCTOLAPSE TAKE-SNAPSHOT, but you can specify an alternative snapshot command within your printer profile if you desire.", + "guid": "b838fe36-1459-4867-8243-ab7604cf0e2d", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Gcode", + "value": "smart_gcode" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 1, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "gcode", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "f8e5e1a2-a54c-489b-961b-2530648e1625": { + "name": "Smart - High Quality", + "description": "Takes a snapshot on every layer with an emphasis on quality. It attempts to reduce the travel distance as much as possible, but only if substantial travel savings are detected. This trigger will not allow snapshots during an extrusion. This trigger will not work with vase mode prints.", + "guid": "f8e5e1a2-a54c-489b-961b-2530648e1625", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - High Quality", + "value": "smart_high_quality" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 3, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "0713c9c9-7562-4a04-81e0-9f2f8953e069": { + "name": "Smart - Snap To Print", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "0713c9c9-7562-4a04-81e0-9f2f8953e069", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print", + "value": "smart_snap_to_print" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "4e1302c8-c0a2-433c-9281-5247ba6de963": { + "name": "Smart - Snap To Print - High Quality", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. It will NOT work with vase mode! As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\n If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "4e1302c8-c0a2-433c-9281-5247ba6de963", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - High Quality", + "value": "smart_snap_to_print_high_quality" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": true, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "b852dc57-4d8e-457f-825f-2d13d0b1f825": { + "name": "Smart - Snap To Print - Smooth", + "description": "This smart layer trigger keeps your extruder over your printed part while taking a snapshot. As long as your camera is quick to return a snapshot, and there are no communication delays with your printer, this stabilization is very low impact. Since there are no extra travel movements, it is the fastest trigger available, and adds very little time to your print. \n\nConsider trying this trigger with the 'disabled' stabilization profile for an interesting effect, depending on the model you're printing.\n\nThis stabilization is suitable for vase mode prints, but may take snapshots more often than normal if you have not entered a 'Layer Height' into your slicer settings. If you are using 'automatic' slicer settings, this will happen for you.\n\nThe smart trigger pre-processes your gcode in order to choose better stabilization points, reducing travel time. This trigger is configured for compatibility with different slicers and printers.", + "guid": "b852dc57-4d8e-457f-825f-2d13d0b1f825", + "automatic_configuration": { + "key_values": [ + { + "name": "Smart - Snap To Print - Smooth", + "value": "smart_snap_to_print_smooth" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "smart", + "smart_layer_trigger_type": 0, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": true, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "layer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "3badd311-31f4-4cb9-b4d4-9d8640f4f761": { + "name": "Timer - Every 1:00", + "description": "This is the classic timer trigger. Will take a snapshot approximately every minute.\n\nNot recommended for vase mode prints!", + "guid": "3badd311-31f4-4cb9-b4d4-9d8640f4f761", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 1m 00s", + "value": "timer_60_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 60, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + }, + "d1fc80ed-3482-419b-a00d-374f682ee13c": { + "name": "Timer - Every 0:30", + "description": "This is the classic timer trigger. Will take a snapshot approximately every 30 seconds.\n\nNot recommended for vase mode prints!", + "guid": "d1fc80ed-3482-419b-a00d-374f682ee13c", + "automatic_configuration": { + "key_values": [ + { + "name": "Classic - Timer 0m 30s", + "value": "timer_30_sec" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": false + }, + "trigger_type": "real-time", + "smart_layer_trigger_type": 2, + "smart_layer_snap_to_print_high_quality": false, + "smart_layer_snap_to_print_smooth": false, + "smart_layer_disable_z_lift": true, + "allow_smart_snapshot_commands": true, + "is_default": false, + "trigger_subtype": "timer", + "timer_trigger_seconds": 30, + "layer_trigger_height": 0.0, + "position_restrictions_enabled": false, + "position_restrictions": [], + "require_zhop": false, + "extruder_state_requirements_enabled": true, + "trigger_on_extruding_start": "trigger_on", + "trigger_on_extruding": "trigger_on", + "trigger_on_primed": "trigger_on", + "trigger_on_retracting_start": "", + "trigger_on_retracting": "", + "trigger_on_partially_retracted": "forbidden", + "trigger_on_retracted": "trigger_on", + "trigger_on_deretracting_start": "", + "trigger_on_deretracting": "forbidden", + "trigger_on_deretracted": "forbidden" + } + }, + "current_rendering_profile_guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "renderings": { + "694b4038-c83d-4edf-a61a-eb767854d882": { + "name": "GIF - Fixed Length - 00:05", + "description": "Generates a GIF with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "694b4038-c83d-4edf-a61a-eb767854d882", + "automatic_configuration": { + "key_values": [ + { + "name": "GIF - Fixed Length - 00:05", + "value": "gif_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "gif", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[0,0]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "d4898ba7-8d27-4479-b7d8-34c063ae7a68": { + "name": "MP4 - 30 FPS", + "description": "Generates an MP4 at a constant 30FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "d4898ba7-8d27-4479-b7d8-34c063ae7a68", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 30 FPS", + "value": "mp4_fixed_fps_30" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "c1f97198-49b1-4b4e-bd32-e65e470d5bca": { + "name": "MP4 - Fixed Length - 00:05", + "description": "Generates an MP4 with a fixed length of 5 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "c1f97198-49b1-4b4e-bd32-e65e470d5bca", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:05", + "value": "mp4_fixed_length_05" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 5, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10,10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false, + "cleanup_after_render_fail": true + }, + "7db3e7d6-3fe6-4955-8419-7f63a10d0eee": { + "name": "MP4 - Fixed Length - 00:10", + "description": "Generates an MP4 with a fixed length of 10 seconds. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "7db3e7d6-3fe6-4955-8419-7f63a10d0eee", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - Fixed Length - 00:10", + "value": "mp4_fixed_length_10" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "duration", + "run_length_seconds": 10, + "fps": 30, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "b87d49db-72de-4285-87f6-50b8044d42b6": { + "name": "MP4 - 60 FPS", + "description": "Generates an MP4 at a constant 60FPS. Adds 2 seconds of pre-roll using the first frame and 2 seconds of post-roll using the last frame.", + "guid": "b87d49db-72de-4285-87f6-50b8044d42b6", + "automatic_configuration": { + "key_values": [ + { + "name": "MP4 - 60 FPS", + "value": "mp4_fixed_fps_60" + } + ], + "version": "1.1", + "suppress_update_notification_version": false, + "is_custom": false + }, + "enabled": true, + "fps_calculation_type": "static", + "run_length_seconds": 5, + "fps": 60, + "max_fps": 120.0, + "min_fps": 2.0, + "output_format": "mp4", + "sync_with_timelapse": true, + "bitrate": "6000k", + "post_roll_seconds": 2, + "pre_roll_seconds": 2, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "enable_watermark": false, + "selected_watermark": "", + "overlay_text_template": "", + "overlay_font_path": "", + "overlay_font_size": 10, + "overlay_text_pos": "[10, 10]", + "overlay_text_alignment": "left", + "overlay_text_valign": "top", + "overlay_text_halign": "left", + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "overlay_outline_color": [ + 0, + 0, + 0, + 1.0 + ], + "overlay_outline_width": 1, + "thread_count": 1, + "archive_snapshots": false + }, + "35aababf-0ecf-46d5-b142-6290d38c8fea": { + "automatic_configuration": { + "key_values": [ + { + "name": "Disabled (for manual rendering)", + "value": "disabled" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.1" + }, + "overlay_text_template": "", + "overlay_text_valign": "top", + "thread_count": 1, + "overlay_text_color": [ + 255, + 255, + 255, + 1 + ], + "min_fps": 2.0, + "guid": "35aababf-0ecf-46d5-b142-6290d38c8fea", + "overlay_font_path": "", + "sync_with_timelapse": true, + "constant_rate_factor": 28, + "overlay_text_pos": "[0,0]", + "overlay_outline_width": 1, + "fps": 30, + "output_template": "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}", + "selected_watermark": "", + "output_format": "mp4", + "description": "No timelapse will be generated, but all snapshots will be in the Octolapse data directory and can be downloaded and manually rendered.", + "overlay_text_alignment": "left", + "archive_snapshots": false, + "fps_calculation_type": "duration", + "post_roll_seconds": 0, + "bitrate": "6000k", + "name": "Disabled", + "overlay_text_halign": "left", + "enabled": false, + "enable_watermark": false, + "pre_roll_seconds": 0, + "overlay_font_size": 10, + "run_length_seconds": 5, + "max_fps": 120.0, + "overlay_outline_color": [ + 0, + 0, + 0, + 1 + ] + } + }, + "current_camera_profile_guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "cameras": { + "354def78-9eea-409a-ad23-ee966dfff4ba": { + "name": "Webcam - Default OctoPi 0.16.0", + "description": "This profile should work for the default settings within OctoPi V0.16.0 for most locally attached USB or Raspberry Pi cameras.", + "guid": "354def78-9eea-409a-ad23-ee966dfff4ba", + "automatic_configuration": { + "key_values": [ + { + "name": "Webcam - Default OctoPi 0.16.0", + "value": "webcam_octopi_0.16.0" + } + ], + "version": "1.0", + "suppress_update_notification_version": false, + "is_custom": true + }, + "enabled": true, + "camera_type": "webcam", + "gcode_camera_script": "", + "on_print_start_script": "", + "on_before_snapshot_script": "", + "external_camera_snapshot_script": "", + "on_after_snapshot_script": "", + "on_before_render_script": "", + "on_after_render_script": "", + "on_print_end_script": "", + "delay": 125, + "timeout_ms": 5000, + "snapshot_transpose": "", + "webcam_settings": { + "address": "http://tako3.prefecture/webcam/", + "snapshot_request_template": "{camera_address}?action=snapshot", + "stream_template": "http://tako3.prefecture:8081/?action=stream", + "ignore_ssl_error": true, + "server_type": "mjpg-streamer", + "username": "", + "password": "", + "type": null, + "use_custom_webcam_settings_page": true, + "mjpg_streamer": { + "name": "mjpg-streamer", + "server_type": "mjpg-streamer", + "controls": {} + }, + "stream_download": false + }, + "enable_custom_image_preferences": false, + "apply_settings_before_print": true, + "apply_settings_at_startup": true + } + }, + "current_logging_profile_guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "logging": { + "4d7411e9-f97e-41c0-921c-02e262660e83": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Parsing/Preprocessing/Position Tracking", + "value": "c++" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Parsing/Preprocessing/Position Tracking", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "4d7411e9-f97e-41c0-921c-02e262660e83", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + } + ], + "description": "This logging profile is useful for debugging problems parsing gcode, tracking the printer's position and state, or preprocessing gcode to create snapshot plans using the new smart layer trigger. This profile will create very large log files and will make preprocessing VERY VERY slow, so use this only when necessary to solve a specific issue." + }, + "afbe12d6-0dee-4911-b77e-ee9ebfae7521": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log All Errors - Recommended", + "value": "errors" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log All Errors", + "log_to_console": false, + "enabled": false, + "default_log_level": 10, + "guid": "afbe12d6-0dee-4911-b77e-ee9ebfae7521", + "enabled_loggers": [], + "description": "Logs only errors and exceptions. This is the recommended default profile, logs a minimal amount of data, and has a low impact on performance." + }, + "a417b5bf-9d26-4456-81ec-ba2f04f57084": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug Logging", + "value": "debug" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Debug", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "a417b5bf-9d26-4456-81ec-ba2f04f57084", + "enabled_loggers": [ + { + "log_level": 10, + "name": "octolapse.__init__" + }, + { + "log_level": 10, + "name": "octolapse.camera" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 10, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 10, + "name": "octolapse.gcode_position" + }, + { + "log_level": 10, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 10, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 10, + "name": "octolapse.position" + }, + { + "log_level": 10, + "name": "octolapse.render" + }, + { + "log_level": 10, + "name": "octolapse.settings" + }, + { + "log_level": 10, + "name": "octolapse.settings_migration" + }, + { + "log_level": 10, + "name": "octolapse.snapshot" + }, + { + "log_level": 10, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 10, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 10, + "name": "octolapse.timelapse" + }, + { + "log_level": 10, + "name": "octolapse.trigger" + }, + { + "log_level": 10, + "name": "octolapse.utility" + }, + { + "log_level": 10, + "name": "octolapse.settings_external" + }, + { + "log_level": 10, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 10, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 10, + "name": "octolapse.script" + }, + { + "log_level": 10, + "name": "octolapse.migration" + } + ], + "description": "Logs all but verbose messages. This will create a very large log file that can affect performance, so only use this when necessary. This is useful for debugging issues, or for submitting error reports in the github repository." + }, + "fa759ab9-bc02-4fd0-8d12-a7c5c89591c1": { + "automatic_configuration": { + "key_values": [ + { + "name": "Debug - Script Cameras (DSLR)", + "value": "debug_script_cameras" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": 1.0 + }, + "name": "Debug - Script Cameras (DSLR)", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "fa759ab9-bc02-4fd0-8d12-a7c5c89591c1", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.render" + } + ], + "description": "This profile is useful for debugging script based cameras, like DSLRs." + }, + "02717525-3684-4bf7-ac53-037cdfbd4e66": { + "automatic_configuration": { + "key_values": [ + { + "name": "Log Everything", + "value": "verbose" + } + ], + "suppress_update_notification_version": false, + "is_custom": false, + "version": "1.0" + }, + "name": "Log Everything", + "log_to_console": false, + "enabled": true, + "default_log_level": 10, + "guid": "02717525-3684-4bf7-ac53-037cdfbd4e66", + "enabled_loggers": [ + { + "log_level": 5, + "name": "octolapse.stabilization_gcode" + }, + { + "log_level": 5, + "name": "octolapse.snapshot_plan" + }, + { + "log_level": 5, + "name": "octolapse.gcode_commands" + }, + { + "log_level": 5, + "name": "octolapse.gcode_position" + }, + { + "log_level": 5, + "name": "octolapse.settings_preprocessor" + }, + { + "log_level": 5, + "name": "octolapse.__init__" + }, + { + "log_level": 5, + "name": "octolapse.camera" + }, + { + "log_level": 5, + "name": "octolapse.messenger_worker" + }, + { + "log_level": 5, + "name": "octolapse.position" + }, + { + "log_level": 5, + "name": "octolapse.render" + }, + { + "log_level": 5, + "name": "octolapse.settings" + }, + { + "log_level": 5, + "name": "octolapse.settings_external" + }, + { + "log_level": 5, + "name": "octolapse.settings_migration" + }, + { + "log_level": 5, + "name": "octolapse.snapshot" + }, + { + "log_level": 5, + "name": "octolapse.stabilization_preprocessing" + }, + { + "log_level": 5, + "name": "octolapse.timelapse" + }, + { + "log_level": 5, + "name": "octolapse.trigger" + }, + { + "log_level": 5, + "name": "octolapse.utility" + }, + { + "log_level": 5, + "name": "octolapse.gcode_processor" + }, + { + "log_level": 5, + "name": "octolapse.gcode_parser" + }, + { + "log_level": 5, + "name": "octolapse.script" + }, + { + "log_level": 5, + "name": "octolapse.migration" + } + ], + "description": "This will log absolutely everything that Octolapse is capable of logging. It will severely impact performance, and is only recommended for very difficult to debug or uncommon errors. I do not recommend you use this profile unless you are asked for a verbose log after creating an issue in the Octolapse Github repository." + } + } + }, + "global_options": null, + "upgrade_info": { + "previous_version": null, + "was_upgraded": false + } +} diff --git a/octoprint_octolapse/data/webcam_types/camera_types.json b/octoprint_octolapse/data/webcam_types/camera_types.json index 76f35379..aeb58c09 100644 --- a/octoprint_octolapse/data/webcam_types/camera_types.json +++ b/octoprint_octolapse/data/webcam_types/camera_types.json @@ -5,7 +5,11 @@ "directory": "mjpg_streamer", "file_names": [ "logitech_c920.json", - "raspi_cam_v2.json" + "logitech_c920_2.json", + "logitech_c920_3.json", + "logitech_c250.json", + "raspi_cam_v2.json", + "raspi_cam_v2.1.json" ] } } diff --git a/octoprint_octolapse/data/webcam_types/mjpg_streamer/logitech_c250.json b/octoprint_octolapse/data/webcam_types/mjpg_streamer/logitech_c250.json new file mode 100644 index 00000000..e7dc9b9e --- /dev/null +++ b/octoprint_octolapse/data/webcam_types/mjpg_streamer/logitech_c250.json @@ -0,0 +1,323 @@ +{ + "key": "logitech_c250", + "name": "Logitech C250", + "make": "Logitech", + "model": "C250", + "template": "octolapse-mjpg-streamer-logitech-c250", + "controls": [ + { + "name": "Brightness", + "id": "9963776", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "128", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Contrast", + "id": "9963777", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "32", + "value": "72", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Saturation", + "id": "9963778", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "31", + "value": "30", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "White Balance Temperature, Auto", + "id": "9963788", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "1", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Gain", + "id": "9963795", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Power Line Frequency", + "id": "9963800", + "type": "3", + "min": "0", + "max": "2", + "step": "1", + "default": "2", + "value": "2", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Disabled", + "1": "50 Hz", + "2": "60 Hz" + } + }, + { + "name": "White Balance Temperature", + "id": "9963802", + "type": "1", + "min": "0", + "max": "10000", + "step": "10", + "default": "4000", + "value": "4000", + "dest": "0", + "flags": "16", + "group": "1" + }, + { + "name": "Sharpness", + "id": "9963803", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "130", + "value": "130", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Backlight Compensation", + "id": "9963804", + "type": "1", + "min": "0", + "max": "1", + "step": "1", + "default": "1", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Exposure, Auto", + "id": "10094849", + "type": "3", + "min": "0", + "max": "3", + "step": "1", + "default": "3", + "value": "3", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "", + "1": "Manual Mode", + "2": "", + "3": "Aperture Priority Mode" + } + }, + { + "name": "Exposure (Absolute)", + "id": "10094850", + "type": "1", + "min": "1", + "max": "10000", + "step": "1", + "default": "166", + "value": "166", + "dest": "0", + "flags": "16", + "group": "1" + }, + { + "name": "Exposure, Auto Priority", + "id": "10094851", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "JPEG quality", + "id": "1", + "type": "1", + "min": "0", + "max": "100", + "step": "1", + "default": "50", + "value": "0", + "dest": "0", + "flags": "0", + "group": "3" + } + ], + "formats": [ + { + "id": "0", + "name": "YUYV 4:2:2", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x120", + "2": "176x144", + "3": "320x240", + "4": "352x288", + "5": "640x360", + "6": "640x400", + "7": "800x600", + "8": "960x720", + "9": "1280x720", + "10": "1280x800", + "11": "1280x1024" + }, + "currentResolution": "255" + }, + { + "id": "1", + "name": "Motion-JPEG", + "compressed": "true", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x120", + "2": "176x144", + "3": "320x240", + "4": "352x288", + "5": "640x360", + "6": "640x400", + "7": "800x600", + "8": "960x720", + "9": "1280x720", + "10": "1280x800", + "11": "1280x1024" + }, + "currentResolution": "11" + }, + { + "id": "2", + "name": "RGB3", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x120", + "2": "176x144", + "3": "320x240", + "4": "352x288", + "5": "640x360", + "6": "640x400", + "7": "800x600", + "8": "960x720", + "9": "1280x720", + "10": "1280x800", + "11": "1280x1024" + }, + "currentResolution": "255" + }, + { + "id": "3", + "name": "BGR3", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x120", + "2": "176x144", + "3": "320x240", + "4": "352x288", + "5": "640x360", + "6": "640x400", + "7": "800x600", + "8": "960x720", + "9": "1280x720", + "10": "1280x800", + "11": "1280x1024" + }, + "currentResolution": "255" + }, + { + "id": "4", + "name": "YU12", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x120", + "2": "176x144", + "3": "320x240", + "4": "352x288", + "5": "640x360", + "6": "640x400", + "7": "800x600", + "8": "960x720", + "9": "1280x720", + "10": "1280x800", + "11": "1280x1024" + }, + "currentResolution": "255" + }, + { + "id": "5", + "name": "YV12", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x120", + "2": "176x144", + "3": "320x240", + "4": "352x288", + "5": "640x360", + "6": "640x400", + "7": "800x600", + "8": "960x720", + "9": "1280x720", + "10": "1280x800", + "11": "1280x1024" + }, + "currentResolution": "255" + } + ] +} diff --git a/octoprint_octolapse/data/webcam_types/mjpg_streamer/logitech_c920_2.json b/octoprint_octolapse/data/webcam_types/mjpg_streamer/logitech_c920_2.json new file mode 100644 index 00000000..3e2ea992 --- /dev/null +++ b/octoprint_octolapse/data/webcam_types/mjpg_streamer/logitech_c920_2.json @@ -0,0 +1,455 @@ +{ + "key": "logitech_c920_2", + "name": "Logitech C920 HD Pro", + "make": "Logitech", + "model": "C920", + "template": "octolapse-mjpg-streamer-logitech-c920", + "controls": [ + { + "name": "Brightness", + "id": "9963776", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "128", + "value": "100", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Contrast", + "id": "9963777", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "128", + "value": "128", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Saturation", + "id": "9963778", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "128", + "value": "128", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "White Balance Temperature, Auto", + "id": "9963788", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "1", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Gain", + "id": "9963795", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Power Line Frequency", + "id": "9963800", + "type": "3", + "min": "0", + "max": "2", + "step": "1", + "default": "2", + "value": "2", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Disabled", + "1": "50 Hz", + "2": "60 Hz" + } + }, + { + "name": "White Balance Temperature", + "id": "9963802", + "type": "1", + "min": "2000", + "max": "6500", + "step": "1", + "default": "4000", + "value": "3388", + "dest": "0", + "flags": "16", + "group": "1" + }, + { + "name": "Sharpness", + "id": "9963803", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "128", + "value": "128", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Backlight Compensation", + "id": "9963804", + "type": "1", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Exposure, Auto", + "id": "10094849", + "type": "3", + "min": "0", + "max": "3", + "step": "1", + "default": "3", + "value": "3", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Brightness", + "1": "Manual Mode", + "2": "rast", + "3": "Aperture Priority Mode" + } + }, + { + "name": "Exposure (Absolute)", + "id": "10094850", + "type": "1", + "min": "3", + "max": "2047", + "step": "1", + "default": "250", + "value": "416", + "dest": "0", + "flags": "16", + "group": "1" + }, + { + "name": "Exposure, Auto Priority", + "id": "10094851", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Pan (Absolute)", + "id": "10094856", + "type": "1", + "min": "-36000", + "max": "36000", + "step": "3600", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Tilt (Absolute)", + "id": "10094857", + "type": "1", + "min": "-36000", + "max": "36000", + "step": "3600", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Focus (absolute)", + "id": "10094858", + "type": "1", + "min": "0", + "max": "250", + "step": "5", + "default": "0", + "value": "0", + "dest": "0", + "flags": "16", + "group": "1" + }, + { + "name": "Focus, Auto", + "id": "10094860", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "1", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Zoom, Absolute", + "id": "10094861", + "type": "1", + "min": "100", + "max": "500", + "step": "1", + "default": "100", + "value": "100", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "JPEG quality", + "id": "1", + "type": "1", + "min": "0", + "max": "100", + "step": "1", + "default": "50", + "value": "0", + "dest": "0", + "flags": "0", + "group": "3" + } + ], + "formats": [ + { + "id": "0", + "name": "YUYV 4:2:2", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + }, + { + "id": "1", + "name": "H.264", + "compressed": "true", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080" + }, + "currentResolution": "255" + }, + { + "id": "2", + "name": "Motion-JPEG", + "compressed": "true", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080" + }, + "currentResolution": "16" + }, + { + "id": "3", + "name": "RGB3", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + }, + { + "id": "4", + "name": "BGR3", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + }, + { + "id": "5", + "name": "YU12", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + }, + { + "id": "6", + "name": "YV12", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + } + ] +} diff --git a/octoprint_octolapse/data/webcam_types/mjpg_streamer/logitech_c920_3.json b/octoprint_octolapse/data/webcam_types/mjpg_streamer/logitech_c920_3.json new file mode 100644 index 00000000..b58f1d72 --- /dev/null +++ b/octoprint_octolapse/data/webcam_types/mjpg_streamer/logitech_c920_3.json @@ -0,0 +1,456 @@ +{ + "key": "logitech_c920_3", + "name": "Logitech C920 HD Pro", + "make": "Logitech", + "model": "C920", + "template": "octolapse-mjpg-streamer-logitech-c920", + "controls": [ + { + "name": "Brightness", + "id": "9963776", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "128", + "value": "128", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Contrast", + "id": "9963777", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "128", + "value": "128", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Saturation", + "id": "9963778", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "128", + "value": "128", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "White Balance Temperature, Auto", + "id": "9963788", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "1", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Gain", + "id": "9963795", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Power Line Frequency", + "id": "9963800", + "type": "3", + "min": "0", + "max": "2", + "step": "1", + "default": "2", + "value": "2", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Disabled", + "1": "50 Hz", + "2": "60 Hz" + } + }, + { + "name": "White Balance Temperature", + "id": "9963802", + "type": "1", + "min": "2000", + "max": "6500", + "step": "1", + "default": "4000", + "value": "4000", + "dest": "0", + "flags": "16", + "group": "1" + }, + { + "name": "Sharpness", + "id": "9963803", + "type": "1", + "min": "0", + "max": "255", + "step": "1", + "default": "128", + "value": "128", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Backlight Compensation", + "id": "9963804", + "type": "1", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Exposure, Auto", + "id": "10094849", + "type": "3", + "min": "0", + "max": "3", + "step": "1", + "default": "3", + "value": "3", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "", + "1": "Manual Mode", + "2": "", + "3": "Aperture Priority Mode" + } + }, + { + "name": "Exposure (Absolute)", + "id": "10094850", + "type": "1", + "min": "3", + "max": "2047", + "step": "1", + "default": "250", + "value": "250", + "dest": "0", + "flags": "16", + "group": "1" + }, + { + "name": "Exposure, Auto Priority", + "id": "10094851", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Pan (Absolute)", + "id": "10094856", + "type": "1", + "min": "-36000", + "max": "36000", + "step": "3600", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Tilt (Absolute)", + "id": "10094857", + "type": "1", + "min": "-36000", + "max": "36000", + "step": "3600", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Focus (absolute)", + "id": "10094858", + "type": "1", + "min": "0", + "max": "250", + "step": "5", + "default": "0", + "value": "0", + "dest": "0", + "flags": "16", + "group": "1" + }, + { + "name": "Focus, Auto", + "id": "10094860", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "1", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Zoom, Absolute", + "id": "10094861", + "type": "1", + "min": "100", + "max": "500", + "step": "1", + "default": "100", + "value": "100", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "JPEG quality", + "id": "1", + "type": "1", + "min": "0", + "max": "100", + "step": "1", + "default": "50", + "value": "0", + "dest": "0", + "flags": "0", + "group": "3" + } + ], + "formats": [ + { + "id": "0", + "name": "YUYV 4:2:2", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + }, + { + "id": "1", + "name": "H.264", + "compressed": "true", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080" + }, + "currentResolution": "255" + }, + { + "id": "2", + "name": "Motion-JPEG", + "compressed": "true", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080" + }, + "currentResolution": "16" + }, + { + "id": "3", + "name": "RGB3", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + }, + { + "id": "4", + "name": "BGR3", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + }, + { + "id": "5", + "name": "YU12", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + }, + { + "id": "6", + "name": "YV12", + "compressed": "false", + "emulated": "true", + "current": "true", + "resolutions": { + "0": "640x480", + "1": "160x90", + "2": "160x120", + "3": "176x144", + "4": "320x180", + "5": "320x240", + "6": "352x288", + "7": "432x240", + "8": "640x360", + "9": "800x448", + "10": "800x600", + "11": "864x480", + "12": "960x720", + "13": "1024x576", + "14": "1280x720", + "15": "1600x896", + "16": "1920x1080", + "17": "2304x1296", + "18": "2304x1536" + }, + "currentResolution": "255" + } + + ] +} diff --git a/octoprint_octolapse/data/webcam_types/mjpg_streamer/raspi_cam_v2.1.json b/octoprint_octolapse/data/webcam_types/mjpg_streamer/raspi_cam_v2.1.json new file mode 100644 index 00000000..54524c04 --- /dev/null +++ b/octoprint_octolapse/data/webcam_types/mjpg_streamer/raspi_cam_v2.1.json @@ -0,0 +1,700 @@ +{ + "key": "raspi_cam_v2", + "name": "Raspberry Pi Camera Module V2.1", + "make": "Raspberry Pi", + "model": "V2.1", + "template": "octolapse-mjpg-streamer-raspi-cam-v2", + "controls": [ + { + "name": "User Controls", + "id": "9961473", + "type": "6", + "min": "0", + "max": "0", + "step": "0", + "default": "0", + "value": "30", + "dest": "0", + "flags": "68", + "group": "1" + }, + { + "name": "Brightness", + "id": "9963776", + "type": "1", + "min": "0", + "max": "100", + "step": "1", + "default": "50", + "value": "61", + "dest": "0", + "flags": "32", + "group": "1" + }, + { + "name": "Contrast", + "id": "9963777", + "type": "1", + "min": "-100", + "max": "100", + "step": "1", + "default": "0", + "value": "12", + "dest": "0", + "flags": "32", + "group": "1" + }, + { + "name": "Saturation", + "id": "9963778", + "type": "1", + "min": "-100", + "max": "100", + "step": "1", + "default": "0", + "value": "-19", + "dest": "0", + "flags": "32", + "group": "1" + }, + { + "name": "Red Balance", + "id": "9963790", + "type": "1", + "min": "1", + "max": "7999", + "step": "1", + "default": "1000", + "value": "1106", + "dest": "0", + "flags": "32", + "group": "1" + }, + { + "name": "Blue Balance", + "id": "9963791", + "type": "1", + "min": "1", + "max": "7999", + "step": "1", + "default": "1000", + "value": "1484", + "dest": "0", + "flags": "32", + "group": "1" + }, + { + "name": "Horizontal Flip", + "id": "9963796", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Vertical Flip", + "id": "9963797", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Power Line Frequency", + "id": "9963800", + "type": "3", + "min": "0", + "max": "3", + "step": "1", + "default": "1", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Disabled", + "1": "50 Hz", + "2": "60 Hz", + "3": "Auto" + } + }, + { + "name": "Sharpness", + "id": "9963803", + "type": "1", + "min": "-100", + "max": "100", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "32", + "group": "1" + }, + { + "name": "Color Effects", + "id": "9963807", + "type": "3", + "min": "0", + "max": "15", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "None", + "1": "Black & White", + "2": "Sepia", + "3": "Negative", + "4": "Emboss", + "5": "Sketch", + "6": "Sky Blue", + "7": "Grass Green", + "8": "Skin Whiten", + "9": "Vivid", + "10": "Aqua", + "11": "Art Freeze", + "12": "Silhouette", + "13": "Solarization", + "14": "Antique", + "15": "Set Cb/Cr" + } + }, + { + "name": "Rotate", + "id": "9963810", + "type": "1", + "min": "0", + "max": "360", + "step": "90", + "default": "0", + "value": "0", + "dest": "0", + "flags": "1024", + "group": "1" + }, + { + "name": "Color Effects, CbCr", + "id": "9963818", + "type": "1", + "min": "0", + "max": "65535", + "step": "1", + "default": "32896", + "value": "32896", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Codec Controls", + "id": "10027009", + "type": "6", + "min": "0", + "max": "0", + "step": "0", + "default": "0", + "value": "0", + "dest": "0", + "flags": "68", + "group": "1" + }, + { + "name": "Video Bitrate Mode", + "id": "10029518", + "type": "3", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "8", + "group": "1", + "menu": { + "0": "Variable Bitrate", + "1": "Constant Bitrate" + } + }, + { + "name": "Video Bitrate", + "id": "10029519", + "type": "1", + "min": "25000", + "max": "25000000", + "step": "25000", + "default": "10000000", + "value": "10000000", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Repeat Sequence Header", + "id": "10029538", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "H264 I-Frame Period", + "id": "10029670", + "type": "1", + "min": "0", + "max": "2147483647", + "step": "1", + "default": "60", + "value": "60", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "H264 Level", + "id": "10029671", + "type": "3", + "min": "0", + "max": "11", + "step": "1", + "default": "11", + "value": "11", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "1", + "1": "1b", + "2": "1.1", + "3": "1.2", + "4": "1.3", + "5": "2", + "6": "2.1", + "7": "2.2", + "8": "3", + "9": "3.1", + "10": "3.2", + "11": "4" + } + }, + { + "name": "H264 Profile", + "id": "10029675", + "type": "3", + "min": "0", + "max": "4", + "step": "1", + "default": "4", + "value": "4", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Baseline", + "1": "Constrained Baseline", + "2": "Main", + "3": " ", + "4": "High" + } + }, + { + "name": "Camera Controls", + "id": "10092545", + "type": "6", + "min": "0", + "max": "0", + "step": "0", + "default": "0", + "value": "0", + "dest": "0", + "flags": "68", + "group": "1" + }, + { + "name": "Auto Exposure", + "id": "10094849", + "type": "3", + "min": "0", + "max": "3", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Auto Mode", + "1": "Manual Mode", + "2": "", + "3": "" + } + }, + { + "name": "Exposure Time, Absolute", + "id": "10094850", + "type": "1", + "min": "1", + "max": "10000", + "step": "1", + "default": "1000", + "value": "1000", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Exposure, Dynamic Framerate", + "id": "10094851", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "Auto Exposure, Bias", + "id": "10094867", + "type": "9", + "min": "0", + "max": "24", + "step": "1", + "default": "12", + "value": "12", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "White Balance, Auto & Preset", + "id": "10094868", + "type": "3", + "min": "0", + "max": "10", + "step": "1", + "default": "1", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Manual", + "1": "Auto", + "2": "Incandescent", + "3": "Fluorescent", + "4": "Fluorescent H", + "5": "Horizon", + "6": "Daylight", + "7": "Flash", + "8": "Cloudy", + "9": "Shade", + "10": "Greyworld" + } + }, + { + "name": "Image Stabilization", + "id": "10094870", + "type": "2", + "min": "0", + "max": "1", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "ISO Sensitivity", + "id": "10094871", + "type": "9", + "min": "0", + "max": "4", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "ISO Sensitivity, Auto", + "id": "10094872", + "type": "3", + "min": "0", + "max": "1", + "step": "1", + "default": "1", + "value": "1", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Manual", + "1": "Auto" + } + }, + { + "name": "Exposure, Metering Mode", + "id": "10094873", + "type": "3", + "min": "0", + "max": "2", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "Average", + "1": "Center Weighted", + "2": "Spot" + } + }, + { + "name": "Scene Mode", + "id": "10094874", + "type": "3", + "min": "0", + "max": "13", + "step": "1", + "default": "0", + "value": "0", + "dest": "0", + "flags": "0", + "group": "1", + "menu": { + "0": "None", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "6": "", + "7": "", + "8": "Night", + "9": "", + "10": "", + "11": "Sports", + "12": "", + "13": "" + } + }, + { + "name": "JPEG Compression Controls", + "id": "10289153", + "type": "6", + "min": "0", + "max": "0", + "step": "0", + "default": "0", + "value": "0", + "dest": "0", + "flags": "68", + "group": "1" + }, + { + "name": "Compression Quality", + "id": "10291459", + "type": "1", + "min": "1", + "max": "100", + "step": "1", + "default": "30", + "value": "30", + "dest": "0", + "flags": "0", + "group": "1" + }, + { + "name": "JPEG quality", + "id": "1", + "type": "1", + "min": "0", + "max": "100", + "step": "1", + "default": "50", + "value": "0", + "dest": "0", + "flags": "0", + "group": "3" + } + ], + "formats": [ + { + "id": "0", + "name": "Planar YUV 4:2:0", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "1", + "name": "YUYV 4:2:2", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "2", + "name": "24-bit RGB 8-8-8", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "3", + "name": "JFIF JPEG", + "compressed": "true", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "4", + "name": "H.264", + "compressed": "true", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "5", + "name": "Motion-JPEG", + "compressed": "true", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "0" + }, + { + "id": "6", + "name": "YVYU 4:2:2", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "7", + "name": "VYUY 4:2:2", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "8", + "name": "UYVY 4:2:2", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "9", + "name": "Y/CbCr 4:2:0", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "10", + "name": "24-bit BGR 8-8-8", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "11", + "name": "Planar YVU 4:2:0", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "12", + "name": "Y/CrCb 4:2:0", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + }, + { + "id": "13", + "name": "32-bit BGRA/X 8-8-8-8", + "compressed": "false", + "emulated": "false", + "current": "true", + "resolutions": { + "0": "32x3280" + }, + "currentResolution": "255" + } + ] +} diff --git a/octoprint_octolapse/error_messages.py b/octoprint_octolapse/error_messages.py new file mode 100644 index 00000000..307942d4 --- /dev/null +++ b/octoprint_octolapse/error_messages.py @@ -0,0 +1,433 @@ +# coding=utf-8 +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## + +_octolapse_errors = { + 'preprocessor': { + 'cpp_quality_issues': { + "1": { + 'name': "Using Fast Trigger", + 'help_link': "quality_issues_fast_trigger.md", + 'cpp_name': "stabilization_quality_issue_fast_trigger", + 'description': "You are using the 'Fast' smart trigger. This could lead to quality issues. If you are " + "having print quality issues, consider using a 'high quality' or 'snap to print' smart " + "trigger. " + }, + "2": { + 'name': "Low Quality Snap-to-print", + 'help_link': "quality_issues_low_quality_snap_to_print.md", + 'cpp_name': "stabilization_quality_issue_snap_to_print_low_quality", + 'description': "In most cases using the 'High Quality' snap to print option will improve print quality, " + "unless you are printing with vase mode enabled. " + }, + "3": { + 'name': "No Print Features Detected", + 'help_link': "quality_issues_no_print_features_detected.md", + 'cpp_name': "stabilization_quality_issue_no_print_features", + 'description': "No print features were found in your gcode file. This can reduce print quality " + "significantly. If you are using Slic3r or PrusaSlicer, please enable 'Verbose G-code' in " + "'Print Settings'->'Output Options'->'Output File'. " + } + }, + 'cpp_processing_errors': { + "1": { + 'name': "XYZ Axis Mode Unknown", + 'help_link': "error_help_preprocessor_axis_mode_xyz_unknown.md", + 'cpp_name': "stabilization_processing_issue_type_xyz_axis_mode_unknown", + 'is_fatal': True, + 'description': "The XYZ axis mode was not set." + }, + "2": { + 'name': "E axis Mode Unknown", + 'help_link': "error_help_preprocessor_axis_mode_e_unknown.md", + 'cpp_name': "stabilization_processing_issue_type_e_axis_mode_unknown", + 'is_fatal': True, + 'description': "The E axis mode was not set" + }, + "3": { + 'name': "No Definite Position", + 'help_link': "error_help_preprocessor_no_definite_position.md", + 'cpp_name': "stabilization_processing_issue_type_no_definite_position", + 'is_fatal': False, + 'description': "Unable to find a definite position." + }, + "4": { + 'name': "Printer Not Primed", + 'help_link': "error_help_preprocessor_printer_not_primed.md", + 'cpp_name': "stabilization_processing_issue_type_printer_not_primed", + 'is_fatal': True, + 'description': "Priming was not detected." + }, + "5": { + 'name': "No Metric Units", + 'help_link': "error_help_preprocessor_no_metric_units.md", + 'cpp_name': "stabilization_processing_issue_type_no_metric_units", + 'is_fatal': True, + 'description': "Gcode unites are not in millimeters." + }, + "6": { + 'name': "No Snapshot Commands Found", + 'help_link': "error_help_preprocessor_no_snapshot_commands_found.md", + 'cpp_name': "stabilization_processing_issue_type_no_snapshot_commands_found", + 'is_fatal': True, + 'description': "No snapshot commands were found. Current Snapshot Commands: @OCTOLAPSE " + "TAKE-SNAPSHOT{snapshot_command_gcode} " + } + }, + 'preprocessor_errors': { + 'incorrect_trigger_type': { + 'name': "Incorrect Trigger Type", + 'help_link': "error_help_preprocessor_incorrect_trigger_type.md", + 'description': "The current trigger is not a pre-calculated trigger." + }, + 'unhandled_exception': { + 'name': "Gcode Preprocessor Error", + 'help_link': "error_help_preprocessor_unhandled_exception.md", + 'description': "An unhandled exception occurred while preprocessing your gcode file." + }, + 'unknown_trigger_type': { + 'name': "Unknown Trigger Type", + 'help_link': "error_help_preprocessor_unknown_trigger_type.md", + 'description': "The current preprocessor type {preprocessor_type} is unknown." + }, + 'no_snapshot_plans_returned': { + 'name': "No Snapshot Plans Returned", + 'help_link': "error_help_preprocessor_no_snapshot_plans_returned.md", + 'description': "No snapshots were found in your gcode file." + } + } + }, + 'timelapse': { + 'cannot_aquire_job_lock': { + "name": "Unable To Acquire Job Lock", + "description": "Unable to start timelapse, failed to acquire a job lock. Print start failed.", + 'help_link': "error_help_timelapse_cannot_aquire_job_lock.md" + } + }, + 'init': { + 'm114_not_supported':{ + "name": "Printer Not Supported", + "description": "Your printer does not support the M114 command, and is incompatible with Octolapse", + 'help_link': "error_help_init_m114_not_supported.md" + }, + 'unexpected_exception': + { + "name": "Unexpected Exception", + "description": "An unexpected exception was raised while starting Octolapse. Check plugin_octolapse.log for more details.", + 'help_link': "error_help_init_unexpected_exception.md" + }, + 'cant_print_from_sd': { + "name": "Can't start from SD", + "description": "Octolapse cannot be used when printing from the SD card. Your print will continue. " + "Disable the plugin from within the Octolapse tab to prevent this message from displaying.", + 'help_link': "error_help_init_cant_print_from_sd.md", + 'options': { + 'is_warning': True, + 'cancel_print': False + } + }, + 'octolapse_is_disabled': { + "name": "Octolapse is Disabled", + "description": "Octolapse is disabled. Cannot start timelapse.", + 'help_link': "error_help_init_octolapse_is_disabled.md" + }, + 'printer_not_configured': { + "name": "Printer Not Configured", + "description": "Your Octolapse printer profile has not been configured. To fix this error go to the " + "Octolapse tab, edit your selected printer via the 'gear' icon, and save your changes.", + 'help_link': "error_help_init_printer_not_configured.md" + }, + 'no_current_job_data_found': { + "name": "No Print Job Data Found", + "description": "Octolapse was unable to acquire job start information from Octoprint." + " Please see plugin_octolapse.log for details.", + 'help_link': "error_help_init_no_current_job_data_found.md" + }, + 'no_current_job_file_data_found': { + "name": "No Print Job File Data Found", + "description": "Octolapse was unable to acquire file information from the current job." + " Please see plugin_octolapse.log for details.", + 'help_link': "error_help_init_no_current_job_file_data_found.md" + }, + 'unknown_file_origin': { + "name": "Unknown File Origin", + "description": "Octolapse cannot tell if you are printing from an SD card or streaming via Octoprint." + " Please see plugin_octolapse.log for details.", + 'help_link': "error_help_init_unknown_file_origin.md" + }, + 'incorrect_printer_state': { + "name": "Incorrect Printer State", + "description": "Unable to start the timelapse when not in the Initializing state." + " Please see plugin_octolapse.log for details.", + 'help_link': "error_help_init_incorrect_print_start_state.md" + }, + 'camera_init_test_failed': { + "name": "Camera Test Failed", + "description": "At least one camera failed testing. Details: {error}", + 'help_link': "error_help_init_camera_init_test_failed.md" + }, + 'no_enabled_cameras': { + "name": "No Cameras Are Enabled", + "description": "At least one camera must be enabled to use Octolapse.", + 'help_link': "error_help_init_no_enabled_cameras.md" + }, + 'camera_settings_apply_failed': { + "name": "Unable To Apply Camera Settings", + "description": "Octolapse could not apply custom image preferences or could not run a camera startup " + "script. Details: {error}", + 'help_link': "error_help_init_camera_settings_apply_failed.md" + }, + 'before_print_start_camera_script_apply_failed': { + "name": "Before Print Start Camera Script Failed", + "description": "There were errors running on print start camera scripts. Details: {error}", + 'help_link': "error_help_init_before_print_start_camera_script_apply_failed.md" + }, + 'incorrect_octoprint_version': { + "name": "Please Upgrade Octoprint", + "description": "Octolapse requires Octoprint v1.3.9 rc3 or above, but version v{installed_version} is " + "installed. Please update Octoprint to use Octolapse.", + 'help_link': "error_help_init_incorrect_octoprint_version.md" + }, + 'rendering_file_template_invalid': { + "name": "Invalid Rendering Template", + "description": "The rendering file template is invalid. Please correct the template" + " within the current rendering profile.", + 'help_link': "error_help_init_rendering_file_template_invalid.md" + }, + 'no_printer_profile_exists': { + "name": "No Printer Profiles Exist", + "description": "There are no printer profiles. Cannot start timelapse. " + "Please create a printer profile in the octolapse settings pages and " + "restart the print.", + 'help_link': "error_help_init_no_printer_profile_exists.md" + }, + 'no_printer_profile_selected': { + "name": "No Printer Profile Selected", + "description": "No default printer profile was selected. Cannot start timelapse. " + "Please select a printer profile in the octolapse settings pages and " + "restart the print.", + 'help_link': "error_help_init_no_printer_profile_selected.md" + }, + 'no_gcode_filepath_found': { + "name": "No Gcode Filepath Found", + "description": "No gcode filpath was found for the current print.", + 'help_link': "error_help_init_no_gcode_filepath_found.md" + }, + 'ffmpeg_path_not_set': { + "name": "Ffmpeg Path Not Set", + "description": "No ffmpeg path was set in the Octoprint settings.", + 'help_link': "error_help_init_ffmpeg_path_not_set.md" + }, + 'ffmpeg_path_retrieve_exception': { + "name": "Ffmpeg Path Not Set", + "description": "An exception was thrown while retrieving the ffmpeg file path from Octoprint Settings.", + 'help_link': "error_help_init_ffmpeg_path_retrieve_exception.md" + }, + 'ffmpeg_not_found_at_path': { + "name": "Ffmpeg Not Found at Path", + "description": "Ffmpeg was not found at the specified path.", + 'help_link': "error_help_init_ffmpeg_not_found_at_path.md" + }, + 'automatic_slicer_no_settings_found': { + "name": "Slicer Settings Not Found", + "description": "No slicer settings were not found in your gcode file.", + 'help_link': "error_help_init_automatic_slicer_no_settings_found.md" + }, + 'automatic_slicer_settings_missing': { + "name": "Slicer Settings Missing", + "description": "Some slicer settings were missing from your gcode file. " + "Missing Settings: {missing_settings}", + 'help_link': "error_help_init_automatic_slicer_settings_missing.md" + }, + 'automatic_slicer_no_settings_found_continue_printing': { + "name": "Slicer Settings Not Found", + "description": "No slicer settings were not found in your gcode file. Continue on failure is enabled so " + "your print will continue, but the timelapse has been aborted.", + 'help_link': "error_help_init_automatic_slicer_no_settings_found.md" + }, + 'automatic_slicer_settings_missing_continue_printing': { + "name": "Slicer Settings Missing", + "description": "Some slicer settings were missing from your gcode file. Continue on failure is enabled " + "so your print will continue, but the timelapse has been aborted. Missing Settings: {" + "missing_settings}", + 'help_link': "error_help_init_automatic_slicer_settings_missing.md" + }, + 'manual_slicer_settings_missing': { + "name": "Manual Slicer Settings Missing", + "description": "Your printer profile is missing some required slicer settings. Either enter the settings" + " in your printer profile, or switch to 'automatic' slicer settings. Missing Settings:" + " {missing_settings}", + 'help_link': "error_help_init_manual_slicer_settings_missing.md" + }, + 'timelapse_start_exception': { + "name": "Error Starting Timelapse", + "description": "An unexpected error occurred while starting the timelapse. See plugin_octolapse.log for " + "details.", + 'help_link': "error_help_init_timelapse_start_exception.md" + }, + 'too_few_extruders_defined': { + "name": "Extruder Count Error", + "description": "Your printer profile has fewer extruders ({printer_num_extruders}) defined than your gcode file ({gcode_num_extruders}).", + 'help_link': "error_help_init_too_few_extruders_defined.md" + }, + 'too_few_extruder_offsets_defined': { + "name": "Extruder Count Error", + "description": "Your printer profile has fewer extruder offsets ({num_extruder_offsets}) defined than extruders ({num_extruders}).", + 'help_link': "error_help_init_too_few_extruders_offsets_defined.md" + }, + 'unable_to_accept_snapshot_plan': { + "name": "Extruder Count Error", + "description": "Unable to accept the snapshot plan. Either it has already been accepted/cancellled, or an unexpected error occurred.", + 'help_link': "error_help_init_unable_to_accept_snapshot_plan.md" + }, + 'directory_test_failed': { + "name": "Directory Tests Failed", + "description": "The following Octolapse directories failed testing: {failed_directories}.", + 'help_link': "error_help_init_directory_test_failed.md" + }, + "overlay_font_path_not_found": { + "name": "Rendering Overlay Font Not Found", + "description": "The current rendering profile has a rendering overlay, but the selected font could not be found in the following location: {overlay_font_path}.", + 'help_link': "error_help_init_overlay_font_path_not_found.md" + } + + + }, + 'settings': { + 'slicer': { + 'simplify3d': { + "duplicate_toolhead_numbers": { + "name": "Duplicate Toolhead Numbers", + "description": "Multiple extruders are defined with the same toolhead number in simplify, " + "which is not supported by Octolapse. Duplicated toolhead numbers: " + "{toolhead_numbers}", + "help_link": "error_help_settings_slicer_simplify3d_duplicate_toolhead_numbers.md" + }, + "extruder_count_mismatch": { + "name": "Extruder Count Mismatch", + "description": "Your printer profile is configured with {configured_extruder_count} extruders, " + "but Simpify 3D is configured with {detected_extruder_count} extruders.", + "help_link": "error_help_settings_slicer_simplify3d_extruder_count_mismatch.md" + }, + "max_extruder_count_exceeded": { + "name": "Max Extruder Count Exceeded", + "description": "Octolapse detected {simplify_extruder_count} extruders in your Simplify 3D " + "gcode file, but can only support {max_extruder_count} extruders.", + "help_link": "error_help_settings_slicer_simplify3d_max_extruder_count_exceeded.md" + }, + "unexpected_max_toolhead_number": { + "name": "Incorrect Max Toolhead Number", + "description": "The maximum toolhead number expected was T{expected_max_toolhead_number}, but " + "Simplify 3D was configured with a max toolhead of {max_toolhead_number}.", + "help_link": "error_help_settings_slicer_simplify3d_unexpected_max_toolhead_number.md" + } + } + } + }, + 'rendering': { + 'archive': { + 'import': { + 'no_files_found': { + "name": "No Files Were Found", + "description": "No files were found within the archive.", + "help_link": "error_help_rendering_archive_import_no_files_found.md" + }, + 'zip_file_too_large': { + "name": "The Archive is Too Large", + "description": "The zip archive is too large to import.", + "help_link": "error_help_rendering_archive_import_zip_file_too_large.md" + }, + 'zip_file_corrupt': { + "name": "The Archive is Corrupt", + "description": "The zip archive is too large to import.", + "help_link": "error_help_rendering_archive_import_zip_file_corrupt.md" + } + } + } + } +} + +_error_not_found = { + "name": "Unknown Error", + "description": "An unknown error was raised, but the provided key could not be found in the " + "ErrorMessages dict. Keys: {keys}", + "help_link": "error_help_error_not_found.md" +} + +_error_not_a_valid_error_dict = { + "name": "Unknown Error", + "description": "An unknown error was raised, but the provided key could not be found in the " + "ErrorMessages dict. Keys: {keys}", + "help_link": "error_help_not_a_valid_error_dict.md" +} + + +def get_error(keys, **kwargs): + current_error_dict = _octolapse_errors + for key in keys: + try: + current_error_dict = current_error_dict[key] + except KeyError: + error = _error_not_found.copy() + error["description"] = error["description"].format(keys=keys) + return error + if not all(k in current_error_dict for k in ["name", "description", "help_link"]): + error = _error_not_a_valid_error_dict.copy() + error["description"] = error["description"].format(keys=keys) + return error + # copy the error so we don't mess up the original dict + error = current_error_dict.copy() + # try to format with any kwargs + try: + error["description"] = error["description"].format(**kwargs) + except KeyError: + pass + + return error + +class OctolapseException(Exception): + def __init__(self, keys, cause=None, **kwargs): + super(Exception, self).__init__() + self.keys = keys + self.cause = cause if cause is not None else None + self.error = get_error(keys, **kwargs) + self.name = self.error["name"] + self.description = self.error["description"] + self.help_link = self.error["help_link"] + + def __str__(self): + if self.cause is None: + return self.description + return "{0} - Inner Exception: {1}".format( + self.description, + "{} - {}".format(type(self.cause), self.cause) + ) + + def to_dict(self): + return { + "name": self.name, + "description": str(self), + "help_link": self.help_link + } + + + diff --git a/octoprint_octolapse/gcode_parser.py b/octoprint_octolapse/gcode_commands.py similarity index 72% rename from octoprint_octolapse/gcode_parser.py rename to octoprint_octolapse/gcode_commands.py index 61347f88..a4d9519d 100644 --- a/octoprint_octolapse/gcode_parser.py +++ b/octoprint_octolapse/gcode_commands.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -21,11 +21,10 @@ # following email address: FormerLurker@pm.me ################################################################################## from __future__ import unicode_literals -from six import string_types +# remove unused using +# from six import string_types import operator -import octoprint_octolapse.utility as utility import re -import string # create the module level logger from octoprint_octolapse.log import LoggingConfigurator @@ -51,7 +50,9 @@ def parse_float_positive(parameter_string): @staticmethod def parse_float(parameter_string): - assert (isinstance(parameter_string, string_types)) + # remove python 2 support + # assert (isinstance(parameter_string, string_types)) + assert (isinstance(parameter_string, str)) parameter_string = parameter_string.lstrip() index = 0 @@ -92,7 +93,9 @@ def parse_float(parameter_string): @staticmethod def parse_int(parameter_string): - assert (isinstance(parameter_string, string_types)) + # remove python 2 support + # assert (isinstance(parameter_string, string_types)) + assert (isinstance(parameter_string, str)) parameter_string = parameter_string.lstrip() index = 0 int_string = "" @@ -117,7 +120,9 @@ def parse_int(parameter_string): @staticmethod def parse_tool(parameter_string): - assert (isinstance(parameter_string, string_types)) + # remove python 2 support + # assert (isinstance(parameter_string, string_types)) + assert (isinstance(parameter_string, str)) parameter_string = parameter_string.strip().upper() if parameter_string[0] in ['?','X','C']: @@ -126,74 +131,6 @@ def parse_tool(parameter_string): return CommandParameter.parse_int(parameter_string) -class ParsedCommand(object): - # define slots for faster creation - __slots__ = ['cmd', 'parameters', 'gcode', 'comment'] - - def __init__(self, cmd, parameters, gcode, comment=None): - self.cmd = cmd - self.parameters = {} if parameters is None else parameters - if comment is None: - self.gcode, self.comment = ParsedCommand.extract_comment(gcode) - else: - self.gcode = gcode - self.comment = comment - - def to_dict(self): - return { - "cmd": self.cmd, - "parameters": self.parameters, - "gcode": self.gcode, - "comment": self.comment - } - - def update_gcode_string(self): - self.gcode = ParsedCommand.to_string(self) - - @classmethod - def create_from_cpp_parsed_command(cls, cpp_parsed_command): - gcode, comment = ParsedCommand.extract_comment(cpp_parsed_command[2]) - return ParsedCommand(cpp_parsed_command[0], cpp_parsed_command[1], gcode, comment) - - @staticmethod - def extract_comment(gcode): - # strip off any trailing comments - comment = "" - ix = gcode.find(";") - if ix > -1: - # we have a comment! - # see if we have anything AFTER the semicolon - if ix+1 < len(gcode): - comment = gcode[ix+1:].strip() - gcode = gcode[0:ix] - return gcode.strip(), comment - - @staticmethod - def to_string(parsed_command): - has_parameters = False - parameter_strings = [] - for key, value in parsed_command.parameters.items(): - has_parameters = True - if value is None: - value_string = "" - elif isinstance(value, float): - if key == "E": - value_string = "{0:.5f}".format(value) - else: - value_string = "{0:.3f}".format(value) - else: - value_string = "{0}".format(value) - - parameter_strings.append("{0}{1}".format(key,value_string)) - - if has_parameters: - separator = " " - else: - separator = "" - - return "{0}{1}{2}".format(parsed_command.cmd, separator, " ".join(parameter_strings)) - - class Command(object): def __init__(self, command, name, display_template=None, parameters=None, text_only_parameter=False): @@ -230,25 +167,6 @@ def parse_parameters(self, parameters_string): return parameters - def parse_parameters_fast(self, parameters_string, start_index, parameters=None): - - string_length = len(parameters_string) - if parameters is None: - parameters = {} - - if start_index < string_length and parameters_string[start_index] in self.Parameters: - if start_index <= string_length > 0: - end_index = Commands.get_float_end_index(parameters_string, start_index+1) - if end_index is None: - end_index = start_index - parameters[parameters_string[start_index]] = None - else: - parameters[parameters_string[start_index]] = float(parameters_string[start_index+1:end_index+1]) - if end_index+1 < string_length: - self.parse_parameters_fast(parameters_string, end_index+1, parameters) - - return parameters - def to_string(self): # if we do not have gcode, construct from the command and parameters command_string = self.Command @@ -632,7 +550,6 @@ class Commands(object): GcodeWords = {"G", "M", "T"} SuppressedSavedCommands = [M105.Command, M400.Command] SuppressedSnapshotGcodeCommands = [M105.Command] - CommandsRequireMetric = [G0.Command, G1.Command, G2.Command, G3.Command, G28.Command, G92.Command] TestModeSuppressExtrusionCommands = [G0.Command, G1.Command, G2.Command, G3.Command] TestModeSuppressCommands = [ G10.Command, G11.Command, @@ -641,13 +558,6 @@ class Commands(object): M116.Command, M106.Command, T.Command ] - @staticmethod - def format_for_fast_parsing(gcode): - ix = gcode.find(";") - if len(gcode) - 1 >= ix > -1: - gcode = gcode[0:ix] - return gcode.encode(u'utf-8').translate(None, string.whitespace).upper() - @staticmethod def strip_comments(gcode, remove_parentheticals=True): # strip off any trailing comments @@ -699,157 +609,6 @@ def strip_comments(gcode, remove_parentheticals=True): # strip whitespace return gcode.strip() - @staticmethod - def get_float_end_index(value, start_index): - index = -1 - has_seen_period = False - for index in range(start_index, len(value)): - if "0" <= value[index] <= "9": - continue - elif value[index] == ".": - if not has_seen_period: - has_seen_period = True - continue - else: - index -= 1 - break - else: - index -= 1 - break - if index >= start_index: - return index - return None - - @staticmethod - def fast_parse(original_gcode): - gcode = Commands.format_for_fast_parsing(original_gcode) - if gcode is None or len(gcode) < 1 or gcode[0] == '%': - return None - - # Now we should be left with the command and any parameters - # Make sure our command is a valid one - if len(gcode) < 2 or gcode[0] not in Commands.GcodeWords: - return None - - command_address_end_index = Commands.get_float_end_index(gcode, 1) - - if command_address_end_index is None: - command_address_end_index = 0 - - command_to_search = gcode[0:command_address_end_index+1] - if command_to_search not in Commands.CommandsDictionary: - return None - - cmd = Commands.CommandsDictionary[command_to_search] - parameters = cmd.parse_parameters_fast(gcode, command_address_end_index + 1) - # get the parameter string - return ParsedCommand(command_to_search, parameters, original_gcode) - - @staticmethod - def parse(gcode, remove_parentheticals=True): - original_gcode = gcode - gcode = Commands.strip_comments(gcode, remove_parentheticals=remove_parentheticals) - if gcode is None: - return ParsedCommand(None, None, original_gcode) - - # ignore blank lines - if len(gcode) < 1: - return ParsedCommand(None, None, original_gcode) - - # ignore any lines that start with a % - if gcode[0] == "%": - return ParsedCommand(None, None, original_gcode) - - # make sure our string is greater than 2 characters - if len(gcode) < 2: - return ParsedCommand(None, None, original_gcode) - - # extract any line numbers - if gcode[0] == "N": - _n_temp = "" - # extract any integers and ignore whitespace - for index in range(0, len(gcode) - 1): - _c = gcode[index].upper() - - if _c.isspace(): - continue - if "0" <= _c <= "9": - _n_temp += _c - else: - break - - # remove the line number from the command string and strip off whitespace from the front - gcode = gcode[index:].lstrip() - - # Now we should be left with the command and any parameters - # Make sure our command is a valid one - if len(gcode) < 2: - return ParsedCommand(None, None, original_gcode) - - # get the command letter - command_letter = gcode[0].upper() - if command_letter not in Commands.GcodeWords: - return ParsedCommand(None, None, original_gcode) - - # search for decimals or periods to build the command address - command_address = "" - has_seen_period = False - for index in range(1, len(gcode)): - c = gcode[index] - if c.isspace(): - continue - elif "0" <= c <= "9": - command_address += c - elif c == ".": - if not has_seen_period: - # add the current command address if it exists - address_number = utility.get_int(command_address, -1) - if address_number > -1: - command_address = "{}".format(address_number) - else: - command_address = '?' - command_address += c - has_seen_period = True - else: - - return ParsedCommand(None, None, original_gcode, error="Cannot parse the gcode address, multiple periods " - "seen.") - else: - break - # If we've not seen any periods, strip any leading 0s from the gcode - if not has_seen_period: - address_number = utility.get_int(command_address, -1) - if address_number > -1: - command_address = "{}".format(address_number) - else: - command_address = '?' - # make sure the command is in the dictionary - if command_letter != "T": - command_to_search = command_letter + command_address - else: - command_to_search = command_letter - - if command_to_search not in Commands.CommandsDictionary.keys(): - return ParsedCommand(command_to_search, None, original_gcode) - - cmd = Commands.CommandsDictionary[command_to_search] - - # get the parameter string - if len(gcode) > index: - parameters = gcode[index:] - else: - parameters = "" - - if not cmd.TextOnlyParameter: - try: - if command_letter == "T": - parameters = cmd.parse_parameters("T"+parameters) - else: - parameters = cmd.parse_parameters(parameters) - except ValueError as e: - ParsedCommand(command_to_search, None, original_gcode, error="{}".format(e)) - return ParsedCommand(command_to_search, parameters, original_gcode) - @staticmethod def to_string(parsed_command): if parsed_command.cmd is None: @@ -890,10 +649,10 @@ class Response(object): # This code was copied from the OctoPrint comm.pi file in order to sidestep an issue # where position responses with spaces after a colon (for example: ok X:150.0 Y:150.0 Z: 0.7 E: 0.0) # were not being detected as a position response, and failed to file a PositionReceived event - regex_float_pattern = "[-+]?[0-9]*\.?[0-9]+" - regex_e_positions = re.compile("E(?P\d+):(?P{float})".format(float=regex_float_pattern)) + regex_float_pattern = r"[-+]?[0-9]*\.?[0-9]+" + regex_e_positions = re.compile(r"E(?P\d+):(?P{float})".format(float=regex_float_pattern)) regex_position = re.compile( - "X:\s*(?P{float})\s*Y:\s*(?P{float})\s*Z:\s*(?P{float})\s*((E:\s*(?P{float}))|(?P(E\d+:{float}\s*)+))" + r"X:\s*(?P{float})\s*Y:\s*(?P{float})\s*Z:\s*(?P{float})\s*((E:\s*(?P{float}))|(?P(E\d+:{float}\s*)+))" .format(float=regex_float_pattern)) @staticmethod diff --git a/octoprint_octolapse/gcode_processor.py b/octoprint_octolapse/gcode_processor.py new file mode 100644 index 00000000..8a962827 --- /dev/null +++ b/octoprint_octolapse/gcode_processor.py @@ -0,0 +1,710 @@ +# coding=utf-8 +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## + +import GcodePositionProcessor +import octoprint_octolapse.utility as utility + +# create the module level logger +from octoprint_octolapse.log import LoggingConfigurator +logging_configurator = LoggingConfigurator() +logger = logging_configurator.get_logger(__name__) + + +class Extruder(utility.JsonSerializable): + __slots__ = [ + "x_firmware_offset", + "y_firmware_offset", + "z_firmware_offset", + "e", + "e_offset", + "e_relative", + "extrusion_length", + "extrusion_length_total", + "retraction_length", + "deretraction_length", + "is_extruding_start", + "is_extruding", + "is_primed", + "is_retracting_start", + "is_retracting", + "is_retracted", + "is_partially_retracted", + "is_deretracting_start", + "is_deretracting", + "is_deretracted" + ] + + def __init__(self, copy_from=None): + if copy_from is None: + self.x_firmware_offset = 0 + self.y_firmware_offset = 0 + self.z_firmware_offset = 0 + self.e = 0 + self.e_offset = 0 + self.e_relative = False + self.extrusion_length = 0 + self.extrusion_length_total = 0 + self.retraction_length = 0 + self.deretraction_length = 0 + self.is_extruding_start = False + self.is_extruding = False + self.is_primed = False + self.is_retracting_start = False + self.is_retracting = False + self.is_retracted = False + self.is_partially_retracted = False + self.is_deretracting_start = False + self.is_deretracting = False + self.is_deretracted = False + else: + self.x_firmware_offset = copy_from.x_firmware_offset + self.y_firmware_offset = copy_from.y_firmware_offset + self.z_firmware_offset = copy_from.z_firmware_offset + self.e = copy_from.e + self.e_offset = copy_from.e_offset + self.e_relative = copy_from.e_relative + self.extrusion_length = copy_from.extrusion_length + self.extrusion_length_total = copy_from.extrusion_length_total + self.retraction_length = copy_from.retraction_length + self.deretraction_length = copy_from.deretraction_length + self.is_extruding_start = copy_from.is_extruding_start + self.is_extruding = copy_from.is_extruding + self.is_primed = copy_from.is_primed + self.is_retracting_start = copy_from.is_retracting_start + self.is_retracting = copy_from.is_retracting + self.is_retracted = copy_from.is_retracted + self.is_partially_retracted = copy_from.is_partially_retracted + self.is_deretracting_start = copy_from.is_deretracting_start + self.is_deretracting = copy_from.is_deretracting + self.is_deretracted = copy_from.is_deretracted + + @staticmethod + def copy_from_cpp_extruder(cpp_extruder, target): + target.x_firmware_offset = cpp_extruder[0] + target.y_firmware_offset = cpp_extruder[1] + target.z_firmware_offset = cpp_extruder[2] + target.e = cpp_extruder[3] + target.e_offset = cpp_extruder[4] + target.e_relative = cpp_extruder[5] + target.extrusion_length = cpp_extruder[6] + target.extrusion_length_total = cpp_extruder[7] + target.retraction_length = cpp_extruder[8] + target.deretraction_length = cpp_extruder[9] + target.is_extruding_start = cpp_extruder[10] > 0 + target.is_extruding = cpp_extruder[11] > 0 + target.is_primed = cpp_extruder[12] > 0 + target.is_retracting_start = cpp_extruder[13] > 0 + target.is_retracting = cpp_extruder[14] > 0 + target.is_retracted = cpp_extruder[15] > 0 + target.is_partially_retracted = cpp_extruder[16] > 0 + target.is_deretracting_start = cpp_extruder[17] > 0 + target.is_deretracting = cpp_extruder[18] > 0 + target.is_deretracted = cpp_extruder[19] > 0 + + @staticmethod + def create_from_cpp_extruder(cpp_extruder): + extruder = Extruder() + Extruder.copy_from_cpp_extruder(cpp_extruder, extruder) + return extruder + + def to_dict(self): + return { + "x_firmware_offset": self.x_firmware_offset, + "y_firmware_offset": self.y_firmware_offset, + "z_firmware_offset": self.z_firmware_offset, + "e": self.e, + "e_offset": self.e_offset, + "e_relative": self.e_relative, + "extrusion_length": self.extrusion_length, + "extrusion_length_total": self.extrusion_length_total, + "retraction_length": self.retraction_length, + "deretraction_length": self.deretraction_length, + "is_extruding_start": self.is_extruding_start, + "is_extruding": self.is_extruding, + "is_primed": self.is_primed, + "is_retracting_start": self.is_retracting_start, + "is_retracting": self.is_retracting, + "is_retracted": self.is_retracted, + "is_partially_retracted": self.is_partially_retracted, + "is_deretracting_start": self.is_deretracting_start, + "is_deretracting": self.is_deretracting, + "is_deretracted": self.is_deretracted + } + + +class Pos(utility.JsonSerializable): + # Add slots for faster copy and init + __slots__ = [ + "x", + "y", + "z", + "f", + "x_offset", + "y_offset", + "z_offset", + "x_firmware_offset", + "y_firmware_offset", + "z_firmware_offset", + "z_relative", + "last_extrusion_height", + "height", + "firmware_retraction_length", + "firmware_unretraction_additional_length", + "firmware_retraction_feedrate", + "firmware_unretraction_feedrate", + "firmware_z_lift", + "layer", + "height_increment", + "height_increment_change_count", + "current_tool", + "num_extruders", + "x_homed", + "y_homed", + "z_homed", + "is_relative", + "is_extruder_relative", + "is_metric", + "is_printer_primed", + "has_definite_position", + "is_layer_change", + "is_height_change", + "is_height_increment_change", + "is_xy_travel", + "is_xyz_travel", + "is_zhop", + "has_xy_position_changed", + "has_position_changed", + "has_received_home_command", + "is_in_position", + "in_path_position", + "file_line_number", + "gcode_number", + "file_position", + "is_in_bounds", + "parsed_command", + "extruders" + ] + + min_length_to_retract = 0.0001 + + def __init__(self): + self.parsed_command = None + self.f = None + self.x = None + self.x_offset = 0 + self.x_firmware_offset = 0 + self.x_homed = False + self.y = None + self.y_offset = 0 + self.y_firmware_offset = 0 + self.y_homed = False + self.z = None + self.z_offset = 0 + self.z_firmware_offset = 0 + self.z_homed = False + self.z_relative = 0 + self.is_relative = False + self.is_extruder_relative = False + self.is_metric = True + self.last_extrusion_height = None + self.layer = 0 + self.height_increment = 0 + self.height_increment_change_count = 0 + self.height = 0 + self.is_printer_primed = False + self.firmware_retraction_length = None + self.firmware_unretraction_additional_length = None + self.firmware_retraction_feedrate = None + self.firmware_unretraction_feedrate = None + self.firmware_z_lift = None + self.has_definite_position = False + # Extruders + self.current_tool = None + self.extruders = [] + + # State + self.is_layer_change = False + self.is_height_change = False + self.is_height_increment_change = False + self.is_xy_travel = False + self.is_xyz_travel = False + self.is_zhop = False + self.has_xy_position_changed = False + self.has_position_changed = False + self.has_received_home_command = False + self.is_in_position = False + self.in_path_position = False + self.is_in_bounds = True + # Gcode File Tracking + self.file_line_number = -1 + self.gcode_number = -1 + self.file_position = -1 + + @staticmethod + def copy_from_cpp_pos(cpp_pos, target): + target.x = None if cpp_pos[43] > 0 else cpp_pos[0] + target.y = None if cpp_pos[44] > 0 else cpp_pos[1] + target.z = None if cpp_pos[45] > 0 else cpp_pos[2] + target.f = None if cpp_pos[46] > 0 else cpp_pos[3] + target.x_offset = cpp_pos[4] + target.y_offset = cpp_pos[5] + target.z_offset = cpp_pos[6] + target.x_firmware_offset = cpp_pos[7] + target.y_firmware_offset = cpp_pos[8] + target.z_firmware_offset = cpp_pos[9] + target.z_relative = cpp_pos[10] + target.last_extrusion_height = None if cpp_pos[49] > 0 else cpp_pos[11] + target.height = cpp_pos[12] + target.firmware_retraction_length = None if cpp_pos[51] > 0 else cpp_pos[13] + target.firmware_unretraction_additional_length = None if cpp_pos[52] > 0 else cpp_pos[14] + target.firmware_retraction_feedrate = None if cpp_pos[53] > 0 else cpp_pos[15] + target.firmware_unretraction_feedrate = None if cpp_pos[54] > 0 else cpp_pos[16] + target.firmware_z_lift = None if cpp_pos[55] > 0 else cpp_pos[17] + target.layer = cpp_pos[18] + target.height_increment = cpp_pos[19] + target.height_increment_change_count = cpp_pos[20] + target.current_tool = cpp_pos[21] + target.num_extruders = cpp_pos[22] + target.x_homed = cpp_pos[23] > 0 + target.y_homed = cpp_pos[24] > 0 + target.z_homed = cpp_pos[25] > 0 + target.is_relative = None if cpp_pos[47] > 0 else cpp_pos[26] > 0 + target.is_extruder_relative = None if cpp_pos[48] > 0 else cpp_pos[27] > 0 + target.is_metric = None if cpp_pos[50] > 0 else cpp_pos[28] > 0 + target.is_printer_primed = cpp_pos[29] > 0 + target.has_definite_position = cpp_pos[30] > 0 + + target.is_layer_change = cpp_pos[31] > 0 + target.is_height_change = cpp_pos[32] > 0 + target.is_height_increment_change = cpp_pos[33] + target.is_xy_travel = cpp_pos[34] > 0 + target.is_xyz_travel = cpp_pos[35] > 0 + target.is_zhop = cpp_pos[36] > 0 + target.has_xy_position_changed = cpp_pos[37] > 0 + target.has_position_changed = cpp_pos[38] > 0 + target.has_received_home_command = cpp_pos[39] > 0 + # Todo: figure out how to deal with these things which must be currently ignored + target.is_in_position = cpp_pos[40] > 0 + target.in_path_position = cpp_pos[41] > 0 + target.is_in_bounds = cpp_pos[42] > 0 + target.file_line_number = cpp_pos[56] + target.gcode_number = cpp_pos[57] + target.file_position = cpp_pos[58] + parsed_command = cpp_pos[59] + if parsed_command is not None: + target.parsed_command = ParsedCommand(parsed_command[0], parsed_command[1], parsed_command[2], parsed_command[3]) + else: + target.parsed_command = None + extruders = cpp_pos[60] + if extruders is not None: + target.extruders = [] + for extruder in extruders: + target.extruders.append(Extruder.create_from_cpp_extruder(extruder)) + else: + target.extruders = None + + return target + + @staticmethod + def create_from_cpp_pos(cpp_pos): + pos = Pos() + Pos.copy_from_cpp_pos(cpp_pos, pos) + return pos + + @staticmethod + def copy(source, target): + """does not copy all items, only what is necessary for the Position.Update command""" + target.f = source.f + target.x = source.x + target.x_offset = source.x_offset + target.x_homed = source.x_homed + target.y = source.y + target.y_offset = source.y_offset + target.y_homed = source.y_homed + target.z = source.z + target.z_offset = source.z_offset + target.z_homed = source.z_homed + target.is_relative = source.is_relative + target.is_extruder_relative = source.is_extruder_relative + target.is_metric = source.is_metric + target.last_extrusion_height = source.last_extrusion_height + target.layer = source.layer + target.height_increment = source.height_increment + target.height_increment_change_count = source.height_increment_change_count + target.height = source.height + target.is_printer_primed = source.is_printer_primed + target.firmware_retraction_length = source.firmware_retraction_length + target.firmware_unretraction_additional_length = source.firmware_unretraction_additional_length + target.firmware_retraction_feedrate = source.firmware_retraction_feedrate + target.firmware_unretraction_feedrate = source.firmware_unretraction_feedrate + target.firmware_z_lift = source.firmware_z_lift + target.has_definite_position = source.has_definite_position + target.in_path_position = source.in_path_position + target.is_zhop = source.is_zhop + target.is_in_position = source.is_in_position + + # copy extruders + target.extruders = [] + for extruder in source.extruders: + target.extruders.append(Extruder(copy_from=extruder)) + + # Resets state changes + target.is_layer_change = False + target.is_height_change = False + target.is_height_increment_change = False + target.is_xy_travel = False + target.has_position_changed = False + target.has_received_home_command = False + target.is_in_bounds = True + + def get_current_extruder(self): + if len(self.extruders) == 0: + logger.error("The current extruder was requested, but none was found.") + return None + + tool_index = self.current_tool + if tool_index > len(self.extruders) - 1: + tool_index = len(self.extruders) - 1 + logger.warning("The requested tool index of %d is greater than the number of extruders ($d).", + self.current_tool, len(self.extruders)) + if tool_index < 0: + tool_index = 0 + logger.warning("The requested tool index was less than zero. Index: %d.", + self.current_tool) + return self.extruders[tool_index] + + def to_extruder_state_dict(self): + extruder = self.get_current_extruder() + return { + "current_tool": self.current_tool, + "x_firmware_offset": self.x_firmware_offset, + "y_firmware_offset": self.y_firmware_offset, + "z_firmware_offset": self.z_firmware_offset, + "e": extruder.e_relative, + "extrusion_length": extruder.extrusion_length, + "extrusion_length_total": extruder.extrusion_length_total, + "retraction_length": extruder.retraction_length, + "deretraction_length": extruder.deretraction_length, + "is_extruding_start": extruder.is_extruding_start, + "is_extruding": extruder.is_extruding, + "is_primed": extruder.is_primed, + "is_retracting_start": extruder.is_retracting_start, + "is_retracting": extruder.is_retracting, + "is_retracted": extruder.is_retracted, + "is_partially_retracted": extruder.is_partially_retracted, + "is_deretracting_start": extruder.is_deretracting_start, + "is_deretracting": extruder.is_deretracting, + "is_deretracted": extruder.is_deretracted + } + + def to_state_dict(self): + return { + "gcode": "" if self.parsed_command is None else self.parsed_command.gcode, + "x_homed": self.x_homed, + "y_homed": self.y_homed, + "z_homed": self.z_homed, + "has_definite_position": self.has_definite_position, + "is_layer_change": self.is_layer_change, + "is_height_change": self.is_height_change, + "is_height_increment_change": self.is_height_change, + "is_zhop": self.is_zhop, + "is_relative": self.is_relative, + "is_extruder_relative": self.is_extruder_relative, + "is_metric": self.is_metric, + "layer": self.layer, + "height": self.height, + "last_extrusion_height": self.last_extrusion_height, + "is_in_position": self.is_in_position, + "in_path_position": self.in_path_position, + "is_printer_primed": self.is_printer_primed, + "has_received_home_command": self.has_received_home_command, + "is_xy_travel": self.is_xy_travel, + "is_in_bounds": self.is_in_bounds, + } + + def to_position_dict(self): + extruder = self.get_current_extruder() + return { + "current_tool": self.current_tool, + "f": self.f, + "x": self.x, + "x_offset": self.x_offset, + "x_firmware_offset": self.x_firmware_offset, + "y": self.y, + "y_offset": self.y_offset, + "y_firmware_offset": self.y_firmware_offset, + "z": self.z, + "z_offset": self.z_offset, + "z_firmware_offset": self.z_firmware_offset, + "e": extruder.e, + "e_offset": extruder.e_offset + } + + def to_dict(self): + extruder = self.get_current_extruder() + return { + "current_tool": self.current_tool, + "gcode": "" if self.parsed_command is None else self.parsed_command.gcode, + "f": self.f, + "x": self.x, + "x_offset": self.x_offset, + "x_firmware_offset": self.x_firmware_offset, + "x_homed": self.x_homed, + "y": self.y, + "y_offset": self.y_offset, + "y_firmware_offset": self.y_firmware_offset, + "y_homed": self.y_homed, + "z": self.z, + "z_offset": self.z_offset, + "z_firmware_offset": self.z_firmware_offset, + "z_homed": self.z_homed, + "z_relative": self.z_relative, + "e": extruder.e, + "e_offset": extruder.e_offset, + "is_relative": self.is_relative, + "is_extruder_relative": self.is_extruder_relative, + "is_metric": self.is_metric, + "last_extrusion_height": self.last_extrusion_height, + "is_layer_change": self.is_layer_change, + "is_height_increment_change": self.is_height_increment_change, + "is_zhop": self.is_zhop, + "is_in_position": self.is_in_position, + "in_path_position": self.in_path_position, + "is_primed": self.is_printer_primed, + "has_xy_position_changed": self.has_xy_position_changed, + "has_position_changed": self.has_position_changed, + "layer": self.layer, + "height_increment": self.height_increment, + "height_increment_change_count": self.height_increment_change_count, + "height": self.height, + "has_received_home_command": self.has_received_home_command, + "file_line_number": self.file_line_number, + "gcode_number": self.gcode_number, + "file_position": self.file_position, + "is_in_bounds": self.is_in_bounds, + "extruders": [x.to_dict() for x in self.extruders] + } + + def distance_to_zlift(self, z_hop, restrict_lift_height=True): + amount_to_lift = ( + None if self.z is None or + self.last_extrusion_height is None + else z_hop - (self.z - self.last_extrusion_height) + ) + if amount_to_lift is None: + return 0 + if restrict_lift_height: + if amount_to_lift < utility.FLOAT_MATH_EQUALITY_RANGE: + return 0 + elif amount_to_lift > z_hop: + return z_hop + return utility.round_to(amount_to_lift, utility.FLOAT_MATH_EQUALITY_RANGE) + + def length_to_retract(self, amount_to_retract): + extruder = self.get_current_extruder() + # if we don't have any history, we want to retract + retract_length = utility.round_to_float_equality_range( + utility.round_to_float_equality_range(amount_to_retract - extruder.retraction_length) + ) + if retract_length < 0: + retract_length = 0 + elif retract_length > amount_to_retract: + retract_length = amount_to_retract + elif retract_length < Pos.min_length_to_retract: + # we don't want to retract less than the min_length_to_retract, + # else we might have quality issues! + retract_length = 0 + # return the calculated retraction length + return retract_length + + def gcode_x(self, x=None): + if x is None: + x = self.x + return x - self.x_offset + self.x_firmware_offset + + def gcode_y(self, y=None): + if y is None: + y = self.y + return y - self.y_offset + self.y_firmware_offset + + def gcode_z(self, z=None): + if z is None: + z = self.z + return z - self.z_offset + self.z_firmware_offset + + def gcode_e(self, e=None): + extruder = self.get_current_extruder() + if e is None: + e = extruder.e + return e - extruder.e_offset + + +class ParsedCommand(utility.JsonSerializable): + # define slots for faster creation + __slots__ = ['cmd', 'parameters', 'gcode', 'comment'] + + def __init__(self, cmd, parameters, gcode, comment=None): + self.cmd = cmd + self.parameters = {} if parameters is None else parameters + self.gcode = gcode + self. comment = comment + + def to_dict(self): + return { + "cmd": self.cmd, + "parameters": self.parameters, + "gcode": self.gcode, + "comment": self.comment + } + + def update_gcode_string(self): + self.gcode = ParsedCommand.to_string(self) + + @classmethod + def create_from_cpp_parsed_command(cls, cpp_parsed_command): + return ParsedCommand(cpp_parsed_command[0], cpp_parsed_command[1], cpp_parsed_command[2], cpp_parsed_command[3]) + + @staticmethod + def clean_gcode(gcode): + if gcode is None: + return None, None + # strip off any trailing comments + comment = "" + ix = gcode.find(";") + if ix > -1: + # we have a comment! + # see if we have anything AFTER the semicolon + if ix+1 < len(gcode): + comment = gcode[ix+1:].strip() + gcode = gcode[0:ix] + # remove any duplicated whitespace, replace all whitespace with a ' ' and make upper case + gcode = ' '.join(gcode.upper()) + return gcode.strip().upper(), comment + + @staticmethod + def to_string(parsed_command): + has_parameters = False + parameter_strings = [] + for key, value in parsed_command.parameters.items(): + has_parameters = True + if value is None: + value_string = "" + elif isinstance(value, float): + if key == "E": + value_string = "{0:.5f}".format(value) + else: + value_string = "{0:.3f}".format(value) + else: + value_string = "{0}".format(value) + + parameter_strings.append("{0}{1}".format(key,value_string)) + + if has_parameters: + separator = " " + else: + separator = "" + + return "{0}{1}{2}".format(parsed_command.cmd, separator, " ".join(parameter_strings)) + + def is_octolapse_command(self): + return self.cmd == "@OCTOLAPSE" and len(self.parameters) == 1 + + +class GcodeProcessor(object): + _key = "plugin_octolapse" + + @staticmethod + def initialize_position_processor(position_args, key=_key): + try: + GcodePositionProcessor.Initialize(key, position_args) + return True + except Exception as e: + logger.exception("An error occurred while initializing the GcodePositionProcessor!") + raise e + return False + + @staticmethod + def parse(gcode): + parsed_command_cpp = GcodePositionProcessor.Parse(gcode.encode('ascii', errors="replace").decode()) + if parsed_command_cpp: + parsed_command = ParsedCommand.create_from_cpp_parsed_command(parsed_command_cpp) + else: + # do our best to create a parsed command since the C++ processor couldn't do it + gcode, comment = ParsedCommand.clean_gcode(gcode) + parsed_command = ParsedCommand(None, None, gcode, comment) + + return parsed_command + + @staticmethod + def get_current_position(key=_key): + current_pos_cpp = GcodePositionProcessor.GetCurrentPositionTuple(key) + return Pos.create_from_cpp_pos(current_pos_cpp) + + @staticmethod + def get_previous_position(key=_key): + previous_pos_cpp = GcodePositionProcessor.GetPreviousPositionTuple(key) + return Pos.create_from_cpp_pos(previous_pos_cpp) + + @staticmethod + def update_position(position, x, y, z, e, f, key=_key): + cpp_pos = GcodePositionProcessor.UpdatePosition( + key, + 0.0 if x is None else x, + True if x is None else False, + 0.0 if y is None else y, + True if y is None else False, + 0.0 if z is None else z, + True if z is None else False, + 0.0 if e is None else e, + True if e is None else False, + 0.0 if f is None else f, + True if f is None else False, + ) + Pos.copy_from_cpp_pos(cpp_pos, position) + return position + + @staticmethod + def undo(key=_key): + GcodePositionProcessor.Undo(key) + + @staticmethod + def update(gcode, position, key=_key): + cpp_pos = GcodePositionProcessor.Update(key, gcode) + Pos.copy_from_cpp_pos(cpp_pos, position) + return position + + +# class GcodeStabilizationProcessor(object): +# +# @staticmethod +# def smart_layer_stabilization(position_args, stabilization_args, smart_layer_args): +# try: +# ret_val = list(GcodePositionProcessor.GetSnapshotPlans_SmartLayer( +# position_args, +# stabilization_args, +# smart_layer_args +# )) +# return ret_val +# except Exception as e: +# logger.exception("An error occurred while running the smart_layer_stabilization processor.") +# raise e diff --git a/octoprint_octolapse/log.py b/octoprint_octolapse/log.py index 2c1fbf4c..2759cfc4 100644 --- a/octoprint_octolapse/log.py +++ b/octoprint_octolapse/log.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -24,12 +24,20 @@ import logging import datetime as datetime import os -import functools -import types -import six -import copy +# Remove python 2 support +# import six from octoprint.logging.handlers import AsyncLogHandlerMixin, CleaningTimedRotatingFileHandler + +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + # custom log level - VERBOSE VERBOSE = 5 DEBUG = logging.DEBUG @@ -68,15 +76,6 @@ def formatTime(self, record, datefmt=None): return s -class Singleton(type): - _instances = {} - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] - - class OctolapseConsoleHandler(logging.StreamHandler, AsyncLogHandlerMixin): def __init__(self, *args, **kwargs): super(OctolapseConsoleHandler, self).__init__(*args, **kwargs) @@ -86,8 +85,18 @@ class OctolapseFileHandler(CleaningTimedRotatingFileHandler, AsyncLogHandlerMixi def __init__(self, *args, **kwargs): super(OctolapseFileHandler, self).__init__(*args, **kwargs) -@six.add_metaclass(Singleton) -class LoggingConfigurator(object): + def delete_all_backups(self): + # clean up old files on handler start + backup_count = self.backupCount + self.backupCount = 0 + for s in self.getFilesToDelete(): + os.remove(s) + self.backupCount = backup_count + +# remove python 2 support +# @six.add_metaclass(Singleton) +class LoggingConfigurator(metaclass=Singleton): + BACKUP_COUNT = 3 def __init__(self): self.logging_formatter = OctolapseFormatter() @@ -138,7 +147,7 @@ def _remove_handlers(self): self._console_handler = None def _add_file_handler(self, log_file_path, log_level): - self._file_handler = OctolapseFileHandler(log_file_path, when="D", backupCount=3) + self._file_handler = OctolapseFileHandler(log_file_path, when="D", backupCount=LoggingConfigurator.BACKUP_COUNT) self._file_handler.setFormatter(self.logging_formatter) self._file_handler.setLevel(log_level) self._root_logger.addHandler(self._file_handler) @@ -149,65 +158,63 @@ def _add_console_handler(self, log_level): self._console_handler.setLevel(log_level) self._root_logger.addHandler(self._console_handler) - def configure_loggers(self, log_file_path=None, debug_settings=None): + def do_rollover(self, clear_all=False): + if self._file_handler is None: + return + # To clear everything, we'll roll over every file + self._file_handler.doRollover() + if clear_all: + self._file_handler.delete_all_backups() + + + def configure_loggers(self, log_file_path=None, logging_settings=None): default_log_level = logging.DEBUG log_to_console = True - if debug_settings is not None: - default_log_level = debug_settings.default_log_level - log_to_console = debug_settings.log_to_console + if logging_settings is not None: + default_log_level = logging_settings.default_log_level + log_to_console = logging_settings.log_to_console # clear any handlers from the root logger self._remove_handlers() # set the log level self._root_logger.setLevel(logging.NOTSET) - if ( - debug_settings is None or - debug_settings.enabled or - debug_settings.enabled_loggers - or debug_settings.log_all_errors - ): - if log_file_path is not None: - # ensure that the logging path and file exist - directory = os.path.dirname(log_file_path) - import distutils.dir_util - distutils.dir_util.mkpath(directory) - if not os.path.isfile(log_file_path): - open(log_file_path, 'w').close() - - # add the file handler - self._add_file_handler(log_file_path, logging.NOTSET) - - # if we are logging to console, add the console logging handler - if log_to_console: - self._add_console_handler(logging.NOTSET) - for logger_full_name in self.child_loggers: - - if logger_full_name.startswith("octolapse."): - logger_name = logger_full_name[10:] - else: - logger_name = logger_full_name - if debug_settings is not None: - current_logger = self._root_logger.getChild(logger_name) - found_enabled_logger = None - for enabled_logger in debug_settings.enabled_loggers: - if enabled_logger["name"] == logger_full_name: - found_enabled_logger = enabled_logger - break - - if ( - debug_settings.log_all_errors and ( - found_enabled_logger is None or current_logger.level > logging.ERROR - ) - ): - current_logger.setLevel(logging.ERROR) - - elif found_enabled_logger is not None: - current_logger.setLevel(found_enabled_logger["log_level"]) - else: - # log level critical + 1 will not log anything - current_logger.setLevel(logging.CRITICAL + 1) + if log_file_path is not None: + # ensure that the logging path and file exist + directory = os.path.dirname(log_file_path) + import distutils.dir_util + distutils.dir_util.mkpath(directory) + if not os.path.isfile(log_file_path): + open(log_file_path, 'w').close() + + # add the file handler + self._add_file_handler(log_file_path, logging.NOTSET) + + # if we are logging to console, add the console logging handler + if log_to_console: + self._add_console_handler(logging.NOTSET) + for logger_full_name in self.child_loggers: + + if logger_full_name.startswith("octolapse."): + logger_name = logger_full_name[10:] + else: + logger_name = logger_full_name + if logging_settings is not None: + current_logger = self._root_logger.getChild(logger_name) + found_enabled_logger = None + for enabled_logger in logging_settings.enabled_loggers: + if enabled_logger.name == logger_full_name: + found_enabled_logger = enabled_logger + break + + if found_enabled_logger is None or current_logger.level > logging.ERROR: + current_logger.setLevel(logging.ERROR) + + elif found_enabled_logger is not None: + current_logger.setLevel(found_enabled_logger.log_level) else: - current_logger = self._root_logger.getChild(logger_name) - current_logger.setLevel(default_log_level) - + # log level critical + 1 will not log anything + current_logger.setLevel(logging.CRITICAL + 1) + else: + current_logger = self._root_logger.getChild(logger_name) + current_logger.setLevel(default_log_level) diff --git a/octoprint_octolapse/messenger_worker.py b/octoprint_octolapse/messenger_worker.py index 855f0143..276ae8e8 100644 --- a/octoprint_octolapse/messenger_worker.py +++ b/octoprint_octolapse/messenger_worker.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -23,7 +23,9 @@ from __future__ import absolute_import from __future__ import unicode_literals from threading import Thread, Lock -from six.moves import queue +# Remove python 2 support +# from six.moves import queue +import queue as queue from collections import deque import time from octoprint_octolapse.log import LoggingConfigurator @@ -119,7 +121,6 @@ def run(self): while True: try: plugin_message = self._queue.get(timeout=self.update_period_seconds) - logger.verbose("Sending Message. Data: {0}".format(plugin_message)) assert(isinstance(plugin_message, PluginMessage)) # add the message to the queue self.message_queue.add(plugin_message) @@ -128,4 +129,4 @@ def run(self): except queue.Empty: pass except Exception as e: - logger.exception(e) + logger.exception("An unexpected exception occurred while sending message to the UI.") diff --git a/octoprint_octolapse/settings_migration.py b/octoprint_octolapse/migration.py similarity index 54% rename from octoprint_octolapse/settings_migration.py rename to octoprint_octolapse/migration.py index 03bc0e06..24cc6964 100644 --- a/octoprint_octolapse/settings_migration.py +++ b/octoprint_octolapse/migration.py @@ -1,14 +1,39 @@ -from distutils.version import LooseVersion +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +from octoprint_octolapse_setuptools import NumberedVersion + import json import sys import os -import six +# remove unused using +# import six import copy +import shutil # create the module level logger from octoprint_octolapse.log import LoggingConfigurator logging_configurator = LoggingConfigurator() logger = logging_configurator.get_logger(__name__) + def get_version(settings_dict): version = 'unknown' if 'version' in settings_dict: @@ -17,12 +42,72 @@ def get_version(settings_dict): version = settings_dict["main_settings"]["version"] return version + +def get_settings_version(settings_dict): + settings_version = None + if 'main_settings' in settings_dict and 'settings_version' in settings_dict["main_settings"]: + settings_version = settings_dict["main_settings"]["settings_version"] + return settings_version + + +# when adding a file migration, the version number in __init__.py.get_settings_version needs to be incremented +# and the current version needs to be set. Only do this if you add a file migration! +settings_version_translation = [ + '0.4.0rc1.dev2', # the version here is ambiguous, but this is only used for file migrations + '0.4.0rc1.dev3', + '0.4.0rc1.dev4', + '0.4.0rc1' # <-- the current file migration version is set to 3, which is THIS version index. +] + + +def get_version_from_settings_index(index): + if index > len(settings_version_translation) - 1 or index < 0: + return None + return settings_version_translation[index] + + +def migrate_files(current_version, target_version, data_directory): + # look up the version names + has_updated = False + if current_version is None: + return False + if NumberedVersion(current_version) < NumberedVersion("0.4.0rc1.dev3"): + if migrate_files_pre_0_4_0_rc1_dev3(current_version, data_directory): + has_updated = True + return has_updated + + +def migrate_files_pre_0_4_0_rc1_dev3(current_version, data_directory): + + src = os.path.join(data_directory, "snapshots") + if not os.path.isdir(src): + return False + # create a new directory called tmp + tmp = os.path.join(data_directory, "tmp") + if not os.path.isdir(tmp): + os.mkdir(tmp) + # make sure a directory called octolapse_snapshots_tmp does not exist in the tmp folder + dst = os.path.join(tmp, "octolapse_snapshots_tmp") + if os.path.isdir(dst): + return False + # copy all of the files from the snapshots folder to the new directory + shutil.copytree(src, dst) + # delete the files within the old directory and remove the folder. + shutil.rmtree(src) + return True + + def migrate_settings(current_version, settings_dict, default_settings_directory, data_directory): # extract the settings version # note that the version moved from settings.version to settings.main_settings.version version = get_version(settings_dict) + settings_version = get_settings_version(settings_dict) + if version == 'unknown': - raise Exception("Could not find the settings version.") + raise Exception("Could not find version information within the settings json, cannot perform migration.") + + if version == "0+unknown": + raise Exception("An unknown settings version was detected, cannot perform migration.") # create a copy of the settings original_settings_copy = copy.deepcopy(settings_dict) @@ -30,19 +115,47 @@ def migrate_settings(current_version, settings_dict, default_settings_directory, # create a flag to indicate that we've updated settings has_updated = False - if LooseVersion(version) <= LooseVersion("0.3.3rc3.dev0"): + if settings_version is None and NumberedVersion(version) <= NumberedVersion("0.3.3rc3.dev0"): has_updated = True settings_dict = migrate_pre_0_3_3_rc3_dev(current_version, settings_dict, os.path.join(default_settings_directory, 'settings_default_0.3.3rc3.dev0.json')) - if LooseVersion(version) < LooseVersion("0.4.0rc1.dev0"): + if settings_version is None and NumberedVersion(version) < NumberedVersion("0.4.0rc1.dev0"): has_updated = True settings_dict = migrate_pre_0_3_5_rc1_dev(current_version, settings_dict, os.path.join(default_settings_directory, 'settings_default_0.4.0rc1.dev0.json')) - # If we've updated the settings, save a backup of the old settings and update the version + if settings_version is None and NumberedVersion(version) < NumberedVersion("0.4.0rc1.dev2"): + has_updated = True + settings_dict = migrate_pre_0_4_0_rc1_dev2(current_version, settings_dict, os.path.join(default_settings_directory, 'settings_default_0.4.0rc1.dev2.json')) + + if settings_version is None and NumberedVersion(version) < NumberedVersion("0.4.0rc1.dev3"): + has_updated = True + settings_dict = migrate_pre_0_4_0_rc1_dev3(current_version, settings_dict, os.path.join(default_settings_directory, 'settings_default_0.4.0rc1.dev3.json')) + + if settings_version is None and NumberedVersion(version) < NumberedVersion("0.4.0rc1.dev4"): + has_updated = True + settings_dict = migrate_pre_0_4_0_rc1_dev4(current_version, settings_dict, os.path.join(default_settings_directory, 'settings_default_0.4.0rc1.dev4.json')) + + # Start using main_settings.settings_version for migrations + # this value will start off as None + if settings_version is None: + has_updated = True + settings_dict = migrate_pre_0_4_0(current_version, settings_dict, os.path.join(default_settings_directory, 'settings_default_0.4.0.json')) + + if settings_version is not None and NumberedVersion(settings_version) < NumberedVersion("0.4.3"): # None < 0.4.0 + has_updated = True + settings_dict = migrate_pre_0_4_0(current_version, settings_dict, + os.path.join(default_settings_directory, 'settings_default_0.4.3.json')) + + # Add other migrations here in the future... + + # if we have updated, create a backup of the current settings if has_updated: with open(get_settings_backup_name(version, data_directory), "w+") as f: json.dump(original_settings_copy, f) - settings_dict["main_settings"]["version"] = current_version + + # Ensure that the version and settings_version within the settings.json file is up to date + settings_dict["main_settings"]["version"] = current_version + settings_dict["main_settings"]["settings_version"] = NumberedVersion.CurrentSettingsVersion return settings_dict @@ -50,6 +163,7 @@ def migrate_settings(current_version, settings_dict, default_settings_directory, def get_settings_backup_name(version, default_settings_directory): return os.path.join(default_settings_directory, "settings_backup_{0}.json".format(version)) + def migrate_post_0_3_5(current_version, settings_dict, log_file_path, default_settings_path): # This is a reminder that the 'version' setting moved to Settings.main_settings.version # so you don't forget :) @@ -163,6 +277,8 @@ def migrate_pre_0_3_5_rc1_dev(current_version, settings_dict, default_settings_p cura["speed_travel"] = None if "movement_speed" not in printer or printer["movement_speed"] is None else float(printer["movement_speed"]) * speed_multiplier cura["max_feedrate_z_override"] = None if "maximum_z_speed" not in printer or printer["maximum_z_speed"] is None else float(printer["maximum_z_speed"]) * speed_multiplier cura["retraction_hop"] = None if "z_hop" not in printer or printer["z_hop"] is None else float(printer["z_hop"]) + cura["retraction_enable"] = False if cura["retraction_amount"] is None or cura["retraction_amount"] <= 0 else True + cura["retraction_hop_enabled"] = False if not cura["retraction_enable"] or not cura["retraction_hop"] or float(cura["retraction_hop"]) < 0 else True printer['slicers']['cura'] = cura elif slicer_type == "other": ## other slicer settings @@ -248,8 +364,11 @@ def migrate_pre_0_3_5_rc1_dev(current_version, settings_dict, default_settings_p # set the current trigger profile profiles['current_trigger_profile_guid'] = settings_dict['current_snapshot_profile_guid'] + + # Remove python 2 support + # for key, default_profile in six.iteritems(default_settings["profiles"]["triggers"]): # add all of the new triggers (the non-real time triggers) - for key, default_profile in six.iteritems(default_settings["profiles"]["triggers"]): + for key, default_profile in default_settings["profiles"]["triggers"].items(): if default_profile["trigger_type"] != 'real-time': # add this setting, it's new! profiles["triggers"][key] = default_profile @@ -283,6 +402,10 @@ def migrate_pre_0_3_5_rc1_dev(current_version, settings_dict, default_settings_p # UPGRADE CAMERA PROFILES. The structure has changed quite a bit. default_camera_profile = default_settings["profiles"]["defaults"]["camera"] for camera in settings_dict['cameras']: + if camera["camera_type"] == "external-script": + camera["camera_type"] = "script" + elif camera["camera_type"] == "printer-gcode": + camera["camera_type"] = "gcode" camera['webcam_settings'] = { "white_balance_temperature": camera['white_balance_temperature'], "sharpness": camera['sharpness'], @@ -339,8 +462,11 @@ def migrate_pre_0_3_5_rc1_dev(current_version, settings_dict, default_settings_p del camera['powerline_frequency'], profiles['cameras'][camera['guid']] = camera + # Remove python 2 support + # for key, default_profile in six.iteritems(default_settings['profiles']['debug']): + # UPGRADE THE DEBUG PROFILES - remove and replace the debug profiles, they are too different to salvage - for key, default_profile in six.iteritems(default_settings['profiles']['debug']): + for key, default_profile in default_settings['profiles']['debug'].items(): profiles['debug'][key] = default_profile # set the default debug profile profiles["current_debug_profile_guid"] = default_settings['profiles']['current_debug_profile_guid'] @@ -395,7 +521,9 @@ def migrate_pre_0_3_5_rc1_dev(current_version, settings_dict, default_settings_p for profile_default in profile_default_types: profile_type = profile_default["profile_type"] default_profile_name = profile_default["default_profile_name"] - for key, profile in six.iteritems(profiles[profile_type]): + # Remove python 2 support + # for key, profile in six.iteritems(profiles[profile_type]): + for key, profile in profiles[profile_type].items(): defaults_copy = copy.deepcopy(default_settings["profiles"]["defaults"][default_profile_name]) defaults_copy.update(profile) profiles[profile_type][key] = defaults_copy @@ -408,6 +536,228 @@ def migrate_pre_0_3_5_rc1_dev(current_version, settings_dict, default_settings_p } +def migrate_pre_0_4_0_rc1_dev2(current_version, settings_dict, default_settings_path): + default_settings = get_default_settings(default_settings_path) + # remove all triggers + settings_dict["profiles"]["triggers"] = {} + # add the default triggers + triggers = default_settings["profiles"]["triggers"] + # Remove python 2 support + # for key, trigger in six.iteritems(triggers): + for key, trigger in triggers.items(): + # Add default triggers + settings_dict["profiles"]["triggers"][key] = trigger + + # set the current trigger + settings_dict["profiles"]["current_trigger_profile_guid"] = default_settings["profiles"]["current_trigger_profile_guid"] + + settings_dict["main_settings"]["version"] = "0.4.0rc1.dev2" + return settings_dict + + +def migrate_pre_0_4_0_rc1_dev3(current_version, settings_dict, default_settings_path): + # adjust each slicer profile to account for the new multiple extruder settings + printers = settings_dict["profiles"]["printers"] + default_settings = get_default_settings(default_settings_path) + # Remove python 2 support + # for key, printer in six.iteritems(printers): + for key, printer in printers.items(): + + # Adjust gcode generation settings + if "gcode_generation_settings" in printer: + gen = printer["gcode_generation_settings"] + extruder = { + "retract_before_move": gen.get('retract_before_move', None), + "retraction_length": gen.get('retraction_length', None), + "retraction_speed": gen.get('retraction_speed', None), + "deretraction_speed": gen.get('deretraction_speed', None), + "lift_when_retracted": gen.get('lift_when_retracted', None), + "z_lift_height": gen.get('z_lift_height', None), + "x_y_travel_speed": gen.get('x_y_travel_speed', None), + "first_layer_travel_speed": gen.get('first_layer_travel_speed', None), + "z_lift_speed": gen.get('z_lift_speed', None) + } + gen["extruders"] = [extruder] + gen.pop("retract_before_move", None) + gen.pop("retraction_length", None) + gen.pop("retraction_speed", None) + gen.pop("deretraction_speed", None) + gen.pop("lift_when_retracted", None) + gen.pop("z_lift_height", None) + gen.pop("x_y_travel_speed", None) + gen.pop("first_layer_travel_speed", None) + gen.pop("z_lift_speed", None) + + # Adjust slicer settings + slicers = printer["slicers"] + # Adjust Cura Settings + if "cura" in slicers: + cura = slicers["cura"] + cura_extruder = { + "version": cura.get("version", None), + "speed_z_hop": cura.get("speed_z_hop", None), + "max_feedrate_z_override": cura.get("max_feedrate_z_override", None), + "retraction_amount": cura.get("retraction_amount", None), + "retraction_hop": cura.get("retraction_hop", None), + "retraction_hop_enabled": cura.get("retraction_hop_enabled", None), + "retraction_enable": cura.get("retraction_enable", None), + "retraction_speed": cura.get("retraction_speed", None), + "retraction_retract_speed": cura.get("retraction_retract_speed", None), + "retraction_prime_speed": cura.get("retraction_prime_speed", None), + "speed_travel": cura.get("speed_travel", None), + } + cura["machine_extruder_count"] = 1 + cura["extruders"] = [cura_extruder] + cura.pop("speed_z_hop", None) + cura.pop("max_feedrate_z_override", None) + cura.pop("retraction_amount", None) + cura.pop("retraction_hop", None) + cura.pop("retraction_hop_enabled", None) + cura.pop("retraction_enable", None) + cura.pop("retraction_speed", None) + cura.pop("retraction_retract_speed", None) + cura.pop("retraction_prime_speed", None) + cura.pop("speed_travel", None) + + if "other" in slicers: + # Adjust Other Slicer Settings + other = slicers["other"] + retract_length = other.get("retract_length", None) + retract_before_move = other.get("retract_before_move", None) + z_hop = other.get("z_hop", None) + lift_when_retracted = other.get("lift_when_retracted", None) + retract_before_move = ( + retract_length > 0 if retract_before_move is None else retract_before_move + ) + lift_when_retracted = ( + retract_before_move and z_hop > 0 if lift_when_retracted is None else lift_when_retracted + ) + + other_extruder = { + "retract_length": retract_length, + "z_hop": z_hop, + "retract_speed": other.get("retract_speed", None), + "deretract_speed": other.get("deretract_speed", None), + "lift_when_retracted": lift_when_retracted, + "travel_speed": other.get("travel_speed", None), + "z_travel_speed": other.get("z_travel_speed", None), + "retract_before_move": retract_before_move + } + other["extruders"] = [other_extruder] + other.pop("retract_length", None) + other.pop("z_hop", None) + other.pop("retract_speed", None) + other.pop("deretract_speed", None) + other.pop("travel_speed", None) + other.pop("z_travel_speed", None) + other.pop("lift_when_retracted", None) + other.pop("retract_before_move", None) + + if "simplify_3d" in slicers: + # Adjust Simplify3D Settings + simplify = slicers["simplify_3d"] + simplify_extruder = { + 'retraction_distance': simplify.get("retraction_distance", None), + 'retraction_vertical_lift': simplify.get("retraction_vertical_lift", None), + 'retraction_speed': simplify.get("retraction_speed", None), + 'extruder_use_retract': simplify.get("extruder_use_retract", None), + } + simplify.pop("extruders", None) + simplify.pop("retraction_distance", None) + simplify.pop("retraction_vertical_lift", None) + simplify.pop("retraction_speed", None) + simplify.pop("extruder_use_retract", None) + + if "slic3r_pe" in slicers: + # Adjust Slic3r Settings + slic3r_pe = slicers["slic3r_pe"] + if "retract_before_travel" in slic3r_pe: + del slic3r_pe["retract_before_travel"] + slic3r_extruder = { + "retract_length": slic3r_pe.get("retract_length", None), + "retract_lift": slic3r_pe.get("retract_lift", None), + "retract_speed": slic3r_pe.get("retract_speed", None), + "deretract_speed": slic3r_pe.get("deretract_speed", None), + } + slic3r_pe["extruders"] = [slic3r_extruder] + slic3r_pe.pop("retract_length", None) + slic3r_pe.pop("retract_lift", None) + slic3r_pe.pop("retract_speed", None) + slic3r_pe.pop("deretract_speed", None) + + renderings = settings_dict["profiles"]["renderings"] + # Remove python 2 support + # for key, render in six.iteritems(renderings): + for key, render in renderings.items(): + render["archive_snapshots"] = not render.get("cleanup_after_render_complete", True) + render.pop("cleanup_after_render_complete",None) + render.pop("cleanup_after_render_fail",None) + render.pop("snapshot_to_skip_end", None) + render.pop("snapshots_to_skip_end", None) + render.pop("snapshots_to_skip_beginning", None) + + triggers = settings_dict["profiles"]["triggers"] + # Remove python 2 support + #for key, trigger in six.iteritems(triggers): + for key, trigger in triggers.items(): + if "trigger_type" in trigger and trigger["trigger_type"] == 'smart-layer': + trigger["trigger_type"] = "smart" + + # Reset the debug profiles to the defaults + if "debug" in settings_dict["profiles"]: + del settings_dict["profiles"]["debug"] + if "current_debug_profile_guid" in settings_dict["profiles"]: + del settings_dict["profiles"]["current_debug_profile_guid"] + settings_dict["profiles"]["logging"] = default_settings["profiles"]["logging"] + settings_dict["profiles"]["current_logging_profile_guid"] = default_settings["profiles"]["current_logging_profile_guid"] + + # set the version + settings_dict["main_settings"]["version"] = "0.4.0rc1.dev3" + return settings_dict + + +def migrate_pre_0_4_0_rc1_dev4(current_version, settings_dict, default_settings_path): + settings_dict["main_settings"]["version"] = "0.4.0rc1.dev4" + return settings_dict + +def migrate_pre_0_4_0(current_version, settings_dict, default_settings_path): + # Switch to Settings Version for migration starting at "0.4.0" + default_settings = get_default_settings(default_settings_path) + + # reset stabilizations + settings_dict["profiles"]["stabilizations"] = default_settings["profiles"]["stabilizations"] + settings_dict["profiles"]["current_stabilization_profile_guid"] = default_settings["profiles"][ + "current_stabilization_profile_guid"] + + # reset renderings + settings_dict["profiles"]["renderings"] = default_settings["profiles"]["renderings"] + settings_dict["profiles"]["current_rendering_profile_guid"] = default_settings["profiles"][ + "current_rendering_profile_guid"] + + # Add 'Smart - Gcode' trigger + smart_gcode_trigger = default_settings["profiles"]["triggers"].get("b838fe36-1459-4867-8243-ab7604cf0e2d", None) + if smart_gcode_trigger is not None: + settings_dict["profiles"]["triggers"]["b838fe36-1459-4867-8243-ab7604cf0e2d"] = smart_gcode_trigger + + # Add the script camera debug logging profile + script_camera_debug_logging = default_settings["profiles"]["logging"].get( + "fa759ab9-bc02-4fd0-8d12-a7c5c89591c1", None + ) + if smart_gcode_trigger is not None: + settings_dict["profiles"]["logging"]["fa759ab9-bc02-4fd0-8d12-a7c5c89591c1"] = script_camera_debug_logging + + return settings_dict + +def migrate_pre_0_4_3(current_version, settings_dict, default_settings_path): + # Switch to Settings Version for migration starting at "0.4.0" + default_settings = get_default_settings(default_settings_path) + + # add the allow_smart_snapshot_commands setting to all triggers with the value True + for trigger in settings_dict["profiles"]["triggers"]: + trigger["allow_smart_snapshot_commands"] = True + return settings_dict + + def get_default_settings(default_settings_path): with open(default_settings_path) as defaultSettingsJson: data = json.load(defaultSettingsJson) diff --git a/octoprint_octolapse/position.py b/octoprint_octolapse/position.py index dc87abfa..29067974 100644 --- a/octoprint_octolapse/position.py +++ b/octoprint_octolapse/position.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -21,396 +21,21 @@ # following email address: FormerLurker@pm.me ################################################################################## from __future__ import unicode_literals -import math import octoprint_octolapse.utility as utility -from octoprint_octolapse.settings import OctolapseGcodeSettings, PrinterProfile -from octoprint_octolapse.gcode_parser import ParsedCommand -import GcodePositionProcessor +from octoprint_octolapse.settings import OctolapseGcodeSettings +# remove unused import +# from octoprint_octolapse.gcode_processor import GcodeProcessor, Pos, Extruder +from octoprint_octolapse.gcode_processor import GcodeProcessor, Pos # create the module level logger from octoprint_octolapse.log import LoggingConfigurator logging_configurator = LoggingConfigurator() logger = logging_configurator.get_logger(__name__) -import collections -class Pos(object): - # Add slots for faster copy and init - __slots__ = [ - "parsed_command", "f", "x", "x_offset", "x_homed", "y", "y_offset", "y_homed", "z", "z_offset", "z_homed", - "e", "e_offset", "is_relative", "is_extruder_relative", "is_metric", "last_extrusion_height", "layer", - "height", "is_printer_primed", "is_in_position", "in_path_position", - "is_xy_travel", "is_xyz_travel","firmware_retraction_length", - "firmware_unretraction_additional_length", "firmware_retraction_feedrate", "firmware_unretraction_feedrate", - "firmware_z_lift", "has_homed_position", "is_layer_change", "is_height_change", "is_zhop", - "has_xy_position_changed", "has_position_changed", "has_state_changed", "has_received_home_command", - "has_position_error", "position_error", 'e_relative', 'z_relative', 'extrusion_length', 'extrusion_length_total', - 'retraction_length','deretraction_length', 'is_extruding_start', 'is_extruding', 'is_primed', - 'is_retracting_start', 'is_retracting', 'is_retracted', 'is_partially_retracted', 'is_deretracting_start', - 'is_deretracting', 'is_deretracted', 'file_line_number', 'gcode_number', "is_in_bounds" - ] - - min_length_to_retract = 0.0001 - - def __init__(self, copy_from_pos=None, reset_state=False): - - if copy_from_pos is not None: - self.parsed_command = copy_from_pos.parsed_command - self.f = copy_from_pos.f - self.x = copy_from_pos.x - self.x_offset = copy_from_pos.x_offset - self.x_homed = copy_from_pos.x_homed - self.y = copy_from_pos.y - self.y_offset = copy_from_pos.y_offset - self.y_homed = copy_from_pos.y_homed - self.z = copy_from_pos.z - self.z_offset = copy_from_pos.z_offset - self.z_homed = copy_from_pos.z_homed - self.e = copy_from_pos.e - self.e_offset = copy_from_pos.e_offset - self.is_relative = copy_from_pos.is_relative - self.is_extruder_relative = copy_from_pos.is_extruder_relative - self.is_metric = copy_from_pos.is_metric - self.last_extrusion_height = copy_from_pos.last_extrusion_height - self.layer = copy_from_pos.layer - self.height = copy_from_pos.height - self.is_printer_primed = copy_from_pos.is_printer_primed - self.firmware_retraction_length = copy_from_pos.firmware_retraction_length - self.firmware_unretraction_additional_length = copy_from_pos.firmware_unretraction_additional_length - self.firmware_retraction_feedrate = copy_from_pos.firmware_retraction_feedrate - self.firmware_unretraction_feedrate = copy_from_pos.firmware_unretraction_feedrate - self.firmware_z_lift = copy_from_pos.firmware_z_lift - self.has_position_error = copy_from_pos.has_position_error - self.position_error = copy_from_pos.position_error - self.has_homed_position = copy_from_pos.has_homed_position - - # Extruder Tracking - self.e_relative = copy_from_pos.e_relative - self.z_relative = copy_from_pos.z_relative - self.extrusion_length = copy_from_pos.extrusion_length - self.extrusion_length_total = copy_from_pos.extrusion_length_total - self.retraction_length = copy_from_pos.retraction_length - self.deretraction_length = copy_from_pos.deretraction_length - self.is_extruding_start = copy_from_pos.is_extruding_start - self.is_extruding = copy_from_pos.is_extruding - self.is_primed = copy_from_pos.is_primed - self.is_retracting_start = copy_from_pos.is_retracting_start - self.is_retracting = copy_from_pos.is_retracting - self.is_retracted = copy_from_pos.is_retracted - self.is_partially_retracted = copy_from_pos.is_partially_retracted - self.is_deretracting_start = copy_from_pos.is_deretracting_start - self.is_deretracting = copy_from_pos.is_deretracting - self.is_deretracted = copy_from_pos.is_deretracted - self.is_in_position = copy_from_pos.is_in_position - - self.is_zhop = copy_from_pos.is_zhop - self.file_line_number = copy_from_pos.file_line_number - self.gcode_number = copy_from_pos.gcode_number - ####### - if reset_state: - # Resets state changes - self.is_layer_change = False - self.is_height_change = False - self.is_xy_travel = False - self.is_xyz_travel = False - self.has_xy_position_changed = False - self.has_position_changed = False - self.has_state_changed = False - self.has_received_home_command = False - self.is_in_bounds = True - self.in_path_position = False - else: - # Copy or default states - self.is_layer_change = copy_from_pos.is_layer_change - self.is_height_change = copy_from_pos.is_height_change - self.is_xy_travel = copy_from_pos.is_xy_travel - self.is_xyz_travel = copy_from_pos.is_xyz_travel - self.has_xy_position_changed = copy_from_pos.has_xy_position_changed - self.has_position_changed = copy_from_pos.has_position_changed - self.has_state_changed = copy_from_pos.has_state_changed - self.has_received_home_command = copy_from_pos.has_received_home_command - self.is_in_bounds = copy_from_pos.is_in_bounds - self.in_path_position = copy_from_pos.in_path_position - - else: - self.parsed_command = None - self.f = None - self.x = None - self.x_offset = 0 - self.x_homed = False - self.y = None - self.y_offset = 0 - self.y_homed = False - self.z = None - self.z_offset = 0 - self.z_homed = False - self.z_relative = 0 - self.e = 0 - self.e_offset = 0 - self.e_relative = 0 - self.is_relative = False - self.is_extruder_relative = False - self.is_metric = True - self.last_extrusion_height = None - self.layer = 0 - self.height = 0 - self.is_printer_primed = False - self.firmware_retraction_length = None - self.firmware_unretraction_additional_length = None - self.firmware_retraction_feedrate = None - self.firmware_unretraction_feedrate = None - self.firmware_z_lift = None - self.has_position_error = False - self.position_error = None - self.has_homed_position = False - - # Extruder Tracking - - self.extrusion_length = 0.0 - self.extrusion_length_total = 0.0 - self.retraction_length = 0.0 - self.deretraction_length = 0.0 - self.is_extruding_start = False - self.is_extruding = False - self.is_primed = False - self.is_retracting_start = False - self.is_retracting = False - self.is_retracted = False - self.is_partially_retracted = False - self.is_deretracting_start = False - self.is_deretracting = False - self.is_deretracted = False - - # Resets state changes - self.is_layer_change = False - self.is_height_change = False - self.is_xy_travel = False - self.is_xyz_travel = False - self.is_zhop = False - self.has_xy_position_changed = False - self.has_position_changed = False - self.has_state_changed = False - self.has_received_home_command = False - self.is_in_position = False - self.in_path_position = False - self.file_line_number = -1 - self.gcode_number = -1 - self.is_in_bounds = True - - @classmethod - def copy_from_cpp_pos(cls, cpp_pos, target): - target.x = None if cpp_pos[53] > 0 else cpp_pos[0] - target.y = None if cpp_pos[54] > 0 else cpp_pos[1] - target.z = None if cpp_pos[55] > 0 else cpp_pos[2] - target.f = None if cpp_pos[56] > 0 else cpp_pos[3] - target.e = cpp_pos[4] - target.x_offset = cpp_pos[5] - target.y_offset = cpp_pos[6] - target.z_offset = cpp_pos[7] - target.e_offset = cpp_pos[8] - target.e_relative = cpp_pos[9] - target.z_relative = cpp_pos[10] - target.extrusion_length = cpp_pos[11] - target.extrusion_length_total = cpp_pos[12] - target.retraction_length = cpp_pos[13] - target.deretraction_length = cpp_pos[14] - target.last_extrusion_height = None if cpp_pos[59] > 0 else cpp_pos[15] - target.height = cpp_pos[16] - target.firmware_retraction_length = None if cpp_pos[61] > 0 else cpp_pos[17] - target.firmware_unretraction_additional_length = None if cpp_pos[62] > 0 else cpp_pos[18] - target.firmware_retraction_feedrate = None if cpp_pos[63] > 0 else cpp_pos[19] - target.firmware_unretraction_feedrate = None if cpp_pos[64] > 0 else cpp_pos[20] - target.firmware_z_lift = None if cpp_pos[66] > 0 else cpp_pos[21] - target.layer = cpp_pos[22] - target.x_homed = cpp_pos[23] > 0 - target.y_homed = cpp_pos[24] > 0 - target.z_homed = cpp_pos[25] > 0 - target.is_relative = None if cpp_pos[57] > 0 else cpp_pos[26] > 0 - target.is_extruder_relative = None if cpp_pos[58] > 0 else cpp_pos[27] > 0 - target.is_metric = None if cpp_pos[60] > 0 else cpp_pos[28] > 0 - target.is_printer_primed = cpp_pos[29] > 0 - target.has_position_error = cpp_pos[30] > 0 - target.has_homed_position = cpp_pos[31] > 0 - target.is_extruding_start = cpp_pos[32] > 0 - target.is_extruding = cpp_pos[33] > 0 - target.is_primed = cpp_pos[34] > 0 - target.is_retracting_start = cpp_pos[35] > 0 - target.is_retracting = cpp_pos[36] > 0 - target.is_retracted = cpp_pos[37] > 0 - target.is_partially_retracted = cpp_pos[38] > 0 - target.is_deretracting_start = cpp_pos[39] > 0 - target.is_deretracting = cpp_pos[40] > 0 - target.is_deretracted = cpp_pos[41] > 0 - target.is_layer_change = cpp_pos[42] > 0 - target.is_height_change = cpp_pos[43] > 0 - target.is_xy_travel = cpp_pos[44] > 0 - target.is_xyz_travel = cpp_pos[45] > 0 - target.is_zhop = cpp_pos[46] > 0 - target.has_xy_position_changed = cpp_pos[47] > 0 - target.has_position_changed = cpp_pos[48] > 0 - target.has_state_changed = cpp_pos[49] > 0 - target.has_received_home_command = cpp_pos[50] > 0 - # Todo: figure out how to deal with these things which must be currently ignored - #target.is_in_position = cpp_pos[51] > 0 - #target.in_path_position = cpp_pos[52] > 0 - target.file_line_number = cpp_pos[66] - target.gcode_number = cpp_pos[67] - target.is_in_bounds = cpp_pos[68] > 0 - fast_position = cpp_pos[69] - if fast_position is not None: - target.parsed_command = ParsedCommand(fast_position[0], fast_position[1], fast_position[2]) - else: - target.parsed_command = None - - return target - - @classmethod - def create_from_cpp_pos(cls, cpp_pos): - pos = Pos() - Pos.copy_from_cpp_pos(cpp_pos, pos) - return pos - - def to_extruder_state_dict(self): - return { - "e": self.e_relative, - "extrusion_length": self.extrusion_length, - "extrusion_length_total": self.extrusion_length_total, - "retraction_length": self.retraction_length, - "deretraction_length": self.deretraction_length, - "is_extruding_start": self.is_extruding_start, - "is_extruding": self.is_extruding, - "is_primed": self.is_primed, - "is_retracting_start": self.is_retracting_start, - "is_retracting": self.is_retracting, - "is_retracted": self.is_retracted, - "is_partially_retracted": self.is_partially_retracted, - "is_deretracting_start": self.is_deretracting_start, - "is_deretracting": self.is_deretracting, - "is_deretracted": self.is_deretracted, - "has_changed": self.has_state_changed - } - - def to_state_dict(self): - return { - "gcode": "" if self.parsed_command is None else self.parsed_command.gcode, - "x_homed": self.x_homed, - "y_homed": self.y_homed, - "z_homed": self.z_homed, - "is_layer_change": self.is_layer_change, - "is_height_change": self.is_height_change, - "is_zhop": self.is_zhop, - "is_relative": self.is_relative, - "is_extruder_relative": self.is_extruder_relative, - "is_metric": self.is_metric, - "layer": self.layer, - "height": self.height, - "last_extrusion_height": self.last_extrusion_height, - "is_in_position": self.is_in_position, - "in_path_position": self.in_path_position, - "is_printer_primed": self.is_printer_primed, - "has_position_error": self.has_position_error, - "position_error": self.position_error, - "has_received_home_command": self.has_received_home_command, - "is_xy_travel": self.is_xy_travel, - "is_in_bounds": self.is_in_bounds - } - - def to_position_dict(self): - return { - "F": self.f, - "x": self.x, - "x_offset": self.x_offset, - "y": self.y, - "y_offset": self.y_offset, - "z": self.z, - "z_offset": self.z_offset, - "e": self.e, - "e_offset": self.e_offset - } - - def to_dict(self): - return { - "gcode": "" if self.parsed_command is None else self.parsed_command.gcode, - "f": self.f, - "x": self.x, - "x_offset": self.x_offset, - "x_homed": self.x_homed, - "y": self.y, - "y_offset": self.y_offset, - "y_homed": self.y_homed, - "z": self.z, - "z_offset": self.z_offset, - "z_homed": self.z_homed, - "z_relative": self.z_relative, - "e": self.e, - "e_offset": self.e_offset, - "is_relative": self.is_relative, - "is_extruder_relative": self.is_extruder_relative, - "is_metric": self.is_metric, - "last_extrusion_height": self.last_extrusion_height, - "is_layer_change": self.is_layer_change, - "is_zhop": self.is_zhop, - "is_in_position": self.is_in_position, - "in_path_position": self.in_path_position, - "is_primed": self.is_primed, - "has_position_error": self.has_position_error, - "position_error": self.position_error, - "has_xy_position_changed": self.has_xy_position_changed, - "has_position_changed": self.has_position_changed, - "has_state_changed": self.has_state_changed, - "layer": self.layer, - "height": self.height, - "has_received_home_command": self.has_received_home_command, - "file_line_number": self.file_line_number, - "gcode_number": self.gcode_number, - "is_in_bounds": self.is_in_bounds - } - - def distance_to_zlift(self, z_hop, restrict_lift_height=True): - amount_to_lift = ( - None if self.z is None or - self.last_extrusion_height is None - else z_hop - (self.z - self.last_extrusion_height) - ) - if restrict_lift_height: - if amount_to_lift < utility.FLOAT_MATH_EQUALITY_RANGE: - return 0 - elif amount_to_lift > z_hop: - return z_hop - return utility.round_to(amount_to_lift, utility.FLOAT_MATH_EQUALITY_RANGE) - - def length_to_retract(self, amount_to_retract): - # if we don't have any history, we want to retract - retract_length = utility.round_to_float_equality_range( - utility.round_to_float_equality_range(amount_to_retract - self.retraction_length) - ) - if retract_length < 0: - retract_length = 0 - elif retract_length > amount_to_retract: - retract_length = amount_to_retract - elif retract_length < Pos.min_length_to_retract: - # we don't want to retract less than the min_length_to_retract, - # else we might have quality issues! - retract_length = 0 - # return the calculated retraction length - return retract_length - - def offset_x(self): - return self.x - self.x_offset - - def offset_y(self): - return self.y - self.y_offset - - def offset_z(self): - return self.z - self.z_offset - - def offset_e(self): - return self.e - self.e_offset - -class Position(object): +class Position(utility.JsonSerializable): def __init__(self, printer_profile, trigger_profile, overridable_printer_profile_settings): # This key is used to call the unique gcode_position object for Octolapse. # This way multiple position trackers can be used at the same time with no # ill effects. - self.key = "plugin_octolapse" self.g90_influences_extruder = overridable_printer_profile_settings["g90_influences_extruder"] self.overridable_printer_profile_settings = overridable_printer_profile_settings @@ -435,14 +60,13 @@ def __init__(self, printer_profile, trigger_profile, overridable_printer_profile # if "G162" not in self._location_detection_commands: # self._location_detection_commands.append("G162") self._gcode_generation_settings = printer_profile.get_current_state_detection_settings() - cpp_position_args = printer_profile.get_position_args(overridable_printer_profile_settings) - GcodePositionProcessor.Initialize(self.key, cpp_position_args) + GcodeProcessor.initialize_position_processor(cpp_position_args) + self._auto_detect_position = printer_profile.auto_detect_position self._priming_height = printer_profile.priming_height self._position_restrictions = None if trigger_profile is None else trigger_profile.position_restrictions - self._retraction_length = self._gcode_generation_settings.retraction_length self._has_restricted_position = False if trigger_profile is None else ( len(trigger_profile.position_restrictions) > 0 and trigger_profile.position_restrictions_enabled @@ -450,37 +74,15 @@ def __init__(self, printer_profile, trigger_profile, overridable_printer_profile self._gcode_generation_settings = printer_profile.get_current_state_detection_settings() assert (isinstance(self._gcode_generation_settings, OctolapseGcodeSettings)) - self._z_lift_height = ( - 0 if self._gcode_generation_settings.z_lift_height is None - else self._gcode_generation_settings.z_lift_height - ) self._priming_height = printer_profile.priming_height self._minimum_layer_height = printer_profile.minimum_layer_height - current_pos_cpp = GcodePositionProcessor.GetCurrentPositionTuple(self.key) - previous_pos_cpp = GcodePositionProcessor.GetPreviousPositionTuple(self.key) - - self.current_pos = Pos.create_from_cpp_pos(current_pos_cpp) - self.previous_pos = Pos.create_from_cpp_pos(current_pos_cpp) - self.undo_pos = Pos() - Pos.copy_from_cpp_pos(previous_pos_cpp, self.undo_pos) + self.current_pos = GcodeProcessor.get_current_position() + self.previous_pos = GcodeProcessor.get_previous_position() + self.undo_pos = GcodeProcessor.get_current_position() def update_position(self, x, y, z, e, f): - cpp_pos = GcodePositionProcessor.UpdatePosition( - self.key, - 0.0 if x is None else x, - True if x is None else False, - 0.0 if y is None else y, - True if y is None else False, - 0.0 if z is None else z, - True if z is None else False, - 0.0 if e is None else e, - True if e is None else False, - 0.0 if f is None else f, - True if f is None else False, - ) - - Pos.copy_from_cpp_pos(cpp_pos, self.current_pos) + GcodeProcessor.update_position(self.current_pos, x, y, z, e, f) def to_position_dict(self): ret_dict = self.current_pos.to_dict() @@ -489,34 +91,6 @@ def to_position_dict(self): def to_state_dict(self): return self.current_pos.to_state_dict() - def distance_to_zlift(self): - # get the lift amount, but don't restrict it so we can log properly - return self.current_pos.distance_to_zlift(self._z_lift_height, True) - - def length_to_retract(self): - return self.current_pos.length_to_retract(self._retraction_length) - - # TODO: do we need this? - def x_relative_to_current(self, x): - if x: - return x - self.current_pos.x + self.current_pos.x_offset - else: - return self.current_pos.x - self.previous_pos.x - - # TODO: do we need this? - def y_relative_to_current(self, y): - if y: - return y - self.current_pos.y + self.current_pos.y_offset - else: - return self.current_pos.y - self.previous_pos.y - - # TODO: do we need this? - def e_relative_to_current(self, e): - if e: - return e - self.current_pos.e + self.current_pos.e_offset - else: - return self.current_pos.e - self.previous_pos.e - def command_requires_location_detection(self, cmd): if self._auto_detect_position: if cmd in self._location_detection_commands: @@ -524,8 +98,7 @@ def command_requires_location_detection(self, cmd): return False def undo_update(self): - - GcodePositionProcessor.Undo(self.key) + GcodeProcessor.undo() # set pos to the previous pos and pop the current position if self.undo_pos is None: raise Exception("Cannot undo updates when there is less than one position in the position queue.") @@ -536,67 +109,7 @@ def undo_update(self): self.undo_pos = None return previous_position - @staticmethod - def copy_pos(source, target): - """does not copy all items, only what is necessary for the Position.Update command""" - target.f = source.f - target.x = source.x - target.x_offset = source.x_offset - target.x_homed = source.x_homed - target.y = source.y - target.y_offset = source.y_offset - target.y_homed = source.y_homed - target.z = source.z - target.z_offset = source.z_offset - target.z_homed = source.z_homed - target.e = source.e - target.e_offset = source.e_offset - target.is_relative = source.is_relative - target.is_extruder_relative = source.is_extruder_relative - target.is_metric = source.is_metric - target.last_extrusion_height = source.last_extrusion_height - target.layer = source.layer - target.height = source.height - target.is_printer_primed = source.is_printer_primed - target.firmware_retraction_length = source.firmware_retraction_length - target.firmware_unretraction_additional_length = source.firmware_unretraction_additional_length - target.firmware_retraction_feedrate = source.firmware_retraction_feedrate - target.firmware_unretraction_feedrate = source.firmware_unretraction_feedrate - target.firmware_z_lift = source.firmware_z_lift - target.has_position_error = source.has_position_error - target.position_error = source.position_error - target.has_homed_position = source.has_homed_position - target.in_path_position = source.in_path_position - target.is_zhop = source.is_zhop - - # Extruder Tracking - target.e_relative = source.e_relative - target.extrusion_length = source.extrusion_length - target.extrusion_length_total = source.extrusion_length_total - target.retraction_length = source.retraction_length - target.deretraction_length = source.deretraction_length - target.is_extruding_start = source.is_extruding_start - target.is_extruding = source.is_extruding - target.is_primed = source.is_primed - target.is_retracting_start = source.is_retracting_start - target.is_retracting = source.is_retracting - target.is_retracted = source.is_retracted - target.is_partially_retracted = source.is_partially_retracted - target.is_deretracting_start = source.is_deretracting_start - target.is_deretracting = source.is_deretracting - target.is_deretracted = source.is_deretracted - target.is_in_position = source.is_in_position - - # Resets state changes - target.is_layer_change = False - target.is_height_change = False - target.is_xy_travel = False - target.has_position_changed = False - target.has_state_changed = False - target.has_received_home_command = False - target.is_in_bounds = True - - def update(self, gcode): + def update(self, gcode, file_line_number=None): # Move the current position to the previous and the previous to the undo position # then copy previous to current if self.undo_pos is None: @@ -605,16 +118,19 @@ def update(self, gcode): self.undo_pos = self.previous_pos self.previous_pos = self.current_pos self.current_pos = old_undo_pos - # TODO: We probably don't need to copy previous since we'll get the current pos from 'Update' below - Position.copy_pos(self.previous_pos, self.current_pos) + + Pos.copy(self.previous_pos, self.current_pos) + + # process the gcode and update our current position + GcodeProcessor.update(gcode, self.current_pos) + + # fill in the file line number if it is supplied. + if file_line_number is not None: + self.current_pos.file_line_number = file_line_number previous = self.previous_pos current = self.current_pos - # parse the gcode command - cpp_pos = GcodePositionProcessor.Update(self.key, gcode) - current = Pos.copy_from_cpp_pos(cpp_pos, current) - # reset the position restriction in_path_position state since it only works # for one gcode at a time. current.in_path_position = False @@ -839,8 +355,6 @@ def update(self, gcode): # # self._logger.log_position_command_received( # "Received G161 - ".format(" ".join(home_strings))) - # pos.has_position_error = False - # pos.position_error = None # # def _g162_received(self, pos): # # Home @@ -897,8 +411,6 @@ def update(self, gcode): # # self._logger.log_position_command_received( # "Received G162 - ".format(" ".join(home_strings))) - # pos.has_position_error = False - # pos.position_error = None def calculate_path_intersections(self, restrictions, x, y, previous_x, previous_y, can_calculate_intersections): @@ -985,6 +497,15 @@ def _extruder_state_triggered(option, state): return None def is_extruder_triggered(self, options): + return self._is_extruder_triggered(self.current_pos, options) + + def is_previous_extruder_triggered(self, options): + return self._is_extruder_triggered(self.previous_pos, options) + + @staticmethod + def _is_extruder_triggered(pos, options): + assert(isinstance(pos, Pos)) + extruder = pos.get_current_extruder() # if there are no extruder trigger options, return true. if options is None: return True @@ -992,35 +513,35 @@ def is_extruder_triggered(self, options): # Matches the supplied extruder trigger options to the current # extruder state. Returns true if triggering, false if not. - extruding_start_triggered = self._extruder_state_triggered( - options.on_extruding_start, self.current_pos.is_extruding_start + extruding_start_triggered = Position._extruder_state_triggered( + options.on_extruding_start, extruder.is_extruding_start ) - extruding_triggered = self._extruder_state_triggered( - options.on_extruding, self.current_pos.is_extruding + extruding_triggered = Position._extruder_state_triggered( + options.on_extruding, extruder.is_extruding ) - primed_triggered = self._extruder_state_triggered( - options.on_primed, self.current_pos.is_primed + primed_triggered = Position._extruder_state_triggered( + options.on_primed, extruder.is_primed ) - retracting_start_triggered = self._extruder_state_triggered( - options.on_retracting_start, self.current_pos.is_retracting_start + retracting_start_triggered = Position._extruder_state_triggered( + options.on_retracting_start, extruder.is_retracting_start ) - retracting_triggered = self._extruder_state_triggered( - options.on_retracting, self.current_pos.is_retracting + retracting_triggered = Position._extruder_state_triggered( + options.on_retracting, extruder.is_retracting ) - partially_retracted_triggered = self._extruder_state_triggered( - options.on_partially_retracted, self.current_pos.is_partially_retracted + partially_retracted_triggered = Position._extruder_state_triggered( + options.on_partially_retracted, extruder.is_partially_retracted ) - retracted_triggered = self._extruder_state_triggered( - options.on_retracted, self.current_pos.is_retracted + retracted_triggered = Position._extruder_state_triggered( + options.on_retracted, extruder.is_retracted ) - deretracting_start_triggered = self._extruder_state_triggered( - options.on_deretracting_start, self.current_pos.is_deretracting_start + deretracting_start_triggered = Position._extruder_state_triggered( + options.on_deretracting_start, extruder.is_deretracting_start ) - deretracting_triggered = self._extruder_state_triggered( - options.on_deretracting, self.current_pos.is_deretracting + deretracting_triggered = Position._extruder_state_triggered( + options.on_deretracting, extruder.is_deretracting ) - deretracted_triggered = self._extruder_state_triggered( - options.on_deretracted, self.current_pos.is_deretracted + deretracted_triggered = Position._extruder_state_triggered( + options.on_deretracted, extruder.is_deretracted ) ret_value = False diff --git a/octoprint_octolapse/render.py b/octoprint_octolapse/render.py index cbd28267..1b53c0b0 100644 --- a/octoprint_octolapse/render.py +++ b/octoprint_octolapse/render.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -21,30 +21,40 @@ # following email address: FormerLurker@pm.me ################################################################################## from __future__ import unicode_literals +import re import math import os -import shutil -import six import sys import threading -from six.moves import queue -from six import string_types +# Remove python 2 support +# from six.moves import queue +import queue as queue +# remove unused usings +# from six import string_types, iteritems import time import json +import copy +import zipfile as zipfile from csv import DictReader # sarge was added to the additional requirements for the plugin import datetime from tempfile import mkdtemp +import uuid +# Recent versions of Pillow changed the case of the import +# Why!? +try: + from pil import Image, ImageDraw, ImageFont +except ImportError: + from PIL import Image, ImageDraw, ImageFont -import sarge -from PIL import Image, ImageDraw, ImageFont import octoprint_octolapse.utility as utility -from octoprint_octolapse.snapshot import SnapshotMetadata - - +import octoprint_octolapse.script as script +from octoprint_octolapse.snapshot import SnapshotMetadata, CameraInfo +from octoprint_octolapse.settings import OctolapseSettings, CameraProfile, RenderingProfile # create the module level logger from octoprint_octolapse.log import LoggingConfigurator + logging_configurator = LoggingConfigurator() logger = logging_configurator.get_logger(__name__) @@ -53,7 +63,7 @@ def is_rendering_template_valid(template, options): # make sure we have all the replacements we need option_dict = {} for option in options: - option_dict[option] = "F" # use any valid file character, F seems ok + option_dict[option] = "F" # use any valid file character, F seems ok except for time_elapsed try: filename = template.format(**option_dict) except KeyError as e: @@ -73,16 +83,35 @@ def is_rendering_template_valid(template, options): except (IOError, OSError): return False, "The resulting filename is not a valid filename. Most likely an invalid character was used." - shutil.rmtree(temp_directory) + utility.rmtree(temp_directory) return True, "" def is_overlay_text_template_valid(template, options): - # make sure we have all the replacements we need + + # create the options dict option_dict = {} for option in options: option_dict[option] = "F" # use any valid file character, F seems ok + # add/edit time_elapsed, since it needs to be there and have a specific value + + # this must be in milliseconds, use a value of 5 days 5 hours 5 minute 5 seconds and 123 MS + option_dict["time_elapsed"] = (5 * 24 * 60 * 60 + 5 * 60 * 60 + 5 * 60 + 5.123) + # first try to replace any date tokens, else these will cause errors in further checks + success, template = format_overlay_date_templates(template, datetime.datetime.now().timestamp()) + if not success: + # in this case, the template will contain the errors + return False, template + + if "time_elapsed" in option_dict: + success, template = format_overlay_timedelta_templates(template, option_dict["time_elapsed"]) + if not success: + # in this case, the template will contain the errors + return False, template + + # at this point, all date format strings should have been replaced, so + # check the rest by attempting to format try: template.format(**option_dict) except KeyError as e: @@ -95,6 +124,144 @@ def is_overlay_text_template_valid(template, options): return True, "" +OVERLAY_DATE_FORMAT_EXTRACT_REGEX = re.compile(r"{current_time:\"([^\"]*)\"}", re.IGNORECASE) +OVERLAY_DATE_TOKEN_MATCH_REGEX = re.compile(r"({current_time:\"[^\"]*\"})", re.IGNORECASE) + + +def format_overlay_date_templates(template, epoch_time): + date_value = datetime.datetime.fromtimestamp(epoch_time) + # find our templates via regex + matches = OVERLAY_DATE_TOKEN_MATCH_REGEX.findall(template) + for match in matches: + # extract the date format string and test it + date_format_match = OVERLAY_DATE_FORMAT_EXTRACT_REGEX.match(match) + if not date_format_match: + return False, "Unable to find format tokens within date token '{0}'".format(match) + date_format_string = date_format_match.group(1) + + try: + date_string = date_value.strftime(date_format_string) + except ValueError as e: + return False, "Incorrect date format string '{0}'".format(date_format_string) + # Replace this token with the date + template = template.replace(match, date_string, 1) + return True, template + + +OVERLAY_TIMEDELTA_FORMAT_EXTRACT_REGEX = re.compile(r"{time_elapsed:\"([^\"]*)\"}", re.IGNORECASE) +OVERLAY_TIMEDELTA_TOKEN_MATCH_REGEX = re.compile(r"({time_elapsed:\"[^\"]*\"})", re.IGNORECASE) +OVERLAY_TIMEDELTA_INNER_TOKEN_MATCH_REGEX = re.compile(r"(%[DdHhMmSsFf](?:(?::[0-9]+.[0-9]+)|(?::.[0-9]+)|(?::[0-9]+))?)") +# time format tokens: +# %D = total days +# %d = day component +# %H = Total Hours +# %h = Hours Component +# %M = Total Minutes +# %m = Minutes Component +# %S = Total Seconds +# %s = Seconds Component +# %F = Total Milliseconds +# %f = Milliseconds Component +# Note: All tokens can be amended with :.X where x is the number of decimals to display + + +def format_overlay_timedelta_templates(template, time_elapsed_seconds): + # calculate all of the time elapsed values (where time_elapsed is in seconds) + # start with totals + total_milliseconds = time_elapsed_seconds * 1000.0 + total_seconds = total_milliseconds / 1000.0 + total_minutes = total_seconds / 60.0 + total_hours = total_minutes / 60 + total_days = total_hours / 24.0 + # now get components + seconds, milliseconds = divmod(time_elapsed_seconds * 1000, 1000) + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + + # find our templates via regex + matches = OVERLAY_TIMEDELTA_TOKEN_MATCH_REGEX.findall(template) + + for match in matches: + # extract the date format string and test it + timedelta_format_match = OVERLAY_TIMEDELTA_FORMAT_EXTRACT_REGEX.match(match) + if not timedelta_format_match: + return False, "Unable to find format tokens within date token '{0}'".format(match) + timedelta_format_string = timedelta_format_match.group(1) + + # find inner token matches and perform token replacement on each + inner_tokens = OVERLAY_TIMEDELTA_INNER_TOKEN_MATCH_REGEX.findall(timedelta_format_string) + # create a variable to hold the time value + for token in inner_tokens: + # I can't think of a good way to do this other than else/if at the moment + # might revisit. + value = 0 + if token.startswith("%D"): + value = total_days + elif token.startswith("%d"): + value = days + elif token.startswith("%H"): + value = total_hours + elif token.startswith("%h"): + value = hours + elif token.startswith("%M"): + value = total_minutes + elif token.startswith("%m"): + value = minutes + elif token.startswith("%S"): + value = total_seconds + elif token.startswith("%s"): + value = seconds + elif token.startswith("%F"): + value = total_milliseconds + elif token.startswith("%f"): + value = milliseconds + else: + return False, "Unknown time delta format token: {0}".format(token) + + # get the decimal format of the number + inner_token_format = "{0:0.0f}" + if len(token) > 3 and token[2] == ":": + # Extract the format + try: + search_string = token[3:] + decimal_index = search_string.find(".") + right = None + left = None + if decimal_index > -1 and len(search_string) > decimal_index + 1: + right = int(search_string[decimal_index+1:]) + if decimal_index > 0: + left = int(search_string[0:decimal_index]) + else: + left = int(search_string) + + inner_token_format = "{0:0" + if left is not None: + inner_token_format += str(left) + + inner_token_format += "." + + if right is not None: + inner_token_format += str(right) + else: + inner_token_format += "0" + + inner_token_format += "f}" + except ValueError as e: + return False, "Unable to convert time delta parameter to int: {0}".format(token) + + + # format the number + inner_token_value = inner_token_format.format(value) + + # replace the found token + timedelta_format_string = timedelta_format_string.replace(token, inner_token_value, 1) + + # Replace this token with the date + template = template.replace(match, timedelta_format_string, 1) + return True, template + + def preview_overlay(rendering_profile, image=None): if rendering_profile.overlay_font_path is None or len(rendering_profile.overlay_font_path.strip()) == 0: # we don't have any overlay path, return @@ -104,7 +271,7 @@ def preview_overlay(rendering_profile, image=None): overlay_outline_color = rendering_profile.get_overlay_outline_color() overlay_outline_width = rendering_profile.overlay_outline_width if image is None: - image_color = (0,0,0,255) + image_color = (0, 0, 0, 255) if isinstance(overlay_text_color, list): image_color = tuple(255 - c for c in overlay_text_color) # Create an image with background color inverse to the text color. @@ -133,12 +300,28 @@ def draw_center(i, t, overlay_text_color, dx=0, dy=0): image_text_color[3] = 255 image = draw_center(image, "Preview", image_text_color, dy=-20) image = draw_center(image, "Click to refresh", image_text_color, dy=20) - - format_vars = {'snapshot_number': 1234, - 'file_name': 'image.jpg', - 'time_taken': time.time(), - 'current_time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())), - 'time_elapsed': "{}".format(datetime.timedelta(seconds=round(9001)))} + time_elapsed_seconds = 5 * 24 * 60 * 60 + 5 * 60 * 60 + 5 * 60 + 5.123 + format_vars = { + 'snapshot_number': 1234, + 'file_name': 'image.jpg', + 'time_taken': time.time(), + 'current_time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())), + 'time_elapsed': time_elapsed_seconds, + 'time_elapsed_formatted': "{}".format(datetime.timedelta(seconds=round(time_elapsed_seconds))), + 'layer': "53", + 'height': "22.3302", + 'x': "150.33222", + 'y': "-23.0001", + 'z': "15.023", + 'e': "1504.63211", + 'f': "1200", + "x_snapshot": "0.000", + "y_snapshot": "250.000", + "gcode_file": "gcode_file_name.gcode", + "gcode_file_name": "gcode_file_name", + "gcode_file_extension": "gcode", + "print_end_state": "COMPLETED", + } image = TimelapseRenderJob.add_overlay(image, text_template=rendering_profile.overlay_text_template, format_vars=format_vars, @@ -154,461 +337,1680 @@ def draw_center(i, t, overlay_text_color, dx=0, dy=0): return image +# function that returns true if a string is a uuid +def _is_valid_uuid(value): + try: + uuid.UUID(str(value)) + return True + except ValueError: + return False + + class RenderJobInfo(object): def __init__( - self, timelapse_job_info, data_directory, current_camera, rendering, ffmpeg_path, job_number=0, jobs_remaining=0 + self, + job_guid, + camera_guid, + timelapse_job_info, + rendering_profile, + camera_profile, + temporary_directory, + snapshot_archive_directory, + timelapse_directory, + ffmpeg_directory, + current_camera_info, + job_number=0, + jobs_remaining=0 ): + self.ffmpeg_directory = ffmpeg_directory self.timelapse_job_info = timelapse_job_info - self.job_id = timelapse_job_info.JobGuid + self.job_guid = job_guid + self.camera_guid = camera_guid self.job_number = job_number self.jobs_remaining = jobs_remaining - self.camera = current_camera - self.job_directory = os.path.join(data_directory, "snapshots", timelapse_job_info.JobGuid) - self.snapshot_directory = os.path.join(data_directory, "snapshots", timelapse_job_info.JobGuid, current_camera.guid) + self.camera = camera_profile + self.camera_info = current_camera_info + self.temporary_directory = temporary_directory + self.job_directory = utility.get_temporary_snapshot_job_path( + self.temporary_directory, job_guid + ) + self.snapshot_directory = utility.get_temporary_snapshot_job_camera_path( + self.temporary_directory, + job_guid, + camera_guid + ) self.snapshot_filename_format = os.path.basename( utility.get_snapshot_filename( - timelapse_job_info.PrintFileName, timelapse_job_info.PrintStartTime, utility.SnapshotNumberFormat + timelapse_job_info.PrintFileName.replace("%", "%%"), utility.SnapshotNumberFormat ) ) self.pre_roll_snapshot_filename_format = utility.get_pre_roll_snapshot_filename( - timelapse_job_info.PrintFileName, timelapse_job_info.PrintStartTime, utility.SnapshotNumberFormat + timelapse_job_info.PrintFileName.replace("%", "%%"), utility.SnapshotNumberFormat + ) + # rendering directory path + self.output_tokens = self._get_output_tokens(self.temporary_directory) + self.rendering_output_format = rendering_profile.output_format + self.rendering_directory = timelapse_directory + self.rendering_filename = RenderJobInfo.get_rendering_filename( + rendering_profile.output_template, self.output_tokens ) - self.output_tokens = self._get_output_tokens(data_directory) - self.rendering = rendering.clone() - self.ffmpeg_path = ffmpeg_path - self.cleanup_after_complete = rendering.cleanup_after_render_complete - self.cleanup_after_fail = rendering.cleanup_after_render_fail + self.rendering_extension = RenderJobInfo.get_extension_from_output_format(rendering_profile.output_format) + self.rendering_filename_with_extension = "{0}.{1}".format(self.rendering_filename, self.rendering_extension) + self.rendering_path = os.path.join( + self.rendering_directory, self.rendering_filename_with_extension + ) + # snapshot archive path + self.snapshot_archive_directory = snapshot_archive_directory + self.snapshot_archive_filename = utility.get_snapshot_archive_filename(self.rendering_filename) + self.snapshot_archive_path = os.path.join(self.snapshot_archive_directory, self.snapshot_archive_filename) + self.rendering = rendering_profile + self.archive_snapshots = self.rendering.archive_snapshots or not self.rendering.enabled + # store any rendering errors + self.rendering_error = None + + def get_snapshot_name_from_index(self, index): + return utility.get_snapshot_filename( + self.timelapse_job_info.PrintFileName, index + ) + + def get_snapshot_full_path_from_index(self, index): + return os.path.join(self.snapshot_directory, self.get_snapshot_name_from_index(index)) def _get_output_tokens(self, data_directory): job_info = self.timelapse_job_info - assert(isinstance(job_info, utility.TimelapseJobInfo)) + assert (isinstance(job_info, utility.TimelapseJobInfo)) + print_end_time = job_info.PrintEndTime + print_start_time = job_info.PrintStartTime + print_end_state = job_info.PrintEndState + print_file_name = job_info.PrintFileName + camera_name = "UNKNOWN" if not self.camera else self.camera.name + return RenderJobInfo.get_output_tokens( + print_end_time, + print_start_time, + print_end_state, + print_file_name, + camera_name, + ) - return { - "FAILEDFLAG": "FAILED" if job_info.PrintEndState != "COMPLETED" else "", - "FAILEDSEPARATOR": "_" if job_info.PrintEndState != "COMPLETED" else "", - "FAILEDSTATE": "" if job_info.PrintEndState == "COMPLETED" else job_info.PrintEndState, - "PRINTSTATE": job_info.PrintEndState, - "GCODEFILENAME": job_info.PrintFileName, - "PRINTENDTIME": time.strftime("%Y%m%d%H%M%S", time.localtime(job_info.PrintEndTime)), - "PRINTENDTIMESTAMP": "{0:d}".format(math.trunc(round(job_info.PrintEndTime, 2) * 100)), - "PRINTSTARTTIME": time.strftime("%Y%m%d%H%M%S", time.localtime(job_info.PrintStartTime)), - "PRINTSTARTTIMESTAMP": "{0:d}".format(math.trunc(round(job_info.PrintStartTime, 2) * 100)), - "DATETIMESTAMP": "{0:d}".format(math.trunc(round(time.time(), 2) * 100)), - "DATADIRECTORY": data_directory, - "SNAPSHOTCOUNT": 0, - "FPS": 0, + @staticmethod + def get_vcodec_from_output_format(output_format): + VCODECS = {"avi": "mpeg4", + "flv": "flv1", + "gif": "gif", + # "h264": "h264", + "h264": "libx264", + "h265": "libx265", + "mp4": "mpeg4", + "mpeg": "mpeg2video", + "vob": "mpeg2video"} + return VCODECS.get(output_format.lower(), "mpeg2video") + + @staticmethod + def get_extension_from_output_format(output_format): + EXTENSIONS = {"avi": "avi", + "flv": "flv", + "h264": "mp4", + "h265": "mp4", + "vob": "vob", + "mp4": "mp4", + "mpeg": "mpeg", + "gif": "gif"} + return EXTENSIONS.get(output_format.lower(), "mp4") + + @staticmethod + def get_ffmpeg_format_from_output_format(output_format): + EXTENSIONS = {"avi": "avi", + "flv": "flv", + "h264": "mp4", + "h265": "mp4", + "vob": "vob", + "mp4": "mp4", + "mpeg": "mpeg", + "gif": "gif"} + return EXTENSIONS.get(output_format.lower(), "mp4") + + @staticmethod + def get_output_tokens( + print_end_time=None, + print_start_time=None, + print_end_state=None, + print_file_name=None, + camera_name=None + ): + tokens = {} + print_end_time_string = ( + "UNKNOWN" if print_end_time is None + else time.strftime("%Y%m%d%H%M%S", time.localtime(print_end_time)) + ) + tokens["PRINTENDTIME"] = print_end_time_string + print_end_timestamp = ( + "UNKNOWN" if print_end_time is None + else "{0:d}".format(math.trunc(round(print_end_time, 2) * 100)) + ) + tokens["PRINTENDTIMESTAMP"] = print_end_timestamp + print_start_time_string = ( + "UNKNOWN" if print_start_time is None + else time.strftime("%Y%m%d%H%M%S", time.localtime(print_start_time)) + ) + tokens["PRINTSTARTTIME"] = print_start_time_string + print_start_timestamp = { + "UNKNOWN" if print_start_time is None + else "{0:d}".format(math.trunc(round(print_start_time, 2) * 100)) } + tokens["PRINTSTARTTIMESTAMP"] = print_start_timestamp + tokens["DATETIMESTAMP"] = "{0:d}".format(math.trunc(round(time.time(), 2) * 100)) + print_failed = print_end_state not in ["COMPLETED", "UNKNOWN"] + failed_flag = "FAILED" if print_failed else "" + tokens["FAILEDFLAG"] = failed_flag + failed_separator = "_" if print_failed else "" + tokens["FAILEDSEPARATOR"] = failed_separator + failed_state = "UNKNOWN" if not print_end_state else ( + "" if print_end_state == "COMPLETED" else print_end_state + ) + tokens["FAILEDSTATE"] = failed_state + tokens["PRINTSTATE"] = "UNKNOWN" if not print_end_state else print_end_state + tokens["GCODEFILENAME"] = "" if not print_file_name else print_file_name + tokens["SNAPSHOTCOUNT"] = 0 + tokens["CAMERANAME"] = "UNKNOWN" if not camera_name else camera_name + tokens["FPS"] = 0 + return tokens + + @staticmethod + def get_output_tokens_from_metadata(metadata): + print_end_time = metadata["print_end_time"] + print_start_time = metadata["print_start_time"] + print_end_state = metadata["print_end_state"] + print_file_name = metadata["print_file_name"] + camera_name = metadata["camera_name"] + return RenderJobInfo.get_output_tokens( + print_end_time, + print_start_time, + print_end_state, + print_file_name, + camera_name, + ) + @staticmethod + def get_sanitized_rendering_filename(output_template, output_tokens): + return utility.sanitize_filename(output_template.format(**output_tokens)) + + @staticmethod + def get_rendering_filename(output_template, output_tokens): + return output_template.format(**output_tokens) + + @staticmethod + def get_sanitized_rendering_name_from_metadata(metadata): + output_tokens = RenderJobInfo.get_output_tokens_from_metadata(metadata) + return RenderJobInfo.get_sanitized_rendering_filename(metadata["output_template"], output_tokens) + + @staticmethod + def get_rendering_name_from_metadata(metadata): + output_tokens = RenderJobInfo.get_output_tokens_from_metadata(metadata) + return RenderJobInfo.get_rendering_name_from_metadata(metadata["output_template"], output_tokens) class RenderingProcessor(threading.Thread): + """Watch for rendering jobs via a rendering queue. Extract jobs from the queue, and spawn a rendering thread, + one at a time for each rendering job. Notify the calling thread of the number of jobs in the queue on demand.""" + def __init__( - self, rendering_task_queue, data_directory, octoprint_timelapse_folder, - on_prerender_start, on_start, on_success, on_error, on_end + self, rendering_task_queue, data_directory, plugin_version, git_version, default_settings_folder, + octoprint_settings, get_current_settings_callback, on_start, on_success, on_render_progress, on_error, on_end, + on_unfinished_renderings_changed, on_in_process_renderings_changed, on_unfinished_renderings_loaded ): super(RenderingProcessor, self).__init__() - self.lock = threading.Lock() - + self._plugin_version = plugin_version + self._git_version = git_version + self._default_settings_folder = default_settings_folder + self._octoprint_settings = octoprint_settings + self._get_current_settings_callback = get_current_settings_callback + self._temporary_directory = None + self._snapshot_archive_directory = None + self._timelapse_directory = None + self._ffmpeg_directory = None + self.r_lock = threading.RLock() + self.temp_files_lock = threading.RLock() self.rendering_task_queue = rendering_task_queue # make a local copy of everything. self.data_directory = data_directory - self.octoprint_timelapse_folder = octoprint_timelapse_folder - self.on_prerender_start = on_prerender_start - self.on_start = on_start - self.on_success = on_success - self.on_error = on_error - self.on_end = on_end - self.cameras = [] - self.print_state = "unknown" - self.time_added = 0 - #self.daemon = True + self._on_start_callback = on_start + self._on_success_callback = on_success + self._on_render_progress_callback = on_render_progress + self._on_error_callback = on_error + self._on_end_callback = on_end + self._on_unfinished_renderings_changed_callback = on_unfinished_renderings_changed + self._on_in_process_renderings_changed_callback = on_in_process_renderings_changed + self._on_unfinished_renderings_loaded_callback = on_unfinished_renderings_loaded self.job_count = 0 self._is_processing = False + self._idle_sleep_seconds = 5 # wait at most 5 seconds for a rendering job from the queue + self._rendering_job_thread = None + self._current_rendering_job = None + # a private dict of rendering jobs by print job ID and camera ID + self._pending_rendering_jobs = {} + # private vars to hold unfinished and in-process rendering state + self._unfinished_renderings = [] + self._unfinished_renderings_size = 0 + self._renderings_in_process = [] + self._renderings_in_process_size = 0 + self._has_working_directories = False + self.update_directories() + self._last_progress_time_update = 0 def is_processing(self): - with self.lock: - return self._is_processing + with self.r_lock: + return self._has_pending_jobs() or self.rendering_task_queue.qsize() > 0 + + def get_failed(self): + with self.r_lock: + return { + "failed": copy.deepcopy(self._unfinished_renderings), + "failed_size": self._unfinished_renderings_size, + } + + def get_in_process(self): + with self.r_lock: + return { + "in_process": copy.deepcopy(self._renderings_in_process), + "in_process_size": self._renderings_in_process_size, + } + + def update_directories(self): + """Returns true if the temporary directory has changed.""" + with self.r_lock: + + # mame sure the directories are tested + success, errors = self._get_current_settings_callback().main_settings.test_directories( + self.data_directory, + self._octoprint_settings.settings.getBaseFolder("timelapse") + ) + if not success: + return False, errors + + temporary_directory_changed = False + snapshot_archive_directory_changed = False + timelapse_directory_changed = False + ffmpeg_directory_changed = False + + temporary_directory = self._get_current_settings_callback().main_settings.get_temporary_directory( + self.data_directory + ) + + if self._temporary_directory != temporary_directory: + temporary_directory_changed = True + self._temporary_directory = temporary_directory + + snapshot_archive_directory = self._get_current_settings_callback().main_settings.get_snapshot_archive_directory( + self.data_directory + ) + if self._snapshot_archive_directory != snapshot_archive_directory: + snapshot_archive_directory_changed = True + self._snapshot_archive_directory = snapshot_archive_directory + + timelapse_directory = self._get_current_settings_callback().main_settings.get_timelapse_directory( + self._octoprint_settings.settings.getBaseFolder("timelapse") + ) + if self._timelapse_directory != timelapse_directory: + timelapse_directory_changed = True + self._timelapse_directory = timelapse_directory + + ffmpeg_directory = self._octoprint_settings.global_get(["webcam", "ffmpeg"]) + if self._ffmpeg_directory != ffmpeg_directory: + ffmpeg_directory_changed = True + self._ffmpeg_directory = ffmpeg_directory + + self._has_working_directories = True + + if temporary_directory_changed: + self._initialize_unfinished_renderings() + + return True, { + "temporary_directory_changed": temporary_directory_changed, + "snapshot_archive_directory_changed": snapshot_archive_directory_changed, + "timelapse_directory_changed": timelapse_directory_changed, + "ffmpeg_directory_changed": ffmpeg_directory_changed + } + + def set_ffmpeg_directory(self, directory): + if self._ffmpeg_directory != directory: + self._ffmpeg_directory = directory + return True + return False + + def archive_unfinished_job( + self, temporary_directory, job_guid, camera_guid, target_path, is_download=False, + progress_callback=None, progress_key='archiving', progress_current_step=0, progress_total_steps=None + ): + # do not archive if there is a no archive file. This means the rendering was created from + # an archive that already existed. If we are downloading, we don't care about this. + if not is_download and utility.has_no_archive_file(temporary_directory, job_guid, camera_guid): + return None + + with self.temp_files_lock: + job_directory = utility.get_temporary_snapshot_job_path(temporary_directory, job_guid) + camera_directory = utility.get_temporary_snapshot_job_camera_path(temporary_directory, job_guid, + camera_guid) + target_directory = utility.get_directory_from_full_path(target_path) + + # make sure the job and camera directories both exist, else bail! + if not os.path.isdir(job_directory) or not os.path.isdir(camera_directory): + return + + if not os.path.exists(target_directory): + try: + os.makedirs(target_directory) + except OSError: + pass + try: + with zipfile.ZipFile(target_path, mode='w', allowZip64=True) as snapshot_archive: + # add the job info + timelapse_info_path = os.path.join(job_directory, + utility.TimelapseJobInfo.timelapse_info_file_name) + + snapshot_files = os.listdir(camera_directory) + if not progress_total_steps: + progress_total_steps = len(snapshot_files) + + if os.path.exists(timelapse_info_path): + snapshot_archive.write( + os.path.join(job_directory, + utility.TimelapseJobInfo.timelapse_info_file_name), + os.path.join(job_guid, utility.TimelapseJobInfo.timelapse_info_file_name) + ) + + progress_current_step += 1 + if progress_callback: + progress_callback(progress_key, progress_current_step, progress_total_steps) + + for name in snapshot_files: + file_path = os.path.join(camera_directory, name) + # ensure that all files we add to the archive were created by Octolapse, and are useful + # for rendering. + if os.path.isfile(file_path) and ( + utility.is_valid_snapshot_extension(utility.get_extension_from_filename(name)) or + CameraInfo.is_camera_info_file(name) or + SnapshotMetadata.is_metadata_file(name) or + OctolapseSettings.is_camera_settings_file(name) or + OctolapseSettings.is_rendering_settings_file(name) + ): + snapshot_archive.write( + file_path, + os.path.join(job_guid, camera_guid, name) + ) + progress_current_step += 1 + if progress_callback: + progress_callback(progress_key, progress_current_step, progress_total_steps) + except zipfile.LargeZipFile as e: + logger.exception("The zip file is too large to open.") + raise e + if is_download: + metadata = self._get_metadata_for_rendering_files(job_guid, camera_guid, temporary_directory) + target_extension = utility.get_extension_from_full_path(target_path) + # TODO: MAKE SURE THIS WORKS! USED TO BE SANITIZED + return "{0}.{1}".format(RenderJobInfo.get_rendering_name_from_metadata(metadata), target_extension) + return None + + def import_snapshot_archive(self, snapshot_archive_path, prevent_archive=False): + """Attempt to import one or more snapshot archives in the following form: + 1. The archive contains images (currently jpg only) in the root. + 2. The archive is contained within a folder named with a GUID that contains another folder + named with a GUID. + Each archive will be imported into its own guid job folder into the temporary directory + """ + # create our dict of archive files + archive_files_dict = {} + temporary_directory = self._temporary_directory + with self.temp_files_lock: + if not os.path.isfile(snapshot_archive_path): + return { + 'success': False, + 'error': 'The uploaded archive does not exist' + } + root_job_guid = "{0}".format(uuid.uuid4()) + root_camera_guid = "{0}".format(uuid.uuid4()) + try: + with zipfile.ZipFile(snapshot_archive_path, mode="r", allowZip64=True) as zip_file: + archive_files_temp_dict = {} # a temporary dict to hold values while we construct the jobs + fileinfos = [x for x in zip_file.infolist() if not x.filename.startswith("__MACOSX") ] + for fileinfo in fileinfos: + # see if the current item is a directory + if not fileinfo.filename.endswith(os.sep): + parts = utility.split_all(fileinfo.filename) + name = os.path.basename(fileinfo.filename) + name_without_extension = utility.get_filename_from_full_path(name) + extension = utility.get_extension_from_filename(name).lower() + item = { + "name": name, + "fileinfo": fileinfo + } + location_type = None + job_guid = None + camera_guid = None + file_type = None + if len(parts) == 1: + job_guid = root_job_guid + camera_guid = root_camera_guid + elif len(parts) == 2 and _is_valid_uuid(parts[0]): + job_guid = parts[0].lower() + elif len(parts) == 3 and _is_valid_uuid(parts[0]) and _is_valid_uuid(parts[1]): + job_guid = parts[0].lower() + camera_guid = parts[1].lower() + else: + continue + + if job_guid not in archive_files_temp_dict: + archive_files_temp_dict[job_guid] = { + 'cameras': {}, + 'file': None + } + if camera_guid and camera_guid not in archive_files_temp_dict[job_guid]["cameras"]: + archive_files_temp_dict[job_guid]['cameras'][camera_guid] = [] + + # this file is in the root. See what kind of file this is + if utility.TimelapseJobInfo.is_timelapse_info_file(name): + item["name"] = utility.TimelapseJobInfo.timelapse_info_file_name + # preserve case of the name, but keep the extension lower case + archive_files_temp_dict[job_guid]["file"] = item + else: + if utility.is_valid_snapshot_extension(extension): + # preserve case of the name, but keep the extension lower case + file_name = "{0}.{1}".format(name_without_extension, extension) + elif CameraInfo.is_camera_info_file(name): + file_name = CameraInfo.camera_info_filename + elif SnapshotMetadata.is_metadata_file(name): + file_name = SnapshotMetadata.METADATA_FILE_NAME + elif OctolapseSettings.is_camera_settings_file(name): + file_name = OctolapseSettings.camera_settings_file_name + elif OctolapseSettings.is_rendering_settings_file(name): + file_name = OctolapseSettings.rendering_settings_file_name + else: + continue + item["name"] = file_name + archive_files_temp_dict[job_guid]['cameras'][camera_guid].append(item) + + # now replace all of the job guids with new ones to prevent conflicts with existing unfinished + # rendering jobs. + for key in archive_files_temp_dict.keys(): + archive_files_dict["{0}".format(uuid.uuid4())] = archive_files_temp_dict[key] + archive_files_temp_dict = {} + + # now create the directories and files and place them in the temp snapshot directory + # remove python 2 support + # for job_guid, job in iteritems(archive_files_dict): + for job_guid, job in archive_files_dict.items(): + job_path = utility.get_temporary_snapshot_job_path(temporary_directory, job_guid) + if not os.path.isdir(job_path): + os.makedirs(job_path) + job_info_file = job["file"] + if job_info_file: + file_path = os.path.join(job_path, job_info_file["name"]) + with zip_file.open(job_info_file["fileinfo"]) as info_file: + with open(file_path, 'wb') as target_file: + target_file.write(info_file.read()) + # remove python 2 support + # for camera_guid, camera in iteritems(job["cameras"]): + for camera_guid, camera in job["cameras"].items(): + camera_path = utility.get_temporary_snapshot_job_camera_path( + temporary_directory, job_guid, camera_guid + ) + if not os.path.isdir(camera_path): + os.makedirs(camera_path) + for camera_fileinfo in camera: + file_path = os.path.join(camera_path, camera_fileinfo["name"]) + with zip_file.open(camera_fileinfo["fileinfo"]) as camera_file: + with open(file_path, 'wb') as target_file: + target_file.write(camera_file.read()) + except zipfile.LargeZipFile: + logger.exception("The zip file at '%s' is too large to open.", snapshot_archive_path) + return { + 'success': False, + 'error_keys': ['rendering', 'archive', 'import', 'zip_file_too_large'] + } + except zipfile.BadZipfile: + logger.exception("The zip file at '%s' appears to be corrupt.", snapshot_archive_path) + return { + 'success': False, + 'error_keys': ['rendering', 'archive', 'import', 'zip_file_corrupt'] + } + # now we should have extracted all of the items, add the job to the queue for these cameras + has_created_jobs = False + # remove python 2 support + # for job_guid, job in iteritems(archive_files_dict): + for job_guid, job in archive_files_dict.items(): + has_created_jobs = True + # remove python 2 support + # for camera_guid, camera in iteritems(job["cameras"]): + for camera_guid, camera in job["cameras"].items(): + # add this job to the queue as an imported item + if prevent_archive: + # add a file that will signify to the rendering engine that no archive should be created + utility.create_no_archive_file(temporary_directory, job_guid, camera_guid) + parameters = { + "job_guid": job_guid, + "camera_guid": camera_guid, + "action": "import", + "rendering_profile": None, + "camera_profile": None, + "temporary_directory": temporary_directory + } + self.rendering_task_queue.put(parameters) + + if has_created_jobs: + return { + 'success': True + } + + return { + 'success': False, + 'error_keys': ['rendering', 'archive', 'import', 'no_files_found'] + } + + def _get_renderings_in_process(self): + pending_jobs = {} + current_rendering_job_guid = self._current_rendering_job.get("job_guid", None) + current_rendering_camera_guid = self._current_rendering_job.get("camera_id", None) + with self.r_lock: + for job_guid in self._pending_rendering_jobs: + jobs = {} + for camera_guid in self._pending_rendering_jobs[job_guid]: + progress = "" + if job_guid == current_rendering_job_guid and camera_guid == current_rendering_camera_guid: + progress = self._current_rendering_progress + jobs[camera_guid] = { + "progress": progress + } + pending_jobs[job_guid] = jobs + return pending_jobs + + @staticmethod + def _has_enough_images(path): + image_count = 0 + for name in os.listdir(path): + if ( + os.path.isfile(os.path.join(path, name)) and + utility.is_valid_snapshot_extension(utility.get_extension_from_full_path(name).upper()) + ): + image_count += 1 + if image_count > 1: + return True + return False + + def _initialize_unfinished_renderings(self): + """ Removes any snapshot folders that cannot be rendered, returns the ones that can + Returns: [{'id':guid_val, 'path':path, paths: [{id:guid_val, 'path':path}]}] + """ + with self.r_lock: + temporary_directory = self._temporary_directory + + snapshot_path = utility.get_temporary_snapshot_directory(temporary_directory) + + # first clean the temporary folder + self._clean_temporary_directory(temporary_directory) + + logger.info("Fetching all unfinished renderings and metadata at '%s'.", snapshot_path) + + self._unfinished_renderings_size = 0 + self._unfinished_renderings = [] + paths_to_return_temp = [] + + if not os.path.isdir(snapshot_path): + return + # test each root level path in the snapshot_path to see if it could contain snapshots and append to the proper list + for basename in utility.walk_directories(snapshot_path): + path = os.path.join(snapshot_path, basename) + if _is_valid_uuid(basename): + paths_to_return_temp.append({'path': path, 'id': basename, 'paths': []}) + + # test each valid subdirectory to see if it is a camera directory + # containing all necessary settings and at least two jpgs + for job in paths_to_return_temp: + is_empty = True + job_guid = job["id"] + job_path = job['path'] + # for every job, keep track of paths we want to delete + delete_paths = [] + for camera_guid in utility.walk_directories(job_path): + path = os.path.join(job_path, camera_guid) + if ( + RenderingProcessor._has_enough_images(path) and + _is_valid_uuid(camera_guid) + ): + job['paths'].append({'path': path, 'id': camera_guid}) + + # ensure that all paths to return contain at least one subdirectory, else add to paths to delete + unfinished_size = 0 + for path in paths_to_return_temp: + if path['paths']: + for camera_path in path['paths']: + rendering_metadata = self._get_metadata_for_rendering_files( + path['id'], camera_path["id"], self._temporary_directory + ) + self._unfinished_renderings.append(rendering_metadata) + self._unfinished_renderings_size += rendering_metadata["file_size"] + + logger.info("Snapshot folder cleaned.") + + @staticmethod + def get_snapshot_file_count(temporary_directory, job_guid, camera_guid): + camera_path = utility.get_temporary_snapshot_job_camera_path(temporary_directory, job_guid, camera_guid) + # get all of the camera files + if os.path.isdir(camera_path): + return len(os.listdir(camera_path)) + return 0 + + def _delete_snapshots_for_job( + self, temporary_directory, job_guid, camera_guid, + progress_callback=None, progress_key='delete_snapshots', progress_current_step=None, progress_total_steps=None + ): + with self.temp_files_lock: + # get the two snapshot paths, one for the job, one for the camera + job_path = utility.get_temporary_snapshot_job_path(temporary_directory, job_guid) + camera_path = utility.get_temporary_snapshot_job_camera_path(temporary_directory, job_guid, camera_guid) + logger.debug("Deleting all snapshot images at: %s", camera_path) + # get all of the camera files + camera_files = [] + if os.path.isdir(camera_path): + camera_files = os.listdir(camera_path) + if progress_total_steps is None: + # there is one more step than camera files if we need to remove the job folder + progress_total_steps = len(camera_files) + 1 + if progress_current_step is None: + progress_current_step = 0 + + for name in camera_files: + extension = utility.get_extension_from_filename(name) + # delete only files made by octolapse (or jpgs) + if ( + utility.is_valid_snapshot_extension(extension) or + CameraInfo.is_camera_info_file(name) or + SnapshotMetadata.is_metadata_file(name) or + OctolapseSettings.is_camera_settings_file(name) or + OctolapseSettings.is_rendering_settings_file(name) + ): + file_path = os.path.join(camera_path, name) + if os.path.isfile(file_path): + utility.remove(os.path.join(file_path)) + if progress_callback: + progress_callback(progress_key, progress_current_step, progress_total_steps) + progress_current_step += 1 + # see if the job path is empty, if it is delete that too + has_files_or_folders = False + for name in os.listdir(job_path): + path = os.path.join(job_path, name) + if os.path.isdir(path) or ( + os.path.isfile(path) and not utility.TimelapseJobInfo.is_timelapse_info_file(name)): + has_files_or_folders = True + break + if not has_files_or_folders: + logger.debug("There are no files or folders in the job path at %s. Deleting.", job_path) + utility.rmtree(job_path) + progress_current_step += 1 + if progress_callback: + progress_callback(progress_key, progress_current_step, progress_total_steps) + + def _clean_temporary_directory(self, temporary_directory, current_camera_guid=None): + with self.temp_files_lock: + snapshot_folder = utility.get_temporary_snapshot_directory(temporary_directory) + + # if the folder doesn't exist, it doesn't need to be cleaned. + if not os.path.isdir(snapshot_folder): + return + logger.info("Cleaning temporary snapshot folders at %s.", temporary_directory) + # function that returns true if a directory has at least two jpegs + paths_to_delete = [] + paths_to_examine = [] + + # test each root level path in the temporary_directory to see if it could contain snapshots and append to the proper list + for basename in utility.walk_directories(snapshot_folder): + path = os.path.join(snapshot_folder, basename) + if _is_valid_uuid(basename): + paths_to_examine.append({'path': path, 'id': basename, 'paths': []}) + + # see if the temp archive directory exists + + # test each valid subdirectory to see if it is a camera directory + # containing all necessary settings and at least two jpgs + for job in paths_to_examine: + is_empty = True + job_guid = job["id"] + job_path = job['path'] + # for every job, keep track of paths we want to delete + delete_paths = [] + for camera_guid in utility.walk_directories(job_path): + if current_camera_guid is not None and camera_guid != current_camera_guid: + is_empty = False + continue + path = os.path.join(job_path, camera_guid) + if ( + RenderingProcessor._has_enough_images(path) and + _is_valid_uuid(camera_guid) + ): + job['paths'].append({'path': path, 'id': camera_guid}) + # commenting this out. Used to check for in_process tasks and prevent them from being viewed + # as unfinished + # if not (job_guid in in_process and camera_guid in in_process[job_guid]): + # job['paths'].append({'path': path, 'id': camera_guid}) + is_empty = False + + else: + delete_paths.append(path) + # if we didn't add any paths for this job, just delete the whole job + if is_empty: + delete_paths = [job_path] + + paths_to_delete.extend(delete_paths) + + # delete all paths that cannot be rendered + for path in paths_to_delete: + if os.path.exists(path): + try: + utility.rmtree(path) + except (OSError, IOError): + logger.exception("Could not remove empty snapshot directories at %s.", path) + # ignore these errors. + pass + + def _get_in_process_rendering_job(self, job_guid, camera_guid): + for rendering in self._renderings_in_process: + if rendering["job_guid"] == job_guid and rendering["camera_guid"] == camera_guid: + return rendering + return None + + def _get_unfinished_rendering_job(self, job_guid, camera_guid): + for rendering in self._unfinished_renderings: + if rendering["job_guid"] == job_guid and rendering["camera_guid"] == camera_guid: + return rendering + return None + + def _get_pending_rendering_job(self, job_guid, camera_guid): + with self.r_lock: + job = self._pending_rendering_jobs.get(job_guid, None) + if job: + return job.get(camera_guid, None) + return None + + def _get_metadata_for_rendering_files(self, job_guid, camera_guid, temporary_directory): + metadata_files = self._get_metadata_files_for_job(job_guid, camera_guid, temporary_directory) + return self._create_job_metadata(job_guid, camera_guid, metadata_files, temporary_directory) + + def _get_metadata_files_for_job(self, job_guid, camera_guid, temporary_directory): + with self.temp_files_lock: + # fetch the job from the pending job list if it exists + job_path = utility.get_temporary_snapshot_job_path(temporary_directory, job_guid) + camera_path = utility.get_temporary_snapshot_job_camera_path(temporary_directory, job_guid, camera_guid) + + print_job_metadata = utility.TimelapseJobInfo.load( + temporary_directory, job_guid, camera_guid=camera_guid + ).to_dict() + + rendering_profile = None + camera_profile = None + pending_job = self._get_pending_rendering_job(job_guid, camera_guid) + if pending_job: + rendering_profile = pending_job["rendering_profile"] + camera_profile = pending_job["camera_profile"] + + if camera_profile: + camera_profile = camera_profile.to_dict() + else: + camera_settings_path = os.path.join(camera_path, OctolapseSettings.camera_settings_file_name) + if os.path.exists(camera_settings_path): + try: + with open(camera_settings_path, 'r') as settings_file: + settings = json.load(settings_file) + camera_profile = settings.get("profile", {}) + camera_profile["guid"] = camera_guid + except (OSError, IOError, ValueError) as e: + logger.exception("Unable to read camera settings from %s.", camera_settings_path) + if not camera_profile: + camera_profile = { + "name": "UNKNOWN", + "guid": None, + } + + if rendering_profile: + rendering_profile = rendering_profile.to_dict() + else: + # get the rendering metadata if it exists + rendering_settings_path = os.path.join(camera_path, OctolapseSettings.rendering_settings_file_name) + if os.path.exists(rendering_settings_path): + try: + with open(rendering_settings_path, 'r') as settings_file: + settings = json.load(settings_file) + rendering_profile = settings.get("profile", {}) + except (OSError, IOError, ValueError) as e: + logger.exception("Unable to read rendering settings from %s.", rendering_settings_path) + if not rendering_profile: + rendering_profile = { + "guid": None, + "name": "UNKNOWN", + } + # get the camera info metadata if it exists + camera_info = CameraInfo.load(self._temporary_directory, job_guid, camera_guid) + + return { + "print_job": print_job_metadata, + "camera_profile": camera_profile, + "rendering_profile": rendering_profile, + "camera_info": camera_info + } + + def _create_job_metadata(self, job_guid, camera_guid, metadata_files, temporary_directory): + print_job_metadata = metadata_files["print_job"] + camera_profile = metadata_files["camera_profile"] + rendering_profile = metadata_files["rendering_profile"] + camera_info = metadata_files["camera_info"] + rendering_metadata = {} + rendering_metadata["job_guid"] = job_guid + rendering_metadata["camera_guid"] = camera_guid + rendering_metadata["camera_profile_guid"] = camera_profile["guid"] + job_path = utility.get_temporary_snapshot_job_path(temporary_directory, job_guid) + rendering_metadata["job_path"] = job_path + camera_path = utility.get_temporary_snapshot_job_camera_path(temporary_directory, job_guid, camera_guid) + rendering_metadata["camera_path"] = camera_path + rendering_metadata["print_start_time"] = print_job_metadata["print_start_time"] + rendering_metadata["print_end_time"] = print_job_metadata["print_end_time"] + rendering_metadata["print_end_state"] = print_job_metadata["print_end_state"] + rendering_metadata["print_file_name"] = print_job_metadata["print_file_name"] + rendering_metadata["print_file_extension"] = print_job_metadata["print_file_extension"] + file_size = utility.get_directory_size(camera_path) + rendering_metadata["file_size"] = file_size + rendering_metadata["camera_name"] = camera_profile.get("name", "UNKNOWN") + + # get the rendering metadata if it exists + rendering_metadata["rendering_name"] = rendering_profile.get("name", "UNKNOWN") + rendering_metadata["rendering_guid"] = rendering_profile.get("guid", None) + rendering_metadata["rendering_description"] = rendering_profile.get("description", "") + rendering_metadata["output_template"] = rendering_profile.get( + "output_template", RenderingProfile.default_output_template + ) + rendering_metadata["snapshot_count"] = camera_info.snapshot_count + rendering_metadata["snapshot_attempt"] = camera_info.snapshot_attempt + rendering_metadata["snapshot_errors_count"] = camera_info.errors_count + return rendering_metadata + + def _has_pending_jobs(self): + with self.r_lock: + return len(self._pending_rendering_jobs) > 0 + + def _is_thread_running(self): + with self.r_lock: + return self._rendering_job_thread and self._rendering_job_thread.is_alive() def run(self): + # initialize + try: + self._on_unfinished_renderings_loaded_callback() + except Exception as e: + logger.exception("An unexpected exception occurred while running the unfinished renderings loaded callback") + + # loop forever, always watching for new tasks to appear in the queue while True: try: - logger.verbose("Looking for rendering tasks.") - job_info = self.rendering_task_queue.get(timeout=5) - assert(isinstance(job_info, RenderJobInfo)) - self.job_count += 1 - with self.lock: - self._is_processing = True - - job_info.job_number = self.job_count - job_info.jobs_remaining = self.rendering_task_queue.qsize() - job = TimelapseRenderJob( - job_info.rendering, - job_info, - self.octoprint_timelapse_folder, - job_info.ffmpeg_path, - job_info.rendering.thread_count, - job_info.rendering.cleanup_after_render_complete, - job_info.rendering.cleanup_after_render_fail, - self.on_prerender_start, - self.on_render_start, - self.on_render_error, - self.on_render_success - ) - # send the rendering start message - try: - job.process() - ## What exceptions can happen here? - except Exception as e: - logger.exception("Failed to process the rendering job") - raise e - finally: - self.rendering_task_queue.task_done() + # see if there are any rendering tasks. + rendering_task_info = self.rendering_task_queue.get(True, self._idle_sleep_seconds) + if rendering_task_info: + + action = rendering_task_info["action"] + if action == "add": + # add the job to the queue if it is not already + self._add_job( + rendering_task_info["job_guid"], + rendering_task_info["camera_guid"], + rendering_task_info["rendering_profile"], + rendering_task_info["camera_profile"], + rendering_task_info["temporary_directory"], + ) + elif action == "remove_unfinished": + # add the job to the queue if it is not already + self._remove_unfinished_job( + rendering_task_info["job_guid"], + rendering_task_info["camera_guid"], + delete=rendering_task_info.get("delete", False), + ) + elif action == "import": + self._add_unfinished_job( + rendering_task_info["job_guid"], + rendering_task_info["camera_guid"], + rendering_task_info["rendering_profile"], + rendering_task_info["camera_profile"], + rendering_task_info["temporary_directory"] + ) + # go ahead and signal that the task queue is finished. We are using another method + # to determine if all rendering jobs are completed. + self.rendering_task_queue.task_done() + except queue.Empty: + pass + except Exception as e: + logger.exception("An unexpected exception occurred while fetching the next item in the rendering task queue.") - if self.rendering_task_queue.qsize() == 0: - with self.lock: + try: + # see if we've finished a task, if so, handle it. + if not self._is_thread_running() and self._is_processing: + with self.r_lock: + # join the thread and retrieve the finished job + finished_job = self._rendering_job_thread.join() + # we are done with the thread. + self._rendering_job_thread = None + # we don't consider a job to be failed for insufficient images. + # failed jobs get added to the unfinished renderings list. + failed = ( + finished_job.rendering_error is not None and not + ( + isinstance(finished_job.rendering_error, RenderError) + and finished_job.rendering_error.type == "insufficient-images" + ) + ) + # remove the job from the _pending_rendering_jobs dict + self._remove_pending_job( + finished_job.job_guid, + finished_job.camera_guid, + failed=failed) + # set our is_processing flag self._is_processing = False - logger.info("Sending render end message") - self.on_render_end() - - except queue.Empty: - if self._is_processing: - self._is_processing = False + self._on_render_end(finished_job.temporary_directory, finished_job.camera_guid) + # see if there are any other jobs remaining + if not self._has_pending_jobs(): + # no more jobs, signal rendering completion + self._on_all_renderings_ended(finished_job.temporary_directory) + + with self.r_lock: + if not self._has_pending_jobs() or self._is_processing: + continue + + # see if there are any jobs to process. If there are, process them + job_info = self._get_next_job_info() + next_job_job_guid = job_info["job_guid"] + next_job_camera_guid = job_info["camera_guid"] + rendering_profile = job_info["rendering_profile"] + camera_profile = job_info["camera_profile"] + temporary_directory = job_info["temporary_directory"] + + if next_job_job_guid and next_job_camera_guid: + if not self._start_job( + next_job_job_guid, next_job_camera_guid, rendering_profile, camera_profile, temporary_directory + ): + # the job never started. Remove it and send an error message. + with self.r_lock: + self._is_processing = False + self._on_render_error( + None, + "Octolapse was unable to start one of the rendering jobs. See plugin_octolapse.log for more " + "details." + ) + self._remove_pending_job(next_job_job_guid, next_job_camera_guid, failed=True) except Exception as e: - logger.exception(e) - raise e + logger.exception("An unexpected exception occurred while processing a queue item.") + + def _add_job(self, job_guid, camera_guid, rendering_profile, camera_profile, temporary_directory): + """Returns true if the job was added, false if it does not exist""" + with self.r_lock: + # see if the job is already pending. If it is, don't add it again. + camera_jobs = self._pending_rendering_jobs.get(job_guid, None) + # The job does not exist, add it. + if not camera_jobs: + # make sure the key exists for the current job_guid + camera_jobs = {} + self._pending_rendering_jobs[job_guid] = camera_jobs + + if camera_guid in camera_jobs: + return False + + self._pending_rendering_jobs[job_guid][camera_guid] = { + 'rendering_profile': rendering_profile, + 'camera_profile': camera_profile, + "temporary_directory": temporary_directory + } + # add job to the pending job list + with self.temp_files_lock: + metadata = self._get_metadata_for_rendering_files(job_guid, camera_guid, temporary_directory) + metadata["progress"] = "pending" + self._renderings_in_process.append(metadata) + self._renderings_in_process_size += metadata["file_size"] + + # see if the job is in the unfinished job list + removed_job = None + for unfinished_job in self._unfinished_renderings: + if unfinished_job["job_guid"] == job_guid and unfinished_job["camera_guid"] == camera_guid: + # the job is in the list. Remove it + self._unfinished_renderings.remove(unfinished_job) + # update the size + self._unfinished_renderings_size -= metadata["file_size"] + removed_job = unfinished_job + break + if removed_job: + self._on_unfinished_renderings_changed(unfinished_job, "removed") + self._on_in_process_renderings_changed(metadata, "added") + return True + + def _add_unfinished_job(self, job_guid, camera_guid, rendering_profile, camera_profile, temporary_directory): + with self.r_lock: + metadata = self._get_metadata_for_rendering_files(job_guid, camera_guid, temporary_directory) + self._unfinished_renderings_size += metadata["file_size"] + self._unfinished_renderings.append(metadata) + self._on_unfinished_renderings_changed(metadata, "added") + + def _remove_unfinished_job(self, job_guid, camera_guid, delete=False): + """Remove a job from the _pending_rendering_jobs dict if it exists""" + with self.r_lock: + job = self._get_unfinished_rendering_job(job_guid, camera_guid) + if job: + self._unfinished_renderings.remove(job) + if delete: + self._delete_snapshots_for_job(self._temporary_directory, job_guid, camera_guid) + + self._on_unfinished_renderings_changed(job, "removed") + + def _remove_pending_job(self, job_guid, camera_guid, failed=False): + removed_job = False + + with self.r_lock: + # handing removal if it's pending + if self._get_pending_rendering_job(job_guid, camera_guid): + + camera_jobs = self._pending_rendering_jobs.get(job_guid, None) + if camera_jobs: + # remove the camera job if it exists + camera_jobs.pop(camera_guid, None) + # remove the print guid key if there are no additional camera jobs + if len(camera_jobs) == 0: + job = self._pending_rendering_jobs.pop(job_guid, None) + + self._current_rendering_job = None + # add job to the unfinished job list + + # see if the job is in the in process job list + removed_job = self._get_in_process_rendering_job(job_guid, camera_guid) + if removed_job: + self._renderings_in_process.remove(removed_job) + # update the size + self._renderings_in_process_size -= removed_job["file_size"] + if failed: + self._unfinished_renderings.append(removed_job) + self._unfinished_renderings_size += removed_job["file_size"] + + if removed_job: + self._on_in_process_renderings_changed(removed_job, "removed") + if failed: + self._on_unfinished_renderings_changed(removed_job, "added") + + def _get_next_job_info(self): + """Gets the next job in the _pending_rendering_jobs dict, or returns Null if one does not exist""" + job_guid = None + camera_guid = None + rendering_profile = None + camera_profile = None + temporary_directory = None + if self._has_pending_jobs(): + job_guid = next(iter(self._pending_rendering_jobs)) + camera_jobs = self._pending_rendering_jobs.get(job_guid, None) + if camera_jobs: + camera_guid = next(iter(camera_jobs)) + camera_settings = camera_jobs[camera_guid] + rendering_profile = camera_settings["rendering_profile"] + camera_profile = camera_settings["camera_profile"] + temporary_directory = camera_settings["temporary_directory"] + + else: + logger.error("Could not find any camera jobs for the print job with guid %s.", job_guid) + return { + "job_guid": job_guid, + "camera_guid": camera_guid, + "rendering_profile": rendering_profile, + "camera_profile": camera_profile, + "temporary_directory": temporary_directory + + } - def on_prerender_start(self, payload): - logger.info("Sending prerender start message") - self.on_prerender_start(payload) + def _get_pending_rendering_job_count(self): + job_count = 0 + for job_guid in self._pending_rendering_jobs: + job_count += len(self._pending_rendering_jobs[job_guid]) + return job_count + + def _get_job_settings(self, job_guid, camera_guid, rendering_profile, camera_profile, temporary_directory): + """Attempt to load all job settings from the snapshot path""" + settings = OctolapseSettings(self._plugin_version, self._git_version) + settings.profiles.cameras = {} + settings.profiles.renderings = {} + + tmp_rendering_profile, tmp_camera_profile = OctolapseSettings.load_rendering_settings( + self._plugin_version, + temporary_directory, + job_guid, + camera_guid + ) + # ensure we have some rendering profile + if not rendering_profile: + rendering_profile = tmp_rendering_profile + if not rendering_profile: + rendering_profile = self._get_current_settings_callback().profiles.current_rendering() + + # ensure we have some camera profile + if not camera_profile: + camera_profile = tmp_camera_profile + if not camera_profile: + camera_profile = CameraProfile() + camera_profile.name = "UNKNOWN" + timelapse_job_info = utility.TimelapseJobInfo.load(temporary_directory, job_guid, camera_guid=camera_guid) + camera_info = CameraInfo.load(temporary_directory, job_guid, camera_guid) + job_number = self.job_count + jobs_remaining = self._get_pending_rendering_job_count() - 1 + + return RenderJobInfo( + job_guid, + camera_guid, + timelapse_job_info, + rendering_profile, + camera_profile, + temporary_directory, + self._snapshot_archive_directory, + self._timelapse_directory, + self._ffmpeg_directory, + camera_info, + job_number, + jobs_remaining + + ) - def on_render_start(self, payload): + def _start_job(self, job_guid, camera_guid, rendering_profile, camera_profile, temporary_directory): + with self.r_lock: + self._is_processing = True + try: + job_info = self._get_job_settings( + job_guid, camera_guid, rendering_profile, camera_profile, temporary_directory + ) + except Exception as e: + logger.exception("Could not load rendering job settings, skipping.") + return False + + self.job_count += 1 + + has_started = threading.Event() + self._rendering_job_thread = TimelapseRenderJob( + job_info, + has_started, + self._on_render_start, + self._on_render_error, + self._on_render_success, + self._on_render_progress, + self._delete_snapshots_for_job, + self.archive_unfinished_job + ) + self._rendering_job_thread.daemon = True + self._current_rendering_job = self._get_in_process_rendering_job(job_guid, camera_guid) + self._previous_render_progress = 0 + self._rendering_job_thread.start() + has_started.wait() + return True + + def _on_render_start(self, payload): logger.info("Sending render start message") - self.on_start(payload) + self._on_start_callback(payload, copy.copy(self._current_rendering_job)) - def on_render_error(self, payload, error): + def _on_render_error(self, payload, error): logger.info("Sending render fail message") - self.on_error(payload, error) - - def on_render_success(self, payload): + with self.r_lock: + job_copy = copy.copy(self._current_rendering_job) + if self._current_rendering_job: + job_guid = self._current_rendering_job["job_guid"] + camera_guid = self._current_rendering_job["camera_guid"] + self._remove_pending_job(job_guid, camera_guid, failed=True) + delete = False + if isinstance(error, RenderError): + if error.type in ['insufficient-images']: + # no need to delete, this was already deleted by the rendering processor + self._remove_unfinished_job(job_guid, camera_guid, delete=False) + self._on_error_callback(payload, error, job_copy) + + def _on_render_success(self, payload): logger.info("Sending render complete message") - self.on_success(payload) + self._on_success_callback(payload, copy.copy(self._current_rendering_job)) + + def _on_render_progress(self, key, current_step=None, total_steps=None): + progress_current_key = self._current_rendering_job["progress"] + cur_time = time.time() + if current_step is not None and total_steps: + progress = round(float(current_step) / total_steps * 100.0, 1) + else: + progress = None + if ( + progress_current_key != key or + (self._last_progress_time_update + 0.5 < cur_time and progress != self._previous_render_progress) + ): + self._current_rendering_job["progress"] = key + logger.verbose( + "Sending render progress message: %s%s", key, " - {0:.1f}".format(progress) if progress else "" + ) + self._on_render_progress_callback(progress, copy.copy(self._current_rendering_job)) + self._previous_render_progress = progress + self._last_progress_time_update = cur_time + + def _on_render_end(self, temporary_directory, camera_guid): + self._clean_temporary_directory(temporary_directory, current_camera_guid=camera_guid) - def on_render_end(self): + def _on_all_renderings_ended(self, temporary_directory): + self._clean_temporary_directory(temporary_directory) logger.info("Sending render end message") - self.on_end() + self._on_end_callback() + def _on_unfinished_renderings_changed(self, rendering, change_type): + self._on_unfinished_renderings_changed_callback(rendering, change_type) -class TimelapseRenderJob(object): + def _on_in_process_renderings_changed(self, rendering, change_type): + self._on_in_process_renderings_changed_callback(rendering, change_type) + + +class TimelapseRenderJob(threading.Thread): + # lock render_job_lock = threading.RLock() + # ffmpeg progress regexes + _ffmpeg_duration_regex = re.compile(r"Duration: (\d{2}):(\d{2}):(\d{2})\.\d{2}") + _ffmpeg_current_regex = re.compile(r"time=(\d{2}):(\d{2}):(\d{2})\.\d{2}") def __init__( self, - rendering, render_job_info, - octoprint_timelapse_folder, - ffmpeg_path, - threads, - cleanup_on_success, - cleanup_on_fail, - on_prerender_start, + on_start_event, on_render_start, on_render_error, - on_render_success + on_render_success, + on_render_progress, + delete_snapshots_callback, + archive_snapshots_callback ): - self._rendering = rendering - - self.render_job_info = render_job_info - - self._octoprintTimelapseFolder = octoprint_timelapse_folder + super(TimelapseRenderJob, self).__init__() + assert (isinstance(render_job_info, RenderJobInfo)) + self._render_job_info = render_job_info + self._on_start_event = on_start_event self._fps = None self._snapshot_metadata = None - self._imageCount = None - self._threads = threads - self._ffmpeg = None - self._images_removed = 0 - if ffmpeg_path is not None: - self._ffmpeg = ffmpeg_path.strip() - if sys.platform == "win32" and not (self._ffmpeg.startswith('"') and self._ffmpeg.endswith('"')): - self._ffmpeg = "\"{0}\"".format(self._ffmpeg) + self._image_count = 0 + self._image_count = 0 + self._max_image_number = 0 + self._images_removed_count = 0 + self._threads = render_job_info.rendering.thread_count + self._ffmpeg = render_job_info.ffmpeg_directory + if self._ffmpeg is not None: + self._ffmpeg = self._ffmpeg.strip() + # if sys.platform == "win32" and not (self._ffmpeg.startswith('"') and self._ffmpeg.endswith('"')): + # self._ffmpeg = "\"{0}\"".format(self._ffmpeg) ########### # callbacks ########### self._thread = None - self.cleanAfterSuccess = cleanup_on_success - self.cleanAfterFail = cleanup_on_fail - self._synchronize = False + self._archive_snapshots = render_job_info.archive_snapshots or not render_job_info.rendering.enabled # full path of the input - self.temp_rendering_dir = None + self._temp_rendering_dir = utility.get_temporary_rendering_directory(render_job_info.temporary_directory) self._output_directory = "" self._output_filename = "" self._output_extension = "" - self._rendering_output_file_path = "" - self._synchronized_directory = "" - self._synchronized_filename = "" - self._synchronize = self._rendering.sync_with_timelapse + self._output_filepath = "" # render script errors - self.before_render_error = None - self.after_render_error = None + self._before_render_error = None + self._after_render_error = None # callbacks - self.on_prerender_start = on_prerender_start self.on_render_start = on_render_start self.on_render_error = on_render_error self.on_render_success = on_render_success + self.on_render_progress = on_render_progress + self._delete_snapshots_for_job_callback = delete_snapshots_callback + self._archive_snapshots_callback = archive_snapshots_callback - def process(self): - return self._render() + def join(self): + super(TimelapseRenderJob, self).join() + return self._render_job_info - def _pre_render(self): - # remove frames from the timelapse as specified by the snapshots_to_skip_beginning and - # snapshot_to_skip_end settings - self._remove_frames() + def run(self): + self._on_start_event.set() + self._render() - # count the remaining snapshots - self._count_snapshots() + def _render(self): + """Process the timelapse render job and report progress""" + # send render start message + self.on_render_start(self._create_callback_payload(0, "Starting to render timelapse.")) + # Make sure we can render a timelapse, and that we aren't missing any critical settings + self._run_prechecks() - # read any metadata produced by the timelapse process - # this is used to create text overlays - self._read_snapshot_metadata() + # set an error variable to None, we will return None if there are no problems + r_error = None + # Variable used to determine if we should delete the snapshots at the end. We can safely + # delete the snapshots only if rendering has completed successfully, or if rendering is + # impossible. + delete_snapshots = False + + # temporary rendering filepath. the rendering will be moved after it has been completed + temp_filepath = os.path.join( + self._temp_rendering_dir, "{0}.{1}".format(str(uuid.uuid4()), "tmp") + ) + # Variables used to calculate cleanup progress, which needs to be specially tracked. + cleanup_current_step = 0 + cleanup_total_steps = None + try: + # set the outputs - output directory, output filename, output extension + self._set_outputs() - # If there aren't enough images, report an error - # First see if there were enough images, but they were removed because of the the - # snapshots_to_skip_beginning and snapshot_to_skip_end settings - if self._images_removed > 1 and self._imageCount < 2: - raise RenderError( - 'insufficient-images', - "Not enough snapshots were found to generate a timelapse for the '{0}' camera profile. {1} snapshot " - "were removed during pre-processing.".format(self.render_job_info.camera.name, self._images_removed) - ) - if self._imageCount == 0: - raise RenderError( - 'insufficient-images', - "No snapshots were available for the '{0}' camera profile.".format(self.render_job_info.camera.name) - ) - if self._imageCount == 1: - raise RenderError('insufficient-images', - "Only 1 frame was available, cannot make a timelapse with a single frame.") - # calculate the FPS - self._calculate_fps() - if self._fps < 1: - raise RenderError('insufficient-images', "The calculated FPS is below 1, which is not allowed. " - "Please check the rendering settings for Min and Max FPS " - "as well as the number of snapshots captured.") + # Run any prerender script configured in the camera profile. This routine reports a progress + # phase change, but does not send completed percentage + self._pre_render_script() - # set the outputs - output directory, output filename, output extension - self._set_outputs() + if self._render_job_info.rendering.enabled: + logger.info("Rendering is enabled for camera %s.", self._render_job_info.camera_guid) - def _pre_render_script(self): - try: - script = self.render_job_info.camera.on_before_render_script.strip() - if not script: - return - try: - script_args = [ - script, - self.render_job_info.camera.name, - self.render_job_info.snapshot_directory, - self.render_job_info.snapshot_filename_format, - os.path.join(self.render_job_info.snapshot_directory, self.render_job_info.snapshot_filename_format) - ] - - logger.info( - "Running the following before-render script command: %s \"%s\" \"%s\" \"%s\" \"%s\"", - script_args[0], - script_args[1], - script_args[2], - script_args[3], - script_args[4] - ) - cmd = utility.POpenWithTimeout() - return_code = cmd.run(script_args, None) - console_output = cmd.stdout - error_message = cmd.stderr - except utility.POpenWithTimeout.ProcessError as e: - raise RenderError( - 'before_render_script_error', - "A script occurred while executing executing the before-render script", - cause=e + # Create and copy images to the temporary rendering directory, converting them to jpg if necessary + # this routine reports progress + self._convert_and_copy_snapshot_images() + + # read any metadata produced by the timelapse process + # this is used to create text overlays + self._read_snapshot_metadata() + + # Verify that we have at least two images. We can't generate a timelapse with a single frame + self._check_image_count() + + # calculate the framerate of the output. + self._calculate_fps() + + try: + + logger.info("Creating the directory at %s", self._output_directory) + if not os.path.exists(self._output_directory): + try: + # create the target output directory + os.makedirs(self._output_directory) + except OSError: + pass + except Exception as e: + raise RenderError('create-render-path', + "Render - An exception was thrown when trying to " + "create the rendering path at: {0}. Please check " + "the logs (plugin_octolapse.log) for details.".format(self._output_directory), + cause=e) + + # Add a watermark if one is selected in the rendering settings. + watermark_path = None + if self._render_job_info.rendering.enable_watermark: + watermark_path = self._render_job_info.rendering.selected_watermark + if watermark_path == '': + logger.error("Watermark was enabled but no watermark file was selected.") + watermark_path = None + elif not os.path.exists(watermark_path): + logger.error("Render - Watermark file does not exist.") + watermark_path = None + elif sys.platform == "win32": + # Because ffmpeg hiccups on windows' drive letters and backslashes we have to give the watermark + # path a special treatment. Yeah, I couldn't believe it either... + watermark_path = watermark_path.replace( + "\\", "/").replace(":", "\\\\:") + + # Do image preprocessing. This relies on the original file name, so no renaming before running + # this function + self._add_text_overlays() + + # rename the images + logger.debug("Renaming images.") + self._rename_images() + + # Add pre and post roll. + self._apply_pre_post_roll() + + # prepare ffmpeg command + command_args = self._create_ffmpeg_command_args( + os.path.join(self._temp_rendering_dir, self._render_job_info.snapshot_filename_format), + temp_filepath, + watermark=watermark_path ) - if error_message: - if error_message.endswith("\r\n"): - error_message = error_message[:-2] - logger.error( - "Error output was returned from the before-rendering script: %s\nThe console output for the " - "error: \n %s", - error_message, - console_output + + # Render the timelapse via ffmpeg/avconv + logger.info("Running ffmpeg.") + with self.render_job_lock: + try: + # create an async thread, along with a callback for processing ffmpeg debug output + # for calculating progress + p = script.POpenWithTimeoutAsync(on_stderr_line_received=self._process_ffmpeg_output) + p.run(command_args) + except Exception as e: + logger.exception("An exception occurred while running the ffmpeg process.") + raise RenderError('rendering-exception', "ffmpeg failed during rendering of movie. " + "Please check plugin_octolapse.log for details.", + cause=e) + if p.return_code != 0: + return_code = p.return_code + stderr_text = "\n".join(p.stderr_lines) + raise RenderError('return-code', "Could not render movie, got return code %r: %s" % ( + return_code, stderr_text)) + else: + # only rename the temporary file if the script completed. + # If it did not, we will get a failed return code later. + utility.move(temp_filepath, self._output_filepath) + + # run any post rendering scripts, notifying the client if scripts are running (but no progress) + self._post_render_script() + + # If snapshot archiving is enabled, or if rendering is disabled, generate an archive + if self._archive_snapshots or not self._render_job_info.rendering.enabled: + # create the copy directory + camera_path = self._render_job_info.snapshot_directory + if not os.path.exists(self._render_job_info.snapshot_archive_directory): + try: + os.makedirs(self._render_job_info.snapshot_archive_directory) + except OSError: + pass + # Archive the snapshots and send progress to the client + self._archive_snapshots_callback( + self._render_job_info.temporary_directory, + self._render_job_info.job_guid, + self._render_job_info.camera_guid, + self._snapshot_archive_path, + progress_callback=self.on_render_progress, + progress_key="archiving" ) - if not return_code == 0: - if error_message: - error_message = "The before-render script failed with the following error message: {0}" \ - .format(error_message) - else: - error_message = ( - "The before-render script returned {0}," - " which indicates an error.".format(return_code) + + # Rendering has completed successfully. Indicate that we can delete all snapshots + delete_snapshots = True + except Exception as e: + logger.exception("Rendering Error") + if isinstance(e, RenderError): + if e.type == 'insufficient-images': + # if there aren't enough images to create a timelapse, just delete the images since they are + # useless. + delete_snapshots = True + r_error = e + else: + r_error = RenderError('render-error', + "Unknown render error. Please check plugin_octolapse.log for more details.", + e) + finally: + # Start cleanup of rendering/snapshot files + + # delete the temp rendering file if it exists. + if os.path.isfile(temp_filepath): + try: + utility.remove(temp_filepath) + except (OSError, IOError): + logger.exception("Could not delete a temporary rendering file!") + pass + + if delete_snapshots: + # We are going to delete all files in the snapshot directory + try: + # get a count of images we need to delete so that we can report accurate progress + num_snapshots = RenderingProcessor.get_snapshot_file_count( + self._render_job_info.temporary_directory, + self._render_job_info.job_guid, + self._render_job_info.camera_guid, + ) + # we will also be deleting temporary files. Get a total count so we can report total progress + cleanup_total_steps = num_snapshots + self._get_num_temporary_files() + + # delete all snapshots for the current render job, making sure to report total cleanup progress + self._delete_snapshots_for_job_callback( + self._render_job_info.temporary_directory, + self._render_job_info.job_guid, + self._render_job_info.camera_guid, + progress_callback=self.on_render_progress, + progress_key='cleanup', + progress_total_steps=cleanup_total_steps ) - raise RenderError('before_render_script_error', error_message) - except RenderError as e: - self.before_render_error = e + # we have deleted the snapshots, so increment our current step appropriately + cleanup_current_step = num_snapshots + except (IOError, OSError) as e: + raise e - def _post_render_script(self): - try: - script = self.render_job_info.camera.on_after_render_script.strip() - if not script: - return try: - script_args = [ - script, - self.render_job_info.camera.name, - self.render_job_info.snapshot_directory, - self.render_job_info.snapshot_filename_format, - os.path.join( - self.render_job_info.snapshot_directory, self.render_job_info.snapshot_filename_format - ), - self._output_directory, - self._output_filename, - self._output_extension, - self._rendering_output_file_path, - self._synchronized_directory, - self._synchronized_filename, - - ] - - logger.info( - 'Running the following after-render script command: %s "%s" "%s" "%s" "%s" "%s" "%s" "%s" ' - '"%s" "%s" "%s"', - - script_args[0], - script_args[1], - script_args[2], - script_args[3], - script_args[4], - script_args[5], - script_args[6], - script_args[7], - script_args[8], - script_args[9], - script_args[10] + if cleanup_total_steps is None: + # if we didn't delete the snapshots due to failure or some other reason, we won't + # have any cleanup steps at this point. Get the number of temp files so we can report progress. + cleanup_total_steps = self._get_num_temporary_files() + # delete all temporary rendering files and report cleanup progress + self._clear_temporary_files( + progress_key="cleanup", + progress_current_step=cleanup_current_step, + progress_total_steps=cleanup_total_steps ) + except (OSError, IOError): + # It's not a huge deal if we can't clean the temporary files at the moment. Log the error and move on. + logger.exception("Could not clean temporary rendering files.") + pass - cmd = utility.POpenWithTimeout() - return_code = cmd.run(script_args, None) - console_output = cmd.stdout - error_message = cmd.stderr - except utility.POpenWithTimeout.ProcessError as e: - raise RenderError( - 'after_render_script_error', - "A script occurred while executing executing the after-render script", - cause=e - ) - if error_message: - if error_message.endswith("\r\n"): - error_message = error_message[:-2] - logger.error( - "Error output was returned from the after-rendering script: %s", - error_message - ) - if not return_code == 0: - if error_message: - error_message = "The after-render script failed with the following error message: {0}" \ - .format(error_message) - else: - error_message = ( - "The after-render script returned {0}," - " which indicates an error.".format(return_code) - ) - raise RenderError('after_render_script_error', error_message) - except RenderError as e: - self.after_render_error = e - - def _remove_frames(self): - start_count = self._rendering.snapshots_to_skip_beginning - end_count = self._rendering.snapshot_to_skip_end - # get the path to the snapshot - path = self.render_job_info.snapshot_directory - # make sure the path exists - if not os.path.exists(self.render_job_info.snapshot_directory): - # there is nothing we can do here. Return - return + if r_error is None: + # Success! + self.on_render_success(self._create_callback_payload(0, "Timelapse rendering is complete.")) + else: + # Fail :( + self._render_job_info.rendering_error = r_error + self.on_render_error(self._create_callback_payload(0, "The render process failed."), r_error) - # see if there are start frames to remove - if start_count > 0: - # iterate a sorted list of snapshot files starting from the first snapshots taken - for index, filename in enumerate(sorted(os.listdir(path))): - # only remove jpeg files - if not filename.lower().endswith(".jpg"): - continue - # break if we've removed enough files - if start_count < 1: - break - # create a path to the current file - file_path = os.path.join(path, filename) - os.remove(file_path) - self._images_removed += 1 - start_count -= 1 + def _run_prechecks(self): + """Verify that we have an ffmepg and bitrate. If not, raise an exception. More prechecks could be done.""" + if self._ffmpeg is None: + raise RenderError('ffmpeg_path', "Cannot create movie, path to ffmpeg is unset. " + "Please configure the ffmpeg path within the " + "'Features->Webcam & Timelapse' settings tab.") - logger.info( - "%d snapshots were removed from the beginning of the timelapse.", - self._rendering.snapshots_to_skip_beginning - ) + if self._render_job_info.rendering.bitrate is None: + raise RenderError('no-bitrate', "Cannot create movie, desired bitrate is unset. " + "Please set the bitrate within the Octolapse rendering profile.") - # see if there are end frames to remove - if end_count > 0: - # iterate a sorted list of snapshot files starting from the last snapshots taken - for index, filename in enumerate(sorted(os.listdir(path), reverse=True)): - # only remove jpeg files - if not filename.lower().endswith(".jpg"): - continue - # break if we've removed enough files - if end_count < 1: - break - # create a path to the current file - file_path = os.path.join(path, filename) - os.remove(file_path) - self._images_removed += 1 - end_count -= 1 + def _set_outputs(self): + """Get all of the paths we need to render a timelapse, making sure there are no collisions.""" + # Rendering path info + logger.info("Setting output paths.") + self._output_filepath = utility.get_collision_free_filepath(self._render_job_info.rendering_path) + self._render_job_info.rendering_path = self._output_filepath + self._output_filename = utility.get_filename_from_full_path(self._output_filepath) + self._render_job_info.rendering_filename = self._output_filename + self._output_directory = utility.get_directory_from_full_path(self._output_filepath) + self._output_extension = utility.get_extension_from_full_path(self._output_filepath) + self._snapshot_archive_path = utility.get_collision_free_filepath( + self._render_job_info.snapshot_archive_path + ) + self._render_job_info.snapshot_archive_path = self._snapshot_archive_path + self._render_job_info.snapshot_archive_filename = utility.get_filename_from_full_path( + self._snapshot_archive_path + ) - logger.info( - "%d snapshots were removed from the end of the timelapse.", - self._rendering.snapshot_to_skip_end + def _convert_and_copy_snapshot_images(self): + """Creates a temporary rendering directory and copies all images to it, verifies all image files, + counts images, and finds the maximum snapshot number + """ + logger.debug("Converting and copying images to the temporary rendering folder") + self._image_count = 0 + if not os.path.isdir(self._render_job_info.snapshot_directory): + # No snapshots were created. Return + return + + # loop through each file in the snapshot directory + snapshot_files = [] + for name in os.listdir(self._render_job_info.snapshot_directory): + path = os.path.join(self._render_job_info.snapshot_directory, name) + # skip non-files and non jpgs + extension = utility.get_extension_from_full_path(path) + if not os.path.isfile(path) or not utility.is_valid_snapshot_extension(extension): + continue + snapshot_files.append(path) + + num_images = len(snapshot_files) + progress_total_steps = 0 + progress_current_step = 0 + num_temp_files = 0 + if os.path.isdir(self._temp_rendering_dir): + # clean any existing temporary files, keeping the progress up to date + num_temp_files = len(os.listdir(self._temp_rendering_dir)) + progress_total_steps = num_temp_files + num_images + self._clear_temporary_files( + delete_folder=False, + progress_key='preparing', + progress_total_steps=progress_total_steps ) + progress_current_step = num_temp_files - def _count_snapshots(self): - self._imageCount = 0 - if os.path.exists(self.render_job_info.snapshot_directory): - for file in os.listdir( - os.path.dirname(os.path.join( - self.render_job_info.snapshot_directory, self.render_job_info.snapshot_filename_format) - ) - ): - if file.endswith(".jpg"): - self._imageCount += 1 + # crete the temp directory + if not os.path.exists(self._temp_rendering_dir): + os.makedirs(self._temp_rendering_dir) - self.render_job_info.output_tokens["SNAPSHOTCOUNT"] = "{0}".format(self._imageCount) - logger.info("Found %s images via a manual search.", self._imageCount) + def convert_and_copy_snapshot_image(file_path, target_folder): + try: + file_name = os.path.basename(file_path) + target = os.path.join(target_folder, file_name) + with Image.open(file_path) as img: + if img.format not in ["JPEG", "JPEG 2000"]: + logger.info( + "The image at %s is in %s format. Attempting to convert to jpeg.", + file_path, + img.format + ) + with img.convert('RGB') as rgb_img: + # save the file with a temp name + rgb_img.save(target) + else: + utility.fast_copy(file_path, target) + return True + except IOError as e: + logger.exception("The file at path %s is not a valid image file, could not be converted, " + "and has been removed.", file_path) + return False + + for path in snapshot_files: + # increment the progress + progress_current_step += 1 + # verify the image and convert if necessary + if not convert_and_copy_snapshot_image(path, self._temp_rendering_dir): + # if there was a copy failure (perhaps the file was of zero size?) we don't really have an image, so + # continue + continue + self._image_count += 1 + + img_num = utility.get_snapshot_number_from_path(path) + if img_num > self._max_image_number: + self._max_image_number = img_num + + self.on_render_progress('preparing', progress_current_step, progress_total_steps) + + # if we have no camera infos, let's create it now + if self._render_job_info.camera_info.is_empty: + self._render_job_info.camera_info.snapshot_attempt = self._max_image_number + self._render_job_info.camera_info.snapshot_count = self._image_count + self._render_job_info.camera_info.errors_count = -1 + + self._render_job_info.output_tokens["SNAPSHOTCOUNT"] = "{0}".format(self._image_count) def _read_snapshot_metadata(self): + """Read all snapshot metadata (csv) if it exists, which is used to generate overlays.""" # get the metadata path - metadata_path = os.path.join(self.render_job_info.snapshot_directory, SnapshotMetadata.METADATA_FILE_NAME) + metadata_path = os.path.join(self._render_job_info.snapshot_directory, SnapshotMetadata.METADATA_FILE_NAME) # make sure the metadata file exists if not os.path.isfile(metadata_path): # nothing to do here. Exit return # see if the metadata file exists - logger.info('Reading snapshot metadata from %s', metadata_path) - skip_beginning = self._rendering.snapshots_to_skip_beginning - skip_end = self._rendering.snapshot_to_skip_end + logger.debug('Reading snapshot metadata from %s', metadata_path) + try: with open(metadata_path, 'r') as metadata_file: # read the metadaata and convert it to a dict dictreader = DictReader(metadata_file, SnapshotMetadata.METADATA_FIELDS) # convert the dict to a list - metadata_list = list(dictreader) - # remove metadata for any removed frames due to the - # snapshots_to_skip_beginning and snapshot_to_skip_end settings - self._snapshot_metadata = metadata_list[skip_beginning:len(metadata_list)-skip_end] - - # add the snapshot count to the output tokens - self.render_job_info.output_tokens["SNAPSHOTCOUNT"] = "{0}".format(self._imageCount) + self._snapshot_metadata = list(dictreader) return except IOError as e: logger.exception("No metadata exists, skipping metadata processing.") @@ -616,337 +2018,191 @@ def _read_snapshot_metadata(self): # Let's not throw an error and just render without the metadata pass + # def _count_snapshot_files(self, path): + # num_images = 0 + # # loop through each file in the snapshot directory + # snapshot_files = [] + # for name in os.listdir(path): + # path = os.path.join(path, name) + # # skip non-files and non jpgs + # extension = utility.get_extension_from_full_path(path) + # if not os.path.isfile(path) or not utility.is_valid_snapshot_extension(extension): + # continue + # num_images += 1 + # return num_images + + def _pre_render_script(self): + """Run any pre-rendering scripts that are configured within the camera profile.""" + script_path = self._render_job_info.camera.on_before_render_script.strip() + if not script_path: + return + self.on_render_progress('pre_render_script') + + # Todo: add the original snapshot directory and template path + logger.debug("Executing the pre-rendering script.") + cmd = script.CameraScriptBeforeRender( + script_path, + self._render_job_info.camera.name, + self._render_job_info.snapshot_directory, + self._render_job_info.snapshot_filename_format, + os.path.join( + self._render_job_info.snapshot_directory, + self._render_job_info.snapshot_filename_format + ) + ) + cmd.run() + if not cmd.success(): + self._before_render_error = RenderError( + 'before_render_script_error', + "A script occurred while executing executing the before-render script. Check " + "plugin_octolapse.log for details. " + ) + return + # adjust the number of images in the temp rendering directory, this number may have changed + # self._image_count = self._count_snapshot_files(self._temp_rendering_dir) + + def _check_image_count(self): + """Make sure we have enough images to generate a timelapse. If not, raise an exception.""" + # If there aren't enough images, report an error + if 0 < self._image_count < 2: + raise RenderError( + 'insufficient-images', + "Not enough snapshots were found to generate a timelapse for the '{0}' camera profile.".format( + self._render_job_info.camera.name, self._images_removed_count + ) + ) + if self._image_count == 0: + raise RenderError( + 'insufficient-images', + "No snapshots were available for the '{0}' camera profile.".format(self._render_job_info.camera.name) + ) + def _calculate_fps(self): - self._fps = self._rendering.fps + """Calculate and record the rendering FPS for fixed length renderings, or return the static framerate.""" + self._fps = self._render_job_info.rendering.fps - if self._rendering.fps_calculation_type == 'duration': + if self._render_job_info.rendering.fps_calculation_type == 'duration': self._fps = utility.round_to( - float(self._imageCount) / float(self._rendering.run_length_seconds), 0.001) - if self._fps > self._rendering.max_fps: - self._fps = self._rendering.max_fps - elif self._fps < self._rendering.min_fps: - self._fps = self._rendering.min_fps + float(self._image_count) / float(self._render_job_info.rendering.run_length_seconds), 0.001) + if self._fps > self._render_job_info.rendering.max_fps: + self._fps = self._render_job_info.rendering.max_fps + elif self._fps < self._render_job_info.rendering.min_fps: + self._fps = self._render_job_info.rendering.min_fps message = ( "FPS Calculation Type:%s, Fps:%s, NumFrames:%s, " "DurationSeconds:%s, Max FPS:%s, Min FPS:%s" ) logger.info( message, - self._rendering.fps_calculation_type, + self._render_job_info.rendering.fps_calculation_type, self._fps, - self._imageCount, - self._rendering.run_length_seconds, - self._rendering.max_fps, - self._rendering.min_fps + self._image_count, + self._render_job_info.rendering.run_length_seconds, + self._render_job_info.rendering.max_fps, + self._render_job_info.rendering.min_fps ) else: - logger.info("FPS Calculation Type:%s, Fps:%s", self._rendering.fps_calculation_type, self._fps) + logger.info("FPS Calculation Type:%s, Fps:%s", self._render_job_info.rendering.fps_calculation_type, + self._fps) # Add the FPS to the output tokens - self.render_job_info.output_tokens["FPS"] = "{0}".format(int(math.ceil(self._fps))) - - def _set_outputs(self): - self._output_directory = "{0}{1}{2}{3}".format( - self.render_job_info.output_tokens["DATADIRECTORY"], os.sep, "timelapse", os.sep - ) - try: - self._output_filename = self._rendering.output_template.format(**self.render_job_info.output_tokens) - except ValueError as e: - logger.exception("Failed to format the rendering output template.") - self._output_filename = "RenderingFilenameTemplateError" - self._output_extension = self._get_extension_from_output_format(self._rendering.output_format) - - # check for a rendered timelapse file collision - original_output_filename = self._output_filename - file_number = 0 - while os.path.isfile( - "{0}{1}.{2}".format( - self._output_directory, - self._output_filename, - self._output_extension) - ): - file_number += 1 - self._output_filename = "{0}_{1}".format(original_output_filename, file_number) - - self._rendering_output_file_path = "{0}{1}.{2}".format( - self._output_directory, self._output_filename, self._output_extension - ) - - synchronized_output_filename = original_output_filename - # check for a synchronized timelapse file collision - file_number = 0 - while os.path.isfile("{0}{1}{2}.{3}".format( - self._octoprintTimelapseFolder, - os.sep, - synchronized_output_filename, - self._output_extension - )): - file_number += 1 - synchronized_output_filename = "{0}_{1}".format(original_output_filename, file_number) - - self._synchronized_directory = "{0}{1}".format(self._octoprintTimelapseFolder, os.sep) - self._synchronized_filename = synchronized_output_filename - - ##################### - # Event Notification - ##################### - - def create_callback_payload(self, return_code, reason): - return RenderingCallbackArgs( - reason, - return_code, - self.render_job_info.job_id, - self.render_job_info.job_directory, - self.render_job_info.snapshot_directory, - self._output_directory, - self._output_filename, - self._output_extension, - self._synchronized_directory, - self._synchronized_filename, - self._synchronize, - self._imageCount, - self.render_job_info.job_number, - self.render_job_info.jobs_remaining, - self.render_job_info.camera.name, - self.before_render_error, - self.after_render_error - - ) - - def _render(self): - """Rendering runnable.""" - # set an error variable to None, we will return None if there are no problems - r_error = None - try: - self.on_prerender_start(self.create_callback_payload(0,"Prerender is starting.")) - - self._pre_render_script() - - self._pre_render() - - logger.info("Starting render.") - self.on_render_start(self.create_callback_payload(0, "Starting to render timelapse.")) - - # Temporary directory to store intermediate results of rendering. - self.temp_rendering_dir = mkdtemp(prefix='octolapse_render') - - if self._ffmpeg is None: - raise RenderError('ffmpeg_path', "Cannot create movie, path to ffmpeg is unset. " - "Please configure the ffmpeg path within the " - "'Features->Webcam & Timelapse' settings tab.") - - if self._rendering.bitrate is None: - raise RenderError('no-bitrate', "Cannot create movie, desired bitrate is unset. " - "Please set the bitrate within the Octolapse rendering profile.") - - try: - logger.info("Creating the directory at %s", self._output_directory) - - if not os.path.exists(self._output_directory): - os.makedirs(self._output_directory) - except Exception as e: - raise RenderError('create-render-path', - "Render - An exception was thrown when trying to " - "create the rendering path at: {0}. Please check " - "the logs (plugin_octolapse.log) for details.".format(self._output_directory), - cause=e) - - watermark_path = None - if self._rendering.enable_watermark: - watermark_path = self._rendering.selected_watermark - if watermark_path == '': - logger.error("Watermark was enabled but no watermark file was selected.") - watermark_path = None - elif not os.path.exists(watermark_path): - logger.error("Render - Watermark file does not exist.") - watermark_path = None - elif sys.platform == "win32": - # Because ffmpeg hiccups on windows' drive letters and backslashes we have to give the watermark - # path a special treatment. Yeah, I couldn't believe it either... - watermark_path = watermark_path.replace( - "\\", "/").replace(":", "\\\\:") - - # Do image preprocessing. This relies on the original file name, so no renaming before running - # this function - self._preprocess_images(self.temp_rendering_dir) - - # rename the images - self._rename_images(self.temp_rendering_dir) - - # Add pre and post roll. - self._apply_pre_post_roll(self.temp_rendering_dir) - - # prepare ffmpeg command - command_str = self._create_ffmpeg_command_string( - os.path.join(self.temp_rendering_dir, self.render_job_info.snapshot_filename_format), - self._rendering_output_file_path, - watermark=watermark_path - ) - logger.info("Running ffmpeg with command string: %s", command_str) - - with self.render_job_lock: - try: - p = sarge.run( - command_str, stdout=sarge.Capture(), stderr=sarge.Capture()) - except Exception as e: - raise RenderError('rendering-exception', "ffmpeg failed during rendering of movie. " - "Please check plugin_octolapse.log for details.", - cause=e) - if p.returncode != 0: - return_code = p.returncode - stderr_text = p.stderr.text - raise RenderError('return-code', "Could not render movie, got return code %r: %s" % ( - return_code, stderr_text)) + self._render_job_info.output_tokens["FPS"] = "{0}".format(int(math.ceil(self._fps))) - # run any post rendering scripts - self._post_render_script() + if self._fps < 1: + raise RenderError('framerate-too-low', "The calculated FPS is below 1, which is not allowed. " + "Please check the rendering settings for Min and Max FPS " + "as well as the number of snapshots captured.") - # Delete preprocessed images. - if self.cleanAfterSuccess: - shutil.rmtree(self.temp_rendering_dir) + def _add_text_overlays(self): + """Adds any text overlays configured within the rendering settings to every timelapse image.""" + if not self._render_job_info.rendering.overlay_text_template: + return - if self._synchronize: - # Move the timelapse to the Octoprint timelapse folder. - try: - # get the timelapse folder for the Octoprint timelapse plugin - synchronization_path = "{0}{1}.{2}".format( - self._synchronized_directory, self._synchronized_filename, self._output_extension - ) - message = ( - "Synchronizing timelapse with the built in " - "timelapse plugin, copying %s to %s" - ) - logger.info(message, self._rendering_output_file_path, synchronization_path) - shutil.move(self._rendering_output_file_path, synchronization_path) - # we've renamed the output due to a sync, update the member - except Exception as e: - raise RenderError('synchronizing-exception', - "Octolapse has failed to synchronize the default timelapse plugin due" - " to an unexpected exception. Check plugin_octolapse.log for more " - " information. You should be able to find your video within your " - " OctoPrint server here:
'{0}'".format(self._rendering_output_file_path), - cause=e) - except Exception as e: - logger.exception("Rendering Error") - if isinstance(e, RenderError): - r_error = e - else: - r_error = RenderError('render-error', - "Unknown render error. Please check plugin_octolapse.log for more details.", - e) - if self.cleanAfterFail: - # Delete preprocessed images. - if self.temp_rendering_dir is not None: - shutil.rmtree(self.temp_rendering_dir) - if r_error is None: - self.on_render_success(self.create_callback_payload(0, "Timelapse rendering is complete.")) - else: - self.on_render_error(self.create_callback_payload(0, "The render process failed."), r_error) + if not os.path.isfile(self._render_job_info.rendering.overlay_font_path): + raise RenderError("overlay-font", "The rendering overlay font path does not exist. Check your rendering settings and select a different font.") - def _preprocess_images(self, preprocessed_directory): - logger.info("Starting preprocessing of images.") if self._snapshot_metadata is None: - logger.info("Snapshot metadata file missing; skipping preprocessing.") - # Just copy images over. - for i in range(self._imageCount): - file_path = os.path.join( - self.render_job_info.snapshot_directory, self.render_job_info.snapshot_filename_format % i) - if os.path.exists(file_path): - output_path = os.path.join(preprocessed_directory, self.render_job_info.snapshot_filename_format % i) - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - shutil.move(file_path, output_path) - else: - logger.error("The snapshot at %s does not exist. Skipping.", file_path) + logger.warning("No snapshot metadata was found, cannot add text overlays images.") return + + logger.info("Started adding text overlays.") first_timestamp = float(self._snapshot_metadata[0]['time_taken']) + num_images = len(self._snapshot_metadata) for index, data in enumerate(self._snapshot_metadata): + self.on_render_progress('adding_overlays', index, num_images) # TODO: MAKE SURE THIS WORKS IF THERE ARE ANY ERRORS # Variables the user can use in overlay_text_template.format(). - format_vars = {} + format_vars = utility.SafeDict() # Extra metadata according to SnapshotMetadata.METADATA_FIELDS. - format_vars['snapshot_number'] = snapshot_number = int(data['snapshot_number']) + + format_vars['gcode_file'] = ( + self._render_job_info.timelapse_job_info.PrintFileName + "." + + self._render_job_info.timelapse_job_info.PrintFileExtension + ) + format_vars['gcode_file_name'] = self._render_job_info.timelapse_job_info.PrintFileName + format_vars['gcode_file_extension'] = self._render_job_info.timelapse_job_info.PrintFileExtension + format_vars['print_end_state'] = self._render_job_info.timelapse_job_info.PrintEndState + + format_vars['snapshot_number'] = snapshot_number = int(data['snapshot_number']) + 1 format_vars['file_name'] = data['file_name'] - format_vars['time_taken_s'] = time_taken = float(data['time_taken']) + format_vars['time_taken'] = time_taken = float(data['time_taken']) + + layer = None if "layer" not in data or data["layer"] is None or data["layer"] == "None" else int(data["layer"]) + height = None if "height" not in data or data["height"] is None or data["height"] == "None" else float(data["height"]) + x = None if "x" not in data or data["x"] is None or data["x"] == "None" else float(data["x"]) + y = None if "y" not in data or data["y"] is None or data["y"] == "None" else float(data["y"]) + z = None if "z" not in data or data["z"] is None or data["z"] == "None" else float(data["z"]) + e = None if "e" not in data or data["e"] is None or data["e"] == "None" else float(data["e"]) + f = None if "f" not in data or data["f"] is None or data["f"] == "None" else int(float(data["f"])) + x_snapshot = None if "x_snapshot" not in data or data["x_snapshot"] is None or data["x_snapshot"] == "None" else float(data["x_snapshot"]) + y_snapshot = None if "y_snapshot" not in data or data["y_snapshot"] is None or data["y_snapshot"] == "None" else float(data["y_snapshot"]) + + format_vars['layer'] = "None" if layer is None else "{0}".format(layer) + format_vars['height'] = "None" if height is None else "{0}".format(height) + format_vars['x'] = "None" if x is None else "{0:.3f}".format(x) + format_vars['y'] = "None" if y is None else "{0:.3f}".format(y) + format_vars['z'] = "None" if z is None else "{0:.3f}".format(z) + format_vars['e'] = "None" if e is None else "{0:.5f}".format(e) + format_vars['f'] = "None" if f is None else "{0}".format(f) + format_vars['x_snapshot'] = "None" if x_snapshot is None else "{0:.3f}".format(x_snapshot) + format_vars['y_snapshot'] = "None" if y_snapshot is None else "{0:.3f}".format(y_snapshot) # Verify that the file actually exists. file_path = os.path.join( - self.render_job_info.snapshot_directory, - self.render_job_info.snapshot_filename_format % snapshot_number + self._temp_rendering_dir, + self._render_job_info.get_snapshot_name_from_index(index) ) if os.path.exists(file_path): # Calculate time elapsed since the beginning of the print. format_vars['current_time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time_taken)) - format_vars['time_elapsed'] = "{}".format( + format_vars['time_elapsed'] = time_taken - first_timestamp + format_vars['time_elapsed_formatted'] = "{}".format( datetime.timedelta(seconds=round(time_taken - first_timestamp)) ) - # Open the image in Pillow and do preprocessing operations. - image = Image.open(file_path) - image = self.add_overlay(image, - text_template=self._rendering.overlay_text_template, - format_vars=format_vars, - font_path=self._rendering.overlay_font_path, - font_size=self._rendering.overlay_font_size, - overlay_location=self._rendering.overlay_text_pos, - overlay_text_alignment=self._rendering.overlay_text_alignment, - overlay_text_valign=self._rendering.overlay_text_valign, - overlay_text_halign=self._rendering.overlay_text_halign, - text_color=self._rendering.get_overlay_text_color(), - outline_color=self._rendering.get_overlay_outline_color(), - outline_width=self._rendering.overlay_outline_width) - # Save processed image. - output_path = os.path.join( - preprocessed_directory, self.render_job_info.snapshot_filename_format % snapshot_number) - if not os.path.exists(os.path.dirname(output_path)): - os.makedirs(os.path.dirname(output_path)) - image.save(output_path) + with Image.open(file_path) as img: + img = self.add_overlay(img, + text_template=self._render_job_info.rendering.overlay_text_template, + format_vars=format_vars, + font_path=self._render_job_info.rendering.overlay_font_path, + font_size=self._render_job_info.rendering.overlay_font_size, + overlay_location=self._render_job_info.rendering.overlay_text_pos, + overlay_text_alignment=self._render_job_info.rendering.overlay_text_alignment, + overlay_text_valign=self._render_job_info.rendering.overlay_text_valign, + overlay_text_halign=self._render_job_info.rendering.overlay_text_halign, + text_color=self._render_job_info.rendering.get_overlay_text_color(), + outline_color=self._render_job_info.rendering.get_overlay_outline_color(), + outline_width=self._render_job_info.rendering.overlay_outline_width) + # Save processed image. + temp_file_name = "{0}.jpg".format(uuid.uuid4()) + output_path = os.path.join(self._temp_rendering_dir, temp_file_name) + img.save(output_path) + utility.remove(file_path) + utility.move(output_path, file_path) else: - file_path = os.path.join( - self.render_job_info.snapshot_directory, - self.render_job_info.snapshot_filename_format % ( - index + self._rendering.snapshots_to_skip_beginning - ) - ) - if os.path.exists(file_path): - output_path = os.path.join( - preprocessed_directory, - self.render_job_info.snapshot_filename_format % ( - index + self._rendering.snapshots_to_skip_beginning - ) - ) - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - shutil.move(file_path, output_path) - else: - logger.error("The snapshot at %s does not exist. Skipping.", file_path) - - logger.info("Preprocessing success!") - - def _rename_images(self, preprocessed_directory): - # First, we need to rename our files, but we have to change the file name so that it won't overwrite any existing files - image_index = 0 - for filename in sorted(os.listdir(preprocessed_directory)): - # make sure the file is a jpg image - if filename.lower().endswith(".jpg"): - output_path = os.path.join( - preprocessed_directory, - "{0}.tmp".format(self.render_job_info.snapshot_filename_format % image_index) - ) - file_path = os.path.join(preprocessed_directory, filename) - shutil.move(file_path, output_path) - image_index += 1 - - # now loop back through all of the files and remove the .tmp extension - for filename in os.listdir(preprocessed_directory): - if filename.endswith(".tmp"): - output_path = os.path.join(preprocessed_directory, filename[:-4]) - file_path = os.path.join(preprocessed_directory, filename) - shutil.move(file_path, output_path) - + logger.error("The snapshot at %s does not exist. Skipping preprocessing.", file_path) + logger.info("Finished adding text overlays.") @staticmethod def add_overlay(image, text_template, format_vars, font_path, font_size, overlay_location, overlay_text_alignment, @@ -960,11 +2216,27 @@ def add_overlay(image, text_template, format_vars, font_path, font_size, overlay # No text to draw. if not text_template: return image - text = text_template.format(**format_vars) - # Retrieve the correct font. - if not font_path: - raise RenderError('overlay-font', "No overlay font was specified when attempting to add overlay.") + success, text_template = format_overlay_date_templates(text_template, format_vars["time_taken"]) + if not success: + # this should not happen, but just in case + # note, text will contain the error if this fails. + raise RenderError('overlay-text', text_template) + + if "time_elapsed" in format_vars: + success, text_template = format_overlay_timedelta_templates(text_template, format_vars["time_elapsed"]) + if not success: + raise RenderError('overlay-text', text_template) + # replace time_elapsed with a formatted string, in case format strings are omitted + format_vars['time_elapsed'] = format_vars['time_elapsed_formatted'] + del format_vars['time_elapsed_formatted'] + text = text_template.format(**format_vars) + + # No font selected + if not font_path or not os.path.isfile(font_path): + raise RenderError('overlay-font', "The rendering overlay font path does not exist. Check your rendering " + "settings and select a different font.") + font = ImageFont.truetype(font_path, size=font_size) # Create the image to draw on. @@ -972,7 +2244,9 @@ def add_overlay(image, text_template, format_vars, font_path, font_size, overlay d = ImageDraw.Draw(text_image) # Process the text position to improve the alignment. - if isinstance(overlay_location, string_types): + # remove python 2 support + # if isinstance(overlay_location, string_types): + if isinstance(overlay_location, str): overlay_location = json.loads(overlay_location) x, y = tuple(overlay_location) # valign. @@ -1000,95 +2274,199 @@ def add_overlay(image, text_template, format_vars, font_path, font_size, overlay raise RenderError('overlay-text-halign', "An invalid overlay text halign ({0}) was specified.".format(overlay_text_halign)) - # Draw overlay text outline - # create outline text - for adj in range(outline_width): - # move right - d.multiline_text(xy=(x - adj, y), text=text, font=font, fill=outline_color_tuple) - # move left - d.multiline_text(xy=(x + adj, y), text=text, font=font, fill=outline_color_tuple) - # move up - d.multiline_text(xy=(x, y + adj), text=text, font=font, fill=outline_color_tuple) - # move down - d.multiline_text(xy=(x, y - adj), text=text, font=font, fill=outline_color_tuple) - # diagnal left up - d.multiline_text(xy=(x - adj, y + adj), text=text, font=font, fill=outline_color_tuple) - # diagnal right up - d.multiline_text(xy=(x + adj, y + adj), text=text, font=font, fill=outline_color_tuple) - # diagnal left down - d.multiline_text(xy=(x - adj, y - adj), text=text, font=font, fill=outline_color_tuple) - # diagnal right down - d.multiline_text(xy=(x + adj, y - adj), text=text, font=font, fill=outline_color_tuple) - # Draw overlay text. - d.multiline_text(xy=(x, y), text=text, fill=text_color_tuple, font=font, align=overlay_text_alignment) + d.multiline_text( + xy=(x, y), + text=text, + fill=text_color_tuple, + font=font, + align=overlay_text_alignment, + stroke_width=outline_width, + stroke_fill=outline_color_tuple + ) return Image.alpha_composite(image.convert('RGBA'), text_image).convert('RGB') - def _apply_pre_post_roll(self, image_dir): + def _rename_images(self, progress_key="rename_images", progress_current_step=None, progress_total_steps=None): + """Rename all images so that they start with 00000 and increment by 1. This is requried for FFMPEG.""" + # First, we need to rename our files, but we have to change the file name so that it won't overwrite any existing files + sorted_temp_files = sorted(os.listdir(self._temp_rendering_dir)) + image_index = 0 + if not progress_total_steps: + progress_total_steps = len(sorted_temp_files) * 2 + if not progress_current_step: + progress_current_step = 0 + # count the files and multiply by two since they need to be + # renamed twice (once with a tmp extention, and another to remove the + # tmp extension) + for filename in sorted_temp_files: + self.on_render_progress(progress_key, progress_current_step, progress_total_steps) + progress_current_step += 1 + # make sure the file is a jpg image + if filename.lower().endswith(".jpg"): + output_path = os.path.join( + self._temp_rendering_dir, + "{0}.{1}".format(self._render_job_info.get_snapshot_name_from_index(image_index), utility.temporary_extension) + ) + file_path = os.path.join(self._temp_rendering_dir, filename) + utility.move(file_path, output_path) + image_index += 1 + + # now loop back through all of the files and remove the .tmp extension + for filename in os.listdir(self._temp_rendering_dir): + self.on_render_progress(progress_key, progress_current_step, progress_total_steps) + progress_current_step += 1 + extension = utility.get_extension_from_filename(filename) + if utility.is_valid_temporary_extension(extension): + src = os.path.join(self._temp_rendering_dir, filename) + dst = os.path.join(self._temp_rendering_dir, utility.remove_extension_from_filename(filename)) + utility.move(src, dst) + + def _apply_pre_post_roll(self): + """Copies the first and final frames depending on the pre/post roll length for the given framerate. + Renames the images if any pre-roll frames were added so that they are all in sequence for ffmpeg. + """ # Here we will be adding pre and post roll frames. # This routine assumes that images exist, that the first image has number 0, and that # there are no missing images - logger.info("Starting pre/post roll.") - # start with pre-roll. - pre_roll_frames = int(self._rendering.pre_roll_seconds * self._fps) + # start with pre_roll. + pre_roll_frames = int(self._render_job_info.rendering.pre_roll_seconds * self._fps) + post_roll_frames = int(self._render_job_info.rendering.post_roll_seconds * self._fps) + # calculate the number of total steps for the pre-post roll process + progress_total_steps = pre_roll_frames + post_roll_frames if pre_roll_frames > 0: + # if there are any pre-roll frames, all files will need to be renamed again with a tmp extension + # and then again to remove the tmp extension. All added pre/post roll frames will also need to be + # renamed + progress_total_steps += 2 * (len(os.listdir(self._temp_rendering_dir)) + pre_roll_frames + post_roll_frames) + progress_current_step = 0 + if pre_roll_frames > 0: + logger.info("Adding %d pre-roll frames.", pre_roll_frames) # We will be adding images starting with -1 and decrementing 1 until we've added the # correct number of frames. # create a variable to hold the new path of the first image - first_image_path = os.path.join(image_dir, self.render_job_info.snapshot_filename_format % 0) + first_image_path = os.path.join( + self._temp_rendering_dir, self._render_job_info.snapshot_filename_format % 0 + ) # rename all of the current files. The snapshot number should be - # incremented by the number of pre-roll frames. Start with the last + # incremented by the number of pre_roll frames. Start with the last # image and work backwards to avoid overwriting files we've already moved for image_number in range(pre_roll_frames): + self.on_render_progress('pre_post_roll', progress_current_step, progress_total_steps) + progress_current_step += 1 new_image_path = os.path.join( - image_dir, - self.render_job_info.pre_roll_snapshot_filename_format % (0, image_number) + self._temp_rendering_dir, + self._render_job_info.pre_roll_snapshot_filename_format % (0, image_number) ) - shutil.copy(first_image_path, new_image_path) - # finish with post - post_roll_frames = int(self._rendering.post_roll_seconds * self._fps) + utility.fast_copy(first_image_path, new_image_path) + if post_roll_frames > 0: - last_frame_index = self._imageCount - 1 - last_image_path = os.path.join(image_dir, self.render_job_info.snapshot_filename_format % last_frame_index) + last_frame_index = self._image_count - 1 + last_image_path = os.path.join( + self._temp_rendering_dir, self._render_job_info.snapshot_filename_format % last_frame_index + ) + logger.info("Adding %d post-roll frames.", post_roll_frames) for post_roll_index in range(post_roll_frames): + self.on_render_progress('pre_post_roll', progress_current_step, progress_total_steps) + progress_current_step += 1 + new_image_path = os.path.join( - image_dir, - self.render_job_info.pre_roll_snapshot_filename_format % (last_frame_index, post_roll_index) + self._temp_rendering_dir, + self._render_job_info.pre_roll_snapshot_filename_format % (last_frame_index, post_roll_index) ) - shutil.copy(last_image_path, new_image_path) - - if pre_roll_frames > 0: + utility.fast_copy(last_image_path, new_image_path) + if pre_roll_frames > 0 or post_roll_frames > 0: + logger.info("Renaming images because pre-roll images were added.") # pre or post roll frames were added, so we need to rename all of our images - self._rename_images(image_dir) + self._rename_images( + progress_key="pre_post_roll", + progress_current_step=progress_current_step, + progress_total_steps=progress_total_steps + ) + + # update the image count + self._image_count += pre_roll_frames + post_roll_frames logger.info("Pre/post roll generated successfully.") - @staticmethod - def _get_extension_from_output_format(output_format): - EXTENSIONS = {"avi": "avi", - "flv": "flv", - "h264": "mp4", - "vob": "vob", - "mp4": "mp4", - "mpeg": "mpeg", - "gif": "gif"} - return EXTENSIONS.get(output_format.lower(), "mp4") - @staticmethod - def _get_vcodec_from_output_format(output_format): - VCODECS = {"avi": "mpeg4", - "flv": "flv1", - "gif": "gif", - "h264": "h264", - "mp4": "mpeg4", - "mpeg": "mpeg2video", - "vob": "mpeg2video"} - return VCODECS.get(output_format.lower(), "mpeg2video") + def _post_render_script(self): + """Run any post render script that is configured within the camera profile.""" + script_path = self._render_job_info.camera.on_after_render_script.strip() + if not script_path: + return + self.on_render_progress('post_render_script') + # Todo: add the original snapshot directory and template path + logger.debug("Executing post-render script.") + cmd = script.CameraScriptAfterRender( + script_path, + self._render_job_info.camera.name, + self._temp_rendering_dir, + self._render_job_info.snapshot_filename_format, + os.path.join( + self._temp_rendering_dir, + self._render_job_info.snapshot_filename_format + ), + self._output_directory, + self._output_filename, + self._output_extension, + self._output_filepath + ) + cmd.run() + if not cmd.success(): + self._after_render_error = RenderError( + 'after_render_script_error', + "A script occurred while executing executing the after-render script. Check " + "plugin_octolapse.log for details. " + ) - def _create_ffmpeg_command_string(self, input_file_format, output_file, watermark=None, pix_fmt="yuv420p"): + def _get_num_temporary_files(self): + """Returns the number of files within the temporary rendering directory. + This is useful for cleanup progress messages + """ + num_files = 0 + if os.path.isdir(self._temp_rendering_dir): + num_files = len(os.listdir(self._temp_rendering_dir)) + return num_files + + def _clear_temporary_files( + self, progress_key='deleting_temp_files', progress_current_step=None, progress_total_steps=None, + delete_folder=True + ): + """Delete all temporary rendering files, and report progress.""" + logger.debug("Cleaning all temporary rendering files.") + if os.path.isdir(self._temp_rendering_dir): + temp_rendering_files = os.listdir(self._temp_rendering_dir) + if progress_total_steps is None: + progress_total_steps = len(temp_rendering_files) + if progress_current_step is None: + progress_current_step = 0 + for filename in temp_rendering_files: + self.on_render_progress(progress_key, progress_current_step, progress_total_steps) + progress_current_step += 1 + filepath = os.path.join(self._temp_rendering_dir, filename) + extension = utility.get_extension_from_filename(filename) + if os.path.isfile(filepath) and ( + utility.is_valid_snapshot_extension(extension) or utility.is_valid_temporary_extension(extension) + ): + utility.remove(filepath) + if delete_folder: + try: + # remove the directory if it is empty, but don't raise an exception. It doesn't really matter + # if the directory is removed, it's just cosmetic + if not os.listdir(self._temp_rendering_dir): + os.rmdir(self._temp_rendering_dir) + except (OSError, IOError): + logger.exception("Could not remove temporary rendering directory.") + pass + + ################### + ## FFMPEG functions + ################### + + def _create_ffmpeg_command_args(self, input_file_format, output_file, watermark=None, pix_fmt="yuv420p"): """ Create ffmpeg command string based on input parameters. Arguments: @@ -1100,24 +2478,39 @@ def _create_ffmpeg_command_string(self, input_file_format, output_file, watermar (str): Prepared command string to render `input` to `output` using ffmpeg. """ - v_codec = self._get_vcodec_from_output_format(self._rendering.output_format) + v_codec = RenderJobInfo.get_vcodec_from_output_format(self._render_job_info.rendering.output_format) - command = [self._ffmpeg, '-framerate', "{}".format(self._fps), '-loglevel', 'error', '-i', - '"{}"'.format(input_file_format)] - command.extend( - ['-threads', "{}".format(self._threads), '-r', "{}".format(self._fps), '-y', '-b', "{}".format(self._rendering.bitrate), '-vcodec', v_codec]) + command = [self._ffmpeg, '-framerate', "{}".format(self._fps), '-loglevel', 'info', '-i', input_file_format] + command.extend([ + '-threads', "{}".format(self._threads), + '-r', "{}".format(self._fps), + '-y', + '-vcodec', v_codec, + '-f', RenderJobInfo.get_ffmpeg_format_from_output_format(self._render_job_info.rendering_output_format)] + ) + + # special parameters from h265 + if self._render_job_info.rendering.output_format == "h265": + command.extend([ + "-tag:v", "hvc1", + "-crf", "{}".format(self._render_job_info.rendering.constant_rate_factor), + ]) + else: + command.extend([ + "-b:v", "{}".format(self._render_job_info.rendering.bitrate), + ]) filter_string = self._create_filter_string(watermark=watermark, pix_fmt=pix_fmt) if filter_string is not None: - logger.debug("Applying video filter chain: %s".format(filter_string)) - command.extend(["-vf", sarge.shell_quote(filter_string)]) + logger.debug("Applying video filter chain: %s", filter_string) + command.extend(["-vf", filter_string]) # finalize command with output file - logger.debug("Rendering movie to %s".format(output_file)) - command.append('"{}"'.format(output_file)) + logger.debug("Rendering movie to %s", output_file) + command.append(output_file) - return " ".join(command) + return command @classmethod def _create_filter_string(cls, watermark=None, pix_fmt="yuv420p"): @@ -1150,16 +2543,51 @@ def _create_filter_string(cls, watermark=None, pix_fmt="yuv420p"): return filter_string - @staticmethod - def _notify_callback(callback, *args, **kwargs): - """Notifies registered callbacks of type `callback`.""" - if callback is not None and callable(callback): - callback(*args, **kwargs) + # Regex for extracting the current frame + _ffmpeg_current_frame_regex = re.compile(r"frame=(?:\s+)?(\d+)") + # This function was inspired by code within the OctoPrint timelapse plugin, which can be found here: + # https://github.com/foosel/OctoPrint/blob/464d9c0757632ecfcbc2c3c0d0ca3f180714fdff/src/octoprint/timelapse.py#L916-L934 + # However, we use a different method based on the number of frames, which is simpler and reports progress more often. + def _process_ffmpeg_output(self, line): + """Parse ffmpeg output and update local variables indicating the current rendering progress.""" + # frame based duration - this is a bit simpler than the time based duration since we already know how many + # frames we have + current_frame = TimelapseRenderJob._ffmpeg_current_frame_regex.search(line) + # see if we've parsed the current frame + if current_frame is not None: + self.on_render_progress('rendering', int(current_frame.groups()[0]), self._image_count) + + ################### + ## Callback Helpers + ################### + + def _create_callback_payload(self, return_code, reason): + """Create callback arguments used for notifying Octolapse of rendering state changes (not progress).""" + return RenderingCallbackArgs( + reason, + return_code, + self._render_job_info.job_guid, + self._render_job_info.job_directory, + self._render_job_info.snapshot_directory, + self._output_directory, + self._output_filename, + self._output_extension, + self._render_job_info.snapshot_archive_path if self._render_job_info.archive_snapshots else None, + self._image_count, + self._render_job_info.job_number, + self._render_job_info.jobs_remaining, + self._render_job_info.camera.name, + self._before_render_error, + self._after_render_error, + self._render_job_info.timelapse_job_info.PrintFileName, + self._render_job_info.timelapse_job_info.PrintFileExtension, + self._render_job_info.rendering.enabled, + ) class RenderError(Exception): def __init__(self, type, message, cause=None): - super(Exception, self).__init__() + super(RenderError, self).__init__() self.type = type self.cause = cause if cause is not None else None self.message = message @@ -1176,48 +2604,44 @@ def __init__( self, reason, return_code, - job_id, + job_guid, job_directory, snapshot_directory, rendering_directory, rendering_filename, rendering_extension, - synchronized_directory, - synchronized_filename, - synchronize, + archive_path, snapshot_count, job_number, jobs_remaining, camera_name, before_render_error, - after_render_error + after_render_error, + gcode_filename, + gcode_file_extension, + rendering_enabled ): self.Reason = reason self.ReturnCode = return_code - self.JobId = job_id + self.JobId = job_guid self.JobDirectory = job_directory self.SnapshotDirectory = snapshot_directory self.RenderingDirectory = rendering_directory self.RenderingFilename = rendering_filename self.RenderingExtension = rendering_extension - self.SynchronizedDirectory = synchronized_directory - self.SynchronizedFilename = synchronized_filename - self.Synchronize = synchronize + self.ArchivePath = archive_path self.SnapshotCount = snapshot_count self.JobNumber = job_number self.JobsRemaining = jobs_remaining self.CameraName = camera_name self.BeforeRenderError = before_render_error self.AfterRenderError = after_render_error + self.GcodeFilename = gcode_filename + self.GcodeFileExtension = gcode_file_extension + self.RenderingEnabled = rendering_enabled def get_rendering_filename(self): return "{0}.{1}".format(self.RenderingFilename, self.RenderingExtension) - def get_synchronization_filename(self): - return "{0}.{1}".format(self.SynchronizedFilename, self.RenderingExtension) - def get_rendering_path(self): - return "{0}{1}".format(self.RenderingDirectory, self.get_rendering_filename()) - - def get_synchronization_path(self): - return "{0}{1}".format(self.SynchronizedDirectory, self.get_synchronization_filename()) + return os.path.join(self.RenderingDirectory, self.get_rendering_filename()) diff --git a/octoprint_octolapse/script.py b/octoprint_octolapse/script.py new file mode 100644 index 00000000..715bdc9a --- /dev/null +++ b/octoprint_octolapse/script.py @@ -0,0 +1,815 @@ +# coding=utf-8 +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +from __future__ import unicode_literals +import subprocess +import threading +import psutil +import os +import sys +import platform + +# create the module level logger +from octoprint_octolapse.log import LoggingConfigurator +logging_configurator = LoggingConfigurator() +logger = logging_configurator.get_logger(__name__) + +if sys.version_info >= (3, 2): + def fsdecode(filename): + return os.fsdecode(filename) +else: + fs_encoding = sys.getfilesystemencoding() + fs_errors = 'surrogateescape' if platform.system() != "Windows" else 'strict' + + def fsdecode(filename): + if isinstance(filename, bytes): + return filename.decode(fs_encoding, fs_errors) + else: + return filename + + +class POpenWithTimeout(object): + + class ProcessError(Exception): + def __init__(self, error_type, message, cause=None): + super(POpenWithTimeout.ProcessError, self).__init__() + self.error_type = error_type + self.cause = cause if cause is not None else None + self.message = message + + def __str__(self): + if self.cause is None: + return "{}: {}".format(self.error_type, self.message) + if isinstance(self.cause, list): + if len(self.cause) > 1: + error_string = "{}: {} - Inner Exceptions".format(self.error_type, self.message) + error_count = 1 + for cause in self.cause: + error_string += "{} {}: {} Exception - {}".format(os.linesep, error_count, type(cause).__name__, cause) + error_count += 1 + return error_string + elif len(self.cause) == 1: + return "{}: {} - Inner Exception: {}".format(self.error_type, self.message, self.cause[0]) + return "{}: {} - Inner Exception: {}".format(self.error_type, self.message, self.cause) + + lock = threading.Lock() + + def __init__(self): + self.name = "Unknown" + self.proc = None + self.stdout = '' + self.stderr = '' + self.error_message = None + self.completed = False + self._exception = None + self._subprocess_kill_exceptions = [] + self._kill_exceptions = None + self.exception = None + self.timeout_seconds = None + self.return_code = -100 + self._timed_out = False + self._was_killed = False + self._success = False + + def success(self): + return self._success + + def kill(self): + if self.proc is None: + return + try: + process = psutil.Process(self.proc.pid) + for proc in process.children(recursive=True): + try: + proc.kill() + except psutil.NoSuchProcess: + # the process must have completed + pass + except (psutil.Error,psutil.AccessDenied, psutil.ZombieProcess) as e: + logger.exception("An error occurred while killing the '%s' process.", self.name) + self._kill_exceptions.append(e) + process.kill() + self._was_killed = True + logger.warning("The '%s' process has been killed.", self.name) + except psutil.NoSuchProcess: + # the process must have completed + pass + except (psutil.Error, psutil.AccessDenied, psutil.ZombieProcess) as e: + logger.exception("An error occurred while killing the '%s' process.", self.name) + self._kill_exceptions = e + finally: + self.read_output_from_proc() + + def set_exceptions(self): + if ( + self._exception is None + and (self._subprocess_kill_exceptions is None or len(self._subprocess_kill_exceptions) == 0) + and self._kill_exceptions is None + ): + return None + causes = [] + error_type = None + error_message = None + if self._exception is not None: + error_type = 'script-execution-error' + error_message = 'An error occurred curing the execution of a custom script.' + causes.append(self._exception) + if self._kill_exceptions is not None: + if error_type is None: + error_type = 'script-kill-error' + error_message = 'A custom script timed out, and an error occurred while terminating the process.' + causes.append(self._kill_exceptions) + if len(self._subprocess_kill_exceptions) > 0: + if error_type is None: + error_type = 'script-subprocess-kill-error' + error_message = 'A custom script timed out, and an error occurred while terminating one of its ' \ + 'subprocesses.' + for cause in self._subprocess_kill_exceptions: + causes.append(cause) + self.exception = POpenWithTimeout.ProcessError( + error_type, + error_message, + cause=causes) + + def read_output_from_proc(self): + if self.proc: + # self.proc could be none! + (exc_stdout, exc_stderr) = self.proc.communicate() + self.stdout = fsdecode(exc_stdout) + self.stderr = fsdecode(exc_stderr) + + # Clean stderr and stdout, removing duplicate and ending line breaks, which make the log hard to + # read and the log file bigger. + + # Clean stderr + if self.stderr: + if self.stderr.endswith(os.linesep): + self.stderr = self.stderr[:-1 * len(os.linesep)] + self.stderr = self.stderr.replace("{0}{0}".format(os.linesep, os.linesep), os.linesep) + + # Clean stdout + if self.stdout: + if self.stdout.endswith(os.linesep): + self.stdout = self.stdout[:-1*len(os.linesep)] + self.stdout = self.stdout.replace("{0}{0}".format(os.linesep, os.linesep), os.linesep) + + def log_command(self, args, timeout_seconds): + if len(args) < 1 or args[0] is None: + script_path = "" + else: + script_path = args[0].strip() + if len(args) > 1: + arguments_list = ["\"{0}\"".format(arg) for arg in args[1:]] + script_text = "\"{0}\" {1}".format(script_path, " ".join(arguments_list)) + else: + script_text = "\"{0}\"".format(script_path) + if timeout_seconds is not None: + logger.debug("Executing %s: %s", self.name, script_text) + else: + timeout_seconds_string = "no" + if timeout_seconds: + timeout_seconds_string = "a {0} second".format(timeout_seconds) + else: + timeout_seconds_string = "no" + logger.debug( + "Executing %s with %s timeout: %s", self.name, timeout_seconds_string, script_text) + + def log_console_and_errors(self): + # log + if self.stdout: + logger.debug( + "Console output (stdout) for '%s':%s", + self.name, + # add a tab after all line breaks + self.stdout.replace(os.linesep, "{0}\t".format(os.linesep)) + ) + + if self.stderr: + logger.error( + "Error output (stderr) for '%s':%s\t%s", + self.name, + os.linesep, + # add a tab after all line breaks + self.stderr.replace(os.linesep, "{0}\t".format(os.linesep)) + ) + + # run a command with the provided args, timeout in timeout_seconds + def run(self, args, timeout_seconds=None): + self.log_command(args, timeout_seconds) + self._run(args, timeout_seconds=timeout_seconds) + self.log_console_and_errors() + return self.return_code + + def _run(self, args, timeout_seconds=None): + if len(args) < 1 or args[0] is None: + self.error_message = "No script path was provided for {0}. Please enter a script path and try again.".format(self.name) + logger.error(self.error_message) + return + script_path = args[0].strip() + if len(script_path) == 0: + self.error_message = "No script path was provided for {0}. Please enter a script path and try again.".format(self.name) + logger.error(self.error_message) + return + + if not os.path.exists(script_path): + self.error_message = "The script at path '{0}' could not be found for for {1}. Please check your script" \ + " path and try again.".format(script_path, self.name) + logger.error(self.error_message) + return + + self.timeout_seconds = timeout_seconds + # Create, start and run the process and fill in stderr and stdout + def execute_process(args): + # get the lock so that we can start the process without encountering a timeout + self.lock.acquire() + has_error = False + try: + # don't start the process if we've already timed out + if not self.completed: + self.proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + else: + logger.error("The '%s' process was completed by the caller before it could be started.", self.name) + return + except (OSError, subprocess.CalledProcessError) as e: + logger.exception("An error occurred while executing '%s'", self.name) + self._exception = e + finally: + self.lock.release() + + self.read_output_from_proc() + try: + self.lock.acquire() + if not self.completed: + self.completed = True + finally: + self.lock.release() + + thread = threading.Thread(target=execute_process, args=[args]) + thread.daemon = True + # start the thread + thread.start() + # join the thread with a timeout + thread.join(timeout=self.timeout_seconds) + # check to see if the thread is alive + if thread.is_alive(): + self.lock.acquire() + try: + if not self.completed: + self._timed_out = self.timeout_seconds is not None + if self.proc is not None: + logger.error("The '%s' process has timed out before completing. Attempting to kill the " + "process.", self.name) + self.kill() + + self.completed = True + except AttributeError: + # It's possible that the process is killed AFTER we check for self.proc is None + # catch that here and pass + pass + finally: + self.lock.release() + + # read and set the return code if possible. + self.set_return_code() + # now set the success value + self._success = not (self._timed_out or self._was_killed or self._exception is not None or self.stderr or self.return_code != 0) + # set the error message + self.set_error_message() + + def set_return_code(self): + if self.proc: + self.return_code = self.proc.returncode + + def set_error_message(self): + if self._timed_out: + if self.stderr: + self.error_message = "The '{0}' timed out in {1} seconds. Errors were returned from the process, " \ + "see plugin_octolapse.log for details.".format( + self.name, self.timeout_seconds) + else: + self.error_message = "The '{0}' timed out in {1} seconds.".format(self.name, self.timeout_seconds) + return + + if self._exception is not None: + if self.stderr: + self.error_message = "The '{0}' raised an exception and returned error output. See " \ + "plugin_octolapse.log for details.".format(self.name) + else: + self.error_message = "The '{0}' raised an exception. See plugin_octolapse.log for details.".format(self.name) + return + + if self.return_code != 0: + if self.stderr: + self.error_message = "The '{0}' reported errors and returned a value of {1}, which indicates an " \ + "error. See plugin_octolapse.log for details.".format(self.name, self.return_code) + else: + self.error_message = "The'{0}' returned a value of {1}, which indicates an error.".format( + self.name, self.return_code) + + if self.proc is None: + self.error_message = "The '{0}' process does not exist. This is unusual, and indicates a bug or other " \ + "unexpected issue. Check plugin_octolapse.log.".format(self.name) + + if self.stderr: + # if we have no error and a null proc, report this. + self.error_message = ( + "The '{0}' process returned errors. See plugin_octolapse.log for details".format(self.name) + ) + + +class POpenWithTimeoutAsync(object): + + class ProcessError(Exception): + def __init__(self, error_type, message, cause=None): + super(POpenWithTimeoutAsync.ProcessError, self).__init__() + self.error_type = error_type + self.cause = cause if cause is not None else None + self.message = message + + def __str__(self): + if self.cause is None: + return "{}: {}".format(self.error_type, self.message) + if isinstance(self.cause, list): + if len(self.cause) > 1: + error_string = "{}: {} - Inner Exceptions".format(self.error_type, self.message) + error_count = 1 + for cause in self.cause: + error_string += "{} {}: {} Exception - {}".format(os.linesep, error_count, type(cause).__name__, cause) + error_count += 1 + return error_string + elif len(self.cause) == 1: + return "{}: {} - Inner Exception: {}".format(self.error_type, self.message, self.cause[0]) + return "{}: {} - Inner Exception: {}".format(self.error_type, self.message, self.cause) + + lock = threading.Lock() + + def __init__(self, on_stdout_line_received=None, on_stderr_line_received=None): + self.name = "Unknown" + self.proc = None + self.stdout_lines = [] + self.stderr_lines = [] + self.error_message = None + self.completed = False + self._exception = None + self._subprocess_kill_exceptions = [] + self._kill_exceptions = None + self.exception = None + self.timeout_seconds = None + self.return_code = -100 + self._timed_out = False + self._was_killed = False + self._success = False + self.stdout_line_received_callback = on_stdout_line_received + self.stderr_line_received_callback = on_stderr_line_received + + def success(self): + return self._success + + def kill(self): + if self.proc is None: + return + try: + process = psutil.Process(self.proc.pid) + for proc in process.children(recursive=True): + try: + proc.kill() + except psutil.NoSuchProcess: + # the process must have completed + pass + except (psutil.Error,psutil.AccessDenied, psutil.ZombieProcess) as e: + logger.exception("An error occurred while killing the '%s' process.", self.name) + self._kill_exceptions.append(e) + process.kill() + self._was_killed = True + logger.warning("The '%s' process has been killed.", self.name) + except psutil.NoSuchProcess: + # the process must have completed + pass + except (psutil.Error, psutil.AccessDenied, psutil.ZombieProcess) as e: + logger.exception("An error occurred while killing the '%s' process.", self.name) + self._kill_exceptions = e + + def set_exceptions(self): + if ( + self._exception is None + and (self._subprocess_kill_exceptions is None or len(self._subprocess_kill_exceptions) == 0) + and self._kill_exceptions is None + ): + return None + causes = [] + error_type = None + error_message = None + if self._exception is not None: + error_type = 'script-execution-error' + error_message = 'An error occurred curing the execution of a custom script.' + causes.append(self._exception) + if self._kill_exceptions is not None: + if error_type is None: + error_type = 'script-kill-error' + error_message = 'A custom script timed out, and an error occurred while terminating the process.' + causes.append(self._kill_exceptions) + if len(self._subprocess_kill_exceptions) > 0: + if error_type is None: + error_type = 'script-subprocess-kill-error' + error_message = 'A custom script timed out, and an error occurred while terminating one of its ' \ + 'subprocesses.' + for cause in self._subprocess_kill_exceptions: + causes.append(cause) + self.exception = POpenWithTimeoutAsync.ProcessError( + error_type, + error_message, + cause=causes) + + def read_output_from_proc(self): + if self.proc: + # self.proc could be none! + (exc_stdout, exc_stderr) = self.proc.communicate() + self.stdout = fsdecode(exc_stdout) + self.stderr = fsdecode(exc_stderr) + + # Clean stderr and stdout, removing duplicate and ending line breaks, which make the log hard to + # read and the log file bigger. + + # Clean stderr + if self.stderr: + if self.stderr.endswith(os.linesep): + self.stderr = self.stderr[:-1 * len(os.linesep)] + self.stderr = self.stderr.replace("{0}{0}".format(os.linesep, os.linesep), os.linesep) + + # Clean stdout + if self.stdout: + if self.stdout.endswith(os.linesep): + self.stdout = self.stdout[:-1*len(os.linesep)] + self.stdout = self.stdout.replace("{0}{0}".format(os.linesep, os.linesep), os.linesep) + + def log_command(self, args, timeout_seconds): + command_string = subprocess.list2cmdline(args) + if timeout_seconds is not None: + logger.debug("Executing %s: %s", self.name, command_string) + else: + if timeout_seconds: + timeout_seconds_string = "a {0} second".format(timeout_seconds) + else: + timeout_seconds_string = "No Timeout" + logger.debug( + "Executing %s with %s timeout: %s", self.name, timeout_seconds_string, command_string) + + @staticmethod + def _read_std_line(line, type_name, callback): + if not line: + return None + line = fsdecode(line) + # remove extra line breaks, so that each line is actually a single line. Should work for windows too. + if line.endswith('\n'): + line = line[:-1 * len(os.linesep)] + if line.endswith('\r'): + line = line[:-1 * len(os.linesep)] + + logger.info("%s: %s", type_name, line) + if callback: + callback(line) + return line + + def _read_stdout_lines(self, proc): + try: + while proc.returncode is None: + line = proc.stdout.readline() + line = POpenWithTimeoutAsync._read_std_line(line, 'stdout', self.stdout_line_received_callback) + if line: + self.stdout_lines.append(line) + proc.poll() + except Exception as e: + logger.exception("An error occurred while reading stdout.") + raise e + + def _read_stderr_lines(self, proc): + try: + while proc.returncode is None: + line = proc.stderr.readline() + line = POpenWithTimeoutAsync._read_std_line(line, 'stderr', self.stderr_line_received_callback) + if line: + self.stderr_lines.append(line) + proc.poll() + except Exception as e: + logger.exception("An error occurred while reading stderr.") + raise e + # run a command with the provided args, timeout in timeout_seconds + def run(self, args, timeout_seconds=None): + self.log_command(args, timeout_seconds) + self._run(args, timeout_seconds=timeout_seconds) + return self.return_code + + def _run(self, args, timeout_seconds=None): + if len(args) < 1 or args[0] is None: + self.error_message = "No script path was provided for {0}. Please enter a script path and try again.".format(self.name) + logger.error(self.error_message) + return + script_path = args[0].strip() + if len(script_path) == 0: + self.error_message = "No script path was provided for {0}. Please enter a script path and try again.".format(self.name) + logger.error(self.error_message) + return + + if not os.path.exists(script_path): + self.error_message = "The script at path '{0}' could not be found for for {1}. Please check your script" \ + " path and try again.".format(script_path, self.name) + logger.error(self.error_message) + return + + self.timeout_seconds = timeout_seconds + # Create, start and run the process and fill in stderr and stdout + def execute_process(args): + # get the lock so that we can start the process without encountering a timeout + with self.lock: + try: + # don't start the process if we've already timed out + if not self.completed: + self.proc = subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True + ) + # create threads to read stdin and stdout + stdout_reader = threading.Thread(target=self._read_stdout_lines, args=[self.proc]) + stdout_reader.daemon = True + stderr_reader = threading.Thread(target=self._read_stderr_lines, args=[self.proc]) + stderr_reader.daemon = True + stdout_reader.start() + stderr_reader.start() + self.proc.wait() + stdout_reader.join() + stderr_reader.join() + else: + logger.error("The '%s' process was completed by the caller before it could be started.", self.name) + return + except (OSError, subprocess.CalledProcessError) as e: + logger.exception("An error occurred while executing '%s'", self.name) + self._exception = e + return + + thread = threading.Thread(target=execute_process, args=[args]) + thread.daemon = True + # start the thread + thread.start() + # join the thread with a timeout + thread.join(timeout=self.timeout_seconds) + # check to see if the thread is alive + if thread.is_alive(): + self.lock.acquire() + try: + if not self.completed: + self._timed_out = self.timeout_seconds is not None + if self.proc is not None: + logger.error("The '%s' process has timed out before completing. Attempting to kill the " + "process.", self.name) + self.kill() + + self.completed = True + except AttributeError: + # It's possible that the process is killed AFTER we check for self.proc is None + # catch that here and pass + pass + finally: + self.lock.release() + + # read and set the return code if possible. + self.set_return_code() + # now set the success value + self._success = not ( + self._timed_out or + self._was_killed or + self._exception is not None or + len(self.stderr_lines) > 0 + or self.return_code != 0 + ) + # set the error message + self.set_error_message() + + def set_return_code(self): + if self.proc: + self.return_code = self.proc.returncode + + def set_error_message(self): + if self._timed_out: + if self.stderr: + self.error_message = "The '{0}' timed out in {1} seconds. Errors were returned from the process, " \ + "see plugin_octolapse.log for details.".format( + self.name, self.timeout_seconds) + else: + self.error_message = "The '{0}' timed out in {1} seconds.".format(self.name, self.timeout_seconds) + return + + if self._exception is not None: + if self.stderr: + self.error_message = "The '{0}' raised an exception and returned error output. See " \ + "plugin_octolapse.log for details.".format(self.name) + else: + self.error_message = "The '{0}' raised an exception. See plugin_octolapse.log for details.".format(self.name) + return + + if self.return_code != 0: + if len(self.stderr_lines) > 0: + self.error_message = "The '{0}' reported errors and returned a value of {1}, which indicates an " \ + "error. See plugin_octolapse.log for details.".format(self.name, self.return_code) + else: + self.error_message = "The'{0}' returned a value of {1}, which indicates an error.".format( + self.name, self.return_code) + return + + if self.proc is None: + if len(self.stderr_lines) > 0: + self.error_message = ( + "The '{0}' process does not exist, but returned errors. This is unusual, and" + " indicates a bug or other unexpected issue. Check plugin_octolapse.log.".format(self.name) + ) + else: + self.error_message = ( + "The '{0}' process does not exist. This is unusual, and indicates a bug or other " + "unexpected issue. Check plugin_octolapse.log.".format(self.name) + ) + return + + if len(self.stderr_lines) > 0: + # if we have no error and a null proc, report this. + self.error_message = ( + "The '{0}' process returned errors. See plugin_octolapse.log for details".format(self.name) + ) + return + + +class CameraScriptSnapshot(POpenWithTimeout): + def __init__(self, script_path, camera_name, snapshot_number, delay_seconds, data_directory, snapshot_directory, + snapshot_filename, snapshot_full_path, timeout_seconds=None): + super(CameraScriptSnapshot, self).__init__() + self.name = "{0} - Snapshot Camera Script".format(camera_name) + self.script_path = script_path + self.timeout_seconds = timeout_seconds + self.camera_name = camera_name + self.snapshot_number = snapshot_number + self.delay_seconds = delay_seconds + self.data_directory = data_directory + self.snapshot_directory = snapshot_directory + self.snapshot_filename = snapshot_filename + self.snapshot_full_path = snapshot_full_path + + def get_args(self): + return [ + self.script_path, + "{}".format(self.snapshot_number), + "{}".format(self.delay_seconds), + self.data_directory, + self.snapshot_directory, + self.snapshot_filename, + self.snapshot_full_path, + self.camera_name + ] + + def run(self): + return super(CameraScriptSnapshot, self).run(self.get_args(), self.timeout_seconds) + + +class CameraScriptBeforeAfterSnapshot(POpenWithTimeout): + def __init__(self, script_path, camera_name, snapshot_number, delay_seconds, data_directory, snapshot_directory, snapshot_filename, snapshot_full_path, timeout_seconds=None): + super(CameraScriptBeforeAfterSnapshot, self).__init__() + self.name = "Before/After Snapshot Script" + self.script_path = script_path + self.timeout_seconds = timeout_seconds + self.camera_name = camera_name + self.snapshot_number = snapshot_number + self.delay_seconds = delay_seconds + self.data_directory = data_directory + self.snapshot_directory = snapshot_directory + self.snapshot_filename = snapshot_filename + self.snapshot_full_path = snapshot_full_path + + def get_args(self): + return [ + self.script_path, + "{}".format(self.snapshot_number), + "{}".format(self.delay_seconds), + self.data_directory, + self.snapshot_directory, + self.snapshot_filename, + self.snapshot_full_path, + self.camera_name + ] + + def run(self): + return super(CameraScriptBeforeAfterSnapshot, self).run(self.get_args(), self.timeout_seconds) + + +class CameraScriptBeforeSnapshot(CameraScriptBeforeAfterSnapshot): + def __init__(self, script_path, camera_name, snapshot_number, delay_seconds, data_directory, snapshot_directory, snapshot_filename, snapshot_full_path, timeout_seconds=None): + super(CameraScriptBeforeSnapshot, self).__init__(script_path, camera_name, snapshot_number, delay_seconds, data_directory, snapshot_directory, snapshot_filename, snapshot_full_path, timeout_seconds) + self.name = "{0} - Before Snapshot Camera Script".format(camera_name) + + +class CameraScriptAfterSnapshot(CameraScriptBeforeAfterSnapshot): + def __init__(self, script_path, camera_name, snapshot_number, delay_seconds, data_directory, snapshot_directory, snapshot_filename, snapshot_full_path, timeout_seconds=None): + super(CameraScriptAfterSnapshot, self).__init__(script_path, camera_name, snapshot_number, delay_seconds, data_directory, snapshot_directory, snapshot_filename, snapshot_full_path, timeout_seconds) + self.name = "{0} - After Snapshot Camera Script".format(camera_name) + + +class CameraScriptBeforeAfterPrint(POpenWithTimeout): + def __init__(self, script_path, camera_name, timeout_seconds=None): + super(CameraScriptBeforeAfterPrint, self).__init__() + self.script_path = script_path + self.timeout_seconds = timeout_seconds + self.camera_name = camera_name + + def get_args(self): + return [ + self.script_path, + self.camera_name + ] + + def run(self): + return super(CameraScriptBeforeAfterPrint, self).run(self.get_args(), self.timeout_seconds) + + +class CameraScriptBeforePrint(CameraScriptBeforeAfterPrint): + def __init__(self, script_path, camera_name, timeout_seconds=None): + super(CameraScriptBeforePrint, self).__init__(script_path, camera_name, timeout_seconds) + self.name = "{0} - Before Print Camera Script".format(camera_name) + + +class CameraScriptAfterPrint(CameraScriptBeforeAfterPrint): + def __init__(self, script_path, camera_name, timeout_seconds=None): + super(CameraScriptAfterPrint, self).__init__(script_path, camera_name, timeout_seconds) + self.name = "{0} - After Print Camera Script".format(camera_name) + + +class CameraScriptBeforeRender(POpenWithTimeout): + def __init__(self, script_path, camera_name, snapshot_directory, snapshot_filename_format, snapshot_path_format, + timeout_seconds=None): + super(CameraScriptBeforeRender, self).__init__() + self.name = "{0} - Before Render Camera Script".format(camera_name) + self.script_path = script_path + self.timeout_seconds = timeout_seconds + self.camera_name = camera_name + self.snapshot_directory = snapshot_directory + self.snapshot_filename_format = snapshot_filename_format + self.snapshot_path_format = snapshot_path_format + + def get_args(self): + return [ + self.script_path, + self.camera_name, + self.snapshot_directory, + self.snapshot_filename_format, + self.snapshot_path_format, + ] + + def run(self): + return super(CameraScriptBeforeRender, self).run(self.get_args(), self.timeout_seconds) + + +class CameraScriptAfterRender(POpenWithTimeout): + def __init__(self, script_path, camera_name, snapshot_directory, snapshot_filename_format, snapshot_path_format, + timelapse_directory, timelapse_filename, timelapse_extension, timelapse_full_path, + timeout_seconds=None): + super(CameraScriptAfterRender, self).__init__() + self.name = "{0} - After Render Camera Script".format(camera_name) + self.script_path = script_path + self.timeout_seconds = timeout_seconds + self.camera_name = camera_name + self.snapshot_directory = snapshot_directory + self.snapshot_filename_format = snapshot_filename_format + self.snapshot_path_format = snapshot_path_format + self.timelapse_directory = timelapse_directory + self.timelapse_filename = timelapse_filename + self.timelapse_extension = timelapse_extension + self.timelapse_full_path = timelapse_full_path + + def get_args(self): + return [ + self.script_path, + self.camera_name, + self.snapshot_directory, + self.snapshot_filename_format, + self.snapshot_path_format, + self.timelapse_directory, + self.timelapse_filename, + self.timelapse_extension, + self.timelapse_full_path + ] + + def run(self): + return super(CameraScriptAfterRender, self).run(self.get_args(), self.timeout_seconds) diff --git a/octoprint_octolapse/settings.py b/octoprint_octolapse/settings.py index 5cca5bbc..d861824a 100644 --- a/octoprint_octolapse/settings.py +++ b/octoprint_octolapse/settings.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -28,15 +28,26 @@ import uuid import tempfile import sys -from distutils.version import LooseVersion +import re +import errno +from octoprint_octolapse_setuptools import NumberedVersion import octoprint_octolapse.utility as utility import octoprint_octolapse.log as log import math -import collections -import octoprint_octolapse.gcode_preprocessing as gcode_preprocessing -import octoprint_octolapse.settings_migration as settings_migration -import six - +# remove python 2 support +#try: +# from collections.abc import Iterable +#except ImportError: +# # Python 2.7 +# from collections import Iterable +from collections.abc import Iterable +import octoprint_octolapse.settings_preprocessor as settings_preprocessor +import octoprint_octolapse.migration as migration +from octoprint_octolapse.gcode_processor import GcodeProcessor, ParsedCommand +# remove unused usings +# import six +from octoprint_octolapse.error_messages import OctolapseException +import inspect # create the module level logger from octoprint_octolapse.log import LoggingConfigurator logging_configurator = LoggingConfigurator() @@ -45,7 +56,11 @@ class SettingsJsonEncoder(json.JSONEncoder): def default(self, obj): - if isinstance(obj, Settings) or isinstance(obj, StaticSettings): + if issubclass(type(obj), Settings): + # Remove python 2 support + # return {k: v for (k, v) in six.iteritems(obj.to_dict()) if not k.startswith("_")} + return {k: v for (k, v) in obj.to_dict().items() if not k.startswith("_")} + elif issubclass(type(obj), StaticSettings): return obj.__dict__ # Let the base class default method raise the TypeError elif isinstance(obj, bool): @@ -78,14 +93,23 @@ class StaticSettings(object): class Settings(object): + def clone(self): return copy.deepcopy(self) def to_dict(self): - return self.__dict__.copy() + copy_dict = self.__dict__.copy() + property_list = [p for p in dir(self) if isinstance(getattr(type(self), p, None), property)] + for prop in property_list: + copy_dict[prop] = getattr(self, prop) + return copy_dict def to_json(self): - return json.dumps(self.to_dict(), cls=SettingsJsonEncoder) + # remove private variables + # Remove python 2 support + # filtered_dict = {k: v for (k, v) in six.iteritems(self.to_dict()) if not k.startswith("_")} + filtered_dict = {k: v for (k, v) in self.to_dict().items() if not k.startswith("_")} + return json.dumps(filtered_dict, cls=SettingsJsonEncoder) @classmethod def encode_json(cls, o): @@ -100,19 +124,28 @@ def _update(source, iterable): return item_to_iterate = iterable - if not isinstance(iterable, collections.Iterable): - item_to_iterate = iterable.__dict__ + if not isinstance(iterable, Iterable): + to_dict = getattr(iterable, "to_dict", None) + if callable(to_dict): + item_to_iterate = iterable.to_dict() + else: + item_to_iterate = iterable.__dict__ for key, value in item_to_iterate.items(): try: + if key.startswith("_"): + continue class_item = getattr(source, key, '{octolapse_no_property_found}') - if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): if isinstance(class_item, Settings): class_item.update(value) elif isinstance(class_item, StaticSettings): pass else: - source.__dict__[key] = source.try_convert_value(class_item, value, key) + # todo - Do not set the dict directly in other places. Consider rewriting whole file since this methos of updating/serializing/deserializing is extremely painful + setattr(source, key, source.try_convert_value(class_item, value, key)) except Exception as e: logger.exception("Settings._update - Failed to update settings. Key:%s, Value:%s", key, value) continue @@ -127,10 +160,11 @@ def try_convert_value(cls, destination, value, key): elif isinstance(destination, bool): return bool(value) elif isinstance(destination, int): + floatValue = float(value) # sometimes the destination can be an int, but the value is a float - if int(value) == float(value): - return int(value) - return float(value) + if int(floatValue) == floatValue: + return int(floatValue) + return floatValue else: # default action, just return the value return value @@ -155,6 +189,15 @@ def create_from(cls, iterable=()): new_object.update(iterable) return new_object + def __setattr__(self, a, v): + attr = getattr(self.__class__, a, None) + if isinstance(attr, property): + if attr.fset is None: + raise AttributeError("No setter is available for the current attribute.") + attr.fset(self, v) + else: + super(Settings, self).__setattr__(a, v) + class ProfileSettings(Settings): @@ -186,7 +229,7 @@ def is_updatable_from_server(self): return False for key in self.key_values: - if not key: + if "value" not in key or key["value"] is None or key["value"] == "null": return False return not self.is_custom @@ -199,7 +242,7 @@ def get_server_update_identifiers_dict(self): } def suppress_updates(self, available_profile): - if LooseVersion(self.version) < LooseVersion(available_profile["version"]): + if NumberedVersion(self.version) < NumberedVersion(available_profile["version"]): self.suppress_update_notification_version = available_profile["version"] def try_convert_value(cls, destination, value, key): @@ -261,13 +304,28 @@ def suppress_updates(self, available_profile): self.automatic_configuration.suppress_updates(available_profile) +class ExtruderOffset(Settings): + def __init__(self): + self.x = 0 + self.y = 0 + + def to_dict(self): + return { + "x": self.x, + "y": self.y + } + + class PrinterProfile(AutomaticConfigurationProfile): + OCTOLAPSE_COMMAND = "@OCTOLAPSE" + DEFAULT_OCTOLAPSE_SNAPSHOT_COMMAND = "TAKE-SNAPSHOT" + LEGACY_SNAPSHOT_COMMAND = "SNAP" minimum_height_increment = 0.05 bed_type_rectangular = 'rectangular' bed_type_circular = 'circular' origin_type_front_left = 'front_left' origin_type_center = 'center' - + def __init__(self, name="New Printer Profile"): super(PrinterProfile, self).__init__(name) # flag that is false until the profile has been saved by the user at least once @@ -278,14 +336,14 @@ def __init__(self, name="New Printer Profile"): self.slicer_type = "automatic" self.gcode_generation_settings = OctolapseGcodeSettings() self.slicers = PrinterProfileSlicers() - self.snapshot_command = "snap" + self._snapshot_command_text = "" + self._parsed_snapshot_command = ParsedCommand(None,None,None) self.suppress_snapshot_command_always = True self.auto_detect_position = True self.origin_type = PrinterProfile.origin_type_front_left self.home_x = None self.home_y = None self.home_z = None - self.abort_out_of_bounds = True self.override_octoprint_profile_settings = False self.bed_type = PrinterProfile.bed_type_rectangular self.diameter_xy = 0.0 @@ -310,15 +368,52 @@ def __init__(self, name="New Printer Profile"): self.auto_position_detection_commands = "" self.priming_height = 0.75 # Extrusion must occur BELOW this level before layer tracking will begin self.minimum_layer_height = 0.05 # Layer tracking won't start until extrusion at this height is reached. - self.e_axis_default_mode = 'require-explicit' # other values are 'relative' and 'absolute' - self.g90_influences_extruder = 'use-octoprint-settings' # other values are 'true' and 'false' - self.xyz_axes_default_mode = 'require-explicit' # other values are 'relative' and 'absolute' + self.e_axis_default_mode = 'absolute' # other values are 'relative' and 'absolute' + self.g90_influences_extruder = 'false' # other values are 'true' and 'false' + self.xyz_axes_default_mode = 'absolute' # other values are 'relative' and 'absolute' self.units_default = 'millimeters' self.axis_speed_display_units = 'mm-min' self.default_firmware_retractions = False self.default_firmware_retractions_zhop = False self.gocde_axis_compatibility_mode_enabled = True self.home_axis_gcode = "G90; Switch to Absolute XYZ\r\nG28 X Y; Home XY Axis" + self.num_extruders = 1 + self.shared_extruder = False + self.zero_based_extruder = True + self.extruder_offsets = [] + self.default_extruder = 1 # The default extruder is 1 based! It is not an index. + + @property + def snapshot_command(self): + return self._snapshot_command_text + + @snapshot_command.setter + def snapshot_command(self, snapshot_command_text): + parsed_command = GcodeProcessor.parse(snapshot_command_text) + self._parsed_snapshot_command = parsed_command + self._snapshot_command_text = snapshot_command_text + return len(parsed_command.gcode) > 0 + + def get_snapshot_command_gcode(self): + if self._parsed_snapshot_command.gcode is None or len(self._parsed_snapshot_command.gcode) == 0: + return None + return self._parsed_snapshot_command.gcode + + @staticmethod + def validate_snapshot_command(command_string): + # there needs to be at least one non-comment non-whitespace character for the gcode command to work. + parsed_command = GcodeProcessor.parse(command_string) + return len(parsed_command.gcode)>0 + + def is_snapshot_command(self, command_string): + if command_string is None or len(command_string) == 0: + return False + snapshot_command_gcode = self.get_snapshot_command_gcode() + return ( + (snapshot_command_gcode and snapshot_command_gcode == command_string) or + command_string.startswith("{0} {1}".format(PrinterProfile.OCTOLAPSE_COMMAND, PrinterProfile.DEFAULT_OCTOLAPSE_SNAPSHOT_COMMAND)) or + PrinterProfile.LEGACY_SNAPSHOT_COMMAND == command_string + ) @classmethod def update_from_server_profile(cls, current_profile, server_profile_dict): @@ -364,6 +459,7 @@ def _get_overridable_settings_dict(self): settings_dict = {} volume_dict = {} volume_dict["bed_type"] = self.bed_type + volume_dict["origin_type"] = self.origin_type if self.custom_bounding_box: # set the bounds to the custom box volume_dict["min_x"] = float(self.min_x) @@ -425,6 +521,8 @@ def get_octoprint_settings_dict(octoprint_printer_profile): else: volume_dict["bed_type"] = "rectangular" + volume_dict["origin_type"] = volume["origin"] + if custom_box: # set the bounds to the custom box volume_dict["min_x"] = float(custom_box["x_min"]) @@ -493,7 +591,7 @@ def get_options(): dict(value='cura_4_2', name='Cura V4.2 and Above'), dict(value='cura', name='Cura V4.1 and Below'), dict(value='simplify-3d', name='Simplify 3D'), - dict(value='slic3r-pe', name='Slic3r Prusa Edition'), + dict(value='slic3r-pe', name='Slic3r/Slic3r PE/Prusa Slicer'), dict(value='other', name='Other Slicer') ], 'e_axis_default_mode_options': [ @@ -525,11 +623,6 @@ def get_options(): dict(value='inches', name='Inches'), dict(value='millimeters', name='Millimeters') ], - 'cura_surface_mode_options': [ - dict(value="normal", name='Normal'), - dict(value="surface", name='Surface'), - dict(value="both", name='Both'), - ], 'cura_combing_mode_options': [ dict(value="off", name='Off'), dict(value="all", name='All'), @@ -556,20 +649,19 @@ def get_slicer_settings_by_type(self, slicer_type): def get_current_state_detection_settings(self): if self.slicer_type == 'automatic': return None - return self.get_current_slicer_settings().get_gcode_generation_settings(slicer_type=self.slicer_type) def get_gcode_settings_from_file(self, gcode_file_path): - simplify_preprocessor = gcode_preprocessing.Simplify3dSettingsProcessor( + simplify_preprocessor = settings_preprocessor.Simplify3dSettingsProcessor( search_direction="both", max_forward_search=1000, max_reverse_search=1000 ) - slic3r_preprocessor = gcode_preprocessing.Slic3rSettingsProcessor( + slic3r_preprocessor = settings_preprocessor.Slic3rSettingsProcessor( search_direction="both", max_forward_search=1000, max_reverse_search=1000 ) - cura_preprocessor = gcode_preprocessing.CuraSettingsProcessor( + cura_preprocessor = settings_preprocessor.CuraSettingsProcessor( search_direction="both", max_forward_search=1000, max_reverse_search=1000 ) - file_processor = gcode_preprocessing.GcodeFileProcessor( + file_processor = settings_preprocessor.GcodeFileProcessor( [simplify_preprocessor, slic3r_preprocessor, cura_preprocessor], 1, None ) results = file_processor.process_file(gcode_file_path, filter_tags=['octolapse_setting']) @@ -600,7 +692,8 @@ def get_gcode_settings_from_file(self, gcode_file_path): else: raise Exception("An invalid slicer type has been detected while extracting settings from gcode.") - new_slicer_settings.update_settings_from_gcode(new_settings) + new_slicer_settings.update_settings_from_gcode(new_settings, self) + # check to make sure all of the required settings are there missing_settings = new_slicer_settings.get_missing_gcode_generation_settings(slicer_type=self.slicer_type) if len(missing_settings) > 0: @@ -630,22 +723,48 @@ def get_location_detection_command_list(self): return [] def try_convert_value(cls, destination, value, key): - if key in ['home_x','home_y','home_z']: + if key in ['home_x', 'home_y', 'home_z']: if value is not None: return float(value) + elif key == "extruder_offsets" and isinstance(value, list): + result = [] + for offset in value: + extruder_offset = ExtruderOffset() + extruder_offset.update(offset) + result.append(extruder_offset) + return result return super(PrinterProfile, cls).try_convert_value(destination, value, key) def get_position_args(self, overridable_profile_settings): + # get the settings that cannot be overridden by Octoprint settings + + # Get the number of extruders from the current gocode generation settings + gcode_generation_settings = self.get_current_state_detection_settings() + num_extruders = gcode_generation_settings.get_num_extruders() + + default_extruder = self.default_extruder + if num_extruders < default_extruder: + # we must have used automatic gcode settings extraction. + default_extruder = num_extruders + + extruder_offsets = [] + if not self.shared_extruder and self.num_extruders > 1: + extruder_offsets = [x.to_dict() for x in self.extruder_offsets] position_args = { "location_detection_commands": self.get_location_detection_command_list(), "xyz_axis_default_mode": self.xyz_axes_default_mode, "e_axis_default_mode": self.e_axis_default_mode, "units_default": self.units_default, "autodetect_position": self.auto_detect_position, - "slicer_settings": self.gcode_generation_settings.to_dict(), + "slicer_settings": gcode_generation_settings.to_dict(), + "zero_based_extruder": self.zero_based_extruder, "priming_height": self.priming_height, "minimum_layer_height": self.minimum_layer_height, + "num_extruders": num_extruders, + "shared_extruder": self.shared_extruder, + "default_extruder_index": default_extruder - 1, # The default extruder is 1 based! + "extruder_offsets": extruder_offsets, "home_position": { "home_x": None if self.home_x is None else float(self.home_x), "home_y": None if self.home_y is None else float(self.home_y), @@ -776,6 +895,13 @@ def __init__(self, name="New Stabilization Profile"): self.y_relative_path = "50" self.y_relative_path_loop = True self.y_relative_path_invert_loop = True + self.wait_for_moves_to_finish = True + + def is_disabled(self): + return ( + self.x_type == StabilizationProfile.STABILIZATION_AXIS_TYPE_DISABLED and + self.y_type == StabilizationProfile.STABILIZATION_AXIS_TYPE_DISABLED + ) def get_stabilization_paths(self): x_stabilization_path = StabilizationPath() @@ -855,14 +981,11 @@ def try_convert_value(cls, destination, value, key): class TriggerProfile(AutomaticConfigurationProfile): TRIGGER_TYPE_REAL_TIME = "real-time" - TRIGGER_TYPE_SNAP_TO_PRINT = "snap-to-print" - TRIGGER_TYPE_SMART_LAYER = "smart-layer" - SMART_TRIGGER_TYPE_FASTEST = 0 + TRIGGER_TYPE_SMART = "smart" + SMART_TRIGGER_TYPE_SNAP_TO_PRINT = 0 SMART_TRIGGER_TYPE_FAST = 1 SMART_TRIGGER_TYPE_COMPATIBILITY = 2 - SMART_TRIGGER_TYPE_NORMAL_QUALITY = 3 - SMART_TRIGGER_TYPE_HIGH_QUALITY = 4 - SMART_TRIGGER_TYPE_BEST_QUALITY = 5 + SMART_TRIGGER_TYPE_HIGH_QUALITY = 3 EXTRUDER_TRIGGER_IGNORE_VALUE = "" EXTRUDER_TRIGGER_REQUIRED_VALUE = "trigger_on" EXTRUDER_TRIGGER_FORBIDDEN_VALUE = "forbidden" @@ -872,14 +995,13 @@ class TriggerProfile(AutomaticConfigurationProfile): def __init__(self, name="New Trigger Profile"): super(TriggerProfile, self).__init__(name) - self.trigger_type = TriggerProfile.TRIGGER_TYPE_SMART_LAYER + self.trigger_type = TriggerProfile.TRIGGER_TYPE_SMART # smart layer trigger options self.smart_layer_trigger_type = TriggerProfile.SMART_TRIGGER_TYPE_COMPATIBILITY - self.smart_layer_trigger_speed_threshold = 0 - self.smart_layer_trigger_distance_threshold_percent = 10 - self.smart_layer_snap_to_print = False + self.smart_layer_snap_to_print_high_quality = False + self.smart_layer_snap_to_print_smooth = False self.smart_layer_disable_z_lift = True - + self.allow_smart_snapshot_commands = True # Settings that were formerly in the snapshot profile (now removed) self.is_default = False self.trigger_subtype = TriggerProfile.LAYER_TRIGGER_TYPE @@ -908,13 +1030,9 @@ def __init__(self, name="New Trigger Profile"): self.trigger_on_deretracted = TriggerProfile.EXTRUDER_TRIGGER_FORBIDDEN_VALUE def get_snapshot_plan_options(self): - if self.trigger_type == TriggerProfile.TRIGGER_TYPE_SNAP_TO_PRINT: - return { - 'disable_z_lift': self.snap_to_print_disable_z_lift, - } if ( - self.trigger_type == TriggerProfile.TRIGGER_TYPE_SMART_LAYER and - self.smart_layer_snap_to_print + self.trigger_type == TriggerProfile.TRIGGER_TYPE_SMART and + self.smart_layer_trigger_type == TriggerProfile.SMART_TRIGGER_TYPE_SNAP_TO_PRINT ): return { 'disable_z_lift': self.smart_layer_disable_z_lift @@ -924,8 +1042,7 @@ def get_snapshot_plan_options(self): @staticmethod def get_precalculated_trigger_types(): return [ - TriggerProfile.TRIGGER_TYPE_SNAP_TO_PRINT, - TriggerProfile.TRIGGER_TYPE_SMART_LAYER + TriggerProfile.TRIGGER_TYPE_SMART ] def get_extruder_trigger_value_string(self, value): @@ -941,7 +1058,7 @@ def get_options(): return { 'trigger_type_options': [ dict(value=TriggerProfile.TRIGGER_TYPE_REAL_TIME, name='Real-Time Triggers'), - dict(value=TriggerProfile.TRIGGER_TYPE_SMART_LAYER, name='Smart Layer Trigger') + dict(value=TriggerProfile.TRIGGER_TYPE_SMART, name='Smart Triggers') ], 'real_time_xy_trigger_type_options': [ dict(value='disabled', name='Disabled'), dict(value='fixed_coordinate', name='Fixed Coordinate'), @@ -949,12 +1066,10 @@ def get_options(): dict(value='relative', name='Relative Coordinate (0-100)'), dict(value='relative_path', name='List of Relative Coordinates') ], 'smart_layer_trigger_type_options': [ - dict(value='{}'.format(TriggerProfile.SMART_TRIGGER_TYPE_FASTEST), name='Fastest'), dict(value='{}'.format(TriggerProfile.SMART_TRIGGER_TYPE_FAST), name='Fast'), dict(value='{}'.format(TriggerProfile.SMART_TRIGGER_TYPE_COMPATIBILITY), name='Compatibility'), - dict(value='{}'.format(TriggerProfile.SMART_TRIGGER_TYPE_NORMAL_QUALITY), name='Normal Quality'), dict(value='{}'.format(TriggerProfile.SMART_TRIGGER_TYPE_HIGH_QUALITY), name='High Quality'), - dict(value='{}'.format(TriggerProfile.SMART_TRIGGER_TYPE_BEST_QUALITY), name='Best Quality'), + dict(value='{}'.format(TriggerProfile.SMART_TRIGGER_TYPE_SNAP_TO_PRINT), name='Snap to Print'), ], 'trigger_subtype_options': [ dict(value=TriggerProfile.LAYER_TRIGGER_TYPE, name="Layer/Height"), dict(value=TriggerProfile.TIMER_TRIGGER_TYPE, name="Timer"), @@ -974,7 +1089,9 @@ def get_options(): @staticmethod def get_extruder_trigger_value(value): - if isinstance(value, six.string_types): + # Remove python 2 support + #if isinstance(value, six.string_types): + if isinstance(value, str): if value is None or len(value) == 0: return None elif value.lower() == TriggerProfile.EXTRUDER_TRIGGER_REQUIRED_VALUE: @@ -1025,6 +1142,7 @@ def try_convert_value(cls, destination, value, key): class RenderingProfile(AutomaticConfigurationProfile): + default_output_template = "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}" def __init__(self, name="New Rendering Profile"): super(RenderingProfile, self).__init__(name) self.enabled = True @@ -1036,9 +1154,10 @@ def __init__(self, name="New Rendering Profile"): self.output_format = 'mp4' self.sync_with_timelapse = True self.bitrate = "8000K" + self.constant_rate_factor = 28 self.post_roll_seconds = 0 self.pre_roll_seconds = 0 - self.output_template = "{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}" + self.output_template = RenderingProfile.default_output_template self.enable_watermark = False self.selected_watermark = "" self.overlay_text_template = "" @@ -1053,11 +1172,7 @@ def __init__(self, name="New Rendering Profile"): self.overlay_outline_width = 1 self.thread_count = 1 # Snapshot Cleanup - self.cleanup_after_render_complete = True - self.cleanup_after_render_fail = False - # Skip Snapshots - self.snapshots_to_skip_beginning = 0 - self.snapshot_to_skip_end = 0 + self.archive_snapshots = False def get_overlay_text_color(self): return RenderingProfile._get_color_(self.overlay_text_color) @@ -1068,7 +1183,9 @@ def get_overlay_outline_color(self): @staticmethod def _get_color_(rgba_color): overlay_text_color = [255, 255, 255, 1.0] - if isinstance(rgba_color, six.string_types): + # Remove python 2 support + # if isinstance(rgba_color, six.string_types): + if isinstance(rgba_color, str): overlay_text_color = json.loads(rgba_color) elif isinstance(rgba_color, list): # make sure to copy the list so we don't alter the original @@ -1090,7 +1207,9 @@ def _get_color_(rgba_color): @classmethod def try_convert_value(cls, destination, value, key): if key in ['overlay_text_color', 'overlay_outline_color']: - if isinstance(value, six.string_types): + # Remove python 2 support + # if isinstance(value, six.string_types): + if isinstance(value, str): value = json.loads(value) if not isinstance(value, list): return None @@ -1100,6 +1219,11 @@ def try_convert_value(cls, destination, value, key): return super(RenderingProfile, cls).try_convert_value(destination, value, key) @staticmethod + def get_archive_formats(): + return { + 'zip' + } + @staticmethod def get_options(): return { 'rendering_file_templates': [ @@ -1114,12 +1238,26 @@ def get_options(): "PRINTSTARTTIME", "PRINTSTARTTIMESTAMP", "SNAPSHOTCOUNT", - "FPS" + "FPS", + "CAMERANAME" ], 'overlay_text_templates': [ "snapshot_number", "current_time", "time_elapsed", + "layer", + "height", + "x", + "y", + "z", + "e", + "f", + "x_snapshot", + "y_snapshot", + "gcode_file", + "gcode_file_name", + "gcode_file_extension", + "print_end_state" ], 'overlay_text_alignment_options': [ "left", @@ -1144,7 +1282,8 @@ def get_options(): dict(value='avi', name='AVI'), dict(value='flv', name='FLV'), dict(value='gif', name='GIF'), - dict(value='h264', name='H.264/MPEG-4 AVC'), + dict(value='h264', name='H.264/MPEG-4 AVC (up to 4096x2048)'), + dict(value='h265', name='H.265/MPEG-4 HEVC (up to 8192×4320)'), dict(value='mp4', name='MP4 (libxvid)'), dict(value='mpeg', name='MPEG'), dict(value='vob', name='VOB'), @@ -1173,8 +1312,9 @@ def controls_match_server(self, server_settings): # first see if the number of controls match if len(self.controls) != len(server_settings): return False - - for key, control in six.iteritems(self.controls): + # Remove python 2 support + # for key, control in six.iteritems(self.controls): + for key, control in self.controls.items(): # convert the key to a string key = str(key) # if the key is not in the server_settings_dict, they don't match @@ -1191,12 +1331,14 @@ def controls_match_server(self, server_settings): def update(self, iterable, **kwargs): item_to_iterate = iterable - if not isinstance(iterable, collections.Iterable): + if not isinstance(iterable, Iterable): item_to_iterate = iterable.__dict__ for key, value in item_to_iterate.items(): class_item = getattr(self, key, '{octolapse_no_property_found}') - if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): if key == 'controls': self.controls = {} if isinstance(value, dict): @@ -1219,7 +1361,7 @@ def update(self, iterable, **kwargs): # don't update any static settings, those come from the class itself! continue else: - self.__dict__[key] = self.try_convert_value(class_item, value, key) + setattr(self, key, self.try_convert_value(class_item, value, key)) class OtherStreamingServer(StreamingServer): @@ -1328,23 +1470,28 @@ def __init__(self): self.type = None self.use_custom_webcam_settings_page = True self.mjpg_streamer = MjpgStreamer() + self.stream_download = False def update(self, iterable, **kwargs): try: item_to_iterate = iterable - if not isinstance(iterable, collections.Iterable): + if not isinstance(iterable, Iterable): item_to_iterate = iterable.__dict__ for key, value in item_to_iterate.items(): class_item = getattr(self, key, '{octolapse_no_property_found}') - if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): if key == 'mjpg_streamer': self.mjpg_streamer.controls = {} if "controls" in value: controls = value["controls"] # controls might be a dict or a list, iterate appropriately if isinstance(controls, dict): - for key, control in six.iteritems(value["controls"]): + # Remove python 2 support + # for key, control in six.iteritems(value["controls"]): + for key, control in value["controls"].items(): if "id" in control: self.mjpg_streamer.controls[key] = ( MjpgStreamerControl.create_from(control) @@ -1363,7 +1510,7 @@ def update(self, iterable, **kwargs): else: self.__dict__[key] = self.try_convert_value(class_item, value, key) except Exception as e: - logger.exception(e) + logger.exception("An unexpected exception occurred while updating webcam settings.") raise e @@ -1376,21 +1523,31 @@ def __init__(self, name="New Camera Profile"): super(CameraProfile, self).__init__(name) self.enabled = True self.camera_type = CameraProfile.camera_type_webcam + self.on_before_snapshot_gcode = "" self.gcode_camera_script = "" + self.on_after_snapshot_gcode = "" + self.on_print_start_script = "" + # self.print_start_script_timeout_ms = 0 self.on_before_snapshot_script = "" + # self.before_snapshot_script_timeout_ms = 0 self.external_camera_snapshot_script = "" self.on_after_snapshot_script = "" + # self.after_snapshot_script_timeout_ms = 0 self.on_before_render_script = "" + # self.before_snapshot_script_timeout_ms = 0 self.on_after_render_script = "" + # self.after_snapshot_script_timeout_ms = 0 + self.on_print_end_script = "" + # self.print_end_script_timeout_ms = 0 self.delay = 125 self.timeout_ms = 5000 - self.snapshot_transpose = "" self.webcam_settings = WebcamSettings() self.enable_custom_image_preferences = False - self.apply_settings_before_print = False - self.apply_settings_at_startup = False + self.apply_settings_before_print = True + self.apply_settings_at_startup = True + self.apply_settings_when_disabled = True def format_snapshot_request_template(self): return self.snapshot_request_template.format(camera_address=self.address) @@ -1401,38 +1558,9 @@ def format_stream_template(self): @staticmethod def format_url(url): if url[0] == "/": - url = "http://172.0.0.1" + url + url = "http://127.0.0.1" + url return url - def get_image_preferences(self): - return { - 'guid': self.guid, - 'name': self.name, - 'stream_template': self.format_stream_template(), - 'address': self.address, - 'brightness': self.brightness, - 'contrast': self.contrast, - 'saturation': self.saturation, - 'white_balance_auto': self.white_balance_auto, - 'gain': self.gain, - 'powerline_frequency': self.powerline_frequency, - 'white_balance_temperature': self.white_balance_temperature, - 'sharpness': self.sharpness, - 'backlight_compensation_enabled': self.backlight_compensation_enabled, - 'exposure_type': self.exposure_type, - 'exposure': self.exposure, - 'exposure_auto_priority_enabled': self.exposure_auto_priority_enabled, - 'pan': self.pan, - 'tilt': self.tilt, - 'autofocus_enabled': self.autofocus_enabled, - 'focus': self.focus, - 'zoom': self.zoom, - 'led1_mode': self.led1_mode, - 'led1_frequency': self.led1_frequency, - 'jpeg_quality': self.jpeg_quality, - 'options': self.get_options() - } - @staticmethod def get_options(): return { @@ -1462,17 +1590,21 @@ def __init__(self, name, log_level): self.name = name self.log_level = log_level + def to_dict(self): + return { + 'name': self.name, + 'log_level': self.log_level + } -class DebugProfile(AutomaticConfigurationProfile): - def __init__(self, name="New Debug Profile"): - super(DebugProfile, self).__init__(name) +class LoggingProfile(AutomaticConfigurationProfile): + + def __init__(self, name="New Logging Profile"): + super(LoggingProfile, self).__init__(name) # Configure the logger if it has not been created - self.log_all_errors = True self.enabled = False self.log_to_console = False self.default_log_level = log.DEBUG - self.is_test_mode = False self.enabled_loggers = [] for logger_name in logging_configurator.get_logger_names(): self.enabled_loggers.append(LoggerSettings(logger_name, self.default_log_level)) @@ -1482,21 +1614,19 @@ def try_convert_value(cls, destination, value, key): if key == 'enabled_loggers': try: if value is not None: - return DebugProfile.get_enabled_loggers(value) + return LoggingProfile.get_enabled_loggers(value) except Exception as e: logger.exception("Unable to convert 'enabled_loggers', returning default.") return [] - return super(DebugProfile, cls).try_convert_value(destination, value, key) + return super(LoggingProfile, cls).try_convert_value(destination, value, key) @classmethod def get_enabled_loggers(cls, values): logger_list = [] for enabled_logger in values: - logger_list.append({ - 'name': enabled_logger["name"], - 'log_level': enabled_logger["log_level"] - }) + logger_settings = LoggerSettings(enabled_logger["name"], enabled_logger["log_level"]) + logger_list.append(logger_settings) return logger_list @staticmethod @@ -1506,17 +1636,15 @@ def get_options(): dict(value=log.VERBOSE, name='Verbose'), dict(value=log.DEBUG, name='Debug'), dict(value=log.INFO, name='Info'), - dict(value=log.WARNING, name='Warning'), - dict(value=log.ERROR, name='Error'), - dict(value=log.CRITICAL, name='Critical'), + dict(value=log.WARNING, name='Warning') ], 'all_logger_names': logging_configurator.get_logger_names() - } class MainSettings(Settings): - def __init__(self, plugin_version): + + def __init__(self, plugin_version, git_version): # Main Settings self.show_navbar_icon = True self.show_navbar_when_not_printing = True @@ -1531,9 +1659,108 @@ def __init__(self, plugin_version): self.cancel_print_on_startup_error = True self.platform = sys.platform self.version = plugin_version + self.settings_version = NumberedVersion.CurrentSettingsVersion + self.git_version = git_version self.preview_snapshot_plans = True + self.preview_snapshot_plan_autoclose = False + self.preview_snapshot_plan_seconds = 30 self.automatic_updates_enabled = True self.automatic_update_interval_days = 7 + self.snapshot_archive_directory = "" + self.timelapse_directory = "" + self.temporary_directory = "" + self.test_mode_enabled = False + + def get_snapshot_archive_directory(self, data_folder): + directory = self.snapshot_archive_directory.strip() + if len(directory) > 0: + return self.snapshot_archive_directory + return os.path.join(data_folder, utility.get_default_snapshot_archive_directory_name()) + + def get_timelapse_directory(self, octoprint_timelapse_directory): + directory = self.timelapse_directory.strip() + if len(directory) > 0: + return self.timelapse_directory + return octoprint_timelapse_directory + + def get_temporary_directory(self, data_folder): + directory = self.temporary_directory.strip() + if len(directory) > 0: + return self.temporary_directory + return os.path.join(data_folder, 'tmp') + + def test_directories(self, data_folder, octoprint_timelapse_directory): + errors = [] + snapshot_archive_directory = self.get_snapshot_archive_directory(data_folder) + timelapse_directory = self.get_timelapse_directory(octoprint_timelapse_directory) + temporary_directory = self.get_temporary_directory(data_folder) + try: + results = MainSettings.test_directory(snapshot_archive_directory) + if not results[0]: + errors.append({ + "name": "Snapshot Archive Directory", + 'error': results[1] + }) + results = MainSettings.test_directory(timelapse_directory) + if not results[0]: + errors.append({ + "name": "Timelapse Directory", + 'error': results[1] + }) + results = MainSettings.test_directory(temporary_directory) + if not results[0]: + errors.append({ + "name": "Temporary Directory", + 'error': results[1] + }) + except Exception as e: + logger.exception("An unexpected exception occurred while testing directories.") + raise e + + return len(errors) == 0, errors + + @staticmethod + def test_directory(path): + # ensure the path is absolute + if not os.path.isabs(path): + return False, 'path-not-absolute' + + # ensure the directory exists + if not os.path.isdir(path): + try: + os.makedirs(path) + except (IOError, OSError) as e: + return False, 'cannot-create' + + subdirectory = os.path.join(path, "octolapse_test_temp") + if not os.path.isdir(subdirectory): + try: + os.mkdir(subdirectory) + except (IOError, OSError) as e: + return False, 'cannot-create-subdirectory' + else: + return False, 'subdirectory-test-path-exists' + + try: + os.rmdir(subdirectory) + except (IOError, OSError) as e: + return False, 'cannot-delete-subdirectory' + + # test create and read + test_create_path = os.path.join(path, "test_create.octolapse.tmp") + try: + with open(test_create_path, 'w+'): + os.utime(path, None) + except (IOError, OSError): + False, 'cannot-write-file' + + # test delete + try: + utility.remove(test_create_path) + except (IOError, OSError): + False, 'cannot-delete-file' + + return True, None class ProfileOptions(StaticSettings): @@ -1543,33 +1770,36 @@ def __init__(self): self.trigger = TriggerProfile.get_options() self.rendering = RenderingProfile.get_options() self.camera = CameraProfile.get_options() - self.debug = DebugProfile.get_options() + self.logging = LoggingProfile.get_options() def update_server_options(self, available_profiles): + if not available_profiles: + return # Printer Profile Update self.printer["server_profiles"] = available_profiles.get("printer", None) self.stabilization["server_profiles"] = available_profiles.get("stabilization", None) self.trigger["server_profiles"] = available_profiles.get("trigger", None) self.rendering["server_profiles"] = available_profiles.get("rendering", None) self.camera["server_profiles"] = available_profiles.get("camera", None) - self.debug["server_profiles"] = available_profiles.get("debug", None) + self.logging["server_profiles"] = available_profiles.get("logging", None) class ProfileDefaults(StaticSettings): - def __init__(self): + def __init__(self, plugin_version, git_version): self.printer = PrinterProfile("Default Printer") self.stabilization = StabilizationProfile("Default Stabilization") self.trigger = TriggerProfile("Default Trigger") self.rendering = RenderingProfile("Default Rendering") self.camera = CameraProfile("Default Camera") - self.debug = DebugProfile("Default Debug") + self.logging = LoggingProfile("Default Logging") + self.main_settings = MainSettings(plugin_version, git_version) class Profiles(Settings): - def __init__(self): + def __init__(self, plugin_version, git_version): self.options = ProfileOptions() # create default profiles - self.defaults = ProfileDefaults() + self.defaults = ProfileDefaults(plugin_version, git_version) # printers is initially empty - user must select a printer self.printers = {} @@ -1588,8 +1818,8 @@ def __init__(self): self.current_camera_profile_guid = self.defaults.camera.guid self.cameras = {self.defaults.camera.guid: self.defaults.camera} - self.current_debug_profile_guid = self.defaults.debug.guid - self.debug = {self.defaults.debug.guid: self.defaults.debug} + self.current_logging_profile_guid = self.defaults.logging.guid + self.logging = {self.defaults.logging.guid: self.defaults.logging} def get_profiles_dict(self): profiles_dict = { @@ -1598,19 +1828,20 @@ def get_profiles_dict(self): 'current_trigger_profile_guid': self.current_trigger_profile_guid, 'current_rendering_profile_guid': self.current_rendering_profile_guid, 'current_camera_profile_guid': self.current_camera_profile_guid, - 'current_debug_profile_guid': self.current_debug_profile_guid, + 'current_logging_profile_guid': self.current_logging_profile_guid, 'printers': [], 'stabilizations': [], 'triggers': [], 'renderings': [], 'cameras': [], - 'debug': [] + 'logging': [] } for key, printer in self.printers.items(): profiles_dict["printers"].append({ "name": printer.name, "guid": printer.guid, + "description": printer.description, "has_been_saved_by_user": printer.has_been_saved_by_user, "slicer_type": printer.slicer_type }) @@ -1618,34 +1849,40 @@ def get_profiles_dict(self): for key, stabilization in self.stabilizations.items(): profiles_dict["stabilizations"].append({ "name": stabilization.name, - "guid": stabilization.guid + "guid": stabilization.guid, + "description": stabilization.description, + "wait_for_moves_to_finish": stabilization.wait_for_moves_to_finish }) for key, trigger in self.triggers.items(): profiles_dict["triggers"].append({ "name": trigger.name, "guid": trigger.guid, + "description": trigger.description, "trigger_type": trigger.trigger_type }) for key, rendering in self.renderings.items(): profiles_dict["renderings"].append({ "name": rendering.name, - "guid": rendering.guid + "guid": rendering.guid, + "description": rendering.description, }) for key, camera in self.cameras.items(): profiles_dict["cameras"].append({ "name": camera.name, "guid": camera.guid, + "description": camera.description, "enabled": camera.enabled, "enable_custom_image_preferences": camera.enable_custom_image_preferences }) - for key, debugProfile in self.debug.items(): - profiles_dict["debug"].append({ - "name": debugProfile.name, - "guid": debugProfile.guid + for key, loggingProfile in self.logging.items(): + profiles_dict["logging"].append({ + "name": loggingProfile.name, + "guid": loggingProfile.guid, + "description": loggingProfile.description }) return profiles_dict @@ -1674,10 +1911,10 @@ def current_camera_profile(self): return self.cameras[self.current_camera_profile_guid] return self.defaults.camera - def current_debug_profile(self): - if self.current_debug_profile_guid in self.debug: - return self.debug[self.current_debug_profile_guid] - return self.defaults.debug + def current_logging_profile(self): + if self.current_logging_profile_guid in self.logging: + return self.logging[self.current_logging_profile_guid] + return self.defaults.logging def active_cameras(self): _active_cameras = [] @@ -1691,33 +1928,27 @@ def after_startup_cameras(self): _startup_cameras = [] for key in self.cameras: _current_camera = self.cameras[key] - if ( - _current_camera.enabled and - _current_camera.camera_type in [ - CameraProfile.camera_type_webcam - ] and + is_startup_camera = ( + (_current_camera.enabled or _current_camera.apply_settings_when_disabled) and + _current_camera.camera_type in [CameraProfile.camera_type_webcam] and _current_camera.apply_settings_at_startup - ): + ) + if is_startup_camera: _startup_cameras.append(_current_camera) return _startup_cameras - def before_print_start_cameras(self): + def before_print_start_webcameras(self): _startup_cameras = [] for key in self.cameras: _current_camera = self.cameras[key] - if ( - _current_camera.enabled and ( - ( - _current_camera.camera_type == CameraProfile.camera_type_webcam and - _current_camera.apply_settings_before_print - ) or - ( - _current_camera.camera_type == CameraProfile.camera_type_script and - _current_camera.on_print_start_script - ) - ) - ): - _startup_cameras.append(_current_camera) + if not _current_camera.camera_type == CameraProfile.camera_type_webcam: + continue + if not _current_camera.enabled or _current_camera.apply_settings_when_disabled: + continue + if not _current_camera.apply_settings_at_startup: + continue + + _startup_cameras.append(_current_camera) return _startup_cameras @@ -1735,8 +1966,8 @@ def get_profile(self, profile_type, guid): profile = self.renderings[guid] elif profile_type == "camera": profile = self.cameras[guid] - elif profile_type == "debug": - profile = self.debug[guid] + elif profile_type == "logging": + profile = self.logging[guid] else: raise ValueError('An unknown profile type {} was received.'.format(profile_type)) return profile @@ -1760,9 +1991,9 @@ def import_profile(self, profile_type, profile_json, update_existing=False): elif profile_type == "camera": new_profile = CameraProfile.create_from(profile_json) existing_profiles = self.cameras - elif profile_type == "debug": - new_profile = DebugProfile.create_from(profile_json) - existing_profiles = self.debug + elif profile_type == "logging": + new_profile = LoggingProfile.create_from(profile_json) + existing_profiles = self.logging else: raise Exception("Unknown settings type:{0}".format(profile_type)) # see if any existing profiles have the same name @@ -1811,11 +2042,11 @@ def add_update_profile(self, profile_type, profile): self.cameras[guid] = new_profile if len(self.cameras) == 1 or self.current_camera_profile_guid not in self.cameras: self.current_camera_profile_guid = new_profile.guid - elif profile_type == "debug": - new_profile = DebugProfile.create_from(profile) - self.debug[guid] = new_profile - if len(self.debug) == 1 or self.current_debug_profile_guid not in self.debug: - self.current_debug_profile_guid = new_profile.guid + elif profile_type == "logging": + new_profile = LoggingProfile.create_from(profile) + self.logging[guid] = new_profile + if len(self.logging) == 1 or self.current_logging_profile_guid not in self.logging: + self.current_logging_profile_guid = new_profile.guid else: raise ValueError('An unknown profile type {} was received.'.format(profile_type)) return new_profile @@ -1841,10 +2072,10 @@ def remove_profile(self, profile_type, guid): del self.renderings[guid] elif profile_type == "camera": del self.cameras[guid] - elif profile_type == "debug": - if self.current_debug_profile_guid == guid: + elif profile_type == "logging": + if self.current_logging_profile_guid == guid: return False - del self.debug[guid] + del self.logging[guid] else: raise ValueError('An unknown profile type {} was received.'.format(profile_type)(profile_type)) @@ -1864,53 +2095,58 @@ def set_current_profile(self, profile_type, guid): self.current_rendering_profile_guid = guid elif profile_type == "camera": self.current_camera_profile_guid = guid - elif profile_type == "debug": - self.current_debug_profile_guid = guid + elif profile_type == "logging": + self.current_logging_profile_guid = guid else: raise ValueError('An unknown profile type {} was received.'.format(profile_type)) def update(self, iterable, **kwargs): - try: - item_to_iterate = iterable - if not isinstance(iterable, collections.Iterable): - item_to_iterate = iterable.__dict__ + item_to_iterate = iterable + if not isinstance(iterable, Iterable): + item_to_iterate = iterable.__dict__ - for key, value in item_to_iterate.items(): - class_item = getattr(self, key, '{octolapse_no_property_found}') - if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): - if key == 'printers': - self.printers = {} - for profile_key, profile_value in value.items(): - self.printers[profile_key] = PrinterProfile.create_from(profile_value) - elif key == 'stabilizations': - self.stabilizations = {} - for profile_key, profile_value in value.items(): - self.stabilizations[profile_key] = StabilizationProfile.create_from(profile_value) - elif key == 'triggers': - self.triggers = {} - for profile_key, profile_value in value.items(): - self.triggers[profile_key] = TriggerProfile.create_from(profile_value) - elif key == 'renderings': - self.renderings = {} - for profile_key, profile_value in value.items(): - self.renderings[profile_key] = RenderingProfile.create_from(profile_value) - elif key == 'cameras': - self.cameras = {} - for profile_key, profile_value in value.items(): - self.cameras[profile_key] = CameraProfile.create_from(profile_value) - elif key == 'debug': - self.debug = {} - for profile_key, profile_value in value.items(): - self.debug[profile_key] = DebugProfile.create_from(profile_value) - elif isinstance(class_item, Settings): - class_item.update(value) - elif isinstance(class_item, StaticSettings): - # don't update any static settings, those come from the class itself! - continue - else: - self.__dict__[key] = self.try_convert_value(class_item, value, key) - except Exception as e: - raise e + for key, value in item_to_iterate.items(): + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + profiles_found = True + + if key == 'printers': + profiles = self.printers + create_from = PrinterProfile.create_from + elif key == 'stabilizations': + profiles = self.stabilizations + create_from = StabilizationProfile.create_from + elif key == 'triggers': + profiles = self.triggers + create_from = TriggerProfile.create_from + elif key == 'renderings': + profiles = self.renderings + create_from = RenderingProfile.create_from + elif key == 'cameras': + profiles = self.cameras + create_from = CameraProfile.create_from + elif key == 'logging': + profiles = self.logging + create_from = LoggingProfile.create_from + else: + profiles_found = False + + if profiles_found: + profiles.clear() + for profile_key, profile_value in value.items(): + profile = create_from(profile_value) + # ensure the guid key matches the profile guid + profile.guid = profile_key + profiles[profile_key] = profile + elif isinstance(class_item, Settings): + class_item.update(value) + elif isinstance(class_item, StaticSettings): + # don't update any static settings, those come from the class itself! + continue + else: + self.__dict__[key] = self.try_convert_value(class_item, value, key) def update_from_server_profile(self, profile_type, current_profile_dict, server_profile_dict): profile_type = profile_type.lower() @@ -1934,10 +2170,10 @@ def update_from_server_profile(self, profile_type, current_profile_dict, server_ # get the current profile current_profile = CameraProfile.create_from(current_profile_dict) new_profile = CameraProfile.update_from_server_profile(current_profile, server_profile_dict) - elif profile_type == "debug": + elif profile_type == "logging": # get the current profile - current_profile = DebugProfile.create_from(current_profile_dict) - new_profile = DebugProfile.update_from_server_profile(current_profile, server_profile_dict) + current_profile = LoggingProfile.create_from(current_profile_dict) + new_profile = LoggingProfile.update_from_server_profile(current_profile, server_profile_dict) else: raise ValueError('An unknown profile type {} was received.'.format(profile_type)) return new_profile @@ -1949,44 +2185,51 @@ def get_updatable_profiles_dict(self): "trigger": [], "rendering": [], "camera": [], - "debug": [] + "logging": [] } has_profiles = False - for key, printer_profile in six.iteritems(self.printers): + # Remove python 2 support + # for key, printer_profile in six.iteritems(self.printers): + for key, printer_profile in self.printers.items(): if printer_profile.is_updatable_from_server(): has_profiles = True identifiers = printer_profile.get_server_update_identifiers_dict() profiles["printer"].append(identifiers) - - for key, stabilization_profile in six.iteritems(self.stabilizations): + # Remove python 2 support + # for key, stabilization_profile in six.iteritems(self.stabilizations): + for key, stabilization_profile in self.stabilizations.items(): if stabilization_profile.is_updatable_from_server(): has_profiles = True identifiers = stabilization_profile.get_server_update_identifiers_dict() profiles["stabilization"].append(identifiers) - - for key, trigger_profile in six.iteritems(self.triggers): + # Remove python 2 support + # for key, trigger_profile in six.iteritems(self.triggers): + for key, trigger_profile in self.triggers.items(): if trigger_profile.is_updatable_from_server(): has_profiles = True identifiers = trigger_profile.get_server_update_identifiers_dict() profiles["trigger"].append(identifiers) - - for key, rendering_profile in six.iteritems(self.renderings): + # Remove python 2 support + # for key, rendering_profile in six.iteritems(self.renderings): + for key, rendering_profile in self.renderings.items(): if rendering_profile.is_updatable_from_server(): has_profiles = True identifiers = rendering_profile.get_server_update_identifiers_dict() profiles["rendering"].append(identifiers) - - for key, camera_profile in six.iteritems(self.cameras): + # Remove python 2 support + # for key, camera_profile in six.iteritems(self.cameras): + for key, camera_profile in self.cameras.items(): if camera_profile.is_updatable_from_server(): has_profiles = True identifiers = camera_profile.get_server_update_identifiers_dict() profiles["camera"].append(identifiers) - - for key, debug_profile in six.iteritems(self.debug): - if debug_profile.is_updatable_from_server(): + # Remove python 2 support + # for key, logging_profile in six.iteritems(self.logging): + for key, logging_profile in self.logging.items(): + if logging_profile.is_updatable_from_server(): has_profiles = True - identifiers = debug_profile.get_server_update_identifiers_dict() - profiles["debug"].append(identifiers) + identifiers = logging_profile.get_server_update_identifiers_dict() + profiles["logging"].append(identifiers) if not has_profiles: return None @@ -2004,26 +2247,146 @@ def __init__(self): class OctolapseSettings(Settings): - DefaultDebugProfile = None - def __init__(self, plugin_version="unknown"): - self.main_settings = MainSettings(plugin_version) - self.profiles = Profiles() + camera_settings_file_name = "camera_settings.json" + + @staticmethod + def is_camera_settings_file(file_name): + return file_name.lower() == OctolapseSettings.camera_settings_file_name + + rendering_settings_file_name = "rendering_settings.json" + + @staticmethod + def is_rendering_settings_file(file_name): + return file_name.lower() == OctolapseSettings.rendering_settings_file_name + + DefaultLoggingProfile = None + + def __init__(self, plugin_version="unknown", git_version=None): + self.main_settings = MainSettings(plugin_version, git_version) + self.profiles = Profiles(plugin_version, git_version) self.global_options = GlobalOptions() self.upgrade_info = { "was_upgraded": False, "previous_version": None } + def save(self, file_path): logger.info("Saving settings to: %s.", file_path) self.save_as_json(file_path) logger.info("Settings saved.") + def save_rendering_settings(self, data_folder, job_guid): + for camera in self.profiles.active_cameras(): + snapshot_directory = utility.get_temporary_snapshot_job_camera_path( + data_folder, job_guid, camera.guid + ) + # make sure the snapshot path exists + if not os.path.exists(snapshot_directory): + try: + os.makedirs(snapshot_directory) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + + # get the rendering profile json + rendering_profile_json = self.get_profile_export_json( + 'rendering', self.profiles.current_rendering_profile_guid + ) + # get the camera profile json + camera_profile_json = self.get_profile_export_json( + 'camera', camera.guid + ) + + # save the rendering json + with open(os.path.join(snapshot_directory, OctolapseSettings.rendering_settings_file_name), "w") as settings_file: + settings_file.write(rendering_profile_json) + # save the camera json + with open(os.path.join(snapshot_directory, OctolapseSettings.camera_settings_file_name), "w") as settings_file: + settings_file.write(camera_profile_json) + + # Todo: raise reasonable exceptions + @staticmethod + def load_rendering_settings(plugin_version, data_directory, job_guid, camera_guid): + """Attempt to load all rendering job settings from the snapshot path""" + + # attempt to load the rendering settings + # get the settings path + snapshot_directory = snapshot_directory = utility.get_temporary_snapshot_job_camera_path( + data_directory, job_guid, camera_guid + ) + + # load the rendering profile + rendering_settings_path = os.path.join(snapshot_directory, OctolapseSettings.rendering_settings_file_name) + rendering_profile = None + if os.path.exists(rendering_settings_path): + try: + with open(rendering_settings_path, 'r') as settings_file: + settings = json.load(settings_file) + if settings["version"] != plugin_version: + # TODO: Attempt to migrate the profile + pass + if settings["type"] == "rendering" and "profile" in settings: + rendering_profile = RenderingProfile.create_from(settings["profile"]) + except (OSError, IOError, ValueError) as e: + logger.exception( + "Could not load rendering settings for the given snapshot job at %s.", rendering_settings_path + ) + else: + logger.error( + "The rendering settings file does not exist for the given snapshot job at %s.", rendering_settings_path + ) + + # load the camera profile + camera_settings_path = os.path.join(snapshot_directory, OctolapseSettings.camera_settings_file_name) + camera_profile = None + if os.path.exists(camera_settings_path): + try: + with open(camera_settings_path, 'r') as settings_file: + settings = json.load(settings_file) + if settings["version"] != plugin_version: + # TODO: Attempt to migrate the profile + pass + if settings["type"] == "camera" and "profile" in settings: + camera_profile = CameraProfile.create_from(settings["profile"]) + # ensure the guid matches the supplied guid + camera_profile.guid = camera_guid + except (OSError, IOError, ValueError) as e: + logger.exception( + "Could not load camera settings for the given snapshot job at %s.", camera_settings_path + ) + else: + logger.error( + "The camera settings file does not exist for the given snapshot job at %s.", camera_settings_path + ) + + if camera_profile is None: + camera_profile = CameraProfile() + camera_profile.name = "UNKNOWN" + + return ( + rendering_profile, + camera_profile + ) + + @classmethod + def get_plugin_version_from_file(cls, file_path): + with open(file_path, 'r') as settings_file: + try: + data = json.load(settings_file) + original_version = migration.get_version(data) + return original_version + except Exception: + return None + @classmethod def load( cls, file_path, plugin_version, + git_version, default_settings_folder, default_settings_filename, data_directory, @@ -2045,8 +2408,8 @@ def load( try: data = json.load(settings_file) - original_version = settings_migration.get_version(data) - data = settings_migration.migrate_settings( + original_version = migration.get_version(data) + data = migration.migrate_settings( plugin_version, data, default_settings_folder, data_directory ) @@ -2058,7 +2421,7 @@ def load( if original_version != plugin_version: new_settings.upgrade_info = { "was_upgraded": True, - "previous_version": None + "previous_version": original_version } else: new_settings.upgrade_info = { @@ -2075,7 +2438,7 @@ def load( with open(os.path.join(default_settings_folder, default_settings_filename), 'r') as settings_file: try: data = json.load(settings_file) - data = settings_migration.migrate_settings( + data = migration.migrate_settings( plugin_version, data, default_settings_folder, data_directory ) # if a settings file does not exist, create one ?? @@ -2093,58 +2456,56 @@ def load( # update the profile options within the octolapse settings new_settings.profiles.options.update_server_options(available_profiles) - # update the settings object with the loaded settings so that we will have all of our static settings - # Note that this will not contain any static settings that - #settings.update(new_settings) + # set the current and git versions (always use the passed in values) + new_settings.main_settings.git_version = git_version + new_settings.main_settings.version = plugin_version + # set the settings version (always use NumberedVersion.CurrentSettingsVersion) + new_settings.main_settings.settings_version = NumberedVersion.CurrentSettingsVersion + return new_settings, load_defualt_settings + @classmethod + def get_settings_version_from_dict(cls, settings): + if "type" in settings: + return settings.get("settings_version", "unknown") + main_settings = settings.get("main_settings", {}) + return main_settings.get("settings_version", "unknown") + def get_profile_export_json(self, profile_type, guid): profile = self.profiles.get_profile(profile_type, guid) export_dict = { 'version': self.main_settings.version, + 'settings_version': NumberedVersion.CurrentSettingsVersion, 'type': profile_type, 'profile': profile } return json.dumps(export_dict, cls=SettingsJsonEncoder) + class IncorrectSettingsVersionException(Exception): + pass + def import_settings_from_file( self, settings_path, plugin_version, + git_version, default_settings_folder, data_directory, available_server_profiles, update_existing=False ): - with open(settings_path, 'r') as settings_file: - settings = json.load(settings_file) - - # see if this is a structured import - if "type" in settings: - if settings["version"] != plugin_version: - raise Exception( - "Cannot import settings from an old version of Octolapse. Current Version:{0}, Settings " - "Version:{1} ".format(settings.version, plugin_version) - ) - else: - self.profiles.import_profile(settings["type"], settings["profile"], update_existing=update_existing) - settings = self - else: - # this is a regular settings file. Try to migrate - migrated_settings = settings_migration.migrate_settings( - plugin_version, settings, default_settings_folder, data_directory - ) - new_settings = OctolapseSettings(plugin_version) - new_settings.update(migrated_settings) - settings = new_settings - - settings.profiles.options.update_server_options(available_server_profiles) - return settings + with open(settings_path, mode='r') as settings_file: + settings_text = settings_file.read() + return self.import_settings_from_text( + settings_text, plugin_version, git_version, default_settings_folder, data_directory, + available_server_profiles, update_existing=update_existing + ) def import_settings_from_text( self, settings_text, plugin_version, + git_version, default_settings_folder, data_directory, available_server_profiles, @@ -2152,22 +2513,31 @@ def import_settings_from_text( ): logger.info("Importing python settings object. Checking settings version.") settings = json.loads(settings_text) + settings_version = OctolapseSettings.get_settings_version_from_dict(settings) # see if this is a structured import if "type" in settings: - if settings["version"] != plugin_version: - raise Exception( - "Cannot import settings from an old version of Octolapse. Current Version:{0}, Settings " - "Version:{1} ".format(settings.version, plugin_version) + if settings_version != NumberedVersion.CurrentSettingsVersion: + raise OctolapseSettings.IncorrectSettingsVersionException( + "Cannot import profiles exported from an incompatible version of Octolapse ({0}). Was expecting settings version: {1} ".format( + settings_version, NumberedVersion.CurrentSettingsVersion + ) ) else: self.profiles.import_profile(settings["type"], settings["profile"], update_existing=update_existing) settings = self else: + # Make sure the settings version is not greater than the current version + if settings_version != "unknown" and NumberedVersion(NumberedVersion.CurrentSettingsVersion) < NumberedVersion(settings_version): + raise OctolapseSettings.IncorrectSettingsVersionException( + "Cannot import settings exported from a newer and incompatible version of Octolapse ({0}). Was expecting settings version: {1} ".format( + settings_version, NumberedVersion.CurrentSettingsVersion + ) + ) # this is a regular settings file. Try to migrate - migrated_settings = settings_migration.migrate_settings( + migrated_settings = migration.migrate_settings( plugin_version, settings, default_settings_folder, data_directory ) - new_settings = OctolapseSettings(plugin_version) + new_settings = OctolapseSettings(plugin_version, git_version) new_settings.update(migrated_settings) settings = new_settings @@ -2199,20 +2569,48 @@ def create_from_iterable(cls, plugin_version, iterable=()): return new_object -class OctolapseGcodeSettings(Settings): +class OctolapseExtruderGcodeSettings(Settings): def __init__(self): self.retract_before_move = None self.retraction_length = None self.retraction_speed = None self.deretraction_speed = None - self.x_y_travel_speed = None - self.first_layer_travel_speed = None self.lift_when_retracted = None self.z_lift_height = None + self.x_y_travel_speed = None + self.first_layer_travel_speed = None self.z_lift_speed = None + + +class OctolapseGcodeSettings(Settings): + def __init__(self): + # Per Extruder Settings + self.extruders = [] + # Print Settings self.vase_mode = None self.layer_height = None + def get_num_extruders(self): + return len(self.extruders) + + def to_dict(self): + extruders_list = [] + for extruder in self.extruders: + if isinstance(extruder, dict): + extruders_list.append(extruder) + else: + extruders_list.append(extruder.to_dict()) + return { + "vase_mode": self.vase_mode, + "layer_height": self.layer_height, + "extruders": extruders_list, + } + + +class SlicerExtruder(Settings): + def __init__(self): + pass + class SlicerSettings(Settings): SlicerTypeOther = 'other' @@ -2223,7 +2621,7 @@ class SlicerSettings(Settings): def __init__(self, slicer_type, version): self.slicer_type = slicer_type self.version = version - + self.extruders = [] def get_speed_tolerance(self): raise NotImplementedError("You must implement get_speed_tolerance") @@ -2235,72 +2633,88 @@ def get_missing_gcode_generation_settings(self, slicer_type=None): settings = self.get_gcode_generation_settings(slicer_type=slicer_type) assert(isinstance(settings, OctolapseGcodeSettings)) issue_list = [] - if settings.retraction_length is None: - issue_list.append("Retraction Length") - if settings.retraction_speed is None: - issue_list.append("Retraction Speed") - if settings.deretraction_speed is None: - issue_list.append("Deretraction Speed") - if settings.x_y_travel_speed is None: - issue_list.append("X/Y Travel Speed") - if settings.z_lift_height is None: - issue_list.append("Z Lift Height") - if settings.z_lift_speed is None: - issue_list.append("Z Travel Speed") - if settings.retract_before_move is None: - issue_list.append("Retract Before Move") - if settings.lift_when_retracted is None: - issue_list.append("Lift When Retracted") + extruder_number = 1 + if len(settings.extruders) == 0: + issue_list.append("No extruder settings were found in your gcode file.") + else: + for extruder in settings.extruders: + if len(settings.extruders) == 1: + extruder_label = "" + else: + extruder_label = "Extruder {0} - ".format(extruder_number) + # Per Extruder Settings + if extruder.retract_before_move is None: + issue_list.append("{}Retract Before Move".format(extruder_label)) + if extruder.retraction_length is None: + issue_list.append("{}Retraction Length".format(extruder_label)) + if extruder.retraction_speed is None: + issue_list.append("{}Retraction Speed".format(extruder_label)) + if extruder.deretraction_speed is None: + issue_list.append("{}Deretraction Speed".format(extruder_label)) + if extruder.lift_when_retracted is None: + issue_list.append("{}Lift When Retracted".format(extruder_label)) + if extruder.z_lift_height is None: + issue_list.append("{}Z Lift Height".format(extruder_label)) + if extruder.x_y_travel_speed is None: + issue_list.append("{}X/Y Travel Speed".format(extruder_label)) + if extruder.z_lift_speed is None: + issue_list.append("{}Z Travel Speed".format(extruder_label)) + extruder_number += 1 + # Print Settings + if settings.vase_mode is None: + issue_list.append("Is Vase Mode") return issue_list def get_speed_mm_min(self, speed, multiplier=None, speed_name=None): """Returns a speed in mm/min for a setting name""" raise NotImplementedError("You must implement get_speed_mm_min") - def update_settings_from_gcode(self, settings_dict): + def update_settings_from_gcode(self, settings_dict, printer_profile): raise NotImplementedError("You must implement update_settings_from_gcode") -class CuraSettings(SlicerSettings): - def __init__(self, version="unknown"): - super(CuraSettings, self).__init__(SlicerSettings.SlicerTypeCura, version) +class CuraExtruder(SlicerExtruder): + def __init__(self): + super(CuraExtruder, self).__init__() + self.version = None + self.speed_z_hop = None + self.max_feedrate_z_override = None self.retraction_amount = None + self.retraction_hop = None + self.retraction_hop_enabled = False + self.retraction_enable = False + self.retraction_speed = None self.retraction_retract_speed = None self.retraction_prime_speed = None - self.retraction_hop_enabled = None # new setting - self.retraction_enable = None # new setting self.speed_travel = None - self.max_feedrate_z_override = None - self.speed_z_hop = None - self.retraction_hop = None - self.axis_speed_display_settings = 'mm-sec' - self.layer_height = None - self.smooth_spiralized_contours = None - self.magic_mesh_surface_mode = None - - def get_speed_tolerance(self): - return 0.1 / 60.0 / 2.0 - def get_gcode_generation_settings(self, slicer_type="cura"): - settings = OctolapseGcodeSettings() - settings.retraction_length = self.get_retraction_amount() - settings.retraction_speed = self.get_retraction_retract_speed() - settings.deretraction_speed = self.get_retraction_prime_speed() - settings.x_y_travel_speed = self.get_speed_travel() - settings.z_lift_height = self.get_retraction_hop() - settings.z_lift_speed = self.get_speed_travel_z(slicer_type=slicer_type) - settings.retract_before_move = ( - self.retraction_enable and settings.retraction_length is not None and settings.retraction_length > 0 + def get_extruder(self, slicer_settings, slicer_type): + extruder = OctolapseExtruderGcodeSettings() + extruder.retract_before_move = self.get_retract_before_move() + extruder.retraction_length = self.get_retraction_amount() + extruder.retraction_speed = self.get_retraction_retract_speed() + extruder.deretraction_speed = self.get_retraction_prime_speed() + extruder.lift_when_retracted = self.get_lift_when_retracted() + extruder.z_lift_height = self.get_retraction_hop() + extruder.x_y_travel_speed = self.get_speed_travel() + extruder.first_layer_travel_speed = self.get_speed_travel() + extruder.z_lift_speed = self.get_speed_travel_z(slicer_type) + return extruder + + def get_retract_before_move(self): + retraction_amount = self.get_retraction_amount() + return ( + self.retraction_enable and retraction_amount is not None and retraction_amount > 0 ) - settings.lift_when_retracted = ( - settings.retract_before_move and + + def get_lift_when_retracted(self): + retraction_hop = self.get_retraction_hop() + return ( + self.get_retract_before_move() and self.retraction_hop_enabled and - settings.retraction_length is not None - and settings.retraction_length > 0 + retraction_hop is not None and + retraction_hop > 0 ) - settings.layer_height = self.layer_height - settings.vase_mode = self.smooth_spiralized_contours and self.magic_mesh_surface_mode == "surface" - return settings def get_retraction_amount(self): if self.retraction_amount is None or len("{}".format(self.retraction_amount).strip()) == 0: @@ -2313,83 +2727,282 @@ def get_retraction_hop(self): return float(self.retraction_hop) def get_retraction_retract_speed(self): - return self.get_speed_mm_min(self.retraction_retract_speed) + if self.retraction_retract_speed: + return CuraSettings.get_speed_mm_min(self.retraction_retract_speed) + return CuraSettings.get_speed_mm_min(self.retraction_speed) def get_retraction_prime_speed(self): - return self.get_speed_mm_min(self.retraction_prime_speed) + if self.retraction_prime_speed: + return CuraSettings.get_speed_mm_min(self.retraction_prime_speed) + return CuraSettings.get_speed_mm_min(self.retraction_speed) def get_speed_travel(self): - return self.get_speed_mm_min(self.speed_travel) + return CuraSettings.get_speed_mm_min(self.speed_travel) def get_speed_travel_z(self, slicer_type=None): if slicer_type == "cura_4_2" or ( self.version and - self.version != 'unknown' and - LooseVersion(self.version) >= LooseVersion("4.2.0") + self.speed_z_hop is not None ): - return self.get_speed_mm_min(self.speed_z_hop) + return CuraSettings.get_speed_mm_min(self.speed_z_hop) z_max_feedrate = 0 if self.max_feedrate_z_override: - z_max_feedrate = self.get_speed_mm_min(self.max_feedrate_z_override) - travel_feedrate = self.get_speed_mm_min(self.speed_travel) + z_max_feedrate = CuraSettings.get_speed_mm_min(self.max_feedrate_z_override) + travel_feedrate = CuraSettings.get_speed_mm_min(self.speed_travel) if z_max_feedrate == 0: return travel_feedrate return min(z_max_feedrate, travel_feedrate) - def get_speed_mm_min(self, speed, multiplier=None, speed_name=None): + def update(self, iterable, **kwargs): + item_to_iterate = iterable + if not isinstance(iterable, Iterable): + item_to_iterate = iterable.__dict__ + for key, value in item_to_iterate.items(): + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + try: + if key in ['retraction_enable', 'retraction_hop_enabled']: + self.__dict__[key] = None if value is None else bool(value) + elif key == "version": + self.__dict__[key] = None if value is None else str(value) + else: + self.__dict__[key] = None if value is None else float(value) + except ValueError as e: + logger.exception( + "Unable to update the current Cura extruder profile. Key:{}, Value:{}".format(key, value)) + continue + + +class CuraSettings(SlicerSettings): + ExtruderNumberSearchRegex = r"([\w\W]*)_(\d+)$" + + def __init__(self, version="unknown"): + super(CuraSettings, self).__init__(SlicerSettings.SlicerTypeCura, version) + self.axis_speed_display_settings = 'mm-sec' + self.layer_height = None + self.smooth_spiralized_contours = False + self.machine_extruder_count = 1 + + def get_extruders(self, slicer_type): + extruders = [] + for extruder in self.extruders: + extruders.append(extruder.get_extruder(self, slicer_type)) + return extruders + + def get_speed_tolerance(self): + return 0.1 / 60.0 / 2.0 + + def get_gcode_generation_settings(self, slicer_type="cura"): + settings = OctolapseGcodeSettings() + settings.layer_height = self.layer_height + settings.vase_mode = False + if self.smooth_spiralized_contours is not None: + # we might not need to include a check for surface mode + settings.vase_mode = self.smooth_spiralized_contours + # Get All Extruder Settings + settings.extruders = self.get_extruders(slicer_type) + return settings + + @staticmethod + def get_speed_mm_min(speed, multiplier=None, speed_name=None): if speed is None or len("{}".format(speed).strip()) == 0: return None - # Convert speed to mm/min speed = float(speed) * 60.0 # round to .1 return utility.round_to(speed, 0.1) - def update_settings_from_gcode(self, settings_dict): - # for cura we can just call the regular update function for most things - self.update(settings_dict) + def update_settings_from_gcode(self, settings_dict, printer_profile): - # if this is version 4.2.0 or greater, blank out max_feedrate_z_override - if self.version and LooseVersion(self.version) >= LooseVersion("4.2.0"): - self.max_feedrate_z_override = None + # get the version if it's in the settings dict + version = "unknown" + if 'version' in settings_dict: + version = settings_dict["version"] + # clear out the extruders + self.extruders = [] + # extract the extruder count if it can be found + num_extruders = 1 + if 'machine_extruder_count' in settings_dict: + num_extruders = int(settings_dict["machine_extruder_count"]) -class Simplify3dSettings(SlicerSettings): + if num_extruders > 8: + num_extruders = 8 - def __init__(self, version="unknown"): - super(Simplify3dSettings, self).__init__(SlicerSettings.SlicerTypeSimplify3D, version) + # add the appropriate number of extruders according to the settings + for i in range(0, num_extruders): + new_extruder = CuraExtruder() + new_extruder.version = version + self.extruders.append(new_extruder) + + for key, value in settings_dict.items(): + try: + # first see if this is an extruder setting. To do that let's look for an underscore then a number + # at the end of the setting value, remove it + + matches = re.search(CuraSettings.ExtruderNumberSearchRegex, key) + if matches and len(matches.groups()) == 2: + extruder_setting_name = matches.groups()[0] + extruder_index = int(matches.groups()[1]) + else: + extruder_setting_name = key + extruder_index = 0 + + if -1 < extruder_index < 8 and extruder_index < num_extruders and extruder_setting_name in [ + "speed_z_hop", + "retraction_amount", + "retraction_hop", + "retraction_hop_enabled", + "retraction_enable", + "retraction_speed", + "retraction_retract_speed", + "retraction_prime_speed", + "speed_travel", + "max_feedrate_z_override" + ]: + # convert the type as needed + if extruder_setting_name in ['retraction_hop_enabled','retraction_enable']: + value = None if value is None else bool(value) + else: + value = None if value is None else float(value) + # get the current extruder + extruder = self.extruders[extruder_index] + # for fun, make sure the setting exists in the extruder class + class_item = getattr(extruder, extruder_setting_name, '{octolapse_no_property_found}') + # Remove python 2 support + if not ( + # Remove python 2 support + # isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}' + isinstance(class_item, str) and class_item == '{octolapse_no_property_found}' + ): + extruder.__dict__[extruder_setting_name] = value + continue + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + if key == 'version': + self.version = value + elif isinstance(class_item, Settings): + class_item.update(value) + else: + self.__dict__[key] = self.try_convert_value(class_item, value, key) + except ValueError: + logger.exception("An error occurred while updating cura settings from the extracted gcode settings") + continue + + def update(self, iterable, **kwargs): + try: + item_to_iterate = iterable + if not isinstance(iterable, Iterable): + item_to_iterate = iterable.__dict__ + for key, value in item_to_iterate.items(): + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + if key == "extruders": + self.extruders = [] + for extruder in value: + new_extruder = CuraExtruder() + new_extruder.update(extruder) + self.extruders.append(new_extruder) + elif isinstance(class_item, Settings): + class_item.update(value) + elif isinstance(class_item, StaticSettings): + # don't update any static settings, those come from the class itself! + continue + else: + self.__dict__[key] = self.try_convert_value(class_item, value, key) + except Exception as e: + raise e + + +class Simplify3dExtruder(SlicerExtruder): + def __init__(self): + super(Simplify3dExtruder, self).__init__() self.retraction_distance = None self.retraction_vertical_lift = None self.retraction_speed = None + self.extruder_use_retract = False + + def get_extruder(self, slicer_settings): + extruder = OctolapseExtruderGcodeSettings() + extruder.retraction_length = self.get_retraction_distance() + extruder.retraction_speed = self.get_retract_speed(slicer_settings) + extruder.deretraction_speed = self.get_deretract_speed(slicer_settings) + extruder.x_y_travel_speed = slicer_settings.get_x_y_axis_movement_speed() + extruder.z_lift_height = self.get_retraction_vertical_lift() + extruder.z_lift_speed = slicer_settings.get_z_axis_movement_speed() + extruder.retract_before_move = ( + self.extruder_use_retract and extruder.retraction_length is not None and extruder.retraction_length > 0 + ) + extruder.lift_when_retracted = ( + extruder.retract_before_move and extruder.z_lift_height is not None and extruder.z_lift_height > 0 + ) + return extruder + + def get_retraction_distance(self): + return None if self.retraction_distance is None else float(self.retraction_distance) + + def get_retraction_vertical_lift(self): + return None if self.retraction_vertical_lift is None else float(self.retraction_vertical_lift) + + def get_retract_speed(self, slicer_settings): + return slicer_settings.get_speed_mm_min(self.retraction_speed) + + def get_deretract_speed(self, slicer_settings): + return self.get_retract_speed(slicer_settings) + + def update(self, iterable, **kwargs): + item_to_iterate = iterable + if not isinstance(iterable, Iterable): + item_to_iterate = iterable.__dict__ + for key, value in item_to_iterate.items(): + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + try: + if key in ['extruder_use_retract']: + self.__dict__[key] = None if value is None else bool(value) + else: + self.__dict__[key] = None if value is None else float(value) + except ValueError as e: + logger.exception( + "Unable to update the current Cura extruder profile. Key:{}, Value:{}".format(key, value)) + continue + + +class Simplify3dSettings(SlicerSettings): + + def __init__(self, version="unknown"): + super(Simplify3dSettings, self).__init__(SlicerSettings.SlicerTypeSimplify3D, version) self.x_y_axis_movement_speed = None self.z_axis_movement_speed = None - self.extruder_use_retract = None - self.spiral_vase_mode = None + self.spiral_vase_mode = False self.layer_height = None # simplify has a fixed speed tolerance self.axis_speed_display_settings = 'mm-min' + def get_extruders(self): + extruders = [] + for extruder in self.extruders: + extruders.append(extruder.get_extruder(self)) + return extruders + def get_speed_tolerance(self): return 1 def get_gcode_generation_settings(self, slicer_type=None): """Returns OctolapseSlicerSettings""" settings = OctolapseGcodeSettings() - settings.retraction_length = self.get_retraction_distance() - settings.retraction_speed = self.get_retract_speed() - settings.deretraction_speed = self.get_deretract_speed() - settings.x_y_travel_speed = self.get_x_y_axis_movement_speed() - settings.z_lift_height = self.get_retraction_vertical_lift() - settings.z_lift_speed = self.get_z_axis_movement_speed() - settings.retract_before_move = ( - self.extruder_use_retract and settings.retraction_length is not None and settings.retraction_length > 0 - ) - settings.lift_when_retracted = ( - settings.retract_before_move and settings.z_lift_height is not None and settings.z_lift_height > 0 - ) settings.vase_mode = self.spiral_vase_mode settings.layer_height = self.layer_height + settings.extruders = self.get_extruders() return settings def get_speed_mm_min(self, speed, multiplier=None, speed_name=None, is_half_speed_multiplier=False): @@ -2404,140 +3017,264 @@ def get_speed_mm_min(self, speed, multiplier=None, speed_name=None, is_half_spee return utility.round_to(speed, 0.1) - def get_retraction_distance(self): - return float(self.retraction_distance) - - def get_retraction_vertical_lift(self): - return float(self.retraction_vertical_lift) - - def get_retract_speed(self): - return self.get_speed_mm_min(self.retraction_speed) - - def get_deretract_speed(self): - return self.get_retract_speed() - def get_x_y_axis_movement_speed(self): return self.get_speed_mm_min(self.x_y_axis_movement_speed) def get_z_axis_movement_speed(self): return self.get_speed_mm_min(self.z_axis_movement_speed) - def update_settings_from_gcode(self, settings_dict): - - def _primary_extruder(): - return settings_dict["primary_extruder"] - - def _retraction_distance(): - return settings_dict["extruder_retraction_distance"][_primary_extruder()] - - def _retraction_vertical_lift(): - return settings_dict["extruder_retraction_z_lift"][_primary_extruder()] - - def _retraction_speed(): - return settings_dict["extruder_retraction_speed"][_primary_extruder()] - + def update_settings_from_gcode(self, settings_dict, printer_profile): + # per print settings extraction functions def _x_y_axis_movement_speed(): return settings_dict["rapid_xy_speed"] def _z_axis_movement_speed(): return settings_dict["rapid_z_speed"] - def _extruder_use_retract(): - return settings_dict["extruder_use_retract"][_primary_extruder()] - - # for simplify we need to manually update the settings :( - self.retraction_distance = _retraction_distance() - self.retraction_vertical_lift = _retraction_vertical_lift() - self.retraction_speed = _retraction_speed() + # update non-extruder settings self.x_y_axis_movement_speed = _x_y_axis_movement_speed() self.z_axis_movement_speed = _z_axis_movement_speed() - self.extruder_use_retract = _extruder_use_retract() + self.spiral_vase_mode = settings_dict['spiral_vase_mode'] + self.layer_height = settings_dict['layer_height'] + # clear existing extruders + self.extruders = [] + # get the toolhead numbers, which is a list of ints that determine the T gcode number for each extruder + # Note that toolhead_numbers is a 0 based index + toolhead_numbers = settings_dict["extruder_tool_number"] + # Simplify 3D has a very odd method of defining extruders. You can define any number of + # extruders and assign them to a tool number (0 based). Multiple extruders can use the same tool number. + # Due to this there are some limitations to how Octolaspe can implement multi-extruder setups in + # simplify. + # + # First, we need to check for duplicate indexes. If we find any, throw an error + duplicate_extruder_toolhead_numbers = set([x for x in toolhead_numbers if toolhead_numbers.count(x) > 1]) + if len(duplicate_extruder_toolhead_numbers) > 0: + raise OctolapseException( + ["settings","slicer","simplify3d","duplicate_toolhead_numbers"], + toolhead_numbers = ','.join([str(x) for x in duplicate_extruder_toolhead_numbers]) + ) + # + + # get the number of extruders + num_extruders = len(toolhead_numbers) + # make sure we have enough extruders + if num_extruders != 1 and num_extruders != printer_profile.num_extruders: + raise OctolapseException( + ["settings", "slicer", "simplify3d", "extruder_count_mismatch"], + configured_extruder_count=printer_profile.num_extruders, detected_extruder_count=num_extruders + ) + max_extruder_count = 8 + if num_extruders > max_extruder_count: + raise OctolapseException( + ["settings", "slicer", "simplify3d", "max_extruder_count_exceeded"], + simplify_extruder_count=num_extruders, max_extruder_count=max_extruder_count + ) + # ensure that the max(toolhead_number) is correct given the number of extruders and zero_based_extruder + zero_based_extruder = ( + (printer_profile.num_extruders > 1 and printer_profile.zero_based_extruder) or + (printer_profile.num_extruders == 1 and max(toolhead_numbers) == 0) + ) -class Slic3rPeSettings(SlicerSettings): - def __init__(self, version="unknown"): - super(Slic3rPeSettings, self).__init__(SlicerSettings.SlicerTypeSlic3rPe, version) - self.retract_before_travel = None + expected_max_toolhead_number = num_extruders - 1 if zero_based_extruder else num_extruders + max_toolhead_number = max(toolhead_numbers) + if num_extruders > 1 and expected_max_toolhead_number != max_toolhead_number: + raise OctolapseException( + ["settings", "slicer", "simplify3d", "unexpected_max_toolhead_number"], + expected_max_toolhead_number=expected_max_toolhead_number, max_toolhead_number=max_toolhead_number + ) + + # create num_extruders extruders for this profile + for i in range(0, num_extruders): + extruder = Simplify3dExtruder() + self.extruders.append(extruder) + + # extract all per-extruder settings for each toolhead + for toolhead_index in range(num_extruders): + # get the extruder for the toolhead + extruder = self.extruders[toolhead_index] + # per defined extruder settings extraction functions + + def _retraction_distance(index): + return settings_dict["extruder_retraction_distance"][index] + + def _retraction_vertical_lift(index): + return settings_dict["extruder_retraction_z_lift"][index] + + def _retraction_speed(index): + return settings_dict["extruder_retraction_speed"][index] + + def _extruder_use_retract(index): + return settings_dict["extruder_use_retract"][index] + + # set all extruder based settings + extruder.retraction_distance = _retraction_distance(toolhead_index) + extruder.retraction_vertical_lift = _retraction_vertical_lift(toolhead_index) + extruder.retraction_speed = _retraction_speed(toolhead_index) + extruder.extruder_use_retract = _extruder_use_retract(toolhead_index) + + def update(self, iterable, **kwargs): + try: + item_to_iterate = iterable + if not isinstance(iterable, Iterable): + item_to_iterate = iterable.__dict__ + for key, value in item_to_iterate.items(): + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + if key == "extruders": + self.extruders = [] + for extruder in value: + new_extruder = Simplify3dExtruder() + new_extruder.update(extruder) + self.extruders.append(new_extruder) + elif isinstance(class_item, Settings): + class_item.update(value) + elif isinstance(class_item, StaticSettings): + # don't update any static settings, those come from the class itself! + continue + else: + self.__dict__[key] = self.try_convert_value(class_item, value, key) + except Exception as e: + raise e + + +class Slic3rPeExtruder(SlicerExtruder): + def __init__(self): + super(Slic3rPeExtruder, self).__init__() self.retract_length = None self.retract_lift = None - self.deretract_speed = None self.retract_speed = None - self.axis_speed_display_units = 'mm-sec' - self.layer_height = None - self.spiral_vase = None - self.travel_speed = None + self.deretract_speed = None - def get_speed_mm_min(self, speed, multiplier=None, setting_name=None): - if speed is None: + def get_retract_length(self): + if self.retract_length is None: return None - speed = float(speed) - if self.axis_speed_display_units == "mm-sec": - speed = speed * 60.0 - return speed - - def get_gcode_generation_settings(self, slicer_type=None): - settings = OctolapseGcodeSettings() - settings.retraction_length = self.get_retract_length() - settings.retraction_speed = self.get_retract_speed() - settings.deretraction_speed = self.get_deretract_speed() - settings.x_y_travel_speed = self.get_travel_speed() - settings.first_layer_travel_speed = self.get_travel_speed() - settings.z_lift_height = self.get_retract_lift() - settings.z_lift_speed = self.get_z_travel_speed() - # calculate retract before travel and lift when retracted - settings.retract_before_move = self.get_retract_before_travel() - settings.lift_when_retracted = self.get_lift_when_retracted() - settings.layer_height = self.layer_height - settings.vase_mode = self.spiral_vase - return settings + else: + return utility.round_to(float(self.retract_length), 0.00001) - def get_retract_before_travel(self): + def get_retract_before_move(self): retract_length = self.get_retract_length() if ( - self.retract_before_travel is not None and - float(self.retract_before_travel) > 0 and retract_length is not None and retract_length > 0 ): return True - return False + else: + return False + + def get_retract_lift_height(self): + if self.retract_lift is None: + return None + return utility.round_to(float(self.retract_lift), 0.001) def get_lift_when_retracted(self): - retract_lift = self.get_retract_lift() + get_retract_before_move = self.get_retract_before_move() + retract_lift_height = self.get_retract_lift_height() if ( - self.get_retract_before_travel() - and retract_lift is not None - and retract_lift > 0 + get_retract_before_move + and retract_lift_height is not None + and retract_lift_height > 0 ): return True return False - def get_z_travel_speed(self): - return self.get_travel_speed() + def get_retract_speed(self): - def get_retract_length(self): - if self.retract_length is None: + if self.retract_speed is None or len("{}".format(self.retract_speed).strip()) == 0: return None + else: + return Slic3rPeSettings.get_speed_from_setting(self.retract_speed, round_to=1) - return utility.round_to(float(self.retract_length), 0.00001) + def get_deretract_speed(self): + retract_speed = self.get_retract_speed() + deretract_speed = self.deretract_speed + if deretract_speed is None or len("{}".format(deretract_speed).strip()) == 0: + return None + deretract_speed = float(deretract_speed) + if deretract_speed == 0: + return retract_speed + else: + return Slic3rPeSettings.get_speed_from_setting(deretract_speed, round_to=1) + + def get_extruder(self, slicer_settings): + extruder = OctolapseExtruderGcodeSettings() + extruder.retract_before_move = self.get_retract_before_move() + extruder.retraction_length = self.get_retract_length() + extruder.retraction_speed = self.get_retract_speed() + extruder.deretraction_speed = self.get_deretract_speed() + extruder.lift_when_retracted = self.get_lift_when_retracted() + extruder.z_lift_height = self.get_retract_lift_height() + extruder.x_y_travel_speed = slicer_settings.get_travel_speed() + extruder.first_layer_travel_speed = slicer_settings.get_travel_speed() + extruder.z_lift_speed = slicer_settings.get_travel_speed() + return extruder - def get_retract_lift(self): - if self.retract_lift is None: + def update(self, iterable, **kwargs): + item_to_iterate = iterable + if not isinstance(iterable, Iterable): + item_to_iterate = iterable.__dict__ + for key, value in item_to_iterate.items(): + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + try: + self.__dict__[key] = None if value is None else float(value) + except ValueError as e: + logger.exception( + "Unable to update the current Slic3r extruder profile. Key:{}, Value:{}".format(key, value)) + continue + + +class Slic3rPeSettings(SlicerSettings): + def __init__(self, version="unknown"): + super(Slic3rPeSettings, self).__init__(SlicerSettings.SlicerTypeSlic3rPe, version) + self.axis_speed_display_units = 'mm-sec' + self.layer_height = None + self.spiral_vase = False + self.travel_speed = None + + def get_extruders(self): + extruders = [] + for extruder in self.extruders: + extruders.append(extruder.get_extruder(self)) + return extruders + + def get_speed_mm_min(self, speed, multiplier=None, setting_name=None): + if speed is None: return None + speed = float(speed) + if self.axis_speed_display_units == "mm-sec": + speed = speed * 60.0 + return speed - return utility.round_to(float(self.retract_lift), 0.001) + def get_gcode_generation_settings(self, slicer_type=None): + settings = OctolapseGcodeSettings() + # get print settings + settings.layer_height = self.layer_height + settings.vase_mode = self.spiral_vase if self.spiral_vase is not None else False + # Get All Extruder Settings + settings.extruders = self.get_extruders() + return settings + + def get_z_travel_speed(self): + return self.get_travel_speed() @staticmethod def parse_percent(parse_string): try: if parse_string is None: return None - if isinstance(parse_string, six.string_types): + # Remove python 2 support + # if isinstance(parse_string, six.string_types): + if isinstance(parse_string, str): percent_index = "{}".format(parse_string).strip().find('%') if percent_index < 1: return None try: - percent = float("{}".format(parse_string).encode(u'utf-8').translate(None, b'%')) / 100.0 + percent = float("{}".format(parse_string).encode(u'utf-8').decode().translate({ord(c):None for c in '%'})) / 100.0 return percent except ValueError: return None @@ -2545,8 +3282,9 @@ def parse_percent(parse_string): except Exception as e: raise e + @staticmethod def get_speed_from_setting( - self, speed_setting, round_to=0.01 + speed_setting, round_to=0.01 ): if speed_setting is None or len("{}".format(speed_setting).strip()) == 0: return None @@ -2558,32 +3296,38 @@ def get_travel_speed(self): self.travel_speed ) - def get_retract_speed(self): - return self.get_speed_from_setting( - self.retract_speed, - round_to=1 - ) - - def get_deretract_speed(self): - if self.deretract_speed is None or len("{}".format(self.deretract_speed).strip()) == 0: - return None - deretract_speed = self.deretract_speed - - if float(self.deretract_speed) == 0: - if self.retract_speed is None or len("{}".format(self.retract_speed).strip()) == 0: - return None - deretract_speed = self.retract_speed - - return self.get_speed_from_setting( - deretract_speed, - round_to=1 - ) - - def update_settings_from_gcode(self, settings_dict): + def update_settings_from_gcode(self, settings_dict, printer_profile): try: + # clear out the extruders + self.extruders = [] for key, value in settings_dict.items(): + # first see if this is an extruder setting + if isinstance(value, list) and key in [ + "retract_length", + "retract_lift", + "retract_speed", + "deretract_speed" + ]: + # expand the extruders list if necessary + while len(value) > len(self.extruders): + self.extruders.append(Slic3rPeExtruder()) + extruder_index = 0 + for extruder_value in value: + extruder = self.extruders[extruder_index] + class_item = getattr(extruder, key, '{octolapse_no_property_found}') + if not ( + # Remove python 2 support + # isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}' + isinstance(class_item, str) and class_item == '{octolapse_no_property_found}' + ): + # All of the extruder values are floats + extruder.__dict__[key] = float(extruder_value) + extruder_index += 1 + continue class_item = getattr(self, key, '{octolapse_no_property_found}') - if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + # Remove python 2 support + #if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): if key == 'version': self.version = value["version"] elif isinstance(class_item, Settings): @@ -2595,61 +3339,128 @@ def update_settings_from_gcode(self, settings_dict): @classmethod def try_convert_value(cls, destination, value, key): - if value is None or (isinstance(value, six.string_types) and value == 'None'): + # Remove python 2 support + # if value is None or (isinstance(value, six.string_types) and value == 'None'): + if value is None or (isinstance(value, str) and value == 'None'): return None + return super(Slic3rPeSettings, cls).try_convert_value(destination, value, key) + def update(self, iterable, **kwargs): + try: + item_to_iterate = iterable + if not isinstance(iterable, Iterable): + item_to_iterate = iterable.__dict__ + for key, value in item_to_iterate.items(): + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + #if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + if key == "extruders": + self.extruders = [] + for extruder in value: + new_extruder = Slic3rPeExtruder() + new_extruder.update(extruder) + self.extruders.append(new_extruder) + elif isinstance(class_item, Settings): + class_item.update(value) + elif isinstance(class_item, StaticSettings): + # don't update any static settings, those come from the class itself! + continue + else: + self.__dict__[key] = self.try_convert_value(class_item, value, key) + except Exception as e: + raise e -class OtherSlicerSettings(SlicerSettings): - def __init__(self, version="unknown"): - super(OtherSlicerSettings, self).__init__(SlicerSettings.SlicerTypeOther, version) + +class OtherSlicerExtruder(SlicerExtruder): + def __init__(self): + super(OtherSlicerExtruder, self).__init__() self.retract_length = None self.z_hop = None - self.travel_speed = None self.retract_speed = None self.deretract_speed = None - self.print_speed = None + self.lift_when_retracted = False + self.retract_before_move = False + self.travel_speed = None self.z_travel_speed = None - self.lift_when_retracted = None - self.retract_before_move = None + + def get_travel_speed(self, slicer_settings): + return slicer_settings.get_speed_mm_min(self.travel_speed) + + def get_z_travel_speed(self, slicer_settings): + return slicer_settings.get_speed_mm_min(self.z_travel_speed) + + def get_retract_speed(self, slicer_settings): + return slicer_settings.get_speed_mm_min(self.retract_speed) + + def get_deretract_speed(self, slicer_settings): + return slicer_settings.get_speed_mm_min(self.deretract_speed) + + def get_extruder(self, slicer_settings): + extruder = OctolapseExtruderGcodeSettings() + extruder.retract_before_move = self.retract_before_move + extruder.retraction_length = self.retract_length + extruder.retraction_speed = self.get_retract_speed(slicer_settings) + extruder.deretraction_speed = self.get_deretract_speed(slicer_settings) + extruder.lift_when_retracted = self.lift_when_retracted + extruder.z_lift_height = self.z_hop + extruder.x_y_travel_speed = self.get_travel_speed(slicer_settings) + extruder.first_layer_travel_speed = self.get_travel_speed(slicer_settings) + extruder.z_lift_speed = self.get_z_travel_speed(slicer_settings) + return extruder + + def update(self, iterable, **kwargs): + item_to_iterate = iterable + if not isinstance(iterable, Iterable): + item_to_iterate = iterable.__dict__ + for key, value in item_to_iterate.items(): + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + try: + if key in ['retract_before_move','lift_when_retracted']: + self.__dict__[key] = None if value is None else bool(value) + else: + self.__dict__[key] = None if value is None else float(value) + except ValueError as e: + logger.exception( + "Unable to update the current Slic3r extruder profile. Key:{}, Value:{}".format(key, value)) + continue + + +class OtherSlicerSettings(SlicerSettings): + def __init__(self, version="unknown"): + super(OtherSlicerSettings, self).__init__(SlicerSettings.SlicerTypeOther, version) self.speed_tolerance = 1 self.axis_speed_display_units = 'mm-min' - self.vase_mode = None + self.vase_mode = False self.layer_height = None - def update_settings_from_gcode(self, settings_dict): + def update_settings_from_gcode(self, settings_dict, printer_profile): raise Exception("Cannot update 'Other Slicer' from gcode file! Please select another slicer type to use this " "function.") + def get_extruders(self): + extruders = [] + for extruder in self.extruders: + extruders.append(extruder.get_extruder(self)) + return extruders + def get_speed_tolerance(self): if self.axis_speed_display_units == 'mm-sec': return self.speed_tolerance * 60.0 return self.speed_tolerance - def get_gcode_generation_settings(self, slicer_typ=None): + def get_gcode_generation_settings(self, slicer_type=None): """Returns OctolapseSlicerSettings""" settings = OctolapseGcodeSettings() - settings.retraction_length = self.get_retract_length() - settings.retraction_speed = self.get_retract_speed() - settings.deretraction_speed = self.get_deretract_speed() - settings.x_y_travel_speed = self.get_travel_speed() - settings.z_lift_height = self.get_z_hop() - settings.z_lift_speed = self.get_z_hop_speed() - settings.lift_when_retracted = self.lift_when_retracted - settings.retract_before_move = self.retract_before_move settings.layer_height = self.layer_height settings.vase_mode = self.vase_mode + settings.extruders = self.get_extruders() return settings - def get_retract_length(self): - return self.retract_length - - def get_z_hop(self): - return self.z_hop - - def get_z_hop_speed(self): - return self.get_travel_speed() - def get_speed_mm_min(self, speed, multiplier=None, setting_name=None): if speed is None: return None @@ -2659,14 +3470,28 @@ def get_speed_mm_min(self, speed, multiplier=None, setting_name=None): # Todo - Look at this, we need to round prob. return speed - def get_travel_speed(self): - return self.get_speed_mm_min(self.travel_speed) - - def get_z_travel_speed(self): - return self.get_speed_mm_min(self.z_travel_speed) - - def get_retract_speed(self): - return self.get_speed_mm_min(self.retract_speed) - - def get_deretract_speed(self): - return self.get_speed_mm_min(self.deretract_speed) + def update(self, iterable, **kwargs): + try: + item_to_iterate = iterable + if not isinstance(iterable, Iterable): + item_to_iterate = iterable.__dict__ + for key, value in item_to_iterate.items(): + class_item = getattr(self, key, '{octolapse_no_property_found}') + # Remove python 2 support + # if not (isinstance(class_item, six.string_types) and class_item == '{octolapse_no_property_found}'): + if not (isinstance(class_item, str) and class_item == '{octolapse_no_property_found}'): + if key == "extruders": + self.extruders = [] + for extruder in value: + new_extruder = OtherSlicerExtruder() + new_extruder.update(extruder) + self.extruders.append(new_extruder) + elif isinstance(class_item, Settings): + class_item.update(value) + elif isinstance(class_item, StaticSettings): + # don't update any static settings, those come from the class itself! + continue + else: + self.__dict__[key] = self.try_convert_value(class_item, value, key) + except Exception as e: + raise e diff --git a/octoprint_octolapse/settings_external.py b/octoprint_octolapse/settings_external.py index f2c18d0e..e3663203 100644 --- a/octoprint_octolapse/settings_external.py +++ b/octoprint_octolapse/settings_external.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -21,13 +21,14 @@ # following email address: FormerLurker@pm.me ################################################################################## from __future__ import unicode_literals -from distutils.version import LooseVersion +from octoprint_octolapse_setuptools import NumberedVersion import requests import uuid -import tempfile import json -import six +# remove unused usings +# import six import os +import copy # create the module level logger from octoprint_octolapse.log import LoggingConfigurator @@ -77,7 +78,6 @@ def _load_available_profiles(file_path): except ValueError as e: raise ExternalSettingsError('Error loading available profiles from json path.', cause=e) - @staticmethod def _get_profiles_from_server(current_octolapse_version): # get the best settings version, or raise an error if we can't get one @@ -119,16 +119,15 @@ def _get_profile_for_keys(profiles, key_values, profile_type): profiles = profile["values"] return None - @staticmethod - def check_for_updates(available_profiles, updatable_profiles, force_updates): + def check_for_updates(available_profiles, updatable_profiles, force_updates, ignore_suppression): profiles_to_update = { "printer": [], "stabilization": [], "trigger": [], "rendering": [], "camera": [], - "debug": [], + "logging": [], } updates_available = False @@ -138,7 +137,9 @@ def check_for_updates(available_profiles, updatable_profiles, force_updates): available_profiles is not None ): # loop through the updatable profile dicts - for profile_type, value in six.iteritems(updatable_profiles): + # Remove python 2 support + # for profile_type, value in six.iteritems(updatable_profiles): + for profile_type, value in updatable_profiles.items(): # loop through the profiles for updatable_profile in value: # get the available profile for the updatable profile keys @@ -154,13 +155,13 @@ def check_for_updates(available_profiles, updatable_profiles, force_updates): if ( updatable_profile["version"] is None or ( - LooseVersion(str(available_profile["version"])) > LooseVersion(str(updatable_profile["version"])) and + NumberedVersion(str(available_profile["version"])) > NumberedVersion(str(updatable_profile["version"])) and ( not updatable_profile["suppress_update_notification_version"] or ( - force_updates or - LooseVersion(str(available_profile["version"])) > - LooseVersion(str(updatable_profile["suppress_update_notification_version"])) + force_updates or ignore_suppression or + NumberedVersion(str(available_profile["version"])) > + NumberedVersion(str(updatable_profile["suppress_update_notification_version"])) ) ) ) @@ -184,16 +185,24 @@ def get_profile(current_octolapse_version, profile_type, key_values): r = requests.get(url, timeout=float(5)) if r.status_code != requests.codes.ok: message = ( - "An invalid status code or {0} was returned while getting available profiles." - .format(r.status_code) + "An invalid status code or {0} was returned while getting available profiles at {1}." + .format(r.status_code, url) ) raise ExternalSettingsError('invalid-status-code', message) if 'content-length' in r.headers and r.headers["content-length"] == 0: - message = "No profile data was returned." + message = "No profile data was returned for a request at {0}.".format(url) raise ExternalSettingsError('no-data', message) # if we're here, we've had great success! - return r.json() + json_value = r.json() + # make sure the key values match by replacing any existing key values with the request values. + if "automatic_configuration" in json_value and "key_values" in json_value["automatic_configuration"]: + json_value["automatic_configuration"]["key_values"] = copy.deepcopy(key_values) + + # remove any guid value + if "guid" in json_value: + del json_value["guid"] + return json_value @staticmethod def _get_url_for_profile(settings_version, profile_type, key_values): @@ -206,21 +215,22 @@ def _get_url_for_profile(settings_version, profile_type, key_values): @staticmethod def _get_best_settings_version(current_version): - # load the available versions + # load the available versions for the current settings version. This number will be incremented as the + # settings change enough to not be backwards compatible versions = ExternalSettings._get_versions()["versions"] if not versions: return None - versions.sort(key=LooseVersion) + + versions.sort(key=NumberedVersion) + settings_version = None for version in versions: - if LooseVersion(str(version)) >= LooseVersion(str(current_version)): - settings_version = version + if NumberedVersion(str(version)) > NumberedVersion(str(NumberedVersion.CurrentSettingsVersion)): break + settings_version = version - if settings_version is None: - message = "No available settings were found for the current version ((0)) of Octolapse. This is probably " \ - "a development or alpha version. Try back soon!".format(version) - raise ExternalSettingsError('no-version-found', message) + if settings_version is None and len(versions) > 0: + settings_version = versions[len(versions)-1] return settings_version @staticmethod @@ -246,7 +256,8 @@ def _get_versions(): except ( requests.exceptions.HTTPError, requests.exceptions.ConnectionError, - requests.exceptions.ConnectTimeout + requests.exceptions.ConnectTimeout, + requests.exceptions.ReadTimeout ) as e: message = "An error occurred while retrieving profiles from the server." raise ExternalSettingsError('profiles-retrieval-error', message, cause=e) diff --git a/octoprint_octolapse/gcode_preprocessing.py b/octoprint_octolapse/settings_preprocessor.py similarity index 87% rename from octoprint_octolapse/gcode_preprocessing.py rename to octoprint_octolapse/settings_preprocessor.py index 712f4909..5bb1b45d 100644 --- a/octoprint_octolapse/gcode_preprocessing.py +++ b/octoprint_octolapse/settings_preprocessor.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -27,7 +27,9 @@ import datetime from file_read_backwards import FileReadBackwards import re -import six +# remove unused usings +# import six +# import string # create the module level logger from octoprint_octolapse.log import LoggingConfigurator logging_configurator = LoggingConfigurator() @@ -228,7 +230,9 @@ def is_complete(self): @staticmethod def get_comment(self, line): - assert (isinstance(line, six.string_types)) + # Remove python 2 support + # assert (isinstance(line, six.string_types)) + assert (isinstance(line, str)) match_position = line.find(u';') if match_position > -1 and len(line) > match_position + 1: return line[match_position + 1:].strip() @@ -261,7 +265,7 @@ def reset(self): def get_regex_definitions(self): raise NotImplementedError(u'You must override get_regex_definitions') - + @staticmethod def get_settings_dictionary(): raise NotImplementedError(u'You must override get_settings_dictionary') @@ -274,7 +278,9 @@ def on_apply_filter(self, filter_tags=None): self.active_settings_dictionary = {} self.active_regex_definitions = {} # copy any matching settings definitions - for key, setting in six.iteritems(self.all_settings_dictionary): + # Remove python 2 support + # for key, setting in six.iteritems(self.all_settings_dictionary): + for key, setting in self.all_settings_dictionary.items(): if ( filter_tags is None or len(filter_tags) == 0 @@ -284,7 +290,9 @@ def on_apply_filter(self, filter_tags=None): setting.name, setting.parsing_function, setting.tags ) # apply regex filters - for key, regex in six.iteritems(self.all_regex_definitions): + # Remove python 2 support + # for key, regex in six.iteritems(self.all_regex_definitions): + for key, regex in self.all_regex_definitions.items(): if ( filter_tags is None or len(filter_tags) == 0 @@ -315,7 +323,9 @@ def process_line(self, line, line_number, process_type): self.reverse_lines_processed += 1 logger.verbose("Process type: %s, line: %s, gcode: %s", process_type, line_number, line) - for key, regex_definition in six.iteritems(self.active_regex_definitions): + # Remove python 2 support + # for key, regex_definition in six.iteritems(self.active_regex_definitions): + for key, regex_definition in self.active_regex_definitions.items(): if regex_definition.match_once and regex_definition.has_matched: continue try: @@ -423,6 +433,10 @@ def get_string(parse_string): def parse_string_csv(parse_string): return map(str.strip, parse_string.split(u',')) + @staticmethod + def parse_string_semicolon_separated_value(parse_string): + return map(str.strip, parse_string.split(u';')) + @staticmethod def parse_float_csv(parse_string): str_array = parse_string.split(u',') @@ -527,7 +541,7 @@ def parse_filament_used(parse_string): str_array = parse_string.split(u' ') if len(str_array) == 2: mm_used = Slic3rParsingFunctions.parse_mm(str_array[0]) - cm3_used = Slic3rParsingFunctions.parse_cm3(str_array[1].encode(u'utf-8').translate(None, b'()')) + cm3_used = Slic3rParsingFunctions.parse_cm3(str_array[1].encode(u'utf-8').decode().translate({"()": None})) return { u'mm': mm_used, u'cm3': cm3_used @@ -538,9 +552,9 @@ def parse_hhmmss(parse_string): # separate the two values str_array = parse_string.split(u' ') if len(str_array) == 3: - hh = Slic3rParsingFunctions.parse_int(str_array[0].encode(u'utf-8').translate(None, b'h')) - mm = Slic3rParsingFunctions.parse_int(str_array[1].encode(u'utf-8').translate(None, b'm')) - ss = Slic3rParsingFunctions.parse_int(str_array[2].encode(u'utf-8').translate(None, b's')) + hh = Slic3rParsingFunctions.parse_int(str_array[0].encode(u'utf-8').decode().translate({ord(c):None for c in 'h'})) + mm = Slic3rParsingFunctions.parse_int(str_array[1].encode(u'utf-8').decode().translate({ord(c):None for c in 'm'})) + ss = Slic3rParsingFunctions.parse_int(str_array[2].encode(u'utf-8').decode().translate({ord(c):None for c in 's'})) return { u'hours': hh, u'minutes': mm, @@ -585,14 +599,18 @@ def parse_bed_shape(parse_string): } @staticmethod - def parse_xy(parse_string): - xy = parse_string[0].split(u'x') - if len(xy) == 2: - return { - u'x': Slic3rParsingFunctions.parse_float(xy[0]), - u'y': Slic3rParsingFunctions.parse_float(xy[1]) - } - return None + def parse_xy_csv(parse_string): + offsets_array = parse_string.split(',') + offsets = [] + for offset in offsets_array: + xy = offset.split('x') + if len(xy) == 2: + offsets.append({ + u'x': Slic3rParsingFunctions.parse_float(xy[0]), + u'y': Slic3rParsingFunctions.parse_float(xy[1]) + }) + + return offsets @staticmethod def parse_version(parse_string): @@ -631,18 +649,30 @@ def parse_percent(parse_string): if percent_index < 1: return None try: - return float(parse_string.encode(u'utf-8').translate(None, b'%')) + return float(parse_string.encode(u'utf-8').decode().translate({ord(c):None for c in '%'})) except ValueError: return 0 - except Exception as e: - raise e + + @staticmethod + def parse_percent_csv(parse_string): + percents_array = parse_string.split(',') + percents = [] + for percent in percents_array: + percent_index = percent.find(u'%') + if percent_index < 1: + percents.append(None) + try: + percents.append(percent.encode(u'utf-8').decode().translate({ord(c):None for c in '%'})) + except ValueError: + percents.append(0) + return percents @staticmethod def parse_percent_or_mm(parse_string): percent_index = parse_string.find(u'%') try: if percent_index > -1: - percent = float(parse_string.encode(u'utf-8').translate(None, b'%')) + percent = float(parse_string.encode(u'utf-8').decode().translate({ord(c):None for c in '%'})) return { u'percent': percent } @@ -662,13 +692,12 @@ def parse_filament_used(parse_string): str_array = parse_string.split(u' ') if len(str_array) == 2: mm_used = Slic3rParsingFunctions.parse_mm(str_array[0]) - cm3_used = Slic3rParsingFunctions.parse_cm3(str_array[1].encode(u'utf-8').translate(None, b'()')) + cm3_used = Slic3rParsingFunctions.parse_cm3(str_array[1].encode(u'utf-8').decode().translate({'()': None})) return { u'mm': mm_used, u'cm3': cm3_used } - @staticmethod def parse_version(parse_string): # get version @@ -736,46 +765,35 @@ def get_regex_definitions(self): def get_settings_dictionary(): return { # Octolapse Settings - u'retract_length': SettingsDefinition(u'retract_length', Slic3rParsingFunctions.parse_float,[u'octolapse_setting']), - u'retract_lift': SettingsDefinition(u'retract_lift', Slic3rParsingFunctions.parse_float, [u'octolapse_setting']), - u'deretract_speed': SettingsDefinition(u'deretract_speed', Slic3rParsingFunctions.parse_float, [u'octolapse_setting']), - u'retract_speed': SettingsDefinition(u'retract_speed', Slic3rParsingFunctions.parse_float, [u'octolapse_setting']), + u'retract_length': SettingsDefinition(u'retract_length', Slic3rParsingFunctions.parse_float_csv,[u'octolapse_setting']), + u'retract_lift': SettingsDefinition(u'retract_lift', Slic3rParsingFunctions.parse_float_csv, [u'octolapse_setting']), + u'deretract_speed': SettingsDefinition(u'deretract_speed', Slic3rParsingFunctions.parse_float_csv, [u'octolapse_setting']), + u'retract_speed': SettingsDefinition(u'retract_speed', Slic3rParsingFunctions.parse_float_csv, [u'octolapse_setting']), u'travel_speed': SettingsDefinition(u'travel_speed', Slic3rParsingFunctions.parse_float, [u'octolapse_setting']), u'first_layer_speed': SettingsDefinition(u'first_layer_speed', Slic3rParsingFunctions.parse_percent_or_mm,[u'octolapse_setting']), - u'retract_before_travel': SettingsDefinition(u'retract_before_travel', Slic3rParsingFunctions.parse_float,[u'octolapse_setting']), # this speed is not yet used + u'layer_height': SettingsDefinition(u'layer_height', Slic3rParsingFunctions.parse_float, [u'octolapse_setting']), u'spiral_vase': SettingsDefinition(u'spiral_vase', Slic3rParsingFunctions.parse_bool, [u'octolapse_setting']), u'version': SettingsDefinition(u'version', None, [u'slicer_info', 'octolapse_setting'], True), + # End Octolapse Settings - The rest are included in case they become useful for Octolapse or another project - u'max_print_speed': SettingsDefinition(u'max_print_speed', Slic3rParsingFunctions.parse_float, - [u'misc']), - - u'perimeter_speed': SettingsDefinition(u'perimeter_speed', Slic3rParsingFunctions.parse_float, - [u'misc']), - u'small_perimeter_speed': SettingsDefinition(u'small_perimeter_speed', - Slic3rParsingFunctions.parse_percent_or_mm, - [u'misc']), - u'external_perimeter_speed': SettingsDefinition(u'external_perimeter_speed', - Slic3rParsingFunctions.parse_percent_or_mm, - [u'misc']), - u'infill_speed': SettingsDefinition(u'infill_speed', Slic3rParsingFunctions.parse_float, - [u'misc']), - u'solid_infill_speed': SettingsDefinition(u'solid_infill_speed', Slic3rParsingFunctions.parse_percent_or_mm, - [u'misc']), - u'top_solid_infill_speed': SettingsDefinition(u'top_solid_infill_speed', - Slic3rParsingFunctions.parse_percent_or_mm, - [u'misc']), - u'support_material_speed': SettingsDefinition(u'support_material_speed', Slic3rParsingFunctions.parse_float, - [u'misc']), - u'bridge_speed': SettingsDefinition(u'bridge_speed', Slic3rParsingFunctions.parse_float, - [u'misc']), - u'gap_fill_speed': SettingsDefinition(u'gap_fill_speed', Slic3rParsingFunctions.parse_float, - [u'misc']), - - u'retract_before_wipe': SettingsDefinition(u'retract_before_wipe', - Slic3rParsingFunctions.parse_percent, [u'misc']), - u'wipe': SettingsDefinition(u'wipe', Slic3rParsingFunctions.parse_bool, [u'misc']), + # This setting appears to be calculated and unnecessary + u'retract_before_travel': SettingsDefinition(u'retract_before_travel', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'single_extruder_multi_material': SettingsDefinition(u'single_extruder_multi_material', + Slic3rParsingFunctions.parse_bool, [u'misc']), + u'max_print_speed': SettingsDefinition(u'max_print_speed', Slic3rParsingFunctions.parse_float, [u'misc']), + u'perimeter_speed': SettingsDefinition(u'perimeter_speed', Slic3rParsingFunctions.parse_float, [u'misc']), + u'small_perimeter_speed': SettingsDefinition(u'small_perimeter_speed', Slic3rParsingFunctions.parse_percent_or_mm, [u'misc']), + u'external_perimeter_speed': SettingsDefinition(u'external_perimeter_speed', Slic3rParsingFunctions.parse_percent_or_mm, [u'misc']), + u'infill_speed': SettingsDefinition(u'infill_speed', Slic3rParsingFunctions.parse_float, [u'misc']), + u'solid_infill_speed': SettingsDefinition(u'solid_infill_speed', Slic3rParsingFunctions.parse_percent_or_mm, [u'misc']), + u'top_solid_infill_speed': SettingsDefinition(u'top_solid_infill_speed', Slic3rParsingFunctions.parse_percent_or_mm, [u'misc']), + u'support_material_speed': SettingsDefinition(u'support_material_speed', Slic3rParsingFunctions.parse_float, [u'misc']), + u'bridge_speed': SettingsDefinition(u'bridge_speed', Slic3rParsingFunctions.parse_float, [u'misc']), + u'gap_fill_speed': SettingsDefinition(u'gap_fill_speed', Slic3rParsingFunctions.parse_float, [u'misc']), + u'retract_before_wipe': SettingsDefinition(u'retract_before_wipe', Slic3rParsingFunctions.parse_percent_csv, [u'misc']), + u'wipe': SettingsDefinition(u'wipe', Slic3rParsingFunctions.parse_bool_csv, [u'misc']), u'external perimeters extrusion width': SettingsDefinition(u'external_perimeters_extrusion_width', Slic3rParsingFunctions.parse_mm, [u'misc']), u'perimeters extrusion width': SettingsDefinition(u'perimeters_extrusion_width', Slic3rParsingFunctions.parse_mm, [u'misc']), u'infill extrusion width': SettingsDefinition(u'infill_extrusion_width', Slic3rParsingFunctions.parse_mm, [u'misc']), @@ -787,62 +805,73 @@ def get_settings_dictionary(): u'estimated printing time': SettingsDefinition(u'estimated_printing_time', Slic3rParsingFunctions.parse_hhmmss,[u'misc']), u'avoid_crossing_perimeters': SettingsDefinition(u'avoid_crossing_perimeters', Slic3rParsingFunctions.parse_bool,[u'misc']), u'bed_shape': SettingsDefinition(u'bed_shape', Slic3rParsingFunctions.parse_bed_shape, [u'misc']), - u'bed_temperature': SettingsDefinition(u'bed_temperature', Slic3rParsingFunctions.parse_float, [u'misc']), + u'bed_temperature': SettingsDefinition(u'bed_temperature', Slic3rParsingFunctions.parse_int_csv, [u'misc']), u'before_layer_gcode': SettingsDefinition(u'before_layer_gcode', Slic3rParsingFunctions.get_string, [u'misc']), u'between_objects_gcode': SettingsDefinition(u'between_objects_gcode', Slic3rParsingFunctions.get_string, [u'misc']), u'bridge_acceleration': SettingsDefinition(u'bridge_acceleration', Slic3rParsingFunctions.parse_float, [u'misc']), u'bridge_fan_speed': SettingsDefinition(u'bridge_fan_speed', Slic3rParsingFunctions.parse_float, [u'misc']), u'brim_width': SettingsDefinition(u'brim_width', Slic3rParsingFunctions.parse_float, [u'misc']), u'complete_objects': SettingsDefinition(u'complete_objects', Slic3rParsingFunctions.parse_bool, [u'misc']), - u'cooling': SettingsDefinition(u'cooling', Slic3rParsingFunctions.parse_bool, [u'misc']), + u'cooling': SettingsDefinition(u'cooling', Slic3rParsingFunctions.parse_bool_csv, [u'misc']), u'cooling_tube_length': SettingsDefinition(u'cooling_tube_length', Slic3rParsingFunctions.parse_float, [u'misc']), u'cooling_tube_retraction': SettingsDefinition(u'cooling_tube_retraction', Slic3rParsingFunctions.parse_float, [u'misc']), u'default_acceleration': SettingsDefinition(u'default_acceleration', Slic3rParsingFunctions.parse_float, [u'misc']), - u'disable_fan_first_layers': SettingsDefinition(u'disable_fan_first_layers', Slic3rParsingFunctions.parse_bool, [u'misc']), + u'disable_fan_first_layers': SettingsDefinition(u'disable_fan_first_layers', Slic3rParsingFunctions.parse_int_csv, [u'misc']), u'duplicate_distance': SettingsDefinition(u'duplicate_distance', Slic3rParsingFunctions.parse_float, [u'misc']), u'end_filament_gcode': SettingsDefinition(u'end_filament_gcode', Slic3rParsingFunctions.get_string, [u'misc']), u'end_gcode': SettingsDefinition(u'end_gcode', Slic3rParsingFunctions.get_string, [u'misc']), u'extruder_clearance_height': SettingsDefinition(u'extruder_clearance_height', Slic3rParsingFunctions.parse_float, [u'misc']), u'extruder_clearance_radius': SettingsDefinition(u'extruder_clearance_radius', Slic3rParsingFunctions.parse_float, [u'misc']), - u'extruder_colour': SettingsDefinition(u'extruder_colour', Slic3rParsingFunctions.get_string, [u'misc']), - u'extruder_offset': SettingsDefinition(u'extruder_offset', Slic3rParsingFunctions.parse_xy, [u'misc']), + u'extruder_colour': SettingsDefinition(u'extruder_colour', Slic3rParsingFunctions.parse_string_semicolon_separated_value, [u'misc']), + u'extruder_offset': SettingsDefinition(u'extruder_offset', Slic3rParsingFunctions.parse_xy_csv, [u'misc']), u'extrusion_axis': SettingsDefinition(u'extrusion_axis', Slic3rParsingFunctions.get_string, [u'misc']), - u'extrusion_multiplier': SettingsDefinition(u'extrusion_multiplier', Slic3rParsingFunctions.parse_float, [u'misc']), - u'fan_always_on': SettingsDefinition(u'fan_always_on', Slic3rParsingFunctions.parse_bool, [u'misc']), - u'fan_below_layer_time': SettingsDefinition(u'fan_below_layer_time', Slic3rParsingFunctions.parse_float, [u'misc']), - u'filament_colour': SettingsDefinition(u'filament_colour', Slic3rParsingFunctions.get_string, [u'misc']), - u'filament_cost': SettingsDefinition(u'filament_cost', Slic3rParsingFunctions.parse_float, [u'misc']), - u'filament_density': SettingsDefinition(u'filament_density', Slic3rParsingFunctions.parse_float, [u'misc']), - u'filament_diameter': SettingsDefinition(u'filament_diameter', Slic3rParsingFunctions.parse_float, [u'misc']), - u'filament_loading_speed': SettingsDefinition(u'filament_loading_speed', Slic3rParsingFunctions.parse_float, [u'misc']), - u'filament_max_volumetric_speed': SettingsDefinition(u'filament_max_volumetric_speed', Slic3rParsingFunctions.parse_float, [u'misc']), + u'extrusion_multiplier': SettingsDefinition(u'extrusion_multiplier', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'fan_always_on': SettingsDefinition(u'fan_always_on', Slic3rParsingFunctions.parse_bool_csv, [u'misc']), + u'fan_below_layer_time': SettingsDefinition(u'fan_below_layer_time', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_colour': SettingsDefinition(u'filament_colour', Slic3rParsingFunctions.parse_string_semicolon_separated_value, [u'misc']), + u'filament_cooling_final_speed': SettingsDefinition(u'filament_cooling_final_speed', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_cooling_initial_speed': SettingsDefinition(u'filament_cooling_initial_speed', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_cooling_moves': SettingsDefinition(u'filament_cooling_moves', Slic3rParsingFunctions.parse_int_csv, [u'misc']), + + u'filament_cost': SettingsDefinition(u'filament_cost', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_density': SettingsDefinition(u'filament_density', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_diameter': SettingsDefinition(u'filament_diameter', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_load_time': SettingsDefinition(u'filament_load_time', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_loading_speed': SettingsDefinition(u'filament_loading_speed', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_loading_speed_start': SettingsDefinition(u'filament_loading_speed_start', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_max_volumetric_speed': SettingsDefinition(u'filament_max_volumetric_speed', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_minimal_purge_on_wipe_tower': SettingsDefinition(u'filament_minimal_purge_on_wipe_tower', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_notes': SettingsDefinition(u'filament_notes', Slic3rParsingFunctions.get_string, [u'misc']), u'filament_ramming_parameters': SettingsDefinition(u'filament_ramming_parameters', Slic3rParsingFunctions.get_string, [u'misc']), - u'filament_soluble': SettingsDefinition(u'filament_soluble', Slic3rParsingFunctions.parse_bool, [u'misc']), - u'filament_toolchange_delay': SettingsDefinition(u'filament_toolchange_delay', Slic3rParsingFunctions.parse_float, [u'misc']), - u'filament_type': SettingsDefinition(u'filament_type', Slic3rParsingFunctions.get_string, [u'misc']), - u'filament_unloading_speed': SettingsDefinition(u'filament_unloading_speed', Slic3rParsingFunctions.parse_float, [u'misc']), + u'filament_settings_id': SettingsDefinition(u'filament_settings_id', Slic3rParsingFunctions.parse_string_semicolon_separated_value, [u'misc']), + u'filament_soluble': SettingsDefinition(u'filament_soluble', Slic3rParsingFunctions.parse_bool_csv, [u'misc']), + u'filament_toolchange_delay': SettingsDefinition(u'filament_toolchange_delay', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_type': SettingsDefinition(u'filament_type', Slic3rParsingFunctions.parse_string_semicolon_separated_value, [u'misc']), + u'filament_unload_time': SettingsDefinition(u'filament_unload_time', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_unloading_speed': SettingsDefinition(u'filament_unloading_speed', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'filament_unloading_speed_start': SettingsDefinition(u'filament_unloading_speed_start', Slic3rParsingFunctions.parse_float_csv, [u'misc']), u'first_layer_acceleration': SettingsDefinition(u'first_layer_acceleration', Slic3rParsingFunctions.parse_float, [u'misc']), - u'first_layer_bed_temperature': SettingsDefinition(u'first_layer_bed_temperature', Slic3rParsingFunctions.parse_float, [u'misc']), + u'first_layer_bed_temperature': SettingsDefinition(u'first_layer_bed_temperature', Slic3rParsingFunctions.parse_float_csv, [u'misc']), u'first_layer_extrusion_width': SettingsDefinition(u'first_layer_extrusion_width', Slic3rParsingFunctions.parse_float, [u'misc']), - u'first_layer_temperature': SettingsDefinition(u'first_layer_temperature', Slic3rParsingFunctions.parse_float, [u'misc']), + u'first_layer_temperature': SettingsDefinition(u'first_layer_temperature', Slic3rParsingFunctions.parse_float_csv, [u'misc']), u'gcode_comments': SettingsDefinition(u'gcode_comments', Slic3rParsingFunctions.parse_bool, [u'misc']), u'gcode_flavor': SettingsDefinition(u'gcode_flavor', Slic3rParsingFunctions.get_string, [u'misc']), u'infill_acceleration': SettingsDefinition(u'infill_acceleration', Slic3rParsingFunctions.parse_float, [u'misc']), u'infill_first': SettingsDefinition(u'infill_first', Slic3rParsingFunctions.parse_bool, [u'misc']), u'layer_gcode': SettingsDefinition(u'layer_gcode', Slic3rParsingFunctions.get_string, [u'misc']), - u'max_fan_speed': SettingsDefinition(u'max_fan_speed', Slic3rParsingFunctions.parse_float, [u'misc']), - u'max_layer_height': SettingsDefinition(u'max_layer_height', Slic3rParsingFunctions.parse_float, [u'misc']), + u'max_fan_speed': SettingsDefinition(u'max_fan_speed', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'max_layer_height': SettingsDefinition(u'max_layer_height', Slic3rParsingFunctions.parse_float_csv, [u'misc']), u'max_print_height': SettingsDefinition(u'max_print_height', Slic3rParsingFunctions.parse_float, [u'misc']), u'max_volumetric_extrusion_rate_slope_negative': SettingsDefinition(u'max_volumetric_extrusion_rate_slope_negative', Slic3rParsingFunctions.parse_float, [u'misc']), u'max_volumetric_extrusion_rate_slope_positive': SettingsDefinition(u'max_volumetric_extrusion_rate_slope_positive', Slic3rParsingFunctions.parse_float, [u'misc']), u'max_volumetric_speed': SettingsDefinition(u'max_volumetric_speed', Slic3rParsingFunctions.parse_float, [u'misc']), - u'min_fan_speed': SettingsDefinition(u'min_fan_speed', Slic3rParsingFunctions.parse_float, [u'misc']), - u'min_layer_height': SettingsDefinition(u'min_layer_height', Slic3rParsingFunctions.parse_float, [u'misc']), - u'min_print_speed': SettingsDefinition(u'min_print_speed', Slic3rParsingFunctions.parse_float, [u'misc']), + u'min_fan_speed': SettingsDefinition(u'min_fan_speed', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'min_layer_height': SettingsDefinition(u'min_layer_height', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'min_print_speed': SettingsDefinition(u'min_print_speed', Slic3rParsingFunctions.parse_float_csv, [u'misc']), u'min_skirt_length': SettingsDefinition(u'min_skirt_length', Slic3rParsingFunctions.parse_float, [u'misc']), u'notes': SettingsDefinition(u'notes', Slic3rParsingFunctions.get_string, [u'misc']), - u'nozzle_diameter': SettingsDefinition(u'nozzle_diameter', Slic3rParsingFunctions.parse_float, [u'misc']), + u'nozzle_diameter': SettingsDefinition(u'nozzle_diameter', Slic3rParsingFunctions.parse_float_csv, [u'misc']), u'only_retract_when_crossing_perimeters': SettingsDefinition(u'only_retract_when_crossing_perimeters', Slic3rParsingFunctions.parse_bool, [u'misc']), u'ooze_prevention': SettingsDefinition(u'ooze_prevention', Slic3rParsingFunctions.parse_bool, [u'misc']), u'output_filename_format': SettingsDefinition(u'output_filename_format', Slic3rParsingFunctions.get_string, [u'misc']), @@ -851,21 +880,21 @@ def get_settings_dictionary(): u'post_process': SettingsDefinition(u'post_process', Slic3rParsingFunctions.get_string, [u'misc']), u'printer_notes': SettingsDefinition(u'printer_notes', Slic3rParsingFunctions.get_string, [u'misc']), u'resolution': SettingsDefinition(u'resolution', Slic3rParsingFunctions.parse_float, [u'misc']), - u'retract_layer_change': SettingsDefinition(u'retract_layer_change', Slic3rParsingFunctions.parse_bool, [u'misc']), - u'retract_length_toolchange': SettingsDefinition(u'retract_length_toolchange', Slic3rParsingFunctions.parse_float, [u'misc']), - u'retract_lift_above': SettingsDefinition(u'retract_lift_above', Slic3rParsingFunctions.parse_float, [u'misc']), - u'retract_lift_below': SettingsDefinition(u'retract_lift_below', Slic3rParsingFunctions.parse_float, [u'misc']), - u'retract_restart_extra': SettingsDefinition(u'retract_restart_extra', Slic3rParsingFunctions.parse_float, [u'misc']), - u'retract_restart_extra_toolchange': SettingsDefinition(u'retract_restart_extra_toolchange', Slic3rParsingFunctions.parse_float, [u'misc']), - u'single_extruder_multi_material': SettingsDefinition(u'single_extruder_multi_material', Slic3rParsingFunctions.parse_bool, [u'misc']), + u'retract_layer_change': SettingsDefinition(u'retract_layer_change', Slic3rParsingFunctions.parse_bool_csv, [u'misc']), + u'retract_length_toolchange': SettingsDefinition(u'retract_length_toolchange', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'retract_lift_above': SettingsDefinition(u'retract_lift_above', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'retract_lift_below': SettingsDefinition(u'retract_lift_below', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'retract_restart_extra': SettingsDefinition(u'retract_restart_extra', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'retract_restart_extra_toolchange': SettingsDefinition(u'retract_restart_extra_toolchange', Slic3rParsingFunctions.parse_float_csv, [u'misc']), + u'single_extruder_multi_material_priming': SettingsDefinition(u'single_extruder_multi_material_priming', Slic3rParsingFunctions.parse_bool, [u'misc']), u'skirt_distance': SettingsDefinition(u'skirt_distance', Slic3rParsingFunctions.parse_float, [u'misc']), u'skirt_height': SettingsDefinition(u'skirt_height', Slic3rParsingFunctions.parse_float, [u'misc']), u'skirts': SettingsDefinition(u'skirts', Slic3rParsingFunctions.parse_bool, [u'misc']), - u'slowdown_below_layer_time': SettingsDefinition(u'slowdown_below_layer_time', Slic3rParsingFunctions.parse_float, [u'misc']), + u'slowdown_below_layer_time': SettingsDefinition(u'slowdown_below_layer_time', Slic3rParsingFunctions.parse_float_csv, [u'misc']), u'standby_temperature_delta': SettingsDefinition(u'standby_temperature_delta', Slic3rParsingFunctions.parse_float, [u'misc']), u'start_filament_gcode': SettingsDefinition(u'start_filament_gcode', Slic3rParsingFunctions.get_string, [u'misc']), u'start_gcode': SettingsDefinition(u'start_gcode', Slic3rParsingFunctions.get_string, [u'misc']), - u'temperature': SettingsDefinition(u'temperature', Slic3rParsingFunctions.parse_float, [u'misc']), + u'temperature': SettingsDefinition(u'temperature', Slic3rParsingFunctions.parse_float_csv, [u'misc']), u'threads': SettingsDefinition(u'threads', Slic3rParsingFunctions.parse_int, [u'misc']), u'toolchange_gcode': SettingsDefinition(u'toolchange_gcode', Slic3rParsingFunctions.get_string, [u'misc']), u'use_firmware_retraction': SettingsDefinition(u'use_firmware_retraction', Slic3rParsingFunctions.parse_bool, [u'misc']), @@ -960,16 +989,17 @@ def __init__(self, search_direction="forward", max_forward_search=295, max_rever def get_regex_definitions(self): return { - "general_setting": RegexDefinition("general_setting", "^;\s\s\s(?P.*?),(?P.*)$", self.default_matching_function, tags=[u'octolapse_setting']), - "printer_models_override": RegexDefinition("printer_models_override", "^;\s\s\sprinterModelsOverride$", self.printer_modesl_override_matched, True), - "version": RegexDefinition("version", ";\sG\-Code\sgenerated\sby\sSimplify3D\(R\)\sVersion\s(?P.*)$", self.version_matched, True, tags=[u'octolapse_setting']), - "gocde_date": RegexDefinition("gocde_date", "^; (?PJan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(?P[0-9]?[0-9]), (?P[0-9]?[0-9]?[0-9]?[0-9]) at (?P[0-9]?[0-9]):(?P[0-9]?[0-9]):(?P[0-9]?[0-9])\s(?PAM|PM)$", self.gcode_date_matched, True) + "general_setting": RegexDefinition("general_setting", r"^;\s\s\s(?P.*?),(?P.*)$", self.default_matching_function, tags=[u'octolapse_setting']), + "printer_models_override": RegexDefinition("printer_models_override", r"^;\s\s\sprinterModelsOverride$", self.printer_modesl_override_matched, True), + "version": RegexDefinition("version", r";\sG\-Code\sgenerated\sby\sSimplify3D\(R\)\sVersion\s(?P.*)$", self.version_matched, True, tags=[u'octolapse_setting']), + "gocde_date": RegexDefinition("gocde_date", r"^; (?PJan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(?P[0-9]?[0-9]), (?P[0-9]?[0-9]?[0-9]?[0-9]) at (?P[0-9]?[0-9]):(?P[0-9]?[0-9]):(?P[0-9]?[0-9])\s(?PAM|PM)$", self.gcode_date_matched, True) } @staticmethod def get_settings_dictionary(): return { # Octolapse Settings + u'extruderToolheadNumber': SettingsDefinition(u'extruder_tool_number', Simplify3dParsingFunctions.parse_int_csv, [u'octolapse_setting']), u'primaryExtruder': SettingsDefinition(u'primary_extruder', Simplify3dParsingFunctions.parse_int,[u'extruder',u'octolapse_setting']), u'extruderRetractionDistance': SettingsDefinition(u'extruder_retraction_distance',Simplify3dParsingFunctions.parse_float_csv,[u'extruder', u'octolapse_setting']), u'extruderRetractionZLift': SettingsDefinition(u'extruder_retraction_z_lift',Simplify3dParsingFunctions.parse_float_csv,[u'extruder', u'octolapse_setting']), @@ -980,30 +1010,15 @@ def get_settings_dictionary(): u'spiralVaseMode': SettingsDefinition(u'spiral_vase_mode', Simplify3dParsingFunctions.parse_bool, [u'octolapse_setting']), u'layerHeight': SettingsDefinition(u'layer_height', Simplify3dParsingFunctions.parse_float, [u'octolapse_setting']), # End Octolapse Settings - The rest is included in case it is ever useful for Octolapse of another project! - u'bridgingSpeedMultiplier': SettingsDefinition(u'bridging_speed_multiplier', - Simplify3dParsingFunctions.parse_float, - [u'misc']), - - u'firstLayerUnderspeed': SettingsDefinition(u'first_layer_underspeed', - Simplify3dParsingFunctions.parse_float, [u'misc']), - u'aboveRaftSpeedMultiplier': SettingsDefinition(u'above_raft_speed_multiplier', - Simplify3dParsingFunctions.parse_float, - [u'misc']), - u'primePillarSpeedMultiplier': SettingsDefinition(u'prime_pillar_speed_multiplier', - Simplify3dParsingFunctions.parse_float, - [u'misc']), - u'oozeShieldSpeedMultiplier': SettingsDefinition(u'ooze_shield_speed_multiplier', - Simplify3dParsingFunctions.parse_float, - [u'misc']), - u'defaultSpeed': SettingsDefinition(u'default_speed', Simplify3dParsingFunctions.parse_float, - [u'misc']), - u'outlineUnderspeed': SettingsDefinition(u'outline_underspeed', Simplify3dParsingFunctions.parse_float, - [u'misc']), - u'solidInfillUnderspeed': SettingsDefinition(u'solid_infill_underspeed', - Simplify3dParsingFunctions.parse_float, - [u'misc']), - u'supportUnderspeed': SettingsDefinition(u'support_underspeed', Simplify3dParsingFunctions.parse_float, - [u'misc']), + u'bridgingSpeedMultiplier': SettingsDefinition(u'bridging_speed_multiplier', Simplify3dParsingFunctions.parse_float, [u'misc']), + u'firstLayerUnderspeed': SettingsDefinition(u'first_layer_underspeed', Simplify3dParsingFunctions.parse_float, [u'misc']), + u'aboveRaftSpeedMultiplier': SettingsDefinition(u'above_raft_speed_multiplier', Simplify3dParsingFunctions.parse_float, [u'misc']), + u'primePillarSpeedMultiplier': SettingsDefinition(u'prime_pillar_speed_multiplier', Simplify3dParsingFunctions.parse_float, [u'misc']), + u'oozeShieldSpeedMultiplier': SettingsDefinition(u'ooze_shield_speed_multiplier', Simplify3dParsingFunctions.parse_float, [u'misc']), + u'defaultSpeed': SettingsDefinition(u'default_speed', Simplify3dParsingFunctions.parse_float, [u'misc']), + u'outlineUnderspeed': SettingsDefinition(u'outline_underspeed', Simplify3dParsingFunctions.parse_float, [u'misc']), + u'solidInfillUnderspeed': SettingsDefinition(u'solid_infill_underspeed', Simplify3dParsingFunctions.parse_float, [u'misc']), + u'supportUnderspeed': SettingsDefinition(u'support_underspeed', Simplify3dParsingFunctions.parse_float, [u'misc']), u'version': SettingsDefinition(u'version', Simplify3dParsingFunctions.strip_string, [u'slicer_info'], True), u'gcodeDate': SettingsDefinition(u'gcode_date', Simplify3dParsingFunctions.strip_string, [u'gcode_info'], True), # IMPORTANT NOTE - printerModelsOverride does NOT have a comma if it's empty @@ -1017,7 +1032,6 @@ def get_settings_dictionary(): u'printQuality':SettingsDefinition(u'print_quality', Simplify3dParsingFunctions.strip_string, [u'misc']), u'printExtruders':SettingsDefinition(u'print_extruders', Simplify3dParsingFunctions.parse_string_csv, [u'extruder']), u'extruderName': SettingsDefinition(u'extruder_names', Simplify3dParsingFunctions.parse_string_csv, [u'extruder']), - u'extruderToolheadNumber': SettingsDefinition(u'extruder_tool_number', Simplify3dParsingFunctions.parse_string_csv, [u'extruder']), u'extruderDiameter': SettingsDefinition(u'extrueder_diameter', Simplify3dParsingFunctions.parse_float_csv, [u'extruder']), u'extruderAutoWidth': SettingsDefinition(u'extruder_auto_width', Simplify3dParsingFunctions.parse_bool_csv, [u'extruder']), u'extruderWidth': SettingsDefinition(u'extruder_width', Simplify3dParsingFunctions.parse_float_csv, [u'extruder']), @@ -1224,10 +1238,10 @@ def __init__(self, search_direction="both", max_forward_search=550, max_reverse_ def get_regex_definitions(self): return { "general_setting": RegexDefinition(u"general_setting", u"^; (?P[^,]*?) = (?P.*)", self.default_matching_function, tags=[u'octolapse_setting']), - "version": RegexDefinition(u"version", u"^;Generated\swith\sCura_SteamEngine\s(?P.*)$",self.version_matched,True, tags=[u'octolapse_setting']), - "filament_used_meters": RegexDefinition(u"filament_used_meters", u"^;Filament\sused:\s(?P.*)m$", self.filament_used_meters_matched, True), + "version": RegexDefinition(u"version", r"^;Generated\swith\sCura_SteamEngine\s(?P.*)$", self.version_matched,True, tags=[u'octolapse_setting']), + "filament_used_meters": RegexDefinition(r"filament_used_meters", r"^;Filament\sused:\s(?P.*)m$", self.filament_used_meters_matched, True), "firmware_flavor": RegexDefinition(u"firmware_flavor", u"^;FLAVOR:(?P.*)$", self.firmware_flavor_matched, True), - "layer_height": RegexDefinition(u"layer_height", u"^;Layer\sheight:\s(?P.*)$", self.layer_height_matched, True), + "layer_height": RegexDefinition(r"layer_height", r"^;Layer\sheight:\s(?P.*)$", self.layer_height_matched, True), } @staticmethod @@ -1237,52 +1251,131 @@ def get_settings_dictionary(): # controls z speed for cura version < 4.2 u'max_feedrate_z_override': SettingsDefinition(u'max_feedrate_z_override', CuraParsingFunctions.parse_float,[u'octolapse_setting']), # controls z speed for cura version >= 4.2 - u'speed_z_hop': SettingsDefinition(u'speed_z_hop', CuraParsingFunctions.parse_float, - [u'octolapse_setting']), + u'speed_z_hop': SettingsDefinition(u'speed_z_hop', CuraParsingFunctions.parse_float, [u'octolapse_setting']), u'retraction_amount': SettingsDefinition(u'retraction_amount', CuraParsingFunctions.parse_float, [u'octolapse_setting']), u'retraction_hop': SettingsDefinition(u'retraction_hop', CuraParsingFunctions.parse_float,[u'octolapse_setting']), u'retraction_hop_enabled': SettingsDefinition(u'retraction_hop_enabled', CuraParsingFunctions.parse_bool,[u'octolapse_setting']), - u'retraction_prime_speed': SettingsDefinition(u'retraction_prime_speed', CuraParsingFunctions.parse_int,[u'octolapse_setting']), - u'retraction_retract_speed': SettingsDefinition(u'retraction_retract_speed', CuraParsingFunctions.parse_int,[u'octolapse_setting']), - u'retraction_speed': SettingsDefinition(u'retraction_speed', CuraParsingFunctions.parse_int,[u'octolapse_setting']), + u'retraction_prime_speed': SettingsDefinition(u'retraction_prime_speed', CuraParsingFunctions.parse_float,[u'octolapse_setting']), + u'retraction_retract_speed': SettingsDefinition(u'retraction_retract_speed', CuraParsingFunctions.parse_float,[u'octolapse_setting']), + u'retraction_speed': SettingsDefinition(u'retraction_speed', CuraParsingFunctions.parse_float, [u'octolapse_setting']), # Note that the below speed doesn't represent the initial layer or travel speed. See speed_print_layer_0 # however, a test will need to be performed. - u'speed_travel': SettingsDefinition(u'speed_travel', CuraParsingFunctions.parse_int, [u'octolapse_setting']), + u'speed_travel': SettingsDefinition(u'speed_travel', CuraParsingFunctions.parse_float, [u'octolapse_setting']), u'retraction_enable': SettingsDefinition(u'retraction_enable', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), u'version': SettingsDefinition(u'version', CuraParsingFunctions.strip_string, [u'octolapse_setting']), - u'layer_height': SettingsDefinition(u'layer_height', CuraParsingFunctions.parse_float, [u'octolapse_setting']), - u'smooth_spiralized_contours': SettingsDefinition(u'smooth_spiralized_contours', - CuraParsingFunctions.parse_bool, [u'octolapse_setting']), - u'magic_mesh_surface_mode': SettingsDefinition(u'magic_mesh_surface_mode', - CuraParsingFunctions.strip_string, [u'octolapse_setting']), + u'smooth_spiralized_contours': SettingsDefinition(u'smooth_spiralized_contours', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'machine_extruder_count': SettingsDefinition(u'machine_extruder_count', CuraParsingFunctions.parse_int, [u'octolapse_setting']), + # Extruder Specific Settings + # speed_z_hop - Cura 5.1- + u'max_feedrate_z_override_0': SettingsDefinition(u'max_feedrate_z_override_0', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'max_feedrate_z_override_1': SettingsDefinition(u'max_feedrate_z_override_1', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'max_feedrate_z_override_2': SettingsDefinition(u'max_feedrate_z_override_2', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'max_feedrate_z_override_3': SettingsDefinition(u'max_feedrate_z_override_3', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'max_feedrate_z_override_4': SettingsDefinition(u'max_feedrate_z_override_4', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'max_feedrate_z_override_5': SettingsDefinition(u'max_feedrate_z_override_5', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'max_feedrate_z_override_6': SettingsDefinition(u'max_feedrate_z_override_6', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'max_feedrate_z_override_7': SettingsDefinition(u'max_feedrate_z_override_7', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + # speed_z_hop - Cura 4.2+ + u'speed_z_hop_0': SettingsDefinition(u'speed_z_hop_0', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_z_hop_1': SettingsDefinition(u'speed_z_hop_1', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_z_hop_2': SettingsDefinition(u'speed_z_hop_2', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_z_hop_3': SettingsDefinition(u'speed_z_hop_3', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_z_hop_4': SettingsDefinition(u'speed_z_hop_4', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_z_hop_5': SettingsDefinition(u'speed_z_hop_5', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_z_hop_6': SettingsDefinition(u'speed_z_hop_6', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_z_hop_7': SettingsDefinition(u'speed_z_hop_7', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + # retraction_amount + u'retraction_amount_0': SettingsDefinition(u'retraction_amount_0', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_amount_1': SettingsDefinition(u'retraction_amount_1', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_amount_2': SettingsDefinition(u'retraction_amount_2', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_amount_3': SettingsDefinition(u'retraction_amount_3', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_amount_4': SettingsDefinition(u'retraction_amount_4', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_amount_5': SettingsDefinition(u'retraction_amount_5', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_amount_6': SettingsDefinition(u'retraction_amount_6', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_amount_7': SettingsDefinition(u'retraction_amount_7', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + # retraction_hop + u'retraction_hop_0': SettingsDefinition(u'retraction_hop_0', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_hop_1': SettingsDefinition(u'retraction_hop_1', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_hop_2': SettingsDefinition(u'retraction_hop_2', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_hop_3': SettingsDefinition(u'retraction_hop_3', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_hop_4': SettingsDefinition(u'retraction_hop_4', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_hop_5': SettingsDefinition(u'retraction_hop_5', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_hop_6': SettingsDefinition(u'retraction_hop_6', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_hop_7': SettingsDefinition(u'retraction_hop_7', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + # retraction_hop_enabled + u'retraction_hop_enabled_0': SettingsDefinition(u'retraction_hop_enabled_0', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_hop_enabled_1': SettingsDefinition(u'retraction_hop_enabled_1', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_hop_enabled_2': SettingsDefinition(u'retraction_hop_enabled_2', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_hop_enabled_3': SettingsDefinition(u'retraction_hop_enabled_3', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_hop_enabled_4': SettingsDefinition(u'retraction_hop_enabled_4', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_hop_enabled_5': SettingsDefinition(u'retraction_hop_enabled_5', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_hop_enabled_6': SettingsDefinition(u'retraction_hop_enabled_6', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_hop_enabled_7': SettingsDefinition(u'retraction_hop_enabled_7', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + # retraction_prime_speed + u'retraction_prime_speed_0': SettingsDefinition(u'retraction_prime_speed_0', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_prime_speed_1': SettingsDefinition(u'retraction_prime_speed_1', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_prime_speed_2': SettingsDefinition(u'retraction_prime_speed_2', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_prime_speed_3': SettingsDefinition(u'retraction_prime_speed_3', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_prime_speed_4': SettingsDefinition(u'retraction_prime_speed_4', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_prime_speed_5': SettingsDefinition(u'retraction_prime_speed_5', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_prime_speed_6': SettingsDefinition(u'retraction_prime_speed_6', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_prime_speed_7': SettingsDefinition(u'retraction_prime_speed_7', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + # retraction_retract_speed + u'retraction_retract_speed_0': SettingsDefinition(u'retraction_retract_speed_0', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_retract_speed_1': SettingsDefinition(u'retraction_retract_speed_1', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_retract_speed_2': SettingsDefinition(u'retraction_retract_speed_2', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_retract_speed_3': SettingsDefinition(u'retraction_retract_speed_3', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_retract_speed_4': SettingsDefinition(u'retraction_retract_speed_4', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_retract_speed_5': SettingsDefinition(u'retraction_retract_speed_5', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_retract_speed_6': SettingsDefinition(u'retraction_retract_speed_6', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_retract_speed_7': SettingsDefinition(u'retraction_retract_speed_7', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + # retraction_speed + u'retraction_speed_0': SettingsDefinition(u'retraction_speed_0', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_speed_1': SettingsDefinition(u'retraction_speed_1', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_speed_2': SettingsDefinition(u'retraction_speed_2', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_speed_3': SettingsDefinition(u'retraction_speed_3', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_speed_4': SettingsDefinition(u'retraction_speed_4', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_speed_5': SettingsDefinition(u'retraction_speed_5', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_speed_6': SettingsDefinition(u'retraction_speed_6', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'retraction_speed_7': SettingsDefinition(u'retraction_speed_7', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + # retraction_enable + u'retraction_enable_0': SettingsDefinition(u'retraction_enable_0', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_enable_1': SettingsDefinition(u'retraction_enable_1', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_enable_2': SettingsDefinition(u'retraction_enable_2', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_enable_3': SettingsDefinition(u'retraction_enable_3', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_enable_4': SettingsDefinition(u'retraction_enable_4', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_enable_5': SettingsDefinition(u'retraction_enable_5', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_enable_6': SettingsDefinition(u'retraction_enable_6', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + u'retraction_enable_7': SettingsDefinition(u'retraction_enable_7', CuraParsingFunctions.parse_bool, [u'octolapse_setting']), + # speed_travel + u'speed_travel_0': SettingsDefinition(u'speed_travel_0', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_travel_1': SettingsDefinition(u'speed_travel_1', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_travel_2': SettingsDefinition(u'speed_travel_2', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_travel_3': SettingsDefinition(u'speed_travel_3', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_travel_4': SettingsDefinition(u'speed_travel_4', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_travel_5': SettingsDefinition(u'speed_travel_5', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_travel_6': SettingsDefinition(u'speed_travel_6', CuraParsingFunctions.parse_float, [u'octolapse_setting']), + u'speed_travel_7': SettingsDefinition(u'speed_travel_7', CuraParsingFunctions.parse_float, [u'octolapse_setting']), # End Octolapse Settings - The rest is included in case it is ever helpful for Octolapse or for other projects! - u'speed_infill': SettingsDefinition(u'speed_infill', CuraParsingFunctions.parse_int, - [u'misc']), - u'skirt_brim_speed': SettingsDefinition(u'skirt_brim_speed', CuraParsingFunctions.parse_float, - [u'misc']), + u'magic_mesh_surface_mode': SettingsDefinition(u'magic_mesh_surface_mode', + CuraParsingFunctions.strip_string, [u'misc']), + u'speed_infill': SettingsDefinition(u'speed_infill', CuraParsingFunctions.parse_float, [u'misc']), + u'skirt_brim_speed': SettingsDefinition(u'skirt_brim_speed', CuraParsingFunctions.parse_float, [u'misc']), u'flavor': SettingsDefinition(u'flavor', CuraParsingFunctions.strip_string, [u'misc']), - u'speed_layer_0': SettingsDefinition(u'speed_layer_0', CuraParsingFunctions.parse_float, - [u'misc']), + u'speed_layer_0': SettingsDefinition(u'speed_layer_0', CuraParsingFunctions.parse_float, [u'misc']), u'speed_print': SettingsDefinition(u'speed_print', CuraParsingFunctions.parse_int, [u'misc']), - u'speed_slowdown_layers': SettingsDefinition(u'speed_slowdown_layers', CuraParsingFunctions.parse_int, - [u'misc']), - u'speed_topbottom': SettingsDefinition(u'speed_topbottom', CuraParsingFunctions.parse_float, - [u'misc']), - u'speed_travel_layer_0': SettingsDefinition(u'speed_travel_layer_0', CuraParsingFunctions.parse_float, - [u'misc']), + u'speed_slowdown_layers': SettingsDefinition(u'speed_slowdown_layers', CuraParsingFunctions.parse_int, [u'misc']), + u'speed_topbottom': SettingsDefinition(u'speed_topbottom', CuraParsingFunctions.parse_float, [u'misc']), + u'speed_travel_layer_0': SettingsDefinition(u'speed_travel_layer_0', CuraParsingFunctions.parse_float, [u'misc']), u'speed_wall': SettingsDefinition(u'speed_wall', CuraParsingFunctions.parse_float, [u'misc']), - u'speed_wall_0': SettingsDefinition(u'speed_wall_0', CuraParsingFunctions.parse_float, - [u'misc']), - u'speed_wall_x': SettingsDefinition(u'speed_wall_x', CuraParsingFunctions.parse_float, - [u'misc']), - u'retraction_combing': SettingsDefinition(u'retraction_combing', CuraParsingFunctions.strip_string, - [u'misc']), - u'speed_print_layer_0': SettingsDefinition(u'speed_print_layer_0', CuraParsingFunctions.parse_float, - [u'misc']), + u'speed_wall_0': SettingsDefinition(u'speed_wall_0', CuraParsingFunctions.parse_float, [u'misc']), + u'speed_wall_x': SettingsDefinition(u'speed_wall_x', CuraParsingFunctions.parse_float, [u'misc']), + u'retraction_combing': SettingsDefinition(u'retraction_combing', CuraParsingFunctions.strip_string, [u'misc']), + u'speed_print_layer_0': SettingsDefinition(u'speed_print_layer_0', CuraParsingFunctions.parse_float, [u'misc']), u'filament_used_meters': SettingsDefinition(u'layer_height', CuraParsingFunctions.parse_float, [u'misc']), u'acceleration_enabled': SettingsDefinition(u'acceleration_enabled', CuraParsingFunctions.parse_bool, [u'misc']), u'acceleration_infill': SettingsDefinition(u'acceleration_infill', CuraParsingFunctions.parse_int, [u'misc']), @@ -1444,7 +1537,6 @@ def get_settings_dictionary(): u'machine_endstop_positive_direction_x': SettingsDefinition(u'machine_endstop_positive_direction_x', CuraParsingFunctions.parse_bool, [u'misc']), u'machine_endstop_positive_direction_y': SettingsDefinition(u'machine_endstop_positive_direction_y', CuraParsingFunctions.parse_bool, [u'misc']), u'machine_endstop_positive_direction_z': SettingsDefinition(u'machine_endstop_positive_direction_z', CuraParsingFunctions.parse_bool, [u'misc']), - u'machine_extruder_count': SettingsDefinition(u'machine_extruder_count', CuraParsingFunctions.parse_int, [u'misc']), u'machine_feeder_wheel_diameter': SettingsDefinition(u'machine_feeder_wheel_diameter', CuraParsingFunctions.parse_float, [u'misc']), u'machine_filament_park_distance': SettingsDefinition(u'machine_filament_park_distance', CuraParsingFunctions.parse_int, [u'misc']), u'machine_firmware_retract': SettingsDefinition(u'machine_firmware_retract', CuraParsingFunctions.parse_bool, [u'misc']), diff --git a/octoprint_octolapse/snapshot.py b/octoprint_octolapse/snapshot.py index 0849fc25..7f46f6ba 100644 --- a/octoprint_octolapse/snapshot.py +++ b/octoprint_octolapse/snapshot.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -20,39 +20,56 @@ # You can contact the author either through the git-hub repository, or at the # following email address: FormerLurker@pm.me ################################################################################## -from subprocess import CalledProcessError -import os as os -import shutil -import six +from __future__ import unicode_literals +# remove unused usings +# import six import os +import json from csv import DictWriter -from io import open as i_open from time import sleep import requests -from PIL import ImageFile -import sys -# PIL is in fact in setup.py. + +# Recent versions of Pillow changed the case of the import +# Why!? +try: + from pil import ImageFile +except ImportError: + from PIL import ImageFile + from requests.auth import HTTPBasicAuth -from threading import Thread, Timer, Event +from threading import Thread, Event from tempfile import mkdtemp from uuid import uuid4 from time import time -from PIL import Image - +# Recent versions of Pillow changed the case of the import +# Why!? +try: + from pil import Image +except ImportError: + from PIL import Image + +import errno # create the module level logger import octoprint_octolapse.camera as camera import octoprint_octolapse.utility as utility -from octoprint_octolapse.gcode_parser import Commands +from octoprint_octolapse.gcode_commands import Commands from octoprint_octolapse.utility import TimelapseJobInfo from octoprint_octolapse.log import LoggingConfigurator +import octoprint_octolapse.script as script logging_configurator = LoggingConfigurator() logger = logging_configurator.get_logger(__name__) class SnapshotMetadata(object): METADATA_FILE_NAME = 'metadata.csv' - METADATA_FIELDS = ['snapshot_number', 'file_name', 'time_taken'] + METADATA_FIELDS = [ + 'snapshot_number', 'file_name', 'time_taken', 'layer', 'height', 'x', 'y', 'z', 'e', 'f', + 'x_snapshot', 'y_snapshot' + ] + @staticmethod + def is_metadata_file(file_name): + return file_name.lower() == SnapshotMetadata.METADATA_FILE_NAME def take_in_memory_snapshot(settings, current_camera): """Takes a snapshot from the given camera in a temporary directory, loads the image into memory, and then deletes the file.""" @@ -62,27 +79,30 @@ def take_in_memory_snapshot(settings, current_camera): temp_snapshot_dir = mkdtemp() snapshot_job_info = SnapshotJobInfo( - TimelapseJobInfo(job_guid=uuid4(), print_start_time=time(), print_file_name='overlay_preview'), - temp_snapshot_dir, 0, current_camera) - if current_camera.camera_type == "external-script": + TimelapseJobInfo(job_guid=uuid4(), + print_start_time=time(), + print_file_name='overlay_preview', + ), + temp_snapshot_dir, 0, current_camera, 'in-memory') + if current_camera.camera_type == "script": snapshot_job = ExternalScriptSnapshotJob(snapshot_job_info, settings) else: - snapshot_job = WebcamSnapshotJob(snapshot_job_info, settings) + snapshot_job = WebcamSnapshotJob(snapshot_job_info) + snapshot_job.daemon = True snapshot_job.start() snapshot_job.join() # Copy the image into memory so that we can delete the original file. - with Image.open(snapshot_job_info.full_path) as image_file: + with Image.open(snapshot_job_info.snapshot_full_path) as image_file: return image_file.copy() finally: # Cleanup. - shutil.rmtree(temp_snapshot_dir) + utility.rmtree(temp_snapshot_dir) class CaptureSnapshot(object): def __init__(self, settings, data_directory, cameras, timelapse_job_info, send_gcode_array_callback - , on_new_thumbnail_available_callback): - self.Settings = settings + , on_new_thumbnail_available_callback, on_post_processing_error_callback): self.Cameras = [] for current_camera in cameras: self.Cameras.append(current_camera) @@ -92,14 +112,15 @@ def __init__(self, settings, data_directory, cameras, timelapse_job_info, send_g self.CameraInfos.update( {"{}".format(current_camera.guid): CameraInfo()} ) - self.DataDirectory = data_directory + self.temporary_directory = settings.main_settings.get_temporary_directory(data_directory) self.TimelapseJobInfo = utility.TimelapseJobInfo(timelapse_job_info) self.SnapshotsTotal = 0 self.ErrorsTotal = 0 self.SendGcodeArrayCallback = send_gcode_array_callback self.OnNewThumbnailAvailableCallback = on_new_thumbnail_available_callback + self.on_post_processing_error_callback = on_post_processing_error_callback - def take_snapshots(self): + def take_snapshots(self, metadata={}, no_wait=False): logger.info("Starting snapshot acquisition") start_time = time() @@ -114,42 +135,76 @@ def take_snapshots(self): # pre_snapshot threads if current_camera.on_before_snapshot_script: before_snapshot_job_info = SnapshotJobInfo( - self.TimelapseJobInfo, self.DataDirectory, camera_info.snapshot_count, current_camera + self.TimelapseJobInfo, + self.temporary_directory, + camera_info.snapshot_attempt, + current_camera, + 'before-snapshot', + metadata=metadata ) + thread = ExternalScriptSnapshotJob(before_snapshot_job_info, 'before-snapshot') + thread.daemon = True before_snapshot_threads.append( - ExternalScriptSnapshotJob(before_snapshot_job_info, self.Settings, 'before-snapshot') + thread ) snapshot_job_info = SnapshotJobInfo( - self.TimelapseJobInfo, self.DataDirectory, camera_info.snapshot_count, current_camera + self.TimelapseJobInfo, + self.temporary_directory, + camera_info.snapshot_attempt, + current_camera, + 'snapshot', + metadata=metadata ) - if current_camera.camera_type == "external-script": + if current_camera.camera_type == "script": thread = ExternalScriptSnapshotJob( snapshot_job_info, - self.Settings, 'snapshot', - on_new_thumbnail_available_callback=self.OnNewThumbnailAvailableCallback + on_new_thumbnail_available_callback=self.OnNewThumbnailAvailableCallback, + on_post_processing_error_callback=self.on_post_processing_error_callback ) + thread.daemon = True snapshot_threads.append((thread, snapshot_job_info, None)) elif current_camera.camera_type == "webcam": download_started_event = Event() thread = WebcamSnapshotJob( snapshot_job_info, - self.Settings, download_started_event=download_started_event, - on_new_thumbnail_available_callback=self.OnNewThumbnailAvailableCallback + on_new_thumbnail_available_callback=self.OnNewThumbnailAvailableCallback, + on_post_processing_error_callback=self.on_post_processing_error_callback ) + thread.daemon = True snapshot_threads.append((thread, snapshot_job_info, download_started_event)) - after_snapshot_job_info = SnapshotJobInfo( - self.TimelapseJobInfo, self.DataDirectory, camera_info.snapshot_count, current_camera - ) # post_snapshot threads if current_camera.on_after_snapshot_script: + after_snapshot_job_info = SnapshotJobInfo( + self.TimelapseJobInfo, + self.temporary_directory, + camera_info.snapshot_attempt, + current_camera, + 'after-snapshot', + metadata=metadata + ) + thread = ExternalScriptSnapshotJob(after_snapshot_job_info, 'after-snapshot') + thread.daemon = True after_snapshot_threads.append( - ExternalScriptSnapshotJob(after_snapshot_job_info, self.Settings, 'after-snapshot') + thread ) + # Now that the before snapshot threads are prepared, send any before snapshot gcodes + for current_camera in self.Cameras: + if current_camera.on_before_snapshot_gcode: + on_before_snapshot_gcode = Commands.string_to_gcode_array(current_camera.on_before_snapshot_gcode) + if len(on_before_snapshot_gcode) > 0: + logger.info("Sending on_before_snapshot_gcode for the %s camera.", current_camera.name) + self.SendGcodeArrayCallback( + on_before_snapshot_gcode, + current_camera.timeout_ms / 1000.0, + wait_for_completion=not no_wait, + tags={'before-snapshot-gcode'} + ) + if len(before_snapshot_threads) > 0: logger.info("Starting %d before snapshot threads", len(before_snapshot_threads)) @@ -159,13 +214,17 @@ def take_snapshots(self): # join the pre-snapshot threads for t in before_snapshot_threads: - result = t.join() - assert (isinstance(result, SnapshotJobInfo)) - info = self.CameraInfos[result.camera_guid] - if not result.success: - info.errors_count += 1 - self.ErrorsTotal += 1 - results.append(result) + if not no_wait: + snapshot_job_info = t.join() + assert (isinstance(snapshot_job_info, SnapshotJobInfo)) + if t.snapshot_thread_error: + snapshot_job_info.success = False + snapshot_job_info.error = t.snapshot_thread_error + else: + snapshot_job_info.success = True + else: + snapshot_job_info.success = True + results.append(snapshot_job_info) if len(before_snapshot_threads) > 0: logger.info("Before snapshot threads finished.") @@ -178,31 +237,48 @@ def take_snapshots(self): # now send any gcode for gcode cameras for current_camera in self.Cameras: - if current_camera.camera_type == "printer-gcode": - logger.info("Sending snapshot gcode array to %s.", current_camera.name) - # just send the gcode now so it all goes in order - self.SendGcodeArrayCallback( - Commands.string_to_gcode_array(current_camera.gcode_camera_script), current_camera.timeout_ms/1000.0 - ) + if current_camera.camera_type == "gcode": + script_sent = False + if current_camera.gcode_camera_script: + gcode_camera_script = Commands.string_to_gcode_array(current_camera.gcode_camera_script) + if len(gcode_camera_script) > 0: + logger.info("Sending snapshot gcode array to %s.", current_camera.name) + # just send the gcode now so it all goes in order + self.SendGcodeArrayCallback( + Commands.string_to_gcode_array(current_camera.gcode_camera_script), + current_camera.timeout_ms/1000.0, + wait_for_completion=not no_wait + ) + script_sent = True + if not script_sent: + logger.warning("The gcode camera '%s' is enabled, but failed to produce any snapshot gcode.", current_camera.name) for t, snapshot_job_info, event in snapshot_threads: - if event: - event.wait() - if t.download_error: + if not no_wait: + if event: + event.wait() + else: + snapshot_job_info = t.join() + if t.snapshot_thread_error: snapshot_job_info.success = False - snapshot_job_info.error = t.download_error + snapshot_job_info.error = t.snapshot_thread_error + elif t.post_processing_error: + snapshot_job_info.success = False + snapshot_job_info.error = t.post_processing_error else: snapshot_job_info.success = True else: - snapshot_job_info = t.join() + snapshot_job_info.success = True + info = self.CameraInfos[snapshot_job_info.camera_guid] + info.snapshot_attempt += 1 if snapshot_job_info.success: info.snapshot_count += 1 self.SnapshotsTotal += 1 else: info.errors_count += 1 self.ErrorsTotal += 1 - + info.save(self.temporary_directory, self.TimelapseJobInfo.JobGuid, snapshot_job_info.camera_guid) results.append(snapshot_job_info) if len(snapshot_threads) > 0: @@ -211,19 +287,37 @@ def take_snapshots(self): if len(after_snapshot_threads) > 0: logger.info("Starting %d after snapshot threads.", len(after_snapshot_threads)) + # Now that the after snapshot threads are prepared, send any after snapshot gcodes + for current_camera in self.Cameras: + if current_camera.on_after_snapshot_gcode: + on_after_snapshot_gcode = Commands.string_to_gcode_array(current_camera.on_after_snapshot_gcode) + if len(on_after_snapshot_gcode) > 0: + logger.info("Sending on_after_snapshot_gcode for the %s camera.", current_camera.name) + self.SendGcodeArrayCallback( + on_after_snapshot_gcode, + current_camera.timeout_ms / 1000.0, + wait_for_completion=not no_wait, + tags={'after-snapshot-gcode'} + ) + # start the after-snapshot threads for t in after_snapshot_threads: t.start() # join the after-snapshot threads for t in after_snapshot_threads: - result = t.join() - assert (isinstance(result, SnapshotJobInfo)) - info = self.CameraInfos[result.camera_guid] - if not result.success: - info.errors_count += 1 - self.ErrorsTotal += 1 - results.append(result) + if not no_wait: + snapshot_job_info = t.join() + assert (isinstance(snapshot_job_info, SnapshotJobInfo)) + info = self.CameraInfos[snapshot_job_info.camera_guid] + if t.snapshot_thread_error: + snapshot_job_info.success = False + snapshot_job_info.error = t.snapshot_thread_error + else: + snapshot_job_info.success = True + else: + snapshot_job_info.success = True + results.append(snapshot_job_info) if len(after_snapshot_threads) > 0: logger.info("After snapshot threads complete.") @@ -232,91 +326,63 @@ def take_snapshots(self): return results - def clean_snapshots(self, snapshot_directory, job_directory): - - # get snapshot directory - logger.info("Cleaning snapshots from: %s", snapshot_directory) - - path = os.path.dirname(snapshot_directory + os.sep) - job_path = os.path.dirname(job_directory + os.sep) - if os.path.isdir(path): - try: - shutil.rmtree(path) - logger.info("Snapshots cleaned.") - if not os.listdir(job_path): - shutil.rmtree(job_path) - logger.info("The job directory was empty, removing.") - except Exception: - # Todo: What exceptions do I catch here? - exception_type = sys.exc_info()[0] - value = sys.exc_info()[1] - message = ( - "Snapshot - Clean - Unable to clean the snapshot " - "path at {0}. It may already have been cleaned. " - "Info: ExceptionType:{1}, Exception Value:{2}" - ).format(path, exception_type, value) - logger.info(message) - else: - logger.info("Snapshot - No need to clean snapshots: they have already been removed.") - - def clean_all_snapshots(self): - #TODO: FIX THIS. IT NEEDS TO REMOVE ALL SUBDIRECTORIES IN THE SNAPSHOT FOLDER. - # get snapshot directory - snapshot_directory = utility.get_snapshot_temp_directory( - self.DataDirectory) - logger.info("Cleaning snapshots from: %s", snapshot_directory) - - path = os.path.dirname(snapshot_directory + os.sep) - if os.path.isdir(path): - try: - shutil.rmtree(path) - logger.info("Snapshots cleaned.") - except: - # Todo: What exceptions to catch here? - exception_type = sys.exc_info()[0] - value = sys.exc_info()[1] - message = ( - "Snapshot - Clean - Unable to clean the snapshot " - "path at {0}. It may already have been cleaned. " - "Info: ExceptionType:{1}, Exception Value:{2}" - ).format(path, exception_type, value) - logger.info(message) - else: - logger.info("Snapshot - No need to clean snapshots: they have already been removed.") - class ImagePostProcessing(object): - def __init__(self, snapshot_job_info, on_new_thumbnail_available_callback=None, request=None): + def __init__(self, snapshot_job_info, on_new_thumbnail_available_callback=None, + on_post_processing_error_callback=None, request=None): + assert(isinstance(snapshot_job_info, SnapshotJobInfo)) self.snapshot_job_info = snapshot_job_info self.on_new_thumbnail_available_callback = on_new_thumbnail_available_callback + self.on_post_processing_error_callback = on_post_processing_error_callback self.request = request def process(self): try: - logger.debug("Post-processing snapshot for %s.", self.snapshot_job_info.camera.name) + logger.debug("Post-processing snapshot for the %s camera.", self.snapshot_job_info.camera.name) if self.request is not None: self.save_image_from_request() self.request.close() # Post Processing and Meta Data Creation + # Always write metadata self.write_metadata() - self.transpose_image() - self.create_thumbnail() - if self.on_new_thumbnail_available_callback is not None: - self.on_new_thumbnail_available_callback(self.snapshot_job_info.camera.guid) + # only process image manipulation if the image exists + if os.path.isfile(self.snapshot_job_info.snapshot_full_path): + self.transpose_image() + self.create_thumbnail() + if self.on_new_thumbnail_available_callback is not None: + self.on_new_thumbnail_available_callback(self.snapshot_job_info.camera.guid) + else: + logger.debug( + "Snapshot #%d does not exist for the %s camera.", + self.snapshot_job_info.snapshot_number, + self.snapshot_job_info.camera.name) + except SnapshotError as e: + if self.on_post_processing_error_callback is not None: + self.on_post_processing_error_callback(e) + finally: - logger.debug("Post-processing snapshot for %s complete.", self.snapshot_job_info.camera.name) + logger.debug("Post-processing snapshot for the %s camera complete.", self.snapshot_job_info.camera.name) def save_image_from_request(self): try: - if not os.path.exists(self.snapshot_job_info.directory): - os.makedirs(self.snapshot_job_info.directory) - with open(self.snapshot_job_info.full_path, 'wb+') as snapshot_file: + if not os.path.exists(self.snapshot_job_info.snapshot_directory): + try: + os.makedirs(self.snapshot_job_info.snapshot_directory) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + with open(self.snapshot_job_info.snapshot_full_path, 'wb+') as snapshot_file: for chunk in self.request.iter_content(chunk_size=512 * 1024): if chunk: snapshot_file.write(chunk) - logger.debug("Snapshot - Snapshot saved to disk at %s", self.snapshot_job_info.full_path) + logger.debug("Snapshot - Snapshot saved to disk for the %s camera at %s", + self.snapshot_job_info.camera.name, self.snapshot_job_info.snapshot_full_path) except Exception as e: + logger.exception("An unexpected exception occurred while saving a snapshot from a request for " + "the %s camera.", self.snapshot_job_info.camera.name) raise SnapshotError( 'snapshot-save-error', "An unexpected exception occurred.", @@ -324,19 +390,42 @@ def save_image_from_request(self): ) def write_metadata(self): + metadata_path = os.path.join(self.snapshot_job_info.snapshot_directory, SnapshotMetadata.METADATA_FILE_NAME) + try: - with open(os.path.join(self.snapshot_job_info.directory, SnapshotMetadata.METADATA_FILE_NAME), 'a') as metadata_file: + if not os.path.exists(self.snapshot_job_info.snapshot_directory): + try: + os.makedirs(self.snapshot_job_info.snapshot_directory) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + + with open(metadata_path, 'a') as metadata_file: dictwriter = DictWriter(metadata_file, SnapshotMetadata.METADATA_FIELDS) dictwriter.writerow({ 'snapshot_number': "{}".format(self.snapshot_job_info.snapshot_number), - 'file_name': self.snapshot_job_info.file_name, + 'file_name': self.snapshot_job_info.snapshot_file_name, 'time_taken': "{}".format(time()), + 'layer': "{}".format(None if "layer" not in self.snapshot_job_info.metadata else self.snapshot_job_info.metadata["layer"]), + 'height': "{}".format(None if "height" not in self.snapshot_job_info.metadata else self.snapshot_job_info.metadata["height"]), + 'x': "{}".format(None if "x" not in self.snapshot_job_info.metadata else self.snapshot_job_info.metadata["x"]), + 'y': "{}".format(None if "y" not in self.snapshot_job_info.metadata else self.snapshot_job_info.metadata["y"]), + 'z': "{}".format(None if "z" not in self.snapshot_job_info.metadata else self.snapshot_job_info.metadata["z"]), + 'e': "{}".format(None if "e" not in self.snapshot_job_info.metadata else self.snapshot_job_info.metadata["e"]), + 'f': "{}".format(None if "f" not in self.snapshot_job_info.metadata else self.snapshot_job_info.metadata["f"]), + 'x_snapshot': "{}".format(None if "x_snapshot" not in self.snapshot_job_info.metadata else self.snapshot_job_info.metadata["x_snapshot"]), + 'y_snapshot': "{}".format(None if "y_snapshot" not in self.snapshot_job_info.metadata else self.snapshot_job_info.metadata["y_snapshot"]), }) except Exception as e: + logger.exception("An unexpected exception occurred while saving snapshot metadata for " + "the %s camera.", self.snapshot_job_info.camera.name) raise SnapshotError( 'snapshot-metadata-error', - "Snapshot Download - An unexpected exception occurred while writing snapshot metadata. " - "Check the log file (plugin_octolapse.log) for details.", + "Snapshot Download - An unexpected exception occurred while writing snapshot metadata for the {0} " + "camera. Check the log file (plugin_octolapse.log) for details.".format( + self.snapshot_job_info.camera.name), cause=e ) @@ -344,7 +433,7 @@ def transpose_image(self): try: transpose_setting = self.snapshot_job_info.camera.snapshot_transpose transpose_method = None - snapshot_full_path = self.snapshot_job_info.full_path + snapshot_full_path = self.snapshot_job_info.snapshot_full_path if transpose_setting is not None and transpose_setting != "": if transpose_setting == 'flip_left_right': @@ -361,14 +450,17 @@ def transpose_image(self): transpose_method = Image.TRANSPOSE if transpose_method is not None: - im = Image.open(snapshot_full_path) - im = im.transpose(transpose_method) - im.save(snapshot_full_path) + with Image.open(snapshot_full_path) as img: + img = img.transpose(transpose_method) + img.save(snapshot_full_path) except IOError as e: + logger.exception("An unexpected exception occurred while transposing an image for " + "the %s camera.", self.snapshot_job_info.camera.name) raise SnapshotError( 'snapshot-transpose-error', - "Snapshot transpose - An unexpected IOException occurred while transposing the image. " - "Check the log file (plugin_octolapse.log) for details.", + "Snapshot transpose - An unexpected IOException occurred while transposing the image for the {0} " + "camera. Check the log file (plugin_octolapse.log) for details.".format( + self.snapshot_job_info.camera.name), cause=e ) @@ -380,89 +472,97 @@ def create_thumbnail(self): # create a copy to be used for the full sized latest snapshot image. latest_snapshot_path = utility.get_latest_snapshot_download_path( - self.snapshot_job_info.DataDirectory, self.snapshot_job_info.camera.guid + self.snapshot_job_info.temporary_directory, self.snapshot_job_info.camera.guid ) - shutil.copy(self.snapshot_job_info.full_path, latest_snapshot_path) + + utility.fast_copy(self.snapshot_job_info.snapshot_full_path, latest_snapshot_path) # create a thumbnail of the image basewidth = 500 - img = Image.open(latest_snapshot_path) - wpercent = (basewidth / float(img.size[0])) - hsize = int((float(img.size[1]) * float(wpercent))) - img.thumbnail([basewidth, hsize], Image.ANTIALIAS) - img.save( - utility.get_latest_snapshot_thumbnail_download_path( - self.snapshot_job_info.DataDirectory, self.snapshot_job_info.camera.guid - ), - "JPEG" - ) - - #img = img.resize((basewidth, hsize), Image.ANTIALIAS) - #img.save(utility.get_latest_snapshot_thumbnail_download_path( - # self.snapshot_job_info.DataDirectory, self.snapshot_job_info.camera.guid), "JPEG" - #) + with Image.open(latest_snapshot_path) as img: + wpercent = (basewidth / float(img.size[0])) + hsize = int((float(img.size[1]) * float(wpercent))) + img.thumbnail([basewidth, hsize], Image.LANCZOS) + img.save( + utility.get_latest_snapshot_thumbnail_download_path( + self.snapshot_job_info.temporary_directory, self.snapshot_job_info.camera.guid + ), + "JPEG" + ) except Exception as e: - + logger.exception("An unexpected exception occurred while creating a snapshot thumbnail for " + "the %s camera.", self.snapshot_job_info.camera.name) # If we can't create the thumbnail, just log raise SnapshotError( 'snapshot-thumbnail-create-error', - "Create Thumbnail - An unexpected exception occurred while creating a snapshot thumbnail. " - "Check the log file (plugin_octolapse.log) for details.", + "Create Thumbnail - An unexpected exception occurred while creating a snapshot thumbnail for the" + " {0} camera. Check the log file (plugin_octolapse.log) for details.".format( + self.snapshot_job_info.camera.name + ), cause=e ) class SnapshotThread(Thread): - def __init__(self, snapshot_job_info, settings, on_new_thumbnail_available_callback=None): + def __init__(self, snapshot_job_info, on_new_thumbnail_available_callback=None, + on_post_processing_error_callback=None): super(SnapshotThread, self).__init__() self.snapshot_job_info = snapshot_job_info - self.Settings = settings self.on_new_thumbnail_available_callback = on_new_thumbnail_available_callback - self.post_processing_errors = None - self.download_error = None + self.on_post_processing_error_callback = on_post_processing_error_callback + self.post_processing_error = None + self.snapshot_thread_error = None def join(self, timeout=None): super(SnapshotThread, self).join(timeout=timeout) + return self.snapshot_job_info def apply_camera_delay(self): # Some users had issues just using sleep.In one examined instance the time.sleep # function was being called to sleep 0.250 S, but waited 0.005 S. To deal with this a sleep loop was - # implemented that makes sure we've waited at least self.DelaySeconds seconds before continuing. + # implemented that makes sure we've waited at least self.delay_seconds seconds before continuing. t0 = time() # record the number of sleep attempts for debug purposes sleep_tries = 0 - delay_seconds = self.snapshot_job_info.DelaySeconds - (time() - t0) + delay_seconds = self.snapshot_job_info.delay_seconds - (time() - t0) logger.info( - "Snapshot Delay - Waiting %s second(s) after executing the snapshot script.", - self.snapshot_job_info.DelaySeconds + "Snapshot Delay - Waiting %s second(s) after executing the snapshot script for the %s camera.", + self.snapshot_job_info.delay_seconds, self.snapshot_job_info.camera.name ) while delay_seconds >= 0.001: sleep_tries += 1 # increment the sleep try counter sleep(delay_seconds) # sleep the calculated amount - delay_seconds = self.snapshot_job_info.DelaySeconds - (time() - t0) + delay_seconds = self.snapshot_job_info.delay_seconds - (time() - t0) def post_process(self, request=None, block=False): # make sure the snapshot exists before attempting post-processing # since it's possible the image isn't on the pi. # for example it could be on a DSLR's SD memory try: - if request is not None or os.path.isfile(self.snapshot_job_info.full_path): - def post_process(post_processor): - post_processor.process() - image_post_processor = ImagePostProcessing(self.snapshot_job_info, self.on_new_thumbnail_available_callback, request=request) - if not block and not request: - processing_thread = Thread(target=post_process, args=[image_post_processor]) - processing_thread.start() - else: - image_post_processor.process() + def post_process_thread(post_processor): + post_processor.process() + image_post_processor = ImagePostProcessing( + self.snapshot_job_info, + on_new_thumbnail_available_callback=self.on_new_thumbnail_available_callback, + on_post_processing_error_callback=self.on_post_processing_error_callback, + request=request) + if not block and not request: + processing_thread = Thread(target=post_process_thread, args=[image_post_processor]) + processing_thread.daemon = True + processing_thread.start() + else: + image_post_processor.process() except SnapshotError as e: - self.post_processing_errors = e + logger.exception("An unexpected exception occurred while post processing an image for " + "the %s camera.", self.snapshot_job_info.camera.name) + self.post_processing_error = e except Exception as e: - logger.exception(e) - message = "An unexpected exception occurred while post-processing images. See plugin.octolapse.log for details" + message = "An unexpected exception occurred while post-processing images for the {0} camera." \ + " See plugin.octolapse.log for details".format(self.snapshot_job_info.camera.name) + logger.exception(message) raise SnapshotError('post-processing-error', message, cause=e) finally: if request: @@ -470,13 +570,15 @@ def post_process(post_processor): class ExternalScriptSnapshotJob(SnapshotThread): - def __init__(self, snapshot_job_info, settings, script_type, on_new_thumbnail_available_callback=None): + def __init__(self, snapshot_job_info, script_type, on_new_thumbnail_available_callback=None, + on_post_processing_error_callback=None): super(ExternalScriptSnapshotJob, self).__init__( snapshot_job_info, - settings, - on_new_thumbnail_available_callback=on_new_thumbnail_available_callback + on_new_thumbnail_available_callback=on_new_thumbnail_available_callback, + on_post_processing_error_callback=on_post_processing_error_callback ) assert (isinstance(snapshot_job_info, SnapshotJobInfo)) + self.ScriptPath = None if script_type == 'before-snapshot': self.ScriptPath = snapshot_job_info.camera.on_before_snapshot_script elif script_type == 'snapshot': @@ -487,98 +589,111 @@ def __init__(self, snapshot_job_info, settings, script_type, on_new_thumbnail_av self.script_type = script_type def run(self): + logger.info("Snapshot - running %s script for the %s camera.", + self.script_type, self.snapshot_job_info.camera.name) + # execute the script and send the parameters + if self.script_type == 'snapshot': + if self.snapshot_job_info.delay_seconds < 0.001: + logger.debug( + "Snapshot Delay - No pre snapshot delay configured for the %s camera.", + self.snapshot_job_info.camera.name) + else: + self.apply_camera_delay() try: - logger.info("Snapshot - running %s script.", self.script_type) - # execute the script and send the parameters - if self.script_type == 'snapshot': - if self.snapshot_job_info.DelaySeconds < 0.001: - logger.info("Snapshot Delay - No pre snapshot delay configured.") - else: - self.apply_camera_delay() - self.execute_script() - - if self.script_type == 'snapshot': - # Make sure the expected snapshot exists before we start working with the snapshot file. - self.post_process(block=self.on_new_thumbnail_available_callback is None) - - self.snapshot_job_info.success = True - except SnapshotError as e: - self.snapshot_job_info.error = "{}".format(e) + self.snapshot_thread_error = e.message + return + except Exception as e: + message = "An unexpected exception occurred while processing the {0} script for the " \ + "{1} camera.".format(self.script_type, self.snapshot_job_info.camera.name) + logger.exception(message) + self.snapshot_thread_error = message + raise e finally: - logger.info("The %s script job completed, signaling task queue.", self.script_type) + if self.script_type == 'snapshot': + # perform post processing + try: + self.post_process(block=self.on_new_thumbnail_available_callback is None) + except SnapshotError as e: + self.post_processing_error = e + except Exception as e: + message = "An unexpected error occurred during post processing for the {0} camera".format( + self.snapshot_job_info.camera.name) + logger.exception(message) + self.post_processing_error = SnapshotError( + 'post-processing-error', + message, + cause=e + ) + raise e + + logger.info("The %s script job completed, signaling task queue.", self.script_type) def execute_script(self): - logger.info( - "Running the following %s script command with a timeout of %s: %s %s %s %s %s %s %s", - self.script_type, - self.snapshot_job_info.TimeoutSeconds, - self.ScriptPath, - "{}".format(self.snapshot_job_info.SnapshotNumber), - "{}".format(self.snapshot_job_info.DelaySeconds), - self.snapshot_job_info.DataDirectory, - self.snapshot_job_info.directory, - self.snapshot_job_info.file_name, - self.snapshot_job_info.full_path - ) - script_args = [ - self.ScriptPath, - "{}".format(self.snapshot_job_info.SnapshotNumber), - "{}".format(self.snapshot_job_info.DelaySeconds), - self.snapshot_job_info.DataDirectory, - self.snapshot_job_info.directory, - self.snapshot_job_info.file_name, - self.snapshot_job_info.full_path - ] - - try: - cmd = utility.POpenWithTimeout() - return_code = cmd.run(script_args, self.snapshot_job_info.TimeoutSeconds) - console_output = cmd.stdout - error_message = cmd.stderr - except utility.POpenWithTimeout.ProcessError as e: + if self.script_type == "before-snapshot": + cmd = script.CameraScriptBeforeSnapshot( + self.ScriptPath, + self.snapshot_job_info.camera.name, + "{}".format(self.snapshot_job_info.snapshot_number), + "{}".format(self.snapshot_job_info.delay_seconds), + self.snapshot_job_info.temporary_directory, + self.snapshot_job_info.snapshot_directory, + self.snapshot_job_info.snapshot_file_name, + self.snapshot_job_info.snapshot_full_path + ) + elif self.script_type == "after-snapshot": + cmd = script.CameraScriptAfterSnapshot( + self.ScriptPath, + self.snapshot_job_info.camera.name, + "{}".format(self.snapshot_job_info.snapshot_number), + "{}".format(self.snapshot_job_info.delay_seconds), + self.snapshot_job_info.temporary_directory, + self.snapshot_job_info.snapshot_directory, + self.snapshot_job_info.snapshot_file_name, + self.snapshot_job_info.snapshot_full_path + ) + else: + cmd = script.CameraScriptSnapshot( + self.ScriptPath, + self.snapshot_job_info.camera.name, + "{}".format(self.snapshot_job_info.snapshot_number), + "{}".format(self.snapshot_job_info.delay_seconds), + self.snapshot_job_info.temporary_directory, + self.snapshot_job_info.snapshot_directory, + self.snapshot_job_info.snapshot_file_name, + self.snapshot_job_info.snapshot_full_path + ) + cmd.run() + if not cmd.success(): raise SnapshotError( '{0}_script_error'.format(self.script_type), - "An OS Error error occurred while executing the {0} script".format(self.script_type), - cause=e - ) - - if error_message and error_message.endswith("\r\n"): - error_message = error_message[:-2] - - if not return_code == 0: - if error_message: - error_message = "The {0} script failed with the following error message: {1}"\ - .format(self.script_type, error_message) - else: - error_message = ( - "The {0} script returned {1}," - " which indicates an error.".format(self.script_type, return_code) - ) - raise SnapshotError('{0}_script_error'.format(self.script_type), error_message) - elif error_message: - logger.error( - "Error output was returned from the %s script: %s", - self.script_type, - error_message + cmd.error_message ) class WebcamSnapshotJob(SnapshotThread): - def __init__(self, snapshot_job_info, settings, download_started_event=None, on_new_thumbnail_available_callback=None): + def __init__( + self, + snapshot_job_info, + download_started_event=None, + on_new_thumbnail_available_callback=None, + on_post_processing_error_callback=None + ): super(WebcamSnapshotJob, self).__init__( snapshot_job_info, - settings, - on_new_thumbnail_available_callback=on_new_thumbnail_available_callback + on_new_thumbnail_available_callback=on_new_thumbnail_available_callback, + on_post_processing_error_callback=on_post_processing_error_callback ) - self.Address = self.snapshot_job_info.camera.webcam_settings.address - if isinstance(self.Address, six.string_types): - self.Address = self.Address.strip() - self.Username = self.snapshot_job_info.camera.webcam_settings.username - self.Password = self.snapshot_job_info.camera.webcam_settings.password - self.IgnoreSslError = self.snapshot_job_info.camera.webcam_settings.ignore_ssl_error + self.address = self.snapshot_job_info.camera.webcam_settings.address + # Remove python 2 support + # if isinstance(self.address, six.string_types): + if isinstance(self.address, str): + self.address = self.address.strip() + self.username = self.snapshot_job_info.camera.webcam_settings.username + self.password = self.snapshot_job_info.camera.webcam_settings.password + self.ignore_ssl_error = self.snapshot_job_info.camera.webcam_settings.ignore_ssl_error url = camera.format_request_template( self.snapshot_job_info.camera.webcam_settings.address, self.snapshot_job_info.camera.webcam_settings.snapshot_request_template, @@ -587,74 +702,119 @@ def __init__(self, snapshot_job_info, settings, download_started_event=None, on_ self.download_started_event = download_started_event if self.download_started_event: self.download_started_event.clear() - self.Url = url + self.url = url + self.stream_download = self.snapshot_job_info.camera.webcam_settings.stream_download def run(self): - try: - if self.snapshot_job_info.DelaySeconds < 0.001: - logger.debug("Starting Snapshot Download Job Immediately.") - else: - # Pre-Snapshot Delay - self.apply_camera_delay() - except Exception as e: - logger.exception(e) - message="An error occurred while applying the snapshot delay. See plugin.octolapse.log for details." - self.download_error = SnapshotError("snapshot-delay-error", message, e) - if self.download_started_event: - self.download_started_event.set() - return + + if self.snapshot_job_info.delay_seconds < 0.001: + logger.debug( + "Snapshot Delay - No pre snapshot delay configured for the %s camera.", + self.snapshot_job_info.camera.name) + else: + self.apply_camera_delay() r = None start_time = time() try: download_start_time = time() - if len(self.Username) > 0: - logger.debug("Snapshot Download - Authenticating and downloading from %s.", self.Url) + if len(self.username) > 0: + logger.debug( + "Snapshot Download - Authenticating and downloading for the %s camera from %s.", + self.snapshot_job_info.camera.name, + self.url) r = requests.get( - self.Url, - auth=HTTPBasicAuth(self.Username, self.Password), - verify=not self.IgnoreSslError, - timeout=float(self.snapshot_job_info.TimeoutSeconds), - stream=True + self.url, + auth=HTTPBasicAuth(self.username, self.password), + verify=not self.ignore_ssl_error, + timeout=float(self.snapshot_job_info.timeout_seconds), + stream=self.stream_download ) else: - logger.debug("Snapshot - downloading from %s.", self.Url) + logger.debug( + "Snapshot - downloading for the %s camera from %s.", + self.snapshot_job_info.camera.name, + self.url) r = requests.get( - self.Url, - verify=not self.IgnoreSslError, - #timeout=float(self.snapshot_job_info.TimeoutSeconds), - timeout=float(self.snapshot_job_info.TimeoutSeconds), - stream=True + self.url, + verify=not self.ignore_ssl_error, + timeout=float(self.snapshot_job_info.timeout_seconds), + stream=self.stream_download ) r.raise_for_status() - logger.debug("Snapshot - downloaded started in %.3f seconds.", time() - download_start_time) + if self.stream_download: + logger.debug( + "Snapshot - received streaming response for the %s camera e in %.3f seconds.", + self.snapshot_job_info.camera.name, + time() - download_start_time) + else: + logger.debug( + "Snapshot - snapshot downloaded for the %s camera in %.3f seconds.", + self.snapshot_job_info.camera.name, + time() - download_start_time) + except requests.ConnectionError as e: + message = ( + "A connection error occurred while downloading a snapshot for the {0} camera." + .format(self.snapshot_job_info.camera.name, self.snapshot_job_info.timeout_seconds) + ) + logger.error(message) + self.snapshot_thread_error = SnapshotError( + 'snapshot-download-error', + message, + cause=e + ) + except (requests.ConnectTimeout, requests.ReadTimeout) as e: + message = "An timeout occurred downloading a snapshot for the {0} camera in {1:.3f} seconds.".format( + self.snapshot_job_info.camera.name, self.snapshot_job_info.timeout_seconds + ) + logger.error(message) + self.snapshot_thread_error = SnapshotError( + 'snapshot-download-error', + message, + cause=e + ) except Exception as e: message = "An error occurred downloading a snapshot for the {0} camera.".format( self.snapshot_job_info.camera.name ) + logger.exception(message) try: if r: r.close() except Exception as c: - logger.exception(c) - self.download_error = SnapshotError( + logger.exception( + "Unable to close the snapshot download request for the %s camera.", + self.snapshot_job_info.camera.name) + self.snapshot_thread_error = SnapshotError( 'snapshot-download-error', message, cause=e ) - return + raise e finally: if self.download_started_event: self.download_started_event.set() - - # start post processing - try: - self.post_process(request=r, block=self.on_new_thumbnail_available_callback is None) - self.snapshot_job_info.success = True - finally: - logger.info("Snapshot Download Job completed in %.3f seconds.", time()-start_time) + # start post processing + try: + self.post_process(request=r, block=self.on_new_thumbnail_available_callback is None) + except SnapshotError as e: + self.post_processing_error = e + except Exception as e: + message = "An unexpected error occurred during post processing for the {0} camera".format( + self.snapshot_job_info.camera.name) + logger.exception(message) + self.post_processing_error = SnapshotError( + 'post-processing-error', + message, + cause=e + ) + raise e + finally: + logger.info("Snapshot Download Job completed for the %s camera in %.3f seconds.", + self.snapshot_job_info.camera.name, + time()-start_time) class SnapshotError(Exception): @@ -671,28 +831,84 @@ def __str__(self): class SnapshotJobInfo(object): - def __init__(self, timelapse_job_info, data_directory, snapshot_number, current_camera): + def __init__(self, timelapse_job_info, temporary_directory, snapshot_number, current_camera, job_type, metadata={}): self.camera = current_camera - self.directory = os.path.join(data_directory, "snapshots", timelapse_job_info.JobGuid, current_camera.guid) - self.file_name = utility.get_snapshot_filename( - timelapse_job_info.PrintFileName, timelapse_job_info.PrintStartTime, snapshot_number + self.temporary_directory = temporary_directory + self.snapshot_directory = utility.get_temporary_snapshot_job_camera_path( + temporary_directory, timelapse_job_info.JobGuid, current_camera.guid + ) + self.snapshot_file_name = utility.get_snapshot_filename( + timelapse_job_info.PrintFileName, snapshot_number ) + self.snapshot_full_path = os.path.join(self.snapshot_directory, self.snapshot_file_name) self.snapshot_number = snapshot_number self.camera_guid = current_camera.guid self.success = False self.error = "" - self.DelaySeconds = current_camera.delay / 1000.0 - self.TimeoutSeconds = current_camera.timeout_ms / 1000.0 - self.SnapshotNumber = snapshot_number - self.DataDirectory = data_directory - self.SnapshotTranspose = current_camera.snapshot_transpose - - @property - def full_path(self): - return os.path.join(self.directory, self.file_name) + self.delay_seconds = current_camera.delay / 1000.0 + self.timeout_seconds = current_camera.timeout_ms / 1000.0 + self.snapshot_number = snapshot_number + self.job_type = job_type + self.metadata = metadata class CameraInfo(object): + camera_info_filename = "camera_info.json" + + @staticmethod + def is_camera_info_file(file_name): + return file_name.lower() == CameraInfo.camera_info_filename + def __init__(self): + self.snapshot_attempt = 0 self.snapshot_count = 0 self.errors_count = 0 + self.is_empty = False + + @staticmethod + def load(data_folder, print_job_guid, camera_guid): + file_path = os.path.join( + utility.get_temporary_snapshot_job_camera_path(data_folder, print_job_guid, camera_guid), + CameraInfo.camera_info_filename + ) + try: + with open(file_path, 'r') as camera_info: + data = json.load(camera_info) + return CameraInfo.from_dict(data) + except (OSError, IOError, ValueError) as e: + logger.exception("Unable to load camera info from %s.", file_path) + ret_val = CameraInfo() + ret_val.is_empty = True + return ret_val + + def save(self, data_folder, print_job_guid, camera_guid): + file_directory = utility.get_temporary_snapshot_job_camera_path(data_folder, print_job_guid, camera_guid) + file_path = os.path.join( + file_directory, + CameraInfo.camera_info_filename + ) + if not os.path.exists(file_directory): + try: + os.makedirs(file_directory) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + with open(file_path, 'w') as camera_info: + json.dump(self.to_dict(), camera_info) + + def to_dict(self): + return { + "snapshot_attempt": self.snapshot_attempt, + "snapshot_count": self.snapshot_count, + "errors_count": self.errors_count, + } + + @staticmethod + def from_dict(dict_obj): + camera_info = CameraInfo() + camera_info.snapshot_attempt = dict_obj["snapshot_attempt"] + camera_info.snapshot_attempt = dict_obj["snapshot_count"] + camera_info.snapshot_attempt = dict_obj["errors_count"] + return camera_info diff --git a/octoprint_octolapse/gcode.py b/octoprint_octolapse/stabilization_gcode.py similarity index 76% rename from octoprint_octolapse/gcode.py rename to octoprint_octolapse/stabilization_gcode.py index 66c777dc..3e63a688 100644 --- a/octoprint_octolapse/gcode.py +++ b/octoprint_octolapse/stabilization_gcode.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -21,12 +21,14 @@ # following email address: FormerLurker@pm.me ################################################################################## from __future__ import unicode_literals -from octoprint_octolapse.position import Pos, Position -from octoprint_octolapse.gcode_parser import ParsedCommand +from octoprint_octolapse.position import Pos +from octoprint_octolapse.gcode_commands import Commands from octoprint_octolapse.settings import * from octoprint_octolapse.trigger import Triggers # create the module level logger from octoprint_octolapse.log import LoggingConfigurator +# remove unused using +# import json logging_configurator = LoggingConfigurator() logger = logging_configurator.get_logger(__name__) @@ -66,13 +68,46 @@ def append(self, command_type, command): self.EndGcode.append(command) def end_index(self): - return len(self.InitializationGcode) + len(self.StartGcode) + len(self.snapshot_commands) + len(self.ReturnCommands) + len(self.EndGcode) - 1 + return ( + len(self.InitializationGcode) + + len(self.StartGcode) + + len(self.snapshot_commands) + + len(self.ReturnCommands) + + len(self.EndGcode) - 1 + ) def snapshot_index(self): return len(self.InitializationGcode) + len(self.StartGcode) + len(self.snapshot_commands) - 1 - -class SnapshotPlanStep(object): + def __str__(self): + gcode_format_string = "{0:14} - {1}" + gcode_strings = [] + current_index = 0 + for gcode in self.InitializationGcode: + gcode_strings.append(gcode_format_string.format("Init", gcode)) + current_index += 1 + for gcode in self.StartGcode: + gcode_strings.append(gcode_format_string.format("Start", gcode)) + current_index += 1 + for gcode in self.snapshot_commands: + # see if this is the snapshot command + if self.snapshot_index() == current_index: + gcode_strings.append(gcode_format_string.format("Take Snapshot", gcode)) + else: + gcode_strings.append(gcode_format_string.format("Snapshot", gcode)) + current_index += 1 + for gcode in self.ReturnCommands: + gcode_strings.append(gcode_format_string.format("Return", gcode)) + current_index += 1 + for gcode in self.EndGcode: + gcode_strings.append(gcode_format_string.format("End", gcode)) + current_index += 1 + if len(gcode_strings) > 0: + gcode_strings[0] = "\t" + gcode_strings[0] + return '\r\n\t'.join(gcode_strings) + + +class SnapshotPlanStep(utility.JsonSerializable): def __init__(self, action, x=None, y=None, z=None, e=None, f=None): self.x = x self.y = y @@ -92,12 +127,13 @@ def to_dict(self): } -class SnapshotPlan(object): +class SnapshotPlan(utility.JsonSerializable): TRAVEL_ACTION = "travel" def __init__(self, file_line_number=None, file_gcode_number=None, + file_position=None, travel_distance=None, saved_travel_distance=None, triggering_command=None, @@ -110,6 +146,7 @@ def __init__(self, self.saved_travel_distance = saved_travel_distance self.file_line_number = file_line_number self.file_gcode_number = file_gcode_number + self.file_position = file_position self.triggering_command = triggering_command self.initial_position = initial_position self.return_position = return_position @@ -122,11 +159,43 @@ def __init__(self, SNAPSHOT_ACTION = "snapshot" + def get_snapshot_metadata(self): + metadata = { + "layer": None, + "height": None, + "x": None, + "y": None, + "z": None, + "e": None, + } + if self.initial_position is not None: + metadata["layer"] = self.initial_position.layer + metadata["height"] = self.initial_position.height + metadata["x"] = self.initial_position.x + metadata["y"] = self.initial_position.y + metadata["z"] = self.initial_position.z + metadata["f"] = self.initial_position.f + extruder = self.initial_position.get_current_extruder() + if extruder is not None: + metadata["e"] = extruder.e + + x_snapshot = self.initial_position.x + y_snapshot = self.initial_position.y + for step in self.steps: + if step.action == SnapshotPlan.TRAVEL_ACTION: + x_snapshot = step.x + y_snapshot = step.y + break + metadata["x_snapshot"] = x_snapshot + metadata["y_snapshot"] = y_snapshot + return metadata + def to_dict(self): try: return { "file_line_number": self.file_line_number, "file_gcode_number": self.file_gcode_number, + "file_position": self.file_position, 'travel_distance': self.travel_distance, 'saved_travel_distance': self.saved_travel_distance, "triggering_command": None if self.triggering_command is None else self.triggering_command.to_dict(), @@ -144,18 +213,24 @@ def to_dict(self): def create_from_cpp_snapshot_plans(cls, cpp_snapshot_plans): # turn the snapshot plans into a class snapshot_plans = [] + plan_number = 1 try: for cpp_plan in cpp_snapshot_plans: # extract the arguments file_line_number = cpp_plan[0] file_gcode_number = cpp_plan[1] - travel_distance = cpp_plan[2] - saved_travel_distance = cpp_plan[3] - triggering_command = None if cpp_plan[4] is None else ParsedCommand.create_from_cpp_parsed_command(cpp_plan[4]) - start_command = None if cpp_plan[5] is None else ParsedCommand.create_from_cpp_parsed_command(cpp_plan[5]) - initial_position = Pos.create_from_cpp_pos(cpp_plan[6]) + file_position = cpp_plan[2] + travel_distance = cpp_plan[3] + saved_travel_distance = cpp_plan[4] + triggering_command = ( + None if cpp_plan[5] is None else ParsedCommand.create_from_cpp_parsed_command(cpp_plan[5]) + ) + start_command = ( + None if cpp_plan[6] is None else ParsedCommand.create_from_cpp_parsed_command(cpp_plan[6]) + ) + initial_position = Pos.create_from_cpp_pos(cpp_plan[7]) steps = [] - for step in cpp_plan[7]: + for step in cpp_plan[8]: action = step[0] x = step[1] y = step[2] @@ -163,11 +238,12 @@ def create_from_cpp_snapshot_plans(cls, cpp_snapshot_plans): e = step[4] f = step[5] steps.append(SnapshotPlanStep(action, x, y, z, e, f)) - return_position = None if cpp_plan[8] is None else Pos.create_from_cpp_pos(cpp_plan[8]) - end_command = None if cpp_plan[9] is None else ParsedCommand.create_from_cpp_parsed_command(cpp_plan[9]) + return_position = None if cpp_plan[9] is None else Pos.create_from_cpp_pos(cpp_plan[9]) + end_command = None if cpp_plan[10] is None else ParsedCommand.create_from_cpp_parsed_command(cpp_plan[10]) snapshot_plan = SnapshotPlan( file_line_number, file_gcode_number, + file_position, travel_distance, saved_travel_distance, start_command, @@ -177,12 +253,15 @@ def create_from_cpp_snapshot_plans(cls, cpp_snapshot_plans): return_position, end_command) snapshot_plans.append(snapshot_plan) + logger.verbose("Plan %d: %s", plan_number, snapshot_plan) + plan_number += 1 except Exception as e: logger.exception("Failed to create snapshot plans") raise e return snapshot_plans + class SnapshotGcodeGenerator(object): CurrentXPathIndex = 0 CurrentYPathIndex = 0 @@ -192,17 +271,14 @@ def __init__(self, octolapse_settings, overridable_printer_profile_settings): self._stabilization = self.Settings.profiles.current_stabilization() self.StabilizationPaths = self._stabilization.get_stabilization_paths() self.Printer = self.Settings.profiles.current_printer() - self.snapshot_command = self.Printer.snapshot_command self.overridable_printer_profile_settings = overridable_printer_profile_settings - self.has_snapshot_position_errors = False - self.snapshot_position_errors = "" self.gcode_generation_settings = self.Printer.get_current_state_detection_settings() # assert(isinstance(self.gcode_generation_settings, OctolapseGcodeSettings)) # this will be determined by the supplied position object self.g90_g91_affect_extruder = None # variables for gcode generation # return (original) values - + # current valuse self.x_current = None self.y_current = None @@ -217,8 +293,22 @@ def __init__(self, octolapse_settings, overridable_printer_profile_settings): self.distance_to_lift = None self.axis_mode_compatibility = self.Printer.gocde_axis_compatibility_mode_enabled - #Snapshot Gcode Variable + # Gcode Generation Settings + self.x_y_travel_speed = None + self.z_lift_speed = None + # Extruder Settings + self.current_tool = None + self.current_extruder = None + self.retract_before_move = None + self.retraction_length = None + self.retraction_speed = None + self.deretraction_speed = None + self.lift_when_retracted = None + self.z_lift_height = None + + # Snapshot Gcode Variable self.snapshot_gcode = None + # state flags self.retracted_by_start_gcode = None self.lifted_by_start_gcode = None @@ -231,9 +321,7 @@ def initialize_for_snapshot_plan_processing( self, snapshot_plan, g90_influences_extruder, options=None ): assert(isinstance(snapshot_plan, SnapshotPlan)) - # reset any errors - self.has_snapshot_position_errors = False - self.snapshot_position_errors = "" + assert(isinstance(snapshot_plan.initial_position, Pos)) # get the triggering command and extruder position by # undo the most recent position update since we haven't yet executed the most recent gcode command @@ -250,21 +338,39 @@ def initialize_for_snapshot_plan_processing( self.x_current = snapshot_plan.initial_position.x self.y_current = snapshot_plan.initial_position.y self.z_current = snapshot_plan.initial_position.z - self.e_current = snapshot_plan.initial_position.e + self.e_current = snapshot_plan.initial_position.get_current_extruder().e self.f_current = snapshot_plan.initial_position.f self.is_relative_current = snapshot_plan.initial_position.is_relative self.is_extruder_relative_current = snapshot_plan.initial_position.is_extruder_relative + self.current_tool = snapshot_plan.initial_position.current_tool + if self.current_tool > len(self.gcode_generation_settings.extruders) - 1: + logger.warning("The requested tool index of %d is greater than the number of extruders ($d).", + self.current_tool, len(self.gcode_generation_settings.extruders)) + self.current_tool = len(self.gcode_generation_settings.extruders) - 1 + + if self.current_tool < 0: + logger.warning("The requested tool index was less than zero. Index: %d.", + self.current_tool) + self.current_tool = 0 + self.current_extruder = self.gcode_generation_settings.extruders[self.current_tool] + # Get per extruder settings for convenience + self.x_y_travel_speed = self.current_extruder.x_y_travel_speed + self.z_lift_speed = self.current_extruder.z_lift_speed + self.retraction_speed = self.current_extruder.retraction_speed + self.deretraction_speed = self.current_extruder.deretraction_speed + self.retract_before_move = self.current_extruder.retract_before_move + self.retraction_length = self.current_extruder.retraction_length + self.lift_when_retracted = self.current_extruder.lift_when_retracted + self.z_lift_height = self.current_extruder.z_lift_height if options is not None and "disable_z_lift" in options and options["disable_z_lift"]: self.distance_to_lift = 0 else: - z_lift_height = self.Printer.gcode_generation_settings.z_lift_height - self.distance_to_lift = snapshot_plan.initial_position.distance_to_zlift(z_lift_height) + self.distance_to_lift = snapshot_plan.initial_position.distance_to_zlift(self.z_lift_height) if self.distance_to_lift is None: self.distance_to_lift = 0 - retraction_length = self.Printer.gcode_generation_settings.retraction_length - self.length_to_retract = snapshot_plan.initial_position.length_to_retract(retraction_length) + self.length_to_retract = snapshot_plan.initial_position.length_to_retract(self.retraction_length) if self.length_to_retract is None: self.length_to_retract = 0 @@ -369,32 +475,39 @@ def get_snapshot_coordinate(self, path, axis): def get_bed_relative_coordinate(self, axis, coord): rel_coordinate = None + volume = self.overridable_printer_profile_settings["volume"] if axis == "x": - rel_coordinate = self.get_bed_relative_x(coord) + rel_coordinate = self.get_bed_relative_x(coord, volume) elif axis == "y": - rel_coordinate = self.get_bed_relative_y(coord) + rel_coordinate = self.get_bed_relative_y(coord, volume) elif axis == "Z": - rel_coordinate = self.get_bed_relative_z(coord) + rel_coordinate = self.get_bed_relative_z(coord, volume) return rel_coordinate - def get_bed_relative_x(self, percent): - min_value = self.overridable_printer_profile_settings["volume"]["min_x"] - max_value = self.overridable_printer_profile_settings["volume"]["max_x"] - return self.get_relative_coordinate(percent, min_value, max_value) + def get_bed_relative_x(self, percent, volume): + min_value = volume["min_x"] + max_value = volume["max_x"] + origin_type = volume["origin_type"] + return self.get_relative_coordinate(percent, min_value, max_value, origin_type) - def get_bed_relative_y(self, percent): - min_value = self.overridable_printer_profile_settings["volume"]["min_y"] - max_value = self.overridable_printer_profile_settings["volume"]["max_y"] - return self.get_relative_coordinate(percent, min_value, max_value) + def get_bed_relative_y(self, percent, volume): + min_value = volume["min_y"] + max_value = volume["max_y"] + origin_type = volume["origin_type"] + return self.get_relative_coordinate(percent, min_value, max_value, origin_type) - def get_bed_relative_z(self, percent): - min_value = self.overridable_printer_profile_settings["volume"]["min_z"] - max_value = self.overridable_printer_profile_settings["volume"]["max_z"] - return self.get_relative_coordinate(percent, min_value, max_value) + def get_bed_relative_z(self, percent, volume): + min_value = volume["min_z"] + max_value = volume["max_z"] + origin_type = volume["origin_type"] + return self.get_relative_coordinate(percent, min_value, max_value, origin_type) @staticmethod - def get_relative_coordinate(percent, min_value, max_value): + def get_relative_coordinate(percent, min_value, max_value, origin_type): + if origin_type == PrinterProfile.origin_type_center: + return ((float(max_value) - float(min_value)) * (percent / 100.0)) - \ + (float(max_value) - float(min_value))/2.0 return ((float(max_value) - float(min_value)) * (percent / 100.0)) + float(min_value) def set_e_to_relative(self, gcode_type): @@ -433,7 +546,7 @@ def set_xyz_to_absolute(self, gcode_type): self.is_relative_current = False # this may also influence the extruder if self.g90_influences_extruder: - self.is_extruder_relative_current = True + self.is_extruder_relative_current = False def get_altered_feedrate(self, feedrate): if feedrate != self.f_current: @@ -443,12 +556,12 @@ def get_altered_feedrate(self, feedrate): return feedrate def can_retract(self): - return self.gcode_generation_settings.retract_before_move and self.length_to_retract > 0 + return self.retract_before_move and self.length_to_retract > 0 def can_zhop(self): if not ( - self.gcode_generation_settings.retract_before_move and - self.gcode_generation_settings.lift_when_retracted and self.distance_to_lift > 0 + self.retract_before_move and + self.lift_when_retracted and self.distance_to_lift > 0 ): return False max_z = self.overridable_printer_profile_settings["volume"]["max_z"] @@ -466,7 +579,7 @@ def retract_relative(self): SnapshotGcode.START_GCODE, self.get_gcode_retract( length_to_retract, - self.get_altered_feedrate(self.gcode_generation_settings.retraction_speed) + self.get_altered_feedrate(self.retraction_speed) ) ) self.retracted_by_start_gcode = True @@ -481,7 +594,7 @@ def deretract_relative(self): SnapshotGcode.END_GCODE, self.get_gcode_retract( length_to_deretract, - self.get_altered_feedrate(self.gcode_generation_settings.deretraction_speed) + self.get_altered_feedrate(self.deretraction_speed) ) ) @@ -495,7 +608,7 @@ def lift_z_relative(self): SnapshotGcode.START_GCODE, self.get_gcode_z_lift( distance_to_lift, - self.get_altered_feedrate(self.gcode_generation_settings.z_lift_speed) + self.get_altered_feedrate(self.z_lift_speed) ) ) self.lifted_by_start_gcode = True @@ -510,7 +623,7 @@ def delift_z_relative(self): SnapshotGcode.END_GCODE, self.get_gcode_z_lift( distance_to_delift, - self.get_altered_feedrate(self.gcode_generation_settings.z_lift_speed) + self.get_altered_feedrate(self.z_lift_speed) ) ) @@ -526,7 +639,7 @@ def add_travel_action_relative(self, step): self.get_gcode_travel( x_relative, y_relative, - self.get_altered_feedrate(self.gcode_generation_settings.x_y_travel_speed) + self.get_altered_feedrate(self.x_y_travel_speed) ) ) @@ -546,7 +659,7 @@ def return_to_original_position_relative(self): self.get_gcode_travel( x_relative, y_relative, - self.get_altered_feedrate(self.gcode_generation_settings.x_y_travel_speed) + self.get_altered_feedrate(self.x_y_travel_speed) ) ) @@ -561,8 +674,8 @@ def retract_absolute(self): self.snapshot_gcode.append( SnapshotGcode.START_GCODE, self.get_gcode_retract( - self.e_current - self.snapshot_plan.initial_position.e_offset, - self.get_altered_feedrate(self.gcode_generation_settings.retraction_speed) + self.snapshot_plan.initial_position.gcode_e(e=self.e_current), + self.get_altered_feedrate(self.retraction_speed) ) ) @@ -574,8 +687,8 @@ def deretract_absolute(self): self.snapshot_gcode.append( SnapshotGcode.END_GCODE, self.get_gcode_retract( - self.e_current - self.snapshot_plan.initial_position.e_offset, - self.get_altered_feedrate(self.gcode_generation_settings.deretraction_speed) + self.snapshot_plan.initial_position.gcode_e(e=self.e_current), + self.get_altered_feedrate(self.deretraction_speed) ) ) @@ -590,7 +703,7 @@ def lift_z_absolute(self): SnapshotGcode.START_GCODE, self.get_gcode_z_lift( self.z_current - self.snapshot_plan.initial_position.z_offset, - self.get_altered_feedrate(self.gcode_generation_settings.z_lift_speed) + self.get_altered_feedrate(self.z_lift_speed) ) ) @@ -604,13 +717,13 @@ def delift_z_absolute(self): SnapshotGcode.END_GCODE, self.get_gcode_z_lift( self.z_current - self.snapshot_plan.initial_position.z_offset, - self.get_altered_feedrate(self.gcode_generation_settings.z_lift_speed) + self.get_altered_feedrate(self.z_lift_speed) ) ) def add_travel_action_absolute(self, step): assert (isinstance(step, SnapshotPlanStep)) - if self.x_current != step.x and self.y_current != step.y: + if not(self.x_current == step.x and self.y_current == step.y): # Move to Snapshot Position self.set_xyz_to_absolute(SnapshotGcode.SNAPSHOT_COMMANDS) self.x_current = step.x @@ -618,9 +731,9 @@ def add_travel_action_absolute(self, step): self.snapshot_gcode.append( SnapshotGcode.SNAPSHOT_COMMANDS, self.get_gcode_travel( - step.x - self.snapshot_plan.initial_position.x_offset, - step.y - self.snapshot_plan.initial_position.y_offset, - self.get_altered_feedrate(self.gcode_generation_settings.x_y_travel_speed) + self.snapshot_plan.initial_position.gcode_x(x=step.x), + self.snapshot_plan.initial_position.gcode_y(y=step.y), + self.get_altered_feedrate(self.x_y_travel_speed) ) ) @@ -641,9 +754,9 @@ def return_to_original_position_absolute(self): self.snapshot_gcode.append( SnapshotGcode.RETURN_COMMANDS, self.get_gcode_travel( - self.snapshot_plan.return_position.x - self.snapshot_plan.return_position.x_offset, - self.snapshot_plan.return_position.y - self.snapshot_plan.return_position.y_offset, - self.get_altered_feedrate(self.gcode_generation_settings.x_y_travel_speed) + self.snapshot_plan.return_position.gcode_x(), + self.snapshot_plan.return_position.gcode_y(), + self.get_altered_feedrate(self.x_y_travel_speed) ) ) @@ -742,13 +855,14 @@ def add_snapshot_action(self): # Move to Snapshot Position self.snapshot_gcode.append( SnapshotGcode.SNAPSHOT_COMMANDS, - self.Printer.snapshot_command + "{0} {1}".format(PrinterProfile.OCTOLAPSE_COMMAND, PrinterProfile.DEFAULT_OCTOLAPSE_SNAPSHOT_COMMAND) ) def return_to_original_coordinate_systems(self): - is_relative_return = self.snapshot_plan.initial_position.is_relative if self.snapshot_plan.return_position is not None: is_relative_return = self.snapshot_plan.return_position.is_relative + else: + is_relative_return = self.snapshot_plan.initial_position.is_relative if is_relative_return != self.is_relative_current: if self.is_relative_current: @@ -762,10 +876,13 @@ def return_to_original_coordinate_systems(self): self.get_gcode_axes_relative() ) self.is_relative_current = is_relative_return + if self.g90_influences_extruder: + self.is_extruder_relative_current = is_relative_return - is_extruder_relative_return = self.snapshot_plan.initial_position.is_extruder_relative if self.snapshot_plan.return_position is not None: is_extruder_relative_return = self.snapshot_plan.return_position.is_extruder_relative + else: + is_extruder_relative_return = self.snapshot_plan.initial_position.is_extruder_relative if is_extruder_relative_return != self.is_extruder_relative_current: if is_extruder_relative_return: @@ -804,14 +921,12 @@ def return_to_original_feedrate(self): # Functions to send start and end commmand gcode ########################### def send_start_command(self, gcode_type=SnapshotGcode.INITIALIZATION_GCODE): - # If we are returning, add the final command to the end gcode if self.snapshot_plan.start_command is not None: self.snapshot_gcode.append( gcode_type, self.snapshot_plan.start_command.gcode) def send_end_command(self, gcode_type=SnapshotGcode.END_GCODE): - # If we are returning, add the final command to the end gcode if self.snapshot_plan.end_command is not None: self.snapshot_gcode.append( gcode_type, @@ -834,10 +949,13 @@ def get_triggered_type(trigger): def create_snapshot_plan(self, position, trigger): snapshot_plan = SnapshotPlan() + # we have to undo the current position update since we will be operating on the previous position! + final_position = position.undo_update() # the parsed command has not been sent, but record it - snapshot_plan.triggering_command = position.current_pos.parsed_command - parsed_command_position = position.current_pos - current_position = position.previous_pos + snapshot_plan.triggering_command = final_position.parsed_command + parsed_command_position = final_position + current_position = position.current_pos + # create the start and end gcode, which would include any split gcode (position restriction intersection) # or any saved command that needs to be appended @@ -860,11 +978,18 @@ def create_snapshot_plan(self, position, trigger): snapshot_plan.steps.append(travel_step) snapshot_plan.steps.append(snapshot_step) snapshot_plan.return_position = current_position - snapshot_plan.end_command = snapshot_plan.triggering_command + # make sure the current command is not the snapshot command before adding it as the end command + if ( + not self.Printer.is_snapshot_command(snapshot_plan.triggering_command.gcode) + and not snapshot_plan.triggering_command.cmd in Commands.SuppressedSavedCommands + ): + snapshot_plan.end_command = snapshot_plan.triggering_command + if parsed_command_position.is_xy_travel: logger.info("The triggering command is travel only, skipping return command generation") snapshot_plan.return_position = None - elif triggered_type == Triggers.TRIGGER_TYPE_IN_PATH: + elif triggered_type == Triggers.TRIGGER_TYPE_IN_PATH and self._stabilization.wait_for_moves_to_finish: + # if we aren't waiting for moves to finish, there is no reason to split up the gcode... # see if the snapshot command is a g1 or g0 if snapshot_plan.triggering_command.cmd: if snapshot_plan.triggering_command.cmd not in ["G0", "G1"]: @@ -881,8 +1006,6 @@ def create_snapshot_plan(self, position, trigger): snapshot_plan.start_command = gcode_command_1 - # undo the previous update to the position processor - position.undo_update() # calculate the initial position position.update(gcode_command_1.gcode) snapshot_plan.initial_position = position.current_pos @@ -904,16 +1027,16 @@ def create_snapshot_plan(self, position, trigger): def split_extrusion_gcode_at_point(self, triggering_command, start_position, end_position, intersection): # get the coordinates necessary to split one gcode into 2 at an intersection point # start offset coordinates - start_x_offset = start_position.offset_x() - start_y_offset = start_position.offset_y() - start_e_offset = start_position.offset_e() + start_x_offset = start_position.gcode_x() + start_y_offset = start_position.gcode_y() + start_e_offset = start_position.gcode_e() # intersection offset coordinates - intersection_x_offset = intersection[0] - start_position.x_offset - intersection_y_offset = intersection[1] - start_position.y_offset + intersection_x_offset = start_position.gcode_x(x=intersection[0]) + intersection_y_offset = start_position.gcode_y(y=intersection[1]) # end offset coordinates - end_x_offset = end_position.offset_x() - end_y_offset = end_position.offset_y() - end_e_offset = end_position.offset_e() + end_x_offset = end_position.gcode_x() + end_y_offset = end_position.gcode_y() + end_e_offset = end_position.gcode_e() # the feedrate won't change, but record it to make this obvious feedrate = end_position.f @@ -985,29 +1108,33 @@ def split_extrusion_gcode_at_point(self, triggering_command, start_position, end def create_gcode_for_snapshot_plan(self, snapshot_plan, g90_influences_extruder, options): + assert(isinstance(snapshot_plan, SnapshotPlan)) if not self.initialize_for_snapshot_plan_processing( snapshot_plan, g90_influences_extruder, options ): return None - if not self.has_snapshot_position_errors: - # Todo: it's possible that the current command is a detract. If it is we eventually will want to prevent - # create the start command if it exists - self.send_start_command() + # create the start command if it exists + self.send_start_command() + + # There is no need to stabilize the extruder if we aren't waiting for moves to finish + if self._stabilization.wait_for_moves_to_finish: # retract if necessary self.retract() # lift if necessary self.lift_z() - has_taken_snapshot = False - assert(isinstance(snapshot_plan, SnapshotPlan)) - for step in snapshot_plan.steps: - assert(isinstance(step, SnapshotPlanStep)) - if step.action == SnapshotPlan.TRAVEL_ACTION: + + assert(isinstance(snapshot_plan, SnapshotPlan)) + for step in snapshot_plan.steps: + assert(isinstance(step, SnapshotPlanStep)) + if step.action == SnapshotPlan.TRAVEL_ACTION: + if self._stabilization.wait_for_moves_to_finish: self.add_travel_action(step) - if step.action == SnapshotPlan.SNAPSHOT_ACTION: - self.add_snapshot_action() + if step.action == SnapshotPlan.SNAPSHOT_ACTION: + self.add_snapshot_action() + if self._stabilization.wait_for_moves_to_finish: # Create Return Gcode self.return_to_original_position() @@ -1022,16 +1149,28 @@ def create_gcode_for_snapshot_plan(self, snapshot_plan, g90_influences_extruder, self.return_to_original_feedrate() - self.send_end_command() + self.send_end_command() # print out log messages - logger.info( - "Snapshot Gcode - snapshot_commandIndex:%s, EndIndex:%s", - self.snapshot_gcode.snapshot_index(), - self.snapshot_gcode.end_index() - ) - for gcode in self.snapshot_gcode.snapshot_gcode(): - logger.info(" %s", gcode) + if snapshot_plan.initial_position.file_line_number > 0: + logger.info( + "Triggered at line: %s by gcode: %s", + snapshot_plan.initial_position.file_line_number, + snapshot_plan.triggering_command.gcode + ) + else: + logger.info( + "Triggered by gcode: %s", + snapshot_plan.triggering_command.gcode + ) + + # enhanced snapshot gcode logger + if len(self.gcode_generation_settings.extruders) > 0 or snapshot_plan.initial_position.current_tool != 0: + logger.info( + "Snapshot Gcode (Tool: %d):\r\n%s", snapshot_plan.initial_position.current_tool + 1, self.snapshot_gcode + ) + else: + logger.info("Snapshot Gcode:\r\n%s", self.snapshot_gcode) return self.snapshot_gcode diff --git a/octoprint_octolapse/stabilization_preprocessing.py b/octoprint_octolapse/stabilization_preprocessing.py index 93ef19dc..39ddab9c 100644 --- a/octoprint_octolapse/stabilization_preprocessing.py +++ b/octoprint_octolapse/stabilization_preprocessing.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -22,11 +22,13 @@ ################################################################################## from __future__ import unicode_literals from threading import Thread -from six.moves import queue -from octoprint_octolapse.gcode import SnapshotPlan, SnapshotGcodeGenerator +# Remove python 2 support +# from six.moves import queue +import queue as queue +from octoprint_octolapse.stabilization_gcode import SnapshotPlan, SnapshotGcodeGenerator from octoprint_octolapse.settings import PrinterProfile, TriggerProfile, StabilizationProfile import GcodePositionProcessor - +import octoprint_octolapse.error_messages as error_messages # create the module level logger from octoprint_octolapse.log import LoggingConfigurator logging_configurator = LoggingConfigurator() @@ -47,6 +49,9 @@ def __init__( ): super(StabilizationPreprocessingThread, self).__init__() + logger.debug( + "Pre-Processing thread is being constructed." + ) printer = timelapse_settings["settings"].profiles.current_printer() stabilization = timelapse_settings["settings"].profiles.current_stabilization() trigger = timelapse_settings["settings"].profiles.current_trigger() @@ -82,74 +87,116 @@ def __init__( self.lines_processed = 0 self.cpp_position_args = printer.get_position_args(timelapse_settings["overridable_printer_profile_settings"]) + logger.debug( + "Pre-Processing thread is constructed." + ) + def run(self): try: + logger.debug( + "Pre-Processing thread is running." + ) # perform the start callback self.start_callback() ret_val, options = self._run_stabilization() logger.info( "Received %s snapshot plans from the GcodePositionProcessor stabilization in %s seconds.", - len(ret_val[2]), ret_val[3] + len(ret_val[1]), ret_val[2] ) results = ( ret_val[0], # success - ret_val[1], # errors - ret_val[2], # snapshot_plans - ret_val[3], # seconds_elapsed - ret_val[4], # gcodes processed - ret_val[5], # lines_processed + ret_val[1], # snapshot_plans + ret_val[2], # seconds_elapsed + ret_val[3], # gcodes processed + ret_val[4], # lines_processed + ret_val[5], # snapshots_missed + ret_val[6], # quality issues + ret_val[7], # processing errors from c++ + ret_val[8], # preprocessing errors from StabilizationPreprocessingThread options, ) - except Exception as e: - logger.exception("Gcode preprocessing failed.") - self.error = "There was a problem preprocessing your gcode file. See plugin_octolapse.log for details" + logger.exception("An error occurred while running the gcode preprocessor.") + error = error_messages.get_error(["preprocessor", "preprocessor_errors", "unhandled_exception"]) results = ( False, # success - self.error, False, - [], 0, 0, - 0 + 0, + 0, + [], + [], + [error], + None ) + logger.info("Unpacking results") success = results[0] - error = results[1] - errors = [] - if error: - errors.append(error) # get snapshot plans - cpp_snapshot_plans = results[2] - if not cpp_snapshot_plans: - snapshot_plans = [] + cpp_snapshot_plans = results[1] + seconds_elapsed = results[2] + gcodes_processed = results[3] + lines_processed = results[4] + missed_snapshots = results[5] + quality_issues = self._get_quality_issues_from_cpp(results[6]) + processing_issues = self._get_processing_issues_from_cpp(results[7]) + other_errors = results[8] + + snapshot_plans = [] + if success and not cpp_snapshot_plans: success = False - error_message = "No snapshots were detected in the Gcode. This is probably either a problem with your " \ - "printer settings, an issue with the gcode file, or a bug in Octolapse. " - errors.insert(0, error_message) - else: + # see if there were any fatal processing issues + has_fatal_issues = False + for issue in processing_issues: + if issue["is_fatal"]: + has_fatal_issues = True + break + # only append the no snapshot plan error if there are no fatal processing issues and no other errors + if not has_fatal_issues and len(other_errors) == 0: + error = error_messages.get_error(["preprocessor", "preprocessor_errors", "no_snapshot_plans_returned"]) + other_errors.append(error) + elif cpp_snapshot_plans: snapshot_plans = SnapshotPlan.create_from_cpp_snapshot_plans(cpp_snapshot_plans) - seconds_elapsed = results[3] - gcodes_processed = results[4] - lines_processed = results[5] + + errors = other_errors + processing_issues self.complete_callback( - success, errors, self.is_cancelled, snapshot_plans, seconds_elapsed, gcodes_processed, lines_processed, - self.timelapse_settings, self.parsed_command + success, self.is_cancelled, snapshot_plans, seconds_elapsed, gcodes_processed, lines_processed, + missed_snapshots, quality_issues, errors, self.timelapse_settings, self.parsed_command + ) + + logger.debug( + "Pre-Processing thread is completed." ) + def _get_quality_issues_from_cpp(self, issues): + quality_issues = [] + for issue in issues: + quality_issues.append( + error_messages.get_error(["preprocessor", "cpp_quality_issues", str(issue[0])]) + ) + + return quality_issues + + def _get_processing_issues_from_cpp(self, issues): + processing_issues = [] + for issue in issues: + processing_issues.append( + error_messages.get_error(["preprocessor", "cpp_processing_errors", str(issue[0])], **issue[2]) + ) + + return processing_issues + def _create_stabilization_args(self): # and vase mode is enabled, use the layer height setting if it exists # I'm keeping this out of the c++ routine so it can be more easily changed based on slicer # changes. I might add this to the settings class. - if self.trigger_profile.layer_trigger_height == 0: - if ( - self.printer_profile.gcode_generation_settings.vase_mode and - self.printer_profile.gcode_generation_settings.layer_height - ): - height_increment = self.printer_profile.gcode_generation_settings.layer_height - else: - # use the default height increment - height_increment = self.printer_profile.minimum_layer_height + if ( + self.trigger_profile.layer_trigger_height == 0 and + self.printer_profile.gcode_generation_settings.vase_mode and + self.printer_profile.gcode_generation_settings.layer_height + ): + height_increment = self.printer_profile.gcode_generation_settings.layer_height else: height_increment = self.trigger_profile.layer_trigger_height @@ -164,7 +211,9 @@ def _create_stabilization_args(self): ), "y_stabilization_disabled": ( self.stabilization_profile.y_type == StabilizationProfile.STABILIZATION_AXIS_TYPE_DISABLED - ) + ), + "allow_snapshot_commands": self.trigger_profile.allow_smart_snapshot_commands, + "snapshot_command": self.printer_profile.snapshot_command } return stabilization_args @@ -176,47 +225,93 @@ def _run_stabilization(self): if self.printer_profile.gcode_generation_settings.vase_mode: self.cpp_position_args["minimum_layer_height"] = stabilization_args["height_increment"] trigger_type = self.trigger_profile.trigger_type + trigger_subtype = self.trigger_profile.trigger_subtype is_precalculated = ( self.trigger_profile.trigger_type in TriggerProfile.get_precalculated_trigger_types() ) - # If the current trigger is not precalculated, return an error if not is_precalculated: - self.error = "The current trigger is not a pre-calculated trigger." + # If the current trigger is not precalculated, return an error + # note that this would mean a programming error + logger.error("The current trigger is not precalculated!") + + other_error = error_messages.get_error(["preprocessor", "preprocessor_errors", 'incorrect_trigger_type']) results = ( False, # success - self.error, # errors [], # snapshot_plans 0, # seconds_elapsed 0, # gcodes_processed - 0 # lines_processed + 0, # lines_processed + 0, # missed_snapshots + [], # quality_issues + [], # processing_issues + [other_error] # other_errors ) - if trigger_type == TriggerProfile.TRIGGER_TYPE_SMART_LAYER: + elif ( + trigger_type == TriggerProfile.TRIGGER_TYPE_SMART and + trigger_subtype == TriggerProfile.LAYER_TRIGGER_TYPE + ): # run smart layer trigger smart_layer_args = { 'trigger_type': int(self.trigger_profile.smart_layer_trigger_type), - 'distance_threshold_percent': ( - self.trigger_profile.smart_layer_trigger_distance_threshold_percent / 100.0 - ), - 'speed_threshold': self.trigger_profile.smart_layer_trigger_speed_threshold, - 'snap_to_print': self.trigger_profile.smart_layer_snap_to_print + 'snap_to_print_high_quality': self.trigger_profile.smart_layer_snap_to_print_high_quality, + 'snap_to_print_smooth': self.trigger_profile.smart_layer_snap_to_print_smooth } - results = GcodePositionProcessor.GetSnapshotPlans_SmartLayer( + ret_val = list(GcodePositionProcessor.GetSnapshotPlans_SmartLayer( self.cpp_position_args, stabilization_args, smart_layer_args - ) + )) + # add the success indicator + ret_val.insert(0, True) + # add the 'other' errors (errors not related to the C++ call) + ret_val.append([]) + # set the results as the ret_val in tuple form + results = tuple(ret_val) + logger.info("Stabilization results received, returning.") + elif ( + trigger_type == TriggerProfile.TRIGGER_TYPE_SMART and + trigger_subtype == TriggerProfile.GCODE_TRIGGER_TYPE + ): + # run smart gcode trigger + # Note: there are no arguments for the smart gcode trigger currently + smart_gcode_args = { + + } + ret_val = list(GcodePositionProcessor.GetSnapshotPlans_SmartGcode( + self.cpp_position_args, + stabilization_args, + smart_gcode_args + )) + # add the success indicator + ret_val.insert(0, True) + # add the 'other' errors (errors not related to the C++ call) + ret_val.append([]) + # set the results as the ret_val in tuple form + results = tuple(ret_val) + logger.info("Stabilization results received, returning.") else: - self.error = "Can't find a preprocessor named {0}, unable to preprocess gcode.".format( - self.trigger_profile.trigger_type) + # If this is an unknown trigger type, report the error + # This could happen if there is settings corruption, manual edits, or profile repo errors + other_error = error_messages.get_error( + ["preprocessor", "preprocessor_errors", 'unknown_trigger_type'], + preprocessor_type=self.trigger_profile.trigger_type + ) results = ( False, # success - self.error, # errors [], # snapshot_plans 0, # seconds_elapsed 0, # gcodes_processed - 0 # lines_processed + 0, # lines processed + 0, # missed_snapshots + [], # quality_issues + [], # processing_issues + [other_error] ) - logger.info("Stabilization results received, returning.") + logger.error("The current precalculated trigger type is unknown.") + # We need to sort the snapshot plans by line number because they can sometimes be out of order + def sort_by_line_number(val): + return val[0] + results[1].sort(key=sort_by_line_number) return results, options def on_progress_received(self, percent_progress, seconds_elapsed, seconds_to_complete, gcodes_processed, diff --git a/octoprint_octolapse/static/css/octolapse.css b/octoprint_octolapse/static/css/octolapse.css index 97a3eb1d..65ed941d 100644 --- a/octoprint_octolapse/static/css/octolapse.css +++ b/octoprint_octolapse/static/css/octolapse.css @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -21,6 +21,11 @@ # following email address: FormerLurker@pm.me ################################################################################## */ + +.octolapse.pagination { + margin: 0px; +} + .octolapse-pnotify-help .alert { text-shadow: none; } @@ -35,6 +40,20 @@ min-height: 100px; } +.octolapse_dialog_help { + float: right; + padding-right:5px; + line-height:20px; +} + +.right { + float:right; +} + +.alert.octolapse{ + padding: 8px 14px 8px 14px; +} + .two-column .column{ /*-webkit-column-gap: 20px; -moz-column-gap: 20px; @@ -80,134 +99,6 @@ text-decoration: none; } -/* CHECKBOX SWITCH CONTROL STYLE */ -/* Remove the existing checkbox */ -.toggle-switch-fa-lg input, -.toggle-switch-fa input, -.toggle-switch-fa-sm input, -.toggle-switch-fa-xsm input { - opacity:0; - box-shadow: none; -} - -.toggle-switch-fa-lg, .toggle-switch-fa, .toggle-switch-fa-sm, .toggle-switch-fa-xsm { - position: relative; - display: inline-block; - -} -.toggle-switch-fa-lg -{ - margin: 5px; -} -/* Sizes */ -.toggle-switch-fa-lg { - width: 70px; - height: 48px; -} - -.toggle-switch-fa { - width: 60px; - height: 33px; - margin-top: 2px; - margin-bottom: 0px; -} - -.toggle-switch-fa-sm -{ - width:35px; - height:20px; -} - -.toggle-switch-fa-xsm -{ - width:30px; - height:19px; -} - - -/* the slider part of the switch */ -.fa-slider { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: lightgray; /*disabled color*/ - cursor: pointer; - border-radius: 10px; - -webkit-transition: 0.5s; - transition: 0.5s; -} - - -/* The round part of the slider */ -.fa-slider i { - position: absolute; - content: ""; - left: 7px; - bottom: 3px; - font-size:24px; - -webkit-transition: .4s; - transition: .4s; -} - -.toggle-switch-fa-sm .fa-slider i -{ - left: 5px; - bottom: 4px; - font-size: 12px; -} -.toggle-switch-fa-xsm .fa-slider i -{ - left: 5px; - bottom: 4px; - font-size: 12px; -} -.toggle-switch-fa-lg .fa-slider i -{ - left: 10px; - bottom: 10px; - font-size: 28px; -} - -input[type="checkbox"]:checked + label .fa-slider { - background-color: #000000; -} -input[type="checkbox"] + label .fa-slider -{ - color: white; -} -input[type="checkbox"]:focus + label .fa-slider { - box-shadow: 2px 2px 2px rgba(129,182,224,0.867) -} -input[type="checkbox"]:checked + label .fa-slider -{ - color: greenyellow; -} - -input[type="checkbox"]:checked + label .fa-slider i{ - -webkit-transform: translateX(24px); - -ms-transform: translateX(24px); - transform: translateX(24px); -} - -.toggle-switch-fa-lg input[type="checkbox"]:checked + label .fa-slider i{ - -webkit-transform: translateX(26px); - -ms-transform: translateX(26px); - transform: translateX(26px); -} - -.toggle-switch-fa-sm input[type="checkbox"]:checked + label .fa-slider i{ - -webkit-transform: translateX(15px); - -ms-transform: translateX(15px); - transform: translateX(15px); -} - -.toggle-switch-fa-xsm input[type="checkbox"]:checked + label .fa-slider i{ - -webkit-transform: translateX(10px); - -ms-transform: translateX(10px); - transform: translateX(10px); -} /* ol-button and ol-hover-button*/ .ol-hover-button a .fa-stack, a.ol-button{ @@ -219,9 +110,6 @@ a.ol-button .fa-square{ color: black; } -.ol-hover-button a:not(.disable):hover .fa-square, .snapshot_refresh_container a:not(.disable):active .fa-square { - /*color: lightgray;*/ -} .ol-hover-button a:not(.disable):hover .fa-stack:nth-child(1){ color: green; } @@ -235,9 +123,7 @@ a.ol-button:not(.disable):active i:nth-child(2){ } .ol-hover-button a:not(.disable):hover:not(:active) i, -a.ol-button:hover:not(:active) span.fs-stack:nth-child(2), -.fa-slider:hover:not(:active) -/*,.ol-hover-button .fa-square*/ +a.ol-button:hover:not(:active) span.fs-stack:nth-child(2) { -webkit-transition: color 0.250s 0s ease-in; -moz-transition: color 0.250s 0s ease-in; @@ -252,10 +138,12 @@ a.ol-button:hover:not(:active) span.fs-stack:nth-child(2), .ol-button i.fa-stop:hover { color:red; } -.ol-button i.fa-gear:hover, .fa-slider:hover i{ +.ol-button i.fa-gear:hover{ color:green; } - +a.unclickable:hover { + cursor: default; +} div#octolapse-current-camera{ margin-left:auto; margin-right:auto; @@ -271,6 +159,20 @@ div#octolapse-current-camera{ background-color: #b94a48!important; color: #cccccc!important; } + +.octolapse button.btn-danger.btn { + color: #fff; + text-shadow: 0 -1px 0 rgba(0,0,0,.25); + background-color: #da4f49; +} +.octolapse button.btn-danger.btn:hover { + color: #fff; + background-color: #bd362f; +} + +.octolapse .info-panel-icon { + color: black; +} .text-left{ text-align:left; } @@ -282,7 +184,13 @@ div#octolapse-current-camera{ text-align: center; width: 100%; height:400px; + position: unset; + top: unset; + left: unset; + transform: unset; + } + .octolapse-pnotify-container .alert { text-shadow:none; @@ -300,6 +208,10 @@ div#octolapse-current-camera{ display: table-cell; vertical-align: middle; font-size: large; + position: unset; + top: unset; + left: unset; + transform: unset; } .no-min-height{ @@ -308,7 +220,7 @@ div#octolapse-current-camera{ #octolapse_add_edit_profile_dialog .input-xlg{ width: 345px; } -#octolapse_add_edit_profile_dialog .input-xxl{ +.octolapse .input-xxl, #octolapse_add_edit_profile_dialog .input-xxl{ width: 430px; } #octolapse_add_edit_profile_dialog .control-group{ @@ -318,6 +230,9 @@ a.disabled { color: #ccc; cursor: default; } +.ui-pnotify.octolapse{ + width:375px !important; +} .ui-pnotify.octolapse .ui-pnotify-text{ word-wrap: break-word; @@ -330,38 +245,137 @@ a.disabled { } span.octolapse-icon-text { font-family: Helvetica Neue,Helvetica,Arial,sans-serif; - position: relative; - top: -3px; - font-size: 16px; + font-size: 26px; font-weight: 600; - color: greenyellow; + color: rgba(173,255,47,1); text-shadow: 1px 0px black, -1px 0px black, 0px 1px black, 0px -1px black; + vertical-align: middle; + position: relative; } -table.octolapse-profiles td { +table.octolapse-table-list { + border-collapse: collapse; + table-layout: fixed; + max-width: 100%; +} + +table.octolapse-table-list td { font-size: small } -table.octolapse-profiles td.profile_name, -table.octolapse-profiles td.profile_description, -table.octolapse-profiles td.profile_action { +table.octolapse-table-list td{ height: 20px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -table.octolapse-profiles td.profile_name { - max-width: 180px; - min-width: 180px; +/* Octolapse Profile Table Column Sizes */ +table.octolapse-table-list td.profile-name, table.octolapse-table-list th.profile-name{ + width:30%; +} +table.octolapse-table-list td.profile-description, table.octolapse-table-list th.profile-description { + width:50%; +} +table.octolapse-table-list td.profile-action, table.octolapse-table-list th.profile-action { + width:20%; +} + +/* Octolapse Unfinished Renderings Table Column Sizes */ +table.octolapse-table-list td.rendering-print-name, table.octolapse-table-list th.rendering-print-name{ + width:21%; +} +table.octolapse-table-list td.rendering-print-end-state, table.octolapse-table-list th.rendering-print-end-state{ + width:10%; +} +table.octolapse-table-list td.rendering-size, table.octolapse-table-list th.rendering-size{ + width:8%; +} +table.octolapse-table-list td.rendering-date, table.octolapse-table-list th.rendering-date{ + width:9%; +} +table.octolapse-table-list td.rendering-camera-name, table.octolapse-table-list th.rendering-camera-name{ + width:12%; +} +table.octolapse-table-list td.rendering-name, table.octolapse-table-list th.rendering-name{ + width:12%; +} +table.octolapse-table-list td.rendering-action, table.octolapse-table-list th.rendering-action{ + width:12%; +} +table.octolapse-table-list td.rendering-progress, table.octolapse-table-list th.rendering-progress{ + width:16%; +} +td.rendering-progress .progress { + position: relative; + background: #888888; + background-image: none; +} +td.rendering-progress .progress .progress-text{ + position: absolute; + width:100%; + left:0px; +} + + +.octolapse-list .list-item-selection { + width:12px; +} +.octolapse-list .list-item-selection-dropdown { + width:36px; + text-align: center; +} +.octolapse-list .list-item-selection input { + margin-top: -2px; +} +.octolapse-list .list-item-selection-header-dropdown { + width:36px; + overflow: visible; +} +.octolapse-list .file-browser-name { + width:69%; +} + +.octolapse-list .file-browser-date { + width:16%; +} + +.octolapse-list .file-browser-size { + width:8%; } -table.octolapse-profiles td.profile_description { - max-width: 300px; - min-width: 300px; + +.octolapse-list .file-browser-action { + width:7%; + text-align: center; } -table.octolapse-profiles td.profile_action { - max-width: 120px; - min-width: 120px; + +.octolapse-list .file-browser-snapshot-archive-action { + width:12%; + text-align: center; +} + +.octolapse-camera-list-item +{ + margin-bottom:20px; +} +.octolapse-table-list tr.disabled { + filter: brightness(75%); +} + +.row-fluid.octolapse-table-list-row .span1, +.row-fluid.octolapse-table-list-row .span2, +.row-fluid.octolapse-table-list-row .span3, +.row-fluid.octolapse-table-list-row .span4, +.row-fluid.octolapse-table-list-row .span5, +.row-fluid.octolapse-table-list-row .span6, +.row-fluid.octolapse-table-list-row .span7, +.row-fluid.octolapse-table-list-row .span8, +.row-fluid.octolapse-table-list-row .span9, +.row-fluid.octolapse-table-list-row .span10, +.row-fluid.octolapse-table-list-row .span11, +.row-fluid.octolapse-table-list-row .span12 +{ + margin-left:0px; } .octolapse_dropzone @@ -398,10 +412,15 @@ div.webcam_settings_preview img{ div.webcam_settings_preview .loading, div.webcam_settings_preview .error{ display:table-cell; vertical-align:middle; + position: unset; + top: unset; + left: unset; + transform: unset; + font-size: unset; } - +div.octolapse_dialog, #octolapse_add_edit_profile_dialog.modal, #octolapse_edit_settings_main_dialog.modal, #octolapse_webcam_settings_dialog.modal @@ -432,10 +451,12 @@ form.octolapse_form .modal-body { .ol-compact-row.row-fluid [class*="span"] { min-height: unset; } - +/* .octolapse_add_edit_dialog .row-fluid [class*="span"] { text-align: left; } +*/ + .octolapse .text-center { text-align: center; @@ -467,11 +488,27 @@ form.octolapse_form .modal-body { color: white; } +.icon_active_color { + color: greenyellow; +} +.icon_idle_color { + color: lightgray; +} +.icon_warning_color { + color: orange; +} + +.icon_error_color { + color: red; +} +.clear_color { + color: unset; +} .position-state-icon, .extruder-icon { color: greenyellow; } -.bg-paused, .bg-not-homed, .bg-lightgray { +.bg-paused, .bg-unknown-position, .bg-lightgray { color: lightgray; } @@ -480,6 +517,14 @@ form.octolapse_form .modal-body { margin: 10px; } +.margin-bottom-small { + margin-bottom:10px; +} + +.padded-small { + padding: 10px; +} + .ol-text-status-title { font-size: medium; } @@ -575,13 +620,6 @@ form.octolapse_form .modal-body { } -table.octolapse-profiles { - border-collapse: collapse; - table-layout: auto; - max-width: 100%; -} - - .button-container.top-right { top: 10px; right: 0; @@ -594,12 +632,6 @@ table.octolapse-profiles { left: 50%; margin-left: -50px; } -.snapshot_refresh_container { - z-index: 999; - position: absolute; - top: 5px; - right: 5px; -} .latest-snapshot-modal .button-container { z-index: 1000; @@ -608,15 +640,34 @@ table.octolapse-profiles { #octolapse_snapshot_thumbnail_container{ position:relative; } -.octolapse-snapshot-text { + +.octolapse-camera-state-text { position: absolute; - top: 15px; - left: 50%; - transform: translate(-50%, 0%); - color: white; - text-shadow: 0px 0px 2px #000000; + display:table; + height:282px; + width:590px; + z-index:257; +} +.octolapse-camera-state-text div.octolapse-camera-state-text-wrapper { + display: table-cell; + vertical-align: middle; + width:100%; +} +.octolapse-camera-state-text div.octolapse-camera-state-text-wrapper div{ + display: table-cell; + vertical-align: middle; font-size:28px; + line-height: 1em; + background-color: rgba(255,255,255,1); + padding-top: 15px; + width:590px; } +.octolapse-camera-state-text p{ + color: black; + margin: 0; + padding: 0 15px 15px 15px; +} + .snapshot-container { position: relative; @@ -651,7 +702,7 @@ div#octolapse_snapshot_image_container .snapshot-container { width:100%; height:100%; } -.latest-snapshot +.latest-snapshot img { z-index:256; } @@ -717,13 +768,12 @@ input.error { font-weight: 400; } -#octolapse_tab i.fa, #octolapse_tab span.fa-slider { +#octolapse_tab i.fa { border: 1px solid #000000; } #octolapse_tab i.fa, #octolapse_tab span.fa, -#octolapse_tab i.fa, #octolapse_tab span.fa-slider i{ - text-shadow: 1px 0px black, -1px 0px black, 0px 1px black, 0px -1px black; +#octolapse_tab i.fa{ border: none; } a.ol-button i { @@ -760,8 +810,84 @@ fieldset.octolapse legend .full-width { width:100%; } +.octolapse-secondary-gray { + opacity: 0.2; +} +.octolapse-secondary-gray-background { + background-color: rgba(0,0,0,0.05); +} + +.select-with-edit { + display:flex; + align-items: center; + margin-bottom: 10px; +} +.select-with-edit select{ + flex:1; + margin-bottom: 0; + z-index:257; +} +.select-with-edit a{ + padding: 0 0 0 6px; + +} + +.fa.outline +{ + text-shadow: -1px 0, 0 1px, 1px 0, 0 -1px; +} + +.octolapse-snapshot-button-overlay +{ + width:100%; + height:100%; + position: relative; +} +.octolapse-snapshot-button-overlay > a, .octolapse-snapshot-button-overlay > span +{ + opacity: 1; + position: absolute; + z-index:257; +} + +.octolapse-snapshot-button-overlay > a, .octolapse-snapshot-button-overlay > span > a +{ + + -webkit-transition: opacity 0.25s; + -moz-transition: opacity 0.25s; + -o-transition: opacity 0.25s; + transition: opacity 0.25s; +} +.octolapse-snapshot-button-overlay .fade-out +{ + opacity: 0; +} + +.octolapse-snapshot-button-overlay .snapshot-indicator +{ + left:50%; + top:50%; +} +.octolapse-snapshot-button-overlay span.play-button a > i, .octolapse-snapshot-button-overlay .snapshot-indicator > i { + margin: -42px 0 0 -42px; +} +.octolapse-snapshot-button-overlay span.play-button{ + left:50%; + top:50%; +} +.snapshot-container:hover .octolapse-snapshot-button-overlay a.play{ + opacity:1; +} +.octolapse-snapshot-button-overlay a.octolapse-fullscreen{ + right:0; + bottom:0; +} +.octolapse-snapshot-button-overlay a.refresh{ + right:0; +} + /* The slider itself */ -.slider { +.octolapse-slider { width: 100%; /* Full-width */ height: 25px; /* Specified height */ opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */ @@ -770,7 +896,7 @@ fieldset.octolapse legend } /* Mouse-over effects */ -.slider:hover { +.octolapse-slider:hover { opacity: 1; /* Fully shown on mouse-over */ } @@ -1233,3 +1359,21 @@ fieldset.octolapse legend width: 10em; overflow-wrap: break-word; } + +div.rendering-overlay-preview img { + max-height:480px; + max-width:890px; +} +div.rendering-overlay-preview { + position:relative; + width:890px; + height:480px; +} + +.octolapse .valign +{ + position:absolute; + top: 50%; + transform: translateY(-50%); + width:100%; +} diff --git a/octoprint_octolapse/static/docs/help/dialog.renderings.in_process.md b/octoprint_octolapse/static/docs/help/dialog.renderings.in_process.md new file mode 100644 index 00000000..53c4cf3f --- /dev/null +++ b/octoprint_octolapse/static/docs/help/dialog.renderings.in_process.md @@ -0,0 +1,22 @@ +This dialog shows the progress of any renderings that are in the rendering queue. Note that even if rendering is disabled, they will be added to the rendering queue so that a snapshot archive file (.zip) can be created. + +### Where do I find the finished timelapses and saved snapshot files (.zip)? + +All timelapse videos and saved snapshots can be found within the Octolapse tab by clicking the **Videos and Images** button, which opens a popup window which will display all of your files. Please note that snapshots are only saved if **Archive Snapshots After Rendering** is enabled, which can be found in your rendering profile under the **Files and Performance** section. + +### What do the progress messages mean? + +Timelapses will be rendered in the order they are sent, and go through following phases: + +1. **Pending** - These timelapses have been queued, but have not yet started the rendering process. +2. **Script - Before** - If your camera has any **Before Render Scripts** configured, Octolapse will run these. Unfortunately there is no way currently to report progress from these scripts, so Octolapse will not show a progress bar while in this phase. I'm considering adding this capability in a future update, though it will require the script to send progress messages to the output screen. +3. **Preparing** - Before rendering starts, a few steps must be completed. First, Octolapse will delete any existing temporary rendering files that might exist. Next, the temporary rendering directory will be created. Finally, all images with valid extensions will be copied to a temporary rendering directory and converted to JPEGs if they do not appear to be in JPEG format. Copying the images does take extra disk space (as opposed to simply moving the images), but allows Octolapse to gracefully recover if there are any errors during any part of the rendering process. +4. **Adding Overlays** - If you have enabled text overlays within your rendering settings, they will be added during this phase. This phase could take a while to complete depending on the resolution of your snapshots. +5. **Rename Images** - Octolapse must rename all image files so that they are sequential. Images names may not be sequential at this point if there were any errors while acquiring snapshots. This process should be very fast. +6. **Pre/Post Roll** - Depending on your rendering settings, pre-roll and post roll frames may be made by copying the first and last images the appropriate number of times in order to achieve the specified pre/post-roll length. This phase can take a while depending on the framerate and the size of your image files. Please note that your images will need to be renamed once again if any pre-roll frames are added, but this usually doesn't take much time at all compared to copying the images. +7. **Rendering** - Thanks to a recent OctoPrint update, I was able to add a rendering progress indicator based on the code from a pull request! This is usually the most time consuming phase, and can take quite a bit of time and CPU power to complete. If Octoprint is running on multi-threaded hardware, you can reduce the rendering time by increasing the number of rendering threads within your rendering profile. Check the **Rendering Thread Count** help within your rendering profile for details. +8. **Archiving** - Depending on your settings, an archive (.zip) file may be created. This can take a while to complete, especially if there are a lot of high resolution images in your timelapse. Note that the snapshots are the original images, unmodified by the rendering process, and will contain metadata and settings that can be used to re-create the rendering. +9. **Post Render Script** - If your camera has any **After Render Scripts** configured, Octolapse will run these. Just like the **Script - Before** phase, there is no progress bar shown. +10. **Cleanup** - At this point all snapshots in the temporary directory are deleted along with the original timelapse images. Progress is displayed during this phase. + + diff --git a/octoprint_octolapse/static/docs/help/dialog.renderings.unfinished.md b/octoprint_octolapse/static/docs/help/dialog.renderings.unfinished.md new file mode 100644 index 00000000..bb5355f5 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/dialog.renderings.unfinished.md @@ -0,0 +1,23 @@ +This dialog shows any renderings that have not finished, or have been added from the timelapse archive dialog. + +There are three main things you can do in this page: + +1. Render unfinished timelapses +2. Delete unfinished rendering files +3. Download images in a zip archive from any unfinished timelapse + +## Rendering + +Rendering unfinished timelapses is as simple as clicking the icon. This will immediately start rendering the timelapse with the current settings. + +Octolapse tries to render your timelapse using the camera and rendering settings that you originally used to generate the snapshots. In some cases Octolapse may not be able to recover these settings, for example if the snapshots were taken prior to Octolapse v0.4, or if you manually uploaded images via the **Saved Snapshots** tab within the **Videos and Images** dialog. In this case, Octolapse will attempt to render the files using your current rendering settings and the default camera settings. If you wish to use other settings, you can select them in the dropdown list prior to rendering the timelapse. + +You can also select multiple rows and render all selected items by clicking the **Render/Delete** button and selecting **Render All Selected**. If you plan to render a lot of videos, please keep in mind that this could take a really long time. + +## Delete + +If you want to delete any unfinished renderings, just click the icon. You can also delete all selected items by clicking the **Render/Delete** button and selecting **Delete All Selected**. Please note that this will delete all of the snapshots permanently. + +## Download + +Click on the icon to zip and download snapshot files. Please note that this operation may take a while if there are a lot of images in the timelapse. Also, it will require approximately as much free disk space to archive the snapshots as the images themselves take up, so be sure you have enough free space before downloading images! diff --git a/octoprint_octolapse/static/docs/help/dialog.timelapse_files.snapshot_archive.import.md b/octoprint_octolapse/static/docs/help/dialog.timelapse_files.snapshot_archive.import.md new file mode 100644 index 00000000..82c4e18f --- /dev/null +++ b/octoprint_octolapse/static/docs/help/dialog.timelapse_files.snapshot_archive.import.md @@ -0,0 +1,20 @@ +Octolapse supports importing a zip archive containing images. These files can come from the download option within the **Saved Snapshot** tab, or you can create them yourself. Currently, Octolapse only supports images with a ```jpg``` extension. After you import the archive, Octolapse will add it to the **Saved Snapshots** tab within the **Videos and Images** dialog. From there you can download, delete, or add the archive to the list of unfinished renderings. + +Archive file generation can be enabled in two ways: + +1. If rendering is disabled, an archive will always be produced. +2. If rendering is enabled, Octolapse will produce an archive if the **Archive Snapshots After Rendering** setting is enabled within your rendering settings. It is disabled by default, since it takes a lot of space. + +Octolapse supports two kinds of file structures: + +1. Everything in the root of the zip archive. This means all images are located right inside of the archive, and are not in any subfolders. All subfolders will be ignored. +2. All images contained within a **job** and **camera** folder. These folders have a special naming scheme that mimics how Octolapse stores timelapse images within the temporary folder. Both the Job and Camera folder are GUIDs. The following would be a valid job/camera folder structure: ```48bb64c2-e878-4b1b-87b8-a06d1b4fc181\354def78-9eea-409a-ad23-ee966dfff4ba```. + +Octolapse will also search for various settings files, and will include settings if you choose to render the snapshots within the archive. These settings files are as follows: +1. timelapse_info.json - This file contains information about the print file name, the start and end time of the print, and the GUID of the original print job. This file must be located within the root, or within the job folder. +2. camera_info.json - This file contains information about the total number of snapshots attempts, failures and successes. This file must be located within the root, or within the camera folder. +3. metadata.csv - This file contains information about the snapshot image file name, the snapshot number, and the time the snapshot was taken. This is used for adding text overlays to the rendered video. This file must be located within the root, or within the camera folder. +4. camera_settings.json - This is the settings file for the camera profile that was used to take the snapshots. Camera settings can affect what scripts run during rendering, and the final rendered video name if the {CAMERANAME} token is used in the rendering **Filename Template**. This file must be located within the root, or within the camera folder. +5. rendering_settings.json - This is the settings file for the rendering profile that was used to take the snapshots. This file will be used to create the rendering if you choose to render a timelapse from the archive. This file must be located within the root, or within the camera folder. + +Note: You can change the camera and rendering settings before you render any videos from a snapshot archive file. diff --git a/octoprint_octolapse/static/docs/help/dialog.timelapse_files.snapshot_archive.tab.md b/octoprint_octolapse/static/docs/help/dialog.timelapse_files.snapshot_archive.tab.md new file mode 100644 index 00000000..cdd53c25 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/dialog.timelapse_files.snapshot_archive.tab.md @@ -0,0 +1,22 @@ +Octolapse can save a .zip file containing all of your snapshots and settings. Archiving can be enabled in two ways: + +1. If rendering is disabled, an archive will always be produced. +2. If rendering is enabled, Octolapse will produce an archive if the **Files and Performance** heading of your **Rendering** profile. It is disabled by default, since it takes a lot of space. + +Archives will appear within this tab after rendering has completed successfully. You can download, delete, or render any archives, or you can import new archive files (see the **Import Snapshot Archive (.zip)** help icon for more details about importing archives). + +### Downloading Archives +Simply click the icon to download an archive. + +### Deleting Archives +You can delete an archive by clicking the icon, or by selecting one or more archives and clicking the **Delete** button above the selection column. + +### Add Archives to Unfinished Renderings +By clicking the icon you can add any archive to the unfinished rendering list. You can access this list by closing the **Videos and Images** dialog, and clicking on the red **Unfinished Renderings** button. From there you will be able to change settings and render a timelapse video from your archive. + +Note: Adding archives to the unfinished rendering list will take up at least as much disk space as the size of the archive. Make sure you have plenty of free disk space before adding archives to the unfinished renderings and before initiating video rendering. + +### Importing Archives +Octolapse supports importing zip files. See the help icon next to the **Import Snapshot Archive** control for more details. + + diff --git a/octoprint_octolapse/static/docs/help/dialog.timelapse_files.timelapse.tab.md b/octoprint_octolapse/static/docs/help/dialog.timelapse_files.timelapse.tab.md new file mode 100644 index 00000000..0c4bfd4b --- /dev/null +++ b/octoprint_octolapse/static/docs/help/dialog.timelapse_files.timelapse.tab.md @@ -0,0 +1,7 @@ +Here you can browse all of your timelapse videos. The video directory defaults to the Octoprint timelapse directory, but can be changed within the Octolapse main settings. Note that by default both Octolapse and OctoPrint timelapses will be visible (see **Main Settings** for information about the timelapse folder location). + +### Download +To download a timelapse, simply click on the . The timelapse will be downloaded by your browser. + +### Delete +You can either delete a timelapse by clicking the , or by selecting one or more rows (via the checkbox or selection drop down), and clicking the **Delete** button. diff --git a/octoprint_octolapse/static/docs/help/error_help_error_not_found.md b/octoprint_octolapse/static/docs/help/error_help_error_not_found.md new file mode 100644 index 00000000..ba91d4a4 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_error_not_found.md @@ -0,0 +1,4 @@ +This is an Octolapse bug caused by an incorrectly supplied error key. Please report the original error message, including the keys in the popup, to the octolapse github repository. + +#### Report an Issue +If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_automatic_slicer_no_settings_found.md b/octoprint_octolapse/static/docs/help/error_help_init_automatic_slicer_no_settings_found.md new file mode 100644 index 00000000..e569169a --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_automatic_slicer_no_settings_found.md @@ -0,0 +1,35 @@ +You are using **Automatic Slicer Settings**, and Octolapse was unable to detect any slicer settings within your gcode file. This usually means one of the following things: + +1. You are using Cura, but have not added the settings script to your start gcode. +2. You are using a printer with multiple extuders or a multi-material printer with a shared nozzle. +3. You are using an unsupported slicer type. Automatic Settings are only supported currently when using Cura, PrusaSlicer/Slic3r/Slic3rPE, and Simplify 3D. +4. It could also mean that your settings file was manually edited, and some settings were removed, or that the gcode file is corrupt. + +See the sections below for more detailed help + +#### I'm using Cura +Cura does not output slicer settings by default. See this guide for enabling automatic slicer settings when using Cura. If you continue to have a problem, see the [Report an Issue](#report-an-issue) section below. + +#### I'm using a multi-material or multi-extruder printer +Support for multi-extruder and multi-material printers is beta. Do not use Octolapse on these printers unless you are comfortable using a beta feature. There are likely still some issues extracting settings, and could be other undiscovered issues. Please use caution, as bugs in the software my exist and cause damage and/or failed prints. + +I have tested several multi-material prints on my Prusa MMU2, but have NOT tried a multi-extruder system. If you are brave and would like to test this, please report your findings! + +Please report any issues with multi-material/multi-extruder printers by looking at the section below titled [Report an Issue](#report-an-issue). + +#### I'm using Simplify 3D +Octolapse should work out of the box for these Simplify 3d. Please see the [Report an Issue](#report-an-issue) section below for assistance. + +#### I'm using another slicer +You are using a slicer that is not supported by Octolapse. See the [Use Manual Slicer Settings](#use-manual-slicer-settings) section below. + +If you would like for your slicer to work with **Automatic Slicer Configuration** feature, consider creating a feature request in the Octolapse github repository. Make sure you read the wiki page explaining how to create a new issue. Also, be sure to include some sample gcode, the exact slicer version, and a link to the slicer's homepage if possible. + +#### Reslice your gocde file +If none of the above suggestions has worked, try re-slicing your gcode file. It's possible that some corruption has occurred. Next, upload the gocde to Octoprint, and try to print with the resliced gcode file + +#### Use Manual Slicer Settings +If Octolapse can't extract your slicer settings from your gcode file, you instead provide your slicer settings to Octolapse directly. First, open up your Octoprint printer profile. Next, select your slicer type from the **Slicer Type** dropdown box. If your slicer type dosen't appear in the list, choose **Other Slicer**. You will then need to copy your slicer settings **EXACTLY** from your slicer to Octolapse. If you are using the **Other Slicer** type, be very careful that you select the correct **Speed Display Units** (either mm/min or mm/sec) when entering any slicer speeds. Any mistakes made while copying your slicer settings can reduce your print quality, and may even prevent Octolapse from working entirely. + +#### Report an Issue +This feature is still experimental, and has a few known issues and limitations. If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. When you submit your issue, be sure to include your gcode file, and the exact slicer version you used to create your gcode file. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_automatic_slicer_settings_missing.md b/octoprint_octolapse/static/docs/help/error_help_init_automatic_slicer_settings_missing.md new file mode 100644 index 00000000..de4629f2 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_automatic_slicer_settings_missing.md @@ -0,0 +1,35 @@ +You are using **Automatic Slicer Settings**, and Octolapse was unable to detect some slicer settings within your gcode file. This usually means one of the following things: + +1. You are using Cura, but have not added or have added the incorrect settings script to your start gcode. +2. You are using a printer with multiple extuders or a multi-material printer with a shared nozzle. +3. You are using an unsupported slicer type. Automatic Settings are only supported currently when using Cura, PrusaSlicer/Slic3r/Slic3rPE, and Simplify 3D. +4. It could also mean that your settings file was manually edited, and some settings were removed, or that the gcode file is corrupt. + +See the sections below for more detailed help + +#### I'm using Cura +Cura does not output slicer settings by default. See this guide for enabling automatic slicer settings when using Cura. If you continue to have a problem, see the [Report an Issue](#report-an-issue) section below. + +#### I'm using a multi-material or multi-extruder printer +Support for multi-extruder and multi-material printers is beta. Do not use Octolapse on these printers unless you are comfortable using a beta feature. There are likely still some issues extracting settings, and could be other undiscovered issues. Please use caution, as bugs in the software my exist and cause damage and/or failed prints. + +I have tested several multi-material prints on my Prusa MMU2, but have NOT tried a multi-extruder system. If you are brave and would like to test this, please report your findings! + +Please report any issues with multi-material/multi-extruder printers by looking at the section below titled [Report an Issue](#report-an-issue). + +#### I'm using Simplify 3D +Octolapse should work out of the box for these Simplify 3d. Please see the [Report an Issue](#report-an-issue) section below for assistance. + +#### I'm using another slicer +You are using a slicer that is not supported by Octolapse. See the [Use Manual Slicer Settings](#use-manual-slicer-settings) section below. + +If you would like for your slicer to work with **Automatic Slicer Configuration** feature, consider creating a feature request in the Octolapse github repository. Make sure you read the wiki page explaining how to create a new issue. Also, be sure to include some sample gcode, the exact slicer version, and a link to the slicer's homepage if possible. + +#### Reslice your gocde file +If none of the above suggestions has worked, try re-slicing your gcode file. It's possible that some corruption has occurred. Next, upload the gocde to Octoprint, and try to print with the resliced gcode file + +#### Use Manual Slicer Settings +If Octolapse can't extract your slicer settings from your gcode file, you instead provide your slicer settings to Octolapse directly. First, open up your Octoprint printer profile. Next, select your slicer type from the **Slicer Type** dropdown box. If your slicer type dosen't appear in the list, choose **Other Slicer**. You will then need to copy your slicer settings **EXACTLY** from your slicer to Octolapse. If you are using the **Other Slicer** type, be very careful that you select the correct **Speed Display Units** (either mm/min or mm/sec) when entering any slicer speeds. Any mistakes made while copying your slicer settings can reduce your print quality, and may even prevent Octolapse from working entirely. + +#### Report an Issue +This feature is still experimental, and has a few known issues and limitations. If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. When you submit your issue, be sure to include your gcode file, and the exact slicer version you used to create your gcode file. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_before_print_start_camera_script_apply_failed.md b/octoprint_octolapse/static/docs/help/error_help_init_before_print_start_camera_script_apply_failed.md new file mode 100644 index 00000000..1baadfb5 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_before_print_start_camera_script_apply_failed.md @@ -0,0 +1,12 @@ +At least one camera profile has a *Before Print Start Script*. There were errors running at least one of these scripts. + +### How to Debug + +One foolproof way to solve this issue is to remove the *Before Print Start Script*. This won't solve your scripting problem really, but it will allow you to print without disabling the camera, or Octolapse. + +I cannot advise you on how to exactly solve all scripting issues since you are most likely using a custom script of some kind. However, google will probably be your friend while you try to debug your custom script. I recommend getting your script to work from the command prompt before trying to debug within Octolapse. If it works from the console, but not within Octolapse, I recommend the following debugging steps: + +1. Change your *Logging* profile to *Log Everything*. +2. Edit the *Log Everything* logging profile and click *Clear Log*, which will make the debugging process easier by removing extra log entries. +3. Edit your camera profile and click the test button next to the *Before Print Start Script*. You may see an error popup after clicking test. +4. Edit the *Log Everything* logging profile and click *Download Log*. Review the log to see detailed error messages and console output for your script. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_camera_init_test_failed.md b/octoprint_octolapse/static/docs/help/error_help_init_camera_init_test_failed.md new file mode 100644 index 00000000..8e86584f --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_camera_init_test_failed.md @@ -0,0 +1 @@ +Octolapse was unable to connect to your web camera. Make sure your camera is reachable by opening up your camera profile and clicking the **Test** button under the **Snapshot Address Template**. If the test fails, click on the icon to the right of the the **Snapshot Address Template** setting for additional information. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_camera_settings_apply_failed.md b/octoprint_octolapse/static/docs/help/error_help_init_camera_settings_apply_failed.md new file mode 100644 index 00000000..e523b66d --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_camera_settings_apply_failed.md @@ -0,0 +1,15 @@ +Octolapse was unable to apply your custom camera preferences. Custom image preferences allow you to control things like contrast, focus, zoom, exposure, and other controls depending on the type of camera you are using. The exact solution depends on the type of camera you are using. + +**Important Note**: Custom image preferences are only supported when streaming with **mjpgstreamer** using a **UVC driver**. If you are using some other streaming server, or a different driver, custom image preferences will not work yet. + +#### I'm Using a Raspberry Pi Camera + +If you've received this error, it's likely that Octolapse can connect to your camera, but some additional steps might be required in order to apply custom image preferences. [See this detailed guide](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Configuring-a-Raspberry-Pi-Camera) for step by step instructions for enabling custom camera preferences + +#### I'm using a USB camera + +If you've received this error, it's likely that Octolapse can connect to your camera, but some additional steps might be required in order to apply custom image preferences. [This guide](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Enabling-Camera-Controls) explains how to enable access to mjpgstreamer's control.htm page, which is used to control the camera settings. + +#### I'm Stuck, and Just Want to Print + +If all else fails, open your Camera profile. Find the **Custom Image Preferences** section and uncheck **Apply Preferences Before Print Start**. That will prevent Octolapse from applying any custom preferences before you print starts, which will bypass this error. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_cant_print_from_sd.md b/octoprint_octolapse/static/docs/help/error_help_init_cant_print_from_sd.md new file mode 100644 index 00000000..025d799a --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_cant_print_from_sd.md @@ -0,0 +1,3 @@ +Octolapse cannot be used when printing from your 3D printer's built-in memory or SD card. Octolapse must be able to access the GCode stream, which can only be done when printing locally via Octoprint. Try uploading your gcode to OctoPrint, and print the file locally instead. + +If you are confused which files are being stored on your printer, and which ones are being stored by OctoPrint, find the OctoPrint file manager (left hand side), and click the wrench/spanner icon above the **search** box. Then select **Only show files stored locally**. This will remove any gcode files stored on your printer's SD card from the file list. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_directory_test_failed.md b/octoprint_octolapse/static/docs/help/error_help_init_directory_test_failed.md new file mode 100644 index 00000000..7f9b8709 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_directory_test_failed.md @@ -0,0 +1,4 @@ +One of the directories octolapse needs to capture snapshots and render timelapses failed testing. Octolapse must have permission to read from, write to, and create directories within all three of the rquired folders. + +### Steps to Resolve +Open the Octolapse **main settings** by navigating to the Octolapse tab and clicking the gear icon. Find the **Folders** section and click the **Test** button next to each folder to see which folder is causing this problem. If you have issues, click the help icon (a question mark) next to the folder setting for more information. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_ffmpeg_not_found_at_path.md b/octoprint_octolapse/static/docs/help/error_help_init_ffmpeg_not_found_at_path.md new file mode 100644 index 00000000..2b6f4f9f --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_ffmpeg_not_found_at_path.md @@ -0,0 +1,3 @@ +Octolapse was not able to find ffmpeg at the path set in OctoPrint's settings. In order to render videos, Octolapse must be able to find ffmpeg or avconv. It must be installed on your machine manually if you are using Windows or MacOS. If you are using OctoPi, ffmpeg/avconv is already installed and configured. If you installed OctoPrint manually, you might need to install ffmpeg or avconv manually. + + The location of ffmpeg/avconv is stored within Octoprint's settings. You can edit the location by opening OctoPrint's settings (the wrench/spanner icon at the top of the OctoPrint webpage), and selecting **Webcam & Timelapse** under the **FEATURES** heading in the menu on the right hand side of the settings popup. Then scroll down to find the **Timelapse Recordings** settings, and edit the **Path to FFMPEG**. Click the **Test** button to verify that ffmpeg/avconv exists at the specified location. Be sure to click 'Save' when you are finished editing this setting. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_ffmpeg_path_not_set.md b/octoprint_octolapse/static/docs/help/error_help_init_ffmpeg_path_not_set.md new file mode 100644 index 00000000..67f4d556 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_ffmpeg_path_not_set.md @@ -0,0 +1,3 @@ +This error means no path has been entered for FFMPEG within OctoPrint's settings screen. In order to render videos, Octolapse must be able to find ffmpeg or avconv. It must be installed on your machine manually if you are using Windows or MacOS. If you are using OctoPi, ffmpeg/avconv is already installed and configured. If you installed OctoPrint manually, you might need to install ffmpeg or avconv manually. + + The location of ffmpeg/avconv is stored within Octoprint's settings. You can edit the location by opening OctoPrint's settings (the wrench/spanner icon at the top of the OctoPrint webpage), and selecting **Webcam & Timelapse** under the **FEATURES** heading in the menu on the right hand side of the settings popup. Then scroll down to find the **Timelapse Recordings** settings, and edit the **Path to FFMPEG**. Click the **Test** button to verify that ffmpeg/avconv exists at the specified location. Be sure to click 'Save' when you are finished editing this setting. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_ffmpeg_path_retrieve_exception.md b/octoprint_octolapse/static/docs/help/error_help_init_ffmpeg_path_retrieve_exception.md new file mode 100644 index 00000000..3468708d --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_ffmpeg_path_retrieve_exception.md @@ -0,0 +1,3 @@ +An error occurred while Octolapse was retrieving the path to FFMPEG from OctoPrint's settings. This usually indicates a problem with the OctoPrint settings file, and can be corrected by re-entering the appropriate path and saving the changes. Octolapse was not able to find ffmpeg at the path set in OctoPrint's settings. In order to render videos, Octolapse must be able to find ffmpeg or avconv. It must be installed on your machine manually if you are using Windows or MacOS. If you are using OctoPi, ffmpeg/avconv is already installed and configured. If you installed OctoPrint manually, you might need to install ffmpeg or avconv manually. + + The location of ffmpeg/avconv is stored within Octoprint's settings. You can edit the location by opening OctoPrint's settings (the wrench/spanner icon at the top of the OctoPrint webpage), and selecting **Webcam & Timelapse** under the **FEATURES** heading in the menu on the right hand side of the settings popup. Then scroll down to find the **Timelapse Recordings** settings, and edit the **Path to FFMPEG**. Click the **Test** button to verify that ffmpeg/avconv exists at the specified location. Be sure to click 'Save' when you are finished editing this setting. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_incorrect_octoprint_version.md b/octoprint_octolapse/static/docs/help/error_help_init_incorrect_octoprint_version.md new file mode 100644 index 00000000..824bfd93 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_incorrect_octoprint_version.md @@ -0,0 +1,5 @@ +You are using an older version of OctoPrint that is not supported by Octolapse. + +#### Upgrading OctoPrint + +You can upgrade OctoPrint by opening the settings (wrench/spanner icon at the top of the OctoPrint webpage), selecting **Software Update** from the leftmost menu (under the **OCTOPRINT** heading), and clicking **Update** next to the OctoPrint item. Reboot when prompted. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_incorrect_print_start_state.md b/octoprint_octolapse/static/docs/help/error_help_init_incorrect_print_start_state.md new file mode 100644 index 00000000..9f6e9946 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_incorrect_print_start_state.md @@ -0,0 +1,3 @@ +Octolapse expects the printer to be in the **Initializing** state while preparing to start a timelapse. If it is in any other state, this indicates some kind of error. It's possible that your printer has disconnected or that the print has been cancelled. Try disconnecting and reconnecting your printer. That will usually fix this issue. If that fails, try powering down your printer, then powering down your Pi (or PC if you are not using a Pi) via the OctoPrint interface. Then turn your printer back on, followed by your Pi + + Note: **NEVER unplug your pi to turn it off! This can lead to SD card corruption, which requires a reinstall to fix!** diff --git a/octoprint_octolapse/static/docs/help/error_help_init_m114_not_supported.md b/octoprint_octolapse/static/docs/help/error_help_init_m114_not_supported.md new file mode 100644 index 00000000..8cbbb90b --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_m114_not_supported.md @@ -0,0 +1 @@ +You printer does not support a critical gcode, M114, which Octolapse uses to correctly time snapshots. You can solve this problem by flashing your printer with firmware that supports this gcode. I recommend you contact the manufacturer, since this gcode is required for many OctoPrint plugins, and is considered a standard. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_manual_slicer_settings_missing.md b/octoprint_octolapse/static/docs/help/error_help_init_manual_slicer_settings_missing.md new file mode 100644 index 00000000..900102ae --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_manual_slicer_settings_missing.md @@ -0,0 +1,22 @@ +Octolapse was started, but some required slicer settings do not exist in your Octolapse printer profile. This most likely indicates that you upgraded Octolapse, but there was some problem migrating your settings. Fortunately this is usually an easy problem to solve + +## Steps to Solve + +The easiest way to fix this issue is to enable **Automatic Slicer Configuration**. This feature does not work in all situations, however, so you might need to manually enter your settings. + +### Solution 1: Use Automatic Slicer Configuration +If you are using Cura, Slic3r, Slic3r PE, PrusaSlicer or Simplify 3D, Octolapse can extract your slicer settings automatically. If you are using a different slicer, see the **I'm using another slicer** section below. + +#### Cura +Cura does not output slicer settings by default. See this guide for enabling automatic slicer settings when using Cura. + +#### Slice3r, Slic3r PE, Prusa Slicer, or Simplify 3D +Octolapse should work out of the box for these slicers. Simply open your Octolapse printer profile and select **Automatic Configuration** from the **Slicer Type** dropdown box and save your changes. + +#### I'm using another slicer +You are using a slicer that is not specifically supported by Octolapse. [Use Manual Slicer Settings](#solution-2-use-manual-slicer-settings) below for information on how to manually enter your slicer settings into Octolapse. + +If you would like for your slicer to work with **Automatic Slicer Configuration** feature, consider creating a feature request in the Octolapse github repository. Make sure you read the wiki page explaining how to create a new issue. Also, be sure to include some sample gcode, the exact slicer version, and a link to the slicer's homepage if possible. + +### Solution 2: Use Manual Slicer Settings +You can manually enter the missing slicer settings into Ocotlapse. First, open your Octolapse printer profile. The missing settings should have a red error message below them. You will then need to copy your slicer settings **EXACTLY** from your slicer to Octolapse. If you are using the **Other Slicer** type, be very careful that you select the correct **Speed Display Units** (either mm/min or mm/sec) when entering any slicer speeds. Any mistakes made while copying your slicer settings can reduce your print quality, and may even prevent Octolapse from working entirely. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_no_current_job_data_found.md b/octoprint_octolapse/static/docs/help/error_help_init_no_current_job_data_found.md new file mode 100644 index 00000000..88dbad94 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_no_current_job_data_found.md @@ -0,0 +1,8 @@ +No data for your current print job was returned from Octoprint. This is most likely due to a programming error, or possibly due to an incompatible version of OctoPrint. + +#### Try Upgrading OctoPrint + +You can upgrade OctoPrint by opening the settings (wrench/spanner icon at the top of the OctoPrint webpage), selecting **Software Update** from the leftmost menu (under the **OCTOPRINT** heading), and clicking **Update** next to the OctoPrint item. Reboot when prompted. + +#### Report an Issue +If you can't solve this problem and you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_no_current_job_file_data_found.md b/octoprint_octolapse/static/docs/help/error_help_init_no_current_job_file_data_found.md new file mode 100644 index 00000000..f9458567 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_no_current_job_file_data_found.md @@ -0,0 +1,8 @@ +Data for the print job was found, but no file data could be retrieved from Octoprint. This is most likely due to a programming error, or possibly due to an incompatible version of OctoPrint. + +#### Try Upgrading OctoPrint + +You can upgrade OctoPrint by opening the settings (wrench/spanner icon at the top of the OctoPrint webpage), selecting **Software Update** from the leftmost menu (under the **OCTOPRINT** heading), and clicking **Update** next to the OctoPrint item. Reboot when prompted. + +#### Report an Issue +If you can't solve this problem and you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_no_enabled_cameras.md b/octoprint_octolapse/static/docs/help/error_help_init_no_enabled_cameras.md new file mode 100644 index 00000000..47070081 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_no_enabled_cameras.md @@ -0,0 +1,8 @@ +Octolapse requires at least one enabled camera to function. Make sure you have created at least one camera profile, and that at least one camera profile is enabled. + +#### Enable a Camera + +You can enable a camera profile one of two ways: + +1. Open the Octolapse Tab and expand the **Current Run Configuration** section. Then check the check box to the left of your camera. +2. Open the OctoPrint settings by clicking on the wrench/spanner icon at the top of the webpage. Find the **Octolapse** menu item within the left menu (usually towards the bottom). Click on the **Camera** tab within the Octolapse settings, and check the checkbox under the **Action** column. Alternatively you can edit your camera settings and click **Enabled** at the very top of the profile under the **Description**. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_no_gcode_filepath_found.md b/octoprint_octolapse/static/docs/help/error_help_init_no_gcode_filepath_found.md new file mode 100644 index 00000000..d301e785 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_no_gcode_filepath_found.md @@ -0,0 +1,8 @@ +Octolapse was unable to find the gcode file at the location specified by OctoPrint. This is most likely due to a programming error, or possibly due to an incompatible version of OctoPrint. + +#### Try Upgrading OctoPrint + +You can upgrade OctoPrint by opening the settings (wrench/spanner icon at the top of the OctoPrint webpage), selecting **Software Update** from the leftmost menu (under the **OCTOPRINT** heading), and clicking **Update** next to the OctoPrint item. Reboot when prompted. + +#### Report an Issue +If you can't solve this problem and you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_no_printer_profile_exists.md b/octoprint_octolapse/static/docs/help/error_help_init_no_printer_profile_exists.md new file mode 100644 index 00000000..dcfb12dc --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_no_printer_profile_exists.md @@ -0,0 +1,3 @@ +You have not yet created a printer profile. Open the Octolapse tab and click on the **Plus** icon to the right of the printer profile drop down. This will open a new printer profile. First, see if a pre-made profile exists by looking for the make of your printer in the **Make** drop down box under the **Import Printer Profile** section. If you can find your Make in the list, select in, and a new dropdown box called **Model** will appear. Try to find your Model there. If you've found both the Make and Model, save your profile and try your print again. + +If you cannot find your printer profile you will need to create a new profile from scratch. I am planning to make a detailed guide regarding new printer setup, but either this has not yet been completed, or these instructions have not been updated. You can also create a feature request asking for help getting a profile created for your printer. If you are willing to put in a bit of effort, consider creating a feature request in the Octolapse github repository. Make sure you read the wiki page explaining how to create a feature request first. Also, be sure to include the make and model of your printer, the build volume, a sliced gcode file (a small one) using the stock slicer profile (if one exists for your printer), and anything else you thing would be useful. It's likely that a bit of back-and-forth will be required before a working profile can be created. It's possible that once the new profile has been verified to work that I will upload it to the profile repository so that everyone can import the new profile. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_no_printer_profile_selected.md b/octoprint_octolapse/static/docs/help/error_help_init_no_printer_profile_selected.md new file mode 100644 index 00000000..822ea4a9 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_no_printer_profile_selected.md @@ -0,0 +1,4 @@ +No printer profile has been selected. You can select a profile by opening the Octolapse Tab, expanding the **Current Run Configuration** section, and selecting a printer from the **Printer** drop down list. If your printer is not in the list, you will need to create a new profile by clicking on the **Printer** link to the left of the drop-down box and clicking the **Add Profile** button. This will open a new printer profile. First, see if a pre-made profile exists by looking for the make of your printer in the **Make** drop down box under the **Import Printer Profile** section. If you can find your Make in the list, select in, and a new dropdown box called **Model** will appear. Try to find your Model there. If you've found both the Make and Model, save your profile and try your print again. + +If you cannot find your printer profile you will need to create a new profile from scratch. I am planning to make a detailed guide regarding new printer setup, but either this has not yet been completed, or these instructions have not been updated. You can also create a feature request asking for help getting a profile created for your printer. If you are willing to put in a bit of effort, consider creating a feature request in the Octolapse github repository. Make sure you read the wiki page explaining how to create a feature request first. Also, be sure to include the make and model of your printer, the build volume, a sliced gcode file (a small one) using the stock slicer profile (if one exists for your printer), and anything else you thing would be useful. It's likely that a bit of back-and-forth will be required before a working profile can be created. It's possible that once the new profile has been verified to work that I will upload it to the profile repository so that everyone can import the new profile. + diff --git a/octoprint_octolapse/static/docs/help/error_help_init_octolapse_is_disabled.md b/octoprint_octolapse/static/docs/help/error_help_init_octolapse_is_disabled.md new file mode 100644 index 00000000..915a569a --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_octolapse_is_disabled.md @@ -0,0 +1,4 @@ +Octolapse attempted to start, but was disabled. This probably means that Octolapse was disabled from another browser while you were starting a print. It could also be a bug. + +#### Report an Issue +If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. When you submit your issue, be sure to include your gcode file, and the exact slicer version you used to create your gcode file. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_overlay_font_path_not_found.md b/octoprint_octolapse/static/docs/help/error_help_init_overlay_font_path_not_found.md new file mode 100644 index 00000000..8f756035 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_overlay_font_path_not_found.md @@ -0,0 +1,4 @@ +Your current rendering profile has overlay text, but Octolapse could not locate the font path. It's possible that you have imported a settings file that contained a font path that does not exist on your machine. + +## Steps to Fix +Open your rendering profile, scroll down to **Overlay** and either delete the overlay text, or select a different font from the list. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_printer_not_configured.md b/octoprint_octolapse/static/docs/help/error_help_init_printer_not_configured.md new file mode 100644 index 00000000..37f60545 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_printer_not_configured.md @@ -0,0 +1 @@ +The printer profile you have selected was never configured. This probably means that you upgraded Octolapse to a newer version, and are using a migrated profile that has never been configured before. First, open the Octolapse tab and expand the **Current Run Configuration** section. The click on the edit Pen to the right of the printer profile drop down box. All of the default profiles from the previous version of Octolapse can be imported automatically, and are pre-configured. Look for your printer in the **Make** drop down box under the **Import Printer Profile** section. A new dropdown box called **Model** will appear. Select your Model there. Next, you will need to configure your slicer settings. Use the help icon (question mark) next to the **Slicer Type** drop down for more information about entering your slicer settings. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_rendering_file_template_invalid.md b/octoprint_octolapse/static/docs/help/error_help_init_rendering_file_template_invalid.md new file mode 100644 index 00000000..7af37bf4 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_rendering_file_template_invalid.md @@ -0,0 +1,5 @@ +An invalid rendering filename template has been detected in your current rendering profile. To fix this open up your current rendering settings by navigating to the Octolapse tab, expanding the **Current Run Configuration** section, and clicking the edit pen to the right of the rendering profile drop down box. Make sure the **Customize Profile** checkbox is checked. Next, scroll to the **Files and Performance** section and find the **Filename Template** setting. There should be an error indicator next to this box. Enter this value in that box to fix the error: + +``` +{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME} +``` diff --git a/octoprint_octolapse/static/docs/help/error_help_init_timelapse_start_exception.md b/octoprint_octolapse/static/docs/help/error_help_init_timelapse_start_exception.md new file mode 100644 index 00000000..e61ada18 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_timelapse_start_exception.md @@ -0,0 +1,4 @@ +This indicates an unhandled exception occurred, which is usually a bad sign, and indicates either an unexpected settings issue, an incompatible version of OctoPrint, or a programming error in Octolapse. It's time to report an issue. + +#### Report an Issue +If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_too_few_extruders_defined.md b/octoprint_octolapse/static/docs/help/error_help_init_too_few_extruders_defined.md new file mode 100644 index 00000000..fd3e5459 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_too_few_extruders_defined.md @@ -0,0 +1 @@ +Your printer profile has fewer extruders defined than your gcode file. Please increase the number of extruders in your printer profile. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_too_few_extruders_offsets_defined.md b/octoprint_octolapse/static/docs/help/error_help_init_too_few_extruders_offsets_defined.md new file mode 100644 index 00000000..954d9b9f --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_too_few_extruders_offsets_defined.md @@ -0,0 +1 @@ +Your printer profile has fewer extruder offsets defined than extruders. This is an unexpected error, and is probably a bug in Octolapse. Double check your printer profile and make sure the number of extruders is correct. If your printer has one nozzle, but supports multi materials (like the Prusa MMU), be sure to check the 'shared nozzle' box in your printer profile. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_unable_to_accept_snapshot_plan.md b/octoprint_octolapse/static/docs/help/error_help_init_unable_to_accept_snapshot_plan.md new file mode 100644 index 00000000..7bd36b43 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_unable_to_accept_snapshot_plan.md @@ -0,0 +1 @@ +This error is usually caused when a snapshot plan is accepted in another browser, but the current browser was not connected to octoprint when the plan was accepted. diff --git a/octoprint_octolapse/static/docs/help/error_help_init_unexpected_exception.md b/octoprint_octolapse/static/docs/help/error_help_init_unexpected_exception.md new file mode 100644 index 00000000..191319c8 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_unexpected_exception.md @@ -0,0 +1,5 @@ +An unexpected exception occurred while starting Octolapse. More details about where the exception occurred should be stored in plugin_octolapse.log, as long as error logging is enabled (see your current debug profile). + +#### Report an Issue +Since this exception was not planned for, you may wish to submit a bug report. If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. When you submit your issue, be sure to include your log file (plugin_octolapse.log). + diff --git a/octoprint_octolapse/static/docs/help/error_help_init_unknown_file_origin.md b/octoprint_octolapse/static/docs/help/error_help_init_unknown_file_origin.md new file mode 100644 index 00000000..1caaccb8 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_init_unknown_file_origin.md @@ -0,0 +1,4 @@ +Octolapse could not tell if the selected gcode file is located on the printer's SD card, or is stored locally. This is an unexpected issue that indicates either an unexpected settings issue, an incompatible version of OctoPrint, or a programming error in Octolapse. It's time to report an issue. + +#### Report an Issue +If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_not_a_valid_error_dict.md b/octoprint_octolapse/static/docs/help/error_help_not_a_valid_error_dict.md new file mode 100644 index 00000000..72433642 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_not_a_valid_error_dict.md @@ -0,0 +1,4 @@ +This is an Octolapse bug caused by an incorrectly supplied error key or a missing error description. Please report the original error message, including the keys in the popup, to the octolapse github repository. + +#### Report an Issue +If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_axis_mode_e_unknown.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_axis_mode_e_unknown.md new file mode 100644 index 00000000..634e12cd --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_axis_mode_e_unknown.md @@ -0,0 +1,6 @@ +Octolapse could not detect your E Axis mode. There are two solutions: + +1. Either add an ```M82``` to the very top of your start gcode in your slicer. +2. Edit your Octolapse printer profile, find the **Firmware Settings** section and set the **E Axis Mode** to **Absolute**. + +_Note for option 2: If you can't find the E-Axis Mode setting, make sure **Customize Profile** is checked under the **Import Printer Profile** section._ diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_axis_mode_xyz_unknown.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_axis_mode_xyz_unknown.md new file mode 100644 index 00000000..56ee9a87 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_axis_mode_xyz_unknown.md @@ -0,0 +1,6 @@ +Octolapse could not detect your XYZ Axis mode. There are two solutions: + +1. Either add a ```G90``` command to the very top of your start gcode in your slicer. +2. Edit your Octolapse printer profile, find the **Firmware Settings** section and set the **X/Y/Z Axis Mode** to **Absolute**. + +_Note for option 2: If you can't find the **X/Y/Z-Axis Mode** setting, make sure **Customize Profile** is checked under the **Import Printer Profile** section._ diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_incorrect_trigger_type.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_incorrect_trigger_type.md new file mode 100644 index 00000000..20a29d22 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_incorrect_trigger_type.md @@ -0,0 +1,4 @@ +An attempt was made to preprocess a gcode file, but the selected trigger is not capable of being preprocessed. This is an unexpected issue that indicates either an unexpected settings issue, an incompatible version of OctoPrint, or a programming error in Octolapse. It's time to report an issue. + +#### Report an Issue +If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_definite_position.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_definite_position.md new file mode 100644 index 00000000..e8e64a2b --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_definite_position.md @@ -0,0 +1,6 @@ +Octolapse was not able to track your printer's movements. This is usually a problem detecting the current axis mode, incorrect distance units (only metric units are supported currently), problems detecting homing, or problems parsing your gcode file. + +If other errors were reported, solving those will likely fix this error. If no other issues were reported by Octolapse, it's time to create an issue. + +#### Report an Issue +If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_metric_units.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_metric_units.md new file mode 100644 index 00000000..be73c3e7 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_metric_units.md @@ -0,0 +1,4 @@ +This error means that Octolapse can't determine what units your gcode is using. There are two ways to fix this error: + +1. Open your Octolapse printer profile settings. Scroll down to the **Firmware Settings** section and change the **Default Units** option to **Millimeters**. +2. Open your slicer and add the following gcode to the very top of your start gcode: G21 diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_snapshot_commands_found.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_snapshot_commands_found.md new file mode 100644 index 00000000..09e8094a --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_snapshot_commands_found.md @@ -0,0 +1,19 @@ +This error indicates that you are using the **Smart Gcode** trigger, but no snapshot commands were found within your gcode file. + +To fix this command, open your Octolapse printer profile and look at the **Snapshot Command** setting. Then open your gcode file and make sure that this command exists within the file. Usually the snapshot command is added within layer change scripts inside of your slicer. + +**A note about case and comments** + +Snapshot commands are NOT case sensitive in Octolapse V0.4+, so upper case and lower case should both work in either the snapshot command or in your gcode file. Please note that all comments are stripped from the alternative snapshot command while Octolapse is running. For example, if you use + +```G4 p1 ; THIS IS A SNAPSHOT``` + +as your snapshot command, Octolapse will strip the comment and will trigger on any occurrence of + +```G4 P1``` + +Since comments are ignored in OctoPrint, Octolapse would also trigger if the following line appears within your gcode file: + +```SNAP ; NOTICE HOW THIS IS A DIFFERENT COMMENT!``` + +In other words, comments are ignored both within the alternative snapshot command, and within the gcode file. diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_snapshot_plans_returned.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_snapshot_plans_returned.md new file mode 100644 index 00000000..b1b33df6 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_no_snapshot_plans_returned.md @@ -0,0 +1,8 @@ +No snapshots plans were returned. This can be caused by incorrect printer profile settings or trigger settings. + +#### Printer profile settings to check +1. Make sure your printer volume is entered correctly. +2. Disable the **Restrict Snapshot Area** setting. +3. Make sure the **Minimum Layer Height** setting is higher than your printed object is tall. The default setting here is 0.05MM. +#### Trigger profile settings to check +1. Check the **Trigger Height** settings and make sure it's set to 0. diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_printer_not_primed.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_printer_not_primed.md new file mode 100644 index 00000000..b3abaf03 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_printer_not_primed.md @@ -0,0 +1,6 @@ +Priming could not be detected. The will prevent Octolapse from taking any snapshots. + +#### Steps to solve + +1. If there are any other errors reported, fix those first. This problem is usually caused by axis mode detection problems. +2. Open your printer profile settings and find the **Priming Height** within the **Layer Change Detection** setting. This should be set to the height at which your printer primes. Many printers prime at 5MM, but some (like mine) prime at about 0.3MM directly on the bed. If your printer primes above your print bed, this setting is critical. If in doubt, set this value to your current layer height (0.2 is common), or set to 0 to disable priming detection. **Warning**: if priming detection is disabled or incorrect, Octolapse may take a snapshot while priming, then will fail to capture more snapshots until the priming height is reached. diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_unhandled_exception.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_unhandled_exception.md new file mode 100644 index 00000000..76916c80 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_unhandled_exception.md @@ -0,0 +1,4 @@ +An attempt was made to preprocess a gcode file, an unexpected error was returned. This could be due to an octolapse installation problem, specifically compiling the gocde preprocessor. It could also be an unexpected settings issue, an incompatible version of OctoPrint, a file corruption or permissions issue or a programming error in Octolapse. It's time to report an issue. + +#### Report an Issue +If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_preprocessor_unknown_trigger_type.md b/octoprint_octolapse/static/docs/help/error_help_preprocessor_unknown_trigger_type.md new file mode 100644 index 00000000..47389649 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_preprocessor_unknown_trigger_type.md @@ -0,0 +1,4 @@ +An attempt was made to preprocess a gcode file, but the selected trigger is unknown. This is an unexpected issue that indicates either an unexpected settings issue, an incompatible version of OctoPrint, or a programming error in Octolapse. It's time to report an issue. + +#### Report an Issue +If you are willing to help me debug your problem (which takes time and effort for both of us), please see this guide for reporting an issue. diff --git a/octoprint_octolapse/static/docs/help/error_help_rendering_archive_import_no_files_found.md b/octoprint_octolapse/static/docs/help/error_help_rendering_archive_import_no_files_found.md new file mode 100644 index 00000000..deacfcea --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_rendering_archive_import_no_files_found.md @@ -0,0 +1,16 @@ +No files were found within the imported zip archive. Octolapse supports two different ways of importing zip archives: + +1. All files in the root of the archive. +2. Files can be located within a JOB_GUID\CAMERA_GUID folder structure. Here is an example of this structure: +``` +dd46e35c-50ab-4f7a-a90d-4c69e59b2a4f\354def78-9eea-409a-ad23-ee966dfff4ba\{All Files Here} +``` + +The easiest way to resolve these errors is to put all of your images in a single folder, and zip them together. Make sure your images have a ```.jpg``` extension. Optionally you can include the following other settings file: +``` +camera_info.json +camera_settings.json +rendering_settings.json +metadata.csv +timelapse_info.json +``` diff --git a/octoprint_octolapse/static/docs/help/error_help_rendering_archive_import_zip_file_corrupt.md b/octoprint_octolapse/static/docs/help/error_help_rendering_archive_import_zip_file_corrupt.md new file mode 100644 index 00000000..70497c71 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_rendering_archive_import_zip_file_corrupt.md @@ -0,0 +1,6 @@ +The zip file you are attempting to import is either in the wrong format, or is corrupted. + +### Possible Solutions +1. Make sure your archive is in zip format. +2. Extract and re-create your archive. It is OK to put all of your files in the root of the zip. +3. Re-upload the zip file. It is possible that the file was corrupted during upload. diff --git a/octoprint_octolapse/static/docs/help/error_help_rendering_archive_import_zip_file_too_large.md b/octoprint_octolapse/static/docs/help/error_help_rendering_archive_import_zip_file_too_large.md new file mode 100644 index 00000000..f264219d --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_rendering_archive_import_zip_file_too_large.md @@ -0,0 +1,7 @@ +Due to limitations of either your python version, file system, or OS, the zip file you are trying to import is too large to be opened. + +### Possible Solutions + +1. Upgrade to python 3. This requires Octoprint 1.4.0 or above. +2. Reduce the zip file size. You can either remove images or reduce the resolution. +3. Change your SD card's file system. Fat32 is limited to files smaller than 4 GB. diff --git a/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_duplicate_toolhead_numbers.md b/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_duplicate_toolhead_numbers.md new file mode 100644 index 00000000..1a9a036e --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_duplicate_toolhead_numbers.md @@ -0,0 +1,4 @@ +Your gcode file, created from Simplify3D, contains multiple extruders referencing the same toolhead index. Though this is allowable in Simplify3D, this is not supported in Octolapse. You have two options: + +1. Make sure each extruder configured in your current Simplify3D process is using a unique *Extruder Toolhead Index*. +2. Disable Octolapse. diff --git a/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_extruder_count_mismatch.md b/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_extruder_count_mismatch.md new file mode 100644 index 00000000..c32138ac --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_extruder_count_mismatch.md @@ -0,0 +1,7 @@ +Your gcode file, created from Simplify3D, contains a different number of defined extruders than the number of extruders defined in your printer profile. + +To solve this problem first make sure your Octolapse printer profile has the correct number of extruders configured. If your printer has multiple physical extruders, or supports multiple materials (like the Prusa MMU1 and MMU2), make sure your Octolapse printer profile has the **Firmware Settings->Number of Extruders/Materials** setting correct. You may have to enter extruder offsets if your printer has multiple physical extruders. Also, the **First Extruder is 0** setting must be correct. If your printer uses the **T0** command to select the first extruder, check this box. If your printer uses **T1** as the first extruder, uncheck this box. + +Next, open Simplify3D and edit your process. Click on the **Extruder** tab and make sure you have one extruder in your **Extruder List** for each extruder/material. Make sure you set a unique tool index for each extruder. If you checked the **First Extruder is 0** option above, make sure your first extruder is at index 0, or 1 if **First Extruder is 0** is unchecked. + +Now, reslice your model using the new process settings and try again! diff --git a/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_max_extruder_count_exceeded.md b/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_max_extruder_count_exceeded.md new file mode 100644 index 00000000..f26fe758 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_max_extruder_count_exceeded.md @@ -0,0 +1 @@ +Your gcode file, created from Simplify3D, contains more than 8 extruders. A max of 8 extruders is currently supported. diff --git a/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_unexpected_max_toolhead_number.md b/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_unexpected_max_toolhead_number.md new file mode 100644 index 00000000..ba55e87c --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_settings_slicer_simplify3d_unexpected_max_toolhead_number.md @@ -0,0 +1,5 @@ +Your gcode file, created from Simplify3D, contains a toolhead index that is higher than expected. First make sure that your Octolapse printer profile is configured with the correct number of extruders by checking **Firmware Settings->Number of Extruders/Materials**. You may have to enter extruder offsets if your printer has multiple extruders and not a shared nozzle. The **First Extruder is 0** setting must also be correct if you have more than one extruder/material. If your printer uses the **T0** command to select the first extruder, check this box. If your printer uses **T1** as the first extruder, uncheck this box. + +Next, open Simplify3D and edit your process. Click on the **Extruder** tab and make sure you have one extruder in your **Extruder List** for each extruder/material. Make sure you set a unique tool index for each extruder. If you checked the **First Extruder is 0** option above, make sure your first extruder is at index 0, or 1 if **First Extruder is 0** is unchecked. + +Now, reslice your model using the new process settings and try again! diff --git a/octoprint_octolapse/static/docs/help/error_help_timelapse_cannot_aquire_job_lock.md b/octoprint_octolapse/static/docs/help/error_help_timelapse_cannot_aquire_job_lock.md new file mode 100644 index 00000000..a99f8afd --- /dev/null +++ b/octoprint_octolapse/static/docs/help/error_help_timelapse_cannot_aquire_job_lock.md @@ -0,0 +1 @@ +Octolapse was not able to prevent OctoPrint from sending gcodes to your printer in order to take a snapshot or detect a home position. This is likely due to another plugin that has acquired the lock. To verify this issue, disable all other plugins and try again. If you can figure out which plugin is causing the issue, by enabling each one at a time, please let me know! diff --git a/octoprint_octolapse/static/docs/help/main_settings.is_test_mode.md b/octoprint_octolapse/static/docs/help/main_settings.is_test_mode.md new file mode 100644 index 00000000..012d3fc7 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/main_settings.is_test_mode.md @@ -0,0 +1,16 @@ +Test mode can be used to test your timelapse settings without wasting filament or waiting for your printer to warm up. When test mode is enabled, your printer will not warm up, fans will not be engaged, and filament will not be extruded. + +Test mode must be activated BEFORE starting a print. Changing this setting mid-print won't alter any current prints. + +Before using test mode, **UNLOAD YOUR FILAMENT**! Even though I attempted to strip all extruder commands, there's a chance that I missed some cases. Please let me know if you see any 'cold extrusion prevented' errors in the terminal window. If you do, please send me your GCode! + +#### How to Enable/Disable Test Mode + +You can enable or disable test mode in two ways: + +1. Expand the **Current Run Configuration** within the Octolapse tab and click on **Test Mode** to toggle the setting. +2. Open the Octolapse **Main Settings** by clicking on the (gear) icon within the Octolapse tab, then click on the **Edit Main Settings** button to edit the Octlapse main settings. You can find and edit **Test Mode** there. Be sure to save your changes! + +#### Important Safety Information + +**NEVER** print with your printer unattended, and that includes running Octolapse in test mode. After starting a test mode print, make sure your extruder and build plate do not warm up, and that your extruder isn't moving. If your build plate or extruder warms up, or if your extruder gear is turning with test mode enabled, DISCONTINUE THE PRINT and please report the problem in the Octolapse github issues page. diff --git a/octoprint_octolapse/static/docs/help/main_settings.preview_snapshot_plan_autoclose.md b/octoprint_octolapse/static/docs/help/main_settings.preview_snapshot_plan_autoclose.md new file mode 100644 index 00000000..14d56616 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/main_settings.preview_snapshot_plan_autoclose.md @@ -0,0 +1,3 @@ +When enabled, the snapshot plan preview will automatically close after a number of seconds. This is useful if you want to preview the snapshot plan normally, but occasionally want to start your print without access to the Octoprint website. For example, if you start your prints with the Cura OctoPrint plugin, or from an Octoprint mobile app. + +Once this option is enabled, you can configure the number of seconds to wait with the 'Auto-Close Snapshot Plan Preview Seconds' setting. diff --git a/octoprint_octolapse/static/docs/help/main_settings.preview_snapshot_plan_seconds.md b/octoprint_octolapse/static/docs/help/main_settings.preview_snapshot_plan_seconds.md new file mode 100644 index 00000000..5381be3b --- /dev/null +++ b/octoprint_octolapse/static/docs/help/main_settings.preview_snapshot_plan_seconds.md @@ -0,0 +1 @@ +The number of seconds to wait before automatically closing the snapshot plan preview and continuing the print. diff --git a/octoprint_octolapse/static/docs/help/main_settings.show_extruder_state_changes.md b/octoprint_octolapse/static/docs/help/main_settings.show_extruder_state_changes.md index 23b41f8b..0a1454c2 100644 --- a/octoprint_octolapse/static/docs/help/main_settings.show_extruder_state_changes.md +++ b/octoprint_octolapse/static/docs/help/main_settings.show_extruder_state_changes.md @@ -1,3 +1,3 @@ -See if filament is primed, extruding, or if it's retracting or detracting. If Octolapse isn't taking snapshots, and you suspect your snapshot settings, this is another place to look. See the [Extruder State Requirements](https://github.com/FormerLurker/Octolapse/wiki/Snapshot-Profiles#extruder-state-requirements) within the snapshot profile. +See if filament is primed, extruding, or if it's retracting or detracting. This is more informational than anything. I used this info panel while I was testing the *Extruder State Requriements* for the classic triggers, which tell Octolapse when it's allowed to or forbidden from taking snapshots based on the extruder state. This is intended to improve print quality. Note: This info panel currently is only available when using real-time triggers. diff --git a/octoprint_octolapse/static/docs/help/main_settings.show_snapshot_plan_information.md b/octoprint_octolapse/static/docs/help/main_settings.show_snapshot_plan_information.md index 61b786db..0180c925 100644 --- a/octoprint_octolapse/static/docs/help/main_settings.show_snapshot_plan_information.md +++ b/octoprint_octolapse/static/docs/help/main_settings.show_snapshot_plan_information.md @@ -1 +1,3 @@ When enabled all planned snapshots will be shown on the Octolapse tab when using the smart layer trigger. Snapshot plan information will not be shown when using any of the real time triggers, like the timer, gcode and classic layer trigger. + +Note: This slider does NOT control the snapshot plan preview popup. You can enable/disable the snapshot plan preview within the *Main Settings* by clicking the (gear) icon, then by clicking *Edit Main Settings*, and finally adjusting the *Snapshot Plan Preview* settings under the *Print Start Options*. diff --git a/octoprint_octolapse/static/docs/help/main_settings.snapshot_archive_directory.md b/octoprint_octolapse/static/docs/help/main_settings.snapshot_archive_directory.md new file mode 100644 index 00000000..3cb89268 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/main_settings.snapshot_archive_directory.md @@ -0,0 +1,34 @@ +This folder will be used to store an archived copy of all timelapse images if archiving is enabled within the rendering profile. The archive will be created either after rendering is completed, or after the timelapse is completed if rendering is not enabled. You can download any archived timelapses within the Octolapse tab. Click the test button to verify that Octolapse has the appropriate permissions to access, and if necessary create this directory. + +### Default Value +If the folder is left empty, Octolapse will use the plugin data directory. In windows, this directory is typically: + +``` +{UserDirectory}/AppData/OctoPrint/data/octolapse/snapshot_archive +``` +where ```{UserDirectory}``` is the user folder under which OctoPrint was installed. In my system this is located within ```C:\users\USERNAME```. + +If you are using OctoPi, the directory is typically: + +``` +/home/pi/.octoprint/data/octolapse/snapshot_archive +``` + +If you know where this folder is under macOS, pleeas let me know. + +### Absolute Path +The provided folder must be an absolute path. In windows this looks like the following: + +``` +c:\some\path\here +``` + +In Unix, Linux, and macOS, an absolute path looks like this: +``` +/this/is/an/absolute/path +``` + +**Important Note**: Keeping snapshot images requires a LOT of space. If you run out of disk space during a print, your print may fail. Always make sure you have enough free space before starting a timelapse! + +### Folder Premissions +Octolapse requires the ability to create and delete files and directories within any selected folders. You can test permissions by clicking the **Test** button to the right of the directory. diff --git a/octoprint_octolapse/static/docs/help/main_settings.temporary_directory.md b/octoprint_octolapse/static/docs/help/main_settings.temporary_directory.md new file mode 100644 index 00000000..5b5ccbe8 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/main_settings.temporary_directory.md @@ -0,0 +1,34 @@ +This folder will be used to store snapshots, including any failed or unfinished renderings. It will also store temporary images used to render a final timelapse, and a temporary rendering until it is moved to the timelapse folder. Click the test button to verify that Octolapse has the appropriate permissions to access, and if necessary create this directory. + +**Important Note**: Due to the way Octolapse constructs timelapses, this folder may require a lot of storage. If you run out of disk space during a print, your print may fail. Always make sure you have enough free space before starting a timelapse! + +### Default Value +If the folder is left empty, Octolapse will use the plugin data directory. In windows, this directory is typically: + +``` +{UserDirectory}/AppData/OctoPrint/data/octolapse/tmp +``` +where ```{UserDirectory}``` is the user folder under which OctoPrint was installed. In my system this is located within ```C:\users\USERNAME```. + +If you are using OctoPi, the directory is typically: + +``` +/home/pi/.octoprint/data/octolapse/tmp +``` + +If you know where this folder is under macOS, pleeas let me know. + +### Absolute Path +The provided folder must be an absolute path. In windows this looks like the following: + +``` +c:\some\path\here +``` + +In Unix, Linux, and macOS, an absolute path looks like this: +``` +/this/is/an/absolute/path +``` + +### Folder Premissions +Octolapse requires the ability to create and delete files and directories within any selected folders. You can test permissions by clicking the **Test** button to the right of the directory. diff --git a/octoprint_octolapse/static/docs/help/main_settings.timelapse_directory.md b/octoprint_octolapse/static/docs/help/main_settings.timelapse_directory.md new file mode 100644 index 00000000..4621a89d --- /dev/null +++ b/octoprint_octolapse/static/docs/help/main_settings.timelapse_directory.md @@ -0,0 +1,20 @@ +This folder is where completed timelapses will be moved after they are rendered. It will also control which timelapse videos are visible within the **Timelapse** tab of the **Videos and Files** dialog. Click the test button to verify that Octolapse has the appropriate permissions to access, and if necessary create this directory. + +### Default Value +If the folder is left empty, Octolapse will use the default Octoprint timelapse folder. Also, if left empty your timelapse files will be available within the default timelapse tab in addition to the Octolapse tab. + + +### Absolute Path +The provided folder must be an absolute path. In windows this looks like the following: + +``` +c:\some\path\here +``` + +In Unix, Linux, and macOS, an absolute path looks like this: +``` +/this/is/an/absolute/path +``` + +### Folder Premissions +Octolapse requires the ability to create and delete files and directories within any selected folders. You can test permissions by clicking the **Test** button to the right of the directory. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.apply_settings_at_startup.md b/octoprint_octolapse/static/docs/help/profiles.camera.apply_settings_at_startup.md similarity index 100% rename from octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.apply_settings_at_startup.md rename to octoprint_octolapse/static/docs/help/profiles.camera.apply_settings_at_startup.md diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.apply_settings_before_print.md b/octoprint_octolapse/static/docs/help/profiles.camera.apply_settings_before_print.md new file mode 100644 index 00000000..b82b3409 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.camera.apply_settings_before_print.md @@ -0,0 +1 @@ +When enabled, Octolapse will apply all of your custom image preferences at the start of each print. This ensures that your camera settings are consistent for every print. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.apply_settings_when_disabled.md b/octoprint_octolapse/static/docs/help/profiles.camera.apply_settings_when_disabled.md new file mode 100644 index 00000000..61a9945f --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.camera.apply_settings_when_disabled.md @@ -0,0 +1,5 @@ +When checked, Octolapse will apply custom web camera image preferences even if the camera is disabled. This is useful if you are not using the camera to generate a timelapse, but still want the custom image preferences to be applied on reboot. + +If Octolapse is disabled within the plugin manager, the custom image preferences will *never* be applied. + +If timelapses are disabled (the *Plugin Enabled* setting on the Octolapse Tab), the custom image preferences will **not** be applied before prints start, but *will* be applied on reboot if this option is enabled. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.camera_type.md b/octoprint_octolapse/static/docs/help/profiles.camera.camera_type.md index e3bb4443..c67962fb 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.camera_type.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.camera_type.md @@ -4,11 +4,15 @@ Three types of cameras are supported in Octolapse: Webcams, external (script/DS ### Webcams Webcams must be accessible via http or https and must deliver jpg images (currently). Octolapse supports mjpg_streamer, yawcam, or any other streaming server or camera that can provide an image via a browser link. -This camera type can be used to call a script when a snapshot needs to be taken. The primary use here is for DSLR cameras that support triggering, though there are lots of interesting things you could probably do here. I haven't done it yet, but I believe you could also use this to trigger a camera via GPIO. +### Script -[See this beta guide](https://github.com/FormerLurker/Octolapse/wiki/Configuring-an-External-Camera) for setting up a DSLR camera on Linux using [gPhoto2](http://gphoto.org/). I have also gotten this to work on Windows using [digiCamControl](http://digicamcontrol.com/), but have not finished up the guide for that part yet. +This camera type can be used to call a script when a snapshot needs to be taken. The primary use here is for DSLR cameras that support external triggering, though there are lots of interesting things you could probably do here. People have been able to trigger external cameras via GPIO and possibly other things I haven't heard about. + +[See this beta guide](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Configuring-an-External-Camera) for setting up a DSLR camera on Linux using [gPhoto2](http://gphoto.org/). I have also gotten this to work on Windows using [digiCamControl](http://digicamcontrol.com/), but have not finished up the guide for that part yet. + +**Important Note**: DSLR cameras are not easy to get working AND have good print quality. Some cameras are not compatible, and it requires a lot of technical skill. I am NOT the author or maintainer of the wonderful [gPhoto2](http://gphoto.org/) or [digiCamControl](http://digicamcontrol.com/) projects, and cannot give any technical support for these projects. I have stopped doing this because of the sheer volume of requests and due to the fact that I only have one DSLR, and cannot test any problems with specific cameras. ### Gcode Camera If your printer contains an internal camera that can be triggered via gcode, normally via M240, you can use this camera type to take trigger your printer's camera. I've actually not tested this except through the debugger since I do not have a printer with a built-in camera, so I'm not 100% sure it will work. For this reason I'd welcome any feedback on this camera type! -Note: You can use a Gcode camera to send gcode commands to your printer while taking snapshots. +Note: You can use a Gcode camera to send gcode commands to your printer while taking snapshots. The gcode is sent after snapshots are initiated on any other enabled cameras. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.delay.md b/octoprint_octolapse/static/docs/help/profiles.camera.delay.md index 9925e3f2..8c5e30c2 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.delay.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.delay.md @@ -1,10 +1,16 @@ -This value is in milliseconds, where 1000 milliseconds = 1 second. The delay will be applied after Octolapse moves the bed and extruder into position, but before a snapshot is taken. This can be used to give the camera enough time to adjust, keeping your images clear. If you set this value too high, you're prints will take longer and you may have stringing/quality issues. The default value is 125, but I've gotten good results with significantly lower values. Some users have reported needing to double this setting or more. +This value is in milliseconds, where 1000 milliseconds = 1 second. The delay will be applied after Octolapse moves the bed and extruder into position, but before a snapshot is taken. This can be used to give the camera enough time to adjust, keeping your images clear. If you set this value too high, you're prints will take longer and you may have stringing/quality issues. The default value is 125, but I've gotten good results with significantly lower values. -In general if your delay needs to be very high in order to get a clear image, there may be a few other things going on: +For best results, I recommend starting out with a delay of 0 and working up to a max of ```1000 / FrameRate```. + +When using the _Snap to Print_ smart layer trigger you want this as low as possible! I recommend 0 delay when using _Snap to Print_ and only increasing it if absolutely necessary. + +When using an external DSLR, set this value to 0. Delay is almost never necessary when using a DSLR, so only increase it as a last resort. + +If you require a large delay (more than 150MS) In general if your delay needs to be very high in order to get a clear image, there may be a few other things going on: 1. Autofocus is enabled - It takes some time to autofocus. You will be able to lower your delay by manually focusing your camera. 2. Other automatic settings - some of these may also impact the delay, but this is unknown. 3. Low light conditions - some cameras require more light than others, and some cameras automatically adjust the exposure. Increasing the lighting and reducing the exposure time can lower the required delay and improve the output video quality significantly. 4. SECURE YOUR CAMERA! A loose or wobbly camera will affect your timelapse. There is no way to get a smooth timelapse if the camera is bouncing around or sliding. -5. Consider adding vibration dampening to your camera/printer, or move your camera to another structure. +5. Consider mounting your camera to your printer. If your camera is mounted securely, it will wobble along with your printer, reducing shaky frames. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.edit_cutsom_image_preferences.md b/octoprint_octolapse/static/docs/help/profiles.camera.edit_cutsom_image_preferences.md new file mode 100644 index 00000000..6ab374c3 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.camera.edit_cutsom_image_preferences.md @@ -0,0 +1 @@ +After enabling custom image preferences and saving your profile, your camera settings can be changed from the Octolapse tab. Under the **Timelapse Preview** display select your camera from the drop down box. Now click the edit pen that appears to the right of the drop down box. If the pen is not there, make sure custom image preferences are enabled for the selected camera (be sure to save your changes). diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.external_camera_snapshot_script.md b/octoprint_octolapse/static/docs/help/profiles.camera.external_camera_snapshot_script.md index 90e8dfb5..0534427b 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.external_camera_snapshot_script.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.external_camera_snapshot_script.md @@ -9,3 +9,5 @@ Snapshot Directory - The path to the current camera's snapshot folder for the cu Snapshot File Name - The expected file name of the snapshot after it has been taken Snapshot Full Path - the full path and filename of the expected snapshot. If this file exists after the script has returned, Octolapse will treat it like any other snapshot, and will apply transposition, record metadata (for rendering overlays), and will generate a thumbnail that will be sent to the client. ``` + +[See this beta guide](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Configuring-an-External-Camera) for setting up a DSLR camera on Linux using [gPhoto2](http://gphoto.org/). I have also gotten this to work on Windows using [digiCamControl](http://digicamcontrol.com/), but have not finished up the guide for that part yet. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.on_after_render_script.md b/octoprint_octolapse/static/docs/help/profiles.camera.on_after_render_script.md index 155fb869..c5c9aaed 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.on_after_render_script.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.on_after_render_script.md @@ -1,15 +1,15 @@ This is called after rendering is completed successfully (not on error), but before any prepossessed images are deleted and before the rendered timelapse is optionally moved into the OctoPrint timelapse folder. This has not been tested too much because I haven't had a use for it yet. It was requested by a user who wanted to move completed timelapses to another server after rendering. +The *Test* button will run this script with the **Snapshot Timeout**, but during a live print there is no timeout. This is to prevent scripts from running for an unusual amount of time when testing. + Parameters: * Camera Name - The name of the camera profile used to generate the images * Snapshot Directory - The path to the current camera's snapshot folder for the current print job. This directory may not exist, so be sure to create it before using the path! -* Snapshot File Name Template - A template that can be used to format the filenames so that ffmpeg can turn them into a timelapse. -* Snapshot Full Path Template - Combines the snapshot directory and the file name template. +* Snapshot File Name Template - A template that can be used to format the filenames so that ffmpeg can turn them into a timelapse. +* Snapshot Full Path Template - Combines the snapshot directory and the file name template. * Timelapse Output Directory - The directory containing the final rendered timelapse. * Timelapse Output Filename - The final timelapse file name without extension * Timelapse Extension - The output extension of the rendered timelapse. * Timelapse Full Path - The current location of any rendered timelapse. -* Synchronization Directory - The path to OctoPrint's Timelapse Folder. -* Synchronization Full Path - The full path to OctoPrint's Timelapse Folder. This path is guaranteed to be unique. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.on_after_snapshot_gcode.md b/octoprint_octolapse/static/docs/help/profiles.camera.on_after_snapshot_gcode.md new file mode 100644 index 00000000..eed74f40 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.camera.on_after_snapshot_gcode.md @@ -0,0 +1,3 @@ +This gcode script will be executed before any other *After Snapshot* bash/batch scripts. Octolapse will wait for each script to complete for a given camera before continuing. The order of each script's execution relative to other cameras, however, is unpredictable. + +Scripts can be multi-line, can contain comments, and will be sent in order. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.on_after_snapshot_script.md b/octoprint_octolapse/static/docs/help/profiles.camera.on_after_snapshot_script.md index a12d6949..8036b306 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.on_after_snapshot_script.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.on_after_snapshot_script.md @@ -1,5 +1,7 @@ This script is executed after ALL cameras have completed (or failed) taking a snapshot. The parameters are the same as for the Before Snapshot Script +The *Test* button will run this script with the **Snapshot Timeout**, but during a live print there is no timeout. This is to prevent scripts from running for an unusual amount of time when testing. + * Snapshot Number - an integer value that increments after each successful snapshot * Delay Seconds - the camera delay in seconds * Data Directory - the path to the Octolapse data folder diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.on_before_render_script.md b/octoprint_octolapse/static/docs/help/profiles.camera.on_before_render_script.md index 86e69150..fbbd529a 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.on_before_render_script.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.on_before_render_script.md @@ -1,11 +1,13 @@ This script is executed before rendering begins for this camera. This script runs with no timeout, so make sure it returns, else your rendering will never complete. It will only run if rendering is enabled, and will run even if there are no snapshots at the proper location to generate a timelapse. I used this script to download all images from my DSLR in order to reduce the amount of time it took to take snapshots (reduced it by more than half) for high res images. It could be used to apply custom image filters, and a whole lot more. Please let me know what you are using this for! +The *Test* button will run this script with the **Snapshot Timeout**, but during a live print there is no timeout. This is to prevent scripts from running for an unusual amount of time when testing. + Parameters: * Camera Name - The name of the camera profile used to generate the images * Snapshot Directory - The path to the current camera's snapshot folder for the current print job. This directory may not exist, so be sure to create it before using the path! -* Snapshot File Name Template - A template that can be used to format the filenames so that ffmpeg can turn them into a timelapse. -* Snapshot Full Path Template - Combines the snapshot directory and the file name template. +* Snapshot File Name Template - A template that can be used to format the filenames so that ffmpeg can turn them into a timelapse. +* Snapshot Full Path Template - Combines the snapshot directory and the file name template. Here is a bash script that I used to move and rename all of the images that I downloaded from my DSLR in order to render a timelapse from them: ``` diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.on_before_snapshot_gcode.md b/octoprint_octolapse/static/docs/help/profiles.camera.on_before_snapshot_gcode.md new file mode 100644 index 00000000..d64b3577 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.camera.on_before_snapshot_gcode.md @@ -0,0 +1,3 @@ +This gcode script will be executed before any other *Before Snapshot* bash/batch scripts. Octolapse will wait for each script to complete for a given camera before continuing. The order of each script's execution relative to other cameras, however, is unpredictable. + +Scripts can be multi-line, can contain comments, and will be sent in order. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.on_before_snapshot_script.md b/octoprint_octolapse/static/docs/help/profiles.camera.on_before_snapshot_script.md index bc7c4d02..1f0254e7 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.on_before_snapshot_script.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.on_before_snapshot_script.md @@ -1,5 +1,7 @@ This script will be called after the printer has stabilized, but before any snapshots are taken. It might be useful to turn on lighting, to send a notification, or other things I haven't even thought of. +The *Test* button will run this script with the **Snapshot Timeout**, but during a live print there is no timeout. This is to prevent scripts from running for an unusual amount of time when testing. + The following parameters, in order, are currently supplied to this script: * Snapshot Number - an integer value that increments after each successful snapshot diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.on_print_end_script.md b/octoprint_octolapse/static/docs/help/profiles.camera.on_print_end_script.md new file mode 100644 index 00000000..986d015c --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.camera.on_print_end_script.md @@ -0,0 +1,7 @@ +This is called after printing is completed or fails. + +The *Test* button will run this script with the **Snapshot Timeout**, but during a live print there is no timeout. This is to prevent scripts from running for an unusual amount of time when testing. + +Parameters: + +* Camera Name - The name of the camera profile used to generate the images diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.on_print_start_script.md b/octoprint_octolapse/static/docs/help/profiles.camera.on_print_start_script.md index d6f3d5a2..1b24ac64 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.on_print_start_script.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.on_print_start_script.md @@ -1 +1,7 @@ This script can be used to prepare your camera before any printing starts. I used this script to erase all of the existing images on my DSLR and to adjust the settings. It could be used for a variety of purposes. Currently only one parameter is passed into the script, the camera's name, but only because I'm not sure what might be useful here. Let me know what arguments would be useful and I'll consider adding them. + +The *Test* button will run this script with the **Snapshot Timeout**, but during a live print there is no timeout. This is to prevent scripts from running for an unusual amount of time when testing. + +Parameters: + +* Camera Name - The name of the camera profile used to generate the images diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.stream_template.md b/octoprint_octolapse/static/docs/help/profiles.camera.stream_template.md index b3db4855..4ddb14a4 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.stream_template.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.stream_template.md @@ -1,5 +1,8 @@ This setting is used to create a live camera stream for adjusting webcam settings. The format is generally a bit different than the snapshot address since the url is used by your browser to connect to the camera stream. -The default value is ```/webcam/?action=stream```, and should work fine as long as you are using the default OctoPi installation (using mjpg_streamer), and your camera is connected directly to the raspberry pi that is running Octoprint. You can also use the full stream address here, but the exact address depends on your raspberry pi's IP address, and/or your DNS settings. For example, if your IP address is 192.168.0.100, and you are using OctoPi + mjpg_streamer, you could use this value: ```http://192.168.0.100/webcam/?action=stream``` +The default value is ```/webcam/?action=stream```, which is a relative address. This is translated into a full url by your browser. For example, octoprint is running on http://192.168.1.100, this relative URL will be translated to ```http://192.168.1.100/webcam/?action=stream```. You could also use the full URL, but it will stop working if your IP address changes. + +The default address should work fine as long as you are using the default OctoPi installation (using mjpg_streamer), and your camera is connected directly to the raspberry pi that is running Octoprint. + +If you are using an external webcam server, use whatever URL you use to view the camera stream within the browser and paste it here. If it works in your browser it will likely work in Octolapse (but not necessarily). -However, it's possible that your IP address will change over time, so it's recommended that you use the default address, which is independant of your IP address. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.address.md b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.address.md index 64c279a2..2c7c53ab 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.address.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.address.md @@ -1,4 +1,4 @@ -This is the full path to the web camera. This address can be used in the Snapshot Address Template and the Camera Settings Request Templates by using the {camera_address} replacement token. +This is the full path to the web camera. This address can be used in the Snapshot Address and the Camera Settings Request Templates by using the {camera_address} replacement token. The initial setting (after install or after you restore the default settings) is based on the OctoPrint settings within the 'Webcam & Timelapse' screen. The OctoPrint default is (I believe) ```http://127.0.0.1:8080/``` diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.apply_settings_before_print.md b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.apply_settings_before_print.md deleted file mode 100644 index 375ce015..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.apply_settings_before_print.md +++ /dev/null @@ -1,9 +0,0 @@ -When enabled, Octolapse will apply all of your custom image preferences at the start of each print. This ensures that your camera settings are consistent for every print. - -As of Octolapse V0.3.4, Custom Image Preferences are only supported if you are using a webcam stream with mjpg_streamer. Octolapse will NOT let you enable these features unless it detects that the camera is running from mjpg_streamer and that control.htm is accessible (read the notes below to see how to do this). - -### Required octopi.txt changes to use custom image preferences - -_**these changes are required in order to use the custom image preferences**_ - -If you are using octopi, to use the custom image preferences you'll need to make a small adjustment to your boot/octopi.txt file. [See this troubleshooting guide](https://github.com/FormerLurker/Octolapse/wiki/Troubleshooting#why-cant-i-change-contrast-zoom-focus-etc) to enable custom printer settings by allowing access to webcam/control.htm. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.webcam.enable_custom_image_preferences.md b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.enable_custom_image_preferences.md similarity index 68% rename from octoprint_octolapse/static/docs/help/profiles.camera.webcam.enable_custom_image_preferences.md rename to octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.enable_custom_image_preferences.md index 82f82089..25eb548b 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.webcam.enable_custom_image_preferences.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.enable_custom_image_preferences.md @@ -1,12 +1,12 @@ -Enabling custom image preferences will allow you to edit many of your webcam settings (if they are supported) like contrast, zoom, focus, etc. +Enabling custom image preferences will allow you to edit many of your webcam settings (if they are supported) like contrast, zoom, focus, etc. As of Octolapse V0.3.4, Custom Image Preferences are only supported if you are using a webcam stream with mjpg_streamer. -## Required boot/octopi.txt changes to use custom image preferences +### Required boot/octopi.txt changes to use custom image preferences _**these changes are required in order to use the custom image preferences**_ -In recent versions of Octopi, the default mjpegstreamer control.htm page is disabled. In order to automatically adjust camera settings, Octolapse needs access to control.htm. +In recent versions of Octopi, the default mjpegstreamer control.htm page is disabled. In order to automatically adjust camera settings, Octolapse needs access to control.htm. To enable access you must connect to a terminal to access your raspberry pi. Then enter the following command to edit your octopi.txt file: ``` @@ -42,13 +42,39 @@ camera_http_webroot="./www" camera_http_options="" ``` -## Required etc/modules and boot/octopi.txt changes for Raspberry Pi Camera Module +### Required etc/modules and boot/octopi.txt changes for Raspberry Pi Camera Module -Connect to a terminal window on your raspberry pi and edit the /etc/modules file with the following command: +A detailed guide for configuring the raspberry pi camera can be found [here](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Configuring-a-Raspberry-Pi-Camera). Brief instructions can be found below. + +The first step is to update your raspberry pi. If you skip this step, the camera driver may not work properly. + +Connect to a terminal window on your raspberry pi and enter the following command followed by the Enter key: + +``` +sudo apt-get update +``` + +Enter your password if you are prompted. You also might be asked to confirm the updates at some point. If so, press Y to confirm. This command may take a while to finish. + +Next we will upgrade the distribution. Enter the following command and press the Enter key. + +``` +sudo apt-get dist-upgrade +``` + +You might be asked to confirm the updates at some point. If so, press Y to confirm. + +Now reboot your pi for the update to take effect by entering the following command, followed by the Enter key: + +``` +sudo reboot +``` + +Wait for your pi to reboot (this usually takes a few minutes), then reconnect to a terminal window on your raspberry pi and edit the /etc/modules file with the following command: ```sudo nano /etc/modules``` -Add the following line to the end of the modules file: +Enter your password if prompted. After the nano editor opens, add the following line to the end of the modules file: ```bcm2835-v4l2``` @@ -94,7 +120,7 @@ camera="usb" ``` Make sure the line that says ```camera=usb``` is NOT commented out. It must not start with a #! -Next find the section that looks similar to this +Next find the section that looks similar to this ``` ### Additional options to supply to MJPG Streamer for the USB camera # diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.mjpg-streamer.options.9963778.md b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.mjpg-streamer.options.9963778.md index dbef77b2..2576f2ae 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.mjpg-streamer.options.9963778.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.mjpg-streamer.options.9963778.md @@ -1 +1 @@ -Adjust the sharpness of the webcam image. Lower values have less sharpness, higher values have more. +Adjust the saturation of the webcam image. Lower values have less saturation, higher values have more. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.mjpg-streamer.options.9963803.md b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.mjpg-streamer.options.9963803.md index 2576f2ae..250b2915 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.mjpg-streamer.options.9963803.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.mjpg-streamer.options.9963803.md @@ -1 +1 @@ -Adjust the saturation of the webcam image. Lower values have less saturation, higher values have more. +Adjust the sharpness of the webcam image. Lower values have less sharpness, higher values have more. \ No newline at end of file diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.snapshot_request_template.md b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.snapshot_request_template.md index ab91f712..6615d6b6 100644 --- a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.snapshot_request_template.md +++ b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.snapshot_request_template.md @@ -3,3 +3,5 @@ You can either enter the full path for your webcam's snapshot page, or you can u The default value is ```{camera_address}?action=snapshot```, which works well for mjpg_streamer (bundled with octopi). If you are using Yawcam you probably want to use ```{camera_address}out.jpg``` The default full url after replacing the {camera_address} token with the default 'Base Address' above would be would be ```http://127.0.0.1:8080/?action=snapshot``` for mjpg_streamer and ```http://127.0.0.1:8888/out.jpg``` for Yawcam. + +If the 'Test' button does not work, you can also enter a full snapshot url here. If the URL works in a browser window and returns a JPEG image, it will likely work in Octolapse. diff --git a/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.stream_download.md b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.stream_download.md new file mode 100644 index 00000000..e1401173 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.camera.webcam_settings.stream_download.md @@ -0,0 +1,7 @@ +**Warning: This is an experimental feature. You might experience unexpected errors when enabling this setting.** + +When stream download is enabled, octolapse will acquire one packet of data from the webcam and will then continue to process the gcode file while downloading the image in the background. For large images, this can substantially reduce the time it takes to acquire an image, reducing print time and increasing quality, especially when using the smart trigger with snap-to-print enabled. + +However, it is also possible that Octolapse will acquire an old image, resulting in a jittery timelapse, where some snapshots come from a previous frame. However, if you are primarily concerned with print quality and are less interested in timelapse quality, you may want to consider enabling this feature. + +Note that this setting only affects webcam download, and not external or gcode camera types. diff --git a/octoprint_octolapse/static/docs/help/profiles.debug.is_test_mode.md b/octoprint_octolapse/static/docs/help/profiles.debug.is_test_mode.md deleted file mode 100644 index 51ebfc87..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.debug.is_test_mode.md +++ /dev/null @@ -1,7 +0,0 @@ -Test mode can be used to try out timelapse settings without printing anything. Test mode must be activated BEFORE starting a print. Changing this setting mid-print won't alter any current prints. - -1. Navigate to the Debug tab and select 'Test Mode'. Note: You shouldn't have to use any of the profiles marked 'Diagnostic' unless you suspect a bug. -2. UNLOAD YOUR FILAMENT! Even though I attempted to strip all extruder commands, there's a chance that I missed some cases. Please let me know if you see any 'cold extrusion prevented' errors in the terminal window. If you do, please send me your GCode! -3. Select a local GCode file and click print. - -Notes: Pay attention to your printer temps during the test print. Everything should remain cool! If not, please let me know, send me the GCode, and I'll try to remedy the situation. See the known issues for details. diff --git a/octoprint_octolapse/static/docs/help/profiles.debug.enabled.md b/octoprint_octolapse/static/docs/help/profiles.logging.enabled.md similarity index 100% rename from octoprint_octolapse/static/docs/help/profiles.debug.enabled.md rename to octoprint_octolapse/static/docs/help/profiles.logging.enabled.md diff --git a/octoprint_octolapse/static/docs/help/profiles.debug.log_all_errors.md b/octoprint_octolapse/static/docs/help/profiles.logging.log_all_errors.md similarity index 100% rename from octoprint_octolapse/static/docs/help/profiles.debug.log_all_errors.md rename to octoprint_octolapse/static/docs/help/profiles.logging.log_all_errors.md diff --git a/octoprint_octolapse/static/docs/help/profiles.debug.log_to_console.md b/octoprint_octolapse/static/docs/help/profiles.logging.log_to_console.md similarity index 100% rename from octoprint_octolapse/static/docs/help/profiles.debug.log_to_console.md rename to octoprint_octolapse/static/docs/help/profiles.logging.log_to_console.md diff --git a/octoprint_octolapse/static/docs/help/profiles.logging.logging_level.md b/octoprint_octolapse/static/docs/help/profiles.logging.logging_level.md new file mode 100644 index 00000000..81bdff6d --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.logging.logging_level.md @@ -0,0 +1,10 @@ +Octolapse has six logging levels, listed below in order from more to less logging: + +* **Verbose** - Logs absolutely everything. Can result in VERY large log files for some modules, but provides the most information. +* **Debug** - A good level to use if you are trying to debug any problems. In general, it will provide a reasonable amount of data to debug a problem without overwhelming the logfile. +* **Info** - Provides some general information about what is going on in Octolapse, but not enough to create large logs through normal usage. +* **Warning** - Shows potential problems that are not usually reported to the UI. +* **Error** - Only logs errors and exceptions. Most errors/exceptions indicate a problem, so this is a good level to use for general printing. Note that the debug profile contains a shortcut for logging all modules at the **Error** level. See the **Log All Errors** setting for more info. +* **Critical** - A critical error generally will affect system stability. These can be out-of-memory or diskspace errors that have the potential to crash the system. Not all critical errors can be logged, and may be logged by OctoPrint itself. + +Note that when a given log level is selected, you will not only receive logs from that level, but from all levels below. For example, if you use **Debug** logging, you will also be logging Info, Warning, Error AND Critical log messages. Similarly, if you log at the **Error** level, you will also get Critical log messages. diff --git a/octoprint_octolapse/static/docs/help/profiles.logging.manage_log_files.md b/octoprint_octolapse/static/docs/help/profiles.logging.manage_log_files.md new file mode 100644 index 00000000..b3ff5013 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.logging.manage_log_files.md @@ -0,0 +1,13 @@ +#### Log Files in Octolapse + +Octolapse stores one current logfile, named plugin_octolapse.log. Once a day the current log file gets backed up with a date extension (for example, plugin_octolapse.log.2020-01-01). Octolapse keeps up to three logfile backups, and will delete any older backup logs. + +#### Clear Log + +This button allows you to clear the current log file, which is very useful if you are trying to create log data for a specific issue, but your log file is already huge. This button does not actually erase the current log, but rather rolls the current log data into another backup log file, erasing any data in the current backup if one exists. + +#### Clear All Logs +This button will clear the current logfile, just like the **Clear Log** button, but will then delete any backup logs. This is useful for freeing up space if your logfiles are huge. + +#### Download Log +This button will download the most recent logfile (plugin_octolapse.log). If you want an older logfile (remember, Octolapse stores up to three backup log files), navigate to Octoprint Settings (wrench/spanner icon)->Logs and look for plugin_octolapse.log.DATE_GOES_HERE). diff --git a/octoprint_octolapse/static/docs/help/profiles.logging.modules_to_log.md b/octoprint_octolapse/static/docs/help/profiles.logging.modules_to_log.md new file mode 100644 index 00000000..1dbfe6cc --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.logging.modules_to_log.md @@ -0,0 +1,24 @@ +The Octolapse logger is module based, allowing you to log only the data you need. Logfiles can get huge, and logging more than you need can make the log file hard to read. + +Octolapse has many modules, and each has a different function. Every module starts with ```octolapse.```, which has been omitted from the list below, where you will find a brief description of each module: + +* **\_\_init\_\_** - This is the core Octolapse module. It handles web requests, OctoPrint events, and client notifications. +* **camera** - This module includes functionality to test webcams and apply webcam image preferences (brightness, focus, etc). +* **gcode_commands** - This module contains information necessary to enable test mode, which strips extrusion and temperature related commands from the gcode stream, allowing you to test Octolapse settings without waiting for you bed to warm up, or wasting filament. +* **gcode_parser** - Part of the GcodePositionProcessor (c++ library) responsible for parsing gcode. All gcode parsing in Octolapse uses this module. +* **gcode_position** - Part of the GcodePositionProcessor (c++ library) responsible for processing gcode and tracking your printer's current state, including position and extruders. +* **gcode_processor** - This module is a wrapper for Octolapse's custom parser/position processor (GcodePositionProcessor written in c++). +* **messenger_worker** - Sends rate limited push notifications to the client. +* **position** - This module has been mostly replaced by the GcodePositionProcessor (see gcode_processor above), but contains some functionality mostly to support the classic triggers (gcode, timer and layer) that has not yet been added to the c++ replacement. Eventually this module will go away. It currently implements custom trigger position restrictions, extruder trigger requirements, and some utility functions for sending position/state information to the client. +* **render** - This module is responsible for rendering snapshots into a video. +* **settings** - Contains all Octolapse configuration data definitions. Is responsible for loading, saving and updating all settings, including default and client data. +* **settings_external** - Provides access to the octolapse profile repository, including import and update capability. This module is used to import profiles from the profile settings pages and to perform updates when newer profiles are available. +* **migration** - Updates the settings for older versions of Octolapse right after installation, or when importing older settings into Octolapse. +* **settings_preprocessor** - Extracts slicer settings from gcode files to support the **Automatic Slicer Settings** option in your Octolapse printer profile. +* **snapshot** - Responsible for taking snapshots for webcams, external script cameras (DSLR), and gcode cameras (built into printer). It also executes any before/after snapshot scripts in your camera profiles. +* **snapshot_plan** - Part of the GcodePositionProcessor (c++ library) responsible for creating snapshot plans for the **Smart** triggers. +* **stabilization_gcode** - Creates the gcode Octolapse uses to stabilize your extruder. It is capable of reading snapshot plans produced by the stabilization preprocessor, or creating a snapshot plan from the current position. +* **stabilization_preprocessing** - Communicates with the GcodePositionProcessor in order to preprocess your gcode file when using any of the **Smart** triggers. It provides updates to the UI showing the preprocessing progress, and returns a list of snapshot plans returned by the stabilization. These plans are used to determine when Octolapse will take a snapshot. +* **timelapse** - This module is responsible for watching incoming and sent gcode commands, communicating with the printer, executing stabilizations, triggering snapshots, starting rendering, and a whole lot more. Most modules in Octolapse are written to support this module. It is the heart of the program (and a bit messy). WARNING - Logging this module at the Debug or Verbose log level, though sometimes necessary, will create massive log files. +* **trigger** - Controls the three classic triggers - gcode, timer, and layer. This module may be retired at some point. + diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.abort_out_of_bounds.md b/octoprint_octolapse/static/docs/help/profiles.printer.abort_out_of_bounds.md deleted file mode 100644 index 6bcc4c85..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.printer.abort_out_of_bounds.md +++ /dev/null @@ -1 +0,0 @@ -When this is enabled, octolapse will skip any snapshots that put the extruder or bed in an out of bounds position. When disabled, Octolapse will use the nearest in-bounds position. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.custom_bounding_box.md b/octoprint_octolapse/static/docs/help/profiles.printer.custom_bounding_box.md new file mode 100644 index 00000000..20784d9b --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.printer.custom_bounding_box.md @@ -0,0 +1,3 @@ +Some printer have different coordinates than their build width/length/height would imply. It is important that Octolapse understand your exact build volume coordinate system in order to properly move your extruder during stabilization, and to prevent Z-Lifting above the printable area. + +For example, the Prusa MK2 has a build volume of 250x210x200, but the Y axis includes a 3mm space reserved for priming. The actual Y axis is 213 MM wide, where Y is between -3 and 210. For this reason a custom bounding box is necessary for these printers. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.default_extruder.md b/octoprint_octolapse/static/docs/help/profiles.printer.default_extruder.md new file mode 100644 index 00000000..34555332 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.printer.default_extruder.md @@ -0,0 +1,3 @@ +This extruder will be used if no other extruder has been set with a T command. This option is only available if the **Number of Extruders** setting is 2 or more. + +**Important note to users of the Prusa MMU printers** - If you are printing in single material mode, make sure you select the extruder you plan to print before every print UNLESS you are using Slic3rPE or PrusaSlicer AND are using 'Automatic Configuration' for your slicer type. If you do not do this, Octolapse may use settings from the wrong extruder, which can degrade print quality. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.extruder_offsets.md b/octoprint_octolapse/static/docs/help/profiles.printer.extruder_offsets.md new file mode 100644 index 00000000..cb198136 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.printer.extruder_offsets.md @@ -0,0 +1,8 @@ +If your printer has more than one nozzle, and you have firmware offsets for each nozzle stored in your firmware, you will need to configure this in the Octolapse printer profile. Octolapse uses these offsets to determine where your printhead is over the bed, which taking the position of each extruder into account. + +Check your slicer settings and make sure the offsets aren't entered in there first. In general if your slicer is adjusting the gcode to account for the offsets, you can leave all of the offsets in Octolapse at 0. + +Octolapse can read the offsets from certain gcodes (G10 and M218), but ONLY if these gcodes appear in your gcode file for every extruder. I recommend manually entering the offsets if at all possible just in case. + +Note that if your printer has a single nozzle but supports multiple materials (like the Prusa MMU), you should check the **Shared Extruder** option. After that no extruder offsets will need to be entered. + diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.num_extruders.md b/octoprint_octolapse/static/docs/help/profiles.printer.num_extruders.md new file mode 100644 index 00000000..bb2ea4cb --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.printer.num_extruders.md @@ -0,0 +1,3 @@ +For machines with multiple extruders OR machines with a shared extruder that supports multiple materials (like the Prusa MMU), you will need to set the number of extruders equal to the number of nozzles or materials. + +Users with a single extruder that have a filament fuser (like the Mosaic Palette) should set this to 1. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.printer_profile_snapshot_command.md b/octoprint_octolapse/static/docs/help/profiles.printer.printer_profile_snapshot_command.md deleted file mode 100644 index 83e17aab..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.printer.printer_profile_snapshot_command.md +++ /dev/null @@ -1,3 +0,0 @@ -When GCode Triggering is enabled (snapshot settings) This command will be detected by Octolapse. When it appears in the gcode file, a snapshot will be taken (depending on the gcode trigger settings). - -Octolapse will automatically prevent the snapshot gcode from being sent to the printer while it is running. By default, it will remove the snapshot gcode EVEN WHILE OCTOLAPSE IS DISABLED to prevent snapshot commands from mistakenly being sent to the printer. See the main settings for this optional setting that can be used to prevent all snapshot commands from being sent to the printer as long as Octolapse is installed. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.shared_extruder.md b/octoprint_octolapse/static/docs/help/profiles.printer.shared_extruder.md new file mode 100644 index 00000000..3047c577 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.printer.shared_extruder.md @@ -0,0 +1 @@ +Machines that have a single nozzle but support multiple filaments should check this box. This includes printers like Prusa's MMU1 and MMU2. If you have more than one physical nozzle, uncheck this box. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.slicer_type.md b/octoprint_octolapse/static/docs/help/profiles.printer.slicer_type.md index f84b3413..7ab18b55 100644 --- a/octoprint_octolapse/static/docs/help/profiles.printer.slicer_type.md +++ b/octoprint_octolapse/static/docs/help/profiles.printer.slicer_type.md @@ -1,18 +1,17 @@ ### Slicer Type Octolapse will be easier to configure if you select the slicer you are using. You have the following options: -* Automatic Configuration -* Cura +* Automatic Configuration - This is the recommended setting +* Cura V4.1 and Below +* Cura V4.2 and Above * Slic3r Prusa Edition * Simplify 3D * Other Slicer (generic slicer) + #### Automatic Configuration -Octolapse will attempt to automatically extract your slicer settings. This should work out of the box for Slic3r PE, Prusa Slicer, and Simplify 3D. If you are using Cura, you will need to insert a settings template into your start or end gcode. -#### Cura Settings -[See this link](https://github.com/FormerLurker/Octolapse/wiki/Printer-Profiles---Cura-Settings) for info on configuring Octolapse for use with Cura. -#### Simplify 3D Settings -[See this link](https://github.com/FormerLurker/Octolapse/wiki/Printer-Profiles---Simplify-3D-Settings) for info on configuring Octolapse for use with Simplify 3D. -#### Slic3r Prusa Edition -[See this link](https://github.com/FormerLurker/Octolapse/wiki/Printer-Profiles---Slic3r-Prusa-Edition-Settings) for info on configuring Octolapse for use with Slic3r Prusa Edition. +Octolapse will attempt to automatically extract your slicer settings. Currently Cura, Slic3r PE, Prusa Slicer, and Simplify 3D are supported. [See this guilde](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Automatic-Slicer-Configuration) for information on how to enable automatic slicer configuration. + +**Important Notes**: Cura requires a script to be added to the start gcode for **Automatic Configuration** to work. Additionally, automatic settings extraction will NOT work when using multi-material/extruder mode in Slic3r/Slic3rPE/PrusaSlicer. Finally, Multi-Material/Multi-Extruder support is beta, and should be used with caution. + #### Other Slicer Types -For all other slicers, [See this link](https://github.com/FormerLurker/Octolapse/wiki/Printer-Profiles---Other-Slicer-Settings) for info on configuring Octolapse with a generic slicer. +For all other slicers, [See this link](https://github.com/FormerLurker/Octolapse/wiki/v0.4---Creating-And-Configuring-Your-Printer-Profile#slicer-settings) for info on configuring Octolapse with a generic slicer. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.slicers.cura.cura_surface_mode.md b/octoprint_octolapse/static/docs/help/profiles.printer.slicers.cura.cura_surface_mode.md deleted file mode 100644 index ccf539e6..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.printer.slicers.cura.cura_surface_mode.md +++ /dev/null @@ -1 +0,0 @@ -This setting is used in conjunction with 'Spiralize Outer Contour' to detect 'Vase Mode' in Cura. Vase mode is detected ONLY if this setting = 'Surface' and 'Spiralize Outer Contour' is enabled. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.slicers.cura.smooth_spiralized_contours.md b/octoprint_octolapse/static/docs/help/profiles.printer.slicers.cura.smooth_spiralized_contours.md index 1c564afc..9fb42038 100644 --- a/octoprint_octolapse/static/docs/help/profiles.printer.slicers.cura.smooth_spiralized_contours.md +++ b/octoprint_octolapse/static/docs/help/profiles.printer.slicers.cura.smooth_spiralized_contours.md @@ -1 +1 @@ -This setting is used in conjunction with 'Surface Mode' to detect 'Vase Mode' in Cura. Vase mode is detected ONLY if this setting is enabled and 'Surface Mode' = 'Surface'. +This setting is used to detect 'Vase Mode' in Cura. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.slicers.other.slicer_other_slicer_layer_height.md b/octoprint_octolapse/static/docs/help/profiles.printer.slicers.other.layer_height.md similarity index 100% rename from octoprint_octolapse/static/docs/help/profiles.printer.slicers.other.slicer_other_slicer_layer_height.md rename to octoprint_octolapse/static/docs/help/profiles.printer.slicers.other.layer_height.md diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.slicers.other.retraction_enable.md b/octoprint_octolapse/static/docs/help/profiles.printer.slicers.other.retract_before_move.md similarity index 100% rename from octoprint_octolapse/static/docs/help/profiles.printer.slicers.other.retraction_enable.md rename to octoprint_octolapse/static/docs/help/profiles.printer.slicers.other.retract_before_move.md diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.snapshot_command.md b/octoprint_octolapse/static/docs/help/profiles.printer.snapshot_command.md new file mode 100644 index 00000000..bef7307d --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.printer.snapshot_command.md @@ -0,0 +1,29 @@ +When using either the **Gcode Trigger** or the **Smart Gcode Trigger**, Octolapse will take a snapshot when it encounters a snapshot command within your gcode file. Since Octolapse v0.4, the default snapshot command is ```@OCTOLAPSE TAKE-SNAPSHOT```, and this command can always be used to trigger a snapshot. However, Octolapse allows you to use any custom command that you choose to initiate a snapshot, with some minor limitations. The command must include at least one non-whitespace character, and may include a comment if you desire. + +**Warning:** Do **NOT** use a command that is required by your printer as the alternative snapshot command, because it WILL NOT be sent to the printer. For example, do NOT EVER use ```G28```, ```G90```, or any similar commands. This **WILL** cause major problems. + +**Why would I not just use default snapshot command ```@OCTOLAPSE TAKE-SNAPSHOT``` to initiate a snapshot?** + +Great question! Though the default command will always work when printing from OctoPrint, and will NEVER be sent to your printer by OctoPrint, some printers with misbehaving firmware will not handle unknown gcodes when printing direct from the printer (via internal SD reader). For this reason some people choose to use other gocdes to trigger a snapshot, like ```G4 P1```. That command would normally would tell the printer to wait for 0 Milliseconds, so it effectively does nothing, and will not alter the print in any way, even if you're printing straight off of your printer's SD card. + +However, in my experience it is pretty unusual that a printer would malfunction when encountering the default snapshot command (```@OCTOLAPSE TAKE-SNAPSHOT```). If you have a printer that misbehaves, consider using ```G4 P1``` as your alternative snapshot command. Better yet, just upgrade your firmware with one that is a bit more tolerant. + +**A note about comments and case:** + +Snapshot commands are NOT case sensitive in Octolapse V0.4+. Please note that all comments are stripped from the alternative snapshot command while Octolapse is running. For example, if you use + +```SNAP ; THIS IS A SNAPSHOT``` + +as your snapshot command, Octolapse will strip the comment and will trigger on any occurrence of + +```SNAP``` + +Since comments are ignored in OctoPrint, Octolapse would also trigger if the following line appears within your gcode file: + +```SNAP ; NOTICE HOW THIS IS A DIFFERENT COMMENT!``` + +In other words, comments are ignored both within the alternative snapshot command, and within the gcode file. + +**Will the snapshot command be sent to the printer?** + +Octolapse will automatically prevent both the default snapshot command and the alternative snapshot command from being sent to the printer while it is running. By default, it will remove the snapshot gcode EVEN WHILE OCTOLAPSE IS DISABLED to prevent snapshot commands from mistakenly being sent to the printer. See the main settings for this optional setting that can be used to prevent all snapshot commands from being sent to the printer as long as Octolapse is installed. diff --git a/octoprint_octolapse/static/docs/help/profiles.printer.zero_based_extruder.md b/octoprint_octolapse/static/docs/help/profiles.printer.zero_based_extruder.md new file mode 100644 index 00000000..4eb79816 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.printer.zero_based_extruder.md @@ -0,0 +1 @@ +Some firmware uses the T1 gcode command for the first extruder. Other firmware uses T0 as the first extruder. If your firmware uses T0 to select the first tool, check this box. diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.archive_snapshots.md b/octoprint_octolapse/static/docs/help/profiles.rendering.archive_snapshots.md new file mode 100644 index 00000000..1f90567e --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.rendering.archive_snapshots.md @@ -0,0 +1,13 @@ +Add all snapshots to an archive after rendering. On OctoPi these snapshots will be located in: +``` +/home/pi/.octoprint/data/octolapse/snapshot_archive/ +``` +On windows it will be located in: +``` +{user_folder}\AppData\Roaming\OctoPrint\data\octolapse\snapshot_archive\ +``` +Note that the user folder can change depending on the system. On mine, it's ```c:\users\USER_NAME_HERE```. Also, the ```AppData``` folder is often hidden, so make sure 'View Hidden Items' is enabled inside of your user folder. + +I hope to add a file browser soon so that searching for the directory won't be a problem. + +**WARNING** - This will take a lot of space. Use caution, and be sure to purge your snapshots often. diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.cleanup_after_render_complete.md b/octoprint_octolapse/static/docs/help/profiles.rendering.cleanup_after_render_complete.md deleted file mode 100644 index 0fb16957..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.rendering.cleanup_after_render_complete.md +++ /dev/null @@ -1,3 +0,0 @@ -When this is enabled, Octolapse will delete ALL snapshots after a successful render operation. This applies to every camera used where a timelapse is generated. - -Warning: Snapshots NOT removed after rendering success must be deleted manually! diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.cleanup_after_render_fail.md b/octoprint_octolapse/static/docs/help/profiles.rendering.cleanup_after_render_fail.md deleted file mode 100644 index 570613e2..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.rendering.cleanup_after_render_fail.md +++ /dev/null @@ -1,3 +0,0 @@ -When this is enabled, Octolapse will delete ALL snapshots after rendering has failed. This applies to every camera used where a timelapse is generated. - -Warning: Snapshots NOT removed after rendering failure must be deleted manually! diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.constant_rate_factor.md b/octoprint_octolapse/static/docs/help/profiles.rendering.constant_rate_factor.md new file mode 100644 index 00000000..192f18f6 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.rendering.constant_rate_factor.md @@ -0,0 +1,5 @@ +CRF (Constant Rate Factor) scales the video quality and compression, and ranges from 0 to 51. A value of 0 yields lossless encoding (highest possible quality), but produces a huge file. A value of 51 would result in the smallest file size with the lowest quality. The default value of 28. A good rule of thumb is that decreasing the CRF by 6 will approximately double the file size, while increasing it by 6 will cut the file size roughly in half. + +Currently this setting is only supported for H265, a High Efficiency Video Coding (HVEC) format since manually specifying the bitrate for single pass encoding is not recommended. + + diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.output_format.md b/octoprint_octolapse/static/docs/help/profiles.rendering.output_format.md index 02099c6b..ce931ef1 100644 --- a/octoprint_octolapse/static/docs/help/profiles.rendering.output_format.md +++ b/octoprint_octolapse/static/docs/help/profiles.rendering.output_format.md @@ -1,3 +1,8 @@ Choose the output video format. Several are available, including mp4, gif, mpeg, -Note that H.264 mp4 codec seems to have problems with very high resolution images (DSLR), which crash the rendering process. H.264 has many advantages over other formats, including higher quality and smaller size. However, if you are using a DSLR or a 4K webcam, consider running a test print to make sure avconv can properly render your H.264 MP4 without any errors. +### Important Notes for High Resolution Videos + +The H.264 codec will most likely fail to encode or fail to play if you try to render a video with a resolution higher than 4096x2048. H.265 supports higher resolution of up to 8192×4320. Also, both of these formats require a large amount of memory to encode, so it's possible that you **will run out of memory** when using either format to encode a very high resolution video. + +### H.265 +This format is BETA, so use at your own risk. My tests have yielded mixed results. I was able to produce very high quality 4K video with this format at a CBR of 28, but playback only works on a small number of players. At a lower CBR, of 5, the resulting file was huge and did not play back properly. diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.output_template.md b/octoprint_octolapse/static/docs/help/profiles.rendering.output_template.md index 645ad2aa..749e359f 100644 --- a/octoprint_octolapse/static/docs/help/profiles.rendering.output_template.md +++ b/octoprint_octolapse/static/docs/help/profiles.rendering.output_template.md @@ -12,6 +12,7 @@ Determines the rendering file name. You can use the following replacement tokens * {PRINTSTARTTIMESTAMP} * {SNAPSHOTCOUNT} * {FPS} + * {CAMERANAME} The default template `{FAILEDFLAG}{FAILEDSEPARATOR}{GCODEFILENAME}_{PRINTENDTIME}` might render something like this for a failed print: diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_font_path.md b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_font_path.md index 8a27a4ad..041f170d 100644 --- a/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_font_path.md +++ b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_font_path.md @@ -1,6 +1,10 @@ -Select the font that will be used to generate an overlay in your timelapse. +Select the font that will be used to generate an overlay in your timelapse. One font is pre-packaged and included with Octolapse. -IMPORTANT NOTE: If you are using OctoPi, or any flavor of Linux, you need Fontconfig installed so that Octolapse can find your system fonts. +## Fonts in Windows +If you are using windows, your system fonts should also be included. Not all fonts are compatible with Pillow (used to create the overlay), so if your preview fails you might want to select a different font. + +## Fonts in Linux +IMPORTANT NOTE: If you are using OctoPi, or any flavor of Linux, you need Fontconfig installed so that Octolapse can find fonts other than the single font packaged with Octolapse. To install fontconfig: @@ -14,3 +18,6 @@ sudo apt-get update sudo apt-get install fontconfig ``` 4. reboot your system for the changes to take effect. + +## Fonts in Mac OS +I do not know how to retrieve the system fonts in Mac OS and have no way to test it currently. However, I think the pre-packaged font should still work. If you run into problems generating a text overlay with OctoPrint running on a Mac (not just your browser, but where OctoPrint is installed), please let me know. Also, if you have python code to find all/some of the system fonts, please let me know! diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_outline_width.md b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_outline_width.md index 6ff4276b..63b12f18 100644 --- a/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_outline_width.md +++ b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_outline_width.md @@ -1 +1,3 @@ Sets the width (number of outlines) of the overlay outline. Note that the Outline Color must be non-transparent, and different from the overlay text color in order to be visible. + +The method I used to add the outline is fairly primitive, but works well for smaller font widths. If you set this too high the results will not look very good. I may try to improve this in the future. diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template.md b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template.md index 1f427a17..27e4b96f 100644 --- a/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template.md +++ b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template.md @@ -1,3 +1,17 @@ -Determines the overlay text. Leave blank to disable the overlay text. You can use the following replacement tokens: {snapshot_number} , {current_time} , {time_elapsed} +Determines the overlay text. Leave blank to disable the overlay text. You can use the following replacement tokens: {snapshot_number}, {current_time}, {time_elapsed}, {layer}, {height}, {x}, {y}, {z}, {e}, {f}, {x_snapshot}, {y_snapshot}, {gcode_file}, {gcode_file_name}, {gcode_file_extension}, {print_end_state}, {current_time:"FORMAT_STRING"}, {elapsed_time:"FORMAT_STRING"} + +### Notes on token values + +x, y, z, e, and f all show the position of the extruder right before it is stabilized. x_snapshot and y_snapshot show the position after the stabilization is completed. + +x, y, z, e, x_snapshot, and y_snapshot are all in absolute coordinates. All floating point values will display with three decimals except for e and e_snapshot, which will show 5. Feedrates (f) will be shown as integers. + +The gcode_file token will return the full file name, including the extension. The gcode_file_name token will return the name without the .gcode extension, and the gcode_file_extension token will return only the extension (typically .gcode). + +The print_end_state token will return one of the following states: COMPLETED, CANCELED, DISCONNECTED, DISCONNECTING, FAILED + +For details about the {current_time:"FORMAT_STRING"} and {elapsed_time:"FORMAT_STRING"} tokens, please click on the help icons (blue question mark) next to those tokens in the rendering profile page. + +If you'd like to see a replacement token that is not listed, please create an issue on the [octolapse repository](https://github.com/FormerLurker/Octolapse/issues) to suggest more options. + -I plan to add more replacement tokens. Create an issue on the [octolapse repository](https://github.com/FormerLurker/Octolapse/issues) to suggest more options. diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template_current_time.md b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template_current_time.md new file mode 100644 index 00000000..6d8b98d5 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template_current_time.md @@ -0,0 +1,15 @@ +The {current_time} token supports an optional date format string. It will format the current time according to a string supplied with the token. + +Here are some examples if we assume the current date is January 1, 2021 at 12:00 noon: + +{current_time:"%m/%d/%Y, %H:%M:%S"} = 01/01/2021, 12:00:00 +{current_time:""%H:%M:%S"} = 12:00:00 +{current_time:""%I:%M %p"} = 12:00 PM +{current_time:"%B %e, %Y"} = January 1, 2021 +{current_time:"%c"} = Fri Jan 1 12:00:00 2021 + +Note: The exact output format will depend on your local. + +The date formatting is done with the [strftime](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior) function. You can find a list of the supported format codes [here](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes). + + diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template_time_elapsed.md b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template_time_elapsed.md new file mode 100644 index 00000000..5090ab19 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.rendering.overlay_text_template_time_elapsed.md @@ -0,0 +1,60 @@ +The {time_elapsed} token supports an optional format string. When no format string is provided, it will output in the following way: + +* elapsed time >= 1 day - X days, H:MM:SS (5 days, 4:15:05) +* elapsed time < 1 day - H:MM:SS (4:15:05) + +If you supply a format string, it will be in the following general format: + +{time_elapsed:"FORMAT_STRING_GOES_HERE"} + +Make sure you included the quotation marks after the semicolon! Do not embed any quotes inside of the format string, else it won't work properly. + +The format strings are custom since no good built in system exists for this. Here are all the supported tokens (they are case sensitive!): + +* %D = total days +* %d = day component +* %H = Total Hours +* %h = Hours Component +* %M = Total Minutes +* %m = Minutes Component +* %S = Total Seconds +* %s = Seconds Component +* %F = Total Milliseconds +* %f = Milliseconds Component + +Here are some examples if we assume the elapsed time is 5 days, 5 hours, 5 minutes and 5.123 seconds: + +{time_elapsed:"%d Days %h Hours, %m Minutes, %s Seconds, %f MS"} = 5 Days, 5 Hours, 5 Minutes, 5 Seconds, 123 MS +{time_elapsed:"%D:0.3 Total Days"} = 5.212 Total Days +{time_elapsed:"%M Minutes"} = 7505 Minutes + +You can also specify the number of decimals to display, and/or pad the string with leading zeros. This formatting style is taken from python's format function, and behaves pretty much the same. here is the format + +%{token_name}:{minimum_total_characters}.{number_of_decimal_places} + +or + +%{token_name}:.{number_of_decimal_places} (this is equivelant to %{token_name}:0.{number_of_decimal_places}) + +or + +%{token_name}:{minimum_total_characters} + +If *minimum_total_characters* is less than the actual number of total characters (including the decimal point), the number will be padded with 0s on the left. If there are more characters than are specified, all are shown. + +See the following examples assuming total minutes = 55.5555: + +* {time_elapsed:"%M:0.1"} - shows the total minutes with 1 decimal place : 55.6 +* {time_elapsed:"%M:0.0"} - Shows 56 (rounds up from 55.5555) +* {time_elapsed:"%M:1.1"} - 55.6 +* {time_elapsed:"%M:0.2"} - 2 decimal places : 55.56 +* {time_elapsed:"%M:4.1"} - 55.6 +* {time_elapsed:"%M:5.1"} - 055.6 +* {time_elapsed:"%M:5.2"} - 55.56 +* {time_elapsed:"%M:5"} - 00055 +* {time_elapsed:"%M:.5"} - 55.55550 + +You can mix and match tokens and add your own text: + +* {time_elapsed:"%d days, %h:2:%m:2:%s:2 (%S Total Seconds)"} - 5 days, 05:05:05 (450305 Total Seconds) +* {time_elapsed:"%s:0.2 seconds"} - shows the total seconds with 2 decimal points - 450305.12 seconds diff --git a/octoprint_octolapse/static/docs/help/profiles.rendering.sync_with_timelapse.md b/octoprint_octolapse/static/docs/help/profiles.rendering.sync_with_timelapse.md deleted file mode 100644 index aab5e4b7..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.rendering.sync_with_timelapse.md +++ /dev/null @@ -1 +0,0 @@ -When synchronizing with the default plugin, your timelapses will appear in the native timelapse plugin tab after rendering is complete. OctoPrint version 1.3.11 or higher is needed in order to synchronize all video types besides MP4, which will synchronize fine in earlier versions of OctoPrint. diff --git a/octoprint_octolapse/static/docs/help/profiles.stabilization.wait_for_moves_to_finish.md b/octoprint_octolapse/static/docs/help/profiles.stabilization.wait_for_moves_to_finish.md new file mode 100644 index 00000000..a8355411 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.stabilization.wait_for_moves_to_finish.md @@ -0,0 +1,9 @@ +This is a **beta** feature, use at your own risk. + +Normally, Octolapse sends an M400 command before taking a snapshot in order to stabilize the snapshot. This flushes the gcode buffer, and introduces a slight pause in the print. However, this is absolutely necessary to get a perfectly smooth timelapse. Disabling this option prevents a pause caused by the M400 command, but will result in a jerky timelapse, similar to the stock timelapse plugin. Additionally, Octolapse will not retract, lift, or travel when this option is disabled. + +Normally, when taking a snapshot Octolapse will execute before and after snapshot scripts synchronously, meaning it will wait for the script to exit before it continues. When wait for moves is disabled, Octolapse will NOT wait for these scripts to complete before continuing. This can cause some problems for your before/after snapshot scripts depending on what they do, so use caution here. + +Wait for moves to finish can be enabled/disabled within the stabilization profile. + +*Note*: Disabling **Wait For Moves To Finish** is not the same as disabling stabilization on each axis. diff --git a/octoprint_octolapse/static/docs/help/profiles.trigger.allow_smart_snapshot_commands.md b/octoprint_octolapse/static/docs/help/profiles.trigger.allow_smart_snapshot_commands.md new file mode 100644 index 00000000..7ec5f267 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.trigger.allow_smart_snapshot_commands.md @@ -0,0 +1 @@ +When using smart triggers, allow the use of the @OCTOLAPSE TAKE-SNAPSHOT command, which will force a snapshot when it is encountered. This is useful for ensuring a snapshot is taken before the print starts and after the print ends. diff --git a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_snap_to_print.md b/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_snap_to_print.md deleted file mode 100644 index d79376c7..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_snap_to_print.md +++ /dev/null @@ -1,3 +0,0 @@ -Prevents the extruder from leaving the print during stabilization, reducing travel movements to 0. This is a great option for improving print quality and reducing the amount of time Octolapse adds to your print, and is especially useful for vase mode. However, your timelapse will likely be a little jerky depending on the shape of your printed part. - -Note: You might consider using snap-to-print with the stabilization profile set to 'Disabled', which has the interesting effect of causing the extruder to (usually) smoothly wander around the print. This is a great combo for vase mode. diff --git a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_snap_to_print_high_quality.md b/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_snap_to_print_high_quality.md new file mode 100644 index 00000000..a45d3109 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_snap_to_print_high_quality.md @@ -0,0 +1 @@ +This mode allows Octolapse to select snap to print positions based on their print quality impacts. This will improve quality for most prints, but will also result in a less stable timelapse. diff --git a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_snap_to_print_smooth.md b/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_snap_to_print_smooth.md new file mode 100644 index 00000000..d0bb9ecc --- /dev/null +++ b/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_snap_to_print_smooth.md @@ -0,0 +1 @@ +When this option is enabled, Octolapse will attempt to stabilize the first snapshot position based on your stabilization setting. After the first snapshot, Octolapse will always choose the closest position available to the previous snapshot, reducing jitter. diff --git a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_distance_threshold_percent.md b/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_distance_threshold_percent.md deleted file mode 100644 index a2c2d6b5..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_distance_threshold_percent.md +++ /dev/null @@ -1,3 +0,0 @@ -When using the 'High Quality' smart trigger, this option will allow Octolapse to choose to reduce travel versus choosing a higher quality position, but ONLY if it reduces travel distance by at least the percentage entered here. This can speed up your print when compared to the 'Best Quality' smart trigger type while providing higher quality than the 'Normal Quality' smart trigger type. - -See the 'Smart Layer Trigger Type' help for more information. diff --git a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_speed_threshold.md b/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_speed_threshold.md deleted file mode 100644 index 3ff352b3..00000000 --- a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_speed_threshold.md +++ /dev/null @@ -1,5 +0,0 @@ -When using the 'Fast' smart trigger type, this option will prevent any snapshots from occurring during extrusion unless the extrusion speed is ABOVE (not equal to) the provided speed. If used correctly, this can prevent Octolapse from taking snapshots while over exterior perimeters, and decreases travel distances over the 'compatibility', 'normal quality', 'high quality' and 'best quality' smart layer trigger types. - -If the speed threshold is not correctly set, your print quality could suffer dramatically. The speed needs to be FASTER than any important print features (faster than outer perimeter speed, bridges, or any other important parts), but SLOWER than the non-quality related parts (infill, sometimes interior perimeters depending on your printer setup). Skip this option unless you know what you're doing. - -Set to 0 to disable the speed threshold. diff --git a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_type.md b/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_type.md index 794b85ef..d5f07161 100644 --- a/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_type.md +++ b/octoprint_octolapse/static/docs/help/profiles.trigger.smart_layer_trigger_type.md @@ -3,37 +3,80 @@ There are several smart layer types that are available: #### Fastest -Gets the closest position. This WILL leave very noticable defects on your print when using this mode. However, it can be VERY useful if you are printing an ooze shield, or want to stabilize on top of a wipe tower. The key is to adjust the stabilization point so that Octolapse always triggers on an unimportant piece. -#### Fast -Gets the closest position, including the fastest extrusion movement on the current layer, optionally EXCLUDING extrusions with feedrates that are below or equal to a supplied speed threshold. If only one extrusion speed is detected on a given layer, and no speed threshold is provided, returns the same position that the 'compatibility' type would return. You can get good results with this option IF you select the proper speed threshold and adjust your slicer to ensure that no critical parts will be printed at any faster speed (say infill). +Gets the closest position. **This WILL leave very noticeable defects on your print when using this mode.** However, it can be VERY useful if you are printing an ooze shield, or want to stabilize on top of a wipe tower. The key is to adjust the stabilization point so that Octolapse always triggers on an unimportant piece. + +**Not recommended for vase mode** #### Compatibility -Attempts to return a high quality position if possible (see position rankings below). If it cannot, it will return the next best position available (including extrusions) to ensure that a snapshot is taken on every layer. Use this trigger if you're concerned with quality, but you want Octolapse to trigger on every layer no matter what. This mode is also more likely to work with some lesser tested slicers and slicer settings. This mode WILL take a snapshot while extruding if it has found no alternative. -#### Normal Quality -Attempts to return a high quality (see position rankings below). Returns a lesser quality position if no good quality position is found. Will not take a snapshot during extrusion. +Attempts to return a high quality position if possible (see position rankings below). If it cannot, it will return the next best position available, **including extrusions**, to ensure that a snapshot is taken on every layer. Use this trigger if you're concerned with quality, but you want Octolapse to trigger on every layer no matter what. This mode is also more likely to work with some lesser tested slicers and slicer settings. This mode WILL take a snapshot while extruding if it has found no alternative and is more likely to leave artifacts than the high quality or smap to print types. +**Not recommended for vase mode** #### High Quality -Gets a close High quality position (see position rankings below) and automatically balance time and quality This mode requires an additional parameter called the 'Distance Threshold Percent', which means that it will choose a lower quality snapshot point ONLY if it gives you a speed improvement that is greater than or equal to the provided percentage. For example, if Octolapse has found a high quality position that is 200mm away from the stabilization point, and your distance threshold is set to 10%, it would return the high quality position unless the closer one is 180mm away or nearer. It repeats this process down the quality rankings (see below), but will NEVER take a snapshot while extruding. -#### Best Quality -Gets the best quality position available. Skips snapshots if no quality position can be found. If you have retraction disabled this mode will probably not take any snapshots at all! +Gets the best quality position available. This includes any slicer comment based gcode features. + +**THIS MODE WILL NOT WORK WITH VASE MODE!** + +Snapshots will be skipped if no quality positions can be found. If you are using this mode and notice fewer snapshots than you expect, try compatibility mode instead if you don't care about reduced quality. If you have retraction disabled this mode will probably not take any snapshots at all. If you use vase mode, you will only get a few snapshots at the beginning and possible a few snapshots at the end of the print. + +#### Snap to Print + +Prevents the extruder from leaving the print during stabilization, reducing travel movements to 0. This is a great option for improving print quality and reducing the amount of time Octolapse adds to your print. However, your timelapse will likely be a little jerky depending on the shape of your printed part. -**Important Note** The Normal, High and Best quality smart triggers will NOT work properly with vase mode. You won't get many (any?) snapshots, and your quality will be severely impacted. If you are printing a vase, consider using the 'snap to print' option, which is much faster, and has a lower impact on vases in general. +**For the highest quality prints , reduce your camera delay to 0.** +**Not recommended for DSLR cameras or with long camera delays!** + +**Works with vase mode**, but you will probably see a seam where snapshots were taken. This can be reduced somewhat by decreasing the time it takes to acquire a snapshot. ### Position Rankings by Quality +Octoalpse ranks positions by both position and feature type. Exactly how it does this depends on the exact smart layer trigger type and options. + +#### Feature Type + +In general, the smart layer trigger will choose positions with a known feature type over those without. However, this depends on the smart layer trigger type. + +Most slicers include some comments within the gcode file that indicates what type of feature is being printed. This information is not always complete, but is often useful for choosing when to take a snapshot with the least impact on print quality. + +These are used with the **Compatibility**, **High Quality**, and **Snap to Print**. + +Note that features are only detected in the **Snap to Print** trigger when the **High Quality Mode** option is enabled. + +Each slicer has slightly different capabilities. Cura and Simplify 3D add gcode comment sections to their sliced gcode files by default. + +Slic3r, Slic3r PE, and PrusaSlicer do not output comments by default. It is **highly recommended** that you enable this by going into ```Print Settings```->```Output Options```->```Output Options``` and enabling ```Verbose G-Code``` by checking the box next to the setting. This will increase file-size somewhat, but has the potential + +There are the following features that can be detected by Octolapse, in order of quality (highest quality to lowest): + +* Prime Pillar - This is usually a waste piece, so it's totally safe to take snapshots when printing a prime pillar. Supported by Simplify3D, Slic3r PE and Prusa Slicer (Pusa slicers supported when printing with an MMU only as far as I know.) +* Infill - This is generally the best place to take a snapshot. Works on all supported slicers. +* Ooze Shield - This is printed to protect a print from ooze. I think this is a good place to take a snapshot, but have not tested it yet. +* Solid Infill - This is usually on the inside of a print, or on the top surface. An OK place to take a snapshot. Not yet supported for Prusa slicers. +* Gap Fill - Another OK place to take a snapshot, but we're starting to get iffy. Only supported by Simplify 3d. +* Skirt - This would be considered a high quality place to take a snapshot, and it is, but having it higher causes problems with snap-to-print. +* Inner Perimeters - Not a great place to take a snapshot, but much better than exterior perimeters. Prusa slicers do not distinguish from internal and external perimeters. + +Some features types are ignored +* Unknown Perimeters - Slic3r doesn't differentiate between internal and external perimeters, so all are considered to be external. +* External Perimeters - The worst place to take snapshots! Octolapse tries to avoid taking snapshots over these features if at all possible. +* Bridge - Prusa Slicers will note when a move is a bridge. Octolapse will avoid taking snapshots over these positions if possible. + + +#### Position Type Here is how Octolapse ranks positions (from best to worst) when choosing a point to start taking a snapshot: -* lifted_retracted_travel - The best time to take a snapshot. Your printer is fully lifted, fully retracted, and is traveling. -* lifting_retracted_travel - Your printer is fully retracted, and is traveling while lifting at the same time. -* retracted_travel - Your printer is rully retracted and traveling. -* retracted_lifted - Your printer is fully retracted and fully lifted. -* retracted_lifting - Your printer is fully retracted, and is lifting. -* retraction - Your printer has just completed a retraction. This can be a good time to take a snapshot. Most of the smart trigger types will return a position without looking for a closer one at this point. Only the 'fastest', and sometimes the 'fast' option will keep searching for a closer position if a higher quality position was already found. -* lifted_travel - Your printer is lifted fully, and traveling. -* lifting_travel - Your printer is lifting and traveling at the same time (Cura does this I believe) -* travel - Your printer is traveling. -* lifted - Your printer is fully lifted (according to the z-hop distance setting in your slicer). -* lifting - Your printer is raising the Z axis. Though not a great place to take a snapshot, it's better than while extruding. -* extrusion - Your printer is extruding. This is usually the worst time to take a snapshot. -* unknown - Octolapse doesn't know, or doesn't care what type of position this is. Snapshots are never taken from unknown positions +* Fastest Extrusion - If a layer has more than one print speed, the extrusions with the fastest speed are considered the best place to take snapshots. This isn't always true, especially if the slicer's minimum layer time is being enforced by slowing down priting speeds, but true more often than it is not. Usually this is the best time to take a snapshot. Note that for layers with only one extrusion speed, this position will not exist. +* Lifted and Retracted Travel Move - Usually a good time to take a snapshot. Your printer is fully lifted, fully retracted, and is traveling. +* Lifting and Retracted Travel Move - Your printer is fully retracted, and is traveling while lifting at the same time. +* Retracted Travel Move - Your printer is rully retracted and traveling. +* Retracted and Lifted - Your printer is fully retracted and fully lifted. +* Retracted and Lifting - Your printer is fully retracted, and is lifting. +* Retracted - Your printer has just completed a retraction. This can be a good time to take a snapshot. Most of the smart trigger types will return a position without looking for a closer one at this point. Only the 'fastest', and sometimes the 'fast' option will keep searching for a closer position if a higher quality position was already found. +* Lifted Travel - Your printer is lifted fully, and traveling. +* Lifting Travel - Your printer is lifting and traveling at the same time (Cura does this I believe) +* Travel - Your printer is traveling. +* Lifted - Your printer is fully lifted (according to the z-hop distance setting in your slicer). +* Lifting - Your printer is raising the Z axis. Though not a great place to take a snapshot, it's better than while extruding. +* Extruding - Your printer is extruding. This is usually the worst time to take a snapshot. +* Unknown - Octolapse doesn't know, or doesn't care what type of position this is. Snapshots are never taken from unknown positions If you believe there are other scenarios that need to be handled, or that the order of any of the items above is incorrect, please let me know. This is a work in progress, and I believe there will be lots of tweaks in the future. + diff --git a/octoprint_octolapse/static/docs/help/profiles.trigger.trigger_subtype.md b/octoprint_octolapse/static/docs/help/profiles.trigger.trigger_subtype.md index 0ea7e172..71ebb749 100644 --- a/octoprint_octolapse/static/docs/help/profiles.trigger.trigger_subtype.md +++ b/octoprint_octolapse/static/docs/help/profiles.trigger.trigger_subtype.md @@ -1,7 +1,16 @@ The smart trigger has only one option: Layer/Height. Real time triggers also allow 'Gcode' and 'Timer' triggers. +#### Layer/Height +This trigger will take a snapshot on every layer change, or at most once for the provided trigger height. +#### Gcode +Take a snapshot every time a snapshot command is encountered. The following two commands will work by default: +* ```@OCTOLAPSE TAKE-SNAPSHOT``` - This is a newly added snapshot command. It will never be sent to your printer by Octoprint, even if Octolapse is disabled. However, if you are printing from SD (Octolapse will not work when printing from SD), depending on your firmware, you may encounter errors. +* ```snap``` - This is a legacy command, and was added for compatibility reasons. It will still work, but it may be removed in a future version. -* Layer/Height - take a snapshot on every layer change, or at most once for the provided trigger height. -* Timer (Real Time Only) - Take a snapshot on a timed interval. Don't set this value too low! -* Gcode (Real Time Only) - Take a snapshot every time the 'Snapshot Command' is encountered. See the 'Snapshot Command' in the Octolapse printer profile for more information. +You can also use any custom command you desire by setting the **Alternative Snapshot Command** within your Octolapse printer profile. By default, this command is set to **G4 P1**, which is a popular custom command. + +Note: The snapshot command will not be sent to your printer, so make sure that whatever command you use is not one that is required for either your printer or Octolapse to work. For example, do NOT use ```M114```, ```M400```, ```G90```, etc. + +#### Timer (Real Time Only) +Take a snapshot on a timed interval. diff --git a/octoprint_octolapse/static/docs/help/profiles.trigger.trigger_type.md b/octoprint_octolapse/static/docs/help/profiles.trigger.trigger_type.md index 13b74571..b7bb8922 100644 --- a/octoprint_octolapse/static/docs/help/profiles.trigger.trigger_type.md +++ b/octoprint_octolapse/static/docs/help/profiles.trigger.trigger_type.md @@ -3,7 +3,16 @@ There are two options for trigger type availiable currently: This is the 'Classic' octolapse trigger type. When selected, Octolapse will read your gcodes before they are sent to your 3D printer and decide when to take a snapshot in real-time (hence the name). These triggers are battle hardened, and have been widely used and tested. However, because Octolapse has no knowledge of what gcodes have not yet been sent to the printer, there are some limitations, both in quality and print speed. However, the Gcode and Timer triggers ONLY work when using real-time processing. ### Smart Layer Trigger This is a brand new trigger type that allows Octolapse to read your entire Gcode file BEFORE printing, which allows us to pick better stabilization points. This trigger type can improve print quality and reduce the time that Octolapse adds to your print. It can also provide you with an (optional) preview of every snapshot that will be taken BEFORE your printer starts printing. This allows you to cancel if you don't like the results, and to play with various stabilization/trigger/printer settings before running your print. It will even show you an estimate of how much travel distance you saved vrs using the regular layer trigger! - - However, pre-processing large gcode files takes some time, and you'll have to wait for it to complete before printing. I've created the vast majority of the smart layer trigger with some great effort in C++ so that it runs as fast as possible. Generally the time saved by using the smart trigger will be much higher than the amount of time you'll have to wait for the process to complete. However, how much time you save is highly dependant on the shape and placement of your print, the current stabilization location, and the trigger options. If you REALLY want to save time check out the snap-to-print smart layer trigger option! + + However, pre-processing large gcode files takes some time, and you'll have to wait for it to complete before printing. I've created the vast majority of the smart layer trigger with some great effort in C++ so that it runs as fast as possible. Generally the time saved by using the smart trigger will be much higher than the amount of time you'll have to wait for the process to complete. However, how much time you save is highly dependant on the shape and placement of your print, the current stabilization location, and the trigger options. If you REALLY want to save time check out the snap-to-print smart layer trigger option! The smart layer trigger is new, and is not as well tested as the real-time triggers, but it solves lots of issues, and offers tons of benefits. I recommend you try it out, but please report any issues you find [here](https://github.com/FormerLurker/Octolapse/issues) so that I can fix them! + +### Smart Gcode Trigger +This trigger functions in the same way as the Real-Time (classic) gcode trigger except that your gcode will be preprocessed. Just like the **Smart Layer Trigger** you will be able to preview all of the snapshots Octolapse will take before your print starts. Also, Just like the **Smart Layer Trigger**, this option uses fewer resources while printing, but pre-processing can take a while to complete. + +When using this trigger, you can initiate a snapshot by including the following command in your gcode file: + +```@OCTOLAPSE TAKE-SNAPSHOT``` + +You can also use any custom command you desire by setting the **Alternative Snapshot Command** within your Octolapse printer profile. diff --git a/octoprint_octolapse/static/docs/help/quality_issues_fast_trigger.md b/octoprint_octolapse/static/docs/help/quality_issues_fast_trigger.md new file mode 100644 index 00000000..6f5b48fb --- /dev/null +++ b/octoprint_octolapse/static/docs/help/quality_issues_fast_trigger.md @@ -0,0 +1,3 @@ +In general the 'Fast' smart trigger will not give you acceptable print quality. It is usually best to select one of the high quality smart triggers' + +However, in some situations the fast trigger may be ideal. For example, if you are using a wipe tower, using the fast trigger and a stabilization that is closer to the wipe tower than to your printed part can result in excellent print quality. diff --git a/octoprint_octolapse/static/docs/help/quality_issues_low_quality_snap_to_print.md b/octoprint_octolapse/static/docs/help/quality_issues_low_quality_snap_to_print.md new file mode 100644 index 00000000..ca8e1800 --- /dev/null +++ b/octoprint_octolapse/static/docs/help/quality_issues_low_quality_snap_to_print.md @@ -0,0 +1 @@ +If you are using the regular 'Snap To Print' trigger profile, you might experience some print quality reductions. If you are unhappy with your print quality, consider using the high quality snap-to-print option (available in the trigger profiles when using the snap to print smart trigger). Please note that if you are using vase mode, this option will have no effect. diff --git a/octoprint_octolapse/static/docs/help/quality_issues_no_print_features_detected.md b/octoprint_octolapse/static/docs/help/quality_issues_no_print_features_detected.md new file mode 100644 index 00000000..f6c45a8e --- /dev/null +++ b/octoprint_octolapse/static/docs/help/quality_issues_no_print_features_detected.md @@ -0,0 +1,7 @@ +This error means that gcode comments are missing from your gcode file. These comments tell Octolapse what print features are currently printing, like Infill, interior and exterior permieters, skirts, ooze shields and more. + +If you are using Slic3r or PrusaSlicer, see [this guide](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Slic3r-Slic3r-PE-and-PrusaSlicer) for enabling verbose gcode comments. + +If you are using Cura or Simplify 3d, consider creating an [issue report](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Reporting-An-Issue), because this isn't supposed to happen. + +If you are using any other slicer, Octolapse does not currently support feature detection. Feel free to submit a feature request enabling Octolapse to recognize your slicer. Learn how to create a feature request [here](https://github.com/FormerLurker/Octolapse/wiki/V0.4---Request-A-New-Feature). diff --git a/octoprint_octolapse/static/docs/help/snapshot_plan_missed_snapshots.md b/octoprint_octolapse/static/docs/help/snapshot_plan_missed_snapshots.md new file mode 100644 index 00000000..ec68af7a --- /dev/null +++ b/octoprint_octolapse/static/docs/help/snapshot_plan_missed_snapshots.md @@ -0,0 +1,9 @@ +Octolapse expected more snapshots than were returned by the smart trigger. + +### Why did Octolapse miss snapshots, and how do I fix it? + +There are a few things that can cause this: + +1. You are using the **High Quality** smart layer trigger, but retraction is disabled in your slicer. Either switch to the **Compatibility** smart layer trigger, or reslice your gcode with retraction enabled. +2. You are using the **High Quality** smart layer trigger while printing in **Vase Mode**. In this case I recommend you switch to the **Snap To Print** smart layer trigger. However, it is possible to use the **Compatibility** smart layer trigger, though this could significantly affect print quality. In general, I do not recommend using Octolapse when printing in **Vase Mode**. + diff --git a/octoprint_octolapse/static/js/jquery.minicolors.min.js b/octoprint_octolapse/static/js/jquery.minicolors.min.js index 08f5d423..36e37c7b 100644 --- a/octoprint_octolapse/static/js/jquery.minicolors.min.js +++ b/octoprint_octolapse/static/js/jquery.minicolors.min.js @@ -5,4 +5,1123 @@ // // Licensed under the MIT license: http://opensource.org/licenses/MIT // -!function(i){"function"==typeof define&&define.amd?define(["jquery"],i):"object"==typeof exports?module.exports=i(require("jquery")):i(jQuery)}(function(i){"use strict";function t(t,o){var s,a,n,e,r,l,h=i('
'),d=i.minicolors.defaults;if(!t.data("minicolors-initialized")){if(o=i.extend(!0,{},d,o),h.addClass("minicolors-theme-"+o.theme).toggleClass("minicolors-with-opacity",o.opacity),void 0!==o.position&&i.each(o.position.split(" "),function(){h.addClass("minicolors-position-"+this)}),a="rgb"===o.format?o.opacity?"25":"20":o.keywords?"11":"7",t.addClass("minicolors-input").data("minicolors-initialized",!1).data("minicolors-settings",o).prop("size",a).wrap(h).after('
'),o.inline||(t.after(''),t.next(".minicolors-input-swatch").on("click",function(i){i.preventDefault(),t.focus()})),r=t.parent().find(".minicolors-panel"),r.on("selectstart",function(){return!1}).end(),o.swatches&&0!==o.swatches.length)for(r.addClass("minicolors-with-swatches"),n=i('
    ').appendTo(r),l=0;l').appendTo(n).data("swatch-color",o.swatches[l]).find(".minicolors-swatch-color").css({backgroundColor:C(e),opacity:e.a}),o.swatches[l]=e;o.inline&&t.parent().addClass("minicolors-inline"),c(t,!1),t.data("minicolors-initialized",!0)}}function o(i){var t=i.parent();i.removeData("minicolors-initialized").removeData("minicolors-settings").removeProp("size").removeClass("minicolors-input"),t.before(i).remove()}function s(i){var t=i.parent(),o=t.find(".minicolors-panel"),s=i.data("minicolors-settings");!i.data("minicolors-initialized")||i.prop("disabled")||t.hasClass("minicolors-inline")||t.hasClass("minicolors-focus")||(a(),t.addClass("minicolors-focus"),o.stop(!0,!0).fadeIn(s.showSpeed,function(){s.show&&s.show.call(i.get(0))}))}function a(){i(".minicolors-focus").each(function(){var t=i(this),o=t.find(".minicolors-input"),s=t.find(".minicolors-panel"),a=o.data("minicolors-settings");s.fadeOut(a.hideSpeed,function(){a.hide&&a.hide.call(o.get(0)),t.removeClass("minicolors-focus")})})}function n(i,t,o){var s,a,n,r,c=i.parents(".minicolors").find(".minicolors-input"),l=c.data("minicolors-settings"),h=i.find("[class$=-picker]"),d=i.offset().left,p=i.offset().top,u=Math.round(t.pageX-d),g=Math.round(t.pageY-p),m=o?l.animationSpeed:0;t.originalEvent.changedTouches&&(u=t.originalEvent.changedTouches[0].pageX-d,g=t.originalEvent.changedTouches[0].pageY-p),u<0&&(u=0),g<0&&(g=0),u>i.width()&&(u=i.width()),g>i.height()&&(g=i.height()),i.parent().is(".minicolors-slider-wheel")&&h.parent().is(".minicolors-grid")&&(s=75-u,a=75-g,n=Math.sqrt(s*s+a*a),r=Math.atan2(a,s),r<0&&(r+=2*Math.PI),n>75&&(n=75,u=75-75*Math.cos(r),g=75-75*Math.sin(r)),u=Math.round(u),g=Math.round(g)),i.is(".minicolors-grid")?h.stop(!0).animate({top:g+"px",left:u+"px"},m,l.animationEasing,function(){e(c,i)}):h.stop(!0).animate({top:g+"px"},m,l.animationEasing,function(){e(c,i)})}function e(i,t){function o(i,t){var o,s;return i.length&&t?(o=i.offset().left,s=i.offset().top,{x:o-t.offset().left+i.outerWidth()/2,y:s-t.offset().top+i.outerHeight()/2}):null}var s,a,n,e,c,h,d,p=i.val(),u=i.attr("data-opacity"),g=i.parent(),m=i.data("minicolors-settings"),v=g.find(".minicolors-input-swatch"),b=g.find(".minicolors-grid"),w=g.find(".minicolors-slider"),y=g.find(".minicolors-opacity-slider"),C=b.find("[class$=-picker]"),M=w.find("[class$=-picker]"),x=y.find("[class$=-picker]"),I=o(C,b),S=o(M,w),z=o(x,y);if(t.is(".minicolors-grid, .minicolors-slider, .minicolors-opacity-slider")){switch(m.control){case"wheel":e=b.width()/2-I.x,c=b.height()/2-I.y,h=Math.sqrt(e*e+c*c),d=Math.atan2(c,e),d<0&&(d+=2*Math.PI),h>75&&(h=75,I.x=69-75*Math.cos(d),I.y=69-75*Math.sin(d)),a=f(h/.75,0,100),s=f(180*d/Math.PI,0,360),n=f(100-Math.floor(S.y*(100/w.height())),0,100),p=k({h:s,s:a,b:n}),w.css("backgroundColor",k({h:s,s:a,b:100}));break;case"saturation":s=f(parseInt(I.x*(360/b.width()),10),0,360),a=f(100-Math.floor(S.y*(100/w.height())),0,100),n=f(100-Math.floor(I.y*(100/b.height())),0,100),p=k({h:s,s:a,b:n}),w.css("backgroundColor",k({h:s,s:100,b:n})),g.find(".minicolors-grid-inner").css("opacity",a/100);break;case"brightness":s=f(parseInt(I.x*(360/b.width()),10),0,360),a=f(100-Math.floor(I.y*(100/b.height())),0,100),n=f(100-Math.floor(S.y*(100/w.height())),0,100),p=k({h:s,s:a,b:n}),w.css("backgroundColor",k({h:s,s:a,b:100})),g.find(".minicolors-grid-inner").css("opacity",1-n/100);break;default:s=f(360-parseInt(S.y*(360/w.height()),10),0,360),a=f(Math.floor(I.x*(100/b.width())),0,100),n=f(100-Math.floor(I.y*(100/b.height())),0,100),p=k({h:s,s:a,b:n}),b.css("backgroundColor",k({h:s,s:100,b:100}))}u=m.opacity?parseFloat(1-z.y/y.height()).toFixed(2):1,r(i,p,u)}else v.find("span").css({backgroundColor:p,opacity:u}),l(i,p,u)}function r(i,t,o){var s,a=i.parent(),n=i.data("minicolors-settings"),e=a.find(".minicolors-input-swatch");n.opacity&&i.attr("data-opacity",o),"rgb"===n.format?(s=v(t)?g(t,!0):I(u(t,!0)),o=""===i.attr("data-opacity")?1:f(parseFloat(i.attr("data-opacity")).toFixed(2),0,1),!isNaN(o)&&n.opacity||(o=1),t=i.minicolors("rgbObject").a<=1&&s&&n.opacity?"rgba("+s.r+", "+s.g+", "+s.b+", "+parseFloat(o)+")":"rgb("+s.r+", "+s.g+", "+s.b+")"):(v(t)&&(t=y(t)),t=p(t,n.letterCase)),i.val(t),e.find("span").css({backgroundColor:t,opacity:o}),l(i,t,o)}function c(t,o){var s,a,n,e,r,c,h,d,w,C,x=t.parent(),I=t.data("minicolors-settings"),S=x.find(".minicolors-input-swatch"),z=x.find(".minicolors-grid"),F=x.find(".minicolors-slider"),T=x.find(".minicolors-opacity-slider"),j=z.find("[class$=-picker]"),D=F.find("[class$=-picker]"),q=T.find("[class$=-picker]");switch(v(t.val())?(s=y(t.val()),r=f(parseFloat(b(t.val())).toFixed(2),0,1),r&&t.attr("data-opacity",r)):s=p(u(t.val(),!0),I.letterCase),s||(s=p(m(I.defaultValue,!0),I.letterCase)),a=M(s),e=I.keywords?i.map(I.keywords.split(","),function(t){return i.trim(t.toLowerCase())}):[],c=""!==t.val()&&i.inArray(t.val().toLowerCase(),e)>-1?p(t.val()):v(t.val())?g(t.val()):s,o||t.val(c),I.opacity&&(n=""===t.attr("data-opacity")?1:f(parseFloat(t.attr("data-opacity")).toFixed(2),0,1),isNaN(n)&&(n=1),t.attr("data-opacity",n),S.find("span").css("opacity",n),d=f(T.height()-T.height()*n,0,T.height()),q.css("top",d+"px")),"transparent"===t.val().toLowerCase()&&S.find("span").css("opacity",0),S.find("span").css("backgroundColor",s),I.control){case"wheel":w=f(Math.ceil(.75*a.s),0,z.height()/2),C=a.h*Math.PI/180,h=f(75-Math.cos(C)*w,0,z.width()),d=f(75-Math.sin(C)*w,0,z.height()),j.css({top:d+"px",left:h+"px"}),d=150-a.b/(100/z.height()),""===s&&(d=0),D.css("top",d+"px"),F.css("backgroundColor",k({h:a.h,s:a.s,b:100}));break;case"saturation":h=f(5*a.h/12,0,150),d=f(z.height()-Math.ceil(a.b/(100/z.height())),0,z.height()),j.css({top:d+"px",left:h+"px"}),d=f(F.height()-a.s*(F.height()/100),0,F.height()),D.css("top",d+"px"),F.css("backgroundColor",k({h:a.h,s:100,b:a.b})),x.find(".minicolors-grid-inner").css("opacity",a.s/100);break;case"brightness":h=f(5*a.h/12,0,150),d=f(z.height()-Math.ceil(a.s/(100/z.height())),0,z.height()),j.css({top:d+"px",left:h+"px"}),d=f(F.height()-a.b*(F.height()/100),0,F.height()),D.css("top",d+"px"),F.css("backgroundColor",k({h:a.h,s:a.s,b:100})),x.find(".minicolors-grid-inner").css("opacity",1-a.b/100);break;default:h=f(Math.ceil(a.s/(100/z.width())),0,z.width()),d=f(z.height()-Math.ceil(a.b/(100/z.height())),0,z.height()),j.css({top:d+"px",left:h+"px"}),d=f(F.height()-a.h/(360/F.height()),0,F.height()),D.css("top",d+"px"),z.css("backgroundColor",k({h:a.h,s:100,b:100}))}t.data("minicolors-initialized")&&l(t,c,n)}function l(i,t,o){var s,a,n,e=i.data("minicolors-settings"),r=i.data("minicolors-lastChange");if(!r||r.value!==t||r.opacity!==o){if(i.data("minicolors-lastChange",{value:t,opacity:o}),e.swatches&&0!==e.swatches.length){for(s=v(t)?g(t,!0):I(t),a=-1,n=0;no&&(i=o),i}function v(i){var t=i.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);return!(!t||4!==t.length)}function b(i){return i=i.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+(\.\d{1,2})?|\.\d{1,2})[\s+]?/i),i&&6===i.length?i[4]:"1"}function w(i){var t={},o=Math.round(i.h),s=Math.round(255*i.s/100),a=Math.round(255*i.b/100);if(0===s)t.r=t.g=t.b=a;else{var n=a,e=(255-s)*a/255,r=(n-e)*(o%60)/60;360===o&&(o=0),o<60?(t.r=n,t.b=e,t.g=e+r):o<120?(t.g=n,t.b=e,t.r=n-r):o<180?(t.g=n,t.r=e,t.b=e+r):o<240?(t.b=n,t.r=e,t.g=n-r):o<300?(t.b=n,t.g=e,t.r=e+r):o<360?(t.r=n,t.g=e,t.b=n-r):(t.r=0,t.g=0,t.b=0)}return{r:Math.round(t.r),g:Math.round(t.g),b:Math.round(t.b)}}function y(i){return i=i.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i),i&&4===i.length?"#"+("0"+parseInt(i[1],10).toString(16)).slice(-2)+("0"+parseInt(i[2],10).toString(16)).slice(-2)+("0"+parseInt(i[3],10).toString(16)).slice(-2):""}function C(t){var o=[t.r.toString(16),t.g.toString(16),t.b.toString(16)];return i.each(o,function(i,t){1===t.length&&(o[i]="0"+t)}),"#"+o.join("")}function k(i){return C(w(i))}function M(i){var t=x(I(i));return 0===t.s&&(t.h=360),t}function x(i){var t={h:0,s:0,b:0},o=Math.min(i.r,i.g,i.b),s=Math.max(i.r,i.g,i.b),a=s-o;return t.b=s,t.s=0!==s?255*a/s:0,0!==t.s?i.r===s?t.h=(i.g-i.b)/a:i.g===s?t.h=2+(i.b-i.r)/a:t.h=4+(i.r-i.g)/a:t.h=-1,t.h*=60,t.h<0&&(t.h+=360),t.s*=100/255,t.b*=100/255,t}function I(i){return i=parseInt(i.indexOf("#")>-1?i.substring(1):i,16),{r:i>>16,g:(65280&i)>>8,b:255&i}}i.minicolors={defaults:{animationSpeed:50,animationEasing:"swing",change:null,changeDelay:0,control:"hue",defaultValue:"",format:"hex",hide:null,hideSpeed:100,inline:!1,keywords:"",letterCase:"lowercase",opacity:!1,position:"bottom",show:null,showSpeed:100,theme:"default",swatches:[]}},i.extend(i.fn,{minicolors:function(n,e){switch(n){case"destroy":return i(this).each(function(){o(i(this))}),i(this);case"hide":return a(),i(this);case"opacity":return void 0===e?i(this).attr("data-opacity"):(i(this).each(function(){c(i(this).attr("data-opacity",e))}),i(this));case"rgbObject":return h(i(this),"rgbaObject"===n);case"rgbString":case"rgbaString":return d(i(this),"rgbaString"===n);case"settings":return void 0===e?i(this).data("minicolors-settings"):(i(this).each(function(){var t=i(this).data("minicolors-settings")||{};o(i(this)),i(this).minicolors(i.extend(!0,t,e))}),i(this));case"show":return s(i(this).eq(0)),i(this);case"value":return void 0===e?i(this).val():(i(this).each(function(){"object"==typeof e&&null!==e?(void 0!==e.opacity&&i(this).attr("data-opacity",f(e.opacity,0,1)),e.color&&i(this).val(e.color)):i(this).val(e),c(i(this))}),i(this));default:return"create"!==n&&(e=n),i(this).each(function(){t(i(this),e)}),i(this)}}}),i([document]).on("mousedown.minicolors touchstart.minicolors",function(t){i(t.target).parents().add(t.target).hasClass("minicolors")||a()}).on("mousedown.minicolors touchstart.minicolors",".minicolors-grid, .minicolors-slider, .minicolors-opacity-slider",function(t){var o=i(this);t.preventDefault(),i(t.delegateTarget).data("minicolors-target",o),n(o,t,!0)}).on("mousemove.minicolors touchmove.minicolors",function(t){var o=i(t.delegateTarget).data("minicolors-target");o&&n(o,t)}).on("mouseup.minicolors touchend.minicolors",function(){i(this).removeData("minicolors-target")}).on("click.minicolors",".minicolors-swatches li",function(t){t.preventDefault();var o=i(this),s=o.parents(".minicolors").find(".minicolors-input"),a=o.data("swatch-color");r(s,a,b(a)),c(s)}).on("mousedown.minicolors touchstart.minicolors",".minicolors-input-swatch",function(t){var o=i(this).parent().find(".minicolors-input");t.preventDefault(),s(o)}).on("focus.minicolors",".minicolors-input",function(){var t=i(this);t.data("minicolors-initialized")&&s(t)}).on("blur.minicolors",".minicolors-input",function(){var t,o,s,a,n,e=i(this),r=e.data("minicolors-settings");e.data("minicolors-initialized")&&(t=r.keywords?i.map(r.keywords.split(","),function(t){return i.trim(t.toLowerCase())}):[],""!==e.val()&&i.inArray(e.val().toLowerCase(),t)>-1?n=e.val():(v(e.val())?s=g(e.val(),!0):(o=u(e.val(),!0),s=o?I(o):null),n=null===s?r.defaultValue:"rgb"===r.format?g(r.opacity?"rgba("+s.r+","+s.g+","+s.b+","+e.attr("data-opacity")+")":"rgb("+s.r+","+s.g+","+s.b+")"):C(s)),a=r.opacity?e.attr("data-opacity"):1,"transparent"===n.toLowerCase()&&(a=0),e.closest(".minicolors").find(".minicolors-input-swatch > span").css("opacity",a),e.val(n),""===e.val()&&e.val(m(r.defaultValue,!0)),e.val(p(e.val(),r.letterCase)))}).on("keydown.minicolors",".minicolors-input",function(t){var o=i(this);if(o.data("minicolors-initialized"))switch(t.which){case 9:a();break;case 13:case 27:a(),o.blur()}}).on("keyup.minicolors",".minicolors-input",function(){var t=i(this);t.data("minicolors-initialized")&&c(t,!0)}).on("paste.minicolors",".minicolors-input",function(){var t=i(this);t.data("minicolors-initialized")&&setTimeout(function(){c(t,!0)},1)})}); +(function (factory) { + if(typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if(typeof exports === 'object') { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + 'use strict'; + + // Defaults + $.minicolors = { + defaults: { + animationSpeed: 50, + animationEasing: 'swing', + change: null, + changeDelay: 0, + control: 'hue', + defaultValue: '', + format: 'hex', + hide: null, + hideSpeed: 100, + inline: false, + keywords: '', + letterCase: 'lowercase', + opacity: false, + position: 'bottom', + show: null, + showSpeed: 100, + theme: 'default', + swatches: [] + } + }; + + // Public methods + $.extend($.fn, { + minicolors: function(method, data) { + + switch(method) { + // Destroy the control + case 'destroy': + $(this).each(function() { + destroy($(this)); + }); + return $(this); + + // Hide the color picker + case 'hide': + hide(); + return $(this); + + // Get/set opacity + case 'opacity': + // Getter + if(data === undefined) { + // Getter + return $(this).attr('data-opacity'); + } else { + // Setter + $(this).each(function() { + updateFromInput($(this).attr('data-opacity', data)); + }); + } + return $(this); + + // Get an RGB(A) object based on the current color/opacity + case 'rgbObject': + return rgbObject($(this), method === 'rgbaObject'); + + // Get an RGB(A) string based on the current color/opacity + case 'rgbString': + case 'rgbaString': + return rgbString($(this), method === 'rgbaString'); + + // Get/set settings on the fly + case 'settings': + if(data === undefined) { + return $(this).data('minicolors-settings'); + } else { + // Setter + $(this).each(function() { + var settings = $(this).data('minicolors-settings') || {}; + destroy($(this)); + $(this).minicolors($.extend(true, settings, data)); + }); + } + return $(this); + + // Show the color picker + case 'show': + show($(this).eq(0)); + return $(this); + + // Get/set the hex color value + case 'value': + if(data === undefined) { + // Getter + return $(this).val(); + } else { + // Setter + $(this).each(function() { + if(typeof(data) === 'object' && data !== null) { + if(data.opacity !== undefined) { + $(this).attr('data-opacity', keepWithin(data.opacity, 0, 1)); + } + if(data.color) { + $(this).val(data.color); + } + } else { + $(this).val(data); + } + updateFromInput($(this)); + }); + } + return $(this); + + // Initializes the control + default: + if(method !== 'create') data = method; + $(this).each(function() { + init($(this), data); + }); + return $(this); + + } + + } + }); + + // Initialize input elements + function init(input, settings) { + var minicolors = $('
    '); + var defaults = $.minicolors.defaults; + var name; + var size; + var swatches; + var swatch; + var swatchString; + var panel; + var i; + + // Do nothing if already initialized + if(input.data('minicolors-initialized')) return; + + // Handle settings + settings = $.extend(true, {}, defaults, settings); + + // The wrapper + minicolors + .addClass('minicolors-theme-' + settings.theme) + .toggleClass('minicolors-with-opacity', settings.opacity); + + // Custom positioning + if(settings.position !== undefined) { + $.each(settings.position.split(' '), function() { + minicolors.addClass('minicolors-position-' + this); + }); + } + + // Input size + if(settings.format === 'rgb') { + size = settings.opacity ? '25' : '20'; + } else { + size = settings.keywords ? '11' : '7'; + } + + // The input + input + .addClass('minicolors-input') + .data('minicolors-initialized', false) + .data('minicolors-settings', settings) + .prop('size', size) + .wrap(minicolors) + .after( + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + ); + + // The swatch + if(!settings.inline) { + input.after(''); + input.next('.minicolors-input-swatch').on('click', function(event) { + event.preventDefault(); + input.trigger('focus'); + }); + } + + // Prevent text selection in IE + panel = input.parent().find('.minicolors-panel'); + panel.on('selectstart', function() { return false; }).end(); + + // Swatches + if(settings.swatches && settings.swatches.length !== 0) { + panel.addClass('minicolors-with-swatches'); + swatches = $('
      ') + .appendTo(panel); + for(i = 0; i < settings.swatches.length; ++i) { + // allow for custom objects as swatches + if($.type(settings.swatches[i]) === 'object') { + name = settings.swatches[i].name; + swatch = settings.swatches[i].color; + } else { + name = ''; + swatch = settings.swatches[i]; + } + swatchString = swatch; + swatch = isRgb(swatch) ? parseRgb(swatch, true) : hex2rgb(parseHex(swatch, true)); + $('
    • ') + .appendTo(swatches) + .data('swatch-color', swatchString) + .find('.minicolors-swatch-color') + .css({ + backgroundColor: rgb2hex(swatch), + opacity: String(swatch.a) + }); + settings.swatches[i] = swatch; + } + } + + // Inline controls + if(settings.inline) input.parent().addClass('minicolors-inline'); + + updateFromInput(input, false); + + input.data('minicolors-initialized', true); + } + + // Returns the input back to its original state + function destroy(input) { + var minicolors = input.parent(); + + // Revert the input element + input + .removeData('minicolors-initialized') + .removeData('minicolors-settings') + .removeProp('size') + .removeClass('minicolors-input'); + + // Remove the wrap and destroy whatever remains + minicolors.before(input).remove(); + } + + // Shows the specified dropdown panel + function show(input) { + var minicolors = input.parent(); + var panel = minicolors.find('.minicolors-panel'); + var settings = input.data('minicolors-settings'); + + // Do nothing if uninitialized, disabled, inline, or already open + if( + !input.data('minicolors-initialized') || + input.prop('disabled') || + minicolors.hasClass('minicolors-inline') || + minicolors.hasClass('minicolors-focus') + ) return; + + hide(); + + minicolors.addClass('minicolors-focus'); + if (panel.animate) { + panel + .stop(true, true) + .fadeIn(settings.showSpeed, function () { + if (settings.show) settings.show.call(input.get(0)); + }); + } else { + panel.show(); + if (settings.show) settings.show.call(input.get(0)); + } + } + + // Hides all dropdown panels + function hide() { + $('.minicolors-focus').each(function() { + var minicolors = $(this); + var input = minicolors.find('.minicolors-input'); + var panel = minicolors.find('.minicolors-panel'); + var settings = input.data('minicolors-settings'); + + if (panel.animate) { + panel.fadeOut(settings.hideSpeed, function () { + if (settings.hide) settings.hide.call(input.get(0)); + minicolors.removeClass('minicolors-focus'); + }); + } else { + panel.hide(); + if (settings.hide) settings.hide.call(input.get(0)); + minicolors.removeClass('minicolors-focus'); + } + }); + } + + // Moves the selected picker + function move(target, event, animate) { + var input = target.parents('.minicolors').find('.minicolors-input'); + var settings = input.data('minicolors-settings'); + var picker = target.find('[class$=-picker]'); + var offsetX = target.offset().left; + var offsetY = target.offset().top; + var x = Math.round(event.pageX - offsetX); + var y = Math.round(event.pageY - offsetY); + var duration = animate ? settings.animationSpeed : 0; + var wx, wy, r, phi, styles; + + // Touch support + if(event.originalEvent.changedTouches) { + x = event.originalEvent.changedTouches[0].pageX - offsetX; + y = event.originalEvent.changedTouches[0].pageY - offsetY; + } + + // Constrain picker to its container + if(x < 0) x = 0; + if(y < 0) y = 0; + if(x > target.width()) x = target.width(); + if(y > target.height()) y = target.height(); + + // Constrain color wheel values to the wheel + if(target.parent().is('.minicolors-slider-wheel') && picker.parent().is('.minicolors-grid')) { + wx = 75 - x; + wy = 75 - y; + r = Math.sqrt(wx * wx + wy * wy); + phi = Math.atan2(wy, wx); + if(phi < 0) phi += Math.PI * 2; + if(r > 75) { + r = 75; + x = 75 - (75 * Math.cos(phi)); + y = 75 - (75 * Math.sin(phi)); + } + x = Math.round(x); + y = Math.round(y); + } + + // Move the picker + styles = { + top: y + 'px' + }; + if(target.is('.minicolors-grid')) { + styles.left = x + 'px'; + } + if (picker.animate) { + picker + .stop(true) + .animate(styles, duration, settings.animationEasing, function() { + updateFromControl(input, target); + }); + } else { + picker + .css(styles); + updateFromControl(input, target); + } + } + + // Sets the input based on the color picker values + function updateFromControl(input, target) { + + function getCoords(picker, container) { + var left, top; + if(!picker.length || !container) return null; + left = picker.offset().left; + top = picker.offset().top; + + return { + x: left - container.offset().left + (picker.outerWidth() / 2), + y: top - container.offset().top + (picker.outerHeight() / 2) + }; + } + + var hue, saturation, brightness, x, y, r, phi; + var hex = input.val(); + var opacity = input.attr('data-opacity'); + + // Helpful references + var minicolors = input.parent(); + var settings = input.data('minicolors-settings'); + var swatch = minicolors.find('.minicolors-input-swatch'); + + // Panel objects + var grid = minicolors.find('.minicolors-grid'); + var slider = minicolors.find('.minicolors-slider'); + var opacitySlider = minicolors.find('.minicolors-opacity-slider'); + + // Picker objects + var gridPicker = grid.find('[class$=-picker]'); + var sliderPicker = slider.find('[class$=-picker]'); + var opacityPicker = opacitySlider.find('[class$=-picker]'); + + // Picker positions + var gridPos = getCoords(gridPicker, grid); + var sliderPos = getCoords(sliderPicker, slider); + var opacityPos = getCoords(opacityPicker, opacitySlider); + + // Handle colors + if(target.is('.minicolors-grid, .minicolors-slider, .minicolors-opacity-slider')) { + + // Determine HSB values + switch(settings.control) { + case 'wheel': + // Calculate hue, saturation, and brightness + x = (grid.width() / 2) - gridPos.x; + y = (grid.height() / 2) - gridPos.y; + r = Math.sqrt(x * x + y * y); + phi = Math.atan2(y, x); + if(phi < 0) phi += Math.PI * 2; + if(r > 75) { + r = 75; + gridPos.x = 69 - (75 * Math.cos(phi)); + gridPos.y = 69 - (75 * Math.sin(phi)); + } + saturation = keepWithin(r / 0.75, 0, 100); + hue = keepWithin(phi * 180 / Math.PI, 0, 360); + brightness = keepWithin(100 - Math.floor(sliderPos.y * (100 / slider.height())), 0, 100); + hex = hsb2hex({ + h: hue, + s: saturation, + b: brightness + }); + + // Update UI + slider.css('backgroundColor', hsb2hex({ h: hue, s: saturation, b: 100 })); + break; + + case 'saturation': + // Calculate hue, saturation, and brightness + hue = keepWithin(parseInt(gridPos.x * (360 / grid.width()), 10), 0, 360); + saturation = keepWithin(100 - Math.floor(sliderPos.y * (100 / slider.height())), 0, 100); + brightness = keepWithin(100 - Math.floor(gridPos.y * (100 / grid.height())), 0, 100); + hex = hsb2hex({ + h: hue, + s: saturation, + b: brightness + }); + + // Update UI + slider.css('backgroundColor', hsb2hex({ h: hue, s: 100, b: brightness })); + minicolors.find('.minicolors-grid-inner').css('opacity', saturation / 100); + break; + + case 'brightness': + // Calculate hue, saturation, and brightness + hue = keepWithin(parseInt(gridPos.x * (360 / grid.width()), 10), 0, 360); + saturation = keepWithin(100 - Math.floor(gridPos.y * (100 / grid.height())), 0, 100); + brightness = keepWithin(100 - Math.floor(sliderPos.y * (100 / slider.height())), 0, 100); + hex = hsb2hex({ + h: hue, + s: saturation, + b: brightness + }); + + // Update UI + slider.css('backgroundColor', hsb2hex({ h: hue, s: saturation, b: 100 })); + minicolors.find('.minicolors-grid-inner').css('opacity', 1 - (brightness / 100)); + break; + + default: + // Calculate hue, saturation, and brightness + hue = keepWithin(360 - parseInt(sliderPos.y * (360 / slider.height()), 10), 0, 360); + saturation = keepWithin(Math.floor(gridPos.x * (100 / grid.width())), 0, 100); + brightness = keepWithin(100 - Math.floor(gridPos.y * (100 / grid.height())), 0, 100); + hex = hsb2hex({ + h: hue, + s: saturation, + b: brightness + }); + + // Update UI + grid.css('backgroundColor', hsb2hex({ h: hue, s: 100, b: 100 })); + break; + } + + // Handle opacity + if(settings.opacity) { + opacity = parseFloat(1 - (opacityPos.y / opacitySlider.height())).toFixed(2); + } else { + opacity = 1; + } + + updateInput(input, hex, opacity); + } + else { + // Set swatch color + swatch.find('span').css({ + backgroundColor: hex, + opacity: String(opacity) + }); + + // Handle change event + doChange(input, hex, opacity); + } + } + + // Sets the value of the input and does the appropriate conversions + // to respect settings, also updates the swatch + function updateInput(input, value, opacity) { + var rgb; + + // Helpful references + var minicolors = input.parent(); + var settings = input.data('minicolors-settings'); + var swatch = minicolors.find('.minicolors-input-swatch'); + + if(settings.opacity) input.attr('data-opacity', opacity); + + // Set color string + if(settings.format === 'rgb') { + // Returns RGB(A) string + + // Checks for input format and does the conversion + if(isRgb(value)) { + rgb = parseRgb(value, true); + } + else { + rgb = hex2rgb(parseHex(value, true)); + } + + opacity = input.attr('data-opacity') === '' ? 1 : keepWithin(parseFloat(input.attr('data-opacity')).toFixed(2), 0, 1); + if(isNaN(opacity) || !settings.opacity) opacity = 1; + + if(input.minicolors('rgbObject').a <= 1 && rgb && settings.opacity) { + // Set RGBA string if alpha + value = 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + parseFloat(opacity) + ')'; + } else { + // Set RGB string (alpha = 1) + value = 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')'; + } + } else { + // Returns hex color + + // Checks for input format and does the conversion + if(isRgb(value)) { + value = rgbString2hex(value); + } + + value = convertCase(value, settings.letterCase); + } + + // Update value from picker + input.val(value); + + // Set swatch color + swatch.find('span').css({ + backgroundColor: value, + opacity: String(opacity) + }); + + // Handle change event + doChange(input, value, opacity); + } + + // Sets the color picker values from the input + function updateFromInput(input, preserveInputValue) { + var hex, hsb, opacity, keywords, alpha, value, x, y, r, phi; + + // Helpful references + var minicolors = input.parent(); + var settings = input.data('minicolors-settings'); + var swatch = minicolors.find('.minicolors-input-swatch'); + + // Panel objects + var grid = minicolors.find('.minicolors-grid'); + var slider = minicolors.find('.minicolors-slider'); + var opacitySlider = minicolors.find('.minicolors-opacity-slider'); + + // Picker objects + var gridPicker = grid.find('[class$=-picker]'); + var sliderPicker = slider.find('[class$=-picker]'); + var opacityPicker = opacitySlider.find('[class$=-picker]'); + + // Determine hex/HSB values + if(isRgb(input.val())) { + // If input value is a rgb(a) string, convert it to hex color and update opacity + hex = rgbString2hex(input.val()); + alpha = keepWithin(parseFloat(getAlpha(input.val())).toFixed(2), 0, 1); + if(alpha) { + input.attr('data-opacity', alpha); + } + } else { + hex = convertCase(parseHex(input.val(), true), settings.letterCase); + } + + if(!hex){ + hex = convertCase(parseInput(settings.defaultValue, true), settings.letterCase); + } + hsb = hex2hsb(hex); + + // Get array of lowercase keywords + keywords = !settings.keywords ? [] : $.map(settings.keywords.split(','), function(a) { + return $.trim(a.toLowerCase()); + }); + + // Set color string + if(input.val() !== '' && $.inArray(input.val().toLowerCase(), keywords) > -1) { + value = convertCase(input.val()); + } else { + value = isRgb(input.val()) ? parseRgb(input.val()) : hex; + } + + // Update input value + if(!preserveInputValue) input.val(value); + + // Determine opacity value + if(settings.opacity) { + // Get from data-opacity attribute and keep within 0-1 range + opacity = input.attr('data-opacity') === '' ? 1 : keepWithin(parseFloat(input.attr('data-opacity')).toFixed(2), 0, 1); + if(isNaN(opacity)) opacity = 1; + input.attr('data-opacity', opacity); + swatch.find('span').css('opacity', String(opacity)); + + // Set opacity picker position + y = keepWithin(opacitySlider.height() - (opacitySlider.height() * opacity), 0, opacitySlider.height()); + opacityPicker.css('top', y + 'px'); + } + + // Set opacity to zero if input value is transparent + if(input.val().toLowerCase() === 'transparent') { + swatch.find('span').css('opacity', String(0)); + } + + // Update swatch + swatch.find('span').css('backgroundColor', hex); + + // Determine picker locations + switch(settings.control) { + case 'wheel': + // Set grid position + r = keepWithin(Math.ceil(hsb.s * 0.75), 0, grid.height() / 2); + phi = hsb.h * Math.PI / 180; + x = keepWithin(75 - Math.cos(phi) * r, 0, grid.width()); + y = keepWithin(75 - Math.sin(phi) * r, 0, grid.height()); + gridPicker.css({ + top: y + 'px', + left: x + 'px' + }); + + // Set slider position + y = 150 - (hsb.b / (100 / grid.height())); + if(hex === '') y = 0; + sliderPicker.css('top', y + 'px'); + + // Update panel color + slider.css('backgroundColor', hsb2hex({ h: hsb.h, s: hsb.s, b: 100 })); + break; + + case 'saturation': + // Set grid position + x = keepWithin((5 * hsb.h) / 12, 0, 150); + y = keepWithin(grid.height() - Math.ceil(hsb.b / (100 / grid.height())), 0, grid.height()); + gridPicker.css({ + top: y + 'px', + left: x + 'px' + }); + + // Set slider position + y = keepWithin(slider.height() - (hsb.s * (slider.height() / 100)), 0, slider.height()); + sliderPicker.css('top', y + 'px'); + + // Update UI + slider.css('backgroundColor', hsb2hex({ h: hsb.h, s: 100, b: hsb.b })); + minicolors.find('.minicolors-grid-inner').css('opacity', hsb.s / 100); + break; + + case 'brightness': + // Set grid position + x = keepWithin((5 * hsb.h) / 12, 0, 150); + y = keepWithin(grid.height() - Math.ceil(hsb.s / (100 / grid.height())), 0, grid.height()); + gridPicker.css({ + top: y + 'px', + left: x + 'px' + }); + + // Set slider position + y = keepWithin(slider.height() - (hsb.b * (slider.height() / 100)), 0, slider.height()); + sliderPicker.css('top', y + 'px'); + + // Update UI + slider.css('backgroundColor', hsb2hex({ h: hsb.h, s: hsb.s, b: 100 })); + minicolors.find('.minicolors-grid-inner').css('opacity', 1 - (hsb.b / 100)); + break; + + default: + // Set grid position + x = keepWithin(Math.ceil(hsb.s / (100 / grid.width())), 0, grid.width()); + y = keepWithin(grid.height() - Math.ceil(hsb.b / (100 / grid.height())), 0, grid.height()); + gridPicker.css({ + top: y + 'px', + left: x + 'px' + }); + + // Set slider position + y = keepWithin(slider.height() - (hsb.h / (360 / slider.height())), 0, slider.height()); + sliderPicker.css('top', y + 'px'); + + // Update panel color + grid.css('backgroundColor', hsb2hex({ h: hsb.h, s: 100, b: 100 })); + break; + } + + // Fire change event, but only if minicolors is fully initialized + if(input.data('minicolors-initialized')) { + doChange(input, value, opacity); + } + } + + // Runs the change and changeDelay callbacks + function doChange(input, value, opacity) { + var settings = input.data('minicolors-settings'); + var lastChange = input.data('minicolors-lastChange'); + var obj, sel, i; + + // Only run if it actually changed + if(!lastChange || lastChange.value !== value || lastChange.opacity !== opacity) { + + // Remember last-changed value + input.data('minicolors-lastChange', { + value: value, + opacity: opacity + }); + + // Check and select applicable swatch + if(settings.swatches && settings.swatches.length !== 0) { + if(!isRgb(value)) { + obj = hex2rgb(value); + } + else { + obj = parseRgb(value, true); + } + sel = -1; + for(i = 0; i < settings.swatches.length; ++i) { + if(obj.r === settings.swatches[i].r && obj.g === settings.swatches[i].g && obj.b === settings.swatches[i].b && obj.a === settings.swatches[i].a) { + sel = i; + break; + } + } + + input.parent().find('.minicolors-swatches .minicolors-swatch').removeClass('selected'); + if(sel !== -1) { + input.parent().find('.minicolors-swatches .minicolors-swatch').eq(i).addClass('selected'); + } + } + + // Fire change event + if(settings.change) { + if(settings.changeDelay) { + // Call after a delay + clearTimeout(input.data('minicolors-changeTimeout')); + input.data('minicolors-changeTimeout', setTimeout(function() { + settings.change.call(input.get(0), value, opacity); + }, settings.changeDelay)); + } else { + // Call immediately + settings.change.call(input.get(0), value, opacity); + } + } + input.trigger('change').trigger('input'); + } + } + + // Generates an RGB(A) object based on the input's value + function rgbObject(input) { + var rgb, + opacity = $(input).attr('data-opacity'); + if( isRgb($(input).val()) ) { + rgb = parseRgb($(input).val(), true); + } else { + var hex = parseHex($(input).val(), true); + rgb = hex2rgb(hex); + } + if( !rgb ) return null; + if( opacity !== undefined ) $.extend(rgb, { a: parseFloat(opacity) }); + return rgb; + } + + // Generates an RGB(A) string based on the input's value + function rgbString(input, alpha) { + var rgb, + opacity = $(input).attr('data-opacity'); + if( isRgb($(input).val()) ) { + rgb = parseRgb($(input).val(), true); + } else { + var hex = parseHex($(input).val(), true); + rgb = hex2rgb(hex); + } + if( !rgb ) return null; + if( opacity === undefined ) opacity = 1; + if( alpha ) { + return 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + parseFloat(opacity) + ')'; + } else { + return 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')'; + } + } + + // Converts to the letter case specified in settings + function convertCase(string, letterCase) { + return letterCase === 'uppercase' ? string.toUpperCase() : string.toLowerCase(); + } + + // Parses a string and returns a valid hex string when possible + function parseHex(string, expand) { + string = string.replace(/^#/g, ''); + if(!string.match(/^[A-F0-9]{3,6}/ig)) return ''; + if(string.length !== 3 && string.length !== 6) return ''; + if(string.length === 3 && expand) { + string = string[0] + string[0] + string[1] + string[1] + string[2] + string[2]; + } + return '#' + string; + } + + // Parses a string and returns a valid RGB(A) string when possible + function parseRgb(string, obj) { + var values = string.replace(/[^\d,.]/g, ''); + var rgba = values.split(','); + + rgba[0] = keepWithin(parseInt(rgba[0], 10), 0, 255); + rgba[1] = keepWithin(parseInt(rgba[1], 10), 0, 255); + rgba[2] = keepWithin(parseInt(rgba[2], 10), 0, 255); + if(rgba[3] !== undefined) { + rgba[3] = keepWithin(parseFloat(rgba[3], 10), 0, 1); + } + + // Return RGBA object + if( obj ) { + if (rgba[3] !== undefined) { + return { + r: rgba[0], + g: rgba[1], + b: rgba[2], + a: rgba[3] + }; + } else { + return { + r: rgba[0], + g: rgba[1], + b: rgba[2] + }; + } + } + + // Return RGBA string + if(typeof(rgba[3]) !== 'undefined' && rgba[3] <= 1) { + return 'rgba(' + rgba[0] + ', ' + rgba[1] + ', ' + rgba[2] + ', ' + rgba[3] + ')'; + } else { + return 'rgb(' + rgba[0] + ', ' + rgba[1] + ', ' + rgba[2] + ')'; + } + + } + + // Parses a string and returns a valid color string when possible + function parseInput(string, expand) { + if(isRgb(string)) { + // Returns a valid rgb(a) string + return parseRgb(string); + } else { + return parseHex(string, expand); + } + } + + // Keeps value within min and max + function keepWithin(value, min, max) { + if(value < min) value = min; + if(value > max) value = max; + return value; + } + + // Checks if a string is a valid RGB(A) string + function isRgb(string) { + var rgb = string.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); + return (rgb && rgb.length === 4) ? true : false; + } + + // Function to get alpha from a RGB(A) string + function getAlpha(rgba) { + rgba = rgba.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+(\.\d{1,2})?|\.\d{1,2})[\s+]?/i); + return (rgba && rgba.length === 6) ? rgba[4] : '1'; + } + + // Converts an HSB object to an RGB object + function hsb2rgb(hsb) { + var rgb = {}; + var h = Math.round(hsb.h); + var s = Math.round(hsb.s * 255 / 100); + var v = Math.round(hsb.b * 255 / 100); + if(s === 0) { + rgb.r = rgb.g = rgb.b = v; + } else { + var t1 = v; + var t2 = (255 - s) * v / 255; + var t3 = (t1 - t2) * (h % 60) / 60; + if(h === 360) h = 0; + if(h < 60) { rgb.r = t1; rgb.b = t2; rgb.g = t2 + t3; } + else if(h < 120) {rgb.g = t1; rgb.b = t2; rgb.r = t1 - t3; } + else if(h < 180) {rgb.g = t1; rgb.r = t2; rgb.b = t2 + t3; } + else if(h < 240) {rgb.b = t1; rgb.r = t2; rgb.g = t1 - t3; } + else if(h < 300) {rgb.b = t1; rgb.g = t2; rgb.r = t2 + t3; } + else if(h < 360) {rgb.r = t1; rgb.g = t2; rgb.b = t1 - t3; } + else { rgb.r = 0; rgb.g = 0; rgb.b = 0; } + } + return { + r: Math.round(rgb.r), + g: Math.round(rgb.g), + b: Math.round(rgb.b) + }; + } + + // Converts an RGB string to a hex string + function rgbString2hex(rgb){ + rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); + return (rgb && rgb.length === 4) ? '#' + + ('0' + parseInt(rgb[1],10).toString(16)).slice(-2) + + ('0' + parseInt(rgb[2],10).toString(16)).slice(-2) + + ('0' + parseInt(rgb[3],10).toString(16)).slice(-2) : ''; + } + + // Converts an RGB object to a hex string + function rgb2hex(rgb) { + var hex = [ + rgb.r.toString(16), + rgb.g.toString(16), + rgb.b.toString(16) + ]; + $.each(hex, function(nr, val) { + if(val.length === 1) hex[nr] = '0' + val; + }); + return '#' + hex.join(''); + } + + // Converts an HSB object to a hex string + function hsb2hex(hsb) { + return rgb2hex(hsb2rgb(hsb)); + } + + // Converts a hex string to an HSB object + function hex2hsb(hex) { + var hsb = rgb2hsb(hex2rgb(hex)); + if(hsb.s === 0) hsb.h = 360; + return hsb; + } + + // Converts an RGB object to an HSB object + function rgb2hsb(rgb) { + var hsb = { h: 0, s: 0, b: 0 }; + var min = Math.min(rgb.r, rgb.g, rgb.b); + var max = Math.max(rgb.r, rgb.g, rgb.b); + var delta = max - min; + hsb.b = max; + hsb.s = max !== 0 ? 255 * delta / max : 0; + if(hsb.s !== 0) { + if(rgb.r === max) { + hsb.h = (rgb.g - rgb.b) / delta; + } else if(rgb.g === max) { + hsb.h = 2 + (rgb.b - rgb.r) / delta; + } else { + hsb.h = 4 + (rgb.r - rgb.g) / delta; + } + } else { + hsb.h = -1; + } + hsb.h *= 60; + if(hsb.h < 0) { + hsb.h += 360; + } + hsb.s *= 100/255; + hsb.b *= 100/255; + return hsb; + } + + // Converts a hex string to an RGB object + function hex2rgb(hex) { + hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16); + return { + r: hex >> 16, + g: (hex & 0x00FF00) >> 8, + b: (hex & 0x0000FF) + }; + } + + // Handle events + $([document]) + // Hide on clicks outside of the control + .on('mousedown.minicolors touchstart.minicolors', function(event) { + if(!$(event.target).parents().add(event.target).hasClass('minicolors')) { + hide(); + } + }) + // Start moving + .on('mousedown.minicolors touchstart.minicolors', '.minicolors-grid, .minicolors-slider, .minicolors-opacity-slider', function(event) { + var target = $(this); + event.preventDefault(); + $(event.delegateTarget).data('minicolors-target', target); + move(target, event, true); + }) + // Move pickers + .on('mousemove.minicolors touchmove.minicolors', function(event) { + var target = $(event.delegateTarget).data('minicolors-target'); + if(target) move(target, event); + }) + // Stop moving + .on('mouseup.minicolors touchend.minicolors', function() { + $(this).removeData('minicolors-target'); + }) + // Selected a swatch + .on('click.minicolors', '.minicolors-swatches li', function(event) { + event.preventDefault(); + var target = $(this), input = target.parents('.minicolors').find('.minicolors-input'), color = target.data('swatch-color'); + updateInput(input, color, getAlpha(color)); + updateFromInput(input); + }) + // Show panel when swatch is clicked + .on('mousedown.minicolors touchstart.minicolors', '.minicolors-input-swatch', function(event) { + var input = $(this).parent().find('.minicolors-input'); + event.preventDefault(); + show(input); + }) + // Show on focus + .on('focus.minicolors', '.minicolors-input', function() { + var input = $(this); + if(!input.data('minicolors-initialized')) return; + show(input); + }) + // Update value on blur + .on('blur.minicolors', '.minicolors-input', function() { + var input = $(this); + var settings = input.data('minicolors-settings'); + var keywords; + var hex; + var rgba; + var swatchOpacity; + var value; + + if(!input.data('minicolors-initialized')) return; + + // Get array of lowercase keywords + keywords = !settings.keywords ? [] : $.map(settings.keywords.split(','), function(a) { + return $.trim(a.toLowerCase()); + }); + + // Set color string + if(input.val() !== '' && $.inArray(input.val().toLowerCase(), keywords) > -1) { + value = input.val(); + } else { + // Get RGBA values for easy conversion + if(isRgb(input.val())) { + rgba = parseRgb(input.val(), true); + } else { + hex = parseHex(input.val(), true); + rgba = hex ? hex2rgb(hex) : null; + } + + // Convert to format + if(rgba === null) { + value = settings.defaultValue; + } else if(settings.format === 'rgb') { + value = settings.opacity ? + parseRgb('rgba(' + rgba.r + ',' + rgba.g + ',' + rgba.b + ',' + input.attr('data-opacity') + ')') : + parseRgb('rgb(' + rgba.r + ',' + rgba.g + ',' + rgba.b + ')'); + } else { + value = rgb2hex(rgba); + } + } + + // Update swatch opacity + swatchOpacity = settings.opacity ? input.attr('data-opacity') : 1; + if(value.toLowerCase() === 'transparent') swatchOpacity = 0; + input + .closest('.minicolors') + .find('.minicolors-input-swatch > span') + .css('opacity', String(swatchOpacity)); + + // Set input value + input.val(value); + + // Is it blank? + if(input.val() === '') input.val(parseInput(settings.defaultValue, true)); + + // Adjust case + input.val(convertCase(input.val(), settings.letterCase)); + + }) + // Handle keypresses + .on('keydown.minicolors', '.minicolors-input', function(event) { + var input = $(this); + if(!input.data('minicolors-initialized')) return; + switch(event.which) { + case 9: // tab + hide(); + break; + case 13: // enter + case 27: // esc + hide(); + input.blur(); + break; + } + }) + // Update on keyup + .on('keyup.minicolors', '.minicolors-input', function() { + var input = $(this); + if(!input.data('minicolors-initialized')) return; + updateFromInput(input, true); + }) + // Update on paste + .on('paste.minicolors', '.minicolors-input', function() { + var input = $(this); + if(!input.data('minicolors-initialized')) return; + setTimeout(function() { + updateFromInput(input, true); + }, 1); + }); +})); diff --git a/octoprint_octolapse/static/js/jquery.validate.min.js b/octoprint_octolapse/static/js/jquery.validate.min.js index 20402da5..6abe4312 100644 --- a/octoprint_octolapse/static/js/jquery.validate.min.js +++ b/octoprint_octolapse/static/js/jquery.validate.min.js @@ -1,4 +1,1657 @@ -/*! jQuery Validation Plugin - v1.17.0 - 7/29/2017 +/*! + * jQuery Validation Plugin v1.19.2 + * * https://jqueryvalidation.org/ - * Copyright (c) 2017 Jörn Zaefferer; Licensed MIT */ -!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){a.extend(a.fn,{validate:function(b){if(!this.length)return void(b&&b.debug&&window.console&&console.warn("Nothing selected, can't validate, returning nothing."));var c=a.data(this[0],"validator");return c?c:(this.attr("novalidate","novalidate"),c=new a.validator(b,this[0]),a.data(this[0],"validator",c),c.settings.onsubmit&&(this.on("click.validate",":submit",function(b){c.submitButton=b.currentTarget,a(this).hasClass("cancel")&&(c.cancelSubmit=!0),void 0!==a(this).attr("formnovalidate")&&(c.cancelSubmit=!0)}),this.on("submit.validate",function(b){function d(){var d,e;return c.submitButton&&(c.settings.submitHandler||c.formSubmitted)&&(d=a("").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),!c.settings.submitHandler||(e=c.settings.submitHandler.call(c,c.currentForm,b),d&&d.remove(),void 0!==e&&e)}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,b||(d=d.concat(c.errorList))}),c.errorList=d),b},rules:function(b,c){var d,e,f,g,h,i,j=this[0];if(null!=j&&(!j.form&&j.hasAttribute("contenteditable")&&(j.form=this.closest("form")[0],j.name=this.attr("name")),null!=j.form)){if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(a,b){i[b]=f[b],delete f[b]}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g)),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}}),a.extend(a.expr.pseudos||a.expr[":"],{blank:function(b){return!a.trim(""+a(b).val())},filled:function(b){var c=a(b).val();return null!==c&&!!a.trim(""+c)},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:void 0===c?b:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",pendingClass:"pending",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||a.inArray(c.keyCode,d)!==-1||(b.name in this.submitted||b.name in this.invalid)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}."),step:a.validator.format("Please enter a multiple of {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){!this.form&&this.hasAttribute("contenteditable")&&(this.form=a(this).closest("form")[0],this.name=a(this).attr("name"));var c=a.data(this.form,"validator"),d="on"+b.type.replace(/^validate/,""),e=c.settings;e[d]&&!a(this).is(e.ignore)&&e[d].call(c,this,b)}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){d[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox'], [contenteditable], [type='button']",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler)},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c,d,e=this.clean(b),f=this.validationTargetFor(e),g=this,h=!0;return void 0===f?delete this.invalid[e.name]:(this.prepareElement(f),this.currentElements=a(f),d=this.groups[f.name],d&&a.each(this.groups,function(a,b){b===d&&a!==f.name&&(e=g.validationTargetFor(g.clean(g.findByName(a))),e&&e.name in g.invalid&&(g.currentElements.push(e),h=g.check(e)&&h))}),c=this.check(f)!==!1,h=h&&c,c?this.invalid[f.name]=!1:this.invalid[f.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),a(b).attr("aria-invalid",!c)),h},showErrors:function(b){if(b){var c=this;a.extend(this.errorMap,b),this.errorList=a.map(this.errorMap,function(a,b){return{message:a,element:c.findByName(b)[0]}}),this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.invalid={},this.submitted={},this.prepareForm(),this.hideErrors();var b=this.elements().removeData("previousValue").removeAttr("aria-invalid");this.resetElements(b)},resetElements:function(a){var b;if(this.settings.unhighlight)for(b=0;a[b];b++)this.settings.unhighlight.call(this,a[b],this.settings.errorClass,""),this.findByName(a[b].name).removeClass(this.settings.validClass);else a.removeClass(this.settings.errorClass).removeClass(this.settings.validClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)void 0!==a[b]&&null!==a[b]&&a[b]!==!1&&c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea, [contenteditable]").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){var d=this.name||a(this).attr("name");return!d&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),this.hasAttribute("contenteditable")&&(this.form=a(this).closest("form")[0],this.name=d),!(d in c||!b.objectLength(a(this).rules()))&&(c[d]=!0,!0)})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},resetInternals:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([])},reset:function(){this.resetInternals(),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d,e=a(b),f=b.type;return"radio"===f||"checkbox"===f?this.findByName(b.name).filter(":checked").val():"number"===f&&"undefined"!=typeof b.validity?b.validity.badInput?"NaN":e.val():(c=b.hasAttribute("contenteditable")?e.text():e.val(),"file"===f?"C:\\fakepath\\"===c.substr(0,12)?c.substr(12):(d=c.lastIndexOf("/"),d>=0?c.substr(d+1):(d=c.lastIndexOf("\\"),d>=0?c.substr(d+1):c)):"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f,g=a(b).rules(),h=a.map(g,function(a,b){return b}).length,i=!1,j=this.elementValue(b);if("function"==typeof g.normalizer?f=g.normalizer:"function"==typeof this.settings.normalizer&&(f=this.settings.normalizer),f){if(j=f.call(b,j),"string"!=typeof j)throw new TypeError("The normalizer should return a string value.");delete g.normalizer}for(d in g){e={method:d,parameters:g[d]};try{if(c=a.validator.methods[d].call(this,j,b,e.parameters),"dependency-mismatch"===c&&1===h){i=!0;continue}if(i=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(k){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",k),k instanceof TypeError&&(k.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),k}}if(!i)return this.objectLength(g)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+""),e=/\$?\{(\d+)\}/g;return"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),d},formatAndAdd:function(a,b){var c=this.defaultMessage(a,b);this.errorList.push({message:c,element:a,method:b.method}),this.errorMap[a.name]=c,this.submitted[a.name]=c},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g,h=this.errorsFor(b),i=this.idOrName(b),j=a(b).attr("aria-describedby");h.length?(h.removeClass(this.settings.validClass).addClass(this.settings.errorClass),h.html(c)):(h=a("<"+this.settings.errorElement+">").attr("id",i+"-error").addClass(this.settings.errorClass).html(c||""),d=h,this.settings.wrapper&&(d=h.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement.call(this,d,a(b)):d.insertAfter(b),h.is("label")?h.attr("for",i):0===h.parents("label[for='"+this.escapeCssMeta(i)+"']").length&&(f=h.attr("id"),j?j.match(new RegExp("\\b"+this.escapeCssMeta(f)+"\\b"))||(j+=" "+f):j=f,a(b).attr("aria-describedby",j),e=this.groups[b.name],e&&(g=this,a.each(g.groups,function(b,c){c===e&&a("[name='"+g.escapeCssMeta(b)+"']",g.currentForm).attr("aria-describedby",h.attr("id"))})))),!c&&this.settings.success&&(h.text(""),"string"==typeof this.settings.success?h.addClass(this.settings.success):this.settings.success(h,b)),this.toShow=this.toShow.add(h)},errorsFor:function(b){var c=this.escapeCssMeta(this.idOrName(b)),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+this.escapeCssMeta(d).replace(/\s+/g,", #")),this.errors().filter(e)},escapeCssMeta:function(a){return a.replace(/([\\!"#$%&'()*+,.\/:;<=>?@\[\]^`{|}~])/g,"\\$1")},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+this.escapeCssMeta(b)+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return!this.dependTypes[typeof a]||this.dependTypes[typeof a](a,b)},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(b){this.pending[b.name]||(this.pendingRequest++,a(b).addClass(this.settings.pendingClass),this.pending[b.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],a(b).removeClass(this.settings.pendingClass),c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.submitButton&&a("input:hidden[name='"+this.submitButton.name+"']",this.currentForm).remove(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b,c){return c="string"==typeof c&&c||"remote",a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,{method:c})})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator").find(".validate-equalTo-blur").off(".validate-equalTo").removeClass("validate-equalTo-blur")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max|step/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0===e.param||e.param:(a.data(c.form,"validator").resetElements(a(c)),delete b[d])}}),a.each(b,function(d,e){b[d]=a.isFunction(e)&&"normalizer"!==d?e(c):e}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var c;b[this]&&(a.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(c=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(c[0]),Number(c[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[\/?#]\S*)?$/i.test(a)},date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a).toString())},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},minlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d},maxlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e<=d},rangelength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d[0]&&e<=d[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||a<=c},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},step:function(b,c,d){var e,f=a(c).attr("type"),g="Step attribute on input type "+f+" is not supported.",h=["text","number","range"],i=new RegExp("\\b"+f+"\\b"),j=f&&!i.test(h.join()),k=function(a){var b=(""+a).match(/(?:\.(\d+))?$/);return b&&b[1]?b[1].length:0},l=function(a){return Math.round(a*Math.pow(10,e))},m=!0;if(j)throw new Error(g);return e=k(d),(k(b)>e||l(b)%l(d)!==0)&&(m=!1),this.optional(c)||m},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.not(".validate-equalTo-blur").length&&e.addClass("validate-equalTo-blur").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d,e){if(this.optional(c))return"dependency-mismatch";e="string"==typeof e&&e||"remote";var f,g,h,i=this.previousValue(c,e);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),i.originalMessage=i.originalMessage||this.settings.messages[c.name][e],this.settings.messages[c.name][e]=i.message,d="string"==typeof d&&{url:d}||d,h=a.param(a.extend({data:b},d.data)),i.old===h?i.valid:(i.old=h,f=this,this.startRequest(c),g={},g[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:g,context:f.currentForm,success:function(a){var d,g,h,j=a===!0||"true"===a;f.settings.messages[c.name][e]=i.originalMessage,j?(h=f.formSubmitted,f.resetInternals(),f.toHide=f.errorsFor(c),f.formSubmitted=h,f.successList.push(c),f.invalid[c.name]=!1,f.showErrors()):(d={},g=a||f.defaultMessage(c,{method:e,parameters:b}),d[c.name]=i.message=g,f.invalid[c.name]=!0,f.showErrors(d)),i.valid=j,f.stopRequest(c,j)}},d)),"pending")}}});var b,c={};return a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,d){var e=a.port;"abort"===a.mode&&(c[e]&&c[e].abort(),c[e]=d)}):(b=a.ajax,a.ajax=function(d){var e=("mode"in d?d:a.ajaxSettings).mode,f=("port"in d?d:a.ajaxSettings).port;return"abort"===e?(c[f]&&c[f].abort(),c[f]=b.apply(this,arguments),c[f]):b.apply(this,arguments)}),a}); \ No newline at end of file + * + * Copyright (c) 2020 Jörn Zaefferer + * Released under the MIT license + */ +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + define( ["jquery"], factory ); + } else if (typeof module === "object" && module.exports) { + module.exports = factory( require( "jquery" ) ); + } else { + factory( jQuery ); + } +}(function( $ ) { + +$.extend( $.fn, { + + // https://jqueryvalidation.org/validate/ + validate: function( options ) { + + // If nothing is selected, return nothing; can't chain anyway + if ( !this.length ) { + if ( options && options.debug && window.console ) { + console.warn( "Nothing selected, can't validate, returning nothing." ); + } + return; + } + + // Check if a validator for this form was already created + var validator = $.data( this[ 0 ], "validator" ); + if ( validator ) { + return validator; + } + + // Add novalidate tag if HTML5. + this.attr( "novalidate", "novalidate" ); + + validator = new $.validator( options, this[ 0 ] ); + $.data( this[ 0 ], "validator", validator ); + + if ( validator.settings.onsubmit ) { + + this.on( "click.validate", ":submit", function( event ) { + + // Track the used submit button to properly handle scripted + // submits later. + validator.submitButton = event.currentTarget; + + // Allow suppressing validation by adding a cancel class to the submit button + if ( $( this ).hasClass( "cancel" ) ) { + validator.cancelSubmit = true; + } + + // Allow suppressing validation by adding the html5 formnovalidate attribute to the submit button + if ( $( this ).attr( "formnovalidate" ) !== undefined ) { + validator.cancelSubmit = true; + } + } ); + + // Validate the form on submit + this.on( "submit.validate", function( event ) { + if ( validator.settings.debug ) { + + // Prevent form submit to be able to see console output + event.preventDefault(); + } + + function handle() { + var hidden, result; + + // Insert a hidden input as a replacement for the missing submit button + // The hidden input is inserted in two cases: + // - A user defined a `submitHandler` + // - There was a pending request due to `remote` method and `stopRequest()` + // was called to submit the form in case it's valid + if ( validator.submitButton && ( validator.settings.submitHandler || validator.formSubmitted ) ) { + hidden = $( "" ) + .attr( "name", validator.submitButton.name ) + .val( $( validator.submitButton ).val() ) + .appendTo( validator.currentForm ); + } + + if ( validator.settings.submitHandler && !validator.settings.debug ) { + result = validator.settings.submitHandler.call( validator, validator.currentForm, event ); + if ( hidden ) { + + // And clean up afterwards; thanks to no-block-scope, hidden can be referenced + hidden.remove(); + } + if ( result !== undefined ) { + return result; + } + return false; + } + return true; + } + + // Prevent submit for invalid forms or custom submit handlers + if ( validator.cancelSubmit ) { + validator.cancelSubmit = false; + return handle(); + } + if ( validator.form() ) { + if ( validator.pendingRequest ) { + validator.formSubmitted = true; + return false; + } + return handle(); + } else { + validator.focusInvalid(); + return false; + } + } ); + } + + return validator; + }, + + // https://jqueryvalidation.org/valid/ + valid: function() { + var valid, validator, errorList; + + if ( $( this[ 0 ] ).is( "form" ) ) { + valid = this.validate().form(); + } else { + errorList = []; + valid = true; + validator = $( this[ 0 ].form ).validate(); + this.each( function() { + valid = validator.element( this ) && valid; + if ( !valid ) { + errorList = errorList.concat( validator.errorList ); + } + } ); + validator.errorList = errorList; + } + return valid; + }, + + // https://jqueryvalidation.org/rules/ + rules: function( command, argument ) { + var element = this[ 0 ], + isContentEditable = typeof this.attr( "contenteditable" ) !== "undefined" && this.attr( "contenteditable" ) !== "false", + settings, staticRules, existingRules, data, param, filtered; + + // If nothing is selected, return empty object; can't chain anyway + if ( element == null ) { + return; + } + + if ( !element.form && isContentEditable ) { + element.form = this.closest( "form" )[ 0 ]; + element.name = this.attr( "name" ); + } + + if ( element.form == null ) { + return; + } + + if ( command ) { + settings = $.data( element.form, "validator" ).settings; + staticRules = settings.rules; + existingRules = $.validator.staticRules( element ); + switch ( command ) { + case "add": + $.extend( existingRules, $.validator.normalizeRule( argument ) ); + + // Remove messages from rules, but allow them to be set separately + delete existingRules.messages; + staticRules[ element.name ] = existingRules; + if ( argument.messages ) { + settings.messages[ element.name ] = $.extend( settings.messages[ element.name ], argument.messages ); + } + break; + case "remove": + if ( !argument ) { + delete staticRules[ element.name ]; + return existingRules; + } + filtered = {}; + $.each( argument.split( /\s/ ), function( index, method ) { + filtered[ method ] = existingRules[ method ]; + delete existingRules[ method ]; + } ); + return filtered; + } + } + + data = $.validator.normalizeRules( + $.extend( + {}, + $.validator.classRules( element ), + $.validator.attributeRules( element ), + $.validator.dataRules( element ), + $.validator.staticRules( element ) + ), element ); + + // Make sure required is at front + if ( data.required ) { + param = data.required; + delete data.required; + data = $.extend( { required: param }, data ); + } + + // Make sure remote is at back + if ( data.remote ) { + param = data.remote; + delete data.remote; + data = $.extend( data, { remote: param } ); + } + + return data; + } +} ); + +// JQuery trim is deprecated, provide a trim method based on String.prototype.trim +var trim = function( str ) { + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim#Polyfill + return str.replace( /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "" ); +}; + +// Custom selectors +$.extend( $.expr.pseudos || $.expr[ ":" ], { // '|| $.expr[ ":" ]' here enables backwards compatibility to jQuery 1.7. Can be removed when dropping jQ 1.7.x support + + // https://jqueryvalidation.org/blank-selector/ + blank: function( a ) { + return !trim( "" + $( a ).val() ); + }, + + // https://jqueryvalidation.org/filled-selector/ + filled: function( a ) { + var val = $( a ).val(); + return val !== null && !!trim( "" + val ); + }, + + // https://jqueryvalidation.org/unchecked-selector/ + unchecked: function( a ) { + return !$( a ).prop( "checked" ); + } +} ); + +// Constructor for validator +$.validator = function( options, form ) { + this.settings = $.extend( true, {}, $.validator.defaults, options ); + this.currentForm = form; + this.init(); +}; + +// https://jqueryvalidation.org/jQuery.validator.format/ +$.validator.format = function( source, params ) { + if ( arguments.length === 1 ) { + return function() { + var args = $.makeArray( arguments ); + args.unshift( source ); + return $.validator.format.apply( this, args ); + }; + } + if ( params === undefined ) { + return source; + } + if ( arguments.length > 2 && params.constructor !== Array ) { + params = $.makeArray( arguments ).slice( 1 ); + } + if ( params.constructor !== Array ) { + params = [ params ]; + } + $.each( params, function( i, n ) { + source = source.replace( new RegExp( "\\{" + i + "\\}", "g" ), function() { + return n; + } ); + } ); + return source; +}; + +$.extend( $.validator, { + + defaults: { + messages: {}, + groups: {}, + rules: {}, + errorClass: "error", + pendingClass: "pending", + validClass: "valid", + errorElement: "label", + focusCleanup: false, + focusInvalid: true, + errorContainer: $( [] ), + errorLabelContainer: $( [] ), + onsubmit: true, + ignore: ":hidden", + ignoreTitle: false, + onfocusin: function( element ) { + this.lastActive = element; + + // Hide error label and remove error class on focus if enabled + if ( this.settings.focusCleanup ) { + if ( this.settings.unhighlight ) { + this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass ); + } + this.hideThese( this.errorsFor( element ) ); + } + }, + onfocusout: function( element ) { + if ( !this.checkable( element ) && ( element.name in this.submitted || !this.optional( element ) ) ) { + this.element( element ); + } + }, + onkeyup: function( element, event ) { + + // Avoid revalidate the field when pressing one of the following keys + // Shift => 16 + // Ctrl => 17 + // Alt => 18 + // Caps lock => 20 + // End => 35 + // Home => 36 + // Left arrow => 37 + // Up arrow => 38 + // Right arrow => 39 + // Down arrow => 40 + // Insert => 45 + // Num lock => 144 + // AltGr key => 225 + var excludedKeys = [ + 16, 17, 18, 20, 35, 36, 37, + 38, 39, 40, 45, 144, 225 + ]; + + if ( event.which === 9 && this.elementValue( element ) === "" || $.inArray( event.keyCode, excludedKeys ) !== -1 ) { + return; + } else if ( element.name in this.submitted || element.name in this.invalid ) { + this.element( element ); + } + }, + onclick: function( element ) { + + // Click on selects, radiobuttons and checkboxes + if ( element.name in this.submitted ) { + this.element( element ); + + // Or option elements, check parent select in that case + } else if ( element.parentNode.name in this.submitted ) { + this.element( element.parentNode ); + } + }, + highlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName( element.name ).addClass( errorClass ).removeClass( validClass ); + } else { + $( element ).addClass( errorClass ).removeClass( validClass ); + } + }, + unhighlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName( element.name ).removeClass( errorClass ).addClass( validClass ); + } else { + $( element ).removeClass( errorClass ).addClass( validClass ); + } + } + }, + + // https://jqueryvalidation.org/jQuery.validator.setDefaults/ + setDefaults: function( settings ) { + $.extend( $.validator.defaults, settings ); + }, + + messages: { + required: "This field is required.", + remote: "Please fix this field.", + email: "Please enter a valid email address.", + url: "Please enter a valid URL.", + date: "Please enter a valid date.", + dateISO: "Please enter a valid date (ISO).", + number: "Please enter a valid number.", + digits: "Please enter only digits.", + equalTo: "Please enter the same value again.", + maxlength: $.validator.format( "Please enter no more than {0} characters." ), + minlength: $.validator.format( "Please enter at least {0} characters." ), + rangelength: $.validator.format( "Please enter a value between {0} and {1} characters long." ), + range: $.validator.format( "Please enter a value between {0} and {1}." ), + max: $.validator.format( "Please enter a value less than or equal to {0}." ), + min: $.validator.format( "Please enter a value greater than or equal to {0}." ), + step: $.validator.format( "Please enter a multiple of {0}." ) + }, + + autoCreateRanges: false, + + prototype: { + + init: function() { + this.labelContainer = $( this.settings.errorLabelContainer ); + this.errorContext = this.labelContainer.length && this.labelContainer || $( this.currentForm ); + this.containers = $( this.settings.errorContainer ).add( this.settings.errorLabelContainer ); + this.submitted = {}; + this.valueCache = {}; + this.pendingRequest = 0; + this.pending = {}; + this.invalid = {}; + this.reset(); + + var currentForm = this.currentForm, + groups = ( this.groups = {} ), + rules; + $.each( this.settings.groups, function( key, value ) { + if ( typeof value === "string" ) { + value = value.split( /\s/ ); + } + $.each( value, function( index, name ) { + groups[ name ] = key; + } ); + } ); + rules = this.settings.rules; + $.each( rules, function( key, value ) { + rules[ key ] = $.validator.normalizeRule( value ); + } ); + + function delegate( event ) { + var isContentEditable = typeof $( this ).attr( "contenteditable" ) !== "undefined" && $( this ).attr( "contenteditable" ) !== "false"; + + // Set form expando on contenteditable + if ( !this.form && isContentEditable ) { + this.form = $( this ).closest( "form" )[ 0 ]; + this.name = $( this ).attr( "name" ); + } + + // Ignore the element if it belongs to another form. This will happen mainly + // when setting the `form` attribute of an input to the id of another form. + if ( currentForm !== this.form ) { + return; + } + + var validator = $.data( this.form, "validator" ), + eventType = "on" + event.type.replace( /^validate/, "" ), + settings = validator.settings; + if ( settings[ eventType ] && !$( this ).is( settings.ignore ) ) { + settings[ eventType ].call( validator, this, event ); + } + } + + $( this.currentForm ) + .on( "focusin.validate focusout.validate keyup.validate", + ":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], " + + "[type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], " + + "[type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], " + + "[type='radio'], [type='checkbox'], [contenteditable], [type='button']", delegate ) + + // Support: Chrome, oldIE + // "select" is provided as event.target when clicking a option + .on( "click.validate", "select, option, [type='radio'], [type='checkbox']", delegate ); + + if ( this.settings.invalidHandler ) { + $( this.currentForm ).on( "invalid-form.validate", this.settings.invalidHandler ); + } + }, + + // https://jqueryvalidation.org/Validator.form/ + form: function() { + this.checkForm(); + $.extend( this.submitted, this.errorMap ); + this.invalid = $.extend( {}, this.errorMap ); + if ( !this.valid() ) { + $( this.currentForm ).triggerHandler( "invalid-form", [ this ] ); + } + this.showErrors(); + return this.valid(); + }, + + checkForm: function() { + this.prepareForm(); + for ( var i = 0, elements = ( this.currentElements = this.elements() ); elements[ i ]; i++ ) { + this.check( elements[ i ] ); + } + return this.valid(); + }, + + // https://jqueryvalidation.org/Validator.element/ + element: function( element ) { + var cleanElement = this.clean( element ), + checkElement = this.validationTargetFor( cleanElement ), + v = this, + result = true, + rs, group; + + if ( checkElement === undefined ) { + delete this.invalid[ cleanElement.name ]; + } else { + this.prepareElement( checkElement ); + this.currentElements = $( checkElement ); + + // If this element is grouped, then validate all group elements already + // containing a value + group = this.groups[ checkElement.name ]; + if ( group ) { + $.each( this.groups, function( name, testgroup ) { + if ( testgroup === group && name !== checkElement.name ) { + cleanElement = v.validationTargetFor( v.clean( v.findByName( name ) ) ); + if ( cleanElement && cleanElement.name in v.invalid ) { + v.currentElements.push( cleanElement ); + result = v.check( cleanElement ) && result; + } + } + } ); + } + + rs = this.check( checkElement ) !== false; + result = result && rs; + if ( rs ) { + this.invalid[ checkElement.name ] = false; + } else { + this.invalid[ checkElement.name ] = true; + } + + if ( !this.numberOfInvalids() ) { + + // Hide error containers on last error + this.toHide = this.toHide.add( this.containers ); + } + this.showErrors(); + + // Add aria-invalid status for screen readers + $( element ).attr( "aria-invalid", !rs ); + } + + return result; + }, + + // https://jqueryvalidation.org/Validator.showErrors/ + showErrors: function( errors ) { + if ( errors ) { + var validator = this; + + // Add items to error list and map + $.extend( this.errorMap, errors ); + this.errorList = $.map( this.errorMap, function( message, name ) { + return { + message: message, + element: validator.findByName( name )[ 0 ] + }; + } ); + + // Remove items from success list + this.successList = $.grep( this.successList, function( element ) { + return !( element.name in errors ); + } ); + } + if ( this.settings.showErrors ) { + this.settings.showErrors.call( this, this.errorMap, this.errorList ); + } else { + this.defaultShowErrors(); + } + }, + + // https://jqueryvalidation.org/Validator.resetForm/ + resetForm: function() { + if ( $.fn.resetForm ) { + $( this.currentForm ).resetForm(); + } + this.invalid = {}; + this.submitted = {}; + this.prepareForm(); + this.hideErrors(); + var elements = this.elements() + .removeData( "previousValue" ) + .removeAttr( "aria-invalid" ); + + this.resetElements( elements ); + }, + + resetElements: function( elements ) { + var i; + + if ( this.settings.unhighlight ) { + for ( i = 0; elements[ i ]; i++ ) { + this.settings.unhighlight.call( this, elements[ i ], + this.settings.errorClass, "" ); + this.findByName( elements[ i ].name ).removeClass( this.settings.validClass ); + } + } else { + elements + .removeClass( this.settings.errorClass ) + .removeClass( this.settings.validClass ); + } + }, + + numberOfInvalids: function() { + return this.objectLength( this.invalid ); + }, + + objectLength: function( obj ) { + /* jshint unused: false */ + var count = 0, + i; + for ( i in obj ) { + + // This check allows counting elements with empty error + // message as invalid elements + if ( obj[ i ] !== undefined && obj[ i ] !== null && obj[ i ] !== false ) { + count++; + } + } + return count; + }, + + hideErrors: function() { + this.hideThese( this.toHide ); + }, + + hideThese: function( errors ) { + errors.not( this.containers ).text( "" ); + this.addWrapper( errors ).hide(); + }, + + valid: function() { + return this.size() === 0; + }, + + size: function() { + return this.errorList.length; + }, + + focusInvalid: function() { + if ( this.settings.focusInvalid ) { + try { + $( this.findLastActive() || this.errorList.length && this.errorList[ 0 ].element || [] ) + .filter( ":visible" ) + .trigger( "focus" ) + + // Manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find + .trigger( "focusin" ); + } catch ( e ) { + + // Ignore IE throwing errors when focusing hidden elements + } + } + }, + + findLastActive: function() { + var lastActive = this.lastActive; + return lastActive && $.grep( this.errorList, function( n ) { + return n.element.name === lastActive.name; + } ).length === 1 && lastActive; + }, + + elements: function() { + var validator = this, + rulesCache = {}; + + // Select all valid inputs inside the form (no submit or reset buttons) + return $( this.currentForm ) + .find( "input, select, textarea, [contenteditable]" ) + .not( ":submit, :reset, :image, :disabled" ) + .not( this.settings.ignore ) + .filter( function() { + var name = this.name || $( this ).attr( "name" ); // For contenteditable + var isContentEditable = typeof $( this ).attr( "contenteditable" ) !== "undefined" && $( this ).attr( "contenteditable" ) !== "false"; + + if ( !name && validator.settings.debug && window.console ) { + console.error( "%o has no name assigned", this ); + } + + // Set form expando on contenteditable + if ( isContentEditable ) { + this.form = $( this ).closest( "form" )[ 0 ]; + this.name = name; + } + + // Ignore elements that belong to other/nested forms + if ( this.form !== validator.currentForm ) { + return false; + } + + // Select only the first element for each name, and only those with rules specified + if ( name in rulesCache || !validator.objectLength( $( this ).rules() ) ) { + return false; + } + + rulesCache[ name ] = true; + return true; + } ); + }, + + clean: function( selector ) { + return $( selector )[ 0 ]; + }, + + errors: function() { + var errorClass = this.settings.errorClass.split( " " ).join( "." ); + return $( this.settings.errorElement + "." + errorClass, this.errorContext ); + }, + + resetInternals: function() { + this.successList = []; + this.errorList = []; + this.errorMap = {}; + this.toShow = $( [] ); + this.toHide = $( [] ); + }, + + reset: function() { + this.resetInternals(); + this.currentElements = $( [] ); + }, + + prepareForm: function() { + this.reset(); + this.toHide = this.errors().add( this.containers ); + }, + + prepareElement: function( element ) { + this.reset(); + this.toHide = this.errorsFor( element ); + }, + + elementValue: function( element ) { + var $element = $( element ), + type = element.type, + isContentEditable = typeof $element.attr( "contenteditable" ) !== "undefined" && $element.attr( "contenteditable" ) !== "false", + val, idx; + + if ( type === "radio" || type === "checkbox" ) { + return this.findByName( element.name ).filter( ":checked" ).val(); + } else if ( type === "number" && typeof element.validity !== "undefined" ) { + return element.validity.badInput ? "NaN" : $element.val(); + } + + if ( isContentEditable ) { + val = $element.text(); + } else { + val = $element.val(); + } + + if ( type === "file" ) { + + // Modern browser (chrome & safari) + if ( val.substr( 0, 12 ) === "C:\\fakepath\\" ) { + return val.substr( 12 ); + } + + // Legacy browsers + // Unix-based path + idx = val.lastIndexOf( "/" ); + if ( idx >= 0 ) { + return val.substr( idx + 1 ); + } + + // Windows-based path + idx = val.lastIndexOf( "\\" ); + if ( idx >= 0 ) { + return val.substr( idx + 1 ); + } + + // Just the file name + return val; + } + + if ( typeof val === "string" ) { + return val.replace( /\r/g, "" ); + } + return val; + }, + + check: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + + var rules = $( element ).rules(), + rulesCount = $.map( rules, function( n, i ) { + return i; + } ).length, + dependencyMismatch = false, + val = this.elementValue( element ), + result, method, rule, normalizer; + + // Prioritize the local normalizer defined for this element over the global one + // if the former exists, otherwise user the global one in case it exists. + if ( typeof rules.normalizer === "function" ) { + normalizer = rules.normalizer; + } else if ( typeof this.settings.normalizer === "function" ) { + normalizer = this.settings.normalizer; + } + + // If normalizer is defined, then call it to retreive the changed value instead + // of using the real one. + // Note that `this` in the normalizer is `element`. + if ( normalizer ) { + val = normalizer.call( element, val ); + + // Delete the normalizer from rules to avoid treating it as a pre-defined method. + delete rules.normalizer; + } + + for ( method in rules ) { + rule = { method: method, parameters: rules[ method ] }; + try { + result = $.validator.methods[ method ].call( this, val, element, rule.parameters ); + + // If a method indicates that the field is optional and therefore valid, + // don't mark it as valid when there are no other rules + if ( result === "dependency-mismatch" && rulesCount === 1 ) { + dependencyMismatch = true; + continue; + } + dependencyMismatch = false; + + if ( result === "pending" ) { + this.toHide = this.toHide.not( this.errorsFor( element ) ); + return; + } + + if ( !result ) { + this.formatAndAdd( element, rule ); + return false; + } + } catch ( e ) { + if ( this.settings.debug && window.console ) { + console.log( "Exception occurred when checking element " + element.id + ", check the '" + rule.method + "' method.", e ); + } + if ( e instanceof TypeError ) { + e.message += ". Exception occurred when checking element " + element.id + ", check the '" + rule.method + "' method."; + } + + throw e; + } + } + if ( dependencyMismatch ) { + return; + } + if ( this.objectLength( rules ) ) { + this.successList.push( element ); + } + return true; + }, + + // Return the custom message for the given element and validation method + // specified in the element's HTML5 data attribute + // return the generic message if present and no method specific message is present + customDataMessage: function( element, method ) { + return $( element ).data( "msg" + method.charAt( 0 ).toUpperCase() + + method.substring( 1 ).toLowerCase() ) || $( element ).data( "msg" ); + }, + + // Return the custom message for the given element name and validation method + customMessage: function( name, method ) { + var m = this.settings.messages[ name ]; + return m && ( m.constructor === String ? m : m[ method ] ); + }, + + // Return the first defined argument, allowing empty strings + findDefined: function() { + for ( var i = 0; i < arguments.length; i++ ) { + if ( arguments[ i ] !== undefined ) { + return arguments[ i ]; + } + } + return undefined; + }, + + // The second parameter 'rule' used to be a string, and extended to an object literal + // of the following form: + // rule = { + // method: "method name", + // parameters: "the given method parameters" + // } + // + // The old behavior still supported, kept to maintain backward compatibility with + // old code, and will be removed in the next major release. + defaultMessage: function( element, rule ) { + if ( typeof rule === "string" ) { + rule = { method: rule }; + } + + var message = this.findDefined( + this.customMessage( element.name, rule.method ), + this.customDataMessage( element, rule.method ), + + // 'title' is never undefined, so handle empty string as undefined + !this.settings.ignoreTitle && element.title || undefined, + $.validator.messages[ rule.method ], + "Warning: No message defined for " + element.name + "" + ), + theregex = /\$?\{(\d+)\}/g; + if ( typeof message === "function" ) { + message = message.call( this, rule.parameters, element ); + } else if ( theregex.test( message ) ) { + message = $.validator.format( message.replace( theregex, "{$1}" ), rule.parameters ); + } + + return message; + }, + + formatAndAdd: function( element, rule ) { + var message = this.defaultMessage( element, rule ); + + this.errorList.push( { + message: message, + element: element, + method: rule.method + } ); + + this.errorMap[ element.name ] = message; + this.submitted[ element.name ] = message; + }, + + addWrapper: function( toToggle ) { + if ( this.settings.wrapper ) { + toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) ); + } + return toToggle; + }, + + defaultShowErrors: function() { + var i, elements, error; + for ( i = 0; this.errorList[ i ]; i++ ) { + error = this.errorList[ i ]; + if ( this.settings.highlight ) { + this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass ); + } + this.showLabel( error.element, error.message ); + } + if ( this.errorList.length ) { + this.toShow = this.toShow.add( this.containers ); + } + if ( this.settings.success ) { + for ( i = 0; this.successList[ i ]; i++ ) { + this.showLabel( this.successList[ i ] ); + } + } + if ( this.settings.unhighlight ) { + for ( i = 0, elements = this.validElements(); elements[ i ]; i++ ) { + this.settings.unhighlight.call( this, elements[ i ], this.settings.errorClass, this.settings.validClass ); + } + } + this.toHide = this.toHide.not( this.toShow ); + this.hideErrors(); + this.addWrapper( this.toShow ).show(); + }, + + validElements: function() { + return this.currentElements.not( this.invalidElements() ); + }, + + invalidElements: function() { + return $( this.errorList ).map( function() { + return this.element; + } ); + }, + + showLabel: function( element, message ) { + var place, group, errorID, v, + error = this.errorsFor( element ), + elementID = this.idOrName( element ), + describedBy = $( element ).attr( "aria-describedby" ); + + if ( error.length ) { + + // Refresh error/success class + error.removeClass( this.settings.validClass ).addClass( this.settings.errorClass ); + + // Replace message on existing label + error.html( message ); + } else { + + // Create error element + error = $( "<" + this.settings.errorElement + ">" ) + .attr( "id", elementID + "-error" ) + .addClass( this.settings.errorClass ) + .html( message || "" ); + + // Maintain reference to the element to be placed into the DOM + place = error; + if ( this.settings.wrapper ) { + + // Make sure the element is visible, even in IE + // actually showing the wrapped element is handled elsewhere + place = error.hide().show().wrap( "<" + this.settings.wrapper + "/>" ).parent(); + } + if ( this.labelContainer.length ) { + this.labelContainer.append( place ); + } else if ( this.settings.errorPlacement ) { + this.settings.errorPlacement.call( this, place, $( element ) ); + } else { + place.insertAfter( element ); + } + + // Link error back to the element + if ( error.is( "label" ) ) { + + // If the error is a label, then associate using 'for' + error.attr( "for", elementID ); + + // If the element is not a child of an associated label, then it's necessary + // to explicitly apply aria-describedby + } else if ( error.parents( "label[for='" + this.escapeCssMeta( elementID ) + "']" ).length === 0 ) { + errorID = error.attr( "id" ); + + // Respect existing non-error aria-describedby + if ( !describedBy ) { + describedBy = errorID; + } else if ( !describedBy.match( new RegExp( "\\b" + this.escapeCssMeta( errorID ) + "\\b" ) ) ) { + + // Add to end of list if not already present + describedBy += " " + errorID; + } + $( element ).attr( "aria-describedby", describedBy ); + + // If this element is grouped, then assign to all elements in the same group + group = this.groups[ element.name ]; + if ( group ) { + v = this; + $.each( v.groups, function( name, testgroup ) { + if ( testgroup === group ) { + $( "[name='" + v.escapeCssMeta( name ) + "']", v.currentForm ) + .attr( "aria-describedby", error.attr( "id" ) ); + } + } ); + } + } + } + if ( !message && this.settings.success ) { + error.text( "" ); + if ( typeof this.settings.success === "string" ) { + error.addClass( this.settings.success ); + } else { + this.settings.success( error, element ); + } + } + this.toShow = this.toShow.add( error ); + }, + + errorsFor: function( element ) { + var name = this.escapeCssMeta( this.idOrName( element ) ), + describer = $( element ).attr( "aria-describedby" ), + selector = "label[for='" + name + "'], label[for='" + name + "'] *"; + + // 'aria-describedby' should directly reference the error element + if ( describer ) { + selector = selector + ", #" + this.escapeCssMeta( describer ) + .replace( /\s+/g, ", #" ); + } + + return this + .errors() + .filter( selector ); + }, + + // See https://api.jquery.com/category/selectors/, for CSS + // meta-characters that should be escaped in order to be used with JQuery + // as a literal part of a name/id or any selector. + escapeCssMeta: function( string ) { + return string.replace( /([\\!"#$%&'()*+,./:;<=>?@\[\]^`{|}~])/g, "\\$1" ); + }, + + idOrName: function( element ) { + return this.groups[ element.name ] || ( this.checkable( element ) ? element.name : element.id || element.name ); + }, + + validationTargetFor: function( element ) { + + // If radio/checkbox, validate first element in group instead + if ( this.checkable( element ) ) { + element = this.findByName( element.name ); + } + + // Always apply ignore filter + return $( element ).not( this.settings.ignore )[ 0 ]; + }, + + checkable: function( element ) { + return ( /radio|checkbox/i ).test( element.type ); + }, + + findByName: function( name ) { + return $( this.currentForm ).find( "[name='" + this.escapeCssMeta( name ) + "']" ); + }, + + getLength: function( value, element ) { + switch ( element.nodeName.toLowerCase() ) { + case "select": + return $( "option:selected", element ).length; + case "input": + if ( this.checkable( element ) ) { + return this.findByName( element.name ).filter( ":checked" ).length; + } + } + return value.length; + }, + + depend: function( param, element ) { + return this.dependTypes[ typeof param ] ? this.dependTypes[ typeof param ]( param, element ) : true; + }, + + dependTypes: { + "boolean": function( param ) { + return param; + }, + "string": function( param, element ) { + return !!$( param, element.form ).length; + }, + "function": function( param, element ) { + return param( element ); + } + }, + + optional: function( element ) { + var val = this.elementValue( element ); + return !$.validator.methods.required.call( this, val, element ) && "dependency-mismatch"; + }, + + startRequest: function( element ) { + if ( !this.pending[ element.name ] ) { + this.pendingRequest++; + $( element ).addClass( this.settings.pendingClass ); + this.pending[ element.name ] = true; + } + }, + + stopRequest: function( element, valid ) { + this.pendingRequest--; + + // Sometimes synchronization fails, make sure pendingRequest is never < 0 + if ( this.pendingRequest < 0 ) { + this.pendingRequest = 0; + } + delete this.pending[ element.name ]; + $( element ).removeClass( this.settings.pendingClass ); + if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() ) { + $( this.currentForm ).submit(); + + // Remove the hidden input that was used as a replacement for the + // missing submit button. The hidden input is added by `handle()` + // to ensure that the value of the used submit button is passed on + // for scripted submits triggered by this method + if ( this.submitButton ) { + $( "input:hidden[name='" + this.submitButton.name + "']", this.currentForm ).remove(); + } + + this.formSubmitted = false; + } else if ( !valid && this.pendingRequest === 0 && this.formSubmitted ) { + $( this.currentForm ).triggerHandler( "invalid-form", [ this ] ); + this.formSubmitted = false; + } + }, + + previousValue: function( element, method ) { + method = typeof method === "string" && method || "remote"; + + return $.data( element, "previousValue" ) || $.data( element, "previousValue", { + old: null, + valid: true, + message: this.defaultMessage( element, { method: method } ) + } ); + }, + + // Cleans up all forms and elements, removes validator-specific events + destroy: function() { + this.resetForm(); + + $( this.currentForm ) + .off( ".validate" ) + .removeData( "validator" ) + .find( ".validate-equalTo-blur" ) + .off( ".validate-equalTo" ) + .removeClass( "validate-equalTo-blur" ) + .find( ".validate-lessThan-blur" ) + .off( ".validate-lessThan" ) + .removeClass( "validate-lessThan-blur" ) + .find( ".validate-lessThanEqual-blur" ) + .off( ".validate-lessThanEqual" ) + .removeClass( "validate-lessThanEqual-blur" ) + .find( ".validate-greaterThanEqual-blur" ) + .off( ".validate-greaterThanEqual" ) + .removeClass( "validate-greaterThanEqual-blur" ) + .find( ".validate-greaterThan-blur" ) + .off( ".validate-greaterThan" ) + .removeClass( "validate-greaterThan-blur" ); + } + + }, + + classRuleSettings: { + required: { required: true }, + email: { email: true }, + url: { url: true }, + date: { date: true }, + dateISO: { dateISO: true }, + number: { number: true }, + digits: { digits: true }, + creditcard: { creditcard: true } + }, + + addClassRules: function( className, rules ) { + if ( className.constructor === String ) { + this.classRuleSettings[ className ] = rules; + } else { + $.extend( this.classRuleSettings, className ); + } + }, + + classRules: function( element ) { + var rules = {}, + classes = $( element ).attr( "class" ); + + if ( classes ) { + $.each( classes.split( " " ), function() { + if ( this in $.validator.classRuleSettings ) { + $.extend( rules, $.validator.classRuleSettings[ this ] ); + } + } ); + } + return rules; + }, + + normalizeAttributeRule: function( rules, type, method, value ) { + + // Convert the value to a number for number inputs, and for text for backwards compability + // allows type="date" and others to be compared as strings + if ( /min|max|step/.test( method ) && ( type === null || /number|range|text/.test( type ) ) ) { + value = Number( value ); + + // Support Opera Mini, which returns NaN for undefined minlength + if ( isNaN( value ) ) { + value = undefined; + } + } + + if ( value || value === 0 ) { + rules[ method ] = value; + } else if ( type === method && type !== "range" ) { + + // Exception: the jquery validate 'range' method + // does not test for the html5 'range' type + rules[ method ] = true; + } + }, + + attributeRules: function( element ) { + var rules = {}, + $element = $( element ), + type = element.getAttribute( "type" ), + method, value; + + for ( method in $.validator.methods ) { + + // Support for in both html5 and older browsers + if ( method === "required" ) { + value = element.getAttribute( method ); + + // Some browsers return an empty string for the required attribute + // and non-HTML5 browsers might have required="" markup + if ( value === "" ) { + value = true; + } + + // Force non-HTML5 browsers to return bool + value = !!value; + } else { + value = $element.attr( method ); + } + + this.normalizeAttributeRule( rules, type, method, value ); + } + + // 'maxlength' may be returned as -1, 2147483647 ( IE ) and 524288 ( safari ) for text inputs + if ( rules.maxlength && /-1|2147483647|524288/.test( rules.maxlength ) ) { + delete rules.maxlength; + } + + return rules; + }, + + dataRules: function( element ) { + var rules = {}, + $element = $( element ), + type = element.getAttribute( "type" ), + method, value; + + for ( method in $.validator.methods ) { + value = $element.data( "rule" + method.charAt( 0 ).toUpperCase() + method.substring( 1 ).toLowerCase() ); + + // Cast empty attributes like `data-rule-required` to `true` + if ( value === "" ) { + value = true; + } + + this.normalizeAttributeRule( rules, type, method, value ); + } + return rules; + }, + + staticRules: function( element ) { + var rules = {}, + validator = $.data( element.form, "validator" ); + + if ( validator.settings.rules ) { + rules = $.validator.normalizeRule( validator.settings.rules[ element.name ] ) || {}; + } + return rules; + }, + + normalizeRules: function( rules, element ) { + + // Handle dependency check + $.each( rules, function( prop, val ) { + + // Ignore rule when param is explicitly false, eg. required:false + if ( val === false ) { + delete rules[ prop ]; + return; + } + if ( val.param || val.depends ) { + var keepRule = true; + switch ( typeof val.depends ) { + case "string": + keepRule = !!$( val.depends, element.form ).length; + break; + case "function": + keepRule = val.depends.call( element, element ); + break; + } + if ( keepRule ) { + rules[ prop ] = val.param !== undefined ? val.param : true; + } else { + $.data( element.form, "validator" ).resetElements( $( element ) ); + delete rules[ prop ]; + } + } + } ); + + // Evaluate parameters + $.each( rules, function( rule, parameter ) { + rules[ rule ] = $.isFunction( parameter ) && rule !== "normalizer" ? parameter( element ) : parameter; + } ); + + // Clean number parameters + $.each( [ "minlength", "maxlength" ], function() { + if ( rules[ this ] ) { + rules[ this ] = Number( rules[ this ] ); + } + } ); + $.each( [ "rangelength", "range" ], function() { + var parts; + if ( rules[ this ] ) { + if ( $.isArray( rules[ this ] ) ) { + rules[ this ] = [ Number( rules[ this ][ 0 ] ), Number( rules[ this ][ 1 ] ) ]; + } else if ( typeof rules[ this ] === "string" ) { + parts = rules[ this ].replace( /[\[\]]/g, "" ).split( /[\s,]+/ ); + rules[ this ] = [ Number( parts[ 0 ] ), Number( parts[ 1 ] ) ]; + } + } + } ); + + if ( $.validator.autoCreateRanges ) { + + // Auto-create ranges + if ( rules.min != null && rules.max != null ) { + rules.range = [ rules.min, rules.max ]; + delete rules.min; + delete rules.max; + } + if ( rules.minlength != null && rules.maxlength != null ) { + rules.rangelength = [ rules.minlength, rules.maxlength ]; + delete rules.minlength; + delete rules.maxlength; + } + } + + return rules; + }, + + // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true} + normalizeRule: function( data ) { + if ( typeof data === "string" ) { + var transformed = {}; + $.each( data.split( /\s/ ), function() { + transformed[ this ] = true; + } ); + data = transformed; + } + return data; + }, + + // https://jqueryvalidation.org/jQuery.validator.addMethod/ + addMethod: function( name, method, message ) { + $.validator.methods[ name ] = method; + $.validator.messages[ name ] = message !== undefined ? message : $.validator.messages[ name ]; + if ( method.length < 3 ) { + $.validator.addClassRules( name, $.validator.normalizeRule( name ) ); + } + }, + + // https://jqueryvalidation.org/jQuery.validator.methods/ + methods: { + + // https://jqueryvalidation.org/required-method/ + required: function( value, element, param ) { + + // Check if dependency is met + if ( !this.depend( param, element ) ) { + return "dependency-mismatch"; + } + if ( element.nodeName.toLowerCase() === "select" ) { + + // Could be an array for select-multiple or a string, both are fine this way + var val = $( element ).val(); + return val && val.length > 0; + } + if ( this.checkable( element ) ) { + return this.getLength( value, element ) > 0; + } + return value !== undefined && value !== null && value.length > 0; + }, + + // https://jqueryvalidation.org/email-method/ + email: function( value, element ) { + + // From https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address + // Retrieved 2014-01-14 + // If you have a problem with this implementation, report a bug against the above spec + // Or use custom methods to implement your own email validation + return this.optional( element ) || /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test( value ); + }, + + // https://jqueryvalidation.org/url-method/ + url: function( value, element ) { + + // Copyright (c) 2010-2013 Diego Perini, MIT licensed + // https://gist.github.com/dperini/729294 + // see also https://mathiasbynens.be/demo/url-regex + // modified to allow protocol-relative URLs + return this.optional( element ) || /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( value ); + }, + + // https://jqueryvalidation.org/date-method/ + date: ( function() { + var called = false; + + return function( value, element ) { + if ( !called ) { + called = true; + if ( this.settings.debug && window.console ) { + console.warn( + "The `date` method is deprecated and will be removed in version '2.0.0'.\n" + + "Please don't use it, since it relies on the Date constructor, which\n" + + "behaves very differently across browsers and locales. Use `dateISO`\n" + + "instead or one of the locale specific methods in `localizations/`\n" + + "and `additional-methods.js`." + ); + } + } + + return this.optional( element ) || !/Invalid|NaN/.test( new Date( value ).toString() ); + }; + }() ), + + // https://jqueryvalidation.org/dateISO-method/ + dateISO: function( value, element ) { + return this.optional( element ) || /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test( value ); + }, + + // https://jqueryvalidation.org/number-method/ + number: function( value, element ) { + return this.optional( element ) || /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test( value ); + }, + + // https://jqueryvalidation.org/digits-method/ + digits: function( value, element ) { + return this.optional( element ) || /^\d+$/.test( value ); + }, + + // https://jqueryvalidation.org/minlength-method/ + minlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength( value, element ); + return this.optional( element ) || length >= param; + }, + + // https://jqueryvalidation.org/maxlength-method/ + maxlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength( value, element ); + return this.optional( element ) || length <= param; + }, + + // https://jqueryvalidation.org/rangelength-method/ + rangelength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength( value, element ); + return this.optional( element ) || ( length >= param[ 0 ] && length <= param[ 1 ] ); + }, + + // https://jqueryvalidation.org/min-method/ + min: function( value, element, param ) { + return this.optional( element ) || value >= param; + }, + + // https://jqueryvalidation.org/max-method/ + max: function( value, element, param ) { + return this.optional( element ) || value <= param; + }, + + // https://jqueryvalidation.org/range-method/ + range: function( value, element, param ) { + return this.optional( element ) || ( value >= param[ 0 ] && value <= param[ 1 ] ); + }, + + // https://jqueryvalidation.org/step-method/ + step: function( value, element, param ) { + var type = $( element ).attr( "type" ), + errorMessage = "Step attribute on input type " + type + " is not supported.", + supportedTypes = [ "text", "number", "range" ], + re = new RegExp( "\\b" + type + "\\b" ), + notSupported = type && !re.test( supportedTypes.join() ), + decimalPlaces = function( num ) { + var match = ( "" + num ).match( /(?:\.(\d+))?$/ ); + if ( !match ) { + return 0; + } + + // Number of digits right of decimal point. + return match[ 1 ] ? match[ 1 ].length : 0; + }, + toInt = function( num ) { + return Math.round( num * Math.pow( 10, decimals ) ); + }, + valid = true, + decimals; + + // Works only for text, number and range input types + // TODO find a way to support input types date, datetime, datetime-local, month, time and week + if ( notSupported ) { + throw new Error( errorMessage ); + } + + decimals = decimalPlaces( param ); + + // Value can't have too many decimals + if ( decimalPlaces( value ) > decimals || toInt( value ) % toInt( param ) !== 0 ) { + valid = false; + } + + return this.optional( element ) || valid; + }, + + // https://jqueryvalidation.org/equalTo-method/ + equalTo: function( value, element, param ) { + + // Bind to the blur event of the target in order to revalidate whenever the target field is updated + var target = $( param ); + if ( this.settings.onfocusout && target.not( ".validate-equalTo-blur" ).length ) { + target.addClass( "validate-equalTo-blur" ).on( "blur.validate-equalTo", function() { + $( element ).valid(); + } ); + } + return value === target.val(); + }, + + // https://jqueryvalidation.org/remote-method/ + remote: function( value, element, param, method ) { + if ( this.optional( element ) ) { + return "dependency-mismatch"; + } + + method = typeof method === "string" && method || "remote"; + + var previous = this.previousValue( element, method ), + validator, data, optionDataString; + + if ( !this.settings.messages[ element.name ] ) { + this.settings.messages[ element.name ] = {}; + } + previous.originalMessage = previous.originalMessage || this.settings.messages[ element.name ][ method ]; + this.settings.messages[ element.name ][ method ] = previous.message; + + param = typeof param === "string" && { url: param } || param; + optionDataString = $.param( $.extend( { data: value }, param.data ) ); + if ( previous.old === optionDataString ) { + return previous.valid; + } + + previous.old = optionDataString; + validator = this; + this.startRequest( element ); + data = {}; + data[ element.name ] = value; + $.ajax( $.extend( true, { + mode: "abort", + port: "validate" + element.name, + dataType: "json", + data: data, + context: validator.currentForm, + success: function( response ) { + var valid = response === true || response === "true", + errors, message, submitted; + + validator.settings.messages[ element.name ][ method ] = previous.originalMessage; + if ( valid ) { + submitted = validator.formSubmitted; + validator.resetInternals(); + validator.toHide = validator.errorsFor( element ); + validator.formSubmitted = submitted; + validator.successList.push( element ); + validator.invalid[ element.name ] = false; + validator.showErrors(); + } else { + errors = {}; + message = response || validator.defaultMessage( element, { method: method, parameters: value } ); + errors[ element.name ] = previous.message = message; + validator.invalid[ element.name ] = true; + validator.showErrors( errors ); + } + previous.valid = valid; + validator.stopRequest( element, valid ); + } + }, param ) ); + return "pending"; + } + } + +} ); + +// Ajax mode: abort +// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); +// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() + +var pendingRequests = {}, + ajax; + +// Use a prefilter if available (1.5+) +if ( $.ajaxPrefilter ) { + $.ajaxPrefilter( function( settings, _, xhr ) { + var port = settings.port; + if ( settings.mode === "abort" ) { + if ( pendingRequests[ port ] ) { + pendingRequests[ port ].abort(); + } + pendingRequests[ port ] = xhr; + } + } ); +} else { + + // Proxy ajax + ajax = $.ajax; + $.ajax = function( settings ) { + var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode, + port = ( "port" in settings ? settings : $.ajaxSettings ).port; + if ( mode === "abort" ) { + if ( pendingRequests[ port ] ) { + pendingRequests[ port ].abort(); + } + pendingRequests[ port ] = ajax.apply( this, arguments ); + return pendingRequests[ port ]; + } + return ajax.apply( this, arguments ); + }; +} +return $; +})); diff --git a/octoprint_octolapse/static/js/octolapse.dialog.js b/octoprint_octolapse/static/js/octolapse.dialog.js new file mode 100644 index 00000000..88703157 --- /dev/null +++ b/octoprint_octolapse/static/js/octolapse.dialog.js @@ -0,0 +1,356 @@ +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ + +$(function () { + // Functions to create a dialog with custom validation and callbacks + Octolapse.OctolapseDialog = function (dialog_id, template_id, options) { + var self = this; + self.dialog_id = ko.observable(dialog_id); + self.template_id = ko.observable(template_id); + self.title = ko.observable("Octolapse Dialog"); + self.dialog_selector = "#" + dialog_id; + self.is_open = false; + // Callbacks + self.on_opened = null; + self.on_closed = null; + self.on_resize = null; + // Buttons + // cancel + self.cancel_button_visible = ko.observable(true); + self.cancel_button_title = ko.observable("Cancel and close the dialog."); + self.cancel_button_text = ko.observable("Cancel"); + + // Configure Help + self.help_enabled = ko.observable(); + self.help_link = ko.observable(); + self.help_tooltip = ko.observable(); + self.help_title = ko.observable(); + + self.set_help = function(enabled, link, tooltip, title) + { + self.help_enabled(enabled); + self.help_link(link || ""); + self.help_tooltip(tooltip || "Click for help with this."); + self.help_title(title || "Help"); + }; + + self.set_help( + !!options.help_enabled, + options.help_link, + options.help_tooltip, + options.help_title + ); + + self.on_cancel_button_clicked = function() { + self.close(); + }; + self.cancel_button_clicked = function() { + if (self.on_cancel_button_clicked) + { + self.on_cancel_button_clicked(); + } + }; + // option + self.option_button_visible = ko.observable(false); + self.option_button_title = ko.observable("Option"); + self.option_button_text = ko.observable("Option"); + self.option_button_clicked = function() { + if(self.on_option_button_clicked) + { + self.on_option_button_clicked(); + } + }; + // ok + self.ok_button_visible = ko.observable(false); + self.ok_button_title = ko.observable("OK"); + self.ok_button_text = ko.observable("OK"); + self.ok_button_clicked = function() { + if(self.on_ok_button_clicked) + { + self.on_ok_button_clicked(); + } + }; + // Must be called after the viewmodels are bound, else the dialog won't open + self.on_after_binding = function() { + // Set jquery variables + self.$dialog = this; + self.$editDialog = $(self.dialog_selector); + self.$editForm = self.$editDialog.find("form"); + self.$cancelButton = self.$editDialog.find("button.cancel"); + self.$closeIcon = $("a.close", self.$editDialog); + self.$optionButton = self.$editDialog.find("button.option"); + self.$okButton = self.$editDialog.find("button.ok"); + self.$summary = self.$editForm.find(".validation_summary"); + self.$errorCount = self.$summary.find(".error-count"); + self.$errorList = self.$summary.find("ul.error-list"); + self.$modalBody = self.$editDialog.find(".modal-body"); + self.$modalHeader = self.$editDialog.find(".modal-header"); + self.$modalFooter = self.$editDialog.find(".modal-footer"); + // Called before a dialog is closed + self.$editDialog.on("hide.bs.modal", function () { + if (!self.can_hide) + return false; + // Clear out error summary + self.$errorCount.empty(); + self.$errorList.empty(); + self.$summary.hide(); + self.unbind_validator(); + }); + + // Called after a dialog is hidden + self.$editDialog.on("hidden.bs.modal", function () { + self.is_open = false; + // see if the current viewmodel has an on_closed function + if (typeof self.on_closed === 'function') { + // call the function + self.on_closed(); + } + }); + + // Called after a dialog is shown. + self.$editDialog.on("shown.bs.modal", function () { + //console.log("Octolapse import dialog is shown."); + Octolapse.Help.bindHelpLinks(self.dialog_selector); + // Create all of the validation rules + + self.bind_validator(); + // Button Configuration + // Cancel and Close + self.$closeIcon.bind("click", self.cancel_button_clicked); + self.is_open = true; + // see if the current viewmodel has an on_opened function + if (typeof self.on_opened === 'function') { + // call the function + self.on_opened(); + } + }); + + // Validation Options + self.validation_enabled = true; + + if (options) { + // Configure Title + self.title(options.title); + + // Configure Validation + self.validation_enabled = options.validation_enabled !== undefined ? options.validation_enabled : self.validation_enabled; + self.validation_options.rules = options.rules ? options.rules : self.rules; + self.validation_options.messages = options.messages ? options.messages : self.messages; + + // Configure Callbacks + self.on_opened = options.on_opened ? options.on_opened : self.on_opened; + self.on_closed = options.on_closed ? options.on_closed : self.on_closed; + self.on_resize = options.on_resize ? options.on_resize : self.on_resize; + + // Configure Buttons + // Cancel + self.cancel_button_visible( + options.cancel_button_visible !== undefined ? options.cancel_button_visible : self.cancel_button_visible() + ); + self.cancel_button_title( + options.cancel_button_title ? options.cancel_button_title : self.cancel_button_title() + ); + self.cancel_button_text( + options.cancel_button_text ? options.cancel_button_text : self.cancel_button_text() + ); + self.on_cancel_button_clicked = options.on_cancel_button_clicked ? options.on_cancel_button_clicked : self.on_cancel_button_clicked; + // Option + self.option_button_visible( + options.option_button_visible !== undefined ? options.option_button_visible : self.option_button_visible() + ); + self.option_button_title( + options.option_button_title ? options.option_button_title : self.option_button_title() + ); + self.option_button_text( + options.option_button_text ? options.option_button_text : self.option_button_text() + ); + self.on_option_button_clicked = self.option_button_clicked; + // OK + self.ok_button_visible( + options.ok_button_visible !== undefined ? options.ok_button_visible : self.ok_button_visible() + ); + self.ok_button_title( + options.ok_button_title ? options.ok_button_title : self.ok_button_title() + ); + self.ok_button_text( + options.ok_button_text ? options.ok_button_text : self.ok_button_text() + ); + self.on_ok_button_clicked = self.ok_button_clicked; + } + }; + + self.validation_options = { + ignore: ".ignore_hidden_errors:hidden, .ignore_hidden_errors.hiding", + errorPlacement: function (error, element) { + var error_id = $(element).attr("name"); + var $field_error = self.$editDialog.find(".error_label_container[data-error-for='" + error_id + "']"); + $field_error.html(error); + }, + highlight: function (element, errorClass) { + var error_id = $(element).attr("name"); + var $field_error = self.$editDialog.find(".error_label_container[data-error-for='" + error_id + "']"); + $field_error.removeClass("checked"); + $field_error.addClass(errorClass); + }, + unhighlight: function (element, errorClass) { + var error_id = self.$editDialog.find(element).attr("name"); + var $field_error = $(".error_label_container[data-error-for='" + error_id + "']"); + $field_error.addClass("checked"); + $field_error.removeClass(errorClass); + }, + invalidHandler: function () { + self.$errorCount.empty(); + self.$summary.show(); + var numErrors = self.validator.numberOfInvalids(); + if (numErrors === 1) + self.$errorCount.text("1 field is invalid"); + else + self.$errorCount.text(numErrors + " fields are invalid"); + }, + errorContainer: "#settings_import_validation_summary", + success: function (label) { + label.html(" "); + label.parent().addClass('checked'); + $(label).parent().parent().parent().removeClass('error'); + }, + onfocusout: function (element, event) { + setTimeout(function () { + if (self.validator) { + self.validator.form(); + } + }, 250); + }, + onclick: function (element, event) { + setTimeout(function () { + self.validator.form(); + //self.resize(); + }, 250); + } + }; + + self.resize = function () { + // create as callback + if (self.on_resize) { + self.on_resize(); + } + else{ + $(window).trigger('resize'); + } + }; + + self.validator = null; + self.unbind_validator = function () { + // Destroy the validator if it exists, both to save on resources, and to clear out any leftover junk. + if (self.validator != null) { + self.validator.destroy(); + self.validator = null; + } + }; + self.bind_validator = function () { + self.unbind_validator(); + if (self.validation_enabled) { + self.validator = self.$editForm.validate(self.validation_options); + } + }; + //console.log("Adding validator to main setting dialog.") + // Close the dialog. + self.can_hide = false; + self.close = function () { + self.can_hide = true; + self.$editDialog.modal("hide"); + }; + + self.ok = function() { + if (self.validation_enabled) { + if (self.$editForm.valid()) { + //console.log("Importing Settings."); + // the form is valid, add or update the profile + self.ok_button_clicked(); + } else { + // Search for any hidden elements that are invalid + //console.log("Checking ofr hidden field error"); + var $fieldErrors = self.$editForm.find('.error_label_container.error'); + $fieldErrors.each(function (index, element) { + // Check to make sure the field is hidden. If it's not, don't bother showing the parent container. + // This can happen if more than one field is invalid in a hidden form + var $errorContainer = $(element); + if (!$errorContainer.is(":visible")) { + //console.log("Hidden error found, showing"); + var $collapsableContainer = $errorContainer.parents(".collapsible"); + if ($collapsableContainer.length > 0) + // The containers may be nested, show each + $collapsableContainer.each(function (index, container) { + //console.log("Showing the collapsed container"); + $(container).show(); + }); + } + + }); + + // The form is invalid, add a shake animation to inform the user + $(self.$editDialog).addClass('shake'); + // set a timeout so the dialog stops shaking + setTimeout(function () { + $(self.$editDialog).removeClass('shake'); + }, 500); + } + } + else { + if (self.ok_button_clicked) + { + self.ok_button_clicked(); + } + } + }; + + // Open the dialog. + self.show = function () { + // Only open the dialog if it is not already open. + if (!self.is_open) { + if (self.$editDialog.length !== 1) + { + console.error("No dialog has been found to open. Is the dialog id correct?"); + } + self.$editDialog.modal({ + backdrop: 'static', + resize: true, + maxHeight: function() { + return Math.max( + window.innerHeight - self.$modalHeader.outerHeight()-self.$modalFooter.outerHeight()-66, + 200 + ); + } + } + ); + } + else{ + console.error("Cannot open a dialog that is already open."); + } + }; + + self.get_help_link_element = function(){ + return $(self.dialog_selector + " a.octolapse_dialog_help"); + }; + }; +}); diff --git a/octoprint_octolapse/static/js/octolapse.dialog.renderings.in_process.js b/octoprint_octolapse/static/js/octolapse.dialog.renderings.in_process.js new file mode 100644 index 00000000..7fe2435c --- /dev/null +++ b/octoprint_octolapse/static/js/octolapse.dialog.renderings.in_process.js @@ -0,0 +1,232 @@ +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ +$(function () { + Octolapse.RenderProgressTypes = { + "pending": "Pending", + "preparing": "Preparing", + "pre_render_script": "Script - Before", + "adding_overlays": "Adding Overlays", + "rename_images": "Renaming Images", + 'pre_post_roll': "Pre/Post Roll", + "rendering": "Rendering", + "archiving": "Archiving", + "post_render_script": "Script - After", + "cleanup": "Cleaning Up" + }; + + Octolapse.InProcessRenderingListItem = function (values) { + var self = this; + self.id = values.job_guid + values.camera_guid; + self.job_guid = values.job_guid; + self.print_start_time = values.print_start_time; + self.print_start_time_text = Octolapse.toLocalDateString(values.print_start_time); + self.print_end_time = values.print_end_time; + self.print_end_time_text = Octolapse.toLocalDateString(values.print_end_time); + self.print_end_state = values.print_end_state; + self.print_file_name = values.print_file_name; + self.print_file_extension = values.print_file_extension; + self.job_path = values.job_path; + self.camera_profile_guid = values.camera_profile_guid; + self.camera_guid = values.camera_guid; + self.camera_path = values.camera_path; + self.camera_name = values.camera_name; + self.rendering_guid = values.rendering_guid; + self.rendering_name = values.rendering_name; + self.rendering_description = values.rendering_description; + self.file_size = values.file_size; + self.file_size_text = Octolapse.toFileSizeString(values.file_size,1); + self.progress = ko.observable(values.progress); + self.progress_percent = ko.observable(null); + self.sort_order = self.progress === "pending" ? 0 : 1; + + self.progress_text = ko.pureComputed(function(){ + var progress = self.progress(); + if (progress in Octolapse.RenderProgressTypes) { + if (self.progress_percent() !== null) { + return Octolapse.RenderProgressTypes[progress] + " " + self.progress_percent().toFixed(1).toString() + "%"; + } + return Octolapse.RenderProgressTypes[progress]; + } + return progress; + }); + + }; + + Octolapse.OctolapseDialogRenderingInProcess = function () { + var self = this; + self.dialog_id = "octolapse_dialog_rendering_in_process"; + self.dialog_options = { + title: "Renderings - In Progress", + validation_enabled: false, + help_enabled: true, + help_title: 'Renderings - In process', + help_link: 'dialog.renderings.in_process.md' + }; + self.template_id= "octolapse-rendering-in-process-template"; + + self.dialog = new Octolapse.OctolapseDialog(self.dialog_id, self.template_id, self.dialog_options); + + self.on_after_binding = function(){ + self.dialog.on_after_binding(); + self.initialize(); + }; + + self.load = function() { + if (!Octolapse.Globals.is_admin()) { + self.in_process_renderings.set([]); + return; + } + $.ajax({ + url: "./plugin/octolapse/loadInProcessRenderings", + type: "POST", + contentType: "application/json", + success: function (results) { + self.update(results); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + self.in_process_renderings.set([]); + var options = { + title: 'Error Loading In Progress Renderings', + text: "Status: " + textStatus + ". Error: " + errorThrown, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "file_load", ["file_load"]); + } + }); + }; + + self.open = function(open_to_tab){ + self.dialog.show(); + }; + // List Items + self.in_process_renderings_id = "octolapse-in-process-renderings"; + self.in_process_renderings_size = ko.observable(0); + self.in_process_renderings_size_formatted = ko.pureComputed(function(){ + return Octolapse.toFileSizeString(self.in_process_renderings_size()); + }); + self.is_empty = ko.pureComputed(function(){ + return self.in_process_renderings.list_items().length == 0; + }); + + // Configure list helper + self.to_list_item = function(item){ + var new_value = new Octolapse.InProcessRenderingListItem(item); + return new Octolapse.ListItemViewModel( + self, new_value.id, null, null, false, new_value + ); + }; + // load the file browser files + self.is_admin = ko.observable(false); + + var list_view_options = { + to_list_item: self.to_list_item, + sortable: false, + sort_column: 'print_end_time', + sort_direction: 'descending', + no_items_template_id: 'octolapse-rendering-in-process-no-items', + top_right_pagination_template_id: 'octolapse-rendering-in-process-file-size', + sort_column: 'progress', + pagination_row_auto_hide: false, + columns: [ + new Octolapse.ListViewColumn('Print', 'print_file_name', {class: 'rendering-print-name', sortable:false}), + new Octolapse.ListViewColumn('Status', 'print_end_state', {class: 'rendering-print-end-state', sortable:false}), + new Octolapse.ListViewColumn('Size', 'file_size_text', {class: 'rendering-size', sortable:false, sort_column_id: "file_size"}), + new Octolapse.ListViewColumn('Date', 'print_start_time_text', {class: 'rendering-date', sortable:false, sort_column_id: "print_start_time"}), + new Octolapse.ListViewColumn('Camera', 'camera_name', {class: 'rendering-camera-name', sortable:false}), + new Octolapse.ListViewColumn('Rendering', 'rendering_name', {class: 'rendering-name', sortable:false}), + new Octolapse.ListViewColumn('Progress', 'progress', {class: 'rendering-progress text-center', sortable: false, sort_column_id: 'sort_order', template_id: 'octolapse-rendering-progress'}) + ] + }; + + self.in_process_renderings = new Octolapse.ListViewModel(self, self.in_process_renderings_id, list_view_options); + + self.count = ko.pureComputed(function(){ + return self.in_process_renderings.list_items().length; + }); + + self.initialize = function(){ + self.is_admin(Octolapse.Globals.is_admin()); + Octolapse.Globals.is_admin.subscribe(function(newValue){ + self.is_admin(newValue); + }); + }; + + self.get_key = function(job_guid, camera_guid){ + return job_guid + camera_guid; + }; + + self.update = function(values){ + if (values.in_process){ + var in_process = values.in_process; + if (in_process.renderings) { + // Update all renderings if provided + self.in_process_renderings.set(in_process.renderings); + self.in_process_renderings_size(in_process.size ? in_process.size : 0); + } + else if (in_process.change_type && in_process.rendering) { + var in_process_rendering_change = in_process.rendering; + var in_process_rendering_change_type = in_process.change_type; + if (in_process_rendering_change_type === "added") { + self.in_process_renderings.add(in_process_rendering_change); + self.in_process_renderings_size(self.in_process_renderings_size() + in_process_rendering_change["file_size"]); + } else if (in_process_rendering_change_type === "removed") { + // Find the in_process rendering and remove it + var removed = self.in_process_renderings.remove( + self.get_key(in_process_rendering_change.job_guid, in_process_rendering_change.camera_guid) + ); + if (removed) { + self.in_process_renderings_size(self.in_process_renderings_size() - in_process_rendering_change["file_size"]); + } + } else if (in_process_rendering_change_type === "changed") { + // Find the in_process rendering and remove it + var replaced = self.in_process_renderings.replace(in_process_rendering_change); + if (replaced) { + self.in_process_renderings_size( + self.in_process_renderings_size() - replaced.value.file_size + in_process_rendering_change["file_size"]); + } + } else if (in_process_rendering_change_type === "progress") { + var progress_percent = in_process.progress_percent; + var progress_rendering = self.in_process_renderings.get( + self.get_key(in_process_rendering_change.job_guid, in_process_rendering_change.camera_guid) + ); + + if (progress_rendering) { + progress_rendering.value.progress(in_process_rendering_change.progress); + progress_rendering.value.progress_percent(progress_percent); + } + + } + } + else + { + console.error("A 'Failed Rendering' update was received, but there was no data to process."); + } + } + }; + + }; + +}); diff --git a/octoprint_octolapse/static/js/octolapse.dialog.renderings.unfinished.js b/octoprint_octolapse/static/js/octolapse.dialog.renderings.unfinished.js new file mode 100644 index 00000000..792f5267 --- /dev/null +++ b/octoprint_octolapse/static/js/octolapse.dialog.renderings.unfinished.js @@ -0,0 +1,562 @@ +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ +$(function () { + Octolapse.FailedRenderingListItem = function (values) { + var self = this; + self.id = values.job_guid + values.camera_guid; + self.job_guid = values.job_guid; + self.print_start_time = values.print_start_time; + self.print_start_time_text = Octolapse.toLocalDateString(values.print_start_time); + self.print_end_time = values.print_end_time; + self.print_end_time_text = Octolapse.toLocalDateString(values.print_end_time); + self.print_end_state = values.print_end_state; + self.print_file_name = values.print_file_name; + self.print_file_extension = values.print_file_extension; + self.job_path = values.job_path; + self.camera_profile_guid = values.camera_profile_guid; + self.camera_guid = values.camera_guid; + self.camera_path = values.camera_path; + self.camera_name = values.camera_name; + self.rendering_guid = values.rendering_guid; + self.rendering_name = values.rendering_name; + self.rendering_description = values.rendering_description; + self.file_size = values.file_size; + self.file_size_text = Octolapse.toFileSizeString(values.file_size,1); + self.render_profile_override_guid = ko.observable(null); + self.camera_profile_override_guid = ko.observable(null); + + self.get_rendering_option_caption = function () { + if (!self.rendering_guid) { + return "Current Profile"; + } else { + return "Original - " + self.rendering_name; + } + }; + + self.set_rendering_profile_option_description_as_title = function (option, item) { + var descrption = ""; + if (!item) + descrption = self.rendering_description; + else { + descrption = item.description; + } + ko.applyBindingsToNode(option, {attr: {title: descrption}}, item); + }; + + self.get_camera_option_caption = function () { + if (!self.camera_profile_guid) { + return "Current Default Settings"; + } else { + return "Original - " + self.camera_name; + } + }; + + self.get_download_url = function(){ + return './plugin/octolapse/downloadFile?type=failed_rendering&job_guid=' + self.job_guid + + '&camera_guid=' + self.camera_guid; + }; + }; + + Octolapse.OctolapseDialogRenderingUnfinished = function () { + var self = this; + self.dialog_id = "octolapse_dialog_rendering_unfinished"; + + self.dialog_options = { + title: "Unfinished Renderings", + validation_enabled: false, + help_enabled: true, + help_title: 'Unfinished Renderings', + help_link: 'dialog.renderings.unfinished.md' + }; + self.template_id= "octolapse-rendering-failed-template"; + + self.dialog = new Octolapse.OctolapseDialog(self.dialog_id, self.template_id, self.dialog_options); + + self.on_after_binding = function(){ + self.dialog.on_after_binding(); + self.initialize(); + }; + + self.load = function() { + if (!Octolapse.Globals.is_admin()) { + self.failed_renderings.set([]); + return; + } + $.ajax({ + url: "./plugin/octolapse/loadFailedRenderings", + type: "POST", + contentType: "application/json", + success: function (results) { + self.update(results); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + self.failed_renderings.set([]); + var options = { + title: 'Error Loading Unfinished Renderings', + text: "Status: " + textStatus + ". Error: " + errorThrown, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "file_load", ["file_load"]); + } + }); + }; + + self.open = function(open_to_tab){ + + self.dialog.show(); + }; + + self.update = function(values){ + if (values.failed) + { + // Update the size if it has been provided + var failed_renderings = values.failed; + if (failed_renderings.renderings) + { + // Perform a complete refresh if necessary + self.failed_renderings.set(failed_renderings.renderings); + self.failed_renderings_size(failed_renderings.size ? failed_renderings.size : 0); + } + else if (failed_renderings.change_type && failed_renderings.rendering) + { + // see if there is a change type + var failed_rendering_change = failed_renderings.rendering; + var failed_rendering_change_type = failed_renderings.change_type; + + if (failed_rendering_change_type === "added") + { + self.failed_renderings.add(failed_rendering_change); + self.failed_renderings_size(self.failed_renderings_size() + failed_rendering_change["file_size"]); + } + else if (failed_rendering_change_type === "removed") + { + // Find the failed rendering and remove it + var removed = self.failed_renderings.remove( + self.get_key(failed_rendering_change.job_guid, failed_rendering_change.camera_guid) + ); + if (removed) { + self.failed_renderings_size( + self.failed_renderings_size() - failed_rendering_change["file_size"] + ); + } + } + else if (failed_rendering_change_type === "changed") + { + var replaced = self.failed_renderings.replace(failed_rendering_change); + if (replaced) { + self.failed_renderings_size( + self.failed_renderings_size() - + replaced.failed_renderings.file_size + + failed_rendering_change["file_size"]); + } + } + } + else + { + console.error("An 'Unfinished Rendering' update was received, but there was no data to process."); + } + } + }; + + self.failed_renderings_id = "octolapse-unfinished-renderings"; + self.failed_renderings_size = ko.observable(0); + self.failed_renderings_size_formatted = ko.pureComputed(function () { + return Octolapse.toFileSizeString(self.failed_renderings_size()); + }); + self.render_profile_override_guid = ko.observable(null); + self.camera_profile_override_guid = ko.observable(null); + self.is_empty = ko.pureComputed(function(){ + return self.failed_renderings.list_items().length === 0; + }); + + // Configure list helper + self.to_list_item = function(item){ + var new_value = new Octolapse.FailedRenderingListItem(item); + return new Octolapse.ListItemViewModel( + self, new_value.id, null, null, false, new_value + ); + }; + // load the file browser files + self.is_admin = ko.observable(false); + + var list_view_options = { + to_list_item: self.to_list_item, + selection_enabled: self.is_admin, + select_all_enabled: self.is_admin, + sort_column: 'print_start_time_text', + sort_direction: 'descending', + top_left_pagination_template_id: 'octolapse-rendering-failed-selected-actions', + top_right_pagination_template_id: 'octolapse-rendering-failed-file-size', + pagination_row_auto_hide: false, + no_items_template_id: 'octolapse-rendering-failed-no-items', + select_header_template_id: 'octolapse-list-select-header-dropdown-template', + selection_class: 'list-item-selection-dropdown', + selection_header_class: 'list-item-selection-header-dropdown', + pagination_style: 'narrow', + columns: [ + new Octolapse.ListViewColumn('Print', 'print_file_name', {class: 'rendering-print-name', sortable:true}), + new Octolapse.ListViewColumn('Status', 'print_end_state', {class: 'rendering-print-end-state', sortable:true}), + new Octolapse.ListViewColumn('Size', 'file_size_text', {class: 'rendering-size', sortable:true, sort_column_id: "file_size"}), + new Octolapse.ListViewColumn('Date', 'print_start_time_text', {class: 'rendering-date', sortable:true, sort_column_id: "print_start_time"}), + new Octolapse.ListViewColumn('Camera', 'camera_name', {class: 'rendering-camera-name', sortable:true, template_id: 'octolapse-rendering-failed-camera-profile'}), + new Octolapse.ListViewColumn('Rendering', 'rendering_name', {class: 'rendering-name', sortable:true, template_id: 'octolapse-rendering-failed-render-profile'}), + new Octolapse.ListViewColumn('Action', null, {class: 'rendering-action text-center', template_id:'octolapse-rendering-failed-action', visible_observable: self.is_admin}) + ] + }; + + self.failed_renderings = new Octolapse.ListViewModel(self, self.failed_renderings_id, list_view_options); + + self.initialize = function(){ + self.is_admin(Octolapse.Globals.is_admin()); + Octolapse.Globals.is_admin.subscribe(function(newValue){ + self.is_admin(newValue); + }); + }; + + self.get_key = function(job_guid, camera_guid){ + return job_guid + camera_guid; + }; + + self.count = ko.pureComputed(function(){ + return self.failed_renderings.list_items().length; + }); + + self._delete = function(item, on_success, on_error){ + if (item.disabled()) + return; + item.disabled(true); + var data = { + 'job_guid': item.value.job_guid, + 'camera_guid': item.value.camera_guid, + }; + $.ajax({ + url: "./plugin/octolapse/deleteFailedRendering", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (results) { + var removed = self.failed_renderings.remove(item); + if (removed) { + self.failed_renderings_size(self.failed_renderings_size() - removed.file_size); + } + if (on_success){ + on_success(item.id); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + item.disabled(false); + if (on_error) { + on_error(XMLHttpRequest, textStatus, errorThrown); + } + } + }); + }; + + self.delete = function(item){ + Octolapse.showConfirmDialog( + "failed_rendering", + "Delete Failed Rendering", + "The selected rendering will be deleted. Are you sure?", + function(){ + self._delete( + item, + function(XMLHttpRequest, textStatus, errorThrown) { + var options = { + title: 'Unfinished Rendering Deleted', + text: "The unfinished rendering was deleted successfully.", + type: 'success', + hide: true, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "file-delete",["file-delete"]); + } + ); + }); + }; + + self._delete_selected = function(){ + var selected_files = self.failed_renderings.selected(); + + if (selected_files.length == 0) + return; + var num_errors = 0; + var num_deleted = 0; + var current_index = 0; + var delete_success = function(id){ + num_deleted += 1; + delete_end(); + }; + var delete_failed = function(XMLHttpRequest, textStatus, errorThrown){ + num_errors += 1; + delete_end(); + }; + + var delete_end = function(){ + current_index += 1; + if (current_index < selected_files.length) + { + delete_item(); + } + else + { + var options = null; + if (num_deleted > 0 && num_errors == 0) + { + var options = { + title: 'Unfinished Renderings Deleted', + text: "All selected unfinished renderings were deleted.", + type: 'success', + hide: true, + addclass: "octolapse" + }; + } + else if (num_deleted == 0) + { + var options = { + title: 'Error Deleting Unfinished Renderings', + text: "Octolapse could not delete the selected unfinished renderings.", + type: 'error', + hide: false, + addclass: "octolapse" + }; + } + else + { + var options = { + title: 'Some Unfinished Renderings Not Deleted', + text: "Octolapse could not delete all of the selected unfinished renderings. " + + num_deleted.toString() + " of " + num_errors.toString() + " files were deleted.", + type: 'error', + hide: false, + addclass: "octolapse" + }; + } + + Octolapse.displayPopupForKey(options, "file-delete",["file-delete"]); + } + }; + + var delete_item = function() { + var item = selected_files[current_index]; + self._delete(item, delete_success, delete_failed); + }; + + delete_item(); + }; + + self.delete_selected = function(){ + Octolapse.showConfirmDialog( + "failed_rendering", + "Delete Selected Unfinished Renderings", + "All selected unfinished renderings will be deleted. Are you sure?", + function(){ + self._delete_selected(); + }); + }; + + self._render = function(item, on_success, on_error){ + if (item.disabled()) + return; + item.disabled(true); + var data = { + 'id': item.id, + 'job_guid': item.value.job_guid, + 'camera_guid': item.value.camera_guid, + 'render_profile_override_guid': item.value.render_profile_override_guid() || null, + 'camera_profile_override_guid': item.value.camera_profile_override_guid() || null + }; + $.ajax({ + url: "./plugin/octolapse/renderFailedRendering", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (results) { + var removed = self.failed_renderings.remove(item.id); + if (removed) { + self.failed_renderings_size(self.failed_renderings_size() - item.value.file_size); + } + if (on_success){ + on_success(item.id); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + item.disabled(false); + if (on_error) { + on_error(XMLHttpRequest, textStatus, errorThrown); + } + } + }); + + }; + + self.render = function(item){ + self._render( + item, + function(){ + var options = { + title: 'Added to Rendering Queue', + text: "An unfinished rendering rendering was added to the rendering queue.", + type: 'success', + hide: true, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "render_message",["render_message"]); + }, + function(){ + var options = { + title: 'Error Deleting Unfinished Rendering', + text: "Status: " + textStatus + ". Error: " + errorThrown, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "render_message",["render_message"]); + }); + + }; + + self._render_selected = function(){ + var selected_unfinished_renderings = self.failed_renderings.selected(); + + if (selected_unfinished_renderings.length == 0) + return; + var num_errors = 0; + var num_rendered = 0; + var current_index = 0; + var render_success = function(id){ + num_rendered += 1; + render_end(); + }; + var render_failed = function(XMLHttpRequest, textStatus, errorThrown){ + num_errors += 1; + render_end(); + }; + + var render_end = function(){ + current_index += 1; + if (current_index < selected_unfinished_renderings.length) + { + render_item(); + } + else + { + var options = null; + if (num_rendered > 0 && num_errors == 0) + { + var options = { + title: 'Unfinished Renderings Queued', + text: "All selected items were queued for rendering.", + type: 'success', + hide: true, + addclass: "octolapse" + }; + } + else if (num_rendered == 0) + { + var options = { + title: 'Error Queueing Unfinished Renderings', + text: "Octolapse could not queue the selected unfinished renderings.", + type: 'error', + hide: false, + addclass: "octolapse" + }; + } + else + { + var options = { + title: 'Some Unfinished Renderings Not Queued', + text: "Octolapse could not queue all of the selected items for renderings. " + + num_rendered.toString() + " of " + num_errors.toString() + " files were queued.", + type: 'error', + hide: false, + addclass: "octolapse" + }; + } + + Octolapse.displayPopupForKey(options, "render_message",["render_message"]); + } + }; + + var render_item = function() { + var item = selected_unfinished_renderings[current_index]; + self._render(item, render_success, render_failed); + }; + + render_item(); + }; + + self.render_selected = function(){ + Octolapse.showConfirmDialog( + "failed_rendering", + "Render All Selected", + "All selected items will be rendered, which could take a long time. Are you sure?", + function(){ + self._render_selected(); + }); + }; + + self._downloading_icon_class = "fa fa-spinner fa-spin disabled"; + self._downloding_icon_error_class = "fa fa-exclamation-triangle text-error"; + self._download_icon_class = "fa fa-lg fa-download"; + self._download_icon_title = "Click to download."; + self._downloading_icon_title = "Downloading your file, please wait."; + self._download_error_icon_title = "An error occurred while downloading your file."; + self.download = function(data, e) + { + // Get the url + var url = data.item.value.get_download_url(); + // Get the icon that was clicked + var $icon = $(e.target); + // If the icon is disabled, exit since it is already downloading. + if ($icon.hasClass('disabled')) + return; + var icon_classes = self._download_icon_class; + var icon_title = self._download_icon_title; + var options = { + on_start: function(event, url){ + $icon.attr('class', self._downloading_icon_class); + $icon.attr('title', self._downloading_icon_title); + }, + on_end: function(e, url){ + if ($icon) + { + $icon.attr('class', icon_classes); + $icon.attr('title', icon_title); + } + }, + on_error: function(message) + { + icon_classes = self._downloding_icon_error_class; + icon_title = self._download_error_icon_title + ' Error: ' + message; + } + }; + Octolapse.download(url, e, options); + }; + }; + +}); diff --git a/octoprint_octolapse/static/js/octolapse.dialog.timelapse_files.js b/octoprint_octolapse/static/js/octolapse.dialog.timelapse_files.js new file mode 100644 index 00000000..1781ece9 --- /dev/null +++ b/octoprint_octolapse/static/js/octolapse.dialog.timelapse_files.js @@ -0,0 +1,227 @@ +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ +$(function () { + Octolapse.OctolapseTimelapseFilesDialog = function () { + var self = this; + + self.dialog_id = "octolapse_timelapse_files_dialog"; + self.open_to_tab = null; + + self.timelapse_tab_button_id = "octolapse_timelapse_videos_tab_button"; + self.snapshot_archive_tab_button_id = "octolapse_snapshot_archive_tab_button"; + self.dialog_options = { + title: "Videos and Images", + validation_enabled: false, + help_enabled: true, + help_title: 'Timelapse Files Dialog', + help_link: 'dialog.timelapse_files.timelapse.tab.md', + cancel_button_text: 'Close', + cancel_button_title: 'Close the dialog.' + }; + self.template_id= "octolapse-timelapse-files-dialog-template"; + self.dialog = new Octolapse.OctolapseDialog(self.dialog_id, self.template_id, self.dialog_options); + + self.archive_browser = new Octolapse.OctolapseFileBrowser( + 'octolapse-archive-browser', + self, + { + file_type: 'snapshot_archive', + resize: self.dialog.resize, + custom_actions_template_id: 'octolapse-snapshot-archive-custom-actions', + actions_class: 'file-browser-snapshot-archive-action', + top_left_template_id: 'octolapse-file-browser-snapshot-archive-import' + } + ); + + self.timelapse_browser = new Octolapse.OctolapseFileBrowser( + 'octolapse-timelapse-browser', + self, + { + file_type: 'timelapse_octolapse', + custom_actions_template_id: 'octolapse-timelapse-files-custom-actions', + actions_class: 'file-browser-snapshot-archive-action', + resize: self.dialog.resize, + } + ); + + self.load = function(){ + self.timelapse_browser.load(); + self.archive_browser.load(); + }; + + self.timelapse_tab_selected = function(){ + // Resize the tab + self.dialog.resize(); + // Configure the help link + self.dialog.set_help(true, "dialog.timelapse_files.timelapse.tab.md", null,"Timelapse Files Dialog"); + }; + + self.snapshot_archive_tab_selected = function(){ + // Resize the tab + self.dialog.resize(); + // Configure the help link + self.dialog.set_help(true, "dialog.timelapse_files.snapshot_archive.tab.md", null,"Saved Snapshot Files Dialog"); + }; + + self.files_changed = function(file_info, action){ + if (file_info.type === "snapshot_archive") + { + self.archive_browser.files_changed(file_info, action); + } + else if (file_info.type === "timelapse_octolapse" || file_info.type === "timelapse_octoprint") + { + self.timelapse_browser.files_changed(file_info, action); + } + }; + + self.initialize_snapshot_upload_button = function() { + // Set up the file upload button. + var $snapshotUploadElement = $('#octolapse_snapshot_upload'); + var $progressBarContainer = $('#octolapse_snapshot_upload_progress'); + var $progressBar = $progressBarContainer.find('.progress-bar'); + + $snapshotUploadElement.fileupload({ + dataType: "json", + maxNumberOfFiles: 1, + headers: OctoPrint.getRequestHeaders(), + start: function(e) { + $progressBar.text("Starting..."); + $progressBar.animate({'width': '0'}, {'queue': false}).removeClass('failed'); + }, + progressall: function (e, data) { + var progress = parseInt(data.loaded / data.total * 100, 10); + $progressBar.text(progress + "%"); + $progressBar.animate({'width': progress + '%'}, {'queue': false}); + }, + done: function (e, data) { + $progressBar.text("Done!"); + $progressBar.animate({'width': '100%'}, {'queue': false}); + }, + fail: function (e, data) { + $progressBar.text("Failed...").addClass('failed'); + $progressBar.animate({'width': '100%'}, {'queue': false}); + } + }); + }; + + self.on_after_binding = function(){ + self.dialog.on_after_binding(); + self.archive_browser.initialize(); + self.timelapse_browser.initialize(); + self.initialize_snapshot_upload_button(); + }; + + self.open = function(open_to_tab){ + self.dialog.show(); + var button_id = null; + if (open_to_tab==="timelapse") + { + button_id = self.timelapse_tab_button_id; + } + else if (open_to_tab==="snapshot_archive") + { + button_id = self.snapshot_archive_tab_button_id; + } + else + { + return; + } + if (button_id) + { + $("#"+self.dialog_id).find("#"+button_id).click(); + } + + }; + + self.add_archive_to_unfinished_rendering = function(item) { + if (item.disabled()) + return; + var data = { + 'archive_name': item.id + }; + + var options = { + title: 'Adding Archive', + text: "Octolapse is unzipping the archive and adding it to the unfinished renderings. Please wait.", + type: 'info', + hide: true, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "add-archive-to-unfinished-renderings", ["add-archive-to-unfinished-renderings"]); + + // disable item + item.disabled(true); + $.ajax({ + url: "./plugin/octolapse/addArchiveToUnfinishedRenderings", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (results) { + item.disabled(false); + if (results.success) { + var options = { + title: 'Archive Added', + text: "The archive was added to the unfinished rendering list. You can render the archive by clicking the unfinished renderings button.", + type: 'success', + hide: true, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "add-archive-to-unfinished-renderings", ["add-archive-to-unfinished-renderings"]); + } + else { + var options = { + title: 'Error Adding Archive', + text: results.errors, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.Help.showPopupForErrors( + options, + "add-archive-to-unfinished-renderings", + ["add-archive-to-unfinished-renderings"], + results["errors"] + ); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + item.disabled(false); + var options = { + title: 'Error Adding Archive', + text: "Status: " + textStatus + ". Error: " + errorThrown, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "add-archive-to-unfinished-renderings",["add-archive-to-unfinished-renderings"]); + } + }); + }; + + }; +}); diff --git a/octoprint_octolapse/static/js/octolapse.file_browser.js b/octoprint_octolapse/static/js/octolapse.file_browser.js new file mode 100644 index 00000000..d61320d1 --- /dev/null +++ b/octoprint_octolapse/static/js/octolapse.file_browser.js @@ -0,0 +1,329 @@ +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ +$(function () { + Octolapse.FileViewModel = function(values) { + var self = this; + self.extension = values.extension; + self.size = values.size; + self.size_formatted = Octolapse.toFileSizeString(values.size, 1); + self.date = values.date; + self.date_formatted = Octolapse.toLocalDateTimeString(values.date); + self.get_download_url = function(list_item){ + var parent = list_item.data.parent; + return './plugin/octolapse/downloadFile?type=' + parent.file_type + '&name=' + list_item.id; + }; + }; + + Octolapse.OctolapseFileBrowser = function (id, parent, options) { + var self = this; + self.data = ko.observable(); + self.data.parent = parent; + self.file_browser_id = id; + self.options = options; + self.dialog_id = "octolapse_file_browsing_dialog"; + self.file_type = options.file_type; + self.no_files_text = options.no_files_text || "There are no files available."; + self.custom_actions_template_id = options.custom_actions_template_id || null; + self.files_not_loaded_template_id = options.files_not_loaded_template_id || "octolapse-file-browser-not-loaded"; + self.actions_class = options.actions_class || "file-browser-action"; + self.has_loaded = ko.observable(false); + self.total_file_size = ko.observable(0); + self.total_file_size_text = ko.pureComputed(function(){ + return Octolapse.toFileSizeString(self.total_file_size(),1); + }); + self.to_list_item = function(item){ + return new Octolapse.ListItemViewModel( + self, encodeURI(item.name), item.name, item.name, false, new Octolapse.FileViewModel(item) + ); + }; + // load the file browser files + self.is_admin = ko.observable(false); + self.pagination_row_auto_hide = ko.observable(false); + + var list_view_options = { + to_list_item: self.to_list_item, + selection_enabled: self.is_admin, + select_all_enabled: self.is_admin, + sort_column: 'date_formatted', + sort_direction: 'descending', + pagination_row_auto_hide: self.pagination_row_auto_hide, + top_left_pagination_template_id: 'octolapse-file-browser-delete-selected', + top_right_pagination_template_id: 'octolapse-file-browser-file-size', + select_header_template_id: 'octolapse-list-select-header-dropdown-template', + selection_class: 'list-item-selection-dropdown', + selection_header_class: 'list-item-selection-header-dropdown', + no_items_template_id: 'octolapse-file-browser-no-items', + custom_row_template_id: 'octolapse-file-browser-custom-row', + columns: [ + new Octolapse.ListViewColumn('Name', 'name', {class: 'file-browser-name', sortable:true}), + new Octolapse.ListViewColumn('Size', 'size_formatted', {class: 'file-browser-size', sortable:true, sort_column_id: "size"}), + new Octolapse.ListViewColumn('Date', 'date_formatted', {class: 'file-browser-date', sortable:true, sort_column_id: "date"}), + new Octolapse.ListViewColumn('Action', null, {class: self.actions_class, template_id:'octolapse-file-browser-action', visible_observable: self.is_admin}) + ] + }; + + self.files = new Octolapse.ListViewModel(self, self.file_browser_id, list_view_options); + + self.initialize = function(){ + self.is_admin(Octolapse.Globals.is_admin); + Octolapse.Globals.is_admin.subscribe(function(newValue){ + self.is_admin(newValue); + self.pagination_row_auto_hide(!newValue); + }); + }; + + self.load = function(){ + if (!Octolapse.Globals.is_admin()) { + self.files.set([]); + } + var data = { + 'type': self.file_type + }; + $.ajax({ + url: "./plugin/octolapse/getFiles", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (results) { + // Build the list items + self.has_loaded(true); + var total_size = 0; + self.files.set(results.files, function(added_item){ + total_size += added_item.value.size; + }); + self.total_file_size(total_size); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + var options = { + title: 'Error Loading Files', + text: "Status: " + textStatus + ". Error: " + errorThrown, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "file_load",["file_load"]); + } + }); + }; + + self.files_changed = function(file_info, action){ + if (action === "removed") + { + self.files.remove(encodeURI(file_info.name)); + self.total_file_size(self.total_file_size() - file_info.size); + } + else if (action === "added") + { + self.files.add(file_info); + self.total_file_size(self.total_file_size() + file_info.size); + } + else if (action === "reload") + { + self.load(); + } + }; + + self._delete_file = function(file, on_success, on_error){ + if (file.disabled()) + { + if (on_error) { + on_error(XMLHttpRequest, textStatus, errorThrown); + } + } + file.disabled(true); + var data = { + 'type': self.file_type, + 'id': file.id, + 'size': file.value.size, + 'client_id': Octolapse.Globals.client_id + }; + $.ajax({ + url: "./plugin/octolapse/deleteFile", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (results) { + self.files.remove(file.id); + self.total_file_size(self.total_file_size() - file.value.size); + if (on_success){ + on_success(file.id); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + if (on_error) { + on_error(XMLHttpRequest, textStatus, errorThrown); + } + file.disabled(false); + } + }); + }; + + self.delete_file = function(file){ + var message = "Are you sure you want to permanently delete this file?"; + Octolapse.showConfirmDialog( + "file-delete", + "Delete Files", + message, + function(){ + self._delete_file( + file, + null, + function() { + var options = { + title: 'Error Deleting File', + text: "Octolapse could not delete the file.", + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "file-delete", ["file-delete"]); + }); + }); + }; + + self._delete_selected = function(){ + var selected_files = self.files.selected(); + if (selected_files.length == 0) + return; + var num_errors = 0; + var num_deleted = 0; + var current_index = 0; + var delete_success = function(id){ + num_deleted += 1; + delete_file_end(); + }; + var delete_failed = function(XMLHttpRequest, textStatus, errorThrown){ + num_errors += 1; + delete_file_end(); + }; + var delete_file_end = function(){ + current_index += 1; + if (current_index < selected_files.length) + { + delete_file(); + } + else + { + var options = null; + if (num_deleted === 1 && num_errors === 0) + { + return; + } + if (num_deleted > 1 && num_errors == 0) + { + var options = { + title: 'Files Deleted', + text: "All selected files were deleted.", + type: 'success', + hide: true, + addclass: "octolapse" + }; + } + else if (num_deleted == 0) + { + var options = { + title: 'Error Deleting File', + text: "Octolapse could not delete the selected files.", + type: 'error', + hide: false, + addclass: "octolapse" + }; + } + else + { + var options = { + title: 'Some Files Not Deleted', + text: "Octolapse could not delete all of the selected files. " + + num_deleted.toString() + " of " + num_errors.toString() + " files were deleted.", + type: 'error', + hide: false, + addclass: "octolapse" + }; + } + + Octolapse.displayPopupForKey(options, "file-delete",["file-delete"]); + } + }; + + var delete_file = function() { + self._delete_file(selected_files[current_index], delete_success, delete_failed); + }; + + delete_file(); + }; + + self.delete_selected = function(){ + var message = "This will permanently delete " + self.files.selected_count() + " files. Are you sure?"; + Octolapse.showConfirmDialog( + "file-delete", + "Delete Selected Files", + message, + function(){ + self._delete_selected(); + } + ); + }; + + self._downloading_icon_class = "fa fa-spinner fa-spin disabled"; + self._downloding_icon_error_class = "fa fa-exclamation-triangle text-error"; + self._download_icon_class = "fa fa-lg fa-download"; + self._download_icon_title = "Click to download."; + self._downloading_icon_title = "Downloading your file, please wait."; + self._download_error_icon_title = "An error occurred while downloading your file. Click to retry."; + self.download = function(data, e) + { + // Get the url + var url = data.value.get_download_url(data); + // Get the icon that was clicked + var $icon = $(e.target); + // If the icon is disabled, exit since it is already downloading. + if ($icon.hasClass('disabled')) + return; + var icon_classes = self._download_icon_class; + var icon_title = self._download_icon_title; + var options = { + on_start: function(event, url){ + $icon.attr('class', self._downloading_icon_class); + $icon.attr('title', self._downloading_icon_title); + }, + on_end: function(e, url){ + if ($icon) + { + $icon.attr('class', icon_classes); + $icon.attr('title', icon_title); + } + }, + on_error: function(message) + { + icon_classes = self._downloding_icon_error_class; + icon_title = self._download_error_icon_title + ' Error: ' + message; + } + }; + Octolapse.download(url, e, options); + }; + + }; +}); diff --git a/octoprint_octolapse/static/js/octolapse.help.js b/octoprint_octolapse/static/js/octolapse.help.js index 077ebec6..df831a3d 100644 --- a/octoprint_octolapse/static/js/octolapse.help.js +++ b/octoprint_octolapse/static/js/octolapse.help.js @@ -1,6 +1,30 @@ +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ $(function () { OctolapseHelp = function () { var self = this; + self.html_text = "unknown"; self.popup_margin = 15; self.popup_width_with_margin = 900; self.popup_width = self.popup_width_with_margin - self.popup_margin*2; @@ -25,7 +49,6 @@ $(function () { Buttons: { closer: false, sticker: false - }, confirm: { confirm: true, @@ -47,6 +70,21 @@ $(function () { }, after_open: function(notice) { + + var $popup_item = $(notice.elem); + var $help_container = $popup_item.find("div.ui-pnotify-container"); + var $help_text = $popup_item.find(".ui-pnotify-text"); + $help_text.html(self.options.text); + var body_color = $("body").css('color'); + var body_background_color = $("body").css('background-color'); + var body_font_weight = $("body").css('font-weight'); + + $help_container.css("background-color", body_background_color) + .css("border-color", "#000000") + .css("border-width", "3px") + .css("color", body_color) + .css("font-weight", body_font_weight); + // Now we want to put the notice inside another div so we can add an overlay effect // since modal doesn't seem to be working in this version of pnotify (I could be wrong // but the docs I can find are definitely not for this version.) @@ -61,21 +99,21 @@ $(function () { notice.remove(); }); //console.log("Adding resize handler."); - self.resize_handler(null, null); - window.addEventListener('resize', self.resize_handler) + self.resize_handler(null, notice.elem); + window.addEventListener('resize', function() { self.resize_handler(this, notice.elem);}); }, after_close: function(notice){ var $overlay = $(notice.elem).parent().find(".octolapse-pnotify-overlay"); $overlay.remove(); Octolapse.removeKeyForClosedPopup('octolapse-help'); //console.log("Removing resize handler."); - window.removeEventListener('resize', self.resize_handler) + window.removeEventListener('resize', self.resize_handler); } }; self.resize_timer = null; self.resize_handler = function(event, elem) { - console.log("Resizing Help."); + //console.log("Resizing Help."); if(self.resize_timer) { clearTimeout(self.resize_timer); @@ -83,7 +121,7 @@ $(function () { } self.resize_timer = setTimeout(resize_help_popup, 100); function resize_help_popup (){ - console.log("Resizing octolapse help."); + //console.log("Resizing octolapse help."); var width = self.popup_width.toString(); if (document.body.clientWidth < self.popup_width_with_margin) { @@ -96,11 +134,11 @@ $(function () { // get the left position var left = (document.body.clientWidth - width)/2; - var $help = $(".octolapse-pnotify-help"); + //var $help = $(".octolapse-pnotify-help"); - $help.css("width", width) + $(elem).css("width", width) .css("left", left) - .css("top", "15px") + .css("top", "15px"); } @@ -113,37 +151,21 @@ $(function () { self.converter.setFlavor('github'); - self.showHelpForLink = function (doc, title, custom_not_found_message) - { - url = "/plugin/octolapse/static/docs/help/" + doc + "?nonce=" + Date.now().toString(); + self.showHelpForLink = function (doc, title, custom_not_found_message){ + url = "./plugin/octolapse/static/docs/help/" + doc + "?nonce=" + Date.now().toString(); $.ajax({ url: url, type: "GET", dataType: "text", success: function (results) { - var body_color = $("body").css('color'); - var body_background_color = $("body").css('background-color'); - var body_font_weight = $("body").css('font-weight'); + //console.log(results); //var text = Octolapse.replaceAll(self.converter.makeHtml(results), '

      \n','

      '); - var help_html = self.converter.makeHtml(results); + self.options.text = self.converter.makeHtml(results); // Set the option text to a known token so that we can replace it with our markdown self.options.title = title; - Octolapse.displayPopupForKey(self.options, "octolapse-help", ["octolapse-help"]); - - var $help_container = $(".octolapse-pnotify-help div.ui-pnotify-container"); - var $help_text = $help_container.find(".ui-pnotify-text"); - $help_text.html(help_html); - // Replace the token with the help HTML. This will preserve our title! - - $help_container.css("background-color", body_background_color) - .css("border-color", "#000000") - .css("border-width", "3px") - .css("color", body_color) - .css("font-weight", body_font_weight); - }, error: function (XMLHttpRequest, textStatus, errorThrown) { if (errorThrown === "NOT FOUND") @@ -161,8 +183,9 @@ $(function () { self.options.title = "Help Could Not Be Found"; - Octolapse.displayPopupForKey(self.options, "octolapse-help", ["octolapse-help"]); - $(".octolapse-pnotify-help div.ui-pnotify-container") + var popup = Octolapse.displayPopupForKey(self.options, "octolapse-help", ["octolapse-help"]); + var $popup_item = $(popup.elem); + $popup_item.find(".octolapse-pnotify-help div.ui-pnotify-container") .css("background-color", body_background_color) .css("border-color", "#000000") .css("border-width", "3px") @@ -183,32 +206,229 @@ $(function () { }); }; - self.bindHelpLinks = function(selector) - { - var default_selector = ".octolapse_help[data-help-url]"; + self.bindHelpLinks = function(selector){ + var default_selector = "a.octolapse_help[data-help-url]"; selector = selector + " " + default_selector; - //console.log("octolapse.help.js - Binding help links to " + selector); - $(selector).each(function(){ - if (!$(this).attr('title')) - $(this).attr('title',"Click for help with this"); - if($(this).children().length == 0) { - var icon = $(''); - $(this).append(icon); + console.log("octolapse.help.js - Binding help links to " + selector); + $(selector).each(function(index, value){ + var $link = $(this); + if (!$link.attr('data-help-title')){ + $link.attr('data-help-title',"Click for help with this"); } + $link.html($('')); + }); $(selector).unbind("click"); $(selector).click( function(e) { //console.log("octolapse.help.js - Help link clicked"); // get the data group data - var url = $(this).data('help-url'); - var title = $(this).data('help-title'); - var custom_not_found_error = $(this).data('help-not-found'); + var url = $(this).attr('data-help-url'); + var title = $(this).attr('data-help-title'); + var custom_not_found_error = $(this).attr('data-help-not-found'); if (!title) title = "Help"; self.showHelpForLink(url, title, custom_not_found_error); e.preventDefault(); }); }; - } + + self.showPopupForErrors = function(options, popup_key, remove_keys, errors) + { + var error_popup = this; + error_popup.original_title = options.title; + error_popup.errors = errors; + error_popup.current_error_index = 0; + error_popup.$error_element = null; + error_popup.$error_title = ""; + error_popup.$error_text = null; + error_popup.$previous_button = null; + error_popup.$next_button = null; + error_popup.$help_button = null; + error_popup.$close_button = null; + error_popup.$button_container = null; + error_popup.$button_row = null; + error_popup.$button_left_column = null; + error_popup.$button_right_column = null; + + error_popup.configure_notice = function(notice){ + // Find notification elements for later use + error_popup.$error_element = notice.get(); + error_popup.$error_title = error_popup.$error_element.find(".ui-pnotify-title"); + error_popup.$error_text = error_popup.$error_element.find("div.ui-pnotify-text"); + error_popup.$previous_button = error_popup.$error_element.find("button.error_previous"); + error_popup.$next_button = error_popup.$error_element.find("button.error_next"); + error_popup.$help_button = error_popup.$error_element.find("button.error_help"); + error_popup.$close_button = error_popup.$error_element.find("button.error_close"); + error_popup.$button_container = error_popup.$close_button.parent(); + // create the button row, left column and right column + error_popup.$button_row = $('
      '); + error_popup.$button_left_column = $('
      '); + error_popup.$button_right_column = $('
      '); + // add the button row + error_popup.$button_container.append(error_popup.$button_row); + // Add the columns + error_popup.$button_row.append(error_popup.$button_left_column); + error_popup.$button_row.append(error_popup.$button_right_column); + // add the buttons to the columns + error_popup.$button_left_column.append(error_popup.$previous_button); + + if(error_popup.errors.length == 1) { + // If there is only one error, add the help button to the right column + error_popup.$button_right_column.append(error_popup.$help_button); + } + else { + // If there are multiple errors, add the help button to the left column. + error_popup.$button_left_column.append(error_popup.$help_button); + } + error_popup.$button_left_column.append(error_popup.$next_button); + error_popup.$button_right_column.append(error_popup.$close_button); + + // configure next/previous button icons + error_popup.$previous_button.html(''); + error_popup.$next_button.html(''); + + // Show/Hide next/previous buttons as required + if(error_popup.errors.length == 1) + { + // hide next/previous buttons + error_popup.$previous_button.hide(); + error_popup.$next_button.hide(); + } + else { + // show next/previous buttons + error_popup.$previous_button.show(); + error_popup.$next_button.show(); + } + }; + + error_popup.current_error = function() + { + return errors[error_popup.current_error_index]; + }; + + error_popup.set_title = function(){ + var title_html = ""; + //var title_text = error_popup.original_title; + var current_error = error_popup.current_error(); + var error_title_text = current_error.name; + if (error_popup.errors.length == 1) + { + title_html = '

      ' + error_popup.original_title + '

      '+ error_title_text +'
      '; + } + else + { + title_html = + '

      ' + error_popup.original_title + + ' - ' + (error_popup.current_error_index+1).toString() + ' of ' + error_popup.errors.length + + '

      '+ error_title_text +'
      '; + } + error_popup.$error_title.html(title_html); + }; + error_popup.show_current_error = function() + { + var current_error = error_popup.current_error(); + error_popup.$error_text.text(current_error.description); + error_popup.set_title(); + // enable/disable buttons + // if we only have 1 error we don't want any previous/next buttons + if(error_popup.errors.length > 1) + { + // disable buttons as necessary + error_popup.$previous_button.prop('disabled', error_popup.current_error_index == 0); + error_popup.$next_button.prop('disabled', error_popup.current_error_index + 1 == self.errors.length); + } + }; + error_popup.display_help_for_current_error = function() + { + var current_error = error_popup.current_error(); + showHelpForLink( + current_error.help_link, + current_error.name, + "No help could be found for this error."); + }; + + error_popup.next_error = function() + { + error_popup.current_error_index = (error_popup.errors.length+error_popup.current_error_index+1) % error_popup.errors.length; + error_popup.show_current_error(); + }; + + error_popup.previous_error = function() + { + error_popup.current_error_index = (error_popup.errors.length+error_popup.current_error_index-1) % error_popup.errors.length; + error_popup.show_current_error(); + }; + + Octolapse.closeConfirmDialogsForKeys([remove_keys]); + // Make sure that the default pnotify buttons exist + Octolapse.checkPNotifyDefaultConfirmButtons(); + Octolapse.ConfirmDialogs[popup_key] = ( + new PNotify({ + title: options.title, + text: options.text, + icon: options.icon, + hide: options.hide, + desktop: options.desktop, + type: options.type, + addclass: options.addclass, + confirm: { + confirm: true, + buttons: [{ + text: 'Ok', + addClass: 'remove_button', + },{ + text: 'Cancel', + addClass: 'remove_button', + },{ + // Previous Error + text: '', + addClass: 'error_previous', + click: function(){ + //console.log("Showing the previous error"); + error_popup.previous_error(); + } + },{ + text: 'Help', + addClass: 'error_help', + click: function() { + var current_error = error_popup.current_error(); + Octolapse.Help.showHelpForLink( + current_error.help_link, + current_error.name, + "No help could be found for this error."); + } + },{ + // Next Error + text: '', + addClass: 'error_next', + click: function(){ + //console.log("Showing the next error"); + error_popup.next_error(); + } + },{ + text: 'Close', + addClass: 'error_close', + click: function(){ + //console.log("Closing popup with key: " + popup_key.toString()); + Octolapse.closeConfirmDialogsForKeys([popup_key]); + } + }] + }, + buttons: { + closer: false, + sticker: false + }, + history: { + history: false + }, + before_open: function(notice) { + notice.get().find(".remove_button").remove(); + error_popup.configure_notice(notice); + error_popup.show_current_error(); + } + }) + ); + }; + }; }); diff --git a/octoprint_octolapse/static/js/octolapse.helpers.js b/octoprint_octolapse/static/js/octolapse.helpers.js new file mode 100644 index 00000000..0a13da23 --- /dev/null +++ b/octoprint_octolapse/static/js/octolapse.helpers.js @@ -0,0 +1,532 @@ +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ +$(function () { + Octolapse.ListViewColumn = function(text, id, options){ + var self = this; + self.text = text; + self.id = id; + self.sortable = (options || {}).sortable || false; + self.sort_column_id = (options || {}).sort_column_id || id; + self.classes = (options || {}).class || ""; + self.template_id = (options || {}).template_id || "octolapse-list-item"; + self.visible_observable = (options || {}).visible_observable || true; + }; + + Octolapse.ListItemViewModel = function(parent, id, name, title, selected, value){ + var self = this; + self.id = id; + self.name = name; + self.title = title; + self.value = value; + self.data = ko.observable(); + self.data.parent = parent; + //self.selected = ko.observable(!!selected); + self.selected = ko.observable(!!selected); + self.disabled = ko.observable(false); + self.get_list_item_value = function(column){ + var value = null; + if (self.value !== null && self.value[column.id] !== undefined) + value = self.value[column.id]; + else if (self[column.id] !== undefined) + value = self[column.id]; + + return { + item: self, + column: column, + value: value + }; + }; + }; + + Octolapse.ListViewModel = function (parent, id, options){ + var self = this; + self.data = ko.observable(); + self.data.parent = parent; + self.list_id = id; + self.options = options ? options : {}; + self.current_page_index = ko.observable(0); + self.all_selected = ko.observable(false); + self.page_selected = ko.observable(false); + self.page_size = ko.observable(10); + self.has_loaded = ko.observable(false); + // Must be at least 6 pages, can be more. + self.list_items = ko.observableArray([]); + self.page_sizes = [ + {name: "10", value: 10}, + {name: "25", value: 25}, + {name: "50", value: 50}, + {name: "100", value: 100}, + {name: "250", value: 250}, + {name: "500", value: 500} + ]; + self.pager_values = ko.observableArray([]); + self.selection_enabled = false; + if (self.options.selection_enabled !== undefined) + { + self.selection_enabled = self.options.selection_enabled; + } + + self.select_all_enabled = false; + if (self.options.select_all_enabled !== undefined) + { + self.select_all_enabled = self.options.select_all_enabled; + } + self.action_text = self.options.action_text || "Action"; + self.on_resize = self.options.on_resize; + self.columns = self.options.columns || [new Octolapse.ListViewColumn('Name', 'name')]; + self.action_class = self.options.action_class || 'list-item-action'; + self.selection_class = self.options.selection_class || 'list-item-selection'; + self.selection_header_class = self.options.selection_header_class || 'list-item-selection'; + self.to_list_item = self.options.to_list_item || function(item) { + return new Octolapse.ListItemViewModel(self.data.parent, item.name, item.name, item.name, false, null); + }; + self.no_items_template_id = self.options.no_items_template_id || "octolapse-list-no-items"; + self.header_template_id = self.options.header_template_id || "octolapse-list-item-header"; + self.list_item_template_id = self.options.list_item_template_id || "octolapse-list-item"; + self.top_left_pagination_template_id = self.options.top_left_pagination_template_id || "octolapse-list-empty-template"; + self.top_right_pagination_template_id = self.options.top_right_pagination_template_id || "octolapse-list-empty-template"; + self.bottom_left_pagination_template_id = self.options.bottom_left_pagination_template_id || "octolapse-list-empty-template"; + self.bottom_right_pagination_template_id = self.options.bottom_right_pagination_template_id || "octolapse-pagination-page-size"; + self.select_header_template_id = self.options.select_header_template_id || "octolapse-list-select-header-checkbox-page-template"; + self.custom_row_template_id = self.options.custom_row_template_id || null; + self.sort_column = ko.observable(self.options.sort_column || "name"); + self.sort_direction_ascending = ko.observable((self.options.sort_direction || "ascending") === "ascending"); + self.default_sort_direction_ascending = self.sort_direction_ascending(); + self.not_loaded_template_id = options.not_loaded_template_id || "octolapse-list-not-loaded"; + self.pagination_pages = self.options.pagination_pages || 11; + self.pagination_style = self.options.pagination_style || "normal"; + self.pagination = self.options.pagination || "top-and-bottom"; + self.pagination_top = self.pagination === 'top' || self.pagination === "top-and-bottom"; + self.pagination_bottom = self.pagination === 'bottom' || self.pagination === "top-and-bottom"; + self.pagination_row_auto_hide = self.options.pagination_row_auto_hide === undefined? true : self.options.pagination_row_auto_hide; + + self.pagination_top_left_class = "span2"; + self.pagination_top_center_class = "span8"; + self.pagination_top_right_class = "span2"; + self.pagination_bottom_left_class = "span2"; + self.pagination_bottom_center_class = "span8"; + self.pagination_bottom_right_class = "span2"; + + if (self.pagination_style === 'wide') + { + self.pagination_top_left_class = "span1"; + self.pagination_top_center_class = "span10"; + self.pagination_top_right_class = "span1"; + self.pagination_bottom_left_class = "span1"; + self.pagination_bottom_center_class = "span10"; + self.pagination_bottom_right_class = "span1"; + if (!self.options.pagination_pages) + self.pagination_pages = 13; + } + else if (self.pagination_style === "narrow") + { + self.pagination_top_left_class = "span3"; + self.pagination_top_center_class = "span6"; + self.pagination_top_right_class = "span3"; + self.pagination_bottom_left_class = "span3"; + self.pagination_bottom_center_class = "span6"; + self.pagination_bottom_right_class = "span3"; + if (!self.options.pagination_pages) + self.pagination_pages = 9; + } + + self.show_pagination_row_top = ko.pureComputed(function(){ + return self.pagination_top && (self.list_items().length > 0 || !self.pagination_row_auto_hide); + }); + + self.show_pagination_row_bottom = ko.pureComputed(function(){ + return self.pagination_bottom && (self.list_items().length > 0 || !self.pagination_row_auto_hide); + }); + + self.resize = function(){ + //console.log("octolapse.helpers.js - resize"); + if (self.on_resize) + self.on_resize(); + }; + self.is_empty = ko.pureComputed(function(){ + return self.list_items().length == 0; + }); + + self.all_selected.subscribe(function(value){ + //console.log("octolapse.helpers.js - all selected"); + self.select('all', value); + }); + + self.page_selected.subscribe(function(value){ + //console.log("octolapse.helpers.js - page selected"); + self.select('page', value); + }); + + self.page_size.octolapseSubscribeChanged(function (newValue, oldValue) { + //console.log("octolapse.helpers.js - page size changed"); + var new_page = Math.floor(self.current_page_index()*oldValue/newValue); + self.current_page_index(new_page); + self.resize(); + }); + + self.num_pages = ko.pureComputed(function(){ + //console.log("octolapse.helpers.js - num pages changed"); + if (self.list_items().length === 0) + return 0; + return Math.ceil(self.list_items().length / self.page_size()); + }); + + self.pager = ko.pureComputed(function(){ + //console.log("octolapse.helpers.js - pager changed"); + if (self.pagination_pages < 6) { + console.error("The file browser pager must have at least 6 pages."); + return []; + } + // determine how many pager items will be in the array. Can be 0-self.num_pager_pages + var num_pages = self.num_pages(); + if (num_pages < 1) + return []; + var pager_size = Math.min(self.pagination_pages, num_pages); + var midpoint = Math.floor((self.pagination_pages - 4)/2); + // If we have only one page, there is no need for a pager! + + var pager_pages = new Array(pager_size); + // set the first and last page + pager_pages[0] = new Octolapse.PagerPageViewModel('link', 0); + pager_pages[pager_size-1] = new Octolapse.PagerPageViewModel('link', num_pages-1); + for (var index=1; index < pager_size-1; index++) + { + // There are four scenarios to handle: + if (num_pages <= self.pagination_pages) + { + // 1. There are up to pagination_pages pages. In that case just add the available indexes. + pager_pages[index] = new Octolapse.PagerPageViewModel('link', index); + } + else if (self.current_page_index() < self.pagination_pages - 3) + { + // 2. We are in the first pagination_pages - 2 page. In that case we want only one + // ellipsis right before the final page. + if (index < self.pagination_pages - 2) + { + pager_pages[index] = new Octolapse.PagerPageViewModel('link', index); + } + else + { + pager_pages[index] = new Octolapse.PagerPageViewModel('ellipsis'); + } + } + else if (self.current_page_index() < num_pages - self.pagination_pages + 3) + { + // 3. We are in the last num_pages - (pagination_pages - 2) page. In that case we want only one + // ellipsis right after the first page. + if (index === 1 || index === self.pagination_pages - 2) + { + pager_pages[index] = new Octolapse.PagerPageViewModel('ellipsis'); + } + else + { + pager_pages[index] = new Octolapse.PagerPageViewModel('link', self.current_page_index() - midpoint + index - 2 ); + } + } + else + { + // 4. We are at the end of the list. In this case we want ellipsis after page 1 and before max_page + // ellipsis right after the first page. + if (index === 1) + { + pager_pages[index] = new Octolapse.PagerPageViewModel('ellipsis'); + } + else + { + + pager_pages[index] = new Octolapse.PagerPageViewModel('link', num_pages - (self.pagination_pages - index)); + } + } + } + return pager_pages; + }); + + self.get_column = function(column_name){ + for (var index=0; index < self.columns.length; index++) + { + var column = self.columns[index]; + if (column.id === column_name) + return column; + } + }; + + self.sort_by_column = function(column){ + if (!column.sortable) + return; + + //console.log("octolapse.helpers.js - sorting by column:" + column.id); + var ascending = true; + if (column.id !== self.sort_column()) + { + ascending = self.default_sort_direction_ascending; + } + else + { + ascending = !self.sort_direction_ascending(); + } + self.sort_direction_ascending(ascending); + self.sort_column(column.id); + self.current_page_index(0); + }; + + self.get_current_page_indexes = function(){ + //console.log("octolapse.helpers.js - current page indexes changed"); + if (self.page_size() === 0 || self.list_items().length === 0) + return null; + var page_start_index = Math.max(self.current_page_index() * self.page_size(), 0); + var page_end_index = Math.min(page_start_index + self.page_size(), self.list_items().length); + return [page_start_index, page_end_index]; + }; + + self.list_items_sorted = ko.pureComputed(function() { + //console.log("octolapse.helpers.js - list items sorting"); + var current_sort_column = self.get_column(self.sort_column()); + if (!current_sort_column) + return self.list_items(); + var left_direction = self.sort_direction_ascending() ? -1 : 1; + var right_direction = left_direction * -1; + var sort_column_id = current_sort_column.sort_column_id; + return self.list_items().sort( + function (left, right) { + var leftItem = (left.value || {})[sort_column_id] || left[sort_column_id]; + var rightItem = (right.value || {})[sort_column_id] || right[sort_column_id]; + return leftItem === rightItem ? 0 : (leftItem < rightItem ? left_direction : right_direction); + }); + }); + + self.current_page = ko.pureComputed(function(){ + //console.log("octolapse.helpers.js - current page changing"); + var page_indexes = self.get_current_page_indexes(); + if (!page_indexes) { + return[]; + } + return self.list_items_sorted().slice(page_indexes[0], page_indexes[1]); + }); + + //self.max_page = ko.pureComputed(function(){return Math.floor(self.list_items().length/self.page_size());}); + + self._fix_page_numbers = function() { + //console.log("octolapse.helpers.js - repairing page numbers"); + // Ensure we are not off of any existing pages. This can happen during a reload, after a delete, or + // maybe some other scenarios. + // Make sure we haven't deleted ourselves off the the current page! + var cur_page = self.current_page_index(); + var has_changed = false; + if (cur_page + 1 >= self.num_pages()) + { + has_changed = true; + cur_page = self.num_pages() - 1; + } + if (cur_page < 0) + { + has_changed = true; + cur_page = 0; + } + + if (has_changed) + self.current_page_index(cur_page); + }; + + self.set = function(items, on_added){ + self.has_loaded(true); + var list_items = []; + for (var index = 0; index < items.length; index++) + { + var list_item = self.to_list_item(items[index]); + list_items.push(list_item); + if (on_added) + { + on_added(list_item); + } + } + self.list_items(list_items); + self._fix_page_numbers(); + self.resize(); + }; + + self.clear = function(){ + self.list_items([]); + }; + + self.add = function(item){ + self.has_loaded(true); + self.list_items.push(self.to_list_item(item)); + }; + + self.get = function(id){ + for (var index = 0; index < self.list_items().length; index++) + { + if (self.list_items()[index].id === id) + { + return self.list_items()[index]; + } + } + return null; + }; + + self.get_index = function(id){ + for (var index = 0; index < self.list_items().length; index++) + { + if (self.list_items()[index].id === id) + { + return index; + } + } + return -1; + }; + + self.remove = function(id){ + var item = self.get(id); + if (item) + { + self.list_items.remove(item); + self._fix_page_numbers(); + return item; + } + return null; + }; + + self.replace = function(item){ + var list_item = self.to_list_item(item); + var index = self.get_index(list_item.id); + if (index > -1) + { + var old_item = self.list_items.splice(index, 1, list_item); + return old_item[0]; + } + return null; + }; + + self.select = function(type, selection){ + //console.log("octolapse.helpers.js - selecting " + type); + // force selection to true or false + selection = !!selection; + var page_indexes = null; + if (type === 'page') + { + page_indexes = self.get_current_page_indexes(); + } + else if (type === 'all') + { + if (self.list_items().length > 0) + { + page_indexes = [0, self.list_items().length]; + } + else { + return; + } + } + + if (!page_indexes) + { + return; + } + var list_items_sorted = self.list_items_sorted(); + for (var index=page_indexes[0]; index < page_indexes[1]; index++) + { + list_items_sorted[index].selected(selection); + } + }; + + self.goto_page = function(page_index){ + var num_pages =self.num_pages(); + if (page_index > num_pages - 1) + page_index = num_pages - 1; + + if (page_index < 0) + page_index = 0; + + self.current_page_index(page_index); + }; + + self.selected_count = ko.pureComputed(function(){ + var selected_count = 0; + self.list_items().forEach(function(item, index){ + if (item.selected()) { + selected_count++; + } + }); + return selected_count; + }); + + self.selected = function(fields_to_return){ + //console.log("octolapse.helpers.js - returning selected items."); + var selected_items = []; + for (var index=0; index < self.list_items().length; index++) + { + var item = self.list_items()[index]; + if (item.selected()) { + if (fields_to_return) + { + var value = {}; + fields_to_return.forEach(function(field, index){ + var is_value_field = false; + var field_value = null; + if (item[field] === undefined) + { + is_value_field = true; + field_value = item.value[field]; + } + else + { + field_value = item[field]; + } + if (field_value && (ko.isObservable(field_value) || ko.isComputed(field_value))) + { + field_value = field_value(); + } + if(is_value_field) + { + if (!value.value) + { + value.value = {}; + } + value.value[field] = field_value; + } + else{ + value[field] = field_value; + } + + }); + selected_items.push(value); + } + else + { + selected_items.push(item); + } + } + } + return selected_items; + }; + + }; + + Octolapse.PagerPageViewModel = function(type, index){ + var self = this; + self.index = type === 'link'? index : -1; + self.name = type === 'link' ? (index+1).toString() : ""; + self.type = type; + }; + +}); diff --git a/octoprint_octolapse/static/js/octolapse.js b/octoprint_octolapse/static/js/octolapse.js index e5472407..5f804680 100644 --- a/octoprint_octolapse/static/js/octolapse.js +++ b/octoprint_octolapse/static/js/octolapse.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -22,7 +22,11 @@ ################################################################################## */ Octolapse = {}; -Octolapse.Printers = { 'current_profile_guid': function () {return null;}}; +Octolapse.Printers = { + 'current_profile_guid': function () { + return null; + } +}; OctolapseViewModel = {}; //UI_API_KEY = ""; Octolapse.Help = null; @@ -31,14 +35,12 @@ $(function () { // Finds the first index of an array with the matching predicate Octolapse.IsShowingSettingsChangedPopup = false; - Octolapse.toggleContentFunction = function ($elm, options, updateObservable) - { + Octolapse.toggleContentFunction = function ($elm, options, updateObservable) { - if(options.toggle_observable){ + if (options.toggle_observable) { //console.log("Toggling element."); - if(updateObservable) { + if (updateObservable) { options.toggle_observable(!options.toggle_observable()); - //console.log("Observable updated - " + options.toggle_observable()) } if (options.toggle_observable()) { if (options.class_showing) { @@ -49,25 +51,23 @@ $(function () { $elm.children('[class^="icon-"]').removeClass(options.class_hiding); $elm.children('[class^="fa"]').removeClass(options.class_hiding); } - if(options.container) { + if (options.container) { if (options.parent) { $elm.parents(options.parent).find(options.container).stop().slideDown('fast', options.onComplete); } else { $(options.container).stop().slideDown('fast', options.onComplete); } } - } - else - { - if (options.class_hiding) { - $elm.children('[class^="icon-"]').addClass(options.class_hiding); - $elm.children('[class^="fa"]').addClass(options.class_hiding); - } + } else { + if (options.class_hiding) { + $elm.children('[class^="icon-"]').addClass(options.class_hiding); + $elm.children('[class^="fa"]').addClass(options.class_hiding); + } if (options.class_showing) { $elm.children('[class^="icon-"]').removeClass(options.class_showing); $elm.children('[class^="fa"]').removeClass(options.class_showing); } - if(options.container) { + if (options.container) { if (options.parent) { $elm.parents(options.parent).find(options.container).stop().slideUp('fast', options.onComplete); } else { @@ -75,8 +75,7 @@ $(function () { } } } - } - else { + } else { if (options.class) { $elm.children('[class^="icon-"]').toggleClass(options.class_hiding + ' ' + options.class_showing); $elm.children('[class^="fa"]').toggleClass(options.class_hiding + ' ' + options.class_showing); @@ -93,32 +92,32 @@ $(function () { }; Octolapse.toggleContent = { - init: function(element, valueAccessor) { - var $elm = $(element), - options = $.extend({ - class_showing: null, - class_hiding: null, - container: null, - parent: null, - toggle_observable: null, - onComplete: function() { - $(document).trigger("slideCompleted"); - } - }, valueAccessor()); - - if(options.toggle_observable) { - Octolapse.toggleContentFunction($elm, options, false); + init: function (element, valueAccessor) { + var $elm = $(element), + options = $.extend({ + class_showing: null, + class_hiding: null, + container: null, + parent: null, + toggle_observable: null, + onComplete: function () { + $(document).trigger("slideCompleted"); } + }, valueAccessor()); + if (options.toggle_observable) { + Octolapse.toggleContentFunction($elm, options, false); + } - $elm.on("click", function(e) { - e.preventDefault(); - Octolapse.toggleContentFunction($elm,options, true); - }); - } - }; - ko.bindingHandlers.octolapseToggle = Octolapse.toggleContent ; + $elm.on("click", function (e) { + e.preventDefault(); + Octolapse.toggleContentFunction($elm, options, true); + + }); + } + }; + ko.bindingHandlers.octolapseToggle = Octolapse.toggleContent; Octolapse.arrayFirstIndexOf = function (array, predicate, predicateOwner) { for (var i = 0, j = array.length; i < j; i++) { @@ -135,6 +134,7 @@ $(function () { .toString(16) .substring(1); } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); }; @@ -178,21 +178,22 @@ $(function () { }); }; - Octolapse.progressBar = function (cancel_callback, initial_text) - { + Octolapse.progressBar = function (cancel_callback, initial_text) { var self = this; self.notice = null; self.$progress = null; self.$progressText = null; self.initial_text = initial_text; - self.close = function() - { + self.popup_margin = 15; + self.popup_width_with_margin = 400; + self.popup_width = self.popup_width_with_margin - self.popup_margin*2; + + self.close = function () { if (self.loader != null) self.loader.remove(); }; - self.update = function(percent_complete, progress_text) - { + self.update = function (percent_complete, progress_text) { self.notice.find(".remove_button").remove(); if (self.$progress == null) @@ -201,12 +202,13 @@ $(function () { percent_complete = 0; if (percent_complete > 100) percent_complete = 100; - if (percent_complete == 100) { + if (percent_complete === 100) { //console.log("Received 100% complete progress message, removing progress bar."); self.loader.remove(); - return null + return null; } - self.$progress.width(percent_complete + "%").attr("aria-valuenow", percent_complete).find("span").html(percent_complete + "%"); + var percent_complete_text = percent_complete.toFixed(1); + self.$progress.width(percent_complete_text + "%").attr("aria-valuenow", percent_complete_text).find("span").html(percent_complete_text + "%"); self.$progressText.text(progress_text); return self; }; @@ -218,12 +220,13 @@ $(function () {
      \
      ', icon: 'fa fa-cog fa-spin', + width: self.popup_width.toString() + "px", confirm: { confirm: Octolapse.Globals.is_admin(), buttons: [{ text: 'Cancel', click: cancel_callback - },{ + }, { text: 'Close', addClass: 'remove_button', click: cancel_callback @@ -237,12 +240,12 @@ $(function () { history: { history: false }, - before_open: function(notice) { + before_open: function (notice) { self.notice = notice.get(); self.$progress = self.notice.find("div.progress-bar"); self.$progressText = self.notice.find("span.progress-text"); self.notice.find(".remove_button").remove(); - self.update(0,self.initial_text); + self.update(0, self.initial_text); } }); @@ -253,11 +256,11 @@ $(function () { Octolapse.COOKIE_EXPIRE_DAYS = 30; Octolapse.setLocalStorage = function (name, value) { - localStorage.setItem("octolapse_"+name,value) + localStorage.setItem("octolapse_" + name, value); }; Octolapse.getLocalStorage = function (name, value) { - return localStorage.getItem("octolapse_"+name) + return localStorage.getItem("octolapse_" + name); }; Octolapse.replaceAll = function (str, find, replace) { @@ -268,13 +271,12 @@ $(function () { new PNotify(options); }; - // Create Helpers - Octolapse.convertAxisSpeedUnit = function (speed, newUnit, previousUnit, tolerance, tolerance_unit){ + // Create Helpers + Octolapse.convertAxisSpeedUnit = function (speed, newUnit, previousUnit, tolerance, tolerance_unit) { if (speed == null) return null; - if(tolerance_unit !== newUnit) - { - switch (newUnit){ + if (tolerance_unit !== newUnit) { + switch (newUnit) { case "mm-min": tolerance = tolerance * 60.0; break; @@ -283,14 +285,14 @@ $(function () { break; } } - if(newUnit === previousUnit) + if (newUnit === previousUnit) return Octolapse.roundToIncrement(speed, tolerance); - switch (newUnit){ + switch (newUnit) { case "mm-min": - return Octolapse.roundToIncrement(speed*60.0, tolerance); + return Octolapse.roundToIncrement(speed * 60.0, tolerance); case "mm-sec": - return Octolapse.roundToIncrement(speed/60.0, tolerance); + return Octolapse.roundToIncrement(speed / 60.0, tolerance); } return null; }; @@ -315,12 +317,12 @@ $(function () { numDecimals = increment.toString().split(".")[1].length; // tofixed can only support 20 decimals, reduce if necessary - if(numDecimals > 20) { + if (numDecimals > 20) { //console.log("Too much precision for tofixed:" + numDecimals + " - Reducing to 20"); numDecimals = 20; } // truncate value to numDecimals decimals - value = parseFloat(value.toFixed(numDecimals).toString()) + value = parseFloat(value.toFixed(numDecimals).toString()); return value; }; @@ -328,34 +330,77 @@ $(function () { Octolapse.Popups = {}; Octolapse.displayPopupForKey = function (options, popup_key, remove_keys) { Octolapse.closePopupsForKeys(remove_keys); - Octolapse.Popups[popup_key] = new PNotify(options); + var popup = new PNotify(options); + Octolapse.Popups[popup_key] = popup; + return popup; }; - Octolapse.closePopupsForKeys = function(remove_keys) { - if (!$.isArray(remove_keys)) - { + Octolapse.closePopupsForKeys = function (remove_keys) { + if (!$.isArray(remove_keys)) { remove_keys = [remove_keys]; } for (var index = 0; index < remove_keys.length; index++) { var key = remove_keys[index]; if (key in Octolapse.Popups) { - - Octolapse.Popups[key].remove(); + var notice = Octolapse.Popups[key]; + if (notice.state === "opening") { + notice.options.animation = "none"; + } + notice.remove(); delete Octolapse.Popups[key]; } } }; - Octolapse.removeKeyForClosedPopup = function(key){ - if (key in Octolapse.Popups) { - delete Octolapse.Popups[key]; - } + Octolapse.removeKeyForClosedPopup = function (key) { + if (key in Octolapse.Popups) { + var notice = Octolapse.Popups[key]; + delete Octolapse.Popups[key]; + } + }; + + Octolapse.checkPNotifyDefaultConfirmButtons = function () { + // check to see if exactly two default pnotify confirm buttons exist. + // If we keep running into problems we might need to inspect the buttons to make sure they + // really are the defaults. + if (PNotify.prototype.options.confirm.buttons.length !== 2) { + // Someone removed the confirmation buttons, darnit! Report the error and re-add the buttons. + var message = "Octolapse detected the removal or addition of PNotify default confirmation buttons, " + + "which should not be done in a shared environment. Some plugins may show strange behavior. Please " + + "report this error at https://github.com/FormerLurker/Octolapse/issues. Octolapse will now clear " + + "and re-add the default PNotify buttons."; + console.error(message); + + // Reset the buttons in case extra buttons were added. + PNotify.prototype.options.confirm.buttons = []; + + var buttons = [ + { + text: "Ok", + addClass: "", + promptTrigger: true, + click: function (b, a) { + b.remove(); + b.get().trigger("pnotify.confirm", [b, a]); + } + }, + { + text: "Cancel", + addClass: "", + promptTrigger: true, + click: function (b) { + b.remove(); + b.get().trigger("pnotify.cancel", b); + } + } + ]; + PNotify.prototype.options.confirm.buttons = buttons; + } }; Octolapse.ConfirmDialogs = {}; - Octolapse.closeConfirmDialogsForKeys = function(remove_keys) { - if (!$.isArray(remove_keys)) - { + Octolapse.closeConfirmDialogsForKeys = function (remove_keys) { + if (!$.isArray(remove_keys)) { remove_keys = [remove_keys]; } for (var index = 0; index < remove_keys.length; index++) { @@ -366,40 +411,75 @@ $(function () { delete Octolapse.ConfirmDialogs[key]; } } - } - Octolapse.showConfirmDialog = function(key, title, text, onConfirm, onCancel, onComplete) - { + }; + + Octolapse.showConfirmDialog = function (key, title, text, onConfirm, onCancel, onComplete, onOption, optionButtonText) { Octolapse.closeConfirmDialogsForKeys([key]); - Octolapse.ConfirmDialogs[key] = ( - new PNotify({ - title: title, - text: text, - icon: 'fa fa-question', - hide: false, - addclass: "octolapse", - confirm: { - confirm: true + // Make sure that the default pnotify buttons exist + Octolapse.checkPNotifyDefaultConfirmButtons(); + options = { + title: title, + text: text, + icon: 'fa fa-question', + hide: false, + addclass: "octolapse", + confirm: { + confirm: true, + }, + buttons: { + closer: false, + sticker: false + }, + history: { + history: false + } + }; + if (onOption && optionButtonText) { + var confirmButtons = [ + { + text: "Ok", + addClass: "", + promptTrigger: true, + click: function (b, a) { + b.remove(); + b.get().trigger("pnotify.confirm", [b, a]); + } }, - buttons: { - closer: false, - sticker: false + { + text: optionButtonText, + click: function () { + if (onOption) + onOption(); + if (onComplete) + onComplete(); + Octolapse.closeConfirmDialogsForKeys([key]); + } }, - history: { - history: false + { + text: "Cancel", + addClass: "", + promptTrigger: true, + click: function (b) { + b.remove(); + b.get().trigger("pnotify.cancel", b); + } } - }) - ).get().on('pnotify.confirm', function(){ - if(onConfirm) + ]; + options.confirm.buttons = confirmButtons; + } + Octolapse.ConfirmDialogs[key] = ( + new PNotify(options) + ).get().on('pnotify.confirm', function () { + if (onConfirm) onConfirm(); - if(onComplete) - { + if (onComplete) { onComplete(); } - }).on('pnotify.cancel', function() { + }).on('pnotify.cancel', function () { if (onCancel) onCancel(); - if(onComplete) { - onComplete(); + if (onComplete) { + onComplete(); } }); }; @@ -435,12 +515,11 @@ $(function () { }, "You must select a file."); - $.validator.addMethod("check_one", function(value, elem, param) - { - //console.log("Validating trigger checks"); - $(param).val(); - return $(param + ":checkbox:checked").length > 0; - } + $.validator.addMethod("check_one", function (value, elem, param) { + //console.log("Validating trigger checks"); + $(param).val(); + return $(param + ":checkbox:checked").length > 0; + } ); // Add custom validator for csv floats @@ -457,46 +536,45 @@ $(function () { return /^-?\d+$/.test(value); }, 'Please enter an integer value.'); - Octolapse.isPercent = function(value){ + Octolapse.isPercent = function (value) { //console.log("is percent - Octolapse") - if(typeof value != 'string') + if (typeof value !== 'string') return false; if (!value) return false; value = value.trim(); - if(! (value.length > 1 && value[value.length-1] === "%")) + if (!(value.length > 1 && value[value.length - 1] === "%")) return false; - value = value.substr(0,value.length-1); - return Octolapse.isFloat(value) + value = value.substr(0, value.length - 1); + return Octolapse.isFloat(value); }; - Octolapse.isFloat = function(value){ + Octolapse.isFloat = function (value) { if (!value) return false; - return !isNaN(value) && !isNaN(parseFloat(value)) + return !isNaN(value) && !isNaN(parseFloat(value)); }; - Octolapse.parseFloat = function(value){ + Octolapse.parseFloat = function (value) { var ret = parseFloat(value); - if(!isNaN(ret)) + if (!isNaN(ret)) return ret; return null; }; - Octolapse.parsePercent = function(value){ + Octolapse.parsePercent = function (value) { value = value.trim(); - if(value.length > 1 && value[value.length-1] === "%") - value = value.substr(0,value.length-1); + if (value.length > 1 && value[value.length - 1] === "%") + value = value.substr(0, value.length - 1); else return null; - return Octolapse.parseFloat(value) - } + return Octolapse.parseFloat(value); + }; $.validator.addMethod('slic3rPEFloatOrPercent', function (value) { if (!value) return true; - if(!Octolapse.isPercent(value) && !Octolapse.isFloat(value)) - { + if (!Octolapse.isPercent(value) && !Octolapse.isFloat(value)) { return false; } return true; @@ -506,14 +584,14 @@ $(function () { function (value) { if (!value) return true; - if(Octolapse.isPercent(value)) + if (Octolapse.isPercent(value)) value = Octolapse.parsePercent(value); - else if(Octolapse.isFloat(value)) + else if (Octolapse.isFloat(value)) value = Octolapse.parseFloat(value); var rounded_value = Octolapse.roundToIncrement(value, 0.0001); - if (rounded_value == value) + if (rounded_value === value) return true; - return false + return false; }, 'Please enter a multiple of 0.0001.'); @@ -523,7 +601,7 @@ $(function () { try { var r = /^\d+$/.test(value); // Check the number against a regex to ensure it contains only digits. var n = +value; // Try to convert to number. - return r && !isNaN(n) && n > 0 && n % 1 == 0; + return r && !isNaN(n) && n > 0 && n % 1 === 0; } catch (e) { return false; } @@ -578,15 +656,14 @@ $(function () { function (value, element, param) { //console.log("ifCheckedEnsureNonNull"); if (value === "on") - for (var index = 0; index < param.length; index++) - { + for (var index = 0; index < param.length; index++) { // Get the target selector var $target = $(param[index]); // If we found no target return false if ($target.length === 0) return false; - var value = $target.val() - if (value == null || value == '') + var targetVal = $target.val(); + if (targetVal == null || targetVal === '') return false; } return true; @@ -597,9 +674,9 @@ $(function () { $.validator.addMethod('ifOtherCheckedEnsureNonNull', function (value, element, param) { // see if the target is checked - var target_checked = $(param + ":checkbox:checked").length > 0 + var target_checked = $(param + ":checkbox:checked").length > 0; if (target_checked) - return value != null && value != ''; + return value != null && value !== ''; return true; }); @@ -607,8 +684,8 @@ $(function () { function (value, element) { var testUrl = value.toUpperCase().replace("{CAMERA_ADDRESS}", 'http://w.com/'); var is_valid = ( - jQuery.validator.methods.url.call(this, testUrl, element) || - jQuery.validator.methods.url.call(this, "http://w.com" + testUrl, element) + $.validator.methods.url.call(this, testUrl, element) || + $.validator.methods.url.call(this, "http://w.com" + testUrl, element) ); return is_valid; }); @@ -617,15 +694,15 @@ $(function () { function (value, element) { var testUrl = value.toUpperCase().replace("{CAMERA_ADDRESS}", 'http://w.com/').replace("{value}", "1"); var is_valid = ( - jQuery.validator.methods.url.call(this, testUrl, element) || - jQuery.validator.methods.url.call(this, "http://w.com" + testUrl, element) + $.validator.methods.url.call(this, testUrl, element) || + $.validator.methods.url.call(this, "http://w.com" + testUrl, element) ); return is_valid; }); $.validator.addMethod('octolapseRenderingTemplate', function (value, element) { - var data = {"rendering_template":value}; + var data = { "rendering_template": value }; $.ajax({ url: "./plugin/octolapse/validateRenderingTemplate", type: "POST", @@ -635,7 +712,7 @@ $(function () { contentType: "application/json", dataType: "json", success: function (result) { - if(result.success) + if (result.success) return true; return false; }, @@ -643,15 +720,15 @@ $(function () { var message = "Octolapse could not validate the rendering template."; var options = { - title: 'Rendering Template Error', - text: message, - type: 'error', - hide: false, - addclass: "octolapse", - desktop: { - desktop: true - } - }; + title: 'Rendering Template Error', + text: message, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; Octolapse.displayPopup(options); return false; } @@ -659,18 +736,32 @@ $(function () { }); - jQuery.extend(jQuery.validator.messages, { + $.validator.addMethod('octolapsePrinterSnapshotCommand', function (value, element, param) { + if (value === "") + return true; + var data = { "snapshot_command": value }; + var rpcParam = { + url: "./plugin/octolapse/validateSnapshotCommand", + type: "POST", + data: JSON.stringify(data), + dataType: "json", + contentType: "application/json", + }; + return $.validator.methods.remote.call(this, value, element, rpcParam, 'octolapsePrinterSnapshotCommand'); + }, "Must be empty, or must contain at least one non-whitespace character that is not part of a gcode comment."); + + jQuery.extend($.validator.messages, { name: "Please enter a name.", required: "This field is required.", url: "Please enter a valid URL.", number: "Please enter a valid number.", equalTo: "Please enter the same value again.", - maxlength: jQuery.validator.format("Please enter no more than {0} characters."), - minlength: jQuery.validator.format("Please enter at least {0} characters."), - rangelength: jQuery.validator.format("Please enter a value between {0} and {1} characters long."), - range: jQuery.validator.format("Please enter a value between {0} and {1}."), - max: jQuery.validator.format("Please enter a value less than or equal to {0}."), - min: jQuery.validator.format("Please enter a value greater than or equal to {0}."), + maxlength: $.validator.format("Please enter no more than {0} characters."), + minlength: $.validator.format("Please enter at least {0} characters."), + rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."), + range: $.validator.format("Please enter a value between {0} and {1}."), + max: $.validator.format("Please enter a value less than or equal to {0}."), + min: $.validator.format("Please enter a value greater than or equal to {0}."), octolapseCameraRequestTemplate: "The value is not a url. You may use {camera_address} or {value} tokens.", octolapseSnapshotTemplate: "The value is not a url. You may use {camera_address} to refer to the web camera address." }); @@ -688,178 +779,173 @@ $(function () { return null; // Check to see if it is a percent - var is_percent = Octolapse.isPercent(val) - if(is_percent) - { - if(round_to_percent) - { + var is_percent = Octolapse.isPercent(val); + if (is_percent) { + if (round_to_percent) { val = Octolapse.parsePercent(val); - } - else + } else return null; - } - else + } else val = Octolapse.parseFloat(val); if (val == null || isNaN(val)) return null; - try{ + try { var round_to_increment = round_to_increment_mm_min; if (is_percent) { - round_to_increment = round_to_percent - } - else if (current_units_observable() === 'mm-sec') { + round_to_increment = round_to_percent; + } else if (current_units_observable() === 'mm-sec') { round_to_increment = round_to_increment_mm_sec; } var rounded = Octolapse.roundToIncrement(val, round_to_increment); - if(is_percent && return_text) + if (is_percent && return_text) return rounded.toString() + "%"; else if (return_text) return rounded.toString(); return rounded; - } - catch (e){ - console.log("Error rounding axis_speed_unit"); + } catch (e) { + console.error("Error rounding axis_speed_unit"); } }; + Octolapse.streamLoading = {}; + Octolapse.streamLoading.on_error = function (e) { + var element = e.target; + var options = e.data.options; + $(element).hide(); + $(options.loading_selector).hide(); + if (options.src !== "") { + console.error("Stream Error."); + $(options.error_selector).html("

      Error loading the stream at: " + options.src + "

      Check the 'Stream Address Template' setting in your camera profile.

      ").fadeIn(1000); + } else { + // This should not happen! + console.error("Stream Error, but src is empty."); + $(options.error_selector).html("

      No stream url was provided. Check the 'Stream Address Template' setting.

      ").fadeIn(1000); + } + }; + Octolapse.streamLoading.on_loaded = function (e) { + var element = e.target; + var options = e.data.options; + + if (options.src === "") { + // If the src is empty, that means we have closed the stream, or we have no stream src. + // When we close the stream we set it to an empty image to prevent the error + // handler from being called. However, assume this is an error. + //console.log("Stream closed, or we have no stream address."); + $(options.loading_selector).hide(); + $(element).hide(); + $(options.error_selector).html("

      No stream url was provided. Check the 'Stream Address Template' setting.

      ").fadeIn(1000); + return; + } + // If we are here, we have a valid stream that is loaded. + var max_height = options.max_height || 333; + var max_width = options.max_width || 588; + $(element).width('auto').height('auto'); + //console.log("Stream Loaded."); + // get the width and height of the stream element + var stream_width = $(element).width(); + var stream_height = $(element).height(); + // See if the image is greater than the max + if (stream_width > max_width || stream_height > max_height) { + //console.log("Resizing Stream."); + var ratioX = max_width / stream_width; + var ratioY = max_height / stream_height; + var ratio = Math.min(ratioX, ratioY); + var newWidth = stream_width * ratio; + var newHeight = stream_height * ratio; + + $(element).width(newWidth).height(newHeight); + } + $(options.error_selector).hide(); + $(options.loading_selector).hide(); + $(element).show(); + //console.log("Stream shown."); + }; ko.bindingHandlers.streamLoading = { - init: function(element, valueAccessor) { + update: function (element, valueAccessor) { - },update: function(element, valueAccessor) { // close the stream if one exists - $(element).attr('src', ""); - console.log("Binding element to streamLoading"); - var self = this; var options = valueAccessor(); - self.max_height = ko.unwrap(options.max_height) || 333; - self.max_width = ko.unwrap(options.max_width) || 588; - self.src = ko.unwrap(options.src); - var error_selector = ko.unwrap(options.error_selector); - var loading_selector = ko.unwrap(options.loading_selector); + var current_src = $(element).attr('src'); + var new_src = options.src; + if (options.src === "") + new_src = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="; + //console.log("Camera stream is updating from old_src: " + current_src + " to new src: " + new_src + " for id: ??."); + $(element).unbind("load").one("load", { options: options }, Octolapse.streamLoading.on_loaded); + $(element).unbind("error").one("error", { options: options }, Octolapse.streamLoading.on_error); + $(options.loading_selector).html("

      Loading webcam stream at: " + options.src + "

      ").show(); $(element).hide(); - $(error_selector).hide(); - $(loading_selector).html("

      Loading webcam stream at: " + self.src + "

      ").show(); - $(element).unbind('load').unbind('error'); - - // Create a handler to handle load and error - self.on_loaded = function(){ - $(element).width('auto').height('auto'); - console.log("Stream Loaded."); - // get the width and height of the stream element - var stream_width = $(element).width(); - var stream_height = $(element).height(); - // See if the image is greater than the max - if (stream_width > self.max_width || stream_height > self.max_height) - { - console.log("Resizing Stream."); - var ratioX = self.max_width / stream_width; - var ratioY = self.max_height / stream_height; - var ratio = Math.min(ratioX, ratioY); - var newWidth = stream_width * ratio; - var newHeight = stream_height * ratio; - - $(element).width(newWidth).height(newHeight); - } - $(error_selector).hide(); - $(loading_selector).hide(); - $(element).show(); - }; - - $(element).off('load', self.on_loaded); - $(element).one('load', self.on_loaded); - - self.on_error = function(){ - $(element).hide(); - $(loading_selector).hide(); - if (src !== "") { - console.log("Stream Error."); - $(error_selector).html("

      Error loading the stream at: " + src + "

      Check the 'Stream Address Template' setting in your camera profile.

      ").fadeIn(1000); - } - else{ - console.log("Stream Closing."); - $(error_selector).html("

      No stream url was provided. Check the 'Stream Address Template' setting.

      ").fadeIn(1000); - } - }; - $(element).off('error', self.on_error); - $(element).one('error', self.on_error); - // set the source for the stream - $(element).attr('src', self.src); + $(options.error_selector).hide(); + $(element).attr('src', new_src); + //console.log("Finished updating camera stream."); + } }; ko.bindingHandlers.octolapseSliderValue = { - // Init, runs on initialization - init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { - if ( ko.isObservable(valueAccessor()) && (element instanceof HTMLInputElement) && (element.type === "range") ) - { - // Add event listener to the slider, this will update the observable on input (just moving the slider), - // Otherwise, you have to move the slider then release it for the value to change - element.addEventListener('input', function(){ - // Update the observable - if (ko.unwrap(valueAccessor()) != element.value) - { - valueAccessor()(element.value); - - // Trigger the change event, awesome fix that makes - // changing a dropdown and a range slider function the same way - element.dispatchEvent(new Event('change')); - } - }); // End event listener + // Init, runs on initialization + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + if (ko.isObservable(valueAccessor()) && (element instanceof HTMLInputElement) && (element.type === "range")) { + // Add event listener to the slider, this will update the observable on input (just moving the slider), + // Otherwise, you have to move the slider then release it for the value to change + element.addEventListener('input', function () { + // Update the observable + if (ko.unwrap(valueAccessor()) !== element.value) { + valueAccessor()(element.value); + + // Trigger the change event, awesome fix that makes + // changing a dropdown and a range slider function the same way + element.dispatchEvent(new Event('change')); + } + }); // End event listener } - }, // End init - // Update, runs whenever observables for this binding change(and on initialization) - update: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + }, // End init + // Update, runs whenever observables for this binding change(and on initialization) + update: function (element, valueAccessor, allBindings, viewModel, bindingContext) { // Make sure the parameter passed is an observable - if ( ko.isObservable(valueAccessor()) && (element instanceof HTMLInputElement) && (element.type === "range") ) - { - // Update the slider value (so if the value changes programatically, the slider will update) - if (element.value != ko.unwrap(valueAccessor())) - { - element.value = ko.unwrap(valueAccessor()); - element.dispatchEvent(new Event('input')); - } + if (ko.isObservable(valueAccessor()) && (element instanceof HTMLInputElement) && (element.type === "range")) { + // Update the slider value (so if the value changes programatically, the slider will update) + if (element.value !== ko.unwrap(valueAccessor())) { + element.value = ko.unwrap(valueAccessor()); + element.dispatchEvent(new Event('input')); + } } - } // End update - }; // End octolapseSliderValue + } // End update + }; // End octolapseSliderValue // Got this cool snippit from the knockoutjs.com site! // I added some jquery to disable elements so that no clicking can be done during the fade out ko.bindingHandlers.slideVisible = { - init: function(element, valueAccessor) { + init: function (element, valueAccessor) { // Initially set the element to be instantly visible/hidden depending on the value var value = valueAccessor(); $(element).toggle(ko.unwrap(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable }, - update: function(element, valueAccessor) { + update: function (element, valueAccessor) { // Whenever the value subsequently changes, slowly fade the element in or out var value = valueAccessor(); - if(ko.unwrap(value)) - { + if (ko.unwrap(value)) { $(element).find(".ignore_hidden_errors").removeClass("hiding"); - $(element).removeClass("octolapse_unclickable").stop( true, true ).slideDown(); - } - else - { + $(element).removeClass("octolapse_unclickable").stop(true, true).slideDown(); + } else { $(element).find(".ignore_hidden_errors").addClass("hiding"); - $(element).addClass("octolapse_unclickable").stop( true, true ).slideUp(); + $(element).addClass("octolapse_unclickable").stop(true, true).slideUp(); } } }; - /* Might need this. Is an interesting extension - ko.subscribable.fn.subscribeChanged = function (callback) { - var savedValue = this.peek(); - return this.subscribe(function (latestValue) { - var oldValue = savedValue; - savedValue = latestValue; - callback(latestValue, oldValue); - }); - }; - */ - ko.extenders.confirmable = function(target, options) { + ko.subscribable.fn.octolapseSubscribeChanged = function (callback) { + var savedValue = this.peek(); + return this.subscribe(function (latestValue) { + var oldValue = savedValue; + savedValue = latestValue; + callback(latestValue, oldValue); + }); + }; + + ko.extenders.confirmable = function (target, options) { var self = this; self.message = "Are you sure?"; self.title = "Confirm"; @@ -879,75 +965,72 @@ $(function () { self.current_value = null; self.is_ignored = null; self.is_confirmed = null; - self.get_options = function(options){ + self.get_options = function (options) { if (!options) return; - if(options.message) + if (options.message) self.message = options.message; - if(options.title) + if (options.title) self.title = options.title; - if(options.key) + if (options.key) self.dialog_key = options.key; - if(options.on_before_changed) + if (options.on_before_changed) self.on_before_changed_callback = options.on_before_changed; - if(options.on_before_confirm) + if (options.on_before_confirm) self.before_confirm_callback = options.on_before_confirm; - if(options.on_cancel) + if (options.on_cancel) self.cancel_callback = options.on_cancel; - if(options.on_confirmed) + if (options.on_confirmed) self.confirmed_callbacked = options.on_confirmed; - if(options.on_complete) + if (options.on_complete) self.complete_callback = options.on_complete; - if(options.ignore) + if (options.ignore) self.ignore_callback = options.ignore; - if(options.auto_confirm) + if (options.auto_confirm) self.auto_confirm_callback = options.auto_confirm; }; self.get_options(options); - self.on_before_changed = function(){ - if (self.on_before_changed_callback){ + self.on_before_changed = function () { + if (self.on_before_changed_callback) { self.on_before_changed_callback(self.new_value, self.current_value); } }; - self.on_before_confirm = function(){ - if(self.before_confirm_callback) - { + self.on_before_confirm = function () { + if (self.before_confirm_callback) { options = self.before_confirm_callback(self.new_value, self.current_value); - if (options) - { + if (options) { self.get_options(options); } } }; - self.on_cancel = function() { - if(self.cancel_callback) + self.on_cancel = function () { + if (self.cancel_callback) self.cancel_callback(self.new_value, self.current_value); }; - self.on_confirmed = function() { - if(self.confirmed_callbacked) - self.confirmed_callbacked(self.new_value, self.current_value); + self.on_confirmed = function () { + if (self.confirmed_callbacked) + self.confirmed_callbacked(self.new_value, self.current_value); }; - self.on_complete = function() { - if(self.complete_callback) + self.on_complete = function () { + if (self.complete_callback) self.complete_callback(self.new_value, self.current_value, self.is_confirmed, self.is_ignored); }; var result = ko.computed({ read: target, //always return the original observables value - write: function(new_value) { + write: function (new_value) { self.is_confirmed = false; self.new_value = new_value; self.current_value = target(); self.on_before_changed(); self.is_ignored = self.ignore_callback && self.ignore_callback(new_value, self.current_value); - if(!is_ignored) - { + if (!is_ignored) { var auto_confirm = ( self.auto_confirm_callback && self.auto_confirm_callback(new_value, self.current_value) @@ -957,15 +1040,14 @@ $(function () { target(new_value); self.is_confirmed = true; self.on_complete(); - } - else { + } else { self.on_before_confirm(); var current_value = target(); Octolapse.showConfirmDialog( self.dialog_key, self.title, self.message, - function() { + function () { self.on_confirmed(); target(new_value); self.is_confirmed = true; @@ -978,8 +1060,7 @@ $(function () { ); } - } - else { + } else { target(new_value); } @@ -1044,17 +1125,16 @@ $(function () { var result = ko.dependentObservable({ read: function () { var val = target(); - val = Octolapse.parseFloat(val) + val = Octolapse.parseFloat(val); if (val == null) return val; - try{ + try { // safari doesn't seem to like toFixed with a precision > 20 - if(precision > 20) + if (precision > 20) precision = 20; return val.toFixed(precision); - } - catch (e){ - console.log("Error converting toFixed"); + } catch (e) { + console.error("Error converting toFixed"); } }, @@ -1065,40 +1145,64 @@ $(function () { return result; }; - Octolapse.pad = function pad(n, width, z) - { + var byte = 1024; + Octolapse.toFileSizeString = function (bytes, precision) { + precision = precision || 0; + + if (Math.abs(bytes) < byte) { + return bytes + ' B'; + } + var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + var u = -1; + do { + bytes /= byte; + ++u; + } while (Math.abs(bytes) >= byte && u < units.length - 1); + return bytes.toFixed(precision) + ' ' + units[u]; + }; + + Octolapse.pad = function pad(n, width, z) { z = z || '0'; - return (String(z).repeat(width) + String(n)).slice(String(n).length) - } + return (String(z).repeat(width) + String(n)).slice(String(n).length); + }; + + Octolapse.toLocalDateString = function (unix_timestamp) { + if (unix_timestamp) { + return (new Date(unix_timestamp * 1000)).toLocaleDateString(); + } + return "UNKNOWN"; + }; + + Octolapse.toLocalDateTimeString = function (unix_timestamp) { + if (unix_timestamp) { + var date = new Date(unix_timestamp * 1000); + return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + return "UNKNOWN"; + }; - /** - * @return {string} - */ Octolapse.ToTime = function (seconds) { if (seconds == null) return Octolapse.NullTimeText; var utcSeconds = seconds; var d = new Date(0); // The 0 there is the key, which sets the date to the epoch d.setUTCSeconds(utcSeconds); - return Octolapse.pad(d.getHours(),2,"0") + ":" - + Octolapse.pad(d.getMinutes(),2,"0") + ":" - + Octolapse.pad(d.getSeconds(),2,"0"); + return Octolapse.pad(d.getHours(), 2, "0") + ":" + + Octolapse.pad(d.getMinutes(), 2, "0") + ":" + + Octolapse.pad(d.getSeconds(), 2, "0"); }; - /** - * @return {string} - */ Octolapse.ToTimer = function (seconds) { if (seconds == null) return ""; if (seconds <= 0) return "0:00"; - seconds = Math.round(seconds) + seconds = Math.round(seconds); var hours = Math.floor(seconds / 3600).toString(); if (hours > 0) { - return ("" + hours).slice(-2) + " Hrs" + return ("" + hours).slice(-2) + " Hrs"; } seconds %= 3600; @@ -1116,7 +1220,9 @@ $(function () { for (var precision = 2; precision >= 1; precision--) { shortValue = parseFloat((suffixNum !== 0 ? (value / Math.pow(1000, suffixNum)) : value).toPrecision(precision)); var dotLessShortValue = (shortValue + '').replace(/[^a-zA-Z 0-9]+/g, ''); - if (dotLessShortValue.length <= 2) { break; } + if (dotLessShortValue.length <= 2) { + break; + } } if (shortValue % 1 !== 0) shortValue = shortValue.toFixed(1); @@ -1131,7 +1237,7 @@ $(function () { var result = ko.dependentObservable({ read: function () { val = target(); - return Octolapse.ToTime(val) + return Octolapse.ToTime(val); }, write: target }); @@ -1140,31 +1246,117 @@ $(function () { return result; }; + Octolapse.createObjectURL = window.webkitURL ? window.webkitURL.createObjectURL : window.URL && window.URL.createObjectURL ? window.URL.createObjectURL : null; + Octolapse.revokeObjectURL = window.webkitURL ? window.webkitURL.revokeObjectURL : window.URL && window.URL.revokeObjectURL ? window.URL.revokeObjectURL : null; + + Octolapse.download = function (url, event, options) { + var on_start = options.on_start; + var on_load = options.on_load; + var on_error = options.on_error; + var on_abort = options.on_abort; + var on_progress = options.on_progress; + var on_end = options.on_end; + + if (on_start) { + // If we can't download in the preferred way, make sure to report that to on_start + on_start(Octolapse.createObjectURL != null, event, url); + } + + if (!(Octolapse.createObjectURL && Octolapse.revokeObjectURL)) { + // Fallback Download + var a = document.createElement('a'); + a.href = url; + a.download = ""; + a.click(); + if (on_end) { + on_end(event, url); + } + return; + } + + var request = new XMLHttpRequest(); + request.responseType = 'blob'; + request.open('GET', url); + request.addEventListener('load', function (e) { + var contentDispo = this.getResponseHeader('Content-Disposition'); + var filename = ""; + if (e.target.status === 200) { + if (contentDispo) { + // https://stackoverflow.com/a/23054920/ + filename = contentDispo.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)[1].replace(/['"]/g, ''); + } + var file_href = Octolapse.createObjectURL(e.target.response); + var a = document.createElement('a'); + a.href = file_href; + a.download = filename; + a.click(); + Octolapse.revokeObjectURL(file_href); + if (on_load) { + on_load(e, filename); + } + } else { + if (on_error) { + var error_message = e.target.status.toString() + " (" + e.target.statusText + ")"; + var console_error_message = "Unable to download an octolapse file from '" + url + "': " + + error_message; + console.error(console_error_message); + on_error(error_message, null, null, null, null); + } + } + + if (on_end) { + on_end(event, url); + } + }); + request.addEventListener('error', function ( + message, source, lineno, colno, error + ) { + console.error("Unable to download a file from '" + url + "'. Error Details: " + ( + message ? message.toString() : "unknown" + )); + if (on_error) { + on_error(message, source, lineno, colno, error); + } + if (on_end) { + on_end(event, url); + } + }); + request.addEventListener('abort', function (e) { + if (on_abort) { + on_abort(e); + } + if (on_end) { + on_end(event, url); + } + }); + request.addEventListener('progress', function (e) { + if (on_progress) { + on_progress(e); + } + }); + request.send(); + }; + OctolapseViewModel = function (parameters) { var self = this; Octolapse.Globals = self; - + Octolapse.Help = new OctolapseHelp(); self.loginState = parameters[0]; Octolapse.PrinterStatus = parameters[1]; - self.OctoprintTimelapse = parameters[2] - // Global Values - self.show_printer_state_changes = ko.observable(false); - self.show_position_changes = ko.observable(false); - self.show_extruder_state_changes = ko.observable(false); - self.show_trigger_state_changes = ko.observable(false); - self.show_snapshot_plan_information = ko.observable(false); - self.preview_snapshot_plans = ko.observable(false); - self.automatic_updates_enabled = ko.observable(true); - self.automatic_update_interval_days = ko.observable(7); - self.auto_reload_latest_snapshot = ko.observable(false); - self.auto_reload_frames = ko.observable(5); + self.OctoprintTimelapse = parameters[2]; + // Main settings + self.main_settings = new Octolapse.MainSettingsViewModel(); + self.version_text = ko.pureComputed(function () { + if (self.main_settings.octolapse_version() && self.main_settings.octolapse_version !== "unknown") { + return "v" + self.main_settings.octolapse_version(); + } + return "unknown"; + }); self.is_admin = ko.observable(false); - self.enabled = ko.observable(false); - self.navbar_enabled = ko.observable(false); - self.show_navbar_when_not_printing = ko.observable(false); - self.cancel_print_on_startup_error = ko.observable(true); + self.toggleAdmin = function () { + self.is_admin(!self.is_admin()); + }; self.preprocessing_job_guid = ""; - self.version = ko.observable("unknown"); // Create a guid to uniquely identify this client. self.client_id = Octolapse.guid(); // Have we loaded the state yet? @@ -1174,6 +1366,7 @@ $(function () { self.onBeforeBinding = function () { self.is_admin(self.loginState.isAdmin()); + }; self.startup_complete = false; @@ -1182,23 +1375,32 @@ $(function () { //console.log("Startup Complete") self.getInitialState(); self.startup_complete = true; - Octolapse.Help = new OctolapseHelp(); + + }; self.onDataUpdaterReconnect = function () { //console.log("Reconnected Client") self.getInitialState(); - }; - self.getInitialState = function() { + self.getInitialState = function () { //console.log("Getting initial state"); - if (!self.startup_complete && self.is_admin()) { - //console.log("octolapse.js - Loading settings for current user after startup."); - Octolapse.Settings.loadSettings(); - } else - { - self.loadState(); + if (self.is_admin()) { + //console.log("octolapse.js - Loading settings for admin current user after startup."); + Octolapse.Settings.loadSettings(function () { + // Settings are loaded, show the UI + self.has_loaded_state(true); + // Check for updates + Octolapse.Settings.checkForProfileUpdates(true); + Octolapse.Status.load_files(); + }); + } else { + self.loadState(function () { + //console.log("octolapse.js - Loading state for non-admin user after startup."); + // Settings are loaded, show the UI + self.has_loaded_state(true); + }); } // reset snapshot error state @@ -1207,7 +1409,7 @@ $(function () { }; - self.loadState = function () { + self.loadState = function (success_callback) { //console.log("octolapse.js - Loading State"); $.ajax({ url: "./plugin/octolapse/loadState", @@ -1217,8 +1419,12 @@ $(function () { contentType: "application/json", dataType: "json", success: function (result) { - //console.log("The state has been loaded. Waiting for message"); + //console.log("The state has been loaded."); self.initial_state_loaded = true; + self.updateState(result); + if (success_callback) { + success_callback(); + } }, error: function (XMLHttpRequest, textStatus, errorThrown) { @@ -1231,12 +1437,12 @@ $(function () { self.onUserLoggedIn = function (user) { self.is_admin(self.loginState.isAdmin()); - if(self.is_admin() && self.startup_complete) { + if (self.is_admin() && self.startup_complete) { //console.log("octolapse.js - User Logged In after startup - Loading settings. User: " + user.name); Octolapse.Settings.loadSettings(); + Octolapse.Status.load_files(); } - //else - //console.log("octolapse.js - User Logged In before startup - waiting to load settings. User: " + user.name); + }; self.onUserLoggedOut = function () { @@ -1245,9 +1451,9 @@ $(function () { Octolapse.Settings.clearSettings(); }; - self.onEventPrinterStateChanged = function(payload){ + self.onEventPrinterStateChanged = function (payload) { //console.log("Octolapse.js - Received print state change."); - if (payload.state_id == "CANCELLING") { + if (payload.state_id === "CANCELLING") { //console.log("Octolapse.js - Printer is cancelling."); // We need to close any progress diagogs if (self.pre_processing_progress != null) { @@ -1260,16 +1466,16 @@ $(function () { //console.log("octolapse.js - updateState"); if (data.state != null) { - Octolapse.Status.updateState(data.state) + Octolapse.Status.updateState(data.state); } if (data.main_settings != null) { //console.log('octolapse.js - Main settings changed'); // detect changes to auto_reload_latest_snapshot - var cur_auto_reload_latest_snapshot = Octolapse.Globals.auto_reload_latest_snapshot(); + var cur_auto_reload_latest_snapshot = Octolapse.Globals.main_settings.auto_reload_latest_snapshot(); - Octolapse.Globals.update(data.main_settings); - if (cur_auto_reload_latest_snapshot !== Octolapse.Globals.auto_reload_latest_snapshot()) { - //console.log('octolapse.js - Octolapse.Globals.auto_reload_latest_snapshot changed, erasing previous snapshot images'); + Octolapse.Globals.main_settings.update(data.main_settings); + if (cur_auto_reload_latest_snapshot !== Octolapse.Globals.main_settings.auto_reload_latest_snapshot()) { + //console.log('octolapse.js - Octolapse.Globals.main_settings.auto_reload_latest_snapshot changed, erasing previous snapshot images'); Octolapse.Status.erasePreviousSnapshotImages('octolapse_snapshot_image_container'); Octolapse.Status.erasePreviousSnapshotImages('octolapse_snapshot_thumbnail_container'); } @@ -1279,100 +1485,22 @@ $(function () { //console.log("octolapse.js - Updating Status"); Octolapse.Status.update(data.status); } + if (data.snapshot_plan_preview) { + var plans = data.snapshot_plan_preview; + self.preprocessing_job_guid = plans.preprocessing_job_guid; + Octolapse.Status.previewSnapshotPlans(plans.snapshot_plans); + } /* if (!self.HasLoadedState) { Octolapse.Status.updateLatestSnapshotImage(true); Octolapse.Status.updateLatestSnapshotThumbnail(true); } */ - self.has_loaded_state(true); - }; - - self.update = function (settings) { - //console.log("octolapse.js - Globals - Updating main_settings globals") - // enabled - if (ko.isObservable(settings.is_octolapse_enabled)) - self.enabled(settings.is_octolapse_enabled()); - else - self.enabled(settings.is_octolapse_enabled); - - if (ko.isObservable(settings.version)) - self.version(settings.version()); - else - self.version(settings.version); - - // self.auto_reload_latest_snapshot - if (ko.isObservable(settings.auto_reload_latest_snapshot)) - self.auto_reload_latest_snapshot(settings.auto_reload_latest_snapshot()); - else - self.auto_reload_latest_snapshot(settings.auto_reload_latest_snapshot); - //auto_reload_frames - if (ko.isObservable(settings.auto_reload_frames)) - self.auto_reload_frames(settings.auto_reload_frames()); - else - self.auto_reload_frames(settings.auto_reload_frames); - // navbar_enabled - if (ko.isObservable(settings.show_navbar_icon)) - self.navbar_enabled(settings.show_navbar_icon()); - else - self.navbar_enabled(settings.show_navbar_icon); - - if (ko.isObservable(settings.show_navbar_when_not_printing)) - self.show_navbar_when_not_printing(settings.show_navbar_when_not_printing()); - else - self.show_navbar_when_not_printing(settings.show_navbar_when_not_printing); - - - if (ko.isObservable(settings.show_printer_state_changes)) - self.show_printer_state_changes(settings.show_printer_state_changes()); - else - self.show_printer_state_changes(settings.show_printer_state_changes); - - if (ko.isObservable(settings.show_position_changes)) - self.show_position_changes(settings.show_position_changes()); - else - self.show_position_changes(settings.show_position_changes); - - if (ko.isObservable(settings.show_extruder_state_changes)) - self.show_extruder_state_changes(settings.show_extruder_state_changes()); - else - self.show_extruder_state_changes(settings.show_extruder_state_changes); - - if (ko.isObservable(settings.show_snapshot_plan_information)) - self.show_snapshot_plan_information(settings.show_snapshot_plan_information()); - else - self.show_snapshot_plan_information(settings.show_snapshot_plan_information); - - if (ko.isObservable(settings.preview_snapshot_plans)) - self.preview_snapshot_plans(settings.preview_snapshot_plans()); - else - self.preview_snapshot_plans(settings.preview_snapshot_plans); - - if (ko.isObservable(settings.automatic_updates_enabled)) - self.automatic_updates_enabled(settings.automatic_updates_enabled()); - else - self.automatic_updates_enabled(settings.automatic_updates_enabled); - - if (ko.isObservable(settings.automatic_update_interval_days )) - self.automatic_update_interval_days(settings.automatic_update_interval_days()); - else - self.automatic_update_interval_days(settings.automatic_update_interval_days); - - if (ko.isObservable(settings.show_trigger_state_changes)) - self.show_trigger_state_changes(settings.show_trigger_state_changes()); - else - self.show_trigger_state_changes(settings.show_trigger_state_changes); - - if (ko.isObservable(settings.cancel_print_on_startup_error)) - self.cancel_print_on_startup_error(settings.cancel_print_on_startup_error()); - else - self.cancel_print_on_startup_error(settings.cancel_print_on_startup_error); - }; - self.acceptSnapshotPlanPreview = function(){ + self.acceptSnapshotPlanPreview = function () { //console.log("Accepting snapshot plan preview."); - var data = {"preprocessing_job_guid":self.preprocessing_job_guid}; + var data = { "preprocessing_job_guid": self.preprocessing_job_guid }; $.ajax({ url: "./plugin/octolapse/acceptSnapshotPlanPreview", type: "POST", @@ -1382,12 +1510,9 @@ $(function () { data: JSON.stringify(data), dataType: "json", success: function (result) { - if(result.success) - { + if (result.success) { Octolapse.Status.SnapshotPlanPreview.closeSnapshotPlanPreviewDialog(); - } - else - { + } else { var options = { title: 'Error Accepting Plan', text: result.error, @@ -1395,7 +1520,7 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -1410,7 +1535,7 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -1419,9 +1544,9 @@ $(function () { }); }; - self.cancelPreprocessing = function(){ + self.cancelPreprocessing = function () { //console.log("Cancelling preprocessing") - var data = {"cancel":true, "preprocessing_job_guid":self.preprocessing_job_guid}; + var data = { "cancel": true, "preprocessing_job_guid": self.preprocessing_job_guid }; $.ajax({ url: "./plugin/octolapse/cancelPreprocessing", type: "POST", @@ -1431,9 +1556,9 @@ $(function () { data: JSON.stringify(data), dataType: "json", success: function (result) { - if (self.pre_processing_progress != null) { - self.pre_processing_progress.close(); - } + if (self.pre_processing_progress != null) { + self.pre_processing_progress.close(); + } }, error: function (XMLHttpRequest, textStatus, errorThrown) { var message = "Could not cancel preprocessing. Status: " + textStatus + ". Error: " + errorThrown; @@ -1444,7 +1569,7 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -1463,8 +1588,8 @@ $(function () { switch (data.type) { case "snapshot-plan-preview": //console.log("Previewing snapshot plans."); - self.preprocessing_job_guid = data.preprocessing_job_guid; - Octolapse.Status.previewSnapshotPlans(data.snapshot_plans); + self.updateState(data); + break; case "snapshot-plan-preview-complete": // create the cancel popup @@ -1475,29 +1600,28 @@ $(function () { // create the cancel popup //console.log("Creating a progress bar."); self.preprocessing_job_guid = data.preprocessing_job_guid; - self.pre_processing_progress = Octolapse.progressBar(self.cancelPreprocessing, "Initializing..."); + self.pre_processing_progress = Octolapse.progressBar(self.cancelPreprocessing, "Initializing..."); break; case "gcode-preprocessing-update": //console.log("Octolapse received pre-processing update processing message."); // TODO: CHANGE THIS TO A PROGRESS INDICATOR - var percent_finished = data.percent_progress.toFixed(1); + var percent_finished = data.percent_progress; var seconds_elapsed = data.seconds_elapsed; var seconds_to_complete = data.seconds_to_complete; var gcodes_processed = data.gcodes_processed; var lines_processed = data.lines_processed; - if (self.pre_processing_progress == null) - { + if (self.pre_processing_progress == null) { //console.log("The pre-processing progress bar is missing, creating the progress bar."); //console.log("Creating progress bar"); - self.pre_processing_progress = Octolapse.progressBar(self.cancelPreprocessing); + self.pre_processing_progress = Octolapse.progressBar(self.cancelPreprocessing); } if (self.pre_processing_progress != null) { var progress_text = "Remaining:" + Octolapse.ToTimer(seconds_to_complete) - + " Elapsed:" + Octolapse.ToTimer(seconds_elapsed) - + " Line:" + lines_processed.toString(); + + " Elapsed:" + Octolapse.ToTimer(seconds_elapsed) + + " Line:" + lines_processed.toString(); //console.log("Receiving Progress - Percent Complete:" + percent_finished + " " + progress_text); self.pre_processing_progress = self.pre_processing_progress.update( percent_finished, progress_text @@ -1509,232 +1633,236 @@ $(function () { // clear the job guid //console.log("Gcode preprocessing failed."); self.preprocessing_job_guid = null; - // report the issue + self.updateState(data); + var options = { - title: 'Preprocessing Error', - text: data.errors, + title: 'Gcode Processing Failed', type: 'error', hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; - Octolapse.displayPopupForKey( - options,['gcode-preprocessing-failed'],['gcode-preprocessing-failed'] + Octolapse.Help.showPopupForErrors( + options, + "gcode-preprocessing-failed", + ["gcode-preprocessing-failed"], + data.errors ); break; + case "updated-profiles-available": - if (self.is_admin()) - { - var message; - if (Octolapse.Status.is_timelapse_active()) - { - message = "There are profiles available to update. " + - "It is safe to update these profiles while a timelapse is running. " + - "Would you like to update these now?"; - } - else - { - message = "There are profiles available to update. " + - " Would you like to update these now?"; - } - Octolapse.showConfirmDialog( - "update-all-automatic-profiles", - "Update Octolapse Profiles", - message, - function(){ - Octolapse.Settings.updateProfilesFromServer(); - }, - function() { - Octolapse.Settings.suppressServerUpdates(); - } - ); + if (self.is_admin()) { + Octolapse.Settings.showProfileUpdateConfirmation(data.available_profile_count); } break; case "external_profiles_list_changed": - Octolapse.Printers.profileOptions.server_profiles = data.server_profiles; + Octolapse.Settings.UpdateAvailableServerProfiles(data.server_profiles); + var msg_options = { + title: 'New Server Profiles Available', + text: "New profiles were found in the octolapse profile repository. These can be imported when adding/editing a profile.", + type: 'notice', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(msg_options, "new-profiles-available", "new-profiles-available"); break; - case "settings-changed": - { - if (Octolapse.Settings.is_loaded()) - self.updateState(data); - // Was this from us? - if (self.client_id !== data.client_id && self.is_admin()) - { - Octolapse.showConfirmDialog( - "reload-settings", - "Reload Settings", - "A settings change was detected from another client. Reload settings?", - function(){ - Octolapse.Settings.loadSettings(); - }); + case "settings-changed": { + // See if the settings changed request came from the current client. If so, ignore it + if (self.client_id !== data.client_id) { + if (self.is_admin()) { + Octolapse.Settings.loadSettings(); + } else { + Octolapse.Globals.loadState(); } + } else { + Octolapse.Globals.loadState(); } + } break; case "slicer_settings_detected": - if(data.saved) { + if (data.saved) { //console.log("Slicer settings detected and saved."); - if(data.printer_profile_json != null) - { + if (data.printer_profile_json != null) { var new_profile = JSON.parse(data.printer_profile_json); var current_profile = Octolapse.Printers.getProfileByGuid(new_profile.guid); - if (current_profile != null) - { + if (current_profile != null) { Octolapse.Printers.profiles.replace(current_profile, new Octolapse.PrinterProfileViewModel(new_profile)); - } - else - { + } else { console.error("Octolapse.js - Unable to find the updated printer profile from the current profiles!"); } //Octolapse.Printers.replace(currentProfile, newProfile); } } - //else - // console.log("Slicer settings detected but not saved."); - // Disable the error notification for profiles that haven't been configured - case "state-loaded": - { - //console.log('octolapse.js - state-loaded'); - self.updateState(data); - } break; - case "state-changed": - { - //console.log('octolapse.js - state-changed'); - //console.log(data); + case "state-changed": { + //console.log('octolapse.js - state-changed'); + if (data.state) { self.updateState(data); + } else if (data.client_id !== self.client_id) { + // The update contains no state. See if it originated from a client + self.loadState(); } + } break; - case "popup": - { - //console.log('octolapse.js - popup'); - var options = { - title: 'Octolapse Notice', - text: data.msg, - type: 'notice', - hide: true, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopup(options); - } + case "popup": { + //console.log('octolapse.js - popup'); + var popup_options = { + title: 'Octolapse Notice', + text: data.msg, + type: 'notice', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopup(popup_options); + } break; - case "popup-error": - { - //console.log('octolapse.js - popup-error'); - self.updateState(data); - var options = { - title: 'Error', - text: data.msg, - type: 'error', - hide: false, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopup(options); - break; - } - case "print-start-error": - { - //console.log('octolapse.js - print-start-error'); - self.updateState(data); - var options = { - title: 'Octolapse Startup Failed', - text: data.msg, - type: 'error', - hide: false, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopupForKey(options,"print-start-error",["print-start-error"]) - break; - } - case "timelapse-start": - { - //console.log('octolapse.js - timelapse-start'); - // Erase any previous images - Octolapse.HasTakenFirstSnapshot = false; - // let the status tab know that a timelapse is starting - Octolapse.Status.onTimelapseStart(); - self.updateState(data); - Octolapse.Status.snapshot_error(false); - } + case "popup-error": { + //console.log('octolapse.js - popup-error'); + self.updateState(data); + var popupErrorOptions = { + title: 'Error', + text: data.msg, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopup(popupErrorOptions); + break; + } + case "print-start-error": { + //console.log('octolapse.js - print-start-error'); + self.updateState(data); + self.preprocessing_job_guid = null; + var printStartPopupoptions = { + title: 'Octolapse Startup Failed', + text: data.msg, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.Help.showPopupForErrors(printStartPopupoptions, "print-start-error", ["print-start-error"], data["errors"]); + break; + } + case "print-start-warning": { + //console.log('octolapse.js - print-start-error'); + self.updateState(data); + self.preprocessing_job_guid = null; + var printStartPopupoptions = { + title: 'Octolapse Cannot Start', + text: data.msg, + type: 'warning', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.Help.showPopupForErrors(printStartPopupoptions, "print-start-error", ["print-start-error"], data["errors"]); + break; + } + case "timelapse-start": { + //console.log('octolapse.js - timelapse-start'); + // Erase any previous images + Octolapse.HasTakenFirstSnapshot = false; + // let the status tab know that a timelapse is starting + Octolapse.Status.onTimelapseStart(); + self.updateState(data); + Octolapse.Status.snapshot_error(false); + } break; - case "timelapse-complete": - { - //console.log('octolapse.js - timelapse-complete'); - Octolapse.Status.snapshot_error(false); - self.updateState(data) + case "timelapse-complete": { + //console.log('octolapse.js - timelapse-complete'); + Octolapse.Status.snapshot_error(false); + self.updateState(data); - } + } break; case "camera-settings-error": // If only the camera image acquisition failed, use the camera error message - var options = { + var cameraSettingsErrorPopup = { title: 'Octolapse - Camera Settings Error', text: data.msg, type: 'error', hide: false, addclass: "octolapse" }; - Octolapse.displayPopupForKey(options, "snapshot_error",["snapshot_error"]); + Octolapse.displayPopupForKey(cameraSettingsErrorPopup, "snapshot_error", ["snapshot_error"]); break; - case "snapshot-start": - { - //console.log('octolapse.js - snapshot-start'); - self.updateState(data); - Octolapse.Status.snapshot_error(false); - } + case "snapshot-start": { + //console.log('octolapse.js - snapshot-start'); + self.updateState(data); + Octolapse.Status.snapshot_error(false); + } break; - case "snapshot-complete": - { - //console.log('octolapse.js - snapshot-complete'); - //console.log(data); - self.updateState(data); + case "snapshot-complete": { + //console.log('octolapse.js - snapshot-complete'); + //console.log(data); + self.updateState(data); - var hasError =!(data.success && data.snapshot_success); - Octolapse.Status.snapshot_error(hasError); - if(hasError) - { - // If only the camera image acquisition failed, use the camera error message - if (!data.success) - { - var options = { - title: "Stabilization Error", - text: data.error, - type: 'error', - hide: false, - addclass: "octolapse" - }; - Octolapse.displayPopupForKey(options, "stabilization_error", ["stabilization_error"]) - } - if (!data.snapshot_success) - { - var options = { - title: "Camera Error", - text: data.snapshot_error, - type: 'error', - hide: false, - addclass: "octolapse" - }; - Octolapse.displayPopupForKey(options, "camera_error",["camera_error"]) - } + var hasError = !(data.success && data.snapshot_success); + Octolapse.Status.snapshot_error(hasError); + if (hasError) { + // If only the camera image acquisition failed, use the camera error message + if (!data.success) { + var snapshotCompleteSuccessPopupOptions = { + title: "Stabilization Error", + text: data.error, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(snapshotCompleteSuccessPopupOptions, "stabilization_error", ["stabilization_error"]); + } + if (!data.snapshot_success) { + var snapshotCompleteErrorPopupOptions = { + title: "Camera Error", + text: data.snapshot_error, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(snapshotCompleteErrorPopupOptions, "camera_error", ["camera_error"]); + } - } + } + } + break; + case "snapshot-post-proocessing-failed": { + self.updateState(data); + Octolapse.Status.snapshot_error(true); + // If only the camera image acquisition failed, use the camera error message + + if (!data.snapshot_success) { + var postProcessingFailedPopupOptions = { + title: "Camera Processing Error", + text: data.message, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(postProcessingFailedPopupOptions, "camera_error", ["camera_error"]); } + } break; case "new-thumbnail-available": - if (data.guid == $("#octolapse_current_snapshot_camera").val()) { + if (data.guid === $("#octolapse_current_snapshot_camera").val()) { //console.log("New thumbnails available"); if (!Octolapse.HasTakenFirstSnapshot) { Octolapse.HasTakenFirstSnapshot = true; @@ -1748,206 +1876,229 @@ $(function () { } } break; - case "prerender-start": + case "directories-changed": { + //console.log('octolapse.js - directories changed.'); + var directories = data.directories; + if (directories.temporary_directory_changed) { + // Load unfinished + Octolapse.Status.dialog_rendering_unfinished.load(); + } + if (directories.snapshot_archive_directory_changed) { + // Load snapshot archive + Octolapse.Status.timelapse_files_dialog.archive_browser.load(); + } + if (directories.timelapse_directory_changed) { + // load timelapses + Octolapse.Status.timelapse_files_dialog.timelapse_browser.load(); + } + } + break; + case "unfinished-renderings-loaded": { + //console.log('octolapse.js - unfinished-renderings-loaded'); self.updateState(data); + } break; - case "render-start": - { - //console.log('octolapse.js - render-start'); - self.updateState(data); - Octolapse.Status.snapshot_error(false); - - var options = { - title: 'Octolapse Rendering Started', - text: data.msg, - type: 'notice', - hide: true, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopupForKey(options,"render_message", ["render_message"]); - } + case "unfinished-renderings-changed": { + //console.log('octolapse.js - unfinished-renderings-changed'); + self.updateState(data); + } break; - case "render-failed":{ - //console.log('octolapse.js - render-failed'); - self.updateState(data); - var options = { - title: 'Octolapse Rendering Failed', - text: data.msg, - type: 'error', - hide: false, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopupForKey(options,"render_failed",["render_message"]); - break; + case "render-start": { + //console.log('octolapse.js - render-start'); + self.updateState(data); + /* + Octolapse.Status.snapshot_error(false); + var options = { + title: 'Octolapse Rendering Started', + text: data.msg, + type: 'notice', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(options,"render_message", ["render_message"]);*/ } - case "post-render-failed":{ - //console.log('octolapse.js - post-render-failed'); - self.updateState(data); - var options = { - title: 'Octolapse Post-Rendering Failed', - text: data.msg, - type: 'error', - hide: false, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopupForKey(options,"post_render_error_message",["render_message"]); - break; + break; + case "render-failed": { + //console.log('octolapse.js - render-failed'); + self.updateState(data); + var renderFailedPopupOptions = { + title: 'Octolapse Rendering Failed', + text: data.msg, + type: 'error', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopup(renderFailedPopupOptions); + break; } + case "post-render-failed": { + //console.log('octolapse.js - post-render-failed'); + self.updateState(data); + var postRenderFailedPopupOptions = { + title: 'Octolapse Post-Rendering Failed', + text: data.msg, + type: 'error', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopup(postRenderFailedPopupOptions); + break; + } + case "render-progress": + //console.log('octolapse.js - render-progress'); + self.updateState(data); + break; case "render-complete": self.updateState(data); - //console.log('octolapse.js - render-complete'); self.OctoprintTimelapse.requestData(); - - // Make sure we aren't synchronized, else there's no reason to display a popup + //console.log('octolapse.js - render-complete'); + /* var options = { title: 'Octolapse Rendering Complete', text: data.msg, type: 'success', - hide: false, + hide: true, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; - Octolapse.displayPopupForKey(options,"render_complete",["render_message"]); + Octolapse.displayPopupForKey(options,"render_complete",["render_complete", "render_message"]);*/ break; - case "render-end": - { - //console.log('octolapse.js - render-end'); - self.updateState(data); - } + case "render-end": { + //console.log('octolapse.js - render-end'); + self.updateState(data); + } break; - case "synchronize-failed": - { - //console.log('octolapse.js - synchronize-failed'); - var options = { - title: 'Octolapse Synchronization Failed', - text: data.msg, - type: 'error', - hide: false, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopup(options); - } + case "timelapse-stopping": { + //console.log('octolapse.js - timelapse-stoping'); + Octolapse.Status.is_timelapse_active(false); + var timelapseStoppingPopupOptions = { + title: 'Octolapse Timelapse Stopping', + text: data.msg, + type: 'notice', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopup(timelapseStoppingPopupOptions); + } break; - case "timelapse-stopping": - { - //console.log('octolapse.js - timelapse-stoping'); - Octolapse.Status.is_timelapse_active(false); - var options = { - title: 'Octolapse Timelapse Stopping', - text: data.msg, - type: 'notice', - hide: true, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopup(options); - } + case "timelapse-stopped": { + //console.log('octolapse.js - timelapse-stopped'); + Octolapse.Status.onTimelapseStop(); + Octolapse.Status.snapshot_error(false); + var timelapseStoppedPopupOptions = { + title: 'Octolapse Timelapse Stopped', + text: data.msg, + type: 'notice', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopup(timelapseStoppedPopupOptions); + } break; - case "timelapse-stopped": - { - //console.log('octolapse.js - timelapse-stopped'); - Octolapse.Status.onTimelapseStop(); - Octolapse.Status.snapshot_error(false); - var options = { - title: 'Octolapse Timelapse Stopped', - text: data.msg, - type: 'notice', - hide: true, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopup(options); - } + case "disabled-running": { + var disabledButRunningPopupOptions = { + title: 'Octolapse Disabled for Next Print', + text: data.msg, + type: 'notice', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(disabledButRunningPopupOptions, "settings-change-not-applied", ["settings-change-not-applied"]); + } break; - case "disabled-running": - { - var options = { - title: 'Octolapse Disabled for Next Print', - text: data.msg, - type: 'notice', - hide: true, - addclass: "octolapse", - desktop: { - desktop: true - } - }; - Octolapse.displayPopup(options); - } - break; - case "timelapse-stopped-error": - { - //console.log('octolapse.js - timelapse-stopped-error'); - Octolapse.Status.onTimelapseStop(); - var options = { - title: 'Octolapse Timelapse Stopped', - text: data.msg, - type: 'error', - hide: false, - addclass: "octolapse" - }; - Octolapse.displayPopup(options); - } + case "test-mode-changed-running": { + var testModeChangedWhileRunningPopupOptions = { + title: 'Test Mode Changed, Octolapse Running', + text: data.msg, + type: 'notice', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(testModeChangedWhileRunningPopupOptions, "settings-change-not-applied", ["settings-change-not-applied"]); + } break; - case "out-of-bounds": - { - //console.log("An out-of-bounds snapshot position was detected.") - var options = { - title: 'Octolapse - Out Of Bounds', - text: data.msg , - type: 'error', - hide: false, - addclass: "octolapse" - }; - Octolapse.displayPopupForKey(options,"out-of-bounds", ["out-of-bounds"]); - } + case "timelapse-stopped-error": { + //console.log('octolapse.js - timelapse-stopped-error'); + Octolapse.Status.onTimelapseStop(); + var timelapseStoppedErrorPopupOptions = { + title: 'Octolapse Timelapse Stopped', + text: data.msg, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopup(timelapseStoppedErrorPopupOptions); + } break; - case "position-error": - { - //console.log("An out-of-bounds snapshot position was detected.") - var options = { - title: 'Octolapse - Position Error', - text: data.msg, - type: 'error', - hide: false, - addclass: "octolapse" - }; - Octolapse.displayPopupForKey(options, "position-error", ["position-error"]); + case "out-of-bounds": { + //console.log("An out-of-bounds snapshot position was detected.") + var outOfBoundsErrorPopupOptions = { + title: 'Octolapse - Out Of Bounds', + text: data.msg, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(outOfBoundsErrorPopupOptions, "out-of-bounds", ["out-of-bounds"]); + } + break; + case "position-error": { + //console.log("An out-of-bounds snapshot position was detected.") + // This should never be called. May need to remove! + var positionErrorPopupOptions = { + title: 'Octolapse - Position Error', + text: data.msg, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(positionErrorPopupOptions, "position-error", ["position-error"]); + } + break; + case "file-changed": { + if (data.client_id !== self.client_id) { + Octolapse.Status.files_changed(data.file, data.action); } + } break; case "warning": //console.log("A warning was sent to the plugin.") - var options = { + var warningPopupOptions = { title: 'Octolapse - Warning', text: data.msg, type: 'notice', hide: true, addclass: "octolapse" }; - Octolapse.displayPopup(options, "warning"); + Octolapse.displayPopup(warningPopupOptions, "warning"); break; - default: - { - //console.log('Octolapse.js - passing on message from server. DataType:' + data.type); - } + default: { + //console.log('Octolapse.js - passing on message from server. DataType:' + data.type); + } } }; diff --git a/octoprint_octolapse/static/js/octolapse.profiles.camera.js b/octoprint_octolapse/static/js/octolapse.profiles.camera.js index e7f55ba6..68182176 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.camera.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.camera.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -32,30 +32,28 @@ $(function() { self.enabled = ko.observable(values.enabled); self.description = ko.observable(values.description); self.camera_type = ko.observable(values.camera_type); + self.on_before_snapshot_gcode = ko.observable(values.on_before_snapshot_gcode); self.gcode_camera_script = ko.observable(values.gcode_camera_script); + self.on_after_snapshot_gcode = ko.observable(values.on_after_snapshot_gcode); self.on_print_start_script = ko.observable(values.on_print_start_script); self.on_before_snapshot_script = ko.observable(values.on_before_snapshot_script); self.external_camera_snapshot_script = ko.observable(values.external_camera_snapshot_script); self.on_after_snapshot_script = ko.observable(values.on_after_snapshot_script); self.on_before_render_script = ko.observable(values.on_before_render_script); self.on_after_render_script = ko.observable(values.on_after_render_script); + self.on_print_end_script = ko.observable(values.on_print_end_script); self.delay = ko.observable(values.delay); self.timeout_ms = ko.observable(values.timeout_ms); self.enable_custom_image_preferences = ko.observable(values.enable_custom_image_preferences); self.apply_settings_before_print = ko.observable(values.apply_settings_before_print); self.apply_settings_at_startup = ko.observable(values.apply_settings_at_startup); + self.apply_settings_when_disabled = ko.observable(values.apply_settings_when_disabled); self.snapshot_transpose = ko.observable(values.snapshot_transpose); - self.camera_stream_closed = false; - self.camera_stream_visible = ko.pureComputed(function(){ - var visible = ( - self.camera_type() === 'webcam' && - self.enable_custom_image_preferences() && - !self.camera_stream_closed - ); - self.webcam_settings.setStreamVisibility(visible); - return visible; - }); - self.webcam_settings = new Octolapse.WebcamSettingsViewModel(null); + self.webcam_settings = new Octolapse.WebcamSettingsViewModel(); + self.webcam_settings_popup = new Octolapse.WebcamSettingsPopupViewModel("octolapse_camera_image_preferences_popup"); + self.webcam_settings_popup.webcam_settings.updateWebcamSettings(values.webcam_settings); + self.is_dialog_open = ko.observable(false); + self.is_testing_custom_image_preferences = ko.observable(false); self.applySettingsToCamera = function (settings_type) { @@ -97,7 +95,7 @@ $(function() { }); }; - self.toggleCamera = function(){ + self.toggleCamera = function(callback){ // If no guid is supplied, this is a new profile. We will need to know that later when we push/update our observable array //console.log("Running camera request."); var data = { 'guid': self.guid(), "client_id": Octolapse.Globals.client_id }; @@ -110,6 +108,10 @@ $(function() { success: function (results) { if (results.success) { self.enabled(results.enabled); + if (callback) + { + callback(results.enabled); + } } else { var options = { @@ -119,13 +121,11 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); - } - }, error: function (XMLHttpRequest, textStatus, errorThrown) { var message = "Unable to toggle the camera:( Status: " + textStatus + ". Error: " + errorThrown; @@ -136,7 +136,7 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -144,6 +144,7 @@ $(function() { }); }; + self.testCamera = function () { // If no guid is supplied, this is a new profile. We will need to know that later when we push/update our observable array //console.log("Running camera request."); @@ -191,25 +192,104 @@ $(function() { }); }; - self.toggleCustomImagePreferences = function () { - var id = 'camera_profile_apply_settings_before_print'; + self.testCameraScript = function(script_type) + { + // If no guid is supplied, this is a new profile. We will need to know that later when we push/update our observable array + //console.log("Running camera request."); + var message = "Testing the " + script_type + " script with " + (self.timeout_ms() / 1000.0).toFixed(2) + " second timeout. Please do not attempt" + + " to run any further tests until this script has finished. If your script times out, try increasing your 'Snapshot Timeout'."; + var testing_popup = { + title: "Testing Camera Script", + text: message, + type: "info", + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(testing_popup, "camera_script_test", ["camera_script_test"]); + + var data = { 'profile': self.toJS(self), 'script_type': script_type }; + $.ajax({ + url: "./plugin/octolapse/testCameraScript", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (results) { + if (results.success){ + var title = "Camera Script Success"; + var message_type='success'; + var message = "The script appears to work! No errors or error codes were returned."; + var hide = true; + if (script_type == 'snapshot') { + if (results.snapshot_created) + { + message = "The script appears to work, and a snapshot was found! No errors or error codes were returned."; + } + else + { + title = "Partial Camera Script Success"; + message = "The script appears to work, but no snapshot was found in the target folder. This is OK if you are leaving images on your DSLR's internal memory. Otherwise this could be a problem."; + message_type = "warning"; + hide = false; + } + } + var success_options = { + title: title, + text: message, + type: message_type, + hide: hide, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(success_options, "camera_script_test", ["camera_script_test"]); + } + else { + var fail_options = { + title: 'Camera Script Failed', + text: 'Errors were detected - ' + results.error, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(fail_options, "camera_script_test",["camera_script_test"]); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + var options = { + title: 'Camera Script Test Failed', + text: "Unable to perform the test, or an unexpected error occurred. Please check the log file for details. Status: " + textStatus + ". Error: " + errorThrown, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "camera_script_test",["camera_script_test"]); + } + }); + }; + + self.toggleCustomImagePreferences = function () { if (self.enable_custom_image_preferences()) { self.enable_custom_image_preferences(false); - return; } - - if (self.camera_stream_visible()) { - self.enable_custom_image_preferences(true); - $('#'+ id).prop("checked",true); - return; + else { + self.tryEnableCustomImagePreferences(); } - self.testCustomImagePreferences(self.enable_custom_image_preferences,id); + }; + self.showWebcamSettings = function() { + var profile = self.toJS(); + self.webcam_settings_popup.showWebcamSettingsForProfile(profile, function(webcam_settings) { + //console.log("Applying new settings to camera profile."); + if (webcam_settings) + { + profile.webcam_settings = webcam_settings; + self.webcam_settings.updateWebcamSettings(profile); + } + }); }; - self.testCustomImagePreferences = function(bool_observable, id){ + self.tryEnableCustomImagePreferences = function(){ self.is_testing_custom_image_preferences(true); // If no guid is supplied, this is a new profile. We will need to know that later when we push/update our observable array //console.log("Running camera request."); @@ -222,11 +302,10 @@ $(function() { dataType: "json", success: function (results) { if (results.success){ - bool_observable(true); - $('#'+ id).prop("checked",true); - self.updateImagePreferencesFromServer(true); + self.enable_custom_image_preferences(true); } else { + self.enable_custom_image_preferences(false); var options = { title: 'Unable To Enable Custom Preferences', text: results.error, @@ -254,38 +333,44 @@ $(function() { }); }; + self.on_opened = function(dialog) { + //console.log("Opening camera profile"); + self.is_dialog_open(true); + }; + self.on_closed = function(){ //console.log("Closing camera profile"); - self.webcam_settings.setStreamVisibility(false); + self.is_dialog_open(false); self.automatic_configuration.on_closed(); }; - self.on_cancelled = function(){ - //console.log("Cancelling camera profile"); - if(self.camera_type() == "webcam" && self.enable_custom_image_preferences()) { - self.webcam_settings.cancelWebcamChanges(); - } - }; - self.updateFromServer = function(values) { - self.guid(values.guid); self.name(values.name); self.enabled(values.enabled); self.description(values.description); self.camera_type(values.camera_type); - //self.gcode_camera_script = ko.observable(values.gcode_camera_script); + self.on_before_snapshot_gcode = ko.observable(values.on_before_snapshot_gcode); + self.gcode_camera_script = ko.observable(values.gcode_camera_script); + self.on_after_snapshot_gcode = ko.observable(values.on_after_snapshot_gcode); + self.on_before_snapshot_gcode = ko.observable(values.on_before_snapshot_gcode); self.enable_custom_image_preferences(values.enable_custom_image_preferences); - self.on_print_start_script(values.on_print_start_script); - self.on_before_snapshot_script(values.on_before_snapshot_script); - //self.external_camera_snapshot_script = ko.observable(values.external_camera_snapshot_script); - //self.on_after_snapshot_script = ko.observable(values.on_after_snapshot_script); - //self.on_before_render_script = ko.observable(values.on_before_render_script); - //self.on_after_render_script = ko.observable(values.on_after_render_script); self.delay(values.delay); self.timeout_ms(values.timeout_ms); self.apply_settings_before_print(values.apply_settings_before_print); self.apply_settings_at_startup(values.apply_settings_at_startup); self.snapshot_transpose(values.snapshot_transpose); + if (typeof values.apply_settings_when_disabled != 'undefined') + { + self.apply_settings_when_disabled(values.apply_settings_when_disabled); + } + // Clear any settings that we don't want to update, unless they aren't important. + self.on_print_start_script(""); + self.on_before_snapshot_script(""); + self.external_camera_snapshot_script(""); + self.on_after_snapshot_script(""); + self.on_before_render_script(""); + self.on_after_render_script(""); + self.on_print_end_script(""); self.webcam_settings.updateWebcamSettings(values); }; @@ -302,8 +387,11 @@ $(function() { // need to remove the parent link from the automatic configuration to prevent a cyclic copy var parent = self.automatic_configuration.parent; self.automatic_configuration.parent = null; + var webcam_settings_popup = self.webcam_settings_popup; + self.webcam_settings_popup = null; var copy = ko.toJS(self); self.automatic_configuration.parent = parent; + self.webcam_settings_popup = webcam_settings_popup; return copy; }; @@ -312,90 +400,18 @@ $(function() { Octolapse.Cameras.setIsClickable(!value); }); - self.updateImagePreferencesFromServer = function(replace) { - self.webcam_settings.getMjpgStreamerControls(replace, function (results) { - // Update the current settings that we just received - self.webcam_settings.updateWebcamSettings(results.settings); - if (results.settings.new_preferences_available) { - // If new settings are available, offer to update them, but don't do it automatically - var message = "Octolapse has detected new camera preferences from your " + - "streaming server. Do you want to overwrite your current image preferences?"; - Octolapse.showConfirmDialog( - "replace-image-preferences", - "New Image Preferences Available", - message, - function () { - self.updateImagePreferencesFromServer(true, function (results) { - self.webcam_settings.updateWebcamSettings(results.settings); - }); - } - ); - } - }, function(results){ - self.enable_custom_image_preferences(false); - var message = "There was a problem retrieving webcam settings from the server."; - if (results && results.error) - message = message + " Details: " + results.error; - var options = { - title: 'Webcam Settings Error', - text: message, - type: 'error', - hide: false, - addclass: "octolapse" - }; - - Octolapse.displayPopupForKey(options, "webcam_settings_error",["webcam_settings_error"]); - }); - }; - - self.on_opened = function() { - console.log("Opening camera profile"); - if (self.enable_custom_image_preferences()) - self.updateImagePreferencesFromServer(false); - }; - // update the webcam settings self.webcam_settings.updateWebcamSettings(values); - - // Now that the webcam settings are updated, subscribe to address changes - // so we can update the streaming server controls - self.webcam_settings.address.subscribe(function(newValue){ - if(self.enable_custom_image_preferences) - self.updateImagePreferencesFromServer(true); - }); }; Octolapse.CameraProfileValidationRules = { rules: { - camera_type: { required: true }, - exposure_type: { required: true }, - led_1_mode: { required: true}, - powerline_frequency: { required: true}, - snapshot_request_template: { octolapseSnapshotTemplate: true }, - stream_template: { octolapseSnapshotTemplate: true }, - brightness_request_template: { octolapseCameraRequestTemplate: true }, - contrast_request_template: { octolapseCameraRequestTemplate: true }, - saturation_request_template: { octolapseCameraRequestTemplate: true }, - white_balance_auto_request_template: { octolapseCameraRequestTemplate: true }, - gain_request_template: { octolapseCameraRequestTemplate: true }, - powerline_frequency_request_template: { octolapseCameraRequestTemplate: true }, - white_balance_temperature_request_template: { octolapseCameraRequestTemplate: true }, - sharpness_request_template: { octolapseCameraRequestTemplate: true }, - backlight_compensation_enabled_request_template: { octolapseCameraRequestTemplate: true }, - exposure_type_request_template: { octolapseCameraRequestTemplate: true }, - exposure_request_template: { octolapseCameraRequestTemplate: true }, - exposure_auto_priority_enabled_request_template: { octolapseCameraRequestTemplate: true }, - pan_request_template: { octolapseCameraRequestTemplate: true }, - tilt_request_template: { octolapseCameraRequestTemplate: true }, - autofocus_enabled_request_template: { octolapseCameraRequestTemplate: true }, - focus_request_template: { octolapseCameraRequestTemplate: true }, - zoom_request_template: { octolapseCameraRequestTemplate: true }, - led1_mode_request_template: { octolapseCameraRequestTemplate: true }, - led1_frequency_request_template: { octolapseCameraRequestTemplate: true }, - jpeg_quality_request_template: { octolapseCameraRequestTemplate: true } + octolapse_camera_camera_type: { required: true }, + octolapse_camera_snapshot_request_template: { octolapseSnapshotTemplate: true }, + octolapse_camera_stream_template: { octolapseSnapshotTemplate: true }, }, messages: { - name: "Please enter a name for your profile" + octolapse_camera_name: "Please enter a name for your profile" } }; diff --git a/octoprint_octolapse/static/js/octolapse.profiles.camera.webcam.js b/octoprint_octolapse/static/js/octolapse.profiles.camera.webcam.js index 37fbe63b..ed4aeb66 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.camera.webcam.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.camera.webcam.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -22,7 +22,7 @@ ################################################################################## */ $(function() { - ko.subscribable.fn.subscribeWithTarget = function (handler, target) { + ko.subscribable.fn.octolapseSubscriptWithTarget = function (handler, target) { var self = this; var _oldValue; self.before_change_subscription = this.subscribe(function (oldValue) { @@ -40,11 +40,21 @@ $(function() { return self; }; - Octolapse.WebcamSettingsPopupViewModel = function (values) { + Octolapse.WebcamSettingsPopupViewModel = function (id) { var self = this; + self.id = id; + self.selector = "#" + id; + self.loading_selector = self.selector + ' div.webcam_settings_preview div.loading'; + self.error_selector = self.selector + ' div.webcam_settings_preview div.error'; + self.dialog = {}; + self.closeWebcamSettingsDialog = function() { self.can_hide = true; - $("#octolapse_webcam_settings_dialog").modal("hide"); + if (self.dialog.$webcamSettingsDialog) + { + self.dialog.$webcamSettingsDialog.modal("hide"); + } + }; // variable to hold the custom webcam image preferences @@ -52,57 +62,74 @@ $(function() { self.can_hide = false; - self.openWebcamSettingsDialog = function(){ - var dialog = this; + self.openWebcamSettingsDialog = function(on_closed){ + self.dialog = {}; + self.on_closed_callback = on_closed; + // Set close callback + self.dialog.on_closed = function(send_settings) { + if (self.on_closed_callback) + { + var settings = null; + if (send_settings) { + settings = ko.toJS(self.webcam_settings); + } + self.on_closed_callback(settings); + } + self.closeWebcamSettingsDialog(); + }; // Show the settings dialog - dialog.$webcamSettingsDialog = $("#octolapse_webcam_settings_dialog"); - dialog.$webcamSettingsForm = dialog.$webcamSettingsDialog.find("#octolapse_webcam_settings_popup_form"); - dialog.$webcamStreamImg = dialog.$webcamSettingsDialog.find("#octolapse_webcam_settings_stream"); - dialog.$cancelButton = $(".cancel", dialog.$webcamSettingsDialog); - dialog.$closeIcon = $("a.close", dialog.$webcamSettingsDialog); - dialog.$saveButton = $(".save", dialog.$webcamSettingsDialog); - dialog.$defaultButton = $(".set-defaults", dialog.$webcamSettingsDialog); - dialog.$modalBody = dialog.$webcamSettingsDialog.find(".modal-body"); - dialog.$modalHeader = dialog.$webcamSettingsDialog.find(".modal-header"); - dialog.$modalFooter = dialog.$webcamSettingsDialog.find(".modal-footer"); - dialog.$cancelButton.unbind("click"); + self.dialog.$webcamSettingsDialog = $(self.selector); + self.dialog.$cancelButton = $(".cancel", self.dialog.$webcamSettingsDialog); + self.dialog.$closeIcon = $("a.close", self.dialog.$webcamSettingsDialog); + self.dialog.$saveButton = $(".save", self.dialog.$webcamSettingsDialog); + self.dialog.$defaultButton = $(".set-defaults", self.dialog.$webcamSettingsDialog); + self.dialog.$modalBody = self.dialog.$webcamSettingsDialog.find(".modal-body"); + self.dialog.$modalHeader = self.dialog.$webcamSettingsDialog.find(".modal-header"); + self.dialog.$modalFooter = self.dialog.$webcamSettingsDialog.find(".modal-footer"); + self.dialog.$cancelButton.unbind("click"); // Called when the user clicks the cancel button in any add/update dialog - dialog.$cancelButton.bind("click", self.webcam_settings.cancelWebcamChanges); - dialog.$closeIcon.unbind("click"); - dialog.$closeIcon.bind("click", self.webcam_settings.cancelWebcamChanges); + self.dialog.$cancelButton.bind("click", function() { + self.webcam_settings.cancelWebcamChanges(self.dialog.on_closed); + }); + self.dialog.$closeIcon.unbind("click"); + self.dialog.$closeIcon.bind("click", function() { + self.webcam_settings.cancelWebcamChanges(self.dialog.on_closed); + }); - dialog.$saveButton.unbind("click"); + self.dialog.$saveButton.unbind("click"); // Called when the user clicks the cancel button in any add/update dialog - dialog.$saveButton.bind("click", function () { + self.dialog.$saveButton.bind("click", function () { // Save the settings. - self.saveWebcamSettings(); + if (self.on_closed_callback) { + self.dialog.on_closed(true); + } + else + { + self.saveWebcamSettings(); + } }); - dialog.$defaultButton.unbind("click"); + self.dialog.$defaultButton.unbind("click"); // Called when the user clicks the cancel button in any add/update dialog - dialog.$defaultButton.bind("click", function () { + self.dialog.$defaultButton.bind("click", function () { // Hide the dialog self.webcam_settings.restoreWebcamDefaults(); }); // Prevent hiding unless the event was initiated by the hideAddEditDialog function - dialog.$webcamSettingsDialog.on("hide.bs.modal", function () { - console.log("Hiding webcam settings dialog"); + self.dialog.$webcamSettingsDialog.on("hide.bs.modal", function () { + //console.log("Hiding webcam settings dialog"); if (!self.can_hide) return false; // Clear out error summary self.webcam_settings.setStreamVisibility(false); }); - dialog.$webcamSettingsDialog.on("show.bs.modal", function () { - - }); - - dialog.$webcamSettingsDialog.on("shown.bs.modal", function () { + self.dialog.$webcamSettingsDialog.on("shown.bs.modal", function () { self.can_hide = false; self.webcam_settings.setStreamVisibility(true); - dialog.$webcamSettingsDialog.css({ + self.dialog.$webcamSettingsDialog.css({ width: '940px', 'margin-left': function () { return -($(this).width() / 2); @@ -110,11 +137,11 @@ $(function() { }); }); - dialog.$webcamSettingsDialog.modal({ + self.dialog.$webcamSettingsDialog.modal({ backdrop: 'static', maxHeight: function() { return Math.max( - window.innerHeight - dialog.$modalHeader.innerHeight()-dialog.$modalFooter.innerHeight()-30, + window.innerHeight - self.dialog.$modalHeader.innerHeight()-self.dialog.$modalFooter.innerHeight()-30, 200 ); } @@ -163,22 +190,39 @@ $(function() { }); }; - self.canEditSettings = ko.pureComputed(function(){ - // Get the current camera profile - var current_camera = Octolapse.Status.getCurrentProfileByGuid(Octolapse.Status.profiles().cameras(),Octolapse.Status.current_camera_guid()); - if (current_camera != null) + self.showWebcamSettingsForProfile = function(profile, on_preferences_changed) { + // First update the current webcam_settings based on the current profile + self.webcam_settings.updateWebcamSettings(profile); + + self.webcam_settings.getMjpgStreamerControls(false, function (results) { + // Update the current settings that we just received + if (results.settings.new_preferences_available) { - return current_camera.enable_custom_image_preferences; + self.webcam_settings.updateWebcamSettings(results.settings); } - return false; - }); - self.showWebcamSettings = function() { + self.openWebcamSettingsDialog(on_preferences_changed); + }, function(results){ + var message = "There was a problem retrieving webcam settings from the server."; + if (results && results.error) + message = message + " Details: " + results.error; + var options = { + title: 'Webcam Settings Error', + text: message, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options, "webcam_settings_error",["webcam_settings_error"]); + }); + }; + + self.showWebcamSettingsForGuid = function(guid) { // Load the current webcam settings // On success, show the dialog // If no guid is supplied, this is a new profile. We will need to know that later when we push/update our observable array var data = { - 'guid': Octolapse.Status.current_camera_guid(), + 'guid': guid, 'replace': false }; self.webcam_settings.getImagePreferences(data, function (results) { @@ -195,7 +239,7 @@ $(function() { message, function () { data = { - 'guid': Octolapse.Status.current_camera_guid(), + 'guid': self.webcam_settings.guid(), 'replace': true }; self.webcam_settings.getImagePreferences(data, function (results) { @@ -217,7 +261,7 @@ $(function() { }; Octolapse.displayPopupForKey(options,"webcam_settings_error",["webcam_settings_error"]); }); - } + }; }; Octolapse.WebcamSettingsViewModel = function (close_callback) { @@ -236,6 +280,7 @@ $(function() { self.timeout_ms = ko.observable(); self.server_type = ko.observable(); self.type = ko.observable(); + self.stream_download = ko.observable(); self.mjpg_streamer = new Octolapse.MjpgStreamerViewModel(); self.use_custom_webcam_settings_page = ko.observable(); self.webcam_two_column_view = ko.observable(Octolapse.getLocalStorage("webcam_two_column_view") || false); @@ -243,7 +288,7 @@ $(function() { self.toggleColumns = function(){ var two_column_view = !self.webcam_two_column_view(); self.webcam_two_column_view(two_column_view); - console.log("Setting two column view."); + //console.log("Setting two column view."); Octolapse.setLocalStorage("webcam_two_column_view", two_column_view); }; @@ -274,14 +319,7 @@ $(function() { if (!self.server_type() || !self.address()) return; var data = { - "guid": self.guid(), - "server_type": self.server_type(), - "camera_name": self.name() ? self.name() : "unknown", - "address": self.address(), - "username": self.username() ? self.username() : "", - "password": self.password() ? self.password() : "", - "ignore_ssl_error": self.ignore_ssl_error() ? self.ignore_ssl_error() : false, - "replace": replace + "profile": ko.toJS(self) }; $.ajax({ @@ -301,9 +339,6 @@ $(function() { } }, error: function (XMLHttpRequest, textStatus, errorThrown) { - - //if(self.close_callback) - // self.close_callback(); error_callback(); } }); @@ -315,11 +350,11 @@ $(function() { return "Switch to Default Page"; } else{ - return "Check for Custom Page"; + return "Switch to Custom Page"; } }); - self.toggleCustomCameraSettingsPage = function(){ + self.toggleCustomCameraSettingsPage = function(prevent_errors, force_custom){ // fetch control settings from the server if (!self.server_type() || !self.address()) { @@ -375,7 +410,7 @@ $(function() { return; } // set our camera type info - var use_custom_webcam_settings_page = !self.use_custom_webcam_settings_page(); + var use_custom_webcam_settings_page = !self.use_custom_webcam_settings_page() || force_custom; if(!results.type) use_custom_webcam_settings_page = false; data = { @@ -390,38 +425,44 @@ $(function() { self.updateWebcamSettings(data); // We meed to rebind the help links here, else they will not show up. Octolapse.Help.bindHelpLinks(".octolapse .webcam_settings"); - var message; - var title = "Webcam type found!"; - var type = "success"; - if (!results.type) { - message = "This webcam does not currently support a custom camera control page."; - title = 'Unknown webcam'; - type = "error"; - } - else if (!use_custom_webcam_settings_page) - { - message = "Successfully switched to the default mjpg-streamer control page."; - } - var options = { + if (!prevent_errors) { + var message; + var title = "Webcam type found!"; + var type = "success"; + if (!results.type) { + message = "There is no custom control for your webcam model at the moment!"; + title = 'Unknown webcam'; + type = "error"; + } else if (!use_custom_webcam_settings_page) { + title = "Changed to Default View"; + message = "Successfully switched to the default mjpg-streamer control page."; + } else { + title = "Webcam type found!"; + message = "A custom image preference control page exists for this camera!."; + } + var options = { title: title, text: message, type: type, hide: true, addclass: "octolapse" }; - Octolapse.displayPopupForKey(options, "camera-detected",["camera-detected"]); + Octolapse.displayPopupForKey(options, "camera-detected", ["camera-detected"]); + } return; }, error: function (XMLHttpRequest, textStatus, errorThrown) { - var options = { - title: 'Unknown Camera Type', - text: "Status: " + textStatus + ". Error: " + errorThrown, - type: 'error', - hide: false, - addclass: "octolapse" - }; self.type(null); - Octolapse.displayPopupForKey(options,"camera_settings_error",["camera_settings_error"]); + if (!prevent_errors) { + var options = { + title: 'Unknown Camera Type', + text: "Status: " + textStatus + ". Error: " + errorThrown, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(options,"camera_settings_error",["camera_settings_error"]); + } } }); }; @@ -450,7 +491,7 @@ $(function() { else value = 'webcam-other-template'; - return value + return value; }); self.get_webcam_data = ko.pureComputed(function(){ @@ -466,7 +507,7 @@ $(function() { self.stream_url = ko.pureComputed(function(){ if(!self.camera_stream_visible()) return ''; - console.log("Calculating stream url."); + //console.log("Calculating stream url."); var url = self.stream_template(); if (url != "") url = url.replace("{camera_address}", self.address()); @@ -474,9 +515,16 @@ $(function() { },this); self.setStreamVisibility = function(value){ - console.log("Attempting to stop the camera stream"); - self.camera_stream_visible(value); - self.stream_url(); + if (self.camera_stream_visible() != value) { + if (value){ + //console.log("Attempting to start the camera stream"); + } + else{ + //console.log("Attempting to stop the camera stream"); + } + self.camera_stream_visible(value); + //self.stream_url(); + } }; self.subscriptions = []; @@ -491,10 +539,11 @@ $(function() { }; self.subscribe_to_mjpg_streamer_changes = function() { + self.unsubscribe_to_settings_changes(); for(var index = 0; index < self.mjpg_streamer.controls().length; index++) { var control = self.mjpg_streamer.controls()[index]; - self.subscriptions.push(control.value.subscribeWithTarget(function (target, oldValue, newValue) { + self.subscriptions.push(control.value.octolapseSubscriptWithTarget(function (target, oldValue, newValue) { if (oldValue !== newValue) { self.applyWebcamSetting(ko.toJS(target)); @@ -512,9 +561,9 @@ $(function() { }; // I found this awesome binding handler created by Michael Rouse, available at https://codepen.io/mwrouse/pen/wWwvmN - self.cancelWebcamChanges = function(){ - if (self.guid() == null || self.guid() == "") - self.close_callback(); + self.cancelWebcamChanges = function(on_closed_callback){ + //if (on_closed_callback && (self.guid() == null || self.guid() == "")) + // on_closed_callback(ko.toJS(self)); //console.log("Undoing webcam changes."); // If no guid is supplied, this is a new profile. We will need to know that later when we push/update our observable array @@ -541,8 +590,8 @@ $(function() { Octolapse.displayPopupForKey(options, "camera_settings_error",["camera_settings_error"]); } - if(self.close_callback) - self.close_callback(); + if (on_closed_callback) + on_closed_callback(false); }, error: function (XMLHttpRequest, textStatus, errorThrown) { var options = { @@ -553,8 +602,8 @@ $(function() { addclass: "octolapse" }; Octolapse.displayPopupForKey(options,"camera_settings_error",["camera_settings_error"]); - if(self.close_callback) - self.close_callback(); + if (on_closed_callback) + on_closed_callback(true); } }); }; @@ -638,8 +687,12 @@ $(function() { self.name(values.name); if ("address" in webcam_settings) self.address(webcam_settings.address); - if ('snapshot_request_template' in webcam_settings) + if ( + 'snapshot_request_template' in webcam_settings && + self.snapshot_request_template() != webcam_settings.snapshot_request_template + ) { self.snapshot_request_template(webcam_settings.snapshot_request_template); + } if ("username" in webcam_settings) self.username(webcam_settings.username); if ("password" in webcam_settings) @@ -655,6 +708,9 @@ $(function() { self.type(webcam_settings.type); } + if ("stream_download" in webcam_settings) + self.stream_download(webcam_settings.stream_download); + if("use_custom_webcam_settings_page" in webcam_settings) { self.use_custom_webcam_settings_page(webcam_settings.use_custom_webcam_settings_page); @@ -665,7 +721,7 @@ $(function() { self.mjpg_streamer.update(webcam_settings.mjpg_streamer, self.type(), self.use_custom_webcam_settings_page()); } // notify subscribers of a possible stream url change. - self.stream_url.notifySubscribers(); + //self.stream_url.notifySubscribers(); // restore the webcam settings template so that the custom controls show self.restore_webcam_settings_template(); setTimeout(function(){ @@ -760,7 +816,8 @@ $(function() { }; self.previewStabilization = function() { - var message = "Make sure your bed is clear, and that your 'Home Axis Gcode Script' \ + var message = "Your extruder will be moved to the selected stabilization position. \ + Make sure your bed is clear, and that your 'Home Axis Gcode Script' \ is correct before attempting to preview the stabilization point. \ Are you sure you want to continue?"; Octolapse.showConfirmDialog("preview-stabilization", "Preview Stabilization", message, function(){ @@ -795,5 +852,5 @@ $(function() { },null); }; - } + }; }); diff --git a/octoprint_octolapse/static/js/octolapse.profiles.camera.webcam.mjpg_streamer.js b/octoprint_octolapse/static/js/octolapse.profiles.camera.webcam.mjpg_streamer.js index 4e72707a..72327943 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.camera.webcam.mjpg_streamer.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.camera.webcam.mjpg_streamer.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -82,7 +82,7 @@ $(function() { self.value("1"); else self.value("0"); - }) + }); } self.help_url = ko.pureComputed(function() { @@ -99,7 +99,7 @@ $(function() { self.checkbox_title = ko.pureComputed(function() { return "Enable or disable " + self.name() + "."; - }) + }); }; @@ -130,7 +130,7 @@ $(function() { self.update = function(values, type, use_custom_webcam_settings_page) { if (!values) return; - self.controls([]); + var controls = []; self.data.controls_dict = {}; // return if we have no controls if (!values.controls) @@ -147,10 +147,10 @@ $(function() { var control = new Octolapse.MjpgStreamerControlViewModel(sortedControls[index]); if ("id" in control) { self.data.controls_dict[control.id()] = control; - self.controls.push(control); + controls.push(control); } } - + self.controls(controls); self.bind_viewmodel_for_camera_type(); }; diff --git a/octoprint_octolapse/static/js/octolapse.profiles.js b/octoprint_octolapse/static/js/octolapse.profiles.js index 60c2e2ae..ab26076e 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -63,7 +63,7 @@ $(function() { }); // Created a sorted observable - self.profiles_sorted = ko.computed(function() { return Octolapse.observableNameSort(self.profiles) }); + self.profiles_sorted = ko.computed(function() { return Octolapse.observableNameSort(self.profiles); }); /* Octoprint Viewmodel Events @@ -121,7 +121,8 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false, + icon: null } }; Octolapse.displayPopup(options); @@ -130,7 +131,7 @@ $(function() { }; //Remove an existing profile from the server settings, then if successful remove it from the observable array. self.removeProfile = function (guid) { - var currentProfile = self.getProfileByGuid(guid) + var currentProfile = self.getProfileByGuid(guid); if (confirm("Are you sure you want to permanently erase the profile:'" + currentProfile.name() + "'?")) { var data = { "client_id": Octolapse.Globals.client_id,'guid': ko.toJS(guid), 'profileType': self.profileTypeName() }; $.ajax({ @@ -151,7 +152,7 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -166,7 +167,7 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -199,7 +200,7 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -213,7 +214,7 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -232,7 +233,7 @@ $(function() { newProfile = new self.profileViewModelCreate(ko.toJS(self.default_profile())); // Create our profile viewmodel } else { - var current_profile = ko.toJS(self.getProfileByGuid(guid)) + var current_profile = ko.toJS(self.getProfileByGuid(guid)); if(current_profile == null) return null; @@ -246,7 +247,7 @@ $(function() { var index = Octolapse.arrayFirstIndexOf(self.profiles(), function(item) { var itemGuid = item.guid(); - return itemGuid === guid + return itemGuid === guid; } ); if (index < 0) { @@ -258,7 +259,7 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -274,7 +275,7 @@ $(function() { var itemGuid = item.guid(); var matchFound = itemGuid === guid; if (matchFound) - return matchFound + return matchFound; } ); if (index < 0) { @@ -332,9 +333,9 @@ $(function() { warning = null; if(Octolapse.Status.is_timelapse_active()) { - if(newProfile.profileTypeName() == 'Debug') + if(newProfile.profileTypeName() == 'Logging') { - warning = "A timelapse is active. All debug settings will IMMEDIATELY take effect, except for 'Test Mode' which will not take effect until the next print."; + warning = "A timelapse is active. All logging settings will IMMEDIATELY take effect."; } else warning = "A timelapse is active. Any changes made here will NOT take effect until the next print."; diff --git a/octoprint_octolapse/static/js/octolapse.profiles.library.js b/octoprint_octolapse/static/js/octolapse.profiles.library.js index 0574d932..e854bcab 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.library.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.library.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -26,6 +26,7 @@ $(function() { values, profiles, profile_type, parent, update_callback ){ var self = this; + self.options_caption = "Not Selected"; self.is_initialized = false; // Available profile and key information if (profiles) @@ -48,7 +49,7 @@ $(function() { var key_value; var has_null_value = false; if (!values.key_values || !values.key_values[index] || has_null_value) { - key_value = {name: "Select Profile", value: "null"}; + key_value = {name: self.options_caption, value: "null"}; has_null_value = true; } else @@ -183,7 +184,8 @@ $(function() { self.is_confirming(false); } }, - on_complete: function (newValue, oldValue, wasConfirmed, wasIgnored,) { + on_complete: function (newValue, oldValue, wasConfirmed, wasIgnored) { + // Todo: What is wasIgnored doing here? Probably need to deal with it. // we don't want to do anything if we're ignoring key changes if (self.ignore_key_change) return; @@ -232,7 +234,7 @@ $(function() { return { "name": option.name, "value": key - } + }; }; self.updateKeyValuesForIndexedKey = function(indexed_key) { @@ -245,12 +247,14 @@ $(function() { // first we have to find this object from the options var index = self.getIndexFromIndexedKey(indexed_key); var option = self.getOptionForIndexedKey(indexed_key); + if (!option) + return; var key = self.getKeyFromIndexedKey(indexed_key); var option_value; if (key == "null") { option_value = { - "name": "Select a value", + "name": self.options_caption, "value": "null" }; } @@ -303,7 +307,7 @@ $(function() { if (current_option_value) { var key = self.getKeyFromIndexedKey(current_option_value); if (!(key in current_parent.values)) { - options.unshift(self.createIndexedOption("Select a value","null", key_index)); + options.unshift(self.createIndexedOption(self.options_caption,"null", key_index)); return; } current_parent = current_parent.values[key]; @@ -329,9 +333,9 @@ $(function() { options.push(self.createIndexedOption(self.original_key[index],self.original_key[index], key_index)); } options.sort(function(left, right) { - return left.name == right.name ? 0 : (left.name < right.name ? -1 : 1) + return left.name == right.name ? 0 : (left.name < right.name ? -1 : 1); }); - options.unshift(self.createIndexedOption("Select a value","null", key_index)); + options.unshift(self.createIndexedOption(self.options_caption,"null", key_index)); self.ignore_key_change = false; }; @@ -352,7 +356,7 @@ $(function() { if (prevent_update && index == self.available_keys.length-1) self.ignore_key_change = true; if (index < keys.length){ - var indexed_key_value = {name:"Select a value", value: "0-null"}; + var indexed_key_value = {name:self.options_caption, value: "0-null"}; if (keys[index]) indexed_key_value = self.createIndexedKey(keys[index].value, index); self.key_values_with_index()[index](indexed_key_value); @@ -466,7 +470,7 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopupForKey( @@ -499,7 +503,7 @@ $(function() { hide: true, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopupForKey( @@ -519,7 +523,7 @@ $(function() { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopupForKey( diff --git a/octoprint_octolapse/static/js/octolapse.profiles.debug.js b/octoprint_octolapse/static/js/octolapse.profiles.logging.js similarity index 51% rename from octoprint_octolapse/static/js/octolapse.profiles.debug.js rename to octoprint_octolapse/static/js/octolapse.profiles.logging.js index 9377d667..311ec693 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.debug.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.logging.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -21,32 +21,28 @@ # following email address: FormerLurker@pm.me ################################################################################## */ -$(function() { - Octolapse.DebugProfileViewModel = function (values) { +$(function () { + Octolapse.LoggingProfileViewModel = function (values) { var self = this; - self.profileTypeName = ko.observable("Debug"); + self.profileTypeName = ko.observable("Logging"); self.guid = ko.observable(values.guid); self.name = ko.observable(values.name); self.description = ko.observable(values.description); self.enabled = ko.observable(values.enabled); - self.is_test_mode = ko.observable(values.is_test_mode); self.log_to_console = ko.observable(values.log_to_console); self.log_all_errors = ko.observable(values.log_all_errors); self.enabled_loggers = ko.observableArray(); - for (var index = 0; index < values.enabled_loggers.length; index++) - { + for (var index = 0; index < values.enabled_loggers.length; index++) { var curItem = values.enabled_loggers[index]; - self.enabled_loggers.push({'name':curItem.name, 'log_level':curItem.log_level}); + self.enabled_loggers.push({'name': curItem.name, 'log_level': curItem.log_level}); } self.logger_name_add = ko.observable(); self.logger_level_add = ko.observable(); self.default_log_level = values.default_log_level; - self.get_enabled_logger_index_by_name = function (name) - { - for (var index = 0; index < self.enabled_loggers().length; index++) - { + self.get_enabled_logger_index_by_name = function (name) { + for (var index = 0; index < self.enabled_loggers().length; index++) { var logger = self.enabled_loggers()[index]; if (logger.name === name) { return index; @@ -54,15 +50,13 @@ $(function() { } return -1; }; - self.available_loggers = ko.computed(function() { + self.available_loggers = ko.computed(function () { var available_loggers = []; - for (var logger_index = 0; logger_index < Octolapse.DebugProfiles.profileOptions.all_logger_names.length; logger_index++) - { - var logger_name = Octolapse.DebugProfiles.profileOptions.all_logger_names[logger_index]; + for (var logger_index = 0; logger_index < Octolapse.LoggingProfiles.profileOptions.all_logger_names.length; logger_index++) { + var logger_name = Octolapse.LoggingProfiles.profileOptions.all_logger_names[logger_index]; var found_logger_index = self.get_enabled_logger_index_by_name(logger_name); - if (found_logger_index === -1) - { - available_loggers.push({'name':logger_name, 'log_level': self.default_log_level}); + if (found_logger_index === -1) { + available_loggers.push({'name': logger_name, 'log_level': self.default_log_level}); } } return available_loggers; @@ -76,20 +70,20 @@ $(function() { return leftName === rightName ? 0 : (leftName < rightName ? -1 : 1); }); }; - self.available_loggers_sorted = ko.computed(function() { - return self.loggerNameSort(self.available_loggers) + self.available_loggers_sorted = ko.computed(function () { + return self.loggerNameSort(self.available_loggers); }); - self.removeLogger = function(logger) { + self.removeLogger = function (logger) { //console.log("removing logger."); self.enabled_loggers.remove(logger); }; - self.addLogger = function() { + self.addLogger = function () { //console.log("Adding logger"); var index = self.get_enabled_logger_index_by_name(self.logger_name_add()); if (index === -1) { - self.enabled_loggers.push({'name':self.logger_name_add(),'log_level':self.logger_level_add()}); + self.enabled_loggers.push({'name': self.logger_name_add(), 'log_level': self.logger_level_add()}); self.scrollToBottom(); } }; @@ -97,38 +91,97 @@ $(function() { // When adding loggers automatically scrolling to the bottom of the page // makes the control much more user friendly if you want to add several loggers. // TODO: Scroll down only the width of a new control, which will be necessary if we add more controls below the logger controls. - self.scrollToBottom = function() - { - var debug_container = document.getElementById("octolapse_add_edit_profile_model_body"); - debug_container.scrollTop = debug_container.scrollHeight; + self.scrollToBottom = function () { + var logging_container = document.getElementById("octolapse_add_edit_profile_model_body"); + logging_container.scrollTop = logging_container.scrollHeight; }; - - self.updateFromServer = function(values) { - self.guid(values.guid); + + self.updateFromServer = function (values) { self.name(values.name); self.description(values.description); self.enabled(values.enabled); - self.is_test_mode(values.is_test_mode); self.log_to_console(values.log_to_console); self.log_all_errors(values.log_all_errors); self.enabled_loggers([]); - for (var index = 0; index < values.enabled_loggers.length; index++) - { + for (var index = 0; index < values.enabled_loggers.length; index++) { var curItem = values.enabled_loggers[index]; - self.enabled_loggers.push({'name':curItem.name, 'log_level':curItem.log_level}); + self.enabled_loggers.push({'name': curItem.name, 'log_level': curItem.log_level}); } }; + self.clearLog = function (clear_all) { + var title; + var message; + if (clear_all) { + title = "Clear All Logs"; + message = "All octolapse log files will be cleared and deleted. Are you sure?"; + } else { + title = "Clear Log"; + message = "The most recent octolapse log file will be cleared. Are you sure?"; + } + Octolapse.showConfirmDialog( + "clear_log", + title, + message, + function () { + if (clear_all) { + title = "Logs Cleared"; + message = "All octolapse log files have been cleared."; + } else { + title = "Most Recent Log Cleared"; + message = "The most recent octolapse log file has been cleared."; + } + var data = { + clear_all: clear_all + }; + $.ajax({ + url: "./plugin/octolapse/clearLog", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (data) { + var options = { + title: title, + text: message, + type: 'success', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(options, "log_file_cleared", "log_file_cleared"); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + var message = "Unable to clear the log.:( Status: " + textStatus + ". Error: " + errorThrown; + var options = { + title: 'Clear Log Error', + text: message, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(options, "log_file_cleared", "log_file_cleared"); + } + }); + } + ); + + }; + self.automatic_configuration = new Octolapse.ProfileLibraryViewModel( values.automatic_configuration, - Octolapse.DebugProfiles.profileOptions.server_profiles, + Octolapse.LoggingProfiles.profileOptions.server_profiles, self.profileTypeName(), self, self.updateFromServer ); - self.toJS = function() - { + self.toJS = function () { // need to remove the parent link from the automatic configuration to prevent a cyclic copy var parent = self.automatic_configuration.parent; self.automatic_configuration.parent = null; @@ -136,17 +189,17 @@ $(function() { self.automatic_configuration.parent = parent; return copy; }; - self.on_closed = function(){ + self.on_closed = function () { self.automatic_configuration.on_closed(); }; - self.automatic_configuration.is_confirming.subscribe(function(value){ + self.automatic_configuration.is_confirming.subscribe(function (value) { //console.log("IsClickable" + value.toString()); - Octolapse.DebugProfiles.setIsClickable(!value); + Octolapse.LoggingProfiles.setIsClickable(!value); }); - + }; - Octolapse.DebugProfileValidationRules = { + Octolapse.LoggingProfileValidationRules = { rules: { name: "required" }, diff --git a/octoprint_octolapse/static/js/octolapse.profiles.printer.js b/octoprint_octolapse/static/js/octolapse.profiles.printer.js index 1f3d9c07..95f6c049 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.printer.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.printer.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -24,78 +24,86 @@ $(function() { Octolapse.PrinterProfileValidationRules = { rules: { - min_x: { lessThanOrEqual: "#octolapse_printer_max_x" }, - max_x: { greaterThanOrEqual: "#octolapse_printer_min_x"}, - min_y: { lessThanOrEqual: "#octolapse_printer_max_y" }, - max_y: { greaterThanOrEqual: "#octolapse_printer_min_y" }, - min_z: { lessThanOrEqual: "#octolapse_printer_max_z" }, - max_z: { greaterThanOrEqual: "#octolapse_printer_min_z" }, - snapshot_min_x: { lessThanOrEqual: "#octolapse_printer_snapshot_max_x" }, - snapshot_max_x: { greaterThanOrEqual: "#octolapse_printer_snapshot_min_x"}, - snapshot_min_y: { lessThanOrEqual: "#octolapse_printer_snapshot_max_y" }, - snapshot_max_y: { greaterThanOrEqual: "#octolapse_printer_snapshot_min_y" }, - snapshot_min_z: { lessThanOrEqual: "#octolapse_printer_snapshot_max_z" }, - snapshot_max_z: { greaterThanOrEqual: "#octolapse_printer_snapshot_min_z" }, - minimum_layer_height: { lessThanOrEqual: "#printer_profile_priming_height" }, - priming_height: { greaterThanOrEqual: "#printer_profile_minimum_layer_height" }, - auto_position_detection_commands: { csvString: true }, - printer_profile_other_slicer_retract_length: {required: true}, - printer_profile_slicer_other_z_hop: {required: true}, - slicer_cura_smooth_spiralized_contours: {ifCheckedEnsureNonNull: ["#slicer_cura_layer_height"] }, - slicer_other_vase_mode: {ifCheckedEnsureNonNull: ["#slicer_other_slicer_layer_height"] }, - slicer_simplify_3d_vase_mode: {ifCheckedEnsureNonNull: ["#slicer_simplify_3d_layer_height"] }, - slicer_slic3r_pe_vase_mode: {ifCheckedEnsureNonNull: ["#slicer_slic3r_pe_layer_height"] }, - slicer_cura_layer_height: {ifOtherCheckedEnsureNonNull: '#slicer_cura_smooth_spiralized_contours'}, - slicer_other_slicer_layer_height: {ifOtherCheckedEnsureNonNull: '#slicer_other_vase_mode'}, - slicer_simplify_3d_layer_height: {ifOtherCheckedEnsureNonNull: '#slicer_simplify_3d_vase_mode'}, - slicer_slic3r_pe_layer_height: {ifOtherCheckedEnsureNonNull: '#slicer_slic3r_pe_vase_mode'} + octolapse_printer_min_x: { lessThanOrEqual: "#octolapse_printer_max_x" }, + octolapse_printer_max_x: { greaterThanOrEqual: "#octolapse_printer_min_x"}, + octolapse_printer_min_y: { lessThanOrEqual: "#octolapse_printer_max_y" }, + octolapse_printer_max_y: { greaterThanOrEqual: "#octolapse_printer_min_y" }, + octolapse_printer_min_z: { lessThanOrEqual: "#octolapse_printer_max_z" }, + octolapse_printer_max_z: { greaterThanOrEqual: "#octolapse_printer_min_z" }, + octolapse_printer_snapshot_min_x: { lessThanOrEqual: "#octolapse_printer_snapshot_max_x" }, + octolapse_printer_snapshot_max_x: { greaterThanOrEqual: "#octolapse_printer_snapshot_min_x"}, + octolapse_printer_snapshot_min_y: { lessThanOrEqual: "#octolapse_printer_snapshot_max_y" }, + octolapse_printer_snapshot_max_y: { greaterThanOrEqual: "#octolapse_printer_snapshot_min_y" }, + octolapse_printer_snapshot_min_z: { lessThanOrEqual: "#octolapse_printer_snapshot_max_z" }, + octolapse_printer_snapshot_max_z: { greaterThanOrEqual: "#octolapse_printer_snapshot_min_z" }, + octolapse_printer_minimum_layer_height: { lessThanOrEqual: "#octolapse_printer_priming_height" }, + octolapse_printer_priming_height: { greaterThanOrEqual: "#octolapse_printer_minimum_layer_height" }, + octolapse_printer_auto_position_detection_commands: { csvString: true }, + octolapse_printer_snapshot_command: {octolapsePrinterSnapshotCommand: true}, + octolapse_other_slicer_retract_length: {required: true}, + octolapse_other_slicer_z_hop: {required: true}, + octolapse_other_slicer_vase_mode: {ifCheckedEnsureNonNull: ["#octolapse_other_slicer_layer_height"] }, + octolapse_other_slicer_layer_height: {ifOtherCheckedEnsureNonNull: '#octolapse_other_slicer_vase_mode'}, + octolapse_cura_smooth_spiralized_contours: {ifCheckedEnsureNonNull: ["#octolapse_cura_layer_height"] }, + octolapse_cura_layer_height: {ifOtherCheckedEnsureNonNull: '#octolapse_cura_smooth_spiralized_contours'}, + octolapse_cura_4_2_smooth_spiralized_contours: {ifCheckedEnsureNonNull: ["#octolapse_cura_4_2_layer_height"] }, + octolapse_cura_4_2_layer_height: {ifOtherCheckedEnsureNonNull: '#octolapse_cura_4_2_smooth_spiralized_contours'}, + octolapse_simplify_3d_vase_mode: {ifCheckedEnsureNonNull: ["#octolapse_simplify_3d_layer_height"] }, + octolapse_simplify_3d_layer_height: {ifOtherCheckedEnsureNonNull: '#octolapse_simplify_3d_vase_mode'}, + octolapse_slic3r_pe_spiral_vase: {ifCheckedEnsureNonNull: ["#octolapse_slic3r_pe_layer_height"] }, + octolapse_slic3r_pe_layer_height: {ifOtherCheckedEnsureNonNull: '#octolapse_slic3r_pe_spiral_vase'} }, messages: { - name: "Please enter a name for your profile", - min_x : { lessThanOrEqual: "Must be less than or equal to the 'X - Width Max' field." }, - max_x : { greaterThanOrEqual: "Must be greater than or equal to the ''X - Width Min'' field." }, - min_y : { lessThanOrEqual: "Must be less than or equal to the 'Y - Width Max' field." }, - max_y : { greaterThanOrEqual: "Must be greater than or equal to the ''Y - Width Min'' field." }, - min_z : { lessThanOrEqual: "Must be less than or equal to the 'Z - Width Max' field." }, - max_z: { greaterThanOrEqual: "Must be greater than or equal to the ''Z - Width Min'' field." }, - snapshot_min_x : { lessThanOrEqual: "Must be less than or equal to the ''Snapshot Max X'' field." }, - snapshot_max_x : { greaterThanOrEqual: "Must be greater than or equal to the ''Snapshot Min X'' field." }, - snapshot_min_y : { lessThanOrEqual: "Must be less than or equal to the 'Snapshot Max Y' field." }, - snapshot_max_y : { greaterThanOrEqual: "Must be greater than or equal to the ''Snapshot Min Y'' field." }, - snapshot_min_z : { lessThanOrEqual: "Must be less than or equal to the 'Snapshot Max Z' field." }, - snapshot_max_z: { greaterThanOrEqual: "Must be greater than or equal to the ''Snapshot Min Z'' field." }, - minimum_layer_height: { lessThanOrEqual: "Must be less than or equal to the ''Priming Height'' field." }, - priming_height: { greaterThanOrEqual: "Must be greater than or equal to the ''Minimum Layer Height'' field." }, - auto_position_detection_commands: { csvString:"Please enter a series of gcode commands (without parameters) separated by commas, or leave this field blank." }, - slicer_cura_smooth_spiralized_contours: {ifCheckedEnsureNonNull: "If vase mode is selected, you must enter a layer height."}, - slicer_other_vase_mode: {ifCheckedEnsureNonNull: "If vase mode is selected, you must enter a layer height."}, - slicer_simplify_3d_vase_mode: {ifCheckedEnsureNonNull: "If vase mode is selected, you must enter a layer height."}, - slicer_slic3r_pe_vase_mode: {ifCheckedEnsureNonNull: "If vase mode is selected, you must enter a layer height."}, - slicer_cura_layer_height: {ifOtherCheckedEnsureNonNull: 'Vase mode is selected, you must enter a layer height.'}, - slicer_other_slicer_layer_height: {ifOtherCheckedEnsureNonNull: 'Vase mode is selected, you must enter a layer height.'}, - slicer_simplify_3d_layer_height: {ifOtherCheckedEnsureNonNull: 'Vase mode is selected, you must enter a layer height.'}, - slicer_slic3r_pe_layer_height: {ifOtherCheckedEnsureNonNull: 'Vase mode is selected, you must enter a layer height.'} + octolapse_printer_name: "Please enter a name for your profile", + octolapse_printer_min_x : { lessThanOrEqual: "Must be less than or equal to the 'X - Width Max' field." }, + octolapse_printer_max_x : { greaterThanOrEqual: "Must be greater than or equal to the ''X - Width Min'' field." }, + octolapse_printer_min_y : { lessThanOrEqual: "Must be less than or equal to the 'Y - Width Max' field." }, + octolapse_printer_max_y : { greaterThanOrEqual: "Must be greater than or equal to the ''Y - Width Min'' field." }, + octolapse_printer_min_z : { lessThanOrEqual: "Must be less than or equal to the 'Z - Width Max' field." }, + octolapse_printer_max_z: { greaterThanOrEqual: "Must be greater than or equal to the ''Z - Width Min'' field." }, + octolapse_printer_snapshot_min_x : { lessThanOrEqual: "Must be less than or equal to the ''Snapshot Max X'' field." }, + octolapse_printer_snapshot_max_x : { greaterThanOrEqual: "Must be greater than or equal to the ''Snapshot Min X'' field." }, + octolapse_printer_snapshot_min_y : { lessThanOrEqual: "Must be less than or equal to the 'Snapshot Max Y' field." }, + octolapse_printer_snapshot_max_y : { greaterThanOrEqual: "Must be greater than or equal to the ''Snapshot Min Y'' field." }, + octolapse_printer_snapshot_min_z : { lessThanOrEqual: "Must be less than or equal to the 'Snapshot Max Z' field." }, + octolapse_printer_snapshot_max_z: { greaterThanOrEqual: "Must be greater than or equal to the ''Snapshot Min Z'' field." }, + octolapse_printer_minimum_layer_height: { lessThanOrEqual: "Must be less than or equal to the ''Priming Height'' field." }, + octolapse_printer_priming_height: { greaterThanOrEqual: "Must be greater than or equal to the ''Minimum Layer Height'' field." }, + octolapse_printer_auto_position_detection_commands: { csvString:"Please enter a series of gcode commands (without parameters) separated by commas, or leave this field blank." }, + octolapse_cura_smooth_spiralized_contours: {ifCheckedEnsureNonNull: "If vase mode is selected, you must enter a layer height."}, + octolapse_cura_layer_height: {ifOtherCheckedEnsureNonNull: 'Vase mode is selected, you must enter a layer height.'}, + octolapse_cura_4_2_smooth_spiralized_contours: {ifCheckedEnsureNonNull: "If vase mode is selected, you must enter a layer height."}, + octolapse_cura_4_2_layer_height: {ifOtherCheckedEnsureNonNull: 'Vase mode is selected, you must enter a layer height.'}, + octolapse_other_slicer_vase_mode: {ifCheckedEnsureNonNull: "If vase mode is selected, you must enter a layer height."}, + octolapse_other_slicer_layer_height: {ifOtherCheckedEnsureNonNull: 'Vase mode is selected, you must enter a layer height.'}, + octolapse_simplify_3d_vase_mode: {ifCheckedEnsureNonNull: "If vase mode is selected, you must enter a layer height."}, + octolapse_simplify_3d_layer_height: {ifOtherCheckedEnsureNonNull: 'Vase mode is selected, you must enter a layer height.'}, + octolapse_slic3r_pe_vase_mode: {ifCheckedEnsureNonNull: "If vase mode is selected, you must enter a layer height."}, + octolapse_slic3r_pe_layer_height: {ifOtherCheckedEnsureNonNull: 'Vase mode is selected, you must enter a layer height.'} } }; - Octolapse.OctolapseGcodeSettings = function(values){ + Octolapse.ExtruderOffset = function(values) + { var self = this; - self.retraction_length = ko.observable(values.retraction_length); - self.retraction_speed = ko.observable(values.retraction_speed); - self.deretraction_speed = ko.observable(values.deretraction_speed); - self.x_y_travel_speed = ko.observable(values.x_y_travel_speed); - self.z_lift_height = ko.observable(values.z_lift_height); - self.z_lift_speed = ko.observable(values.z_lift_speed); + self.x = ko.observable(0); + self.y = ko.observable(0); + if (values) + { + self.x(values.x); + self.y(values.y); + } }; - Octolapse.Slicers = function(values) { + Octolapse.Slicers = function(values, num_extruders_observable) { //console.log("Creating Slicers"); var self = this; - self.cura = new Octolapse.CuraViewmodel(values.cura); - self.other = new Octolapse.OtherSlicerViewModel(values.other); - self.simplify_3d = new Octolapse.Simplify3dViewModel(values.simplify_3d); - self.slic3r_pe = new Octolapse.Slic3rPeViewModel(values.slic3r_pe); + self.cura = new Octolapse.CuraViewmodel(values.cura, num_extruders_observable); + self.other = new Octolapse.OtherSlicerViewModel(values.other, num_extruders_observable); + self.simplify_3d = new Octolapse.Simplify3dViewModel(values.simplify_3d, num_extruders_observable); + self.slic3r_pe = new Octolapse.Slic3rPeViewModel(values.slic3r_pe, num_extruders_observable); + }; Octolapse.PrinterProfileViewModel = function (values) { @@ -104,18 +112,21 @@ $(function() { self.guid = ko.observable(values.guid); self.name = ko.observable(values.name); self.description = ko.observable(values.description); - /* - self.automatic_configuration = new Octolapse.AutomaticPrinterConfigurationViewModel( - values.automatic_configuration, self - ); - */ - - self.gcode_generation_settings = new Octolapse.OctolapseGcodeSettings(values.gcode_generation_settings); - self.slicers = new Octolapse.Slicers(values.slicers); + self.num_extruders = ko.observable(values.num_extruders); + self.shared_extruder = ko.observable(values.shared_extruder); + self.extruder_offsets = ko.observableArray([]); + for(var index = 0; index < values.extruder_offsets.length; index++) + { + var offset = new Octolapse.ExtruderOffset(values.extruder_offsets[index]); + self.extruder_offsets.push(offset); + } + self.default_extruder = ko.observable(values.default_extruder); + self.zero_based_extruder = ko.observable(values.zero_based_extruder); + self.slicers = new Octolapse.Slicers(values.slicers, self.num_extruders); // has_been_saved_by_user profile setting, computed and always returns true // This will switch has_been_saved_by_user from false to true // after any user save - self.has_been_saved_by_user = ko.observable(values.has_been_saved_by_user); + self.has_been_saved_by_user = ko.observable(true); self.slicer_type = ko.observable(values.slicer_type); self.snapshot_command = ko.observable(values.snapshot_command); self.auto_detect_position = ko.observable(values.auto_detect_position); @@ -124,7 +135,6 @@ $(function() { self.home_x = ko.observable(values.home_x); self.home_y = ko.observable(values.home_y); self.home_z = ko.observable(values.home_z); - self.abort_out_of_bounds = ko.observable(values.abort_out_of_bounds); self.override_octoprint_profile_settings = ko.observable(values.override_octoprint_profile_settings); self.bed_type = ko.observable(values.bed_type); self.diameter_xy = ko.observable(values.diameter_xy); @@ -161,6 +171,34 @@ $(function() { self.gocde_axis_compatibility_mode_enabled = ko.observable(values.gocde_axis_compatibility_mode_enabled); self.home_axis_gcode = ko.observable(values.home_axis_gcode); + self.dialog = null; + + self.num_extruders.subscribe(function() { + var num_extruders = self.num_extruders(); + if (num_extruders < 1) { + num_extruders = 1; + } + else if (num_extruders > 16){ + num_extruders = 16; + } + + var has_changed = false; + while(self.extruder_offsets().length < num_extruders) + { + has_changed = true; + self.extruder_offsets.push(new Octolapse.ExtruderOffset()); + } + while(self.extruder_offsets().length > num_extruders) + { + has_changed = true; + self.extruder_offsets.pop(); + } + if (has_changed) { + self.dialog.bind_validation(); + self.dialog.bind_help_links(); + } + }); + self.origin_type_options = ko.pureComputed(function(){ var options = []; if (self.bed_type() == 'circular') @@ -185,6 +223,10 @@ $(function() { self.name(server_profile.name); self.description(server_profile.description); self.has_been_saved_by_user(true); + self.num_extruders(server_profile.num_extruders); + self.shared_extruder(server_profile.shared_extruder); + self.extruder_offsets(server_profile.extruder_offsets); + self.zero_based_extruder(server_profile.zero_based_extruder); self.snapshot_command(server_profile.snapshot_command); self.auto_detect_position(server_profile.auto_detect_position); self.auto_position_detection_commands(server_profile.auto_position_detection_commands); @@ -192,7 +234,6 @@ $(function() { self.home_x(server_profile.home_x); self.home_y(server_profile.home_y); self.home_z(server_profile.home_z); - self.abort_out_of_bounds(server_profile.abort_out_of_bounds); self.override_octoprint_profile_settings(server_profile.override_octoprint_profile_settings); self.bed_type(server_profile.bed_type); self.diameter_xy(server_profile.diameter_xy); @@ -228,6 +269,10 @@ $(function() { self.home_axis_gcode(server_profile.home_axis_gcode); }; + self.on_opened = function(dialog) + { + self.dialog = dialog; + }; self.on_closed = function() { self.automatic_configuration.on_closed(); @@ -235,9 +280,10 @@ $(function() { self.toJS = function() { - // need to remove the parent link from the automatic configuration to prevent a cyclic copy + var dialog = self.dialog; + self.dialog = null; var copy = ko.toJS(self); - delete copy.helpers; + self.dialog = dialog; return copy; }; diff --git a/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.cura.js b/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.cura.js index 43bc99b3..a7003a87 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.cura.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.cura.js @@ -1,19 +1,69 @@ -Octolapse.CuraViewmodel = function (values) { - var self = this; +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ +Octolapse.CuraExtruderViewModel = function (values, extruder_index) { + var self=this; + self.index = extruder_index; + self.speed_z_hop = ko.observable(null); + self.max_feedrate_z_override = ko.observable(null); + self.retraction_amount = ko.observable(null); + self.retraction_hop = ko.observable(null); + self.retraction_hop_enabled = ko.observable(false); + self.retraction_enable = ko.observable(false); + self.retraction_speed = ko.observable(null); + self.retraction_retract_speed = ko.observable(null); + self.retraction_prime_speed = ko.observable(null); + self.speed_travel = ko.observable(null); + + if (values && values.extruders.length > self.index) { + var extruder = values.extruders[self.index]; + if (!extruder) + return; + self.speed_z_hop(extruder.speed_z_hop); + self.max_feedrate_z_override(extruder.max_feedrate_z_override); + self.retraction_amount(extruder.retraction_amount); + self.retraction_hop(extruder.retraction_hop); + self.retraction_hop_enabled(extruder.retraction_hop_enabled || false); + self.retraction_enable(extruder.retraction_enable || false); + self.retraction_speed(extruder.retraction_speed); + self.retraction_retract_speed(extruder.retraction_retract_speed); + self.retraction_prime_speed(extruder.retraction_prime_speed); + self.speed_travel(extruder.speed_travel); + } +}; - // Create Observables - self.retraction_amount = ko.observable(values.retraction_amount); - self.retraction_retract_speed = ko.observable(values.retraction_retract_speed); - self.retraction_prime_speed = ko.observable(values.retraction_prime_speed); - self.speed_travel = ko.observable(values.speed_travel); - self.max_feedrate_z_override = ko.observable(values.max_feedrate_z_override); - self.speed_z_hop = ko.observable(values.speed_z_hop); - self.retraction_hop = ko.observable(values.retraction_hop); - self.retraction_hop_enabled = ko.observable(values.retraction_hop_enabled); - self.retraction_enable = ko.observable(values.retraction_enable); +Octolapse.CuraViewmodel = function (values, num_extruders_observable) { + var self = this; + // Observables + self.num_extruders_observable = num_extruders_observable; + var extruders = []; + for (var index = 0; index < self.num_extruders_observable(); index++) + { + extruders.push(new Octolapse.CuraExtruderViewModel(values, index)); + } + self.extruders = ko.observableArray(extruders); self.layer_height = ko.observable(values.layer_height); - self.smooth_spiralized_contours = ko.observable(values.smooth_spiralized_contours); - self.magic_mesh_surface_mode = ko.observable(values.magic_mesh_surface_mode); + self.smooth_spiralized_contours = ko.observable(values.smooth_spiralized_contours || false); // Create constants self.speed_tolerance = 0.1 / 60.0 / 2.0; self.round_to_increment_mm_min = 0.00000166667; @@ -21,13 +71,26 @@ Octolapse.CuraViewmodel = function (values) { self.round_to_increment_length = 0.0001; self.round_to_increment_num_layers = 1; - self.get_all_speed_settings = function() - { - return [ - self.retraction_retract_speed, - self.retraction_prime_speed, - self.speed_travel, - self.max_feedrate_z_override, - ] - } + self.num_extruders_observable.subscribe(function() { + var num_extruders = self.num_extruders_observable(); + if (num_extruders < 1) { + num_extruders = 1; + } + else if (num_extruders > 16){ + num_extruders = 16; + } + + var extruders = self.extruders(); + while(extruders.length < num_extruders) + { + var new_extruder = new Octolapse.CuraExtruderViewModel(null, extruders.length-1); + extruders.push(new_extruder); + } + while(extruders.length > num_extruders) + { + extruders.pop(); + } + self.extruders(extruders); + + }); }; diff --git a/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.other.js b/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.other.js index 51e5aa30..7123e0d8 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.other.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.other.js @@ -1,46 +1,99 @@ -Octolapse.OtherSlicerViewModel = function (values) { - var self = this; - // Create Observables - self.retract_length = ko.observable(values.retract_length); - self.z_hop = ko.observable(values.z_hop); - self.travel_speed = ko.observable(values.travel_speed); - self.retract_speed = ko.observable(values.retract_speed); - self.deretract_speed = ko.observable(values.deretract_speed); - self.z_travel_speed = ko.observable(values.z_travel_speed); - self.speed_tolerance = ko.observable(values.speed_tolerance); - self.vase_mode = ko.observable(values.vase_mode); - self.layer_height = ko.observable(values.layer_height); - self.axis_speed_display_units = ko.observable(values.axis_speed_display_units); +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ +Octolapse.OtherSlicerExtruderViewModel = function (values, extruder_index) { + var self=this; + self.index = extruder_index; + self.retract_length = ko.observable(null); + self.z_hop = ko.observable(null); + self.retract_speed = ko.observable(null); + self.deretract_speed = ko.observable(null); + self.travel_speed = ko.observable(null); + self.z_travel_speed = ko.observable(null); + self.retract_before_move = ko.observable(false); + + if (values && values.extruders.length > self.index) { + var extruder = values.extruders[self.index]; + if (!extruder) + return; + self.retract_length(extruder.retract_length); + self.z_hop(extruder.z_hop); + self.retract_speed(extruder.retract_speed); + self.deretract_speed(extruder.deretract_speed); + self.travel_speed(extruder.travel_speed); + self.z_travel_speed(extruder.z_travel_speed); + self.retract_before_move(extruder.retract_before_move || false); + } - self.retract_before_move = ko.pureComputed(function () { - if (self.retract_length() != null && self.retract_length() > 0) - return true; - return false; - }, self); self.lift_when_retracted = ko.pureComputed(function () { if (self.retract_before_move() && self.z_hop() != null && self.z_hop() > 0) return true; return false; }, self); +}; - self.get_all_speed_settings = function() +Octolapse.OtherSlicerViewModel = function (values, num_extruders_observable) { + var self = this; + // Observables + self.num_extruders_observable = num_extruders_observable; + var extruders = []; + for (var index = 0; index < self.num_extruders_observable(); index++) { - return [ - self.travel_speed, - self.retract_speed, - self.deretract_speed, - self.z_travel_speed, - self.speed_tolerance, - self.axis_speed_display_units - ] - }; + extruders.push(new Octolapse.OtherSlicerExtruderViewModel(values, index)); + } + self.extruders = ko.observableArray(extruders); + self.speed_tolerance = ko.observable(values.speed_tolerance); + self.vase_mode = ko.observable(values.vase_mode || false); + self.layer_height = ko.observable(values.layer_height); + self.axis_speed_display_units = ko.observable(values.axis_speed_display_units); // Declare constants self.round_to_increment_mm_min = 0.001; self.round_to_increment_mm_sec = 0.000001; + self.num_extruders_observable.subscribe(function() { + var num_extruders = self.num_extruders_observable(); + if (num_extruders < 1) { + num_extruders = 1; + } + else if (num_extruders > 16){ + num_extruders = 16; + } + var extruders = self.extruders(); + while(extruders.length < num_extruders) + { + var new_extruder = new Octolapse.OtherSlicerExtruderViewModel(null, extruders.length-1); + extruders.push(new_extruder); + } + while(extruders.length > num_extruders) + { + extruders.pop(); + } + self.extruders(extruders); + }); + // get the time component of the axis speed units (min/mm) - self.getAxisSpeedTimeUnit = ko.pureComputed(function () { + self.getAxisSpeedTimeUnit = ko.computed(function () { if (self.axis_speed_display_units() === "mm-min") return 'min'; if (self.axis_speed_display_units() === "mm-sec") @@ -48,19 +101,16 @@ Octolapse.OtherSlicerViewModel = function (values) { return '?'; }, self); - - self.axisSpeedDisplayUnitsChanged = function (obj, event) { if (Octolapse.Globals.is_admin()) { if (event.originalEvent) { // Get the current guid - var newUnit = $("#octolapse_axis_speed_display_unit_options").val(); + var newUnit = $("#octolapse_other_slicer_axis_speed_display_unit_options").val(); var previousUnit = self.axis_speed_display_units(); if (newUnit === previousUnit) { //console.log("Axis speed display units, no change detected!") return false; - } //console.log("Changing axis speed from " + previousUnit + " to " + newUnit) // in case we want to have more units in the future, check all cases @@ -69,11 +119,15 @@ Octolapse.OtherSlicerViewModel = function (values) { var axis_speed_round_to_increment = 0.000001; var axis_speed_round_to_unit = 'mm-sec'; self.speed_tolerance(Octolapse.convertAxisSpeedUnit(self.speed_tolerance(), newUnit, previousUnit, axis_speed_round_to_increment, axis_speed_round_to_unit)); - self.retract_speed(Octolapse.convertAxisSpeedUnit(self.retract_speed(), newUnit, previousUnit, self.round_to_increment_mm_min, previousUnit)); - self.deretract_speed(Octolapse.convertAxisSpeedUnit(self.deretract_speed(), newUnit, previousUnit, self.round_to_increment_mm_min, previousUnit)); - self.travel_speed(Octolapse.convertAxisSpeedUnit(self.travel_speed(), newUnit, previousUnit, self.round_to_increment_mm_min, previousUnit)); - self.z_travel_speed(Octolapse.convertAxisSpeedUnit(self.z_travel_speed(), newUnit, previousUnit, self.round_to_increment_mm_min, previousUnit)); - // Optional values + for (var i=0; i < self.extruders().length; i++) { + var extruder = self.extruders()[i]; + extruder.retract_speed(Octolapse.convertAxisSpeedUnit(extruder.retract_speed(), newUnit, previousUnit, self.round_to_increment_mm_min, previousUnit)); + extruder.deretract_speed(Octolapse.convertAxisSpeedUnit(extruder.deretract_speed(), newUnit, previousUnit, self.round_to_increment_mm_min, previousUnit)); + extruder.travel_speed(Octolapse.convertAxisSpeedUnit(extruder.travel_speed(), newUnit, previousUnit, self.round_to_increment_mm_min, previousUnit)); + extruder.z_travel_speed(Octolapse.convertAxisSpeedUnit(extruder.z_travel_speed(), newUnit, previousUnit, self.round_to_increment_mm_min, previousUnit)); + // Optional values + } + self.axis_speed_display_units(newUnit); return true; } } diff --git a/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.simplify_3d.js b/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.simplify_3d.js index c5809c78..637dbb55 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.simplify_3d.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.simplify_3d.js @@ -1,29 +1,85 @@ -Octolapse.Simplify3dViewModel = function (values) { - var self = this; +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ +Octolapse.Simplify3dExtruderViewModel = function (values, extruder_index) { + var self=this; + self.index = extruder_index; + self.retraction_distance = ko.observable(null); + self.retraction_vertical_lift = ko.observable(null); + self.retraction_speed = ko.observable(null); + self.extruder_use_retract = ko.observable(false); + + if (values && values.extruders.length > self.index) { + var extruder = values.extruders[self.index]; + if (!extruder) + return; + self.retraction_distance(extruder.retraction_distance); + self.retraction_vertical_lift(extruder.retraction_vertical_lift); + self.retraction_speed(extruder.retraction_speed); + self.extruder_use_retract(extruder.extruder_use_retract || false); + } +}; +Octolapse.Simplify3dViewModel = function (values, num_extruders_observable) { + var self = this; // Observables - self.retraction_distance = ko.observable(values.retraction_distance); - self.retraction_vertical_lift = ko.observable(values.retraction_vertical_lift); - self.retraction_speed = ko.observable(values.retraction_speed); + self.num_extruders_observable = num_extruders_observable; + var extruders = []; + for (var index = 0; index < self.num_extruders_observable(); index++) + { + extruders.push(new Octolapse.Simplify3dExtruderViewModel(values, index)); + } + self.extruders = ko.observableArray(extruders); self.x_y_axis_movement_speed = ko.observable(values.x_y_axis_movement_speed); self.z_axis_movement_speed = ko.observable(values.z_axis_movement_speed); - self.extruder_use_retract = ko.observable(values.extruder_use_retract); - self.spiral_vase_mode = ko.observable(values.spiral_vase_mode); + self.spiral_vase_mode = ko.observable(values.spiral_vase_mode || false); self.layer_height = ko.observable(values.layer_height); - self.get_all_speed_settings = function() - { - return [ - self.retraction_speed, - self.x_y_axis_movement_speed, - self.z_axis_movement_speed, - ] - }; // Constants - self.speed_tolerance = values.speed_tolerance - self.axis_speed_display_settings = 'mm-min' + self.speed_tolerance = values.speed_tolerance; + self.axis_speed_display_settings = 'mm-min'; self.round_to_increment_percent = 1; self.round_to_increment_speed_mm_min = 0.1; self.round_to_increment_length = 0.01; self.percent_value_default = 100.0; + self.num_extruders_observable.subscribe(function() { + var num_extruders = self.num_extruders_observable(); + if (num_extruders < 1) { + num_extruders = 1; + } + else if (num_extruders > 16){ + num_extruders = 16; + } + var extruders = self.extruders(); + while(extruders.length < num_extruders) + { + var new_extruder = new Octolapse.Simplify3dExtruderViewModel(null, extruders.length-1); + extruders.push(new_extruder); + } + while(extruders.length > num_extruders) + { + extruders.pop(); + } + self.extruders(extruders); + }); }; diff --git a/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.slic3r_pe.js b/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.slic3r_pe.js index 64cdfab8..7177434c 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.slic3r_pe.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.printer.slicer.slic3r_pe.js @@ -1,20 +1,60 @@ -Octolapse.Slic3rPeViewModel = function (values) { - var self = this; +/* +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## +*/ +Octolapse.Slic3rPeExtruderViewModel = function (values, extruder_index) { + var self=this; + self.index = extruder_index; + self.retract_length = ko.observable(null); + self.retract_lift = ko.observable(null); + self.retract_speed = ko.observable(null); + self.deretract_speed = ko.observable(null); + + if (values && values.extruders.length > self.index) { + var extruder = values.extruders[self.index]; + if (!extruder) + return; + self.retract_length(extruder.retract_length); + self.retract_lift(extruder.retract_lift); + self.retract_speed(extruder.retract_speed); + self.deretract_speed(extruder.deretract_speed); + } +}; + +Octolapse.Slic3rPeViewModel = function (values, num_extruders_observable) { + var self = this; // Observables - self.retract_length = ko.observable(values.retract_length); - self.retract_lift = ko.observable(values.retract_lift); - self.retract_speed = ko.observable(values.retract_speed); - self.deretract_speed = ko.observable(values.deretract_speed); + + self.num_extruders_observable = num_extruders_observable; + var extruders = []; + for (var index = 0; index < self.num_extruders_observable(); index++) + { + extruders.push(new Octolapse.Slic3rPeExtruderViewModel(values, index)); + } + self.extruders = ko.observableArray(extruders); self.travel_speed = ko.observable(values.travel_speed); self.layer_height = ko.observable(values.layer_height); - self.spiral_vase = ko.observable(values.spiral_vase); - self.retract_before_travel = ko.pureComputed(function () { - if (self.retract_length() != null && self.retract_length() > 0) - return true; - return false; - }, self); - + self.spiral_vase = ko.observable(values.spiral_vase || false); // Constants self.speed_tolerance = 0.01 / 60.0 / 2.0; self.axis_speed_display_units = 'mm-sec'; @@ -25,4 +65,25 @@ Octolapse.Slic3rPeViewModel = function (values) { self.round_to_increment_retraction_length = 0.000001; self.round_to_increment_lift_z = 0.0001; + self.num_extruders_observable.subscribe(function() { + var num_extruders = self.num_extruders_observable(); + if (num_extruders < 1) { + num_extruders = 1; + } + else if (num_extruders > 16){ + num_extruders = 16; + } + var extruders = self.extruders(); + while(extruders.length < num_extruders) + { + var new_extruder = new Octolapse.Slic3rPeExtruderViewModel(null, extruders.length-1); + extruders.push(new_extruder); + } + while(extruders.length > num_extruders) + { + extruders.pop(); + } + self.extruders(extruders); + + }); }; diff --git a/octoprint_octolapse/static/js/octolapse.profiles.rendering.js b/octoprint_octolapse/static/js/octolapse.profiles.rendering.js index 37e798df..3706b73e 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.rendering.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.rendering.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -60,8 +60,8 @@ $(function() { self.max_fps = ko.observable(values.max_fps); self.min_fps = ko.observable(values.min_fps); self.output_format = ko.observable(values.output_format); - self.sync_with_timelapse = ko.observable(values.sync_with_timelapse); self.bitrate = ko.observable(values.bitrate); + self.constant_rate_factor = ko.observable(values.constant_rate_factor); self.post_roll_seconds = ko.observable(values.post_roll_seconds); self.pre_roll_seconds = ko.observable(values.pre_roll_seconds); self.output_template = ko.observable(values.output_template); @@ -71,12 +71,8 @@ $(function() { self.overlay_text_template = ko.observable(values.overlay_text_template); self.overlay_font_path = ko.observable(values.overlay_font_path); self.overlay_font_size = ko.observable(values.overlay_font_size); - self.cleanup_after_render_complete = ko.observable(values.cleanup_after_render_complete); - self.cleanup_after_render_fail = ko.observable(values.cleanup_after_render_fail); + self.archive_snapshots = ko.observable(values.archive_snapshots); self.thread_count = ko.observable(values.thread_count); - self.snapshots_to_skip_beginning = ko.observable(values.snapshots_to_skip_beginning); - self.snapshot_to_skip_end = ko.observable(values.snapshot_to_skip_end); - self.data.font_list = ko.observableArray(); // A list of Fonts that are available for selection on the server. // Text position as a JSON string. self.overlay_text_pos = ko.pureComputed({ @@ -96,7 +92,7 @@ $(function() { return; } try { - positions = JSON.parse(value) + var positions = JSON.parse(value); self.overlay_text_pos_x(positions[0]); self.overlay_text_pos_y(positions[1]); } @@ -132,7 +128,7 @@ $(function() { rgba = JSON.parse(text_color); } // Build the correct string. - return 'rgba(' + rgba.join(', ') + ')' + return 'rgba(' + rgba.join(', ') + ')'; }; self.css_to_text_color = function(css){ @@ -171,19 +167,15 @@ $(function() { }); self.overlay_preview_image_alt_text = ko.computed(function() { if (self.data.overlay_preview_image_error.length == 0) { - return 'A preview of the overlay text.' + return 'A preview of the overlay text.'; } return 'Image could not be retrieved from server. The error returned was: ' + self.data.overlay_preview_image_error() + '.'; }); - self.data.can_synchronize_format = ko.pureComputed(function() { - return ['mp4','h264'].indexOf(self.output_format()) > -1; - }); - // This function is called when the Edit Profile dialog shows. self.onShow = function(parent) { - $('#rendering_profile_overlay_color').minicolors({format: 'rgb', opacity: true}); - $('#rendering_profile_outline_color').minicolors({format: 'rgb', opacity: true}); + $('#octolapse_rendering_overlay_color').minicolors({format: 'rgb', opacity: true}); + $('#octolapse_rendering_outline_color').minicolors({format: 'rgb', opacity: true}); self.updateWatermarkList(); self.updateFontList(); self.initWatermarkUploadButton(); @@ -224,22 +216,22 @@ $(function() { return OctoPrint.get(OctoPrint.getBlueprintUrl('octolapse') + 'rendering/watermark') .then(function(response) { - self.watermark_list.removeAll() - // The let format is not working in some versions of safari + var watermarks = []; for (var index = 0; index < response['filepaths'].length;index++) { - self.watermark_list.push(new WatermarkImage(response['filepaths'][index])); + watermarks.push(new WatermarkImage(response['filepaths'][index])); } + self.watermark_list(watermarks); }, function(response) { - self.watermark_list.removeAll() + var watermarks = [new WatermarkImage("Failed to load watermarks from Octolapse data directory.")]; // Hacky solution, but good enough. We shouldn't encounter this error too much anyways. - self.watermark_list.push(new WatermarkImage("Failed to load watermarks from Octolapse data directory.")); + self.watermark_list(watermarks); }); }; self.initWatermarkUploadButton = function() { // Set up the file upload button. - var $watermarkUploadElement = $('#octolapse_watermark_path_upload'); - var $progressBarContainer = $('#octolapse-upload-watermark-progress'); + var $watermarkUploadElement = $('#octolapse_rendering_watermark_path_upload'); + var $progressBarContainer = $('#octolapse_rendering_upload_watermark_progress'); var $progressBar = $progressBarContainer.find('.progress-bar'); $watermarkUploadElement.fileupload({ @@ -250,6 +242,10 @@ $(function() { // TODO: Monitor issue with chunking and re-add when it is fixed. //maxChunkSize: 1000000, maxFilesize: 10, + start: function(e) { + $progressBar.text("Starting..."); + $progressBar.animate({'width': '0'}, {'queue': false}).removeClass('failed'); + }, progressall: function (e, data) { // TODO: Get a better progress bar implementation. var progress = parseInt(data.loaded / data.total * 100, 10); @@ -271,7 +267,7 @@ $(function() { //var matchingWatermarks = self.watermark_list().filter(w=>w.getFilename() == data.files[0].name); if (matchingWatermarks.length == 0) { //console.log("Error: No matching watermarks found!"); - return + return; } if (matchingWatermarks > 1){ //console.log("Error: More than one matching watermark found! Selecting best guess."); @@ -290,11 +286,14 @@ $(function() { self.updateFontList = function() { return OctoPrint.get(OctoPrint.getBlueprintUrl('octolapse') + 'rendering/font') .then(function(response) { - self.data.font_list.removeAll(); + var server_fonts = response.fonts; + var font_list = []; + // The let expression was not working in safari - for (var index = 0; index< response.length; index++) { - self.data.font_list.push(new Font(response[index])); + for (var index = 0; index< server_fonts.length; index++) { + font_list.push(new Font(server_fonts[index])); } + self.data.font_list(font_list); }, function(response) { // Failed to load any fonts. self.data.font_list.removeAll(); @@ -308,6 +307,23 @@ $(function() { // Request a preview of the overlay from the server. self.requestOverlayPreview = function() { + if (!self.overlay_text_template()) + { + self.data.overlay_preview_image(''); + self.data.overlay_preview_image_error( + "Enter the text you wish to appear in your overlay in the 'Text' box above, and click refresh to preview the rendering overlay." + ); + return; + } + if (self.overlay_font_path() === "") + { + self.data.overlay_preview_image(''); + self.data.overlay_preview_image_error( + "Choose a font from the list above, and click refresh to preview the rendering overlay." + ); + return; + } + var data = { 'overlay_text_template': self.overlay_text_template(), 'overlay_font_path': self.overlay_font_path(), @@ -345,9 +361,8 @@ $(function() { var copy = ko.toJS(self); return copy; }; - + self.updateFromServer = function(values) { - self.guid(values.guid); self.name(values.name); self.description(values.description); self.enabled(values.enabled); @@ -357,14 +372,21 @@ $(function() { self.max_fps(values.max_fps); self.min_fps(values.min_fps); self.output_format(values.output_format); - self.sync_with_timelapse(values.sync_with_timelapse); self.bitrate(values.bitrate); + // Might not be included in server profiles. Make sure it is. + if (typeof values.constant_rate_factor !== 'undefined') + self.constant_rate_factor(values.constant_rate_factor); self.post_roll_seconds(values.post_roll_seconds); self.pre_roll_seconds(values.pre_roll_seconds); self.output_template(values.output_template); - self.cleanup_after_render_complete(values.cleanup_after_render_complete); - self.cleanup_after_render_fail(values.cleanup_after_render_fail); - self.thread_count(values.thread_count); + self.archive_snapshots(values.archive_snapshots); + self.thread_count(values.thread_count); + // Clear any settings that we don't want to update, unless they aren't important. + self.overlay_text_template(""); + self.selected_watermark(""); + self.enable_watermark(false); + self.overlay_font_path(""); + }; self.automatic_configuration = new Octolapse.ProfileLibraryViewModel( @@ -395,36 +417,37 @@ $(function() { }; Octolapse.RenderingProfileValidationRules = { rules: { - bitrate: { required: true, ffmpegBitRate: true }, - output_format : {required: true}, - fps_calculation_type: {required: true}, - min_fps: { lessThanOrEqual: '#rendering_profile_max_fps' }, - max_fps: { greaterThanOrEqual: '#rendering_profile_min_fps' }, - overlay_text_valign: {required: true}, - overlay_text_halign: {required: true}, - overlay_text_alignment: {required: true}, - output_template: { + octolapse_rendering_bitrate: { required: true, ffmpegBitRate: true }, + octolapse_rendering_output_format : {required: true}, + octolapse_rendering_fps_calculation_type: {required: true}, + octolapse_rendering_min_fps: { lessThanOrEqual: '#octolapse_rendering_max_fps' }, + octolapse_rendering_max_fps: { greaterThanOrEqual: '#octolapse_rendering_min_fps' }, + octolapse_rendering_overlay_text_valign: {required: true}, + octolapse_rendering_overlay_text_halign: {required: true}, + octolapse_rendering_overlay_text_alignment: {required: true}, + octolapse_rendering_output_template: { remote: { url: "./plugin/octolapse/validateRenderingTemplate", type:"post" } }, - overlay_text_template: { + octolapse_rendering_overlay_text_template: { remote: { url: "./plugin/octolapse/validateOverlayTextTemplate", type:"post" } + }, - octolapse_overlay_font_size: { required: true, integerPositive: true }, - octolapse_overlay_text_pos: { required: true }, + octolapse_rendering_overlay_font_size: { required: true, integerPositive: true }, + octolapse_rendering_overlay_text_pos: { required: true }, }, messages: { - name: "Please enter a name for your profile", - min_fps: { lessThanOrEqual: 'Must be less than or equal to the maximum fps.' }, - max_fps: { greaterThanOrEqual: 'Must be greater than or equal to the minimum fps.' }, - output_template: { octolapseRenderingTemplate: 'Either there is an invalid token in the rendering template, or the resulting file name is not valid.' }, - overlay_text_template: { octolapseOverlayTextTemplate: 'Either there is an invalid token in the overlay text template, or the resulting file name is not valid.' }, - octolapse_overlay_text_pos: { required: 'Position offsets must be valid integers.' }, + octolapse_rendering_name: "Please enter a name for your profile", + octolapse_rendering_min_fps: { lessThanOrEqual: 'Must be less than or equal to the maximum fps.' }, + octolapse_rendering_max_fps: { greaterThanOrEqual: 'Must be greater than or equal to the minimum fps.' }, + octolapse_rendering_output_template: { octolapseRenderingTemplate: 'Either there is an invalid token in the rendering template, or the resulting file name is not valid.' }, + octolapse_rendering_overlay_text_template: { octolapseOverlayTextTemplate: 'Either there is an invalid token in the overlay text template, or the resulting file name is not valid.' }, + octolapse_rendering_overlay_text_pos: { required: 'Position offsets must be valid integers.' }, } }; }); diff --git a/octoprint_octolapse/static/js/octolapse.profiles.stabilization.js b/octoprint_octolapse/static/js/octolapse.profiles.stabilization.js index 3a542ed5..1feb4fbc 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.stabilization.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.stabilization.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -48,9 +48,9 @@ $(function () { self.y_relative_path = ko.observable(values.y_relative_path); self.y_relative_path_loop = ko.observable(values.y_relative_path_loop); self.y_relative_path_invert_loop = ko.observable(values.y_relative_path_invert_loop); + self.wait_for_moves_to_finish = ko.observable(values.wait_for_moves_to_finish); self.updateFromServer = function(values) { - self.guid(values.guid); self.name(values.name); self.description(values.description); self.x_type(values.x_type); @@ -73,6 +73,9 @@ $(function () { self.y_relative_path(values.y_relative_path); self.y_relative_path_loop(values.y_relative_path_loop); self.y_relative_path_invert_loop(values.y_relative_path_invert_loop); + if (typeof values.wait_for_moves_to_finish !== 'undefined') { + self.wait_for_moves_to_finish(values.wait_for_moves_to_finish); + } }; self.automatic_configuration = new Octolapse.ProfileLibraryViewModel( @@ -106,23 +109,21 @@ $(function () { Octolapse.StabilizationProfileValidationRules = { rules: { - name: "required" - , stabilization_type: "required" - , x_type: "required" - , x_fixed_coordinate: {number: true, required: true} - , x_fixed_path: {required: true, csvFloat: true} - , x_relative: {required: true, number: true, min: 0.0, max: 100.0} - , x_relative_path: {required: true, csvRelative: true} - , y_type: "required" - , y_fixed_coordinate: {number: true, required: true} - , y_fixed_path: {required: true, csvFloat: true} - , y_relative: {required: true, number: true, min: 0.0, max: 100.0} - , y_relative_path: {required: true, csvRelative: true}, - // Rules formerly belonging to snapshot profile - + octolapse_stabilization_name: "required" + , octolapse_stabilization_stabilization_type: "required" + , octolapse_stabilization_x_type: "required" + , octolapse_stabilization_x_fixed_coordinate: {number: true, required: true} + , octolapse_stabilization_x_fixed_path: {required: true, csvFloat: true} + , octolapse_stabilization_x_relative: {required: true, number: true, min: 0.0, max: 100.0} + , octolapse_stabilization_x_relative_path: {required: true, csvRelative: true} + , octolapse_stabilization_y_type: "required" + , octolapse_stabilization_y_fixed_coordinate: {number: true, required: true} + , octolapse_stabilization_y_fixed_path: {required: true, csvFloat: true} + , octolapse_stabilization_y_relative: {required: true, number: true, min: 0.0, max: 100.0} + , octolapse_stabilization_y_relative_path: {required: true, csvRelative: true}, }, messages: { - name: "Please enter a name for your profile", + octolapse_stabilization_name: "Please enter a name for your profile", } }; }); diff --git a/octoprint_octolapse/static/js/octolapse.profiles.trigger.js b/octoprint_octolapse/static/js/octolapse.profiles.trigger.js index 8e21d35b..9212a627 100644 --- a/octoprint_octolapse/static/js/octolapse.profiles.trigger.js +++ b/octoprint_octolapse/static/js/octolapse.profiles.trigger.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -23,7 +23,7 @@ */ $(function () { Octolapse.TriggerProfileViewModel = function (values) { - + var self = this; self.profileTypeName = ko.observable("Trigger"); self.guid = ko.observable(values.guid); @@ -31,13 +31,12 @@ $(function () { self.description = ko.observable(values.description); self.trigger_type = ko.observable(values.trigger_type); // Pre-calculated trigger options - self.snap_to_print_disable_z_lift = ko.observable(values.snap_to_print_disable_z_lift); - self.fastest_speed = ko.observable(values.fastest_speed); self.smart_layer_trigger_type = ko.observable(values.smart_layer_trigger_type); - self.smart_layer_trigger_speed_threshold = ko.observable(values.smart_layer_trigger_speed_threshold); - self.smart_layer_trigger_distance_threshold_percent = ko.observable(values.smart_layer_trigger_distance_threshold_percent); - self.smart_layer_snap_to_print = ko.observable(values.smart_layer_snap_to_print); + self.smart_layer_snap_to_print_high_quality = ko.observable(values.smart_layer_snap_to_print_high_quality); + self.smart_layer_snap_to_print_smooth = ko.observable(values.smart_layer_snap_to_print_smooth); + self.smart_layer_disable_z_lift = ko.observable(values.smart_layer_disable_z_lift); + self.allow_smart_snapshot_commands = ko.observable(values.allow_smart_snapshot_commands); self.trigger_subtype = ko.observable(values.trigger_subtype); /* Timer Trigger Settings @@ -47,7 +46,7 @@ $(function () { Layer/Height Trigger Settings */ self.layer_trigger_height = ko.observable(values.layer_trigger_height); - + /* * Quaity Settiings */ @@ -64,16 +63,17 @@ $(function () { self.trigger_on_deretracting = ko.observable(values.trigger_on_deretracting); self.trigger_on_deretracted = ko.observable(values.trigger_on_deretracted); self.require_zhop = ko.observable(values.require_zhop); - + /* * Position Restrictions * */ self.position_restrictions_enabled = ko.observable(values.position_restrictions_enabled); - self.position_restrictions = ko.observableArray([]); + var position_restrictions = []; for (var index = 0; index < values.position_restrictions.length; index++) { - self.position_restrictions.push( + position_restrictions.push( ko.observable(values.position_restrictions[index])); } + self.position_restrictions = ko.observableArray(position_restrictions); // Temporary variables to hold new layer position restrictions self.new_position_restriction_type = ko.observable('required'); @@ -84,13 +84,14 @@ $(function () { self.new_position_restriction_y2 = ko.observable(1); self.new_position_restriction_r = ko.observable(1); self.new_calculate_intersections = ko.observable(false); - + // Hold the parent dialog. + self.dialog = null; self.get_trigger_subtype_options = ko.pureComputed( function () { - if (self.trigger_type() !== 'real-time') { + if (self.trigger_type() == 'smart') { var options = []; for (var index = 0; index < Octolapse.Triggers.profileOptions.trigger_subtype_options.length; index++) { var curItem = Octolapse.Triggers.profileOptions.trigger_subtype_options[index]; - if (curItem.value !== 'layer') { + if (curItem.value == 'timer') { continue; } options.push(curItem); @@ -122,6 +123,11 @@ $(function () { self.addPositionRestriction = function () { //console.log("Adding " + type + " position restriction."); + if (!self.dialog.IsValid()) + { + return; + } + var restriction = ko.observable({ "type": self.new_position_restriction_type(), "shape": self.new_position_restriction_shape(), @@ -139,19 +145,16 @@ $(function () { //console.log("Removing restriction at index: " + index); self.position_restrictions.splice(index, 1); }; - + self.updateFromServer = function(values) { - self.guid(values.guid); self.name(values.name); self.description(values.description); self.trigger_type(values.trigger_type); - self.snap_to_print_disable_z_lift(values.snap_to_print_disable_z_lift); - self.fastest_speed(values.fastest_speed); + self.smart_layer_snap_to_print_high_quality(values.smart_layer_snap_to_print_high_quality); + self.smart_layer_snap_to_print_smooth(values.smart_layer_snap_to_print_smooth); self.smart_layer_trigger_type(values.smart_layer_trigger_type); - self.smart_layer_trigger_speed_threshold(values.smart_layer_trigger_speed_threshold); - self.smart_layer_trigger_distance_threshold_percent(values.smart_layer_trigger_distance_threshold_percent); - self.smart_layer_snap_to_print(values.smart_layer_snap_to_print); self.smart_layer_disable_z_lift(values.smart_layer_disable_z_lift); + self.allow_smart_snapshot_commands(values.allow_smart_snapshot_commands); self.trigger_subtype(values.trigger_subtype); self.timer_trigger_seconds(values.timer_trigger_seconds); self.layer_trigger_height(values.layer_trigger_height); @@ -168,11 +171,12 @@ $(function () { self.trigger_on_deretracted(values.trigger_on_deretracted); self.require_zhop(values.require_zhop); self.position_restrictions_enabled(values.position_restrictions_enabled); - self.position_restrictions([]); + var position_restrictions = []; for (var index = 0; index < values.position_restrictions.length; index++) { - self.position_restrictions.push( + position_restrictions.push( ko.observable(values.position_restrictions[index])); } + self.position_restrictions(position_restrictions); }; self.automatic_configuration = new Octolapse.ProfileLibraryViewModel( @@ -187,11 +191,19 @@ $(function () { { // need to remove the parent link from the automatic configuration to prevent a cyclic copy var parent = self.automatic_configuration.parent; + var dialog = self.dialog; + self.dialog = null; self.automatic_configuration.parent = null; var copy = ko.toJS(self); + self.dialog = dialog; self.automatic_configuration.parent = parent; return copy; }; + + self.on_opened = function(dialog) + { + self.dialog = dialog; + }; self.on_closed = function(){ self.automatic_configuration.on_closed(); }; @@ -207,24 +219,18 @@ $(function () { rules: { - name: "required", - new_position_restriction_x: { lessThan: "#octolapse_trigger_new_position_restriction_x2:visible" }, - new_position_restriction_x2: { greaterThan: "#octolapse_trigger_new_position_restriction_x:visible" }, - new_position_restriction_y: { lessThan: "#octolapse_trigger_new_position_restriction_y2:visible" }, - new_position_restriction_y2: { greaterThan: "#octolapse_trigger_new_position_restriction_y:visible" }, - layer_trigger_enabled: {check_one: ".octolapse_trigger_enabled"}, - gcode_trigger_enabled: {check_one: ".octolapse_trigger_enabled"}, - timer_trigger_enabled: {check_one: ".octolapse_trigger_enabled"}, + octolapse_trigger_name: "required", + octolapse_trigger_new_position_restriction_x: { lessThan: "#octolapse_trigger_new_position_restriction_x2:visible" }, + octolapse_trigger_new_position_restriction_x2: { greaterThan: "#octolapse_trigger_new_position_restriction_x:visible" }, + octolapse_trigger_new_position_restriction_y: { lessThan: "#octolapse_trigger_new_position_restriction_y2:visible" }, + octolapse_trigger_new_position_restriction_y2: { greaterThan: "#octolapse_trigger_new_position_restriction_y:visible" }, }, messages: { - name: "Please enter a name for your profile", - new_position_restriction_x : { lessThan: "Must be less than the 'X2' field." }, - new_position_restriction_x2: { greaterThan: "Must be greater than the 'X' field." }, - new_position_restriction_y: { lessThan: "Must be less than the 'Y2." }, - new_position_restriction_y2: { greaterThan: "Must be greater than the 'Y' field." }, - layer_trigger_enabled: {check_one: "No triggers are enabled. You must enable at least one trigger."}, - gcode_trigger_enabled: {check_one: "No triggers are enabled. You must enable at least one trigger."}, - timer_trigger_enabled: {check_one: "No triggers are enabled. You must enable at least one trigger."}, + octolapse_trigger_name: "Please enter a name for your profile", + octolapse_trigger_new_position_restriction_x : { lessThan: "Must be less than the 'X2' field." }, + octolapse_trigger_new_position_restriction_x2: { greaterThan: "Must be greater than the 'X' field." }, + octolapse_trigger_new_position_restriction_y: { lessThan: "Must be less than the 'Y2." }, + octolapse_trigger_new_position_restriction_y2: { greaterThan: "Must be greater than the 'Y' field." }, } }; }); diff --git a/octoprint_octolapse/static/js/octolapse.settings.import.js b/octoprint_octolapse/static/js/octolapse.settings.import.js index 69509810..7c57d724 100644 --- a/octoprint_octolapse/static/js/octolapse.settings.import.js +++ b/octoprint_octolapse/static/js/octolapse.settings.import.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -39,20 +39,20 @@ $(function () { self.$importFileUploadElement = null; self.$progressBar = null; self.current_upload_data = null; - self.initialize = function() { + self.initialize = function () { self.initSettingsFileUpload(); }; - self.update = function(settings) { + self.update = function (settings) { self.options.import_methods(settings.global_options.import_options.settings_import_methods); }; - self.importSettings = function(){ + self.importSettings = function () { if (self.import_method() == 'text') { //console.log("Importing Settings from Text"); var data = { 'import_method': self.import_method(), - 'import_text' : self.import_text(), + 'import_text': self.import_text(), 'client_id': Octolapse.Globals.client_id }; $.ajax({ @@ -62,11 +62,26 @@ $(function () { contentType: "application/json", dataType: "json", success: function (data) { + var message = data.msg; + if (!data.success) { + var options = { + title: 'Unable To Import Settings', + text: message, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(options, "settings_import_error", ["settings_import_error"]); + return; + } self.import_text(""); var settings = JSON.parse(data.settings); - var message = data.msg; + Octolapse.Settings.updateSettings(settings); - Octolapse.Globals.update(settings.main_settings); + Octolapse.Globals.main_settings.update(settings.main_settings); // maybe add a success popup? var options = { title: 'Settings Imported', @@ -75,11 +90,12 @@ $(function () { hide: true, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); self.closeSettingsImportPopup(); + Octolapse.Settings.checkForProfileUpdates(); }, error: function (XMLHttpRequest, textStatus, errorThrown) { var message = "Unable to import the provided settings:( Status: " + textStatus + ". Error: " + errorThrown; @@ -90,15 +106,13 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); } }); - } - else - { + } else { //console.log("Importing Settings from File"); if (self.current_upload_data != null) { self.current_upload_data.submit(); @@ -108,16 +122,16 @@ $(function () { }; self.submitFileData = null; - self.hasImportSettingsFile = function(){ + self.hasImportSettingsFile = function () { return self.current_upload_data != null; }; - self.initSettingsFileUpload = function(){ + self.initSettingsFileUpload = function () { // Set up the file upload button. - self.$importFileUploadElement = $('#octolapse_settings_import_path_upload'); - var $progressBarContainer = $('#octolapse_upload_settings_progress'); - self.$progressBar = $progressBarContainer.find('.progress-bar'); - self.$importFileUploadElement.fileupload({ + self.$importFileUploadElement = $('#octolapse_settings_import_path_upload'); + var $progressBarContainer = $('#octolapse_upload_settings_progress'); + self.$progressBar = $progressBarContainer.find('.progress-bar'); + self.$importFileUploadElement.fileupload({ dataType: "json", maxNumberOfFiles: 1, autoUpload: false, @@ -128,10 +142,10 @@ $(function () { // maxChunkSize: 100000, formData: {client_id: Octolapse.Globals.client_id}, dropZone: "#octolapse_settings_import_dialog .octolapse_dropzone", - add: function(e, data) { + add: function (e, data) { //console.log("Adding file"); self.$progressBar.text(""); - self.$progressBar.removeClass('failed').animate({'width': '0%'}, {'queue':false}); + self.$progressBar.removeClass('failed').animate({'width': '0%'}, {'queue': false}); self.current_upload_data = data; self.$dialog.validator.form(); self.import_file_path(data.files[0].name); @@ -145,17 +159,32 @@ $(function () { //console.log("Uploading Progress"); var progress = parseInt(data.loaded / data.total * 100, 10); self.$progressBar.text(progress + "%"); - self.$progressBar.animate({'width': progress + '%'}, {'queue':false}); + self.$progressBar.animate({'width': progress + '%'}, {'queue': false}); }, - done: function(e, data) { + done: function (e, data) { //console.log("Upload Done"); - var settings = JSON.parse(data.result.settings); var message = data.result.msg; + if (!data.result.success) { + var options = { + title: 'Unable To Import Settings', + text: message, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(options, "settings_import_error", ["settings_import_error"]); + return; + } + var settings = JSON.parse(data.result.settings); + Octolapse.Settings.updateSettings(settings); - Octolapse.Globals.update(settings.main_settings); + Octolapse.Globals.main_settings.update(settings.main_settings); self.$progressBar.text(""); - self.$progressBar.animate({'width': '0%'}, {'queue':false}); + self.$progressBar.animate({'width': '0%'}, {'queue': false}); self.closeSettingsImportPopup(); // maybe add a success popup? var options = { @@ -165,21 +194,22 @@ $(function () { hide: true, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); + Octolapse.Settings.checkForProfileUpdates(); }, - fail: function(e, data) { + fail: function (e, data) { //console.log("Upload Failed"); self.$progressBar.text("Failed...").addClass('failed'); - self.$progressBar.animate({'width': '100%'}, {'queue':false}); + self.$progressBar.animate({'width': '100%'}, {'queue': false}); }, - complete: function(e){ + complete: function (e) { //console.log("Upload Complete"); self.import_file_path(""); } - }); + }); }; self.showSettingsImportPopup = function () { @@ -201,18 +231,18 @@ $(function () { messages: Octolapse.SettingsImportValidationRules.messages, ignore: ".ignore_hidden_errors:hidden, .ignore_hidden_errors.hiding", errorPlacement: function (error, element) { - var error_id = $(element).attr("id"); - var $field_error = $(".error_label_container[data-error-for='" + error_id + "']"); + var error_id = $(element).attr("name"); + var $field_error = self.$dialog.$editDialog.find(".error_label_container[data-error-for='" + error_id + "']"); $field_error.html(error); }, highlight: function (element, errorClass) { - var error_id = $(element).attr("id"); - var $field_error = $(".error_label_container[data-error-for='" + error_id + "']"); + var error_id = $(element).attr("name"); + var $field_error = self.$dialog.$editDialog.find(".error_label_container[data-error-for='" + error_id + "']"); $field_error.removeClass("checked"); $field_error.addClass(errorClass); }, unhighlight: function (element, errorClass) { - var error_id = $(element).attr("id"); + var error_id = self.$dialog.$editDialog.find(element).attr("name"); var $field_error = $(".error_label_container[data-error-for='" + error_id + "']"); $field_error.addClass("checked"); $field_error.removeClass(errorClass); @@ -233,21 +263,20 @@ $(function () { $(label).parent().parent().parent().removeClass('error'); }, onfocusout: function (element, event) { - setTimeout(function() { - if(self.$dialog.validator) - { + setTimeout(function () { + if (self.$dialog.validator) { self.$dialog.validator.form(); } }, 250); }, onclick: function (element, event) { - setTimeout(function() { + setTimeout(function () { self.$dialog.validator.form(); self.resize(); }, 250); } }; - self.resize = function(){ + self.resize = function () { /*self.$dialog.$editDialog.css("top","0px").css( 'margin-top', Math.max(0 - self.$dialog.$editDialog.height() / 2, 0) @@ -291,8 +320,7 @@ $(function () { //console.log("Importing Settings."); // the form is valid, add or update the profile self.importSettings(); - } - else { + } else { // Search for any hidden elements that are invalid //console.log("Checking ofr hidden field error"); var $fieldErrors = self.$dialog.$editForm.find('.error_label_container.error'); @@ -304,7 +332,7 @@ $(function () { //console.log("Hidden error found, showing"); var $collapsableContainer = $errorContainer.parents(".collapsible"); if ($collapsableContainer.length > 0) - // The containers may be nested, show each + // The containers may be nested, show each $collapsableContainer.each(function (index, container) { //console.log("Showing the collapsed container"); $(container).show(); @@ -324,8 +352,7 @@ $(function () { }); // see if the current viewmodel has an on_opened function - if (typeof self.on_opened === 'function') - { + if (typeof self.on_opened === 'function') { // call the function self.on_opened(); } @@ -335,40 +362,38 @@ $(function () { maxHeight: function () { return Math.max( - window.innerHeight - + window.innerHeight - self.$dialog.$modalHeader.outerHeight() - self.$dialog.$modalFooter.outerHeight() - 66, - 200 + 200 ); } } ); }; self.can_hide = false; - self.closeSettingsImportPopup = function() { + self.closeSettingsImportPopup = function () { if (self.$dialog != null) { self.can_hide = true; self.$dialog.$editDialog.modal("hide"); } }; - self.on_opened = function(){ + self.on_opened = function () { //console.log("Opening settings import dialog.") - if(self.$importFileUploadElement === null) + if (self.$importFileUploadElement === null) return; self.$importFileUploadElement.empty(); self.import_file_path(""); self.$progressBar.text(""); - self.$progressBar.animate({'width': '0%'}, {'queue':false}); + self.$progressBar.animate({'width': '0%'}, {'queue': false}); }; Octolapse.SettingsImportValidationRules = { rules: { - octolapse_settings_import_path_upload: { uploadFileRequired: [self.hasImportSettingsFile]}, + octolapse_settings_import_path_upload: {uploadFileRequired: [self.hasImportSettingsFile]}, }, - messages: { - - } + messages: {} }; diff --git a/octoprint_octolapse/static/js/octolapse.settings.js b/octoprint_octolapse/static/js/octolapse.settings.js index d210175f..dfa72da7 100644 --- a/octoprint_octolapse/static/js/octolapse.settings.js +++ b/octoprint_octolapse/static/js/octolapse.settings.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -37,6 +37,7 @@ $(function () { "templateName": "empty-template", "profileObservable": ko.observable() }); + Octolapse.MainSettingsDisplay = new Octolapse.MainSettingsEditViewModel(); // Assign the Octoprint settings to our namespace Octolapse.Settings.global_settings = parameters[0]; Octolapse.Settings.is_loaded = ko.observable(false); @@ -45,12 +46,6 @@ $(function () { Octolapse.SettingsImport = new Octolapse.SettingsImportViewModel(); // Called before octoprint binds the viewmodel to the plugin self.onBeforeBinding = function () { - /* - Create our global settings - */ - self.settings = self.global_settings.settings.plugins.octolapse; - var settings = ko.toJS(self.settings); // just get the values - /** * Profiles - These are bound by octolapse.profiles.js */ @@ -93,7 +88,7 @@ $(function () { , 'setCurrentProfilePath': 'setCurrentProfile' }; Octolapse.Stabilizations = new Octolapse.ProfilesViewModel(stabilizationSettings); - + /* Create our triggers view model */ @@ -113,7 +108,7 @@ $(function () { , 'setCurrentProfilePath': 'setCurrentProfile' }; Octolapse.Triggers = new Octolapse.ProfilesViewModel(triggerSettings); - + /* Create our rendering view model */ @@ -154,24 +149,24 @@ $(function () { Octolapse.Cameras = new Octolapse.ProfilesViewModel(cameraSettings); /* - Create our debug view model + Create our logging view model */ - var debugSettings = + var loggingSettings = { 'current_profile_guid': null, 'profiles': [], 'default_profile': null, 'profileOptions': {}, - 'profileViewModelCreateFunction': Octolapse.DebugProfileViewModel, - 'profileValidationRules': Octolapse.DebugProfileValidationRules, - 'bindingElementId': 'octolapse_debug_tab', - 'addEditTemplateName': 'debug-template', - 'profileTypeName': 'Debug', + 'profileViewModelCreateFunction': Octolapse.LoggingProfileViewModel, + 'profileValidationRules': Octolapse.LoggingProfileValidationRules, + 'bindingElementId': 'octolapse_logging_tab', + 'addEditTemplateName': 'logging-profile-template', + 'profileTypeName': 'Logging', 'addUpdatePath': 'addUpdateProfile', 'removeProfilePath': 'removeProfile', 'setCurrentProfilePath': 'setCurrentProfile' }; - Octolapse.DebugProfiles = new Octolapse.ProfilesViewModel(debugSettings); + Octolapse.LoggingProfiles = new Octolapse.ProfilesViewModel(loggingSettings); }; @@ -187,63 +182,68 @@ $(function () { // GlobalOptions Octolapse.SettingsImport.update(settings); // SettingsMain - Octolapse.SettingsMain.update(settings.main_settings); + Octolapse.MainSettingsDisplay.defaults = settings.profiles.defaults.main_settings; // Printers - Octolapse.Printers.profiles([]); + var printers = []; Octolapse.Printers.default_profile(settings.profiles.defaults.printer); Octolapse.Printers.profileOptions = settings.profiles.options.printer; Octolapse.Printers.current_profile_guid(settings.profiles.current_printer_profile_guid); Object.keys(settings.profiles.printers).forEach(function(key) { - Octolapse.Printers.profiles.push(new Octolapse.PrinterProfileViewModel(settings.profiles.printers[key])); + printers.push(new Octolapse.PrinterProfileViewModel(settings.profiles.printers[key])); }); + Octolapse.Printers.profiles(printers); // Stabilizations - Octolapse.Stabilizations.profiles([]); + var stabilizations = []; Octolapse.Stabilizations.default_profile(settings.profiles.defaults.stabilization); Octolapse.Stabilizations.profileOptions = settings.profiles.options.stabilization; Octolapse.Stabilizations.current_profile_guid(settings.profiles.current_stabilization_profile_guid); Object.keys(settings.profiles.stabilizations).forEach(function(key) { - Octolapse.Stabilizations.profiles.push(new Octolapse.StabilizationProfileViewModel(settings.profiles.stabilizations[key])); + stabilizations.push(new Octolapse.StabilizationProfileViewModel(settings.profiles.stabilizations[key])); }); + Octolapse.Stabilizations.profiles(stabilizations); // Triggers - Octolapse.Triggers.profiles([]); + var triggers = []; Octolapse.Triggers.default_profile(settings.profiles.defaults.trigger); Octolapse.Triggers.profileOptions = settings.profiles.options.trigger; Octolapse.Triggers.current_profile_guid(settings.profiles.current_trigger_profile_guid); Object.keys(settings.profiles.triggers).forEach(function(key) { - Octolapse.Triggers.profiles.push(new Octolapse.TriggerProfileViewModel(settings.profiles.triggers[key])); + triggers.push(new Octolapse.TriggerProfileViewModel(settings.profiles.triggers[key])); }); - + Octolapse.Triggers.profiles(triggers); + // Renderings - Octolapse.Renderings.profiles([]); + var renderings = []; Octolapse.Renderings.default_profile(settings.profiles.defaults.rendering); Octolapse.Renderings.profileOptions = settings.profiles.options.rendering; Octolapse.Renderings.current_profile_guid(settings.profiles.current_rendering_profile_guid); Object.keys(settings.profiles.renderings).forEach(function(key) { - Octolapse.Renderings.profiles.push(new Octolapse.RenderingProfileViewModel(settings.profiles.renderings[key])); + renderings.push(new Octolapse.RenderingProfileViewModel(settings.profiles.renderings[key])); }); + Octolapse.Renderings.profiles(renderings); // Cameras - Octolapse.Cameras.profiles([]); + var cameras = []; Octolapse.Cameras.default_profile(settings.profiles.defaults.camera); Octolapse.Cameras.profileOptions = settings.profiles.options.camera; //console.log("Creating initial camera profiles."); Object.keys(settings.profiles.cameras).forEach(function(key) { - Octolapse.Cameras.profiles.push(new Octolapse.CameraProfileViewModel(settings.profiles.cameras[key])); + cameras.push(new Octolapse.CameraProfileViewModel(settings.profiles.cameras[key])); }); - - // Debug - Octolapse.DebugProfiles.profiles([]); - Octolapse.DebugProfiles.default_profile(settings.profiles.defaults.debug); - Octolapse.DebugProfiles.profileOptions = settings.profiles.options.debug; - Octolapse.DebugProfiles.current_profile_guid(settings.profiles.current_debug_profile_guid); - //console.log("Creating Debug Profiles") - Object.keys(settings.profiles.debug).forEach(function(key) { - Octolapse.DebugProfiles.profiles.push(new Octolapse.DebugProfileViewModel(settings.profiles.debug[key])); + Octolapse.Cameras.profiles(cameras); + + // Logging + var logging_profiles=[]; + Octolapse.LoggingProfiles.default_profile(settings.profiles.defaults.logging); + Octolapse.LoggingProfiles.profileOptions = settings.profiles.options.logging; + Octolapse.LoggingProfiles.current_profile_guid(settings.profiles.current_logging_profile_guid); + //console.log("Creating Logging Profiles") + Object.keys(settings.profiles.logging).forEach(function(key) { + logging_profiles.push(new Octolapse.LoggingProfileViewModel(settings.profiles.logging[key])); }); - + Octolapse.LoggingProfiles.profiles(logging_profiles); Octolapse.Settings.is_loaded(true); }; @@ -254,7 +254,7 @@ $(function () { var itemGuid = item.guid(); var matchFound = itemGuid === guid; if (matchFound) - return matchFound + return matchFound; } ); if (index < 0) { @@ -281,8 +281,8 @@ $(function () { success: function (newSettings) { self.updateSettings(newSettings); - Octolapse.Globals.update(newSettings.main_settings); - var message = "The default settings have been restored. It is recommended that you restart the OctoPrint server now."; + Octolapse.Globals.main_settings.update(newSettings.main_settings); + var message = "The default settings have been restored. Octolapse is checking for profile updates now."; var options = { title: 'Default Settings Restored', text: message, @@ -290,10 +290,11 @@ $(function () { hide: true, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); + self.checkForProfileUpdates(); }, error: function (XMLHttpRequest, textStatus, errorThrown) { var message = "Unable to restore the default settings. Status: " + textStatus + ". Error: " + errorThrown; @@ -304,7 +305,7 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -316,20 +317,31 @@ $(function () { /* load all settings default settings */ - self.loadSettings = function () { + self.loadSettings = function (success_callback) { // If no guid is supplied, this is a new profile. We will need to know that later when we push/update our observable array $.ajax({ - url: "./plugin/octolapse/loadSettings", + url: "./plugin/octolapse/loadSettingsAndState", type: "POST", contentType: "application/json", dataType: "json", success: function (newSettings) { - self.updateSettings(newSettings); - Octolapse.Globals.loadState(); + self.updateSettings(newSettings.settings); + Octolapse.Globals.updateState(newSettings); + // Load the current state before running the success callback. + //console.log("Settings and state loaded."); + + // Todo: Add an error callback if the current state cannot be loaded + if(success_callback) + { + success_callback(); + } + + //console.log("Settings have been loaded."); }, error: function (XMLHttpRequest, textStatus, errorThrown) { + // Todo: Add an error callback if the current settings cannot be loaded var message = "Octolapse was unable to load the current settings. Status: " + textStatus + ". Error: " + errorThrown; var options = { title: 'Settings Load Error', @@ -338,7 +350,7 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -374,13 +386,108 @@ $(function () { Octolapse.Cameras.default_profile(null); Octolapse.Cameras.current_profile_guid(null); Octolapse.Cameras.profileOptions = {}; - // Debugs - Octolapse.DebugProfiles.profiles([]); - Octolapse.DebugProfiles.default_profile(null); - Octolapse.DebugProfiles.current_profile_guid(null); - Octolapse.DebugProfiles.profileOptions = {}; + // Logging Profiles + Octolapse.LoggingProfiles.profiles([]); + Octolapse.LoggingProfiles.default_profile(null); + Octolapse.LoggingProfiles.current_profile_guid(null); + Octolapse.LoggingProfiles.profileOptions = {}; + }; + + self.checkForProfileUpdates = function(is_silent_test){ + //console.log("Updating Octolapse profiles from the server."); + // is_silent_test is optional, make sure it is either true or false + is_silent_test = is_silent_test == true; + var data = { + 'is_silent_test': is_silent_test + }; + $.ajax({ + url: "./plugin/octolapse/checkForProfileUpdates", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (values) { + var available_profile_count = values.available_profile_count; + var options; + if (available_profile_count > 0) + { + // Always show profile update confirmation if updates are found at startup + self.showProfileUpdateConfirmation(available_profile_count); + } + else if (!is_silent_test) + { + // Only report that settings are up-to-date if this is NOT a silent update request + options = { + title: 'Octolapse Updates', + text: "Your Octolapse profiles are all up-to-date.", + type: 'success', + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(options, 'update-profile-from-server','update-profile-from-server'); + } + //console.log("Check for updates complete."); + + + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + + var message = "Octolapse was unable to check for updates. Status: " + textStatus + ". Error: " + errorThrown; + console.error(message); + var options = { + title: 'Profile Update Error', + text: message, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(options, 'update-profile-from-server','update-profile-from-server'); + } + }); }; + self.showProfileUpdateConfirmation = function(num_profiles_available){ + var message = "There are updates available for " + num_profiles_available.toString() + + " profiles. " + + "Would you like to apply these updates now?"; + Octolapse.showConfirmDialog( + "update-all-automatic-profiles", + "Apply Octolapse Updates", + message, + function(){ + var options = { + title: 'Octolapse is Updating', + text: num_profiles_available.toString() +" update are being applied. Please wait.", + type: 'info', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(options, 'update-profile-from-server','update-profile-from-server'); + Octolapse.Settings.updateProfilesFromServer(); + }, + function() { + + }, + function() { + + }, + function(){ + Octolapse.Settings.suppressServerUpdates(); + }, + "Ignore" + ); + }; + + self.updateProfilesFromServer = function() { //console.log("Updating Octolapse profiles from the server."); @@ -396,28 +503,31 @@ $(function () { dataType: "json", success: function (values) { var message; + var title; //console.log("Octolapse profiles updated from the server."); var num_updated = values.num_updated; + var options = { + hide: true, + addclass: "octolapse", + desktop: { + desktop: false + } + }; if (num_updated > 0) { var settings = JSON.parse(values.settings); self.updateSettings(settings); Octolapse.Globals.loadState(); - message = num_updated.toString() + " Octolapse profiles were updated."; + options.title = "Octolapse Update Complete"; + options.text = num_updated.toString() + " profiles were updated successfully!"; + options.type = 'success'; } else { - message = "No new updates found. Your Octolapse profiles are up-to-date." + options.title = "No Updates Applied"; + options.text = "Octolapse was already up-to-date."; + options.type = 'warning'; } - var options = { - title: 'Octolapse Updates', - text: message, - type: 'success', - hide: true, - addclass: "octolapse", - desktop: { - desktop: true - } - }; + Octolapse.displayPopupForKey(options, 'update-profile-from-server','update-profile-from-server'); }, error: function (XMLHttpRequest, textStatus, errorThrown) { @@ -431,7 +541,7 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopupForKey(options, 'update-profile-from-server','update-profile-from-server'); @@ -439,6 +549,17 @@ $(function () { }); }; + Octolapse.Settings.UpdateAvailableServerProfiles = function(server_profiles) + { + Octolapse.Printers.profileOptions.server_profiles = server_profiles["printer"]; + Octolapse.Stabilizations.profileOptions.server_profiles = server_profiles["stabilization"]; + Octolapse.Triggers.profileOptions.server_profiles = server_profiles["trigger"]; + Octolapse.Renderings.profileOptions.server_profiles = server_profiles["rendering"]; + Octolapse.Cameras.profileOptions.server_profiles = server_profiles["camera"]; + Octolapse.LoggingProfiles.profileOptions.server_profiles = server_profiles["logging"]; + }; + + self.suppressServerUpdates = function() { var data = { 'client_id': Octolapse.Globals.client_id @@ -452,7 +573,7 @@ $(function () { success: function (newSettings) { self.updateSettings(newSettings); Octolapse.Globals.loadState(); - var message = "Update notifications have been suppressed. You can force an update at any time within the main settings page.."; + var message = "Update notifications have been suppressed. You can force an update at any time within the main settings page."; var options = { title: 'Updates Suppressed', text: message, @@ -460,7 +581,7 @@ $(function () { hide: true, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopupForKey(options, 'updates-suppressed','updates-suppressed'); @@ -476,14 +597,13 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopupForKey(options, 'updates-suppressed','updates-suppressed'); } }); }; - /* Profile Add/Update routine for showAddEditDialog */ @@ -504,8 +624,8 @@ $(function () { case "camera-template": Octolapse.Cameras.addUpdateProfile(profile.profileObservable, self.hideAddEditDialog()); break; - case "debug-template": - Octolapse.DebugProfiles.addUpdateProfile(profile.profileObservable, self.hideAddEditDialog()); + case "logging-profile-template": + Octolapse.LoggingProfiles.addUpdateProfile(profile.profileObservable, self.hideAddEditDialog()); break; default: var message = "Cannot save the object, the template (" + profile.templateName + ") is unknown!"; @@ -516,27 +636,25 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); break; } - }; - /* Modal Dialog Functions */ // hide the modal dialog self.can_hide = false; self.hideAddEditDialog = function () { - console.log("Add update dialog will be closed."); + //console.log("Add update dialog will be closed."); self.can_hide = true; $("#octolapse_add_edit_profile_dialog").modal("hide"); }; self.cancelAddEditDialog = function () { - console.log("Add update dialog cancelled."); + //console.log("Add update dialog cancelled."); // Hide the dialog self.hideAddEditDialog(); // see if the current viewmodel has an on_canceled function @@ -556,10 +674,10 @@ $(function () { dialog.templateName = options.templateName; dialog.$addEditDialog = $("#octolapse_add_edit_profile_dialog"); dialog.$addEditForm = dialog.$addEditDialog.find("#octolapse_add_edit_profile_form"); - dialog.$cancelButton = $("button.cancel", dialog.$addEditDialog); - dialog.$closeIcon = $("a.close", dialog.$addEditDialog); - dialog.$saveButton = $("button.save", dialog.$addEditDialog); - dialog.$defaultButton = $("button.set-defaults", dialog.$addEditDialog); + dialog.$cancelButton = dialog.$addEditForm.find(".modal-footer button.cancel"); + dialog.$closeIcon = dialog.$addEditForm.find(".modal-header a.close"); + dialog.$saveButton = dialog.$addEditForm.find(".modal-footer button.save"); + dialog.$defaultButton = dialog.$addEditForm.find(".modal-footer button.set-defaults"); dialog.$dialogTitle = $("h3.modal-title", dialog.$addEditDialog); dialog.$dialogWarningContainer = $("div.dialog-warning", dialog.$addEditDialog); dialog.$dialogWarningText = $("span", dialog.$dialogWarningContainer); @@ -571,24 +689,28 @@ $(function () { dialog.$modalFooter = dialog.$addEditDialog.find(".modal-footer"); // Create all of the validation rules - var rules = { + dialog.rules = { rules: options.validationRules.rules, messages: options.validationRules.messages, ignore: ".ignore_hidden_errors:hidden, .ignore_hidden_errors.hiding", errorPlacement: function (error, element) { - var error_id = $(element).attr("id"); - var $field_error = $(".error_label_container[data-error-for='" + error_id + "']"); + var error_id = $(element).attr("name"); + var $field_error = dialog.$addEditDialog.find(".error_label_container[data-error-for='" + error_id + "']"); $field_error.html(error); }, highlight: function (element, errorClass) { - var error_id = $(element).attr("id"); - var $field_error = $(".error_label_container[data-error-for='" + error_id + "']"); + if (!element) + return; + var error_id = $(element).attr("name"); + var $field_error = dialog.$addEditDialog.find(".error_label_container[data-error-for='" + error_id + "']"); $field_error.removeClass("checked"); $field_error.addClass(errorClass); }, unhighlight: function (element, errorClass) { - var error_id = $(element).attr("id"); - var $field_error = $(".error_label_container[data-error-for='" + error_id + "']"); + if (!element) + return; + var error_id = $(element).attr("name"); + var $field_error = dialog.$addEditDialog.find(".error_label_container[data-error-for='" + error_id + "']"); $field_error.addClass("checked"); $field_error.removeClass(errorClass); }, @@ -624,17 +746,15 @@ $(function () { }, 250); } }; - dialog.resize = function(){ - /*dialog.$addEditDialog.css("top","0px").css( - 'margin-top', - Math.max(0 - dialog.$addEditDialog.height() / 2,0) - );*/ + + dialog.resize = function() { + }; dialog.validator = null; // Prevent hiding unless the event was initiated by the hideAddEditDialog function dialog.$addEditDialog.on("hide.bs.modal", function () { - console.log("About to hide add edit dialog"); + //console.log("About to hide add edit dialog"); if (!self.can_hide) return false; //return self.can_hide; @@ -643,15 +763,12 @@ $(function () { dialog.$errorList.empty(); dialog.$summary.hide(); // Destroy the validator if it exists, both to save on resources, and to clear out any leftover junk. - if (dialog.validator != null) { - dialog.validator.destroy(); - dialog.validator = null; - } + dialog.unbind_validation(); // see if the current viewmodel has an on_closed function if (typeof self.profileObservable().on_closed === 'function') { // call the function - console.log("Closing the profile dialog"); + //console.log("Closing the profile dialog"); self.profileObservable().on_closed(); } }); @@ -677,14 +794,14 @@ $(function () { dialog.$dialogWarningContainer.show(); } - +/* dialog.$addEditDialog.css({ width: 'auto', 'margin-left': function () { return -($(this).width() / 2); } }); - +*/ // Initialize the profile. var onShow = Octolapse.Settings.AddEditProfile().profileObservable().onShow; if (typeof onShow == 'function') { @@ -693,20 +810,20 @@ $(function () { }); // Configure the shown event dialog.$addEditDialog.on("shown.bs.modal", function () { - console.log("Showing profile settings dialog."); + //console.log("Showing profile settings dialog."); self.can_hide = false; // Unbind all click events dialog.$addEditDialog.unbind('click'); // bind any help links - Octolapse.Help.bindHelpLinks("#octolapse_add_edit_profile_dialog"); - dialog.validator = dialog.$addEditForm.validate(rules); + dialog.bind_help_links(); + dialog.IsValid = function() { if (dialog.validator != null) return dialog.validator.numberOfInvalids() == 0; return true; }; - dialog.validator.form(); + // Remove any click event bindings from the cancel button dialog.$cancelButton.unbind("click"); dialog.$closeIcon.unbind("click"); @@ -726,7 +843,7 @@ $(function () { dialog.$saveButton.unbind("click"); // Called when a user clicks the save button on any add/update dialog. dialog.$saveButton.bind("click", function () { - console.log("Save button clicked on add/edit profile"); + //console.log("Save button clicked on add/edit profile"); // now see if the form is valid if (dialog.$addEditForm.valid()) { // the form is valid, add or update the profile @@ -750,7 +867,6 @@ $(function () { $(container).show(); }); } - }); // The form is invalid, add a shake animation to inform the user @@ -763,22 +879,50 @@ $(function () { }); - // Resize the dialog - dialog.resize(); // see if the current viewmodel has an on_opened function if (typeof self.profileObservable().on_opened === 'function'){ // call the function - self.profileObservable().on_opened(); + self.profileObservable().on_opened(dialog); } - + dialog.bind_validation(); }); + dialog.unbind_validation = function() + { + if (dialog.validator != null) { + //console.log("octolapse.settings.js - Unbinding validation."); + dialog.validator.destroy(); + dialog.validator = null; + } + }; + dialog.bind_validation = function() + { + if (dialog.validator != null) { + dialog.unbind_validation(); + } + dialog.validator = dialog.$addEditForm.validate(dialog.rules); + dialog.validator.form(); + // Sometimes the form doesn't display the validation messages below some fields with dynamic IDs + // Due to an update of data. Make sure the validator is called a bit late as a hack fix. + // Must investigate this, but probably has something to do with knockout binding templates. + setTimeout(function(){ + if (dialog.validator != null) + { + dialog.validator.form(); + } + }, 1000); + + }; + dialog.bind_help_links = function() + { + Octolapse.Help.bindHelpLinks("#octolapse_add_edit_profile_dialog"); + }; // Open the add/edit profile dialog dialog.$addEditDialog.modal({ backdrop: 'static', resize: true, maxHeight: function() { return Math.max( - window.innerHeight - dialog.$modalHeader.outerHeight()-dialog.$modalFooter.outerHeight()-66, + window.outerHeight - dialog.$modalHeader.outerHeight()-dialog.$modalFooter.outerHeight()-66, 200 ); } diff --git a/octoprint_octolapse/static/js/octolapse.settings.main.js b/octoprint_octolapse/static/js/octolapse.settings.main.js index 778600ed..c71625a1 100644 --- a/octoprint_octolapse/static/js/octolapse.settings.main.js +++ b/octoprint_octolapse/static/js/octolapse.settings.main.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -23,16 +23,8 @@ */ $(function () { - Octolapse.MainSettingsViewModel = function (parameters) { - // Create a reference to this object + Octolapse.MainSettingsViewModel = function() { var self = this; - - // Add this object to our Octolapse namespace - Octolapse.SettingsMain = this; - // Assign the Octoprint settings to our namespace - self.global_settings = parameters[0]; - - // Settings values self.is_octolapse_enabled = ko.observable(); self.auto_reload_latest_snapshot = ko.observable(); self.auto_reload_frames = ko.observable(); @@ -45,23 +37,39 @@ $(function () { self.show_trigger_state_changes = ko.observable(); self.show_snapshot_plan_information = ko.observable(); self.preview_snapshot_plans = ko.observable(); + self.preview_snapshot_plan_autoclose = ko.observable(); + self.preview_snapshot_plan_seconds = ko.observable(); self.automatic_updates_enabled = ko.observable(); self.automatic_update_interval_days = ko.observable(); - // Informational Values - self.platform = ko.observable(); - - - self.onBeforeBinding = function () { - - }; - // Get the dialog element - self.onAfterBinding = function () { - - - - }; + self.snapshot_archive_directory = ko.observable(); + self.timelapse_directory = ko.observable(); + self.temporary_directory = ko.observable(); + self.test_mode_enabled = ko.observable(); + // rename this so that it never gets updated when saved + self.octolapse_version = ko.observable("unknown"); + self.settings_version = ko.observable("unknown"); + self.octolapse_git_version = ko.observable(null); + // Computed Observables + self.preview_snapshot_plan_seconds_text = ko.pureComputed(function(){ + if (!self.preview_snapshot_plan_seconds()) + return "unknown"; + return self.preview_snapshot_plan_seconds().toString(); + }); + + self.github_link = ko.pureComputed(function(){ + var git_version = self.octolapse_git_version(); + if (!git_version) + return null; + // If this is a commit, link to the commit + if (self.octolapse_version().includes("+")) + { + return 'https://github.com/FormerLurker/Octolapse/commit/' + Octolapse.Globals.main_settings.octolapse_git_version(); + } + // This is a release, link to the tag + return 'https://github.com/FormerLurker/Octolapse/releases/tag/v' + self.octolapse_version(); + }); - self.update = function (settings) { + self.update = function (settings, defaults) { //console.log("Updating Main Settings") self.is_octolapse_enabled(settings.is_octolapse_enabled); self.auto_reload_latest_snapshot(settings.auto_reload_latest_snapshot); @@ -74,19 +82,29 @@ $(function () { self.show_trigger_state_changes(settings.show_trigger_state_changes); self.show_snapshot_plan_information(settings.show_snapshot_plan_information); self.preview_snapshot_plans(settings.preview_snapshot_plans); + self.preview_snapshot_plan_autoclose(settings.preview_snapshot_plan_autoclose); + self.preview_snapshot_plan_seconds(settings.preview_snapshot_plan_seconds); self.cancel_print_on_startup_error(settings.cancel_print_on_startup_error); self.automatic_update_interval_days(settings.automatic_update_interval_days); self.automatic_updates_enabled(settings.automatic_updates_enabled); - - //self.platform(settings.platform()); - + self.snapshot_archive_directory(settings.snapshot_archive_directory); + self.timelapse_directory(settings.timelapse_directory); + self.temporary_directory(settings.temporary_directory); + self.settings_version(settings.settings_version); + self.test_mode_enabled(settings.test_mode_enabled); + self.octolapse_version(settings.version || settings.octolapse_version || null); + self.octolapse_git_version(settings.git_version || settings.octolapse_git_version || null); + + if (defaults) + self.defaults = settings.defaults; }; self.toggleOctolapse = function(){ - var previousEnabledValue = !Octolapse.Globals.enabled(); + var newValue = !self.is_octolapse_enabled(); var data = { - "is_octolapse_enabled": Octolapse.Globals.enabled() + "is_octolapse_enabled": newValue, + "client_id": Octolapse.Globals.client_id }; //console.log("Toggling octolapse.") $.ajax({ @@ -95,8 +113,10 @@ $(function () { data: JSON.stringify(data), contentType: "application/json", dataType: "json", - success: function () { - + success: function (results) { + // update the global value and the local value + self.is_octolapse_enabled(newValue); + Octolapse.Globals.main_settings.is_octolapse_enabled(newValue); }, error: function (XMLHttpRequest, textStatus, errorThrown) { var message = "Unable to enable/disable Octolapse. Status: " + textStatus + ". Error: " + errorThrown; @@ -107,17 +127,182 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); + } + }); + return true; + }; - Octolapse.Globals.enabled(previousEnabledValue); + self.toggleTestMode = function(){ + + var newValue = !self.test_mode_enabled(); + var data = { + "test_mode_enabled": newValue, + "client_id": Octolapse.Globals.client_id + }; + //console.log("Toggling test mode.") + $.ajax({ + url: "./plugin/octolapse/setTestMode", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (results) { + // update the global value and the local value + self.test_mode_enabled(newValue); + Octolapse.Globals.main_settings.test_mode_enabled(newValue); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + var message = "Unable to enable/disable test mode. Status: " + textStatus + ". Error: " + errorThrown; + var options = { + title: 'Enable/Disable Test Mode Error', + text: message, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopup(options); + } + }); + return true; + }; + + self.toggleSnapshotPlanPreview = function(){ + + var newValue = !self.preview_snapshot_plans(); + var data = { + "preview_snapshot_plans_enabled": newValue, + "client_id": Octolapse.Globals.client_id + }; + //console.log("Toggling test mode.") + $.ajax({ + url: "./plugin/octolapse/setPreviewSnapshotPlans", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (results) { + // update the global value and the local value + self.preview_snapshot_plans(newValue); + Octolapse.Globals.main_settings.preview_snapshot_plans(newValue); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + var message = "Unable to enable/disable preview snapshot plans. Status: " + textStatus + ". Error: " + errorThrown; + var options = { + title: 'Enable/Disable Preview Snapshot Plans Error', + text: message, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopup(options); } }); return true; }; + self.testDirectory = function(type, directory){ + + var data = { + "type": type, + "directory": directory + }; + //console.log("Toggling octolapse.") + $.ajax({ + url: "./plugin/octolapse/testDirectory", + type: "POST", + data: JSON.stringify(data), + contentType: "application/json", + dataType: "json", + success: function (results) { + if (results.success){ + + var success_options = { + title: 'Directory Test Passed', + text: 'The selected directory passed all tests and is ready to be used!', + type: 'success', + hide: true, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(success_options, "directory_test", ["directory_test"]); + } + else { + var fail_options = { + title: 'Directory Test Failed', + text: 'Errors were detected - ' + results.error, + type: 'error', + hide: false, + addclass: "octolapse" + }; + Octolapse.displayPopupForKey(fail_options, "camera_settings_failed",["camera_settings_failed"]); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + var message = "Unable to test the directory. Status: " + textStatus + ". Error: " + errorThrown; + var options = { + title: 'Enable/Disable Error', + text: message, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopup(options); + } + }); + return true; + }; + + self.toJS = function() + { + // need to remove the parent link from the automatic configuration to prevent a cyclic copy + var dialog = self.dialog; + self.dialog = null; + var js = ko.toJS(self); + self.dialog = dialog; + return js; + }; + + Octolapse.MainSettingsValidationRules = { + rules: { + + }, + messages: { + + } + }; + }; + Octolapse.MainSettingsEditViewModel = function () { + // Create a reference to this object + var self = this; + + // Settings values + self.dialog = {}; + // Informational Values + self.platform = ko.observable(); + + self.main_settings = new Octolapse.MainSettingsViewModel(); + self.defaults = {}; +/* + self.onBeforeBinding = function () { + + }; + // Get the dialog element + self.onAfterBinding = function () { + + }; +*/ // hide the modal dialog self.can_hide = false; self.hideDialog = function () { @@ -127,36 +312,19 @@ $(function () { self.showEditMainSettingsPopup = function () { //console.log("showing main settings") - self.is_octolapse_enabled(Octolapse.Globals.enabled()); - self.auto_reload_latest_snapshot(Octolapse.Globals.auto_reload_latest_snapshot()); - self.auto_reload_frames(Octolapse.Globals.auto_reload_frames()); - self.show_navbar_icon(Octolapse.Globals.navbar_enabled()); - self.show_navbar_when_not_printing(Octolapse.Globals.show_navbar_when_not_printing()); - self.show_printer_state_changes(Octolapse.Globals.show_printer_state_changes()); - self.show_position_changes(Octolapse.Globals.show_position_changes()); - self.show_extruder_state_changes(Octolapse.Globals.show_extruder_state_changes()); - self.show_trigger_state_changes(Octolapse.Globals.show_trigger_state_changes()); - self.show_snapshot_plan_information(Octolapse.Globals.show_snapshot_plan_information()); - self.preview_snapshot_plans(Octolapse.Globals.preview_snapshot_plans()); - self.automatic_update_interval_days(Octolapse.Globals.automatic_update_interval_days()); - self.automatic_updates_enabled(Octolapse.Globals.automatic_updates_enabled()); - - self.cancel_print_on_startup_error(Octolapse.Globals.cancel_print_on_startup_error()); - - var dialog = this; - dialog.$editDialog = $("#octolapse_edit_settings_main_dialog"); - dialog.$editForm = $("#octolapse_edit_main_settings_form"); - dialog.$cancelButton = $(".cancel", dialog.$editDialog); - dialog.$closeIcon = $("a.close", dialog.$editDialog); - dialog.$saveButton = $(".save", dialog.$editDialog); - dialog.$defaultButton = $(".set-defaults", dialog.$editDialog); - dialog.$summary = dialog.$editForm.find("#edit_validation_summary"); - dialog.$errorCount = dialog.$summary.find(".error-count"); - dialog.$errorList = dialog.$summary.find("ul.error-list"); - dialog.$modalBody = dialog.$editDialog.find(".modal-body"); - dialog.$modalHeader = dialog.$editDialog.find(".modal-header"); - dialog.$modalFooter = dialog.$editDialog.find(".modal-footer"); - dialog.rules = { + self.dialog.$editDialog = $("#octolapse_edit_settings_main_dialog"); + self.dialog.$editForm = $("#octolapse_edit_main_settings_form"); + self.dialog.$cancelButton = $(".cancel", self.dialog.$editDialog); + self.dialog.$closeIcon = $("a.close", self.dialog.$editDialog); + self.dialog.$saveButton = $(".save", self.dialog.$editDialog); + self.dialog.$defaultButton = $(".set-defaults", self.dialog.$editDialog); + self.dialog.$summary = self.dialog.$editForm.find("#edit_validation_summary"); + self.dialog.$errorCount = self.dialog.$summary.find(".error-count"); + self.dialog.$errorList = self.dialog.$summary.find("ul.error-list"); + self.dialog.$modalBody = self.dialog.$editDialog.find(".modal-body"); + self.dialog.$modalHeader = self.dialog.$editDialog.find(".modal-header"); + self.dialog.$modalFooter = self.dialog.$editDialog.find(".modal-footer"); + self.dialog.rules = { rules: Octolapse.MainSettingsValidationRules.rules, messages: Octolapse.MainSettingsValidationRules.messages, ignore: ".ignore_hidden_errors:hidden, .ignore_hidden_errors.hiding", @@ -179,13 +347,13 @@ $(function () { $field_error.removeClass(errorClass); }, invalidHandler: function () { - dialog.$errorCount.empty(); - dialog.$summary.show(); - var numErrors = dialog.validator.numberOfInvalids(); + self.dialog.$errorCount.empty(); + self.dialog.$summary.show(); + var numErrors = self.dialog.validator.numberOfInvalids(); if (numErrors === 1) - dialog.$errorCount.text("1 field is invalid"); + self.dialog.$errorCount.text("1 field is invalid"); else - dialog.$errorCount.text(numErrors + " fields are invalid"); + self.dialog.$errorCount.text(numErrors + " fields are invalid"); }, errorContainer: "#edit_validation_summary", success: function (label) { @@ -195,101 +363,72 @@ $(function () { }, onfocusout: function (element, event) { setTimeout(function() { - if (dialog.validator) + if (self.dialog.validator) { - dialog.validator.form(); + self.dialog.validator.form(); } }, 250); }, onclick: function (element, event) { - setTimeout(() => dialog.validator.form(), 250); + //setTimeout(() => self.dialog.validator.form(), 250); setTimeout(function() { - dialog.validator.form(); - dialog.resize(); + self.dialog.validator.form(); + self.dialog.resize(); }, 250); } }; - dialog.resize = function(){ - /* - dialog.$editDialog.css("top","0px").css( - 'margin-top', - Math.max(0 - dialog.$editDialog.height() / 2, 0) - );*/ + self.dialog.resize = function(){ }; - dialog.validator = null; + self.dialog.validator = null; - dialog.$editDialog.on("hide.bs.modal", function () { + self.dialog.$editDialog.on("hide.bs.modal", function () { if (!self.can_hide) return false; // Clear out error summary - dialog.$errorCount.empty(); - dialog.$errorList.empty(); - dialog.$summary.hide(); + self.dialog.$errorCount.empty(); + self.dialog.$errorList.empty(); + self.dialog.$summary.hide(); // Destroy the validator if it exists, both to save on resources, and to clear out any leftover junk. - if (dialog.validator != null) { - dialog.validator.destroy(); - dialog.validator = null; + if (self.dialog.validator != null) { + self.dialog.validator.destroy(); + self.dialog.validator = null; } }); - dialog.$editDialog.on("shown.bs.modal", function () { + self.dialog.$editDialog.on("show.bs.modal", function () { + self.main_settings.update(Octolapse.Globals.main_settings.toJS()); + }); + self.dialog.$editDialog.on("shown.bs.modal", function () { self.can_hide = false; // bind any help links Octolapse.Help.bindHelpLinks("#octolapse_edit_settings_main_dialog"); // Create all of the validation rules - dialog.validator = dialog.$editForm.validate(dialog.rules); + self.dialog.validator = self.dialog.$editForm.validate(self.dialog.rules); // Remove any click event bindings from the cancel button - dialog.$cancelButton.unbind("click"); - dialog.$closeIcon.unbind("click"); + self.dialog.$cancelButton.unbind("click"); + self.dialog.$closeIcon.unbind("click"); // Called when the user clicks the cancel button in any add/update dialog - dialog.$cancelButton.bind("click", self.hideDialog); - dialog.$closeIcon.bind("click", self.hideDialog); + self.dialog.$cancelButton.bind("click", self.hideDialog); + self.dialog.$closeIcon.bind("click", self.hideDialog); // remove any click event bindings from the defaults button - dialog.$defaultButton.unbind("click"); - dialog.$defaultButton.bind("click", function () { + self.dialog.$defaultButton.unbind("click"); + self.dialog.$defaultButton.bind("click", function () { // Set the options to the current settings - self.is_octolapse_enabled(true); - self.auto_reload_latest_snapshot(true); - self.auto_reload_frames(5); - self.show_navbar_icon(true); - self.show_navbar_when_not_printing(false); - self.show_printer_state_changes(false); - self.show_position_changes(false); - self.show_extruder_state_changes(false); - self.show_trigger_state_changes(false); - self.show_snapshot_plan_information(false); - self.preview_snapshot_plans(false); - self.automatic_update_interval_days(7); - self.automatic_updates_enabled(true); - + if (self.defaults) + self.main_settings.update(self.defaults); }); // Remove any click event bindings from the save button - dialog.$saveButton.unbind("click"); - // Called when a user clicks the save button on any add/update dialog. - dialog.$saveButton.bind("click", function () + self.dialog.$saveButton.unbind("click"); + // Called when a user clicks the save button on any add/update self.dialog. + self.dialog.$saveButton.bind("click", function () { - if (dialog.$editForm.valid()) { + if (self.dialog.$editForm.valid()) { // the form is valid, add or update the profile - var data = { - "is_octolapse_enabled": self.is_octolapse_enabled() - , "auto_reload_latest_snapshot": self.auto_reload_latest_snapshot() - , "auto_reload_frames": self.auto_reload_frames() - , "show_navbar_icon": self.show_navbar_icon() - , "show_navbar_when_not_printing": self.show_navbar_when_not_printing() - , "show_printer_state_changes": self.show_printer_state_changes() - , "show_position_changes": self.show_position_changes() - , "show_extruder_state_changes": self.show_extruder_state_changes() - , "show_trigger_state_changes": self.show_trigger_state_changes() - , "show_snapshot_plan_information": self.show_snapshot_plan_information() - , "preview_snapshot_plans": self.preview_snapshot_plans() - , "automatic_update_interval_days": self.automatic_update_interval_days() - , "automatic_updates_enabled": self.automatic_updates_enabled() - , "cancel_print_on_startup_error": self.cancel_print_on_startup_error() - , "client_id": Octolapse.Globals.client_id - }; + var data = self.main_settings.toJS(); + data["client_id"] = Octolapse.Globals.client_id; //console.log("Saving main settings.") $.ajax({ url: "./plugin/octolapse/saveMainSettings", @@ -297,8 +436,26 @@ $(function () { data: JSON.stringify(data), contentType: "application/json", dataType: "json", - success: function () { - self.hideDialog(); + success: function (results) { + if (results.success) + { + self.hideDialog(); + Octolapse.Globals.main_settings.update(data); + } + else { + var options = { + title: 'Main Settings Save Error', + text: results.error, + type: 'error', + hide: false, + addclass: "octolapse", + desktop: { + desktop: false + } + }; + Octolapse.displayPopupForKey(options, "settings-error", ["settings-error"]); + } + }, error: function (XMLHttpRequest, textStatus, errorThrown) { var message = "Unable to save the main settings. Status: " + textStatus + ". Error: " + errorThrown; @@ -309,7 +466,7 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -320,7 +477,7 @@ $(function () { { // Search for any hidden elements that are invalid //console.log("Checking ofr hidden field error"); - var $fieldErrors = dialog.$editForm.find('.error_label_container.error'); + var $fieldErrors = self.dialog.$editForm.find('.error_label_container.error'); $fieldErrors.each(function (index, element) { // Check to make sure the field is hidden. If it's not, don't bother showing the parent container. // This can happen if more than one field is invalid in a hidden form @@ -339,20 +496,20 @@ $(function () { }); // The form is invalid, add a shake animation to inform the user - $(dialog.$editDialog).addClass('shake'); + $(self.dialog.$editDialog).addClass('shake'); // set a timeout so the dialog stops shaking - setTimeout(function () { $(dialog.$editDialog).removeClass('shake'); }, 500); + setTimeout(function () { $(self.dialog.$editDialog).removeClass('shake'); }, 500); } }); // Resize the dialog - dialog.resize(); + self.dialog.resize(); }); - dialog.$editDialog.modal({ + self.dialog.$editDialog.modal({ backdrop: 'static', maxHeight: function() { return Math.max( - window.innerHeight - dialog.$modalHeader.outerHeight()-dialog.$modalFooter.outerHeight()-66, + window.innerHeight - self.dialog.$modalHeader.outerHeight()-self.dialog.$modalFooter.outerHeight()-66, 200 ); } @@ -360,15 +517,9 @@ $(function () { }; - Octolapse.MainSettingsValidationRules = { - rules: { - - }, - messages: { - } - }; }; + // Bind the settings view model to the plugin settings element OCTOPRINT_VIEWMODELS.push([ Octolapse.MainSettingsViewModel diff --git a/octoprint_octolapse/static/js/octolapse.status.js b/octoprint_octolapse/static/js/octolapse.status.js index 91113981..9f81c9e3 100644 --- a/octoprint_octolapse/static/js/octolapse.status.js +++ b/octoprint_octolapse/static/js/octolapse.status.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -22,6 +22,33 @@ ################################################################################## */ $(function () { + + Octolapse.CurrentSettingViewModel = function(type, values) + { + var self = this; + // defaults + self.guid = values.guid; + self.name = values.name; + self.description = values.description; + if (type === "printer") + { + self.has_been_saved_by_user = values.has_been_saved_by_user; + } + else if (type === "stabilization") + { + self.wait_for_moves_to_finish = values.wait_for_moves_to_finish; + } + else if (type ==="trigger") + { + self.trigger_type = values.trigger_type; + } + else if (type === "camera") + { + self.enabled = ko.observable(values.enabled); + self.enable_custom_image_preferences = values.enable_custom_image_preferences; + } + }; + Octolapse.StatusViewModel = function () { // Create a reference to this object var self = this; @@ -32,6 +59,7 @@ $(function () { self.is_taking_snapshot = ko.observable(false); self.is_rendering = ko.observable(false); self.snapshot_count = ko.observable(0); + self.snapshot_failed_count = ko.observable(0); self.snapshot_error = ko.observable(false); self.waiting_to_render = ko.observable(); self.current_printer_profile_guid = ko.observable(); @@ -39,34 +67,89 @@ $(function () { self.current_trigger_profile_guid = ko.observable(); self.current_snapshot_profile_guid = ko.observable(); self.current_rendering_profile_guid = ko.observable(); - self.current_debug_profile_guid = ko.observable(); + self.current_logging_profile_guid = ko.observable(); self.current_settings_showing = ko.observable(true); self.profiles = ko.observable({ - 'printers': ko.observableArray([{name: "Unknown", guid: "", has_been_saved_by_user: false}]), - 'stabilizations': ko.observableArray([{name: "Unknown", guid: ""}]), - 'triggers': ko.observableArray([{name: "Unknown", guid: ""}]), - 'snapshots': ko.observableArray([{name: "Unknown", guid: ""}]), - 'renderings': ko.observableArray([{name: "Unknown", guid: ""}]), - 'cameras': ko.observableArray([{name: "Unknown", guid: "", enabled: false}]), - 'debug_profiles': ko.observableArray([{name: "Unknown", guid: ""}]) + 'printers': ko.observableArray([new Octolapse.CurrentSettingViewModel("printer", {name: "Unknown", guid: "", description:"",has_been_saved_by_user: false})]), + 'stabilizations': ko.observableArray([new Octolapse.CurrentSettingViewModel("stabilization", {name: "Unknown", guid: "", description:""})]), + 'triggers': ko.observableArray([new Octolapse.CurrentSettingViewModel("trigger", {name: "Unknown", guid: "", description:""})]), + 'snapshots': ko.observableArray([new Octolapse.CurrentSettingViewModel("snapshot", {name: "Unknown", guid: "", description:""})]), + 'renderings': ko.observableArray([new Octolapse.CurrentSettingViewModel("rendering", {name: "Unknown", guid: "", description:""})]), + 'cameras': ko.observableArray([new Octolapse.CurrentSettingViewModel("camera", {name: "Unknown", guid: "", description:"", enabled: false})]), + 'logging_profiles': ko.observableArray([new Octolapse.CurrentSettingViewModel("logging_profile", {name: "Unknown", guid: "", description:""})]) }); self.is_real_time = ko.observable(true); + self.is_test_mode_active = ko.observable(false); + self.wait_for_moves_to_finish = ko.observable(false); self.current_camera_guid = ko.observable(null); + self.dialog_rendering_unfinished = new Octolapse.OctolapseDialogRenderingUnfinished(); + self.dialog_rendering_in_process = new Octolapse.OctolapseDialogRenderingInProcess(); + self.timelapse_files_dialog = new Octolapse.OctolapseTimelapseFilesDialog(); self.PrinterState = new Octolapse.printerStateViewModel(); self.Position = new Octolapse.positionViewModel(); self.ExtruderState = new Octolapse.extruderStateViewModel(); self.TriggerState = new Octolapse.TriggersStateViewModel(); self.SnapshotPlanState = new Octolapse.snapshotPlanStateViewModel(); - self.WebcamSettings = new Octolapse.WebcamSettingsPopupViewModel(); + self.webcam_settings_popup = new Octolapse.WebcamSettingsPopupViewModel("octolapse_tab_custom_image_preferences_popup"); self.SnapshotPlanPreview = new Octolapse.SnapshotPlanPreviewPopupViewModel({ on_closed: function(){ self.SnapshotPlanState.is_preview = false; } }); - self.IsTabShowing = false; self.IsLatestSnapshotDialogShowing = false; self.current_print_volume = null; self.current_camera_enabled = ko.observable(false); + self.show_play_button = ko.observable(false); + self.camera_image_states = {}; + self.current_camera_state_text = ko.observable(""); + self.canEditSettings = ko.pureComputed(function(){ + // Get the current camera profile + var current_camera = self.getCurrentProfileByGuid(self.profiles().cameras(),self.current_camera_guid()); + if (current_camera != null) + { + return current_camera.enable_custom_image_preferences; + } + return false; + }); + + self.showWebcamSettings = function(){ + self.webcam_settings_popup.showWebcamSettingsForGuid(self.current_camera_guid()); + }; + + self.openTimelapseFilesDialog = function() { + self.timelapse_files_dialog.open(); + }; + + self.getEnabledButtonText = ko.pureComputed(function(){ + if (Octolapse.Globals.main_settings.is_octolapse_enabled()) + { + if (self.is_timelapse_active()) + return "Plugin Enabled and Running"; + else + return "Plugin Enabled"; + } + else{ + return "Plugin Disabled"; + } + }); + + self.open_rendering_text = ko.pureComputed(function(){ + if(!self.dialog_rendering_unfinished.is_empty()) + return "Failed Renderings"; + return "Renderings"; + }); + + self.unfinished_renderings_changed = function(data){ + if (data.failed) + { + self.dialog_rendering_unfinished.update(data.failed); + } + if (data.in_process) + { + self.dialog_rendering_in_process.update(data.in_process); + } + }; + self.showLatestSnapshotDialog = function () { var $SnapshotDialog = $("#octolapse_latest_snapshot_dialog"); @@ -95,9 +178,9 @@ $(function () { closeSnapshotDialog($SnapshotDialog); }); - $SnapshotDialog.find('.snapshot-container').one('click', function() { + /*$SnapshotDialog.find('#octolapse_snapshot_image_container').one('click', function() { closeSnapshotDialog($SnapshotDialog); - } ); + } );*/ function closeSnapshotDialog($dialog) { //console.log("Hiding snapshot dialog."); @@ -132,10 +215,121 @@ $(function () { self.onAfterBinding = function () { self.current_settings_showing.subscribe(function (newData) { //console.log("Setting local storage (" + self.SETTINGS_VISIBLE_KEY + ") to " + newData); - Octolapse.setLocalStorage(self.SETTINGS_VISIBLE_KEY, newData) + Octolapse.setLocalStorage(self.SETTINGS_VISIBLE_KEY, newData); }); + self.dialog_rendering_in_process.on_after_binding(); + self.dialog_rendering_unfinished.on_after_binding(); + self.timelapse_files_dialog.on_after_binding(); + Octolapse.Help.bindHelpLinks("#octolapse_tab"); + }; + + // Update the current tab state + self.updateState = function(state){ + //console.log("octolapse.status.js - Updating State") + if (state.trigger_type != null) + self.is_real_time(state.trigger_type === "real-time"); + + if (state.position != null) { + self.Position.update(state.position); + } + if (state.printer_state != null) { + self.PrinterState.update(state.printer_state); + } + if (state.extruder != null) { + self.ExtruderState.update(state.extruder); + } + if (state.trigger_state != null) { + self.TriggerState.update(state.trigger_state); + } + if (state.snapshot_plan != null) { + self.SnapshotPlanState.update(state.snapshot_plan); + } + + }; + + self.create_current_settings_profile = function(type, values){ + var profiles = []; + if (values) + { + for (var index=0; index < values.length; index++) + { + profiles.push(new Octolapse.CurrentSettingViewModel(type, values[index])); + } + } + return profiles; + }; + + self.load_files = function(){ + //console.log("ocotlapse.status.js - loading dialog files."); + self.timelapse_files_dialog.load(); + self.dialog_rendering_in_process.load(); + self.dialog_rendering_unfinished.load(); + }; + + self.files_changed = function(file_info, action){ + self.timelapse_files_dialog.files_changed(file_info, action); + }; + + self.update = function (settings) { + //console.log("octolapse.status.js - Updating main settings."); + if (settings.is_timelapse_active !== undefined) + self.is_timelapse_active(settings.is_timelapse_active); + if (settings.snapshot_count !== undefined) + self.snapshot_count(settings.snapshot_count); + if (settings.snapshot_failed_count !== undefined) + self.snapshot_failed_count(settings.snapshot_failed_count); + if (settings.is_taking_snapshot !== undefined) + self.is_taking_snapshot(settings.is_taking_snapshot); + if (settings.is_rendering !== undefined) + self.is_rendering(settings.is_rendering); + if (settings.waiting_to_render !== undefined) + self.waiting_to_render(settings.waiting_to_render); + if (settings.is_test_mode_active !== undefined) + self.is_test_mode_active(settings.is_test_mode_active); + + //console.log("Updating Profiles"); + if (settings.profiles) { + self.profiles().printers(self.create_current_settings_profile("printer", settings.profiles.printers)); + self.profiles().stabilizations(self.create_current_settings_profile("stabilization", settings.profiles.stabilizations)); + self.profiles().triggers(self.create_current_settings_profile("trigger", settings.profiles.triggers)); + self.profiles().renderings(self.create_current_settings_profile("rendering", settings.profiles.renderings)); + self.profiles().cameras(self.create_current_settings_profile("camera", settings.profiles.cameras)); + self.profiles().logging_profiles(self.create_current_settings_profile("logging_profile", settings.profiles.logging_profiles)); + self.current_printer_profile_guid(settings.profiles.current_printer_profile_guid); + self.current_stabilization_profile_guid(settings.profiles.current_stabilization_profile_guid); + self.current_trigger_profile_guid(settings.profiles.current_trigger_profile_guid); + self.current_snapshot_profile_guid(settings.profiles.current_snapshot_profile_guid); + self.current_rendering_profile_guid(settings.profiles.current_rendering_profile_guid); + self.current_logging_profile_guid(settings.profiles.current_logging_profile_guid); + } + if (settings.unfinished_renderings) + { + self.dialog_rendering_in_process.update(settings.unfinished_renderings); + self.dialog_rendering_unfinished.update(settings.unfinished_renderings); + } + + self.is_real_time(self.getCurrentTriggerProfileIsRealTime()); + self.current_camera_guid(self.getInitialCameraSelection()); + self.set_current_camera_enabled(); }; + self.current_stabilization_profile_guid.subscribe(function(newValue){ + var current_stabilization_profile = null; + for (var i = 0; i < self.profiles().stabilizations().length; i++) { + var stabilization_profile = self.profiles().stabilizations()[i]; + if (stabilization_profile.guid == self.current_stabilization_profile_guid()) { + current_stabilization_profile = stabilization_profile; + break; + } + } + if (current_stabilization_profile) + self.wait_for_moves_to_finish(current_stabilization_profile.wait_for_moves_to_finish); + else + self.wait_for_moves_to_finish(false); + }); + + + // Subscribe to current camera guid changes self.current_camera_guid.subscribe(function(newValue){ self.snapshotCameraChanged(); @@ -173,12 +367,11 @@ $(function () { }; self.hasOneCameraEnabled = ko.pureComputed(function(){ - var hasConfigIssue = true; for (var i = 0; i < self.profiles().cameras().length; i++) { - if(self.profiles().cameras()[i].enabled) + if(self.profiles().cameras()[i].enabled()) { - return true + return true; } } return false; @@ -201,7 +394,7 @@ $(function () { return true; },this); - self.is_current_trigger_real_time = function(){ + self.getCurrentTriggerProfileIsRealTime = function(){ var current_trigger = self.getCurrentProfileByGuid(self.profiles().triggers(),Octolapse.Status.current_trigger_profile_guid()); if (current_trigger != null) //console.log(current_trigger.trigger_type); @@ -213,7 +406,7 @@ $(function () { if (guid != null) { for (var i = 0; i < profiles.length; i++) { if (profiles[i].guid == guid) { - return profiles[i] + return profiles[i]; } } } @@ -225,18 +418,6 @@ $(function () { return hasConfigIssues; },this); - self.onTabChange = function (current, previous) { - if (current != null && current === "#tab_plugin_octolapse") { - //console.log("Octolapse Tab is showing"); - self.IsTabShowing = true; - self.updateLatestSnapshotThumbnail(true, true); - - } - else if (previous != null && previous === "#tab_plugin_octolapse") { - //console.log("Octolapse Tab is not showing"); - self.IsTabShowing = false; - } - }; /* Snapshot client animation preview functions */ @@ -255,8 +436,8 @@ $(function () { } //console.log("Starting Snapshot Animation for" + targetId); // Hide and show the play/refresh button - if (Octolapse.Globals.auto_reload_latest_snapshot()) { - var $startAnimationButton = $('#' + targetId + ' .snapshot_refresh_container a.start-animation'); + if (Octolapse.Globals.main_settings.auto_reload_latest_snapshot()) { + var $startAnimationButton = $('#' + targetId + ' .octolapse-snapshot-button-overlay a.play'); self.IsAnimating[targetId] = true; $startAnimationButton.fadeOut({ start: function () { @@ -267,8 +448,7 @@ $(function () { if ($images.length > 0) { $current.css("opacity","0"); - animate_snapshots($images, $current, $images.length-1, -1, 0, 25) - + animate_snapshots($images, $current, $images.length-1, -1, 0, 25); } else { @@ -277,8 +457,6 @@ $(function () { self.IsAnimating[targetId] = false; return; } - - function animate_snapshots($images, $current, index, step, opacity, delay) { if ( (step > 0 && index < $images.length) || @@ -287,7 +465,7 @@ $(function () { { setTimeout(function () { $images.eq(index).css("opacity",opacity.toString()); - animate_snapshots($images, $current, index+step, step, opacity, delay) + animate_snapshots($images, $current, index+step, step, opacity, delay); }, delay); } else if(step > 0) @@ -300,7 +478,7 @@ $(function () { } else { - // fade out the current image + // fade out the current image animate_snapshots($images, $current, 0, 1, 1, 100); } @@ -318,22 +496,40 @@ $(function () { force = force || false; //console.log("Trying to update the latest snapshot thumbnail."); if (!force) { - if (!self.IsTabShowing) { - //console.log("The tab is not showing, not updating the thumbnail. Clearing the image history."); - return - } - else if (!Octolapse.Globals.auto_reload_latest_snapshot()) { + if (!Octolapse.Globals.main_settings.auto_reload_latest_snapshot()) { //console.log("Not updating the thumbnail, auto-reload is disabled."); - return + return; } } - if (self.current_camera_guid() === null || self.current_camera_guid() === "") - console.error("Current camera guid requested, but it is null."); - else - self.updateSnapshotAnimation('octolapse_snapshot_thumbnail_container', getLatestSnapshotThumbnailUrl(self.current_camera_guid()) - + "&time=" + new Date().getTime()); + var snapshotUrl = null; + if (self.current_camera_guid()) + { + snapshotUrl = getLatestSnapshotThumbnailUrl(self.current_camera_guid())+ "&time=" + new Date().getTime(); + } + self.updateSnapshotAnimation('octolapse_snapshot_thumbnail_container', snapshotUrl); + }; + + self.updateLatestSnapshotImage = function (force) { + force = force || false; + //console.log("Trying to update the latest snapshot image."); + if (!force) { + if (!Octolapse.Globals.main_settings.auto_reload_latest_snapshot()) { + //console.log("Auto-Update latest snapshot image is disabled."); + return; + } + else if (!self.IsLatestSnapshotDialogShowing) { + //console.log("The full screen dialog is not showing, not updating the latest snapshot."); + return; + } + } + var snapshotUrl = null; + if (self.current_camera_guid()) + { + snapshotUrl = getLatestSnapshotUrl(self.current_camera_guid())+ "&time=" + new Date().getTime(); + } + self.updateSnapshotAnimation('octolapse_snapshot_image_container', snapshotUrl); }; self.erasePreviousSnapshotImages = function (targetId, eraseCurrentImage) { @@ -358,7 +554,9 @@ $(function () { // Get the latest image var $latestSnapshotContainer = $target.find('.latest-snapshot'); var $latestSnapshot = $latestSnapshotContainer.find('img'); - if (Octolapse.Globals.auto_reload_latest_snapshot()) { + var $fullscreenControl = $target.find("a.octolapse-fullscreen"); + + if (Octolapse.Globals.main_settings.auto_reload_latest_snapshot()) { // Get the previous snapshot container var $previousSnapshotContainer = $target.find('.previous-snapshots'); @@ -378,8 +576,8 @@ $(function () { var $previousSnapshots = $previousSnapshotContainer.find("img"); var numSnapshots = $previousSnapshots.length; - - while (numSnapshots > parseInt(Octolapse.Globals.auto_reload_frames())) { + self.show_play_button(numSnapshots>0); + while (numSnapshots > parseInt(Octolapse.Globals.main_settings.auto_reload_frames())) { //console.log("Removing overflow previous images according to Auto Reload Frames setting."); var $element = $previousSnapshots.first(); $element.remove(); @@ -396,29 +594,58 @@ $(function () { $element.css('z-index', previousImageIndex.toString()); } } - + else + { + self.show_play_button(false); + } // create the newest image var $newSnapshot = $(document.createElement('img')); - // Clear any existing events - $newSnapshot.off('load'); - $newSnapshot.off('error'); - // append the image to the container + self.current_camera_state_text(""); + $fullscreenControl.hide(); + // Set the error handler + var error_message = "No snapshots have been taken with the current camera. A preview of your" + + " timelapse will start to appear here as snapshots are taken by Octolapse."; + var on_snapshot_load_error = function(){ + //console.error("An error occurred loading the newest image, reverting to previous image."); + // move the latest preview image back into the newest image section + $latestSnapshot.removeClass(); + $latestSnapshot.appendTo($latestSnapshotContainer); + self.current_camera_state_text(error_message); + + }; + + if (!newSnapshotAddress) + { + error_message = "No camera is selected. Choose a camera in the dropdown below."; + on_snapshot_load_error(error_message); + return; + } + + $newSnapshot.one('error', on_snapshot_load_error); //console.log("Adding the new snapshot image to the latest snapshot container."); // create on load event for the newest image - if (Octolapse.Globals.auto_reload_latest_snapshot()) { + if (Octolapse.Globals.main_settings.auto_reload_latest_snapshot()) { // Add the new snapshot to the container $newSnapshot.appendTo($latestSnapshotContainer); - $newSnapshot.one('load', function () { - self.startSnapshotAnimation(targetId); - }); - + if ((!(targetId in self.IsAnimating) || !self.IsAnimating[targetId])) + { + $newSnapshot.one('load', function () { + self.startSnapshotAnimation(targetId); + $fullscreenControl.show(); + }); + } + else { + // We could have a race condition here. Need to ensure the opacity of this element is + // set to 1 after any animations are finished. + $newSnapshot.css("opacity","0"); + } } else { $newSnapshot.one('load', function () { + $fullscreenControl.show(); // Hide the latest image - if ($latestSnapshot.length == 1) { $latestSnapshot.fadeOut(250, function () { @@ -444,44 +671,27 @@ $(function () { } }); } - $newSnapshot.one('error', function () { - //console.log("An error occurred loading the newest image, reverting to previous image."); - // move the latest preview image back into the newest image section - $latestSnapshot.removeClass(); - $latestSnapshot.appendTo($latestSnapshotContainer) - - }); - // set the src and start to load - $newSnapshot.attr('src', newSnapshotAddress) + $newSnapshot.attr('src', newSnapshotAddress); }; - self.updateLatestSnapshotImage = function (force) { - force = force || false; - //console.log("Trying to update the latest snapshot image."); - if (!force) { - if (!Octolapse.Globals.auto_reload_latest_snapshot()) { - //console.log("Auto-Update latest snapshot image is disabled."); - return - } - else if (!self.IsLatestSnapshotDialogShowing) { - //console.log("The full screen dialog is not showing, not updating the latest snapshot."); - return - } - } - //console.log("Requesting image for camera:" + Octolapse.Status.current_camera_guid()) - if(!(Octolapse.Status.current_camera_guid() == null || Octolapse.Status.current_camera_guid() == "")) - self.updateSnapshotAnimation('octolapse_snapshot_image_container', getLatestSnapshotUrl(Octolapse.Status.current_camera_guid()) + "&time=" + new Date().getTime()); - - }; - - self.toggleInfoPanel = function (panelType){ + self.toggleInfoPanel = function (observable, panelType){ + data = { + panel_type: panelType, + client_id: Octolapse.Globals.client_id + }; $.ajax({ url: "./plugin/octolapse/toggleInfoPanel", type: "POST", - data: JSON.stringify({panel_type: panelType}), + data: JSON.stringify(data), contentType: "application/json", dataType: "json", + success: function(result){ + if (result.success) + { + observable(result.enabled); + } + }, error: function (XMLHttpRequest, textStatus, errorThrown) { var message = "Unable to toggle the panel. Status: " + textStatus + ". Error: " + errorThrown; var options = { @@ -491,7 +701,7 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -511,7 +721,7 @@ $(function () { case "timer": return "timer-trigger-status-template"; default: - return "trigger-status-template" + return "trigger-status-template"; } }; @@ -521,11 +731,11 @@ $(function () { return "Octolapse is waiting for print to complete."; if( self.is_rendering()) return "Octolapse is rendering a timelapse."; - if(!Octolapse.Globals.enabled()) + if(!Octolapse.Globals.main_settings.is_octolapse_enabled()) return 'Octolapse is disabled.'; return 'Octolapse is enabled and idle.'; } - if(!Octolapse.Globals.enabled()) + if(!Octolapse.Globals.main_settings.is_octolapse_enabled()) return 'Octolapse is disabled.'; if(Octolapse.Status.is_real_time()) { @@ -540,61 +750,15 @@ $(function () { }, self); - self.getTimelapseStateText = ko.pureComputed(function () { - //console.log("GettingTimelapseStateText") - if(!self.is_timelapse_active()) - return 'Octolapse is not running'; - if(!self.PrinterState.is_initialized()) - return 'Waiting for update from server. You may have to turn on the "Position State Info Panel" from the "Current Settings" below to receive an update.'; - if( self.PrinterState.hasPrinterStateErrors()) - return 'Waiting to initialize'; - return 'Octolapse is initialized and running'; - }, self); - - self.getTimelapseStateColor = ko.pureComputed(function () { - if(!self.is_timelapse_active()) - return ''; - if(self.is_real_time() && (!self.PrinterState.is_initialized() || self.PrinterState.hasPrinterStateErrors())) - return 'orange'; - return 'greenyellow'; - }, self); - self.getStatusText = ko.pureComputed(function () { if (self.is_timelapse_active() || self.is_rendering()) - return 'Octolapse'; + return ''; if (self.waiting_to_render()) - return 'Octolapse - Stopped'; - if (Octolapse.Globals.enabled()) - return 'Octolapse'; - return 'Octolapse - Disabled'; + return 'Timelapse Canceled'; + return ''; }, self); - self.updateState = function(state) - { - - //console.log("octolapse.status.js - Updating State") - if (state.trigger_type != null) - self.is_real_time(state.trigger_type == "real-time"); - - if (state.position != null) { - self.Position.update(state.position); - } - if (state.printer_state != null) { - self.PrinterState.update(state.printer_state); - } - if (state.extruder != null) { - self.ExtruderState.update(state.extruder); - } - if (state.trigger_state != null) { - self.TriggerState.update(state.trigger_state); - } - if (state.snapshot_plan != null) { - self.SnapshotPlanState.update(state.snapshot_plan); - } - - }; - self.previewSnapshotPlans = function(data) - { + self.previewSnapshotPlans = function(data){ //console.log("Updating snapshot plan state with a preview of the snapshot plans"); self.SnapshotPlanState.is_preview = true; self.SnapshotPlanState.update(data); @@ -602,35 +766,6 @@ $(function () { }; - self.update = function (settings) { - //console.log("octolapse.status.js - Updating main settings."); - self.is_timelapse_active(settings.is_timelapse_active); - self.snapshot_count(settings.snapshot_count); - self.is_taking_snapshot(settings.is_taking_snapshot); - self.is_rendering(settings.is_rendering); - self.waiting_to_render(settings.waiting_to_render); - //console.log("Updating Profiles"); - self.profiles().printers(settings.profiles.printers); - self.profiles().stabilizations(settings.profiles.stabilizations); - self.profiles().triggers(settings.profiles.triggers); - self.profiles().renderings(settings.profiles.renderings); - self.profiles().cameras(settings.profiles.cameras); - self.profiles().debug_profiles(settings.profiles.debug_profiles); - self.current_printer_profile_guid(settings.profiles.current_printer_profile_guid); - self.current_stabilization_profile_guid(settings.profiles.current_stabilization_profile_guid); - self.current_trigger_profile_guid(settings.profiles.current_trigger_profile_guid); - self.current_snapshot_profile_guid(settings.profiles.current_snapshot_profile_guid); - self.current_rendering_profile_guid(settings.profiles.current_rendering_profile_guid); - self.current_debug_profile_guid(settings.profiles.current_debug_profile_guid); - - self.is_real_time(self.is_current_trigger_real_time()); - self.current_camera_guid(self.getInitialCameraSelection()); - self.set_current_camera_enabled(); - // Update snapshots - //self.updateLatestSnapshotImage(true); - //self.updateLatestSnapshotThumbnail(true, false); - }; - self.onTimelapseStart = function () { self.TriggerState.removeAll(); self.PrinterState.is_initialized(false); @@ -664,7 +799,7 @@ $(function () { hide: false, addclass: "octolapse", desktop: { - desktop: true + desktop: false } }; Octolapse.displayPopup(options); @@ -699,7 +834,8 @@ $(function () { } }; // Printer Profile Settings - self.printers_sorted = ko.computed(function() { return self.nameSort(self.profiles().printers) }); + self.printers_sorted = ko.computed(function() { return self.nameSort(self.profiles().printers); }); + self.openCurrentPrinterProfile = function () { //console.log("Opening current printer profile from tab.") Octolapse.Printers.showAddEditDialog(self.current_printer_profile_guid(), false); @@ -721,7 +857,7 @@ $(function () { }; // Stabilization Profile Settings - self.stabilizations_sorted = ko.computed(function() { return self.nameSort(self.profiles().stabilizations) }); + self.stabilizations_sorted = ko.computed(function() { return self.nameSort(self.profiles().stabilizations); }); self.openCurrentStabilizationProfile = function () { //console.log("Opening current stabilization profile from tab.") Octolapse.Stabilizations.showAddEditDialog(self.current_stabilization_profile_guid(), false); @@ -738,8 +874,9 @@ $(function () { } }; + // Trigger Profile Settings - self.triggers_sorted = ko.computed(function() { return self.nameSort(self.profiles().triggers) }); + self.triggers_sorted = ko.computed(function() { return self.nameSort(self.profiles().triggers); }); self.openCurrentTriggerProfile = function () { //console.log("Opening current trigger profile from tab.") Octolapse.Triggers.showAddEditDialog(self.current_trigger_profile_guid(), false); @@ -756,9 +893,8 @@ $(function () { } }; - // Rendering Profile Settings - self.renderings_sorted = ko.computed(function() { return self.nameSort(self.profiles().renderings) }); + self.renderings_sorted = ko.computed(function() { return self.nameSort(self.profiles().renderings); }); self.openCurrentRenderingProfile = function () { //console.log("Opening current rendering profile from tab.") Octolapse.Renderings.showAddEditDialog(self.current_rendering_profile_guid(), false); @@ -776,21 +912,20 @@ $(function () { }; // Camera Profile Settings - self.cameras_sorted = ko.computed(function() { return self.nameSort(self.profiles().cameras) }); - + self.cameras_sorted = ko.computed(function() { return self.nameSort(self.profiles().cameras); }); self.openCameraProfile = function (guid) { //console.log("Opening current camera profile from tab.") Octolapse.Cameras.showAddEditDialog(guid, false); }; - self.addNewCameraProfile = function () { //console.log("Opening current camera profile from tab.") Octolapse.Cameras.showAddEditDialog(null, false); }; - self.toggleCamera = function (guid) { //console.log("Opening current camera profile from tab.") - Octolapse.Cameras.getProfileByGuid(guid).toggleCamera(); + Octolapse.Cameras.getProfileByGuid(guid).toggleCamera(function(value){ + self.getCurrentProfileByGuid(self.profiles().cameras(), guid).enabled(value); + }); }; self.set_current_camera_enabled = function() { var current_camera = null; @@ -802,11 +937,10 @@ $(function () { } } if (current_camera) - self.current_camera_enabled(current_camera.enabled); + self.current_camera_enabled(current_camera.enabled()); else self.current_camera_enabled(true); }; - self.snapshotCameraChanged = function() { if (self.current_camera_guid()) { // Set the local storage guid here. @@ -824,24 +958,61 @@ $(function () { self.updateLatestSnapshotImage(true); }; - // Debug Profile Settings - self.debug_sorted = ko.computed(function() { return self.nameSort(self.profiles().debug_profiles) }); - self.openCurrentDebugProfile = function () { - //console.log("Opening current debug profile from tab.") - Octolapse.DebugProfiles.showAddEditDialog(self.current_debug_profile_guid(), false); + // Logging Profile Settings + self.logging_profiles_sorted = ko.computed(function() { return self.nameSort(self.profiles().logging_profiles); }); + self.openCurrentLoggingProfile = function () { + //console.log("Opening current logging profile from tab.") + Octolapse.LoggingProfiles.showAddEditDialog(self.current_logging_profile_guid(), false); }; - self.defaultDebugProfileChanged = function (obj, event) { + self.defaultLoggingProfileChanged = function (obj, event) { if (Octolapse.Globals.is_admin()) { if (event.originalEvent) { // Get the current guid - var guid = $("#octolapse_tab_debug_profile").val(); - //console.log("Default Debug Profile is changing to " + guid); - Octolapse.DebugProfiles.setCurrentProfile(guid); + var guid = $("#octolapse_tab_logging_profile").val(); + //console.log("Default Logging Profile is changing to " + guid); + Octolapse.LoggingProfiles.setCurrentProfile(guid); return true; } } }; + self.setDescriptionAsTitle = function(option, item) { + if (!item) + return; + ko.applyBindingsToNode(option, {attr: {title: item.description}}, item); + }; + + self.getPrinterProfileTitle = ko.computed(function () { + var currentProfile = self.getCurrentProfileByGuid(self.profiles().printers(),Octolapse.Status.current_printer_profile_guid()); + if (currentProfile == null) + return ""; + return currentProfile.description; + }); + self.getStabilizationProfileTitle = ko.computed(function () { + var currentProfile = self.getCurrentProfileByGuid(self.profiles().stabilizations(),Octolapse.Status.current_stabilization_profile_guid()); + if (currentProfile == null) + return ""; + return currentProfile.description; + }); + self.getTriggerProfileTitle = ko.computed(function () { + var currentProfile = self.getCurrentProfileByGuid(self.profiles().triggers(),Octolapse.Status.current_trigger_profile_guid()); + if (currentProfile == null) + return ""; + return currentProfile.description; + }); + self.getRenderingProfileTitle = ko.computed(function () { + var currentProfile = self.getCurrentProfileByGuid(self.profiles().renderings(),Octolapse.Status.current_rendering_profile_guid()); + if (currentProfile == null) + return ""; + return currentProfile.description; + }); + self.getLoggingProfileTitle = ko.computed(function () { + var currentProfile = self.getCurrentProfileByGuid(self.profiles().logging_profiles(),Octolapse.Status.current_logging_profile_guid()); + if (currentProfile == null) + return ""; + return currentProfile.description; + }); + }; /* Status Tab viewmodels @@ -852,6 +1023,7 @@ $(function () { self.x_homed = ko.observable(false); self.y_homed = ko.observable(false); self.z_homed = ko.observable(false); + self.has_definite_position = ko.observable(false); self.is_layer_change = ko.observable(false); self.is_height_change = ko.observable(false); self.is_in_position = ko.observable(false); @@ -862,30 +1034,29 @@ $(function () { self.layer = ko.observable(0); self.height = ko.observable(0).extend({numeric: 2}); self.last_extruder_height = ko.observable(0).extend({numeric: 2}); - self.has_position_error = ko.observable(false); - self.position_error = ko.observable(false); self.is_metric = ko.observable(null); self.is_initialized = ko.observable(false); + self.is_printer_primed = ko.observable(false); self.update = function (state) { - this.gcode(state.gcode); - this.x_homed(state.x_homed); - this.y_homed(state.y_homed); - this.z_homed(state.z_homed); - this.is_layer_change(state.is_layer_change); - this.is_height_change(state.is_height_change); - this.is_in_position(state.is_in_position); - this.in_path_position(state.in_path_position); - this.is_zhop(state.is_zhop); - this.is_relative(state.is_relative); - this.is_extruder_relative(state.is_extruder_relative); - this.layer(state.layer); - this.height(state.height); - this.last_extruder_height(state.last_extruder_height); - this.has_position_error(state.has_position_error); - this.position_error(state.position_error); - this.is_metric(state.is_metric); - this.is_initialized(true); + self.gcode(state.gcode); + self.x_homed(state.x_homed); + self.y_homed(state.y_homed); + self.z_homed(state.z_homed); + self.has_definite_position(state.has_definite_position); + self.is_layer_change(state.is_layer_change); + self.is_height_change(state.is_height_change); + self.is_in_position(state.is_in_position); + self.in_path_position(state.in_path_position); + self.is_zhop(state.is_zhop); + self.is_relative(state.is_relative); + self.is_extruder_relative(state.is_extruder_relative); + self.layer(state.layer); + self.height(state.height); + self.last_extruder_height(state.last_extruder_height); + self.is_metric(state.is_metric); + self.is_printer_primed(state.is_printer_primed); + self.is_initialized(true); }; self.getCheckedIconClass = function (value, trueClass, falseClass, nullClass) { @@ -901,7 +1072,6 @@ $(function () { }); }; - self.getColor = function (value, trueColor, falseColor, nullColor) { return ko.computed({ read: function () { @@ -922,7 +1092,7 @@ $(function () { || self.is_relative() == null || self.is_extruder_relative() == null || !self.is_metric() - || self.has_position_error()) + ) return true; return false; },self); @@ -946,7 +1116,14 @@ $(function () { return "Not a zhop"; }, self); - self.getis_in_printerStateText = ko.pureComputed(function () { + self.getHasDefinitePositionText = ko.pureComputed(function(){ + if (self.has_definite_position()) + return "Current position is definite"; + else + return "Current position is not fully known."; + }); + + self.getIsInPositionStateText = ko.pureComputed(function () { if (self.is_in_position()) return "In position"; else if (self.in_path_position()) @@ -955,7 +1132,7 @@ $(function () { return "Not in position"; }, self); - self.getis_metricStateText = ko.pureComputed(function () { + self.getIsMetricStateText = ko.pureComputed(function () { if (self.is_metric()) return "Metric"; else if (self.is_metric() === null) @@ -996,20 +1173,21 @@ $(function () { else return "Absolute"; }, self); - - self.getHasPositionErrorStateText = ko.pureComputed(function () { - if (self.has_position_error()) - return "A position error was detected"; - else - return "No current position errors"; - }, self); - self.getis_layer_changeStateText = ko.pureComputed(function () { + self.getIsLayerChangeStateText = ko.pureComputed(function () { if (self.is_layer_change()) return "Layer change detected"; else return "Not changing layers"; }, self); + + self.getIsPrinterPrimedStateTitle = ko.pureComputed(function(){ + if(self.is_printer_primed()) + return "Primed"; + else + return "Not Primed"; + }, self); }; + Octolapse.positionViewModel = function () { var self = this; self.f = ko.observable(0).extend({numeric: 2}); @@ -1031,21 +1209,10 @@ $(function () { this.z_offset(state.z_offset); this.e(state.e); this.e_offset(state.e_offset); - //self.plotPosition(state.x, state.y, state.z); }; - /* - self.plotPosition = function(x, y,z) - { - //console.log("Plotting Position") - var canvas = document.getElementById("octolapse_position_canvas"); - canvas.width = 250; - canvas.height = 200; - var ctx = canvas.getContext("2d"); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillRect(x + 2, x - 2,4, 4); - - }*/ + }; + Octolapse.extruderStateViewModel = function () { var self = this; // State variables @@ -1090,7 +1257,7 @@ $(function () { else if (self.is_retracted() && !self.is_partially_retracted()) return "fa-angle-double-up"; } - return "fa-times-circle"; + return "fa-times"; }, self); self.getRetractionStateText = ko.pureComputed(function () { @@ -1116,12 +1283,12 @@ $(function () { self.getDeretractionIconClass = ko.pureComputed(function () { if (self.is_retracting() && self.is_deretracting()) - return "fa-exclamation-circle"; + return "fa-exclamation"; if (self.is_deretracting() && self.is_deretracting_start) return "fa-level-down"; if (self.is_deretracting()) - return "fa-long-arrow-down"; - return "fa-times-circle"; + return "fa-arrow-down"; + return "fa-times"; }, self); self.getDeretractionStateText = ko.pureComputed(function () { @@ -1148,12 +1315,12 @@ $(function () { return "exclamation-circle"; if (self.is_primed()) - return "fa-arrows-h"; + return "fa-minus"; if (self.is_extruding_start()) return "fa-play-circle-o"; if (self.is_extruding()) return "fa-play"; - return "fa-times-circle"; + return "fa-times"; }, self); self.getExtrudingStateText = ko.pureComputed(function () { if (self.is_extruding_start() && !self.is_extruding()) @@ -1167,6 +1334,7 @@ $(function () { return "None"; }, self); }; + Octolapse.TriggersStateViewModel = function () { var self = this; @@ -1230,7 +1398,7 @@ $(function () { self.is_waiting_on_extruder = ko.observable(state.is_waiting_on_extruder); self.require_zhop = ko.observable(state.require_zhop); self.trigger_count = ko.observable(state.trigger_count).extend({compactint: 1}); - self.is_homed = ko.observable(state.is_homed); + self.has_definite_position = ko.observable(state.has_definite_position); self.is_in_position = ko.observable(state.is_in_position); self.in_path_position = ko.observable(state.Isin_path_position); self.update = function (state) { @@ -1242,13 +1410,13 @@ $(function () { self.is_waiting_on_extruder(state.is_waiting_on_extruder); self.require_zhop(state.require_zhop); self.trigger_count(state.trigger_count); - self.is_homed(state.is_homed); + self.has_definite_position(state.has_definite_position); self.is_in_position(state.is_in_position); self.in_path_position(state.in_path_position); }; self.triggerBackgroundIconClass = ko.pureComputed(function () { - if (!self.is_homed()) - return "bg-not-homed"; + if (!self.has_definite_position()) + return "bg-unknown-position"; else if (!self.is_triggered() && Octolapse.PrinterStatus.isPaused()) return " bg-paused"; else @@ -1257,8 +1425,8 @@ $(function () { /* style related computed functions */ self.triggerStateText = ko.pureComputed(function () { //console.log("Calculating trigger state text."); - if (!self.is_homed()) - return "Idle until all axes are homed"; + if (!self.has_definite_position()) + return "Idle until a definite position is found"; else if (self.is_triggered()) return "Triggering a snapshot"; else if (Octolapse.PrinterStatus.isPaused()) @@ -1287,7 +1455,7 @@ $(function () { }, self); self.triggerIconClass = ko.pureComputed(function () { - if (!self.is_homed()) + if (!self.has_definite_position()) return "not-homed"; if (self.is_triggered()) return "trigger"; @@ -1306,6 +1474,7 @@ $(function () { return ""; }, self); }; + Octolapse.gcodeTriggerStateViewModel = function (state) { //console.log("creating gcode trigger state view model"); var self = this; @@ -1318,7 +1487,7 @@ $(function () { self.snapshot_command = ko.observable(state.snapshot_command); self.require_zhop = ko.observable(state.require_zhop); self.trigger_count = ko.observable(state.trigger_count).extend({compactint: 1}); - self.is_homed = ko.observable(state.is_homed); + self.has_definite_position = ko.observable(state.has_definite_position); self.is_in_position = ko.observable(state.is_in_position); self.in_path_position = ko.observable(state.Isin_path_position); self.update = function (state) { @@ -1331,74 +1500,61 @@ $(function () { self.snapshot_command(state.snapshot_command); self.require_zhop(state.require_zhop); self.trigger_count(state.trigger_count); - self.is_homed(state.is_homed); + self.has_definite_position(state.has_definite_position); self.is_in_position(state.is_in_position); self.in_path_position(state.in_path_position); }; - self.triggerBackgroundIconClass = ko.pureComputed(function () { - if (!self.is_homed()) - return "bg-not-homed"; - else if (!self.is_triggered() && Octolapse.PrinterStatus.isPaused()) - return " bg-paused"; - else - return ""; + self.getSnapshotCommands = ko.pureComputed(function () { + commands = ["@OCTOLAPSE TAKE-SNAPSHOT", "SNAP"]; + if (self.snapshot_command().length > 0) + { + commands.push(self.snapshot_command()); + } + return commands; }, self); /* style related computed functions */ self.triggerStateText = ko.pureComputed(function () { - if (!self.is_homed()) - return "Idle until all axes are homed"; + if (!self.has_definite_position()) + return "Idle until the printer's position is known."; else if (self.is_triggered()) - return "Triggering a snapshot"; + return "Triggering a snapshot."; else if (Octolapse.PrinterStatus.isPaused()) - return "Paused"; + return "The trigger is paused."; else if (self.is_waiting()) { - // Create a list of things we are waiting on - var waitText = "Waiting"; + var waitText = "Waiting for"; var waitList = []; if (self.is_waiting_on_zhop()) waitList.push("zhop"); if (self.is_waiting_on_extruder()) waitList.push("extruder"); if (!self.is_in_position() && !self.in_path_position()) + { waitList.push("position"); - if (waitList.length > 1) { - waitText += " for " + waitList.join(" and "); - waitText += " to trigger"; + //console.log("Waiting on position."); + } + if (waitList.length > 0) { + if (waitList.length === 1) { + waitText += waitList[0]; + } + else if (waitList.length > 1) { + var commaSeparated = waitList.slice(0, waitList.length - 2); + waitText += commaSeparated.join(", "); + if (waitList.length > 2) + waitText += ", "; + waitText += waitList[waitList.length-1]; + } + waitText += " to trigger."; } - else if (waitList.length === 1) - waitText += " for " + waitList[0] + " to trigger"; return waitText; } - - else - return "Looking for snapshot gcode"; - - }, self); - self.triggerIconClass = ko.pureComputed(function () { - if (!self.is_homed()) - return "not-homed"; - if (self.is_triggered()) - return "trigger"; - if (Octolapse.PrinterStatus.isPaused()) - return "paused"; - if (self.is_waiting()) - return "wait"; - else - return "fa-inverse"; - }, self); - - self.getInfoText = ko.pureComputed(function () { - return "Triggering on gcode command: " + self.snapshot_command(); - - - }, self); - self.getInfoIconText = ko.pureComputed(function () { - return self.snapshot_command() + else{ + return "Waiting for a snapshot command."; + } }, self); - }; + Octolapse.layerTriggerStateViewModel = function (state) { //console.log("creating layer trigger state view model"); var self = this; @@ -1414,51 +1570,49 @@ $(function () { self.is_height_change = ko.observable(state.is_height_change); self.is_height_change_wait = ko.observable(state.is_height_change_wait); self.height_increment = ko.observable(state.height_increment).extend({numeric: 2}); + self.has_definite_position = ko.observable(state.has_definite_position); + self.is_in_position = ko.observable(state.is_in_position); + self.in_path_position = ko.observable(state.in_path_position); + self.require_zhop = ko.observable(state.require_zhop); self.trigger_count = ko.observable(state.trigger_count).extend({compactint: 1}); - self.is_homed = ko.observable(state.is_homed); self.layer = ko.observable(state.layer); - self.is_in_position = ko.observable(state.is_in_position); - self.in_path_position = ko.observable(state.in_path_position); + self.update = function (state) { self.type(state.type); self.name(state.name); + // Config Info + self.require_zhop(state.require_zhop); + self.height_increment(state.height_increment); + // Layer and current increment + self.layer(state.layer); + self.current_increment(state.current_increment); + self.trigger_count(state.trigger_count); + // Triggering/Waiting Status self.is_triggered(state.is_triggered); self.is_waiting(state.is_waiting); - self.is_waiting_on_zhop(state.is_waiting_on_zhop); - self.is_waiting_on_extruder(state.is_waiting_on_extruder); - self.current_increment(state.current_increment); self.is_layer_change(state.is_layer_change); self.is_layer_change_wait(state.is_layer_change_wait); self.is_height_change(state.is_height_change); self.is_height_change_wait(state.is_height_change_wait); - self.height_increment(state.height_increment); - self.require_zhop(state.require_zhop); - self.trigger_count(state.trigger_count); - self.is_homed(state.is_homed); - self.layer(state.layer); + self.is_waiting_on_zhop(state.is_waiting_on_zhop); + self.is_waiting_on_extruder(state.is_waiting_on_extruder); + self.has_definite_position(state.has_definite_position); self.is_in_position(state.is_in_position); self.in_path_position(state.in_path_position); + // Layer/Height change Info }; - self.triggerBackgroundIconClass = ko.pureComputed(function () { - if (!self.is_homed()) - return "bg-not-homed"; - else if (!self.is_triggered() && Octolapse.PrinterStatus.isPaused()) - return " bg-paused"; - }, self); /* style related computed functions */ self.triggerStateText = ko.pureComputed(function () { - if (!self.is_homed()) - return "Idle until all axes are homed"; + if (!self.has_definite_position()) + return "Idle until the printer's position is known."; else if (self.is_triggered()) - return "Triggering a snapshot"; + return "Triggering a snapshot."; else if (Octolapse.PrinterStatus.isPaused()) - return "Paused"; + return "The trigger is paused."; else if (self.is_waiting()) { - // Create a list of things we are waiting on - //console.log("Generating wait state text for LayerTrigger"); - var waitText = "Waiting"; + var waitText = "Waiting for"; var waitList = []; if (self.is_waiting_on_zhop()) waitList.push("zhop"); @@ -1469,59 +1623,55 @@ $(function () { waitList.push("position"); //console.log("Waiting on position."); } - if (waitList.length > 1) { - waitText += " for " + waitList.join(" and "); - waitText += " to trigger"; + if (waitList.length > 0) { + if (waitList.length === 1) { + waitText += waitList[0]; + } + else if (waitList.length > 1) { + var commaSeparated = waitList.slice(0, waitList.length - 2); + waitText += commaSeparated.join(", "); + if (waitList.length > 2) + waitText += ", "; + waitText += waitList[waitList.length-1]; + } + waitText += " to trigger."; } - - else if (waitList.length === 1) - waitText += " for " + waitList[0] + " to trigger"; return waitText; } - else if (self.height_increment() > 0) { - var heightToTrigger = self.height_increment() * self.current_increment(); - return "Triggering when height reaches " + heightToTrigger.toFixed(1) + " mm"; + else if (self.height_increment() != null && self.height_increment() > 0) { + var heightToTrigger = self.height_increment() * (self.current_increment() + 1); + return "Triggering when height reaches " + Octolapse.roundToIncrement(heightToTrigger,0.01).toString() + "mm."; } else - return "Triggering on next layer change"; + return "Triggering on next layer change."; }, self); - self.triggerIconClass = ko.pureComputed(function () { - if (!self.is_homed()) - return "not-homed"; - if (self.is_triggered()) - return "trigger"; - if (Octolapse.PrinterStatus.isPaused()) - return "paused"; - if (self.is_waiting()) - return " wait"; - else - return " fa-inverse"; - }, self); - - self.getInfoText = ko.pureComputed(function () { - var val = 0; - if (self.height_increment() > 0) - - val = self.height_increment() + " mm"; + self.triggerTypeText = ko.pureComputed(function(){ + if (self.height_increment() === 0) { + return "Layer"; + } + return "Height"; + }); - else - val = "layer"; - return "Triggering every " + Octolapse.ToCompactInt(val); + self.currentTriggerIncrement = ko.pureComputed(function(){ + if (self.height_increment() === 0) { + return self.layer; + } + return Octolapse.roundToIncrement(self.current_increment(),0.01).toString() + "mm"; + }); + self.triggerOnText = ko.pureComputed(function () { + if (self.height_increment !== null || self.height_increment() === 0) + { + return "Every Layer"; + } - }, self); - self.getInfoIconText = ko.pureComputed(function () { - var val = 0; - if (self.height_increment() > 0) - val = self.current_increment(); - else - val = self.layer(); - return Octolapse.ToCompactInt(val); + return "Every " + Octolapse.roundToIncrement(self.height_increment(),0.01).toString() + "mm"; }, self); }; + Octolapse.timerTriggerStateViewModel = function (state) { //console.log("creating timer trigger state view model"); var self = this; @@ -1537,7 +1687,7 @@ $(function () { self.pause_time = ko.observable(state.pause_time).extend({time: null}); self.require_zhop = ko.observable(state.require_zhop); self.trigger_count = ko.observable(state.trigger_count); - self.is_homed = ko.observable(state.is_homed); + self.has_definite_position = ko.observable(state.has_definite_position); self.is_in_position = ko.observable(state.is_in_position); self.in_path_position = ko.observable(state.Isin_path_position); self.update = function (state) { @@ -1553,67 +1703,59 @@ $(function () { self.pause_time(state.pause_time); self.interval_seconds(state.interval_seconds); self.trigger_count(state.trigger_count); - self.is_homed(state.is_homed); + self.has_definite_position(state.has_definite_position); self.is_in_position(state.is_in_position); self.in_path_position(state.in_path_position); }; - /* style related computed functions */ self.triggerStateText = ko.pureComputed(function () { - if (!self.is_homed()) - return "Idle until all axes are homed"; + if (!self.has_definite_position()) + return "Idle until the printer's position is known."; else if (self.is_triggered()) - return "Triggering a snapshot"; + return "Triggering a snapshot."; else if (Octolapse.PrinterStatus.isPaused()) - return "Paused"; + return "The trigger is paused."; else if (self.is_waiting()) { - // Create a list of things we are waiting on - var waitText = "Waiting"; + var waitText = "Waiting for"; var waitList = []; if (self.is_waiting_on_zhop()) waitList.push("zhop"); if (self.is_waiting_on_extruder()) waitList.push("extruder"); if (!self.is_in_position() && !self.in_path_position()) + { waitList.push("position"); - if (waitList.length > 1) { - waitText += " for " + waitList.join(" and "); - waitText += " to trigger"; + //console.log("Waiting on position."); + } + if (waitList.length > 0) { + if (waitList.length === 1) { + waitText += waitList[0]; + } + else if (waitList.length > 1) { + var commaSeparated = waitList.slice(0, waitList.length - 2); + waitText += commaSeparated.join(", "); + if (waitList.length > 2) + waitText += ", "; + waitText += waitList[waitList.length-1]; + } + waitText += " to trigger."; } - else if (waitList.length === 1) - waitText += " for " + waitList[0] + " to trigger"; return waitText; } - else - return "Triggering in " + self.seconds_to_trigger() + " seconds"; + return "Triggering in " + self.secondsToTrigger() + "."; }, self); - self.triggerBackgroundIconClass = ko.pureComputed(function () { - if (!self.is_homed()) - return "bg-not-homed"; - else if (!self.is_triggered() && Octolapse.PrinterStatus.isPaused()) - return " bg-paused"; - }, self); - self.triggerIconClass = ko.pureComputed(function () { - if (!self.is_homed()) - return "not-homed"; - if (self.is_triggered()) - return "trigger"; - if (Octolapse.PrinterStatus.isPaused()) - return "paused"; - if (self.is_waiting()) - return " wait"; - else - return " fa-inverse"; - }, self); - self.getInfoText = ko.pureComputed(function () { - return "Triggering every " + Octolapse.ToTimer(self.interval_seconds()); + + self.secondsInterval = ko.pureComputed(function () { + return Octolapse.ToTimer(self.interval_seconds()); }, self); - self.getInfoIconText = ko.pureComputed(function () { - return "Triggering every " + Octolapse.ToTimer(self.interval_seconds()); + + self.secondsToTrigger = ko.pureComputed(function () { + return Octolapse.ToTimer(self.seconds_to_trigger()); }, self); + }; OCTOPRINT_VIEWMODELS.push([ diff --git a/octoprint_octolapse/static/js/octolapse.status.snapshotplan.js b/octoprint_octolapse/static/js/octolapse.status.snapshotplan.js index ffcf0a2b..830c5d1a 100644 --- a/octoprint_octolapse/static/js/octolapse.status.snapshotplan.js +++ b/octoprint_octolapse/static/js/octolapse.status.snapshotplan.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -32,12 +32,21 @@ Octolapse.snapshotPlanStateViewModel = function() { self.plan_count = ko.observable(null); self.lines_remaining = ko.observable(null); self.lines_total = ko.observable(null); - self.x_initial = ko.observable(null).extend({numeric: 2}); - self.y_initial = ko.observable(null).extend({numeric: 2}); - self.z_initial = ko.observable(null).extend({numeric: 2}); - self.x_return = ko.observable(null).extend({numeric: 2}); - self.y_return = ko.observable(null).extend({numeric: 2}); - self.z_return = ko.observable(null).extend({numeric: 2}); + self.x_initial = ko.observable(null); + self.y_initial = ko.observable(null); + self.z_initial = ko.observable(null); + self.x_return = ko.observable(null); + self.y_return = ko.observable(null); + self.z_return = ko.observable(null); + + self.format_coordinates = function(val) + { + if (val) return val.toFixed(2); + return ""; + }; + + self.multi_extruder = ko.observable(false); + self.current_tool = ko.observable(0); self.progress_percent = ko.observable(null).extend({numeric: 2}); self.snapshot_positions = ko.observableArray([]); self.is_animating_plans = ko.observable(false); @@ -49,6 +58,15 @@ Octolapse.snapshotPlanStateViewModel = function() { self.axes = null; self.is_preview = false; self.is_confirmation_popup = ko.observable(false); + self.autoclose = ko.observable(false); + self.autoclose_seconds = ko.observable(0); + self.quality_issues = ko.observableArray(); + self.missed_snapshots = ko.observable(0); + + setInterval(function() { + var newTimer = self.autoclose_seconds() -1; + self.autoclose_seconds(newTimer <= 0 ? 1 : newTimer); + }, 1000); self.update = function (state) { if (state.snapshot_plans != null) @@ -64,6 +82,24 @@ Octolapse.snapshotPlanStateViewModel = function() { } self.total_saved_travel_percent(percent_saved); + if(typeof state.quality_issues !== 'undefined') + { + self.quality_issues(state.quality_issues); + } + + if(typeof state.missed_snapshots !== 'undefined') + { + self.missed_snapshots(state.missed_snapshots); + } + + if (typeof state.autoclose !== 'undefined') { + //console.log("Setting snapshot plan preview autoclose"); + self.autoclose(state.autoclose); + self.autoclose_seconds(state.autoclose_seconds); + } + else { + console.error("Autoclose property not set!"); + } } if (state.current_plan_index != null) { @@ -104,25 +140,36 @@ Octolapse.snapshotPlanStateViewModel = function() { self.lines_remaining(lines_remaining); var previous_line = 0; if (self.current_plan_index() - 1 > 0) - previous_plan = self.snapshot_plans()[self.current_plan_index() - 1] + previous_plan = self.snapshot_plans()[self.current_plan_index() - 1]; if (previous_plan != null) previous_line = previous_plan.file_gcode_number; var lines_total = current_plan.file_gcode_number - previous_line; self.lines_total(lines_total); self.progress_percent((1-(lines_remaining / lines_total)) * 100); + self.multi_extruder(showing_plan.initial_position.extruders.length > 1); + self.current_tool(showing_plan.initial_position.current_tool); + self.x_initial(showing_plan.initial_position.x); self.y_initial(showing_plan.initial_position.y); self.z_initial(showing_plan.initial_position.z); - self.x_return(showing_plan.return_position.x); - self.y_return(showing_plan.return_position.y); - self.z_return(showing_plan.return_position.z); + if (showing_plan.return_position) { + self.x_return(showing_plan.return_position.x); + self.y_return(showing_plan.return_position.y); + self.z_return(showing_plan.return_position.z); + } + else + { + self.x_return(showing_plan.initial_position.x); + self.y_return(showing_plan.initial_position.y); + self.z_return(showing_plan.initial_position.z); + } self.travel_distance(showing_plan.total_travel_distance); self.saved_travel_distance(showing_plan.total_saved_travel_distance); var x_current = showing_plan.initial_position.x; var y_current = showing_plan.initial_position.y; var z_current = showing_plan.initial_position.z; - self.snapshot_positions([]); + var snapshot_positions = []; // Create snapshot positions from steps for (var stepIndex = 0; stepIndex < showing_plan.steps.length; stepIndex++) { @@ -138,9 +185,10 @@ Octolapse.snapshotPlanStateViewModel = function() { } else if(current_step.action == "snapshot") { - self.snapshot_positions.push({x: x_current, y: y_current, z: z_current}); + snapshot_positions.push({x: x_current, y: y_current, z: z_current}); } } + self.snapshot_positions(snapshot_positions); // Update Canvass self.updateCanvas(); }; @@ -149,11 +197,18 @@ Octolapse.snapshotPlanStateViewModel = function() { { self.is_animating_plans(false); self.view_current_plan(false); + if (self.snapshot_plans().length < 1) + return; + var index = self.plan_index()+1; if (index < self.snapshot_plans().length) { self.plan_index(index); } + else + { + self.plan_index(0); + } self.update_current_plan(); return false; }; @@ -162,11 +217,17 @@ Octolapse.snapshotPlanStateViewModel = function() { { self.is_animating_plans(false); self.view_current_plan(false); + if (self.snapshot_plans().length < 1) + return false; var index = self.plan_index()-1; - if (index > -1 && self.snapshot_plans().length > 0) + if (index > -1) { self.plan_index(index); } + else + { + self.plan_index(self.snapshot_plans().length - 1); + } self.update_current_plan(); return false; }; @@ -432,7 +493,7 @@ Octolapse.snapshotPlanStateViewModel = function() { "Y", self.canvas_border_size[0] * 0.5 - text_width.width/2 - 1, self.canvas_printer_size[1] - self.axis_line_length + self.canvas_border_size[1] * 1.5 - 2 - ) + ); // *** draw the X arrow // draw the horizontal line @@ -476,7 +537,7 @@ Octolapse.snapshotPlanStateViewModel = function() { "X", self.canvas_border_size[0] * 0.5 + self.axis_line_length, self.canvas_printer_size[1] + self.canvas_border_size[1] * 1.5 + 4.5 - ) + ); }; @@ -590,20 +651,28 @@ Octolapse.snapshotPlanStateViewModel = function() { return (max - min) - coord; }; - self.normalize_coordinate = function(coord, min) + self.normalize_coordinate = function(coord, min, max) { - return coord - min; + if (self.printer_volume.origin_type == 'center') + { + return coord + (max - min) / 2.0; + } + else + { + return coord - min; + } + }; self.to_canvas_x = function(x) { - x = self.normalize_coordinate(x, self.printer_volume.min_x); + x = self.normalize_coordinate(x, self.printer_volume.min_x, self.printer_volume.max_x); return x *self.x_canvas_scale + self.canvas_border_size[0]; }; self.to_canvas_y = function(y) { - y = self.normalize_coordinate(y, self.printer_volume.min_y); + y = self.normalize_coordinate(y, self.printer_volume.min_y, self.printer_volume.max_y); // Y coordinates are flipped on the camera when compared to the standard 3d printer // coordinates, so invert the coordinate y = self.invert_coordinate(y, self.printer_volume.min_y, self.printer_volume.max_y); @@ -613,7 +682,8 @@ Octolapse.snapshotPlanStateViewModel = function() { self.to_canvas_z = function(z) { - z = self.normalize_coordinate(z, self.printer_volume.min_z); + // This isn't right, z coordinates always start at 0! + z = self.normalize_coordinate(z, 0, 0); return z *self.z_canvas_scale + self.canvas_border_size[2]; }; diff --git a/octoprint_octolapse/static/js/octolapse.status.snapshotplan_preview.js b/octoprint_octolapse/static/js/octolapse.status.snapshotplan_preview.js index 2585eeff..0b5439e9 100644 --- a/octoprint_octolapse/static/js/octolapse.status.snapshotplan_preview.js +++ b/octoprint_octolapse/static/js/octolapse.status.snapshotplan_preview.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -107,6 +107,6 @@ $(function() { self.acceptPreview = function() { Octolapse.Globals.acceptSnapshotPlanPreview(); }; - + }; }); diff --git a/octoprint_octolapse/static/js/showdown.min.js b/octoprint_octolapse/static/js/showdown.min.js index b9208f8e..ee264278 100644 --- a/octoprint_octolapse/static/js/showdown.min.js +++ b/octoprint_octolapse/static/js/showdown.min.js @@ -1,27 +1,5143 @@ -/*! -showdown v 1.9.0 - 10-11-2018 +;/*! showdown v 1.9.1 - 02-11-2019 */ +(function(){ +/** + * Created by Tivie on 13-07-2015. + */ -MIT License +function getDefaultOpts (simple) { + 'use strict'; -Copyright (c) 2018 ShowdownJS + var defaultOptions = { + omitExtraWLInCodeBlocks: { + defaultValue: false, + describe: 'Omit the default extra whiteline added to code blocks', + type: 'boolean' + }, + noHeaderId: { + defaultValue: false, + describe: 'Turn on/off generated header id', + type: 'boolean' + }, + prefixHeaderId: { + defaultValue: false, + describe: 'Add a prefix to the generated header ids. Passing a string will prefix that string to the header id. Setting to true will add a generic \'section-\' prefix', + type: 'string' + }, + rawPrefixHeaderId: { + defaultValue: false, + describe: 'Setting this option to true will prevent showdown from modifying the prefix. This might result in malformed IDs (if, for instance, the " char is used in the prefix)', + type: 'boolean' + }, + ghCompatibleHeaderId: { + defaultValue: false, + describe: 'Generate header ids compatible with github style (spaces are replaced with dashes, a bunch of non alphanumeric chars are removed)', + type: 'boolean' + }, + rawHeaderId: { + defaultValue: false, + describe: 'Remove only spaces, \' and " from generated header ids (including prefixes), replacing them with dashes (-). WARNING: This might result in malformed ids', + type: 'boolean' + }, + headerLevelStart: { + defaultValue: false, + describe: 'The header blocks level start', + type: 'integer' + }, + parseImgDimensions: { + defaultValue: false, + describe: 'Turn on/off image dimension parsing', + type: 'boolean' + }, + simplifiedAutoLink: { + defaultValue: false, + describe: 'Turn on/off GFM autolink style', + type: 'boolean' + }, + excludeTrailingPunctuationFromURLs: { + defaultValue: false, + describe: 'Excludes trailing punctuation from links generated with autoLinking', + type: 'boolean' + }, + literalMidWordUnderscores: { + defaultValue: false, + describe: 'Parse midword underscores as literal underscores', + type: 'boolean' + }, + literalMidWordAsterisks: { + defaultValue: false, + describe: 'Parse midword asterisks as literal asterisks', + type: 'boolean' + }, + strikethrough: { + defaultValue: false, + describe: 'Turn on/off strikethrough support', + type: 'boolean' + }, + tables: { + defaultValue: false, + describe: 'Turn on/off tables support', + type: 'boolean' + }, + tablesHeaderId: { + defaultValue: false, + describe: 'Add an id to table headers', + type: 'boolean' + }, + ghCodeBlocks: { + defaultValue: true, + describe: 'Turn on/off GFM fenced code blocks support', + type: 'boolean' + }, + tasklists: { + defaultValue: false, + describe: 'Turn on/off GFM tasklist support', + type: 'boolean' + }, + smoothLivePreview: { + defaultValue: false, + describe: 'Prevents weird effects in live previews due to incomplete input', + type: 'boolean' + }, + smartIndentationFix: { + defaultValue: false, + description: 'Tries to smartly fix indentation in es6 strings', + type: 'boolean' + }, + disableForced4SpacesIndentedSublists: { + defaultValue: false, + description: 'Disables the requirement of indenting nested sublists by 4 spaces', + type: 'boolean' + }, + simpleLineBreaks: { + defaultValue: false, + description: 'Parses simple line breaks as
      (GFM Style)', + type: 'boolean' + }, + requireSpaceBeforeHeadingText: { + defaultValue: false, + description: 'Makes adding a space between `#` and the header text mandatory (GFM Style)', + type: 'boolean' + }, + ghMentions: { + defaultValue: false, + description: 'Enables github @mentions', + type: 'boolean' + }, + ghMentionsLink: { + defaultValue: 'https://github.com/{u}', + description: 'Changes the link generated by @mentions. Only applies if ghMentions option is enabled.', + type: 'string' + }, + encodeEmails: { + defaultValue: true, + description: 'Encode e-mail addresses through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities', + type: 'boolean' + }, + openLinksInNewWindow: { + defaultValue: false, + description: 'Open all links in new windows', + type: 'boolean' + }, + backslashEscapesHTMLTags: { + defaultValue: false, + description: 'Support for HTML Tag escaping. ex: \
      foo\
      ', + type: 'boolean' + }, + emoji: { + defaultValue: false, + description: 'Enable emoji support. Ex: `this is a :smile: emoji`', + type: 'boolean' + }, + underline: { + defaultValue: false, + description: 'Enable support for underline. Syntax is double or triple underscores: `__underline word__`. With this option enabled, underscores no longer parses into `` and ``', + type: 'boolean' + }, + completeHTMLDocument: { + defaultValue: false, + description: 'Outputs a complete html document, including ``, `` and `` tags', + type: 'boolean' + }, + metadata: { + defaultValue: false, + description: 'Enable support for document metadata (defined at the top of the document between `«««` and `»»»` or between `---` and `---`).', + type: 'boolean' + }, + splitAdjacentBlockquotes: { + defaultValue: false, + description: 'Split adjacent blockquote blocks', + type: 'boolean' + } + }; + if (simple === false) { + return JSON.parse(JSON.stringify(defaultOptions)); + } + var ret = {}; + for (var opt in defaultOptions) { + if (defaultOptions.hasOwnProperty(opt)) { + ret[opt] = defaultOptions[opt].defaultValue; + } + } + return ret; +} -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +function allOptionsOn () { + 'use strict'; + var options = getDefaultOpts(true), + ret = {}; + for (var opt in options) { + if (options.hasOwnProperty(opt)) { + ret[opt] = true; + } + } + return ret; +} -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +/** + * Created by Tivie on 06-01-2015. + */ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.*/ +// Private properties +var showdown = {}, + parsers = {}, + extensions = {}, + globalOptions = getDefaultOpts(true), + setFlavor = 'vanilla', + flavor = { + github: { + omitExtraWLInCodeBlocks: true, + simplifiedAutoLink: true, + excludeTrailingPunctuationFromURLs: true, + literalMidWordUnderscores: true, + strikethrough: true, + tables: true, + tablesHeaderId: true, + ghCodeBlocks: true, + tasklists: true, + disableForced4SpacesIndentedSublists: true, + simpleLineBreaks: true, + requireSpaceBeforeHeadingText: true, + ghCompatibleHeaderId: true, + ghMentions: true, + backslashEscapesHTMLTags: true, + emoji: true, + splitAdjacentBlockquotes: true + }, + original: { + noHeaderId: true, + ghCodeBlocks: false + }, + ghost: { + omitExtraWLInCodeBlocks: true, + parseImgDimensions: true, + simplifiedAutoLink: true, + excludeTrailingPunctuationFromURLs: true, + literalMidWordUnderscores: true, + strikethrough: true, + tables: true, + tablesHeaderId: true, + ghCodeBlocks: true, + tasklists: true, + smoothLivePreview: true, + simpleLineBreaks: true, + requireSpaceBeforeHeadingText: true, + ghMentions: false, + encodeEmails: true + }, + vanilla: getDefaultOpts(true), + allOn: allOptionsOn() + }; -(function(){function e(e){"use strict";var r={omitExtraWLInCodeBlocks:{defaultValue:!1,describe:"Omit the default extra whiteline added to code blocks",type:"boolean"},noHeaderId:{defaultValue:!1,describe:"Turn on/off generated header id",type:"boolean"},prefixHeaderId:{defaultValue:!1,describe:"Add a prefix to the generated header ids. Passing a string will prefix that string to the header id. Setting to true will add a generic 'section-' prefix",type:"string"},rawPrefixHeaderId:{defaultValue:!1,describe:'Setting this option to true will prevent showdown from modifying the prefix. This might result in malformed IDs (if, for instance, the " char is used in the prefix)',type:"boolean"},ghCompatibleHeaderId:{defaultValue:!1,describe:"Generate header ids compatible with github style (spaces are replaced with dashes, a bunch of non alphanumeric chars are removed)",type:"boolean"},rawHeaderId:{defaultValue:!1,describe:"Remove only spaces, ' and \" from generated header ids (including prefixes), replacing them with dashes (-). WARNING: This might result in malformed ids",type:"boolean"},headerLevelStart:{defaultValue:!1,describe:"The header blocks level start",type:"integer"},parseImgDimensions:{defaultValue:!1,describe:"Turn on/off image dimension parsing",type:"boolean"},simplifiedAutoLink:{defaultValue:!1,describe:"Turn on/off GFM autolink style",type:"boolean"},excludeTrailingPunctuationFromURLs:{defaultValue:!1,describe:"Excludes trailing punctuation from links generated with autoLinking",type:"boolean"},literalMidWordUnderscores:{defaultValue:!1,describe:"Parse midword underscores as literal underscores",type:"boolean"},literalMidWordAsterisks:{defaultValue:!1,describe:"Parse midword asterisks as literal asterisks",type:"boolean"},strikethrough:{defaultValue:!1,describe:"Turn on/off strikethrough support",type:"boolean"},tables:{defaultValue:!1,describe:"Turn on/off tables support",type:"boolean"},tablesHeaderId:{defaultValue:!1,describe:"Add an id to table headers",type:"boolean"},ghCodeBlocks:{defaultValue:!0,describe:"Turn on/off GFM fenced code blocks support",type:"boolean"},tasklists:{defaultValue:!1,describe:"Turn on/off GFM tasklist support",type:"boolean"},smoothLivePreview:{defaultValue:!1,describe:"Prevents weird effects in live previews due to incomplete input",type:"boolean"},smartIndentationFix:{defaultValue:!1,description:"Tries to smartly fix indentation in es6 strings",type:"boolean"},disableForced4SpacesIndentedSublists:{defaultValue:!1,description:"Disables the requirement of indenting nested sublists by 4 spaces",type:"boolean"},simpleLineBreaks:{defaultValue:!1,description:"Parses simple line breaks as
      (GFM Style)",type:"boolean"},requireSpaceBeforeHeadingText:{defaultValue:!1,description:"Makes adding a space between `#` and the header text mandatory (GFM Style)",type:"boolean"},ghMentions:{defaultValue:!1,description:"Enables github @mentions",type:"boolean"},ghMentionsLink:{defaultValue:"https://github.com/{u}",description:"Changes the link generated by @mentions. Only applies if ghMentions option is enabled.",type:"string"},encodeEmails:{defaultValue:!0,description:"Encode e-mail addresses through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities",type:"boolean"},openLinksInNewWindow:{defaultValue:!1,description:"Open all links in new windows",type:"boolean"},backslashEscapesHTMLTags:{defaultValue:!1,description:"Support for HTML Tag escaping. ex:
      foo
      ",type:"boolean"},emoji:{defaultValue:!1,description:"Enable emoji support. Ex: `this is a :smile: emoji`",type:"boolean"},underline:{defaultValue:!1,description:"Enable support for underline. Syntax is double or triple underscores: `__underline word__`. With this option enabled, underscores no longer parses into `` and ``",type:"boolean"},completeHTMLDocument:{defaultValue:!1,description:"Outputs a complete html document, including ``, `` and `` tags",type:"boolean"},metadata:{defaultValue:!1,description:"Enable support for document metadata (defined at the top of the document between `«««` and `»»»` or between `---` and `---`).",type:"boolean"},splitAdjacentBlockquotes:{defaultValue:!1,description:"Split adjacent blockquote blocks",type:"boolean"}};if(!1===e)return JSON.parse(JSON.stringify(r));var t={};for(var a in r)r.hasOwnProperty(a)&&(t[a]=r[a].defaultValue);return t}function r(e,r){"use strict";var t=r?"Error in "+r+" extension->":"Error in unnamed extension",n={valid:!0,error:""};a.helper.isArray(e)||(e=[e]);for(var s=0;s").replace(/&/g,"&")};var c=function(e,r,t,a){"use strict";var n,s,o,i,l,c=a||"",u=c.indexOf("g")>-1,d=new RegExp(r+"|"+t,"g"+c.replace(/g/g,"")),p=new RegExp(r,c.replace(/g/g,"")),h=[];do{for(n=0;o=d.exec(e);)if(p.test(o[0]))n++||(i=(s=d.lastIndex)-o[0].length);else if(n&&!--n){l=o.index+o[0].length;var _={left:{start:i,end:s},match:{start:s,end:o.index},right:{start:o.index,end:l},wholeMatch:{start:i,end:l}};if(h.push(_),!u)return h}}while(n&&(d.lastIndex=s));return h};a.helper.matchRecursiveRegExp=function(e,r,t,a){"use strict";for(var n=c(e,r,t,a),s=[],o=0;o0){var d=[];0!==i[0].wholeMatch.start&&d.push(e.slice(0,i[0].wholeMatch.start));for(var p=0;p=0?n+(t||0):n},a.helper.splitAtIndex=function(e,r){"use strict";if(!a.helper.isString(e))throw"InvalidArgumentError: first parameter of showdown.helper.regexIndexOf function must be a string";return[e.substring(0,r),e.substring(r)]},a.helper.encodeEmailAddress=function(e){"use strict";var r=[function(e){return"&#"+e.charCodeAt(0)+";"},function(e){return"&#x"+e.charCodeAt(0).toString(16)+";"},function(e){return e}];return e=e.replace(/./g,function(e){if("@"===e)e=r[Math.floor(2*Math.random())](e);else{var t=Math.random();e=t>.9?r[2](e):t>.45?r[1](e):r[0](e)}return e})},a.helper.padEnd=function(e,r,t){"use strict";return r>>=0,t=String(t||" "),e.length>r?String(e):((r-=e.length)>t.length&&(t+=t.repeat(r/t.length)),String(e)+t.slice(0,r))},"undefined"==typeof console&&(console={warn:function(e){"use strict";alert(e)},log:function(e){"use strict";alert(e)},error:function(e){"use strict";throw e}}),a.helper.regexes={asteriskDashAndColon:/([*_:~])/g},a.helper.emojis={"+1":"👍","-1":"👎",100:"💯",1234:"🔢","1st_place_medal":"🥇","2nd_place_medal":"🥈","3rd_place_medal":"🥉","8ball":"🎱",a:"🅰️",ab:"🆎",abc:"🔤",abcd:"🔡",accept:"🉑",aerial_tramway:"🚡",airplane:"✈️",alarm_clock:"⏰",alembic:"⚗️",alien:"👽",ambulance:"🚑",amphora:"🏺",anchor:"⚓️",angel:"👼",anger:"💢",angry:"😠",anguished:"😧",ant:"🐜",apple:"🍎",aquarius:"♒️",aries:"♈️",arrow_backward:"◀️",arrow_double_down:"⏬",arrow_double_up:"⏫",arrow_down:"⬇️",arrow_down_small:"🔽",arrow_forward:"▶️",arrow_heading_down:"⤵️",arrow_heading_up:"⤴️",arrow_left:"⬅️",arrow_lower_left:"↙️",arrow_lower_right:"↘️",arrow_right:"➡️",arrow_right_hook:"↪️",arrow_up:"⬆️",arrow_up_down:"↕️",arrow_up_small:"🔼",arrow_upper_left:"↖️",arrow_upper_right:"↗️",arrows_clockwise:"🔃",arrows_counterclockwise:"🔄",art:"🎨",articulated_lorry:"🚛",artificial_satellite:"🛰",astonished:"😲",athletic_shoe:"👟",atm:"🏧",atom_symbol:"⚛️",avocado:"🥑",b:"🅱️",baby:"👶",baby_bottle:"🍼",baby_chick:"🐤",baby_symbol:"🚼",back:"🔙",bacon:"🥓",badminton:"🏸",baggage_claim:"🛄",baguette_bread:"🥖",balance_scale:"⚖️",balloon:"🎈",ballot_box:"🗳",ballot_box_with_check:"☑️",bamboo:"🎍",banana:"🍌",bangbang:"‼️",bank:"🏦",bar_chart:"📊",barber:"💈",baseball:"⚾️",basketball:"🏀",basketball_man:"⛹️",basketball_woman:"⛹️‍♀️",bat:"🦇",bath:"🛀",bathtub:"🛁",battery:"🔋",beach_umbrella:"🏖",bear:"🐻",bed:"🛏",bee:"🐝",beer:"🍺",beers:"🍻",beetle:"🐞",beginner:"🔰",bell:"🔔",bellhop_bell:"🛎",bento:"🍱",biking_man:"🚴",bike:"🚲",biking_woman:"🚴‍♀️",bikini:"👙",biohazard:"☣️",bird:"🐦",birthday:"🎂",black_circle:"⚫️",black_flag:"🏴",black_heart:"🖤",black_joker:"🃏",black_large_square:"⬛️",black_medium_small_square:"◾️",black_medium_square:"◼️",black_nib:"✒️",black_small_square:"▪️",black_square_button:"🔲",blonde_man:"👱",blonde_woman:"👱‍♀️",blossom:"🌼",blowfish:"🐡",blue_book:"📘",blue_car:"🚙",blue_heart:"💙",blush:"😊",boar:"🐗",boat:"⛵️",bomb:"💣",book:"📖",bookmark:"🔖",bookmark_tabs:"📑",books:"📚",boom:"💥",boot:"👢",bouquet:"💐",bowing_man:"🙇",bow_and_arrow:"🏹",bowing_woman:"🙇‍♀️",bowling:"🎳",boxing_glove:"🥊",boy:"👦",bread:"🍞",bride_with_veil:"👰",bridge_at_night:"🌉",briefcase:"💼",broken_heart:"💔",bug:"🐛",building_construction:"🏗",bulb:"💡",bullettrain_front:"🚅",bullettrain_side:"🚄",burrito:"🌯",bus:"🚌",business_suit_levitating:"🕴",busstop:"🚏",bust_in_silhouette:"👤",busts_in_silhouette:"👥",butterfly:"🦋",cactus:"🌵",cake:"🍰",calendar:"📆",call_me_hand:"🤙",calling:"📲",camel:"🐫",camera:"📷",camera_flash:"📸",camping:"🏕",cancer:"♋️",candle:"🕯",candy:"🍬",canoe:"🛶",capital_abcd:"🔠",capricorn:"♑️",car:"🚗",card_file_box:"🗃",card_index:"📇",card_index_dividers:"🗂",carousel_horse:"🎠",carrot:"🥕",cat:"🐱",cat2:"🐈",cd:"💿",chains:"⛓",champagne:"🍾",chart:"💹",chart_with_downwards_trend:"📉",chart_with_upwards_trend:"📈",checkered_flag:"🏁",cheese:"🧀",cherries:"🍒",cherry_blossom:"🌸",chestnut:"🌰",chicken:"🐔",children_crossing:"🚸",chipmunk:"🐿",chocolate_bar:"🍫",christmas_tree:"🎄",church:"⛪️",cinema:"🎦",circus_tent:"🎪",city_sunrise:"🌇",city_sunset:"🌆",cityscape:"🏙",cl:"🆑",clamp:"🗜",clap:"👏",clapper:"🎬",classical_building:"🏛",clinking_glasses:"🥂",clipboard:"📋",clock1:"🕐",clock10:"🕙",clock1030:"🕥",clock11:"🕚",clock1130:"🕦",clock12:"🕛",clock1230:"🕧",clock130:"🕜",clock2:"🕑",clock230:"🕝",clock3:"🕒",clock330:"🕞",clock4:"🕓",clock430:"🕟",clock5:"🕔",clock530:"🕠",clock6:"🕕",clock630:"🕡",clock7:"🕖",clock730:"🕢",clock8:"🕗",clock830:"🕣",clock9:"🕘",clock930:"🕤",closed_book:"📕",closed_lock_with_key:"🔐",closed_umbrella:"🌂",cloud:"☁️",cloud_with_lightning:"🌩",cloud_with_lightning_and_rain:"⛈",cloud_with_rain:"🌧",cloud_with_snow:"🌨",clown_face:"🤡",clubs:"♣️",cocktail:"🍸",coffee:"☕️",coffin:"⚰️",cold_sweat:"😰",comet:"☄️",computer:"💻",computer_mouse:"🖱",confetti_ball:"🎊",confounded:"😖",confused:"😕",congratulations:"㊗️",construction:"🚧",construction_worker_man:"👷",construction_worker_woman:"👷‍♀️",control_knobs:"🎛",convenience_store:"🏪",cookie:"🍪",cool:"🆒",policeman:"👮",copyright:"©️",corn:"🌽",couch_and_lamp:"🛋",couple:"👫",couple_with_heart_woman_man:"💑",couple_with_heart_man_man:"👨‍❤️‍👨",couple_with_heart_woman_woman:"👩‍❤️‍👩",couplekiss_man_man:"👨‍❤️‍💋‍👨",couplekiss_man_woman:"💏",couplekiss_woman_woman:"👩‍❤️‍💋‍👩",cow:"🐮",cow2:"🐄",cowboy_hat_face:"🤠",crab:"🦀",crayon:"🖍",credit_card:"💳",crescent_moon:"🌙",cricket:"🏏",crocodile:"🐊",croissant:"🥐",crossed_fingers:"🤞",crossed_flags:"🎌",crossed_swords:"⚔️",crown:"👑",cry:"😢",crying_cat_face:"😿",crystal_ball:"🔮",cucumber:"🥒",cupid:"💘",curly_loop:"➰",currency_exchange:"💱",curry:"🍛",custard:"🍮",customs:"🛃",cyclone:"🌀",dagger:"🗡",dancer:"💃",dancing_women:"👯",dancing_men:"👯‍♂️",dango:"🍡",dark_sunglasses:"🕶",dart:"🎯",dash:"💨",date:"📅",deciduous_tree:"🌳",deer:"🦌",department_store:"🏬",derelict_house:"🏚",desert:"🏜",desert_island:"🏝",desktop_computer:"🖥",male_detective:"🕵️",diamond_shape_with_a_dot_inside:"💠",diamonds:"♦️",disappointed:"😞",disappointed_relieved:"😥",dizzy:"💫",dizzy_face:"😵",do_not_litter:"🚯",dog:"🐶",dog2:"🐕",dollar:"💵",dolls:"🎎",dolphin:"🐬",door:"🚪",doughnut:"🍩",dove:"🕊",dragon:"🐉",dragon_face:"🐲",dress:"👗",dromedary_camel:"🐪",drooling_face:"🤤",droplet:"💧",drum:"🥁",duck:"🦆",dvd:"📀","e-mail":"📧",eagle:"🦅",ear:"👂",ear_of_rice:"🌾",earth_africa:"🌍",earth_americas:"🌎",earth_asia:"🌏",egg:"🥚",eggplant:"🍆",eight_pointed_black_star:"✴️",eight_spoked_asterisk:"✳️",electric_plug:"🔌",elephant:"🐘",email:"✉️",end:"🔚",envelope_with_arrow:"📩",euro:"💶",european_castle:"🏰",european_post_office:"🏤",evergreen_tree:"🌲",exclamation:"❗️",expressionless:"😑",eye:"👁",eye_speech_bubble:"👁‍🗨",eyeglasses:"👓",eyes:"👀",face_with_head_bandage:"🤕",face_with_thermometer:"🤒",fist_oncoming:"👊",factory:"🏭",fallen_leaf:"🍂",family_man_woman_boy:"👪",family_man_boy:"👨‍👦",family_man_boy_boy:"👨‍👦‍👦",family_man_girl:"👨‍👧",family_man_girl_boy:"👨‍👧‍👦",family_man_girl_girl:"👨‍👧‍👧",family_man_man_boy:"👨‍👨‍👦",family_man_man_boy_boy:"👨‍👨‍👦‍👦",family_man_man_girl:"👨‍👨‍👧",family_man_man_girl_boy:"👨‍👨‍👧‍👦",family_man_man_girl_girl:"👨‍👨‍👧‍👧",family_man_woman_boy_boy:"👨‍👩‍👦‍👦",family_man_woman_girl:"👨‍👩‍👧",family_man_woman_girl_boy:"👨‍👩‍👧‍👦",family_man_woman_girl_girl:"👨‍👩‍👧‍👧",family_woman_boy:"👩‍👦",family_woman_boy_boy:"👩‍👦‍👦",family_woman_girl:"👩‍👧",family_woman_girl_boy:"👩‍👧‍👦",family_woman_girl_girl:"👩‍👧‍👧",family_woman_woman_boy:"👩‍👩‍👦",family_woman_woman_boy_boy:"👩‍👩‍👦‍👦",family_woman_woman_girl:"👩‍👩‍👧",family_woman_woman_girl_boy:"👩‍👩‍👧‍👦",family_woman_woman_girl_girl:"👩‍👩‍👧‍👧",fast_forward:"⏩",fax:"📠",fearful:"😨",feet:"🐾",female_detective:"🕵️‍♀️",ferris_wheel:"🎡",ferry:"⛴",field_hockey:"🏑",file_cabinet:"🗄",file_folder:"📁",film_projector:"📽",film_strip:"🎞",fire:"🔥",fire_engine:"🚒",fireworks:"🎆",first_quarter_moon:"🌓",first_quarter_moon_with_face:"🌛",fish:"🐟",fish_cake:"🍥",fishing_pole_and_fish:"🎣",fist_raised:"✊",fist_left:"🤛",fist_right:"🤜",flags:"🎏",flashlight:"🔦",fleur_de_lis:"⚜️",flight_arrival:"🛬",flight_departure:"🛫",floppy_disk:"💾",flower_playing_cards:"🎴",flushed:"😳",fog:"🌫",foggy:"🌁",football:"🏈",footprints:"👣",fork_and_knife:"🍴",fountain:"⛲️",fountain_pen:"🖋",four_leaf_clover:"🍀",fox_face:"🦊",framed_picture:"🖼",free:"🆓",fried_egg:"🍳",fried_shrimp:"🍤",fries:"🍟",frog:"🐸",frowning:"😦",frowning_face:"☹️",frowning_man:"🙍‍♂️",frowning_woman:"🙍",middle_finger:"🖕",fuelpump:"⛽️",full_moon:"🌕",full_moon_with_face:"🌝",funeral_urn:"⚱️",game_die:"🎲",gear:"⚙️",gem:"💎",gemini:"♊️",ghost:"👻",gift:"🎁",gift_heart:"💝",girl:"👧",globe_with_meridians:"🌐",goal_net:"🥅",goat:"🐐",golf:"⛳️",golfing_man:"🏌️",golfing_woman:"🏌️‍♀️",gorilla:"🦍",grapes:"🍇",green_apple:"🍏",green_book:"📗",green_heart:"💚",green_salad:"🥗",grey_exclamation:"❕",grey_question:"❔",grimacing:"😬",grin:"😁",grinning:"😀",guardsman:"💂",guardswoman:"💂‍♀️",guitar:"🎸",gun:"🔫",haircut_woman:"💇",haircut_man:"💇‍♂️",hamburger:"🍔",hammer:"🔨",hammer_and_pick:"⚒",hammer_and_wrench:"🛠",hamster:"🐹",hand:"✋",handbag:"👜",handshake:"🤝",hankey:"💩",hatched_chick:"🐥",hatching_chick:"🐣",headphones:"🎧",hear_no_evil:"🙉",heart:"❤️",heart_decoration:"💟",heart_eyes:"😍",heart_eyes_cat:"😻",heartbeat:"💓",heartpulse:"💗",hearts:"♥️",heavy_check_mark:"✔️",heavy_division_sign:"➗",heavy_dollar_sign:"💲",heavy_heart_exclamation:"❣️",heavy_minus_sign:"➖",heavy_multiplication_x:"✖️",heavy_plus_sign:"➕",helicopter:"🚁",herb:"🌿",hibiscus:"🌺",high_brightness:"🔆",high_heel:"👠",hocho:"🔪",hole:"🕳",honey_pot:"🍯",horse:"🐴",horse_racing:"🏇",hospital:"🏥",hot_pepper:"🌶",hotdog:"🌭",hotel:"🏨",hotsprings:"♨️",hourglass:"⌛️",hourglass_flowing_sand:"⏳",house:"🏠",house_with_garden:"🏡",houses:"🏘",hugs:"🤗",hushed:"😯",ice_cream:"🍨",ice_hockey:"🏒",ice_skate:"⛸",icecream:"🍦",id:"🆔",ideograph_advantage:"🉐",imp:"👿",inbox_tray:"📥",incoming_envelope:"📨",tipping_hand_woman:"💁",information_source:"ℹ️",innocent:"😇",interrobang:"⁉️",iphone:"📱",izakaya_lantern:"🏮",jack_o_lantern:"🎃",japan:"🗾",japanese_castle:"🏯",japanese_goblin:"👺",japanese_ogre:"👹",jeans:"👖",joy:"😂",joy_cat:"😹",joystick:"🕹",kaaba:"🕋",key:"🔑",keyboard:"⌨️",keycap_ten:"🔟",kick_scooter:"🛴",kimono:"👘",kiss:"💋",kissing:"😗",kissing_cat:"😽",kissing_closed_eyes:"😚",kissing_heart:"😘",kissing_smiling_eyes:"😙",kiwi_fruit:"🥝",koala:"🐨",koko:"🈁",label:"🏷",large_blue_circle:"🔵",large_blue_diamond:"🔷",large_orange_diamond:"🔶",last_quarter_moon:"🌗",last_quarter_moon_with_face:"🌜",latin_cross:"✝️",laughing:"😆",leaves:"🍃",ledger:"📒",left_luggage:"🛅",left_right_arrow:"↔️",leftwards_arrow_with_hook:"↩️",lemon:"🍋",leo:"♌️",leopard:"🐆",level_slider:"🎚",libra:"♎️",light_rail:"🚈",link:"🔗",lion:"🦁",lips:"👄",lipstick:"💄",lizard:"🦎",lock:"🔒",lock_with_ink_pen:"🔏",lollipop:"🍭",loop:"➿",loud_sound:"🔊",loudspeaker:"📢",love_hotel:"🏩",love_letter:"💌",low_brightness:"🔅",lying_face:"🤥",m:"Ⓜ️",mag:"🔍",mag_right:"🔎",mahjong:"🀄️",mailbox:"📫",mailbox_closed:"📪",mailbox_with_mail:"📬",mailbox_with_no_mail:"📭",man:"👨",man_artist:"👨‍🎨",man_astronaut:"👨‍🚀",man_cartwheeling:"🤸‍♂️",man_cook:"👨‍🍳",man_dancing:"🕺",man_facepalming:"🤦‍♂️",man_factory_worker:"👨‍🏭",man_farmer:"👨‍🌾",man_firefighter:"👨‍🚒",man_health_worker:"👨‍⚕️",man_in_tuxedo:"🤵",man_judge:"👨‍⚖️",man_juggling:"🤹‍♂️",man_mechanic:"👨‍🔧",man_office_worker:"👨‍💼",man_pilot:"👨‍✈️",man_playing_handball:"🤾‍♂️",man_playing_water_polo:"🤽‍♂️",man_scientist:"👨‍🔬",man_shrugging:"🤷‍♂️",man_singer:"👨‍🎤",man_student:"👨‍🎓",man_teacher:"👨‍🏫",man_technologist:"👨‍💻",man_with_gua_pi_mao:"👲",man_with_turban:"👳",tangerine:"🍊",mans_shoe:"👞",mantelpiece_clock:"🕰",maple_leaf:"🍁",martial_arts_uniform:"🥋",mask:"😷",massage_woman:"💆",massage_man:"💆‍♂️",meat_on_bone:"🍖",medal_military:"🎖",medal_sports:"🏅",mega:"📣",melon:"🍈",memo:"📝",men_wrestling:"🤼‍♂️",menorah:"🕎",mens:"🚹",metal:"🤘",metro:"🚇",microphone:"🎤",microscope:"🔬",milk_glass:"🥛",milky_way:"🌌",minibus:"🚐",minidisc:"💽",mobile_phone_off:"📴",money_mouth_face:"🤑",money_with_wings:"💸",moneybag:"💰",monkey:"🐒",monkey_face:"🐵",monorail:"🚝",moon:"🌔",mortar_board:"🎓",mosque:"🕌",motor_boat:"🛥",motor_scooter:"🛵",motorcycle:"🏍",motorway:"🛣",mount_fuji:"🗻",mountain:"⛰",mountain_biking_man:"🚵",mountain_biking_woman:"🚵‍♀️",mountain_cableway:"🚠",mountain_railway:"🚞",mountain_snow:"🏔",mouse:"🐭",mouse2:"🐁",movie_camera:"🎥",moyai:"🗿",mrs_claus:"🤶",muscle:"💪",mushroom:"🍄",musical_keyboard:"🎹",musical_note:"🎵",musical_score:"🎼",mute:"🔇",nail_care:"💅",name_badge:"📛",national_park:"🏞",nauseated_face:"🤢",necktie:"👔",negative_squared_cross_mark:"❎",nerd_face:"🤓",neutral_face:"😐",new:"🆕",new_moon:"🌑",new_moon_with_face:"🌚",newspaper:"📰",newspaper_roll:"🗞",next_track_button:"⏭",ng:"🆖",no_good_man:"🙅‍♂️",no_good_woman:"🙅",night_with_stars:"🌃",no_bell:"🔕",no_bicycles:"🚳",no_entry:"⛔️",no_entry_sign:"🚫",no_mobile_phones:"📵",no_mouth:"😶",no_pedestrians:"🚷",no_smoking:"🚭","non-potable_water":"🚱",nose:"👃",notebook:"📓",notebook_with_decorative_cover:"📔",notes:"🎶",nut_and_bolt:"🔩",o:"⭕️",o2:"🅾️",ocean:"🌊",octopus:"🐙",oden:"🍢",office:"🏢",oil_drum:"🛢",ok:"🆗",ok_hand:"👌",ok_man:"🙆‍♂️",ok_woman:"🙆",old_key:"🗝",older_man:"👴",older_woman:"👵",om:"🕉",on:"🔛",oncoming_automobile:"🚘",oncoming_bus:"🚍",oncoming_police_car:"🚔",oncoming_taxi:"🚖",open_file_folder:"📂",open_hands:"👐",open_mouth:"😮",open_umbrella:"☂️",ophiuchus:"⛎",orange_book:"📙",orthodox_cross:"☦️",outbox_tray:"📤",owl:"🦉",ox:"🐂",package:"📦",page_facing_up:"📄",page_with_curl:"📃",pager:"📟",paintbrush:"🖌",palm_tree:"🌴",pancakes:"🥞",panda_face:"🐼",paperclip:"📎",paperclips:"🖇",parasol_on_ground:"⛱",parking:"🅿️",part_alternation_mark:"〽️",partly_sunny:"⛅️",passenger_ship:"🛳",passport_control:"🛂",pause_button:"⏸",peace_symbol:"☮️",peach:"🍑",peanuts:"🥜",pear:"🍐",pen:"🖊",pencil2:"✏️",penguin:"🐧",pensive:"😔",performing_arts:"🎭",persevere:"😣",person_fencing:"🤺",pouting_woman:"🙎",phone:"☎️",pick:"⛏",pig:"🐷",pig2:"🐖",pig_nose:"🐽",pill:"💊",pineapple:"🍍",ping_pong:"🏓",pisces:"♓️",pizza:"🍕",place_of_worship:"🛐",plate_with_cutlery:"🍽",play_or_pause_button:"⏯",point_down:"👇",point_left:"👈",point_right:"👉",point_up:"☝️",point_up_2:"👆",police_car:"🚓",policewoman:"👮‍♀️",poodle:"🐩",popcorn:"🍿",post_office:"🏣",postal_horn:"📯",postbox:"📮",potable_water:"🚰",potato:"🥔",pouch:"👝",poultry_leg:"🍗",pound:"💷",rage:"😡",pouting_cat:"😾",pouting_man:"🙎‍♂️",pray:"🙏",prayer_beads:"📿",pregnant_woman:"🤰",previous_track_button:"⏮",prince:"🤴",princess:"👸",printer:"🖨",purple_heart:"💜",purse:"👛",pushpin:"📌",put_litter_in_its_place:"🚮",question:"❓",rabbit:"🐰",rabbit2:"🐇",racehorse:"🐎",racing_car:"🏎",radio:"📻",radio_button:"🔘",radioactive:"☢️",railway_car:"🚃",railway_track:"🛤",rainbow:"🌈",rainbow_flag:"🏳️‍🌈",raised_back_of_hand:"🤚",raised_hand_with_fingers_splayed:"🖐",raised_hands:"🙌",raising_hand_woman:"🙋",raising_hand_man:"🙋‍♂️",ram:"🐏",ramen:"🍜",rat:"🐀",record_button:"⏺",recycle:"♻️",red_circle:"🔴",registered:"®️",relaxed:"☺️",relieved:"😌",reminder_ribbon:"🎗",repeat:"🔁",repeat_one:"🔂",rescue_worker_helmet:"⛑",restroom:"🚻",revolving_hearts:"💞",rewind:"⏪",rhinoceros:"🦏",ribbon:"🎀",rice:"🍚",rice_ball:"🍙",rice_cracker:"🍘",rice_scene:"🎑",right_anger_bubble:"🗯",ring:"💍",robot:"🤖",rocket:"🚀",rofl:"🤣",roll_eyes:"🙄",roller_coaster:"🎢",rooster:"🐓",rose:"🌹",rosette:"🏵",rotating_light:"🚨",round_pushpin:"📍",rowing_man:"🚣",rowing_woman:"🚣‍♀️",rugby_football:"🏉",running_man:"🏃",running_shirt_with_sash:"🎽",running_woman:"🏃‍♀️",sa:"🈂️",sagittarius:"♐️",sake:"🍶",sandal:"👡",santa:"🎅",satellite:"📡",saxophone:"🎷",school:"🏫",school_satchel:"🎒",scissors:"✂️",scorpion:"🦂",scorpius:"♏️",scream:"😱",scream_cat:"🙀",scroll:"📜",seat:"💺",secret:"㊙️",see_no_evil:"🙈",seedling:"🌱",selfie:"🤳",shallow_pan_of_food:"🥘",shamrock:"☘️",shark:"🦈",shaved_ice:"🍧",sheep:"🐑",shell:"🐚",shield:"🛡",shinto_shrine:"⛩",ship:"🚢",shirt:"👕",shopping:"🛍",shopping_cart:"🛒",shower:"🚿",shrimp:"🦐",signal_strength:"📶",six_pointed_star:"🔯",ski:"🎿",skier:"⛷",skull:"💀",skull_and_crossbones:"☠️",sleeping:"😴",sleeping_bed:"🛌",sleepy:"😪",slightly_frowning_face:"🙁",slightly_smiling_face:"🙂",slot_machine:"🎰",small_airplane:"🛩",small_blue_diamond:"🔹",small_orange_diamond:"🔸",small_red_triangle:"🔺",small_red_triangle_down:"🔻",smile:"😄",smile_cat:"😸",smiley:"😃",smiley_cat:"😺",smiling_imp:"😈",smirk:"😏",smirk_cat:"😼",smoking:"🚬",snail:"🐌",snake:"🐍",sneezing_face:"🤧",snowboarder:"🏂",snowflake:"❄️",snowman:"⛄️",snowman_with_snow:"☃️",sob:"😭",soccer:"⚽️",soon:"🔜",sos:"🆘",sound:"🔉",space_invader:"👾",spades:"♠️",spaghetti:"🍝",sparkle:"❇️",sparkler:"🎇",sparkles:"✨",sparkling_heart:"💖",speak_no_evil:"🙊",speaker:"🔈",speaking_head:"🗣",speech_balloon:"💬",speedboat:"🚤",spider:"🕷",spider_web:"🕸",spiral_calendar:"🗓",spiral_notepad:"🗒",spoon:"🥄",squid:"🦑",stadium:"🏟",star:"⭐️",star2:"🌟",star_and_crescent:"☪️",star_of_david:"✡️",stars:"🌠",station:"🚉",statue_of_liberty:"🗽",steam_locomotive:"🚂",stew:"🍲",stop_button:"⏹",stop_sign:"🛑",stopwatch:"⏱",straight_ruler:"📏",strawberry:"🍓",stuck_out_tongue:"😛",stuck_out_tongue_closed_eyes:"😝",stuck_out_tongue_winking_eye:"😜",studio_microphone:"🎙",stuffed_flatbread:"🥙",sun_behind_large_cloud:"🌥",sun_behind_rain_cloud:"🌦",sun_behind_small_cloud:"🌤",sun_with_face:"🌞",sunflower:"🌻",sunglasses:"😎",sunny:"☀️",sunrise:"🌅",sunrise_over_mountains:"🌄",surfing_man:"🏄",surfing_woman:"🏄‍♀️",sushi:"🍣",suspension_railway:"🚟",sweat:"😓",sweat_drops:"💦",sweat_smile:"😅",sweet_potato:"🍠",swimming_man:"🏊",swimming_woman:"🏊‍♀️",symbols:"🔣",synagogue:"🕍",syringe:"💉",taco:"🌮",tada:"🎉",tanabata_tree:"🎋",taurus:"♉️",taxi:"🚕",tea:"🍵",telephone_receiver:"📞",telescope:"🔭",tennis:"🎾",tent:"⛺️",thermometer:"🌡",thinking:"🤔",thought_balloon:"💭",ticket:"🎫",tickets:"🎟",tiger:"🐯",tiger2:"🐅",timer_clock:"⏲",tipping_hand_man:"💁‍♂️",tired_face:"😫",tm:"™️",toilet:"🚽",tokyo_tower:"🗼",tomato:"🍅",tongue:"👅",top:"🔝",tophat:"🎩",tornado:"🌪",trackball:"🖲",tractor:"🚜",traffic_light:"🚥",train:"🚋",train2:"🚆",tram:"🚊",triangular_flag_on_post:"🚩",triangular_ruler:"📐",trident:"🔱",triumph:"😤",trolleybus:"🚎",trophy:"🏆",tropical_drink:"🍹",tropical_fish:"🐠",truck:"🚚",trumpet:"🎺",tulip:"🌷",tumbler_glass:"🥃",turkey:"🦃",turtle:"🐢",tv:"📺",twisted_rightwards_arrows:"🔀",two_hearts:"💕",two_men_holding_hands:"👬",two_women_holding_hands:"👭",u5272:"🈹",u5408:"🈴",u55b6:"🈺",u6307:"🈯️",u6708:"🈷️",u6709:"🈶",u6e80:"🈵",u7121:"🈚️",u7533:"🈸",u7981:"🈲",u7a7a:"🈳",umbrella:"☔️",unamused:"😒",underage:"🔞",unicorn:"🦄",unlock:"🔓",up:"🆙",upside_down_face:"🙃",v:"✌️",vertical_traffic_light:"🚦",vhs:"📼",vibration_mode:"📳",video_camera:"📹",video_game:"🎮",violin:"🎻",virgo:"♍️",volcano:"🌋",volleyball:"🏐",vs:"🆚",vulcan_salute:"🖖",walking_man:"🚶",walking_woman:"🚶‍♀️",waning_crescent_moon:"🌘",waning_gibbous_moon:"🌖",warning:"⚠️",wastebasket:"🗑",watch:"⌚️",water_buffalo:"🐃",watermelon:"🍉",wave:"👋",wavy_dash:"〰️",waxing_crescent_moon:"🌒",wc:"🚾",weary:"😩",wedding:"💒",weight_lifting_man:"🏋️",weight_lifting_woman:"🏋️‍♀️",whale:"🐳",whale2:"🐋",wheel_of_dharma:"☸️",wheelchair:"♿️",white_check_mark:"✅",white_circle:"⚪️",white_flag:"🏳️",white_flower:"💮",white_large_square:"⬜️",white_medium_small_square:"◽️",white_medium_square:"◻️",white_small_square:"▫️",white_square_button:"🔳",wilted_flower:"🥀",wind_chime:"🎐",wind_face:"🌬",wine_glass:"🍷",wink:"😉",wolf:"🐺",woman:"👩",woman_artist:"👩‍🎨",woman_astronaut:"👩‍🚀",woman_cartwheeling:"🤸‍♀️",woman_cook:"👩‍🍳",woman_facepalming:"🤦‍♀️",woman_factory_worker:"👩‍🏭",woman_farmer:"👩‍🌾",woman_firefighter:"👩‍🚒",woman_health_worker:"👩‍⚕️",woman_judge:"👩‍⚖️",woman_juggling:"🤹‍♀️",woman_mechanic:"👩‍🔧",woman_office_worker:"👩‍💼",woman_pilot:"👩‍✈️",woman_playing_handball:"🤾‍♀️",woman_playing_water_polo:"🤽‍♀️",woman_scientist:"👩‍🔬",woman_shrugging:"🤷‍♀️",woman_singer:"👩‍🎤",woman_student:"👩‍🎓",woman_teacher:"👩‍🏫",woman_technologist:"👩‍💻",woman_with_turban:"👳‍♀️",womans_clothes:"👚",womans_hat:"👒",women_wrestling:"🤼‍♀️",womens:"🚺",world_map:"🗺",worried:"😟",wrench:"🔧",writing_hand:"✍️",x:"❌",yellow_heart:"💛",yen:"💴",yin_yang:"☯️",yum:"😋",zap:"⚡️",zipper_mouth_face:"🤐",zzz:"💤",octocat:':octocat:',showdown:"S"},a.Converter=function(e){"use strict";function t(e,t){if(t=t||null,a.helper.isString(e)){if(e=a.helper.stdExtName(e),t=e,a.extensions[e])return console.warn("DEPRECATION WARNING: "+e+" is an old extension that uses a deprecated loading method.Please inform the developer that the extension should be updated!"),void function(e,t){"function"==typeof e&&(e=e(new a.Converter));a.helper.isArray(e)||(e=[e]);var n=r(e,t);if(!n.valid)throw Error(n.error);for(var s=0;s[ \t]+¨NBSP;<"),!r){if(!window||!window.document)throw new Error("HTMLParser is undefined. If in a webworker or nodejs environment, you need to provide a WHATWG DOM and HTML such as JSDOM");r=window.document}var n=r.createElement("div");n.innerHTML=e;var s={preList:function(e){for(var r=e.querySelectorAll("pre"),t=[],n=0;n'}else t.push(r[n].innerHTML),r[n].innerHTML="",r[n].setAttribute("prenum",n.toString());return t}(n)};t(n);for(var o=n.childNodes,i="",l=0;l? ?(['"].*['"])?\)$/m)>-1)o="";else if(!o){if(s||(s=n.toLowerCase().replace(/ ?\n/g," ")),o="#"+s,a.helper.isUndefined(t.gUrls[s]))return e;o=t.gUrls[s],a.helper.isUndefined(t.gTitles[s])||(c=t.gTitles[s])}var u='"};return e=(e=t.converter._dispatch("anchors.before",e,r,t)).replace(/\[((?:\[[^\]]*]|[^\[\]])*)] ?(?:\n *)?\[(.*?)]()()()()/g,n),e=e.replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]?<([^>]*)>(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g,n),e=e.replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]??(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g,n),e=e.replace(/\[([^\[\]]+)]()()()()()/g,n),r.ghMentions&&(e=e.replace(/(^|\s)(\\)?(@([a-z\d]+(?:[a-z\d.-]+?[a-z\d]+)*))/gim,function(e,t,n,s,o){if("\\"===n)return t+s;if(!a.helper.isString(r.ghMentionsLink))throw new Error("ghMentionsLink option must be a string");var i=r.ghMentionsLink.replace(/\{u}/g,o),l="";return r.openLinksInNewWindow&&(l=' target="¨E95Eblank"'),t+'"+s+""})),e=t.converter._dispatch("anchors.after",e,r,t)});var u=/([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+?\.[^'">\s]+?)()(\1)?(?=\s|$)(?!["<>])/gi,d=/([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+?)([.!?,()\[\]])?(\1)?(?=\s|$)(?!["<>])/gi,p=/()<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)()>()/gi,h=/(^|\s)(?:mailto:)?([A-Za-z0-9!#$%&'*+-/=?^_`{|}~.]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?=$|\s)/gim,_=/<()(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,g=function(e){"use strict";return function(r,t,n,s,o,i,l){var c=n=n.replace(a.helper.regexes.asteriskDashAndColon,a.helper.escapeCharactersCallback),u="",d="",p=t||"",h=l||"";return/^www\./i.test(n)&&(n=n.replace(/^www\./i,"http://www.")),e.excludeTrailingPunctuationFromURLs&&i&&(u=i),e.openLinksInNewWindow&&(d=' target="¨E95Eblank"'),p+'"+c+""+u+h}},m=function(e,r){"use strict";return function(t,n,s){var o="mailto:";return n=n||"",s=a.subParser("unescapeSpecialChars")(s,e,r),e.encodeEmails?(o=a.helper.encodeEmailAddress(o+s),s=a.helper.encodeEmailAddress(s)):o+=s,n+''+s+""}};a.subParser("autoLinks",function(e,r,t){"use strict";return e=t.converter._dispatch("autoLinks.before",e,r,t),e=e.replace(p,g(r)),e=e.replace(_,m(r,t)),e=t.converter._dispatch("autoLinks.after",e,r,t)}),a.subParser("simplifiedAutoLinks",function(e,r,t){"use strict";return r.simplifiedAutoLink?(e=t.converter._dispatch("simplifiedAutoLinks.before",e,r,t),e=r.excludeTrailingPunctuationFromURLs?e.replace(d,g(r)):e.replace(u,g(r)),e=e.replace(h,m(r,t)),e=t.converter._dispatch("simplifiedAutoLinks.after",e,r,t)):e}),a.subParser("blockGamut",function(e,r,t){"use strict";return e=t.converter._dispatch("blockGamut.before",e,r,t),e=a.subParser("blockQuotes")(e,r,t),e=a.subParser("headers")(e,r,t),e=a.subParser("horizontalRule")(e,r,t),e=a.subParser("lists")(e,r,t),e=a.subParser("codeBlocks")(e,r,t),e=a.subParser("tables")(e,r,t),e=a.subParser("hashHTMLBlocks")(e,r,t),e=a.subParser("paragraphs")(e,r,t),e=t.converter._dispatch("blockGamut.after",e,r,t)}),a.subParser("blockQuotes",function(e,r,t){"use strict";e=t.converter._dispatch("blockQuotes.before",e,r,t),e+="\n\n";var n=/(^ {0,3}>[ \t]?.+\n(.+\n)*\n*)+/gm;return r.splitAdjacentBlockquotes&&(n=/^ {0,3}>[\s\S]*?(?:\n\n)/gm),e=e.replace(n,function(e){return e=e.replace(/^[ \t]*>[ \t]?/gm,""),e=e.replace(/¨0/g,""),e=e.replace(/^[ \t]+$/gm,""),e=a.subParser("githubCodeBlocks")(e,r,t),e=a.subParser("blockGamut")(e,r,t),e=e.replace(/(^|\n)/g,"$1 "),e=e.replace(/(\s*
      [^\r]+?<\/pre>)/gm,function(e,r){var t=r;return t=t.replace(/^  /gm,"¨0"),t=t.replace(/¨0/g,"")}),a.subParser("hashBlock")("
      \n"+e+"\n
      ",r,t)}),e=t.converter._dispatch("blockQuotes.after",e,r,t)}),a.subParser("codeBlocks",function(e,r,t){"use strict";e=t.converter._dispatch("codeBlocks.before",e,r,t);return e=(e+="¨0").replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g,function(e,n,s){var o=n,i=s,l="\n";return o=a.subParser("outdent")(o,r,t),o=a.subParser("encodeCode")(o,r,t),o=a.subParser("detab")(o,r,t),o=o.replace(/^\n+/g,""),o=o.replace(/\n+$/g,""),r.omitExtraWLInCodeBlocks&&(l=""),o="
      "+o+l+"
      ",a.subParser("hashBlock")(o,r,t)+i}),e=e.replace(/¨0/,""),e=t.converter._dispatch("codeBlocks.after",e,r,t)}),a.subParser("codeSpans",function(e,r,t){"use strict";return void 0===(e=t.converter._dispatch("codeSpans.before",e,r,t))&&(e=""),e=e.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(e,n,s,o){var i=o;return i=i.replace(/^([ \t]*)/g,""),i=i.replace(/[ \t]*$/g,""),i=a.subParser("encodeCode")(i,r,t),i=n+""+i+"",i=a.subParser("hashHTMLSpans")(i,r,t)}),e=t.converter._dispatch("codeSpans.after",e,r,t)}),a.subParser("completeHTMLDocument",function(e,r,t){"use strict";if(!r.completeHTMLDocument)return e;e=t.converter._dispatch("completeHTMLDocument.before",e,r,t);var a="html",n="\n",s="",o='\n',i="",l="";void 0!==t.metadata.parsed.doctype&&(n="\n","html"!==(a=t.metadata.parsed.doctype.toString().toLowerCase())&&"html5"!==a||(o=''));for(var c in t.metadata.parsed)if(t.metadata.parsed.hasOwnProperty(c))switch(c.toLowerCase()){case"doctype":break;case"title":s=""+t.metadata.parsed.title+"\n";break;case"charset":o="html"===a||"html5"===a?'\n':'\n';break;case"language":case"lang":i=' lang="'+t.metadata.parsed[c]+'"',l+='\n';break;default:l+='\n'}return e=n+"\n\n"+s+o+l+"\n\n"+e.trim()+"\n\n",e=t.converter._dispatch("completeHTMLDocument.after",e,r,t)}),a.subParser("detab",function(e,r,t){"use strict";return e=t.converter._dispatch("detab.before",e,r,t),e=e.replace(/\t(?=\t)/g," "),e=e.replace(/\t/g,"¨A¨B"),e=e.replace(/¨B(.+?)¨A/g,function(e,r){for(var t=r,a=4-t.length%4,n=0;n/g,">"),e=t.converter._dispatch("encodeAmpsAndAngles.after",e,r,t)}),a.subParser("encodeBackslashEscapes",function(e,r,t){"use strict";return e=t.converter._dispatch("encodeBackslashEscapes.before",e,r,t),e=e.replace(/\\(\\)/g,a.helper.escapeCharactersCallback),e=e.replace(/\\([`*_{}\[\]()>#+.!~=|-])/g,a.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeBackslashEscapes.after",e,r,t)}),a.subParser("encodeCode",function(e,r,t){"use strict";return e=t.converter._dispatch("encodeCode.before",e,r,t),e=e.replace(/&/g,"&").replace(//g,">").replace(/([*_{}\[\]\\=~-])/g,a.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeCode.after",e,r,t)}),a.subParser("escapeSpecialCharsWithinTagAttributes",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.before",e,r,t)).replace(/<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi,function(e){return e.replace(/(.)<\/?code>(?=.)/g,"$1`").replace(/([\\`*_~=|])/g,a.helper.escapeCharactersCallback)}),e=e.replace(/-]|-[^>])(?:[^-]|-[^-])*)--)>/gi,function(e){return e.replace(/([\\`*_~=|])/g,a.helper.escapeCharactersCallback)}),e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.after",e,r,t)}),a.subParser("githubCodeBlocks",function(e,r,t){"use strict";return r.ghCodeBlocks?(e=t.converter._dispatch("githubCodeBlocks.before",e,r,t),e+="¨0",e=e.replace(/(?:^|\n)(?: {0,3})(```+|~~~+)(?: *)([^\s`~]*)\n([\s\S]*?)\n(?: {0,3})\1/g,function(e,n,s,o){var i=r.omitExtraWLInCodeBlocks?"":"\n";return o=a.subParser("encodeCode")(o,r,t),o=a.subParser("detab")(o,r,t),o=o.replace(/^\n+/g,""),o=o.replace(/\n+$/g,""),o="
      "+o+i+"
      ",o=a.subParser("hashBlock")(o,r,t),"\n\n¨G"+(t.ghCodeBlocks.push({text:e,codeblock:o})-1)+"G\n\n"}),e=e.replace(/¨0/,""),t.converter._dispatch("githubCodeBlocks.after",e,r,t)):e}),a.subParser("hashBlock",function(e,r,t){"use strict";return e=t.converter._dispatch("hashBlock.before",e,r,t),e=e.replace(/(^\n+|\n+$)/g,""),e="\n\n¨K"+(t.gHtmlBlocks.push(e)-1)+"K\n\n",e=t.converter._dispatch("hashBlock.after",e,r,t)}),a.subParser("hashCodeTags",function(e,r,t){"use strict";e=t.converter._dispatch("hashCodeTags.before",e,r,t);return e=a.helper.replaceRecursiveRegExp(e,function(e,n,s,o){var i=s+a.subParser("encodeCode")(n,r,t)+o;return"¨C"+(t.gHtmlSpans.push(i)-1)+"C"},"]*>","","gim"),e=t.converter._dispatch("hashCodeTags.after",e,r,t)}),a.subParser("hashElement",function(e,r,t){"use strict";return function(e,r){var a=r;return a=a.replace(/\n\n/g,"\n"),a=a.replace(/^\n/,""),a=a.replace(/\n+$/g,""),a="\n\n¨K"+(t.gHtmlBlocks.push(a)-1)+"K\n\n"}}),a.subParser("hashHTMLBlocks",function(e,r,t){"use strict";e=t.converter._dispatch("hashHTMLBlocks.before",e,r,t);var n=["pre","div","h1","h2","h3","h4","h5","h6","blockquote","table","dl","ol","ul","script","noscript","form","fieldset","iframe","math","style","section","header","footer","nav","article","aside","address","audio","canvas","figure","hgroup","output","video","p"],s=function(e,r,a,n){var s=e;return-1!==a.search(/\bmarkdown\b/)&&(s=a+t.converter.makeHtml(r)+n),"\n\n¨K"+(t.gHtmlBlocks.push(s)-1)+"K\n\n"};r.backslashEscapesHTMLTags&&(e=e.replace(/\\<(\/?[^>]+?)>/g,function(e,r){return"<"+r+">"}));for(var o=0;o]*>)","im"),c="<"+n[o]+"\\b[^>]*>",u="";-1!==(i=a.helper.regexIndexOf(e,l));){var d=a.helper.splitAtIndex(e,i),p=a.helper.replaceRecursiveRegExp(d[1],s,c,u,"im");if(p===d[1])break;e=d[0].concat(p)}return e=e.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,a.subParser("hashElement")(e,r,t)),e=a.helper.replaceRecursiveRegExp(e,function(e){return"\n\n¨K"+(t.gHtmlBlocks.push(e)-1)+"K\n\n"},"^ {0,3}\x3c!--","--\x3e","gm"),e=e.replace(/(?:\n\n)( {0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,a.subParser("hashElement")(e,r,t)),e=t.converter._dispatch("hashHTMLBlocks.after",e,r,t)}),a.subParser("hashHTMLSpans",function(e,r,t){"use strict";function a(e){return"¨C"+(t.gHtmlSpans.push(e)-1)+"C"}return e=t.converter._dispatch("hashHTMLSpans.before",e,r,t),e=e.replace(/<[^>]+?\/>/gi,function(e){return a(e)}),e=e.replace(/<([^>]+?)>[\s\S]*?<\/\1>/g,function(e){return a(e)}),e=e.replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g,function(e){return a(e)}),e=e.replace(/<[^>]+?>/gi,function(e){return a(e)}),e=t.converter._dispatch("hashHTMLSpans.after",e,r,t)}),a.subParser("unhashHTMLSpans",function(e,r,t){"use strict";e=t.converter._dispatch("unhashHTMLSpans.before",e,r,t);for(var a=0;a]*>\\s*]*>","^ {0,3}\\s*
      ","gim"),e=t.converter._dispatch("hashPreCodeTags.after",e,r,t)}),a.subParser("headers",function(e,r,t){"use strict";function n(e){var n,s;if(r.customizedHeaderId){var o=e.match(/\{([^{]+?)}\s*$/);o&&o[1]&&(e=o[1])}return n=e,s=a.helper.isString(r.prefixHeaderId)?r.prefixHeaderId:!0===r.prefixHeaderId?"section-":"",r.rawPrefixHeaderId||(n=s+n),n=r.ghCompatibleHeaderId?n.replace(/ /g,"-").replace(/&/g,"").replace(/¨T/g,"").replace(/¨D/g,"").replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g,"").toLowerCase():r.rawHeaderId?n.replace(/ /g,"-").replace(/&/g,"&").replace(/¨T/g,"¨").replace(/¨D/g,"$").replace(/["']/g,"-").toLowerCase():n.replace(/[^\w]/g,"").toLowerCase(),r.rawPrefixHeaderId&&(n=s+n),t.hashLinkCounts[n]?n=n+"-"+t.hashLinkCounts[n]++:t.hashLinkCounts[n]=1,n}e=t.converter._dispatch("headers.before",e,r,t);var s=isNaN(parseInt(r.headerLevelStart))?1:parseInt(r.headerLevelStart),o=r.smoothLivePreview?/^(.+)[ \t]*\n={2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n=+[ \t]*\n+/gm,i=r.smoothLivePreview?/^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n-+[ \t]*\n+/gm;e=(e=e.replace(o,function(e,o){var i=a.subParser("spanGamut")(o,r,t),l=r.noHeaderId?"":' id="'+n(o)+'"',c=""+i+"";return a.subParser("hashBlock")(c,r,t)})).replace(i,function(e,o){var i=a.subParser("spanGamut")(o,r,t),l=r.noHeaderId?"":' id="'+n(o)+'"',c=s+1,u=""+i+"";return a.subParser("hashBlock")(u,r,t)});var l=r.requireSpaceBeforeHeadingText?/^(#{1,6})[ \t]+(.+?)[ \t]*#*\n+/gm:/^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm;return e=e.replace(l,function(e,o,i){var l=i;r.customizedHeaderId&&(l=i.replace(/\s?\{([^{]+?)}\s*$/,""));var c=a.subParser("spanGamut")(l,r,t),u=r.noHeaderId?"":' id="'+n(i)+'"',d=s-1+o.length,p=""+c+"";return a.subParser("hashBlock")(p,r,t)}),e=t.converter._dispatch("headers.after",e,r,t)}),a.subParser("horizontalRule",function(e,r,t){"use strict";e=t.converter._dispatch("horizontalRule.before",e,r,t);var n=a.subParser("hashBlock")("
      ",r,t);return e=e.replace(/^ {0,2}( ?-){3,}[ \t]*$/gm,n),e=e.replace(/^ {0,2}( ?\*){3,}[ \t]*$/gm,n),e=e.replace(/^ {0,2}( ?_){3,}[ \t]*$/gm,n),e=t.converter._dispatch("horizontalRule.after",e,r,t)}),a.subParser("images",function(e,r,t){"use strict";function n(e,r,n,s,o,i,l,c){var u=t.gUrls,d=t.gTitles,p=t.gDimensions;if(n=n.toLowerCase(),c||(c=""),e.search(/\(? ?(['"].*['"])?\)$/m)>-1)s="";else if(""===s||null===s){if(""!==n&&null!==n||(n=r.toLowerCase().replace(/ ?\n/g," ")),s="#"+n,a.helper.isUndefined(u[n]))return e;s=u[n],a.helper.isUndefined(d[n])||(c=d[n]),a.helper.isUndefined(p[n])||(o=p[n].width,i=p[n].height)}r=r.replace(/"/g,""").replace(a.helper.regexes.asteriskDashAndColon,a.helper.escapeCharactersCallback);var h=''+r+'"}return e=(e=t.converter._dispatch("images.before",e,r,t)).replace(/!\[([^\]]*?)] ?(?:\n *)?\[([\s\S]*?)]()()()()()/g,n),e=e.replace(/!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g,function(e,r,t,a,s,o,i,l){return a=a.replace(/\s/g,""),n(e,r,t,a,s,o,0,l)}),e=e.replace(/!\[([^\]]*?)][ \t]*()\([ \t]?<([^>]*)>(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(?:(["'])([^"]*?)\6))?[ \t]?\)/g,n),e=e.replace(/!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g,n),e=e.replace(/!\[([^\[\]]+)]()()()()()/g,n),e=t.converter._dispatch("images.after",e,r,t)}),a.subParser("italicsAndBold",function(e,r,t){"use strict";function a(e,r,t){return r+e+t}return e=t.converter._dispatch("italicsAndBold.before",e,r,t),e=r.literalMidWordUnderscores?(e=(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,r){return a(r,"","")})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,r){return a(r,"","")})).replace(/\b_(\S[\s\S]*?)_\b/g,function(e,r){return a(r,"","")}):(e=(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e})).replace(/__(\S[\s\S]*?)__/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e})).replace(/_([^\s_][\s\S]*?)_/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e}),e=r.literalMidWordAsterisks?(e=(e=e.replace(/([^*]|^)\B\*\*\*(\S[\s\S]*?)\*\*\*\B(?!\*)/g,function(e,r,t){return a(t,r+"","")})).replace(/([^*]|^)\B\*\*(\S[\s\S]*?)\*\*\B(?!\*)/g,function(e,r,t){return a(t,r+"","")})).replace(/([^*]|^)\B\*(\S[\s\S]*?)\*\B(?!\*)/g,function(e,r,t){return a(t,r+"","")}):(e=(e=e.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e})).replace(/\*\*(\S[\s\S]*?)\*\*/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e})).replace(/\*([^\s*][\s\S]*?)\*/g,function(e,r){return/\S$/.test(r)?a(r,"",""):e}),e=t.converter._dispatch("italicsAndBold.after",e,r,t)}),a.subParser("lists",function(e,r,t){"use strict";function n(e,n){t.gListLevel++,e=e.replace(/\n{2,}$/,"\n");var s=/(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0| {0,3}([*+-]|\d+[.])[ \t]+))/gm,o=/\n[ \t]*\n(?!¨0)/.test(e+="¨0");return r.disableForced4SpacesIndentedSublists&&(s=/(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0|\2([*+-]|\d+[.])[ \t]+))/gm),e=e.replace(s,function(e,n,s,i,l,c,u){u=u&&""!==u.trim();var d=a.subParser("outdent")(l,r,t),p="";return c&&r.tasklists&&(p=' class="task-list-item" style="list-style-type: none;"',d=d.replace(/^[ \t]*\[(x|X| )?]/m,function(){var e='-1?(d=a.subParser("githubCodeBlocks")(d,r,t),d=a.subParser("blockGamut")(d,r,t)):(d=(d=a.subParser("lists")(d,r,t)).replace(/\n$/,""),d=(d=a.subParser("hashHTMLBlocks")(d,r,t)).replace(/\n\n+/g,"\n\n"),d=o?a.subParser("paragraphs")(d,r,t):a.subParser("spanGamut")(d,r,t)),d=d.replace("¨A",""),d=""+d+"\n"}),e=e.replace(/¨0/g,""),t.gListLevel--,n&&(e=e.replace(/\s+$/,"")),e}function s(e,r){if("ol"===r){var t=e.match(/^ *(\d+)\./);if(t&&"1"!==t[1])return' start="'+t[1]+'"'}return""}function o(e,t,a){var o=r.disableForced4SpacesIndentedSublists?/^ ?\d+\.[ \t]/gm:/^ {0,3}\d+\.[ \t]/gm,i=r.disableForced4SpacesIndentedSublists?/^ ?[*+-][ \t]/gm:/^ {0,3}[*+-][ \t]/gm,l="ul"===t?o:i,c="";if(-1!==e.search(l))!function r(u){var d=u.search(l),p=s(e,t);-1!==d?(c+="\n\n<"+t+p+">\n"+n(u.slice(0,d),!!a)+"\n",l="ul"===(t="ul"===t?"ol":"ul")?o:i,r(u.slice(d))):c+="\n\n<"+t+p+">\n"+n(u,!!a)+"\n"}(e);else{var u=s(e,t);c="\n\n<"+t+u+">\n"+n(e,!!a)+"\n"}return c}return e=t.converter._dispatch("lists.before",e,r,t),e+="¨0",e=t.gListLevel?e.replace(/^(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm,function(e,r,t){return o(r,t.search(/[*+-]/g)>-1?"ul":"ol",!0)}):e.replace(/(\n\n|^\n?)(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm,function(e,r,t,a){return o(t,a.search(/[*+-]/g)>-1?"ul":"ol",!1)}),e=e.replace(/¨0/,""),e=t.converter._dispatch("lists.after",e,r,t)}),a.subParser("metadata",function(e,r,t){"use strict";function a(e){t.metadata.raw=e,(e=(e=e.replace(/&/g,"&").replace(/"/g,""")).replace(/\n {4}/g," ")).replace(/^([\S ]+): +([\s\S]+?)$/gm,function(e,r,a){return t.metadata.parsed[r]=a,""})}return r.metadata?(e=t.converter._dispatch("metadata.before",e,r,t),e=e.replace(/^\s*«««+(\S*?)\n([\s\S]+?)\n»»»+\n/,function(e,r,t){return a(t),"¨M"}),e=e.replace(/^\s*---+(\S*?)\n([\s\S]+?)\n---+\n/,function(e,r,n){return r&&(t.metadata.format=r),a(n),"¨M"}),e=e.replace(/¨M/g,""),e=t.converter._dispatch("metadata.after",e,r,t)):e}),a.subParser("outdent",function(e,r,t){"use strict";return e=t.converter._dispatch("outdent.before",e,r,t),e=e.replace(/^(\t|[ ]{1,4})/gm,"¨0"),e=e.replace(/¨0/g,""),e=t.converter._dispatch("outdent.after",e,r,t)}),a.subParser("paragraphs",function(e,r,t){"use strict";for(var n=(e=(e=(e=t.converter._dispatch("paragraphs.before",e,r,t)).replace(/^\n+/g,"")).replace(/\n+$/g,"")).split(/\n{2,}/g),s=[],o=n.length,i=0;i=0?s.push(l):l.search(/\S/)>=0&&(l=(l=a.subParser("spanGamut")(l,r,t)).replace(/^([ \t]*)/g,"

      "),l+="

      ",s.push(l))}for(o=s.length,i=0;i]*>\s*]*>/.test(u)&&(d=!0)}s[i]=u}return e=s.join("\n"),e=e.replace(/^\n+/g,""),e=e.replace(/\n+$/g,""),t.converter._dispatch("paragraphs.after",e,r,t)}),a.subParser("runExtension",function(e,r,t,a){"use strict";if(e.filter)r=e.filter(r,a.converter,t);else if(e.regex){var n=e.regex;n instanceof RegExp||(n=new RegExp(n,"g")),r=r.replace(n,e.replace)}return r}),a.subParser("spanGamut",function(e,r,t){"use strict";return e=t.converter._dispatch("spanGamut.before",e,r,t),e=a.subParser("codeSpans")(e,r,t),e=a.subParser("escapeSpecialCharsWithinTagAttributes")(e,r,t),e=a.subParser("encodeBackslashEscapes")(e,r,t),e=a.subParser("images")(e,r,t),e=a.subParser("anchors")(e,r,t),e=a.subParser("autoLinks")(e,r,t),e=a.subParser("simplifiedAutoLinks")(e,r,t),e=a.subParser("emoji")(e,r,t),e=a.subParser("underline")(e,r,t),e=a.subParser("italicsAndBold")(e,r,t),e=a.subParser("strikethrough")(e,r,t),e=a.subParser("ellipsis")(e,r,t),e=a.subParser("hashHTMLSpans")(e,r,t),e=a.subParser("encodeAmpsAndAngles")(e,r,t),r.simpleLineBreaks?/\n\n¨K/.test(e)||(e=e.replace(/\n+/g,"
      \n")):e=e.replace(/ +\n/g,"
      \n"),e=t.converter._dispatch("spanGamut.after",e,r,t)}),a.subParser("strikethrough",function(e,r,t){"use strict";return r.strikethrough&&(e=(e=t.converter._dispatch("strikethrough.before",e,r,t)).replace(/(?:~){2}([\s\S]+?)(?:~){2}/g,function(e,n){return function(e){return r.simplifiedAutoLink&&(e=a.subParser("simplifiedAutoLinks")(e,r,t)),""+e+""}(n)}),e=t.converter._dispatch("strikethrough.after",e,r,t)),e}),a.subParser("stripLinkDefinitions",function(e,r,t){"use strict";var n=function(e,n,s,o,i,l,c){return n=n.toLowerCase(),s.match(/^data:.+?\/.+?;base64,/)?t.gUrls[n]=s.replace(/\s/g,""):t.gUrls[n]=a.subParser("encodeAmpsAndAngles")(s,r,t),l?l+c:(c&&(t.gTitles[n]=c.replace(/"|'/g,""")),r.parseImgDimensions&&o&&i&&(t.gDimensions[n]={width:o,height:i}),"")};return e=(e+="¨0").replace(/^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm,n),e=e.replace(/^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm,n),e=e.replace(/¨0/,"")}),a.subParser("tables",function(e,r,t){"use strict";function n(e){return/^:[ \t]*--*$/.test(e)?' style="text-align:left;"':/^--*[ \t]*:[ \t]*$/.test(e)?' style="text-align:right;"':/^:[ \t]*--*[ \t]*:$/.test(e)?' style="text-align:center;"':""}function s(e,n){var s="";return e=e.trim(),(r.tablesHeaderId||r.tableHeaderId)&&(s=' id="'+e.replace(/ /g,"_").toLowerCase()+'"'),e=a.subParser("spanGamut")(e,r,t),""+e+"\n"}function o(e,n){return""+a.subParser("spanGamut")(e,r,t)+"\n"}function i(e){var i,l=e.split("\n");for(i=0;i\n\n\n",n=0;n\n";for(var s=0;s\n"}return t+="\n\n"}(p,_)}if(!r.tables)return e;return e=t.converter._dispatch("tables.before",e,r,t),e=e.replace(/\\(\|)/g,a.helper.escapeCharactersCallback),e=e.replace(/^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|¨0)/gm,i),e=e.replace(/^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm,i),e=t.converter._dispatch("tables.after",e,r,t)}),a.subParser("underline",function(e,r,t){"use strict";return r.underline?(e=t.converter._dispatch("underline.before",e,r,t),e=r.literalMidWordUnderscores?(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,r){return""+r+""})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,r){return""+r+""}):(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/__(\S[\s\S]*?)__/g,function(e,r){return/\S$/.test(r)?""+r+"":e}),e=e.replace(/(_)/g,a.helper.escapeCharactersCallback),e=t.converter._dispatch("underline.after",e,r,t)):e}),a.subParser("unescapeSpecialChars",function(e,r,t){"use strict";return e=t.converter._dispatch("unescapeSpecialChars.before",e,r,t),e=e.replace(/¨E(\d+)E/g,function(e,r){var t=parseInt(r);return String.fromCharCode(t)}),e=t.converter._dispatch("unescapeSpecialChars.after",e,r,t)}),a.subParser("makeMarkdown.blockquote",function(e,r){"use strict";var t="";if(e.hasChildNodes())for(var n=e.childNodes,s=n.length,o=0;o "+t.split("\n").join("\n> ")}),a.subParser("makeMarkdown.codeBlock",function(e,r){"use strict";var t=e.getAttribute("language"),a=e.getAttribute("precodenum");return"```"+t+"\n"+r.preList[a]+"\n```"}),a.subParser("makeMarkdown.codeSpan",function(e){"use strict";return"`"+e.innerHTML+"`"}),a.subParser("makeMarkdown.emphasis",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="*";for(var n=e.childNodes,s=n.length,o=0;o",e.hasAttribute("width")&&e.hasAttribute("height")&&(r+=" ="+e.getAttribute("width")+"x"+e.getAttribute("height")),e.hasAttribute("title")&&(r+=' "'+e.getAttribute("title")+'"'),r+=")"),r}),a.subParser("makeMarkdown.links",function(e,r){"use strict";var t="";if(e.hasChildNodes()&&e.hasAttribute("href")){var n=e.childNodes,s=n.length;t="[";for(var o=0;o",e.hasAttribute("title")&&(t+=' "'+e.getAttribute("title")+'"'),t+=")"}return t}),a.subParser("makeMarkdown.list",function(e,r,t){"use strict";var n="";if(!e.hasChildNodes())return"";for(var s=e.childNodes,o=s.length,i=e.getAttribute("start")||1,l=0;l"+r.preList[t]+""}),a.subParser("makeMarkdown.strikethrough",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="~~";for(var n=e.childNodes,s=n.length,o=0;otr>th"),l=e.querySelectorAll("tbody>tr");for(t=0;t_&&(_=g)}for(t=0;t/g,"\\$1>"),r=r.replace(/^#/gm,"\\#"),r=r.replace(/^(\s*)([-=]{3,})(\s*)$/,"$1\\$2$3"),r=r.replace(/^( {0,3}\d+)\./gm,"$1\\."),r=r.replace(/^( {0,3})([+-])/gm,"$1\\$2"),r=r.replace(/]([\s]*)\(/g,"\\]$1\\("),r=r.replace(/^ {0,3}\[([\S \t]*?)]:/gm,"\\[$1]:")});"function"==typeof define&&define.amd?define(function(){"use strict";return a}):"undefined"!=typeof module&&module.exports?module.exports=a:this.showdown=a}).call(this); -//# sourceMappingURL=showdown.min.js.map +/** + * helper namespace + * @type {{}} + */ +showdown.helper = {}; + +/** + * TODO LEGACY SUPPORT CODE + * @type {{}} + */ +showdown.extensions = {}; + +/** + * Set a global option + * @static + * @param {string} key + * @param {*} value + * @returns {showdown} + */ +showdown.setOption = function (key, value) { + 'use strict'; + globalOptions[key] = value; + return this; +}; + +/** + * Get a global option + * @static + * @param {string} key + * @returns {*} + */ +showdown.getOption = function (key) { + 'use strict'; + return globalOptions[key]; +}; + +/** + * Get the global options + * @static + * @returns {{}} + */ +showdown.getOptions = function () { + 'use strict'; + return globalOptions; +}; + +/** + * Reset global options to the default values + * @static + */ +showdown.resetOptions = function () { + 'use strict'; + globalOptions = getDefaultOpts(true); +}; + +/** + * Set the flavor showdown should use as default + * @param {string} name + */ +showdown.setFlavor = function (name) { + 'use strict'; + if (!flavor.hasOwnProperty(name)) { + throw Error(name + ' flavor was not found'); + } + showdown.resetOptions(); + var preset = flavor[name]; + setFlavor = name; + for (var option in preset) { + if (preset.hasOwnProperty(option)) { + globalOptions[option] = preset[option]; + } + } +}; + +/** + * Get the currently set flavor + * @returns {string} + */ +showdown.getFlavor = function () { + 'use strict'; + return setFlavor; +}; + +/** + * Get the options of a specified flavor. Returns undefined if the flavor was not found + * @param {string} name Name of the flavor + * @returns {{}|undefined} + */ +showdown.getFlavorOptions = function (name) { + 'use strict'; + if (flavor.hasOwnProperty(name)) { + return flavor[name]; + } +}; + +/** + * Get the default options + * @static + * @param {boolean} [simple=true] + * @returns {{}} + */ +showdown.getDefaultOptions = function (simple) { + 'use strict'; + return getDefaultOpts(simple); +}; + +/** + * Get or set a subParser + * + * subParser(name) - Get a registered subParser + * subParser(name, func) - Register a subParser + * @static + * @param {string} name + * @param {function} [func] + * @returns {*} + */ +showdown.subParser = function (name, func) { + 'use strict'; + if (showdown.helper.isString(name)) { + if (typeof func !== 'undefined') { + parsers[name] = func; + } else { + if (parsers.hasOwnProperty(name)) { + return parsers[name]; + } else { + throw Error('SubParser named ' + name + ' not registered!'); + } + } + } +}; + +/** + * Gets or registers an extension + * @static + * @param {string} name + * @param {object|function=} ext + * @returns {*} + */ +showdown.extension = function (name, ext) { + 'use strict'; + + if (!showdown.helper.isString(name)) { + throw Error('Extension \'name\' must be a string'); + } + + name = showdown.helper.stdExtName(name); + + // Getter + if (showdown.helper.isUndefined(ext)) { + if (!extensions.hasOwnProperty(name)) { + throw Error('Extension named ' + name + ' is not registered!'); + } + return extensions[name]; + + // Setter + } else { + // Expand extension if it's wrapped in a function + if (typeof ext === 'function') { + ext = ext(); + } + + // Ensure extension is an array + if (!showdown.helper.isArray(ext)) { + ext = [ext]; + } + + var validExtension = validate(ext, name); + + if (validExtension.valid) { + extensions[name] = ext; + } else { + throw Error(validExtension.error); + } + } +}; + +/** + * Gets all extensions registered + * @returns {{}} + */ +showdown.getAllExtensions = function () { + 'use strict'; + return extensions; +}; + +/** + * Remove an extension + * @param {string} name + */ +showdown.removeExtension = function (name) { + 'use strict'; + delete extensions[name]; +}; + +/** + * Removes all extensions + */ +showdown.resetExtensions = function () { + 'use strict'; + extensions = {}; +}; + +/** + * Validate extension + * @param {array} extension + * @param {string} name + * @returns {{valid: boolean, error: string}} + */ +function validate (extension, name) { + 'use strict'; + + var errMsg = (name) ? 'Error in ' + name + ' extension->' : 'Error in unnamed extension', + ret = { + valid: true, + error: '' + }; + + if (!showdown.helper.isArray(extension)) { + extension = [extension]; + } + + for (var i = 0; i < extension.length; ++i) { + var baseMsg = errMsg + ' sub-extension ' + i + ': ', + ext = extension[i]; + if (typeof ext !== 'object') { + ret.valid = false; + ret.error = baseMsg + 'must be an object, but ' + typeof ext + ' given'; + return ret; + } + + if (!showdown.helper.isString(ext.type)) { + ret.valid = false; + ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given'; + return ret; + } + + var type = ext.type = ext.type.toLowerCase(); + + // normalize extension type + if (type === 'language') { + type = ext.type = 'lang'; + } + + if (type === 'html') { + type = ext.type = 'output'; + } + + if (type !== 'lang' && type !== 'output' && type !== 'listener') { + ret.valid = false; + ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang/language", "output/html" or "listener"'; + return ret; + } + + if (type === 'listener') { + if (showdown.helper.isUndefined(ext.listeners)) { + ret.valid = false; + ret.error = baseMsg + '. Extensions of type "listener" must have a property called "listeners"'; + return ret; + } + } else { + if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) { + ret.valid = false; + ret.error = baseMsg + type + ' extensions must define either a "regex" property or a "filter" method'; + return ret; + } + } + + if (ext.listeners) { + if (typeof ext.listeners !== 'object') { + ret.valid = false; + ret.error = baseMsg + '"listeners" property must be an object but ' + typeof ext.listeners + ' given'; + return ret; + } + for (var ln in ext.listeners) { + if (ext.listeners.hasOwnProperty(ln)) { + if (typeof ext.listeners[ln] !== 'function') { + ret.valid = false; + ret.error = baseMsg + '"listeners" property must be an hash of [event name]: [callback]. listeners.' + ln + + ' must be a function but ' + typeof ext.listeners[ln] + ' given'; + return ret; + } + } + } + } + + if (ext.filter) { + if (typeof ext.filter !== 'function') { + ret.valid = false; + ret.error = baseMsg + '"filter" must be a function, but ' + typeof ext.filter + ' given'; + return ret; + } + } else if (ext.regex) { + if (showdown.helper.isString(ext.regex)) { + ext.regex = new RegExp(ext.regex, 'g'); + } + if (!(ext.regex instanceof RegExp)) { + ret.valid = false; + ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' + typeof ext.regex + ' given'; + return ret; + } + if (showdown.helper.isUndefined(ext.replace)) { + ret.valid = false; + ret.error = baseMsg + '"regex" extensions must implement a replace string or function'; + return ret; + } + } + } + return ret; +} + +/** + * Validate extension + * @param {object} ext + * @returns {boolean} + */ +showdown.validateExtension = function (ext) { + 'use strict'; + + var validateExtension = validate(ext, null); + if (!validateExtension.valid) { + console.warn(validateExtension.error); + return false; + } + return true; +}; + +/** + * showdownjs helper functions + */ + +if (!showdown.hasOwnProperty('helper')) { + showdown.helper = {}; +} + +/** + * Check if var is string + * @static + * @param {string} a + * @returns {boolean} + */ +showdown.helper.isString = function (a) { + 'use strict'; + return (typeof a === 'string' || a instanceof String); +}; + +/** + * Check if var is a function + * @static + * @param {*} a + * @returns {boolean} + */ +showdown.helper.isFunction = function (a) { + 'use strict'; + var getType = {}; + return a && getType.toString.call(a) === '[object Function]'; +}; + +/** + * isArray helper function + * @static + * @param {*} a + * @returns {boolean} + */ +showdown.helper.isArray = function (a) { + 'use strict'; + return Array.isArray(a); +}; + +/** + * Check if value is undefined + * @static + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. + */ +showdown.helper.isUndefined = function (value) { + 'use strict'; + return typeof value === 'undefined'; +}; + +/** + * ForEach helper function + * Iterates over Arrays and Objects (own properties only) + * @static + * @param {*} obj + * @param {function} callback Accepts 3 params: 1. value, 2. key, 3. the original array/object + */ +showdown.helper.forEach = function (obj, callback) { + 'use strict'; + // check if obj is defined + if (showdown.helper.isUndefined(obj)) { + throw new Error('obj param is required'); + } + + if (showdown.helper.isUndefined(callback)) { + throw new Error('callback param is required'); + } + + if (!showdown.helper.isFunction(callback)) { + throw new Error('callback param must be a function/closure'); + } + + if (typeof obj.forEach === 'function') { + obj.forEach(callback); + } else if (showdown.helper.isArray(obj)) { + for (var i = 0; i < obj.length; i++) { + callback(obj[i], i, obj); + } + } else if (typeof (obj) === 'object') { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + callback(obj[prop], prop, obj); + } + } + } else { + throw new Error('obj does not seem to be an array or an iterable object'); + } +}; + +/** + * Standardidize extension name + * @static + * @param {string} s extension name + * @returns {string} + */ +showdown.helper.stdExtName = function (s) { + 'use strict'; + return s.replace(/[_?*+\/\\.^-]/g, '').replace(/\s/g, '').toLowerCase(); +}; + +function escapeCharactersCallback (wholeMatch, m1) { + 'use strict'; + var charCodeToEscape = m1.charCodeAt(0); + return '¨E' + charCodeToEscape + 'E'; +} + +/** + * Callback used to escape characters when passing through String.replace + * @static + * @param {string} wholeMatch + * @param {string} m1 + * @returns {string} + */ +showdown.helper.escapeCharactersCallback = escapeCharactersCallback; + +/** + * Escape characters in a string + * @static + * @param {string} text + * @param {string} charsToEscape + * @param {boolean} afterBackslash + * @returns {XML|string|void|*} + */ +showdown.helper.escapeCharacters = function (text, charsToEscape, afterBackslash) { + 'use strict'; + // First we have to escape the escape characters so that + // we can build a character class out of them + var regexString = '([' + charsToEscape.replace(/([\[\]\\])/g, '\\$1') + '])'; + + if (afterBackslash) { + regexString = '\\\\' + regexString; + } + + var regex = new RegExp(regexString, 'g'); + text = text.replace(regex, escapeCharactersCallback); + + return text; +}; + +/** + * Unescape HTML entities + * @param txt + * @returns {string} + */ +showdown.helper.unescapeHTMLEntities = function (txt) { + 'use strict'; + + return txt + .replace(/"/g, '"') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&'); +}; + +var rgxFindMatchPos = function (str, left, right, flags) { + 'use strict'; + var f = flags || '', + g = f.indexOf('g') > -1, + x = new RegExp(left + '|' + right, 'g' + f.replace(/g/g, '')), + l = new RegExp(left, f.replace(/g/g, '')), + pos = [], + t, s, m, start, end; + + do { + t = 0; + while ((m = x.exec(str))) { + if (l.test(m[0])) { + if (!(t++)) { + s = x.lastIndex; + start = s - m[0].length; + } + } else if (t) { + if (!--t) { + end = m.index + m[0].length; + var obj = { + left: {start: start, end: s}, + match: {start: s, end: m.index}, + right: {start: m.index, end: end}, + wholeMatch: {start: start, end: end} + }; + pos.push(obj); + if (!g) { + return pos; + } + } + } + } + } while (t && (x.lastIndex = s)); + + return pos; +}; + +/** + * matchRecursiveRegExp + * + * (c) 2007 Steven Levithan + * MIT License + * + * Accepts a string to search, a left and right format delimiter + * as regex patterns, and optional regex flags. Returns an array + * of matches, allowing nested instances of left/right delimiters. + * Use the "g" flag to return all matches, otherwise only the + * first is returned. Be careful to ensure that the left and + * right format delimiters produce mutually exclusive matches. + * Backreferences are not supported within the right delimiter + * due to how it is internally combined with the left delimiter. + * When matching strings whose format delimiters are unbalanced + * to the left or right, the output is intentionally as a + * conventional regex library with recursion support would + * produce, e.g. "<" and ">" both produce ["x"] when using + * "<" and ">" as the delimiters (both strings contain a single, + * balanced instance of ""). + * + * examples: + * matchRecursiveRegExp("test", "\\(", "\\)") + * returns: [] + * matchRecursiveRegExp(">>t<>", "<", ">", "g") + * returns: ["t<>", ""] + * matchRecursiveRegExp("
      test
      ", "]*>", "
      ", "gi") + * returns: ["test"] + */ +showdown.helper.matchRecursiveRegExp = function (str, left, right, flags) { + 'use strict'; + + var matchPos = rgxFindMatchPos (str, left, right, flags), + results = []; + + for (var i = 0; i < matchPos.length; ++i) { + results.push([ + str.slice(matchPos[i].wholeMatch.start, matchPos[i].wholeMatch.end), + str.slice(matchPos[i].match.start, matchPos[i].match.end), + str.slice(matchPos[i].left.start, matchPos[i].left.end), + str.slice(matchPos[i].right.start, matchPos[i].right.end) + ]); + } + return results; +}; + +/** + * + * @param {string} str + * @param {string|function} replacement + * @param {string} left + * @param {string} right + * @param {string} flags + * @returns {string} + */ +showdown.helper.replaceRecursiveRegExp = function (str, replacement, left, right, flags) { + 'use strict'; + + if (!showdown.helper.isFunction(replacement)) { + var repStr = replacement; + replacement = function () { + return repStr; + }; + } + + var matchPos = rgxFindMatchPos(str, left, right, flags), + finalStr = str, + lng = matchPos.length; + + if (lng > 0) { + var bits = []; + if (matchPos[0].wholeMatch.start !== 0) { + bits.push(str.slice(0, matchPos[0].wholeMatch.start)); + } + for (var i = 0; i < lng; ++i) { + bits.push( + replacement( + str.slice(matchPos[i].wholeMatch.start, matchPos[i].wholeMatch.end), + str.slice(matchPos[i].match.start, matchPos[i].match.end), + str.slice(matchPos[i].left.start, matchPos[i].left.end), + str.slice(matchPos[i].right.start, matchPos[i].right.end) + ) + ); + if (i < lng - 1) { + bits.push(str.slice(matchPos[i].wholeMatch.end, matchPos[i + 1].wholeMatch.start)); + } + } + if (matchPos[lng - 1].wholeMatch.end < str.length) { + bits.push(str.slice(matchPos[lng - 1].wholeMatch.end)); + } + finalStr = bits.join(''); + } + return finalStr; +}; + +/** + * Returns the index within the passed String object of the first occurrence of the specified regex, + * starting the search at fromIndex. Returns -1 if the value is not found. + * + * @param {string} str string to search + * @param {RegExp} regex Regular expression to search + * @param {int} [fromIndex = 0] Index to start the search + * @returns {Number} + * @throws InvalidArgumentError + */ +showdown.helper.regexIndexOf = function (str, regex, fromIndex) { + 'use strict'; + if (!showdown.helper.isString(str)) { + throw 'InvalidArgumentError: first parameter of showdown.helper.regexIndexOf function must be a string'; + } + if (regex instanceof RegExp === false) { + throw 'InvalidArgumentError: second parameter of showdown.helper.regexIndexOf function must be an instance of RegExp'; + } + var indexOf = str.substring(fromIndex || 0).search(regex); + return (indexOf >= 0) ? (indexOf + (fromIndex || 0)) : indexOf; +}; + +/** + * Splits the passed string object at the defined index, and returns an array composed of the two substrings + * @param {string} str string to split + * @param {int} index index to split string at + * @returns {[string,string]} + * @throws InvalidArgumentError + */ +showdown.helper.splitAtIndex = function (str, index) { + 'use strict'; + if (!showdown.helper.isString(str)) { + throw 'InvalidArgumentError: first parameter of showdown.helper.regexIndexOf function must be a string'; + } + return [str.substring(0, index), str.substring(index)]; +}; + +/** + * Obfuscate an e-mail address through the use of Character Entities, + * transforming ASCII characters into their equivalent decimal or hex entities. + * + * Since it has a random component, subsequent calls to this function produce different results + * + * @param {string} mail + * @returns {string} + */ +showdown.helper.encodeEmailAddress = function (mail) { + 'use strict'; + var encode = [ + function (ch) { + return '&#' + ch.charCodeAt(0) + ';'; + }, + function (ch) { + return '&#x' + ch.charCodeAt(0).toString(16) + ';'; + }, + function (ch) { + return ch; + } + ]; + + mail = mail.replace(/./g, function (ch) { + if (ch === '@') { + // this *must* be encoded. I insist. + ch = encode[Math.floor(Math.random() * 2)](ch); + } else { + var r = Math.random(); + // roughly 10% raw, 45% hex, 45% dec + ch = ( + r > 0.9 ? encode[2](ch) : r > 0.45 ? encode[1](ch) : encode[0](ch) + ); + } + return ch; + }); + + return mail; +}; + +/** + * + * @param str + * @param targetLength + * @param padString + * @returns {string} + */ +showdown.helper.padEnd = function padEnd (str, targetLength, padString) { + 'use strict'; + /*jshint bitwise: false*/ + // eslint-disable-next-line space-infix-ops + targetLength = targetLength>>0; //floor if number or convert non-number to 0; + /*jshint bitwise: true*/ + padString = String(padString || ' '); + if (str.length > targetLength) { + return String(str); + } else { + targetLength = targetLength - str.length; + if (targetLength > padString.length) { + padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed + } + return String(str) + padString.slice(0,targetLength); + } +}; + +/** + * POLYFILLS + */ +// use this instead of builtin is undefined for IE8 compatibility +if (typeof console === 'undefined') { + console = { + warn: function (msg) { + 'use strict'; + alert(msg); + }, + log: function (msg) { + 'use strict'; + alert(msg); + }, + error: function (msg) { + 'use strict'; + throw msg; + } + }; +} + +/** + * Common regexes. + * We declare some common regexes to improve performance + */ +showdown.helper.regexes = { + asteriskDashAndColon: /([*_:~])/g +}; + +/** + * EMOJIS LIST + */ +showdown.helper.emojis = { + '+1':'\ud83d\udc4d', + '-1':'\ud83d\udc4e', + '100':'\ud83d\udcaf', + '1234':'\ud83d\udd22', + '1st_place_medal':'\ud83e\udd47', + '2nd_place_medal':'\ud83e\udd48', + '3rd_place_medal':'\ud83e\udd49', + '8ball':'\ud83c\udfb1', + 'a':'\ud83c\udd70\ufe0f', + 'ab':'\ud83c\udd8e', + 'abc':'\ud83d\udd24', + 'abcd':'\ud83d\udd21', + 'accept':'\ud83c\ude51', + 'aerial_tramway':'\ud83d\udea1', + 'airplane':'\u2708\ufe0f', + 'alarm_clock':'\u23f0', + 'alembic':'\u2697\ufe0f', + 'alien':'\ud83d\udc7d', + 'ambulance':'\ud83d\ude91', + 'amphora':'\ud83c\udffa', + 'anchor':'\u2693\ufe0f', + 'angel':'\ud83d\udc7c', + 'anger':'\ud83d\udca2', + 'angry':'\ud83d\ude20', + 'anguished':'\ud83d\ude27', + 'ant':'\ud83d\udc1c', + 'apple':'\ud83c\udf4e', + 'aquarius':'\u2652\ufe0f', + 'aries':'\u2648\ufe0f', + 'arrow_backward':'\u25c0\ufe0f', + 'arrow_double_down':'\u23ec', + 'arrow_double_up':'\u23eb', + 'arrow_down':'\u2b07\ufe0f', + 'arrow_down_small':'\ud83d\udd3d', + 'arrow_forward':'\u25b6\ufe0f', + 'arrow_heading_down':'\u2935\ufe0f', + 'arrow_heading_up':'\u2934\ufe0f', + 'arrow_left':'\u2b05\ufe0f', + 'arrow_lower_left':'\u2199\ufe0f', + 'arrow_lower_right':'\u2198\ufe0f', + 'arrow_right':'\u27a1\ufe0f', + 'arrow_right_hook':'\u21aa\ufe0f', + 'arrow_up':'\u2b06\ufe0f', + 'arrow_up_down':'\u2195\ufe0f', + 'arrow_up_small':'\ud83d\udd3c', + 'arrow_upper_left':'\u2196\ufe0f', + 'arrow_upper_right':'\u2197\ufe0f', + 'arrows_clockwise':'\ud83d\udd03', + 'arrows_counterclockwise':'\ud83d\udd04', + 'art':'\ud83c\udfa8', + 'articulated_lorry':'\ud83d\ude9b', + 'artificial_satellite':'\ud83d\udef0', + 'astonished':'\ud83d\ude32', + 'athletic_shoe':'\ud83d\udc5f', + 'atm':'\ud83c\udfe7', + 'atom_symbol':'\u269b\ufe0f', + 'avocado':'\ud83e\udd51', + 'b':'\ud83c\udd71\ufe0f', + 'baby':'\ud83d\udc76', + 'baby_bottle':'\ud83c\udf7c', + 'baby_chick':'\ud83d\udc24', + 'baby_symbol':'\ud83d\udebc', + 'back':'\ud83d\udd19', + 'bacon':'\ud83e\udd53', + 'badminton':'\ud83c\udff8', + 'baggage_claim':'\ud83d\udec4', + 'baguette_bread':'\ud83e\udd56', + 'balance_scale':'\u2696\ufe0f', + 'balloon':'\ud83c\udf88', + 'ballot_box':'\ud83d\uddf3', + 'ballot_box_with_check':'\u2611\ufe0f', + 'bamboo':'\ud83c\udf8d', + 'banana':'\ud83c\udf4c', + 'bangbang':'\u203c\ufe0f', + 'bank':'\ud83c\udfe6', + 'bar_chart':'\ud83d\udcca', + 'barber':'\ud83d\udc88', + 'baseball':'\u26be\ufe0f', + 'basketball':'\ud83c\udfc0', + 'basketball_man':'\u26f9\ufe0f', + 'basketball_woman':'\u26f9\ufe0f‍\u2640\ufe0f', + 'bat':'\ud83e\udd87', + 'bath':'\ud83d\udec0', + 'bathtub':'\ud83d\udec1', + 'battery':'\ud83d\udd0b', + 'beach_umbrella':'\ud83c\udfd6', + 'bear':'\ud83d\udc3b', + 'bed':'\ud83d\udecf', + 'bee':'\ud83d\udc1d', + 'beer':'\ud83c\udf7a', + 'beers':'\ud83c\udf7b', + 'beetle':'\ud83d\udc1e', + 'beginner':'\ud83d\udd30', + 'bell':'\ud83d\udd14', + 'bellhop_bell':'\ud83d\udece', + 'bento':'\ud83c\udf71', + 'biking_man':'\ud83d\udeb4', + 'bike':'\ud83d\udeb2', + 'biking_woman':'\ud83d\udeb4‍\u2640\ufe0f', + 'bikini':'\ud83d\udc59', + 'biohazard':'\u2623\ufe0f', + 'bird':'\ud83d\udc26', + 'birthday':'\ud83c\udf82', + 'black_circle':'\u26ab\ufe0f', + 'black_flag':'\ud83c\udff4', + 'black_heart':'\ud83d\udda4', + 'black_joker':'\ud83c\udccf', + 'black_large_square':'\u2b1b\ufe0f', + 'black_medium_small_square':'\u25fe\ufe0f', + 'black_medium_square':'\u25fc\ufe0f', + 'black_nib':'\u2712\ufe0f', + 'black_small_square':'\u25aa\ufe0f', + 'black_square_button':'\ud83d\udd32', + 'blonde_man':'\ud83d\udc71', + 'blonde_woman':'\ud83d\udc71‍\u2640\ufe0f', + 'blossom':'\ud83c\udf3c', + 'blowfish':'\ud83d\udc21', + 'blue_book':'\ud83d\udcd8', + 'blue_car':'\ud83d\ude99', + 'blue_heart':'\ud83d\udc99', + 'blush':'\ud83d\ude0a', + 'boar':'\ud83d\udc17', + 'boat':'\u26f5\ufe0f', + 'bomb':'\ud83d\udca3', + 'book':'\ud83d\udcd6', + 'bookmark':'\ud83d\udd16', + 'bookmark_tabs':'\ud83d\udcd1', + 'books':'\ud83d\udcda', + 'boom':'\ud83d\udca5', + 'boot':'\ud83d\udc62', + 'bouquet':'\ud83d\udc90', + 'bowing_man':'\ud83d\ude47', + 'bow_and_arrow':'\ud83c\udff9', + 'bowing_woman':'\ud83d\ude47‍\u2640\ufe0f', + 'bowling':'\ud83c\udfb3', + 'boxing_glove':'\ud83e\udd4a', + 'boy':'\ud83d\udc66', + 'bread':'\ud83c\udf5e', + 'bride_with_veil':'\ud83d\udc70', + 'bridge_at_night':'\ud83c\udf09', + 'briefcase':'\ud83d\udcbc', + 'broken_heart':'\ud83d\udc94', + 'bug':'\ud83d\udc1b', + 'building_construction':'\ud83c\udfd7', + 'bulb':'\ud83d\udca1', + 'bullettrain_front':'\ud83d\ude85', + 'bullettrain_side':'\ud83d\ude84', + 'burrito':'\ud83c\udf2f', + 'bus':'\ud83d\ude8c', + 'business_suit_levitating':'\ud83d\udd74', + 'busstop':'\ud83d\ude8f', + 'bust_in_silhouette':'\ud83d\udc64', + 'busts_in_silhouette':'\ud83d\udc65', + 'butterfly':'\ud83e\udd8b', + 'cactus':'\ud83c\udf35', + 'cake':'\ud83c\udf70', + 'calendar':'\ud83d\udcc6', + 'call_me_hand':'\ud83e\udd19', + 'calling':'\ud83d\udcf2', + 'camel':'\ud83d\udc2b', + 'camera':'\ud83d\udcf7', + 'camera_flash':'\ud83d\udcf8', + 'camping':'\ud83c\udfd5', + 'cancer':'\u264b\ufe0f', + 'candle':'\ud83d\udd6f', + 'candy':'\ud83c\udf6c', + 'canoe':'\ud83d\udef6', + 'capital_abcd':'\ud83d\udd20', + 'capricorn':'\u2651\ufe0f', + 'car':'\ud83d\ude97', + 'card_file_box':'\ud83d\uddc3', + 'card_index':'\ud83d\udcc7', + 'card_index_dividers':'\ud83d\uddc2', + 'carousel_horse':'\ud83c\udfa0', + 'carrot':'\ud83e\udd55', + 'cat':'\ud83d\udc31', + 'cat2':'\ud83d\udc08', + 'cd':'\ud83d\udcbf', + 'chains':'\u26d3', + 'champagne':'\ud83c\udf7e', + 'chart':'\ud83d\udcb9', + 'chart_with_downwards_trend':'\ud83d\udcc9', + 'chart_with_upwards_trend':'\ud83d\udcc8', + 'checkered_flag':'\ud83c\udfc1', + 'cheese':'\ud83e\uddc0', + 'cherries':'\ud83c\udf52', + 'cherry_blossom':'\ud83c\udf38', + 'chestnut':'\ud83c\udf30', + 'chicken':'\ud83d\udc14', + 'children_crossing':'\ud83d\udeb8', + 'chipmunk':'\ud83d\udc3f', + 'chocolate_bar':'\ud83c\udf6b', + 'christmas_tree':'\ud83c\udf84', + 'church':'\u26ea\ufe0f', + 'cinema':'\ud83c\udfa6', + 'circus_tent':'\ud83c\udfaa', + 'city_sunrise':'\ud83c\udf07', + 'city_sunset':'\ud83c\udf06', + 'cityscape':'\ud83c\udfd9', + 'cl':'\ud83c\udd91', + 'clamp':'\ud83d\udddc', + 'clap':'\ud83d\udc4f', + 'clapper':'\ud83c\udfac', + 'classical_building':'\ud83c\udfdb', + 'clinking_glasses':'\ud83e\udd42', + 'clipboard':'\ud83d\udccb', + 'clock1':'\ud83d\udd50', + 'clock10':'\ud83d\udd59', + 'clock1030':'\ud83d\udd65', + 'clock11':'\ud83d\udd5a', + 'clock1130':'\ud83d\udd66', + 'clock12':'\ud83d\udd5b', + 'clock1230':'\ud83d\udd67', + 'clock130':'\ud83d\udd5c', + 'clock2':'\ud83d\udd51', + 'clock230':'\ud83d\udd5d', + 'clock3':'\ud83d\udd52', + 'clock330':'\ud83d\udd5e', + 'clock4':'\ud83d\udd53', + 'clock430':'\ud83d\udd5f', + 'clock5':'\ud83d\udd54', + 'clock530':'\ud83d\udd60', + 'clock6':'\ud83d\udd55', + 'clock630':'\ud83d\udd61', + 'clock7':'\ud83d\udd56', + 'clock730':'\ud83d\udd62', + 'clock8':'\ud83d\udd57', + 'clock830':'\ud83d\udd63', + 'clock9':'\ud83d\udd58', + 'clock930':'\ud83d\udd64', + 'closed_book':'\ud83d\udcd5', + 'closed_lock_with_key':'\ud83d\udd10', + 'closed_umbrella':'\ud83c\udf02', + 'cloud':'\u2601\ufe0f', + 'cloud_with_lightning':'\ud83c\udf29', + 'cloud_with_lightning_and_rain':'\u26c8', + 'cloud_with_rain':'\ud83c\udf27', + 'cloud_with_snow':'\ud83c\udf28', + 'clown_face':'\ud83e\udd21', + 'clubs':'\u2663\ufe0f', + 'cocktail':'\ud83c\udf78', + 'coffee':'\u2615\ufe0f', + 'coffin':'\u26b0\ufe0f', + 'cold_sweat':'\ud83d\ude30', + 'comet':'\u2604\ufe0f', + 'computer':'\ud83d\udcbb', + 'computer_mouse':'\ud83d\uddb1', + 'confetti_ball':'\ud83c\udf8a', + 'confounded':'\ud83d\ude16', + 'confused':'\ud83d\ude15', + 'congratulations':'\u3297\ufe0f', + 'construction':'\ud83d\udea7', + 'construction_worker_man':'\ud83d\udc77', + 'construction_worker_woman':'\ud83d\udc77‍\u2640\ufe0f', + 'control_knobs':'\ud83c\udf9b', + 'convenience_store':'\ud83c\udfea', + 'cookie':'\ud83c\udf6a', + 'cool':'\ud83c\udd92', + 'policeman':'\ud83d\udc6e', + 'copyright':'\u00a9\ufe0f', + 'corn':'\ud83c\udf3d', + 'couch_and_lamp':'\ud83d\udecb', + 'couple':'\ud83d\udc6b', + 'couple_with_heart_woman_man':'\ud83d\udc91', + 'couple_with_heart_man_man':'\ud83d\udc68‍\u2764\ufe0f‍\ud83d\udc68', + 'couple_with_heart_woman_woman':'\ud83d\udc69‍\u2764\ufe0f‍\ud83d\udc69', + 'couplekiss_man_man':'\ud83d\udc68‍\u2764\ufe0f‍\ud83d\udc8b‍\ud83d\udc68', + 'couplekiss_man_woman':'\ud83d\udc8f', + 'couplekiss_woman_woman':'\ud83d\udc69‍\u2764\ufe0f‍\ud83d\udc8b‍\ud83d\udc69', + 'cow':'\ud83d\udc2e', + 'cow2':'\ud83d\udc04', + 'cowboy_hat_face':'\ud83e\udd20', + 'crab':'\ud83e\udd80', + 'crayon':'\ud83d\udd8d', + 'credit_card':'\ud83d\udcb3', + 'crescent_moon':'\ud83c\udf19', + 'cricket':'\ud83c\udfcf', + 'crocodile':'\ud83d\udc0a', + 'croissant':'\ud83e\udd50', + 'crossed_fingers':'\ud83e\udd1e', + 'crossed_flags':'\ud83c\udf8c', + 'crossed_swords':'\u2694\ufe0f', + 'crown':'\ud83d\udc51', + 'cry':'\ud83d\ude22', + 'crying_cat_face':'\ud83d\ude3f', + 'crystal_ball':'\ud83d\udd2e', + 'cucumber':'\ud83e\udd52', + 'cupid':'\ud83d\udc98', + 'curly_loop':'\u27b0', + 'currency_exchange':'\ud83d\udcb1', + 'curry':'\ud83c\udf5b', + 'custard':'\ud83c\udf6e', + 'customs':'\ud83d\udec3', + 'cyclone':'\ud83c\udf00', + 'dagger':'\ud83d\udde1', + 'dancer':'\ud83d\udc83', + 'dancing_women':'\ud83d\udc6f', + 'dancing_men':'\ud83d\udc6f‍\u2642\ufe0f', + 'dango':'\ud83c\udf61', + 'dark_sunglasses':'\ud83d\udd76', + 'dart':'\ud83c\udfaf', + 'dash':'\ud83d\udca8', + 'date':'\ud83d\udcc5', + 'deciduous_tree':'\ud83c\udf33', + 'deer':'\ud83e\udd8c', + 'department_store':'\ud83c\udfec', + 'derelict_house':'\ud83c\udfda', + 'desert':'\ud83c\udfdc', + 'desert_island':'\ud83c\udfdd', + 'desktop_computer':'\ud83d\udda5', + 'male_detective':'\ud83d\udd75\ufe0f', + 'diamond_shape_with_a_dot_inside':'\ud83d\udca0', + 'diamonds':'\u2666\ufe0f', + 'disappointed':'\ud83d\ude1e', + 'disappointed_relieved':'\ud83d\ude25', + 'dizzy':'\ud83d\udcab', + 'dizzy_face':'\ud83d\ude35', + 'do_not_litter':'\ud83d\udeaf', + 'dog':'\ud83d\udc36', + 'dog2':'\ud83d\udc15', + 'dollar':'\ud83d\udcb5', + 'dolls':'\ud83c\udf8e', + 'dolphin':'\ud83d\udc2c', + 'door':'\ud83d\udeaa', + 'doughnut':'\ud83c\udf69', + 'dove':'\ud83d\udd4a', + 'dragon':'\ud83d\udc09', + 'dragon_face':'\ud83d\udc32', + 'dress':'\ud83d\udc57', + 'dromedary_camel':'\ud83d\udc2a', + 'drooling_face':'\ud83e\udd24', + 'droplet':'\ud83d\udca7', + 'drum':'\ud83e\udd41', + 'duck':'\ud83e\udd86', + 'dvd':'\ud83d\udcc0', + 'e-mail':'\ud83d\udce7', + 'eagle':'\ud83e\udd85', + 'ear':'\ud83d\udc42', + 'ear_of_rice':'\ud83c\udf3e', + 'earth_africa':'\ud83c\udf0d', + 'earth_americas':'\ud83c\udf0e', + 'earth_asia':'\ud83c\udf0f', + 'egg':'\ud83e\udd5a', + 'eggplant':'\ud83c\udf46', + 'eight_pointed_black_star':'\u2734\ufe0f', + 'eight_spoked_asterisk':'\u2733\ufe0f', + 'electric_plug':'\ud83d\udd0c', + 'elephant':'\ud83d\udc18', + 'email':'\u2709\ufe0f', + 'end':'\ud83d\udd1a', + 'envelope_with_arrow':'\ud83d\udce9', + 'euro':'\ud83d\udcb6', + 'european_castle':'\ud83c\udff0', + 'european_post_office':'\ud83c\udfe4', + 'evergreen_tree':'\ud83c\udf32', + 'exclamation':'\u2757\ufe0f', + 'expressionless':'\ud83d\ude11', + 'eye':'\ud83d\udc41', + 'eye_speech_bubble':'\ud83d\udc41‍\ud83d\udde8', + 'eyeglasses':'\ud83d\udc53', + 'eyes':'\ud83d\udc40', + 'face_with_head_bandage':'\ud83e\udd15', + 'face_with_thermometer':'\ud83e\udd12', + 'fist_oncoming':'\ud83d\udc4a', + 'factory':'\ud83c\udfed', + 'fallen_leaf':'\ud83c\udf42', + 'family_man_woman_boy':'\ud83d\udc6a', + 'family_man_boy':'\ud83d\udc68‍\ud83d\udc66', + 'family_man_boy_boy':'\ud83d\udc68‍\ud83d\udc66‍\ud83d\udc66', + 'family_man_girl':'\ud83d\udc68‍\ud83d\udc67', + 'family_man_girl_boy':'\ud83d\udc68‍\ud83d\udc67‍\ud83d\udc66', + 'family_man_girl_girl':'\ud83d\udc68‍\ud83d\udc67‍\ud83d\udc67', + 'family_man_man_boy':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc66', + 'family_man_man_boy_boy':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc66‍\ud83d\udc66', + 'family_man_man_girl':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc67', + 'family_man_man_girl_boy':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc67‍\ud83d\udc66', + 'family_man_man_girl_girl':'\ud83d\udc68‍\ud83d\udc68‍\ud83d\udc67‍\ud83d\udc67', + 'family_man_woman_boy_boy':'\ud83d\udc68‍\ud83d\udc69‍\ud83d\udc66‍\ud83d\udc66', + 'family_man_woman_girl':'\ud83d\udc68‍\ud83d\udc69‍\ud83d\udc67', + 'family_man_woman_girl_boy':'\ud83d\udc68‍\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc66', + 'family_man_woman_girl_girl':'\ud83d\udc68‍\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc67', + 'family_woman_boy':'\ud83d\udc69‍\ud83d\udc66', + 'family_woman_boy_boy':'\ud83d\udc69‍\ud83d\udc66‍\ud83d\udc66', + 'family_woman_girl':'\ud83d\udc69‍\ud83d\udc67', + 'family_woman_girl_boy':'\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc66', + 'family_woman_girl_girl':'\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc67', + 'family_woman_woman_boy':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc66', + 'family_woman_woman_boy_boy':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc66‍\ud83d\udc66', + 'family_woman_woman_girl':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc67', + 'family_woman_woman_girl_boy':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc66', + 'family_woman_woman_girl_girl':'\ud83d\udc69‍\ud83d\udc69‍\ud83d\udc67‍\ud83d\udc67', + 'fast_forward':'\u23e9', + 'fax':'\ud83d\udce0', + 'fearful':'\ud83d\ude28', + 'feet':'\ud83d\udc3e', + 'female_detective':'\ud83d\udd75\ufe0f‍\u2640\ufe0f', + 'ferris_wheel':'\ud83c\udfa1', + 'ferry':'\u26f4', + 'field_hockey':'\ud83c\udfd1', + 'file_cabinet':'\ud83d\uddc4', + 'file_folder':'\ud83d\udcc1', + 'film_projector':'\ud83d\udcfd', + 'film_strip':'\ud83c\udf9e', + 'fire':'\ud83d\udd25', + 'fire_engine':'\ud83d\ude92', + 'fireworks':'\ud83c\udf86', + 'first_quarter_moon':'\ud83c\udf13', + 'first_quarter_moon_with_face':'\ud83c\udf1b', + 'fish':'\ud83d\udc1f', + 'fish_cake':'\ud83c\udf65', + 'fishing_pole_and_fish':'\ud83c\udfa3', + 'fist_raised':'\u270a', + 'fist_left':'\ud83e\udd1b', + 'fist_right':'\ud83e\udd1c', + 'flags':'\ud83c\udf8f', + 'flashlight':'\ud83d\udd26', + 'fleur_de_lis':'\u269c\ufe0f', + 'flight_arrival':'\ud83d\udeec', + 'flight_departure':'\ud83d\udeeb', + 'floppy_disk':'\ud83d\udcbe', + 'flower_playing_cards':'\ud83c\udfb4', + 'flushed':'\ud83d\ude33', + 'fog':'\ud83c\udf2b', + 'foggy':'\ud83c\udf01', + 'football':'\ud83c\udfc8', + 'footprints':'\ud83d\udc63', + 'fork_and_knife':'\ud83c\udf74', + 'fountain':'\u26f2\ufe0f', + 'fountain_pen':'\ud83d\udd8b', + 'four_leaf_clover':'\ud83c\udf40', + 'fox_face':'\ud83e\udd8a', + 'framed_picture':'\ud83d\uddbc', + 'free':'\ud83c\udd93', + 'fried_egg':'\ud83c\udf73', + 'fried_shrimp':'\ud83c\udf64', + 'fries':'\ud83c\udf5f', + 'frog':'\ud83d\udc38', + 'frowning':'\ud83d\ude26', + 'frowning_face':'\u2639\ufe0f', + 'frowning_man':'\ud83d\ude4d‍\u2642\ufe0f', + 'frowning_woman':'\ud83d\ude4d', + 'middle_finger':'\ud83d\udd95', + 'fuelpump':'\u26fd\ufe0f', + 'full_moon':'\ud83c\udf15', + 'full_moon_with_face':'\ud83c\udf1d', + 'funeral_urn':'\u26b1\ufe0f', + 'game_die':'\ud83c\udfb2', + 'gear':'\u2699\ufe0f', + 'gem':'\ud83d\udc8e', + 'gemini':'\u264a\ufe0f', + 'ghost':'\ud83d\udc7b', + 'gift':'\ud83c\udf81', + 'gift_heart':'\ud83d\udc9d', + 'girl':'\ud83d\udc67', + 'globe_with_meridians':'\ud83c\udf10', + 'goal_net':'\ud83e\udd45', + 'goat':'\ud83d\udc10', + 'golf':'\u26f3\ufe0f', + 'golfing_man':'\ud83c\udfcc\ufe0f', + 'golfing_woman':'\ud83c\udfcc\ufe0f‍\u2640\ufe0f', + 'gorilla':'\ud83e\udd8d', + 'grapes':'\ud83c\udf47', + 'green_apple':'\ud83c\udf4f', + 'green_book':'\ud83d\udcd7', + 'green_heart':'\ud83d\udc9a', + 'green_salad':'\ud83e\udd57', + 'grey_exclamation':'\u2755', + 'grey_question':'\u2754', + 'grimacing':'\ud83d\ude2c', + 'grin':'\ud83d\ude01', + 'grinning':'\ud83d\ude00', + 'guardsman':'\ud83d\udc82', + 'guardswoman':'\ud83d\udc82‍\u2640\ufe0f', + 'guitar':'\ud83c\udfb8', + 'gun':'\ud83d\udd2b', + 'haircut_woman':'\ud83d\udc87', + 'haircut_man':'\ud83d\udc87‍\u2642\ufe0f', + 'hamburger':'\ud83c\udf54', + 'hammer':'\ud83d\udd28', + 'hammer_and_pick':'\u2692', + 'hammer_and_wrench':'\ud83d\udee0', + 'hamster':'\ud83d\udc39', + 'hand':'\u270b', + 'handbag':'\ud83d\udc5c', + 'handshake':'\ud83e\udd1d', + 'hankey':'\ud83d\udca9', + 'hatched_chick':'\ud83d\udc25', + 'hatching_chick':'\ud83d\udc23', + 'headphones':'\ud83c\udfa7', + 'hear_no_evil':'\ud83d\ude49', + 'heart':'\u2764\ufe0f', + 'heart_decoration':'\ud83d\udc9f', + 'heart_eyes':'\ud83d\ude0d', + 'heart_eyes_cat':'\ud83d\ude3b', + 'heartbeat':'\ud83d\udc93', + 'heartpulse':'\ud83d\udc97', + 'hearts':'\u2665\ufe0f', + 'heavy_check_mark':'\u2714\ufe0f', + 'heavy_division_sign':'\u2797', + 'heavy_dollar_sign':'\ud83d\udcb2', + 'heavy_heart_exclamation':'\u2763\ufe0f', + 'heavy_minus_sign':'\u2796', + 'heavy_multiplication_x':'\u2716\ufe0f', + 'heavy_plus_sign':'\u2795', + 'helicopter':'\ud83d\ude81', + 'herb':'\ud83c\udf3f', + 'hibiscus':'\ud83c\udf3a', + 'high_brightness':'\ud83d\udd06', + 'high_heel':'\ud83d\udc60', + 'hocho':'\ud83d\udd2a', + 'hole':'\ud83d\udd73', + 'honey_pot':'\ud83c\udf6f', + 'horse':'\ud83d\udc34', + 'horse_racing':'\ud83c\udfc7', + 'hospital':'\ud83c\udfe5', + 'hot_pepper':'\ud83c\udf36', + 'hotdog':'\ud83c\udf2d', + 'hotel':'\ud83c\udfe8', + 'hotsprings':'\u2668\ufe0f', + 'hourglass':'\u231b\ufe0f', + 'hourglass_flowing_sand':'\u23f3', + 'house':'\ud83c\udfe0', + 'house_with_garden':'\ud83c\udfe1', + 'houses':'\ud83c\udfd8', + 'hugs':'\ud83e\udd17', + 'hushed':'\ud83d\ude2f', + 'ice_cream':'\ud83c\udf68', + 'ice_hockey':'\ud83c\udfd2', + 'ice_skate':'\u26f8', + 'icecream':'\ud83c\udf66', + 'id':'\ud83c\udd94', + 'ideograph_advantage':'\ud83c\ude50', + 'imp':'\ud83d\udc7f', + 'inbox_tray':'\ud83d\udce5', + 'incoming_envelope':'\ud83d\udce8', + 'tipping_hand_woman':'\ud83d\udc81', + 'information_source':'\u2139\ufe0f', + 'innocent':'\ud83d\ude07', + 'interrobang':'\u2049\ufe0f', + 'iphone':'\ud83d\udcf1', + 'izakaya_lantern':'\ud83c\udfee', + 'jack_o_lantern':'\ud83c\udf83', + 'japan':'\ud83d\uddfe', + 'japanese_castle':'\ud83c\udfef', + 'japanese_goblin':'\ud83d\udc7a', + 'japanese_ogre':'\ud83d\udc79', + 'jeans':'\ud83d\udc56', + 'joy':'\ud83d\ude02', + 'joy_cat':'\ud83d\ude39', + 'joystick':'\ud83d\udd79', + 'kaaba':'\ud83d\udd4b', + 'key':'\ud83d\udd11', + 'keyboard':'\u2328\ufe0f', + 'keycap_ten':'\ud83d\udd1f', + 'kick_scooter':'\ud83d\udef4', + 'kimono':'\ud83d\udc58', + 'kiss':'\ud83d\udc8b', + 'kissing':'\ud83d\ude17', + 'kissing_cat':'\ud83d\ude3d', + 'kissing_closed_eyes':'\ud83d\ude1a', + 'kissing_heart':'\ud83d\ude18', + 'kissing_smiling_eyes':'\ud83d\ude19', + 'kiwi_fruit':'\ud83e\udd5d', + 'koala':'\ud83d\udc28', + 'koko':'\ud83c\ude01', + 'label':'\ud83c\udff7', + 'large_blue_circle':'\ud83d\udd35', + 'large_blue_diamond':'\ud83d\udd37', + 'large_orange_diamond':'\ud83d\udd36', + 'last_quarter_moon':'\ud83c\udf17', + 'last_quarter_moon_with_face':'\ud83c\udf1c', + 'latin_cross':'\u271d\ufe0f', + 'laughing':'\ud83d\ude06', + 'leaves':'\ud83c\udf43', + 'ledger':'\ud83d\udcd2', + 'left_luggage':'\ud83d\udec5', + 'left_right_arrow':'\u2194\ufe0f', + 'leftwards_arrow_with_hook':'\u21a9\ufe0f', + 'lemon':'\ud83c\udf4b', + 'leo':'\u264c\ufe0f', + 'leopard':'\ud83d\udc06', + 'level_slider':'\ud83c\udf9a', + 'libra':'\u264e\ufe0f', + 'light_rail':'\ud83d\ude88', + 'link':'\ud83d\udd17', + 'lion':'\ud83e\udd81', + 'lips':'\ud83d\udc44', + 'lipstick':'\ud83d\udc84', + 'lizard':'\ud83e\udd8e', + 'lock':'\ud83d\udd12', + 'lock_with_ink_pen':'\ud83d\udd0f', + 'lollipop':'\ud83c\udf6d', + 'loop':'\u27bf', + 'loud_sound':'\ud83d\udd0a', + 'loudspeaker':'\ud83d\udce2', + 'love_hotel':'\ud83c\udfe9', + 'love_letter':'\ud83d\udc8c', + 'low_brightness':'\ud83d\udd05', + 'lying_face':'\ud83e\udd25', + 'm':'\u24c2\ufe0f', + 'mag':'\ud83d\udd0d', + 'mag_right':'\ud83d\udd0e', + 'mahjong':'\ud83c\udc04\ufe0f', + 'mailbox':'\ud83d\udceb', + 'mailbox_closed':'\ud83d\udcea', + 'mailbox_with_mail':'\ud83d\udcec', + 'mailbox_with_no_mail':'\ud83d\udced', + 'man':'\ud83d\udc68', + 'man_artist':'\ud83d\udc68‍\ud83c\udfa8', + 'man_astronaut':'\ud83d\udc68‍\ud83d\ude80', + 'man_cartwheeling':'\ud83e\udd38‍\u2642\ufe0f', + 'man_cook':'\ud83d\udc68‍\ud83c\udf73', + 'man_dancing':'\ud83d\udd7a', + 'man_facepalming':'\ud83e\udd26‍\u2642\ufe0f', + 'man_factory_worker':'\ud83d\udc68‍\ud83c\udfed', + 'man_farmer':'\ud83d\udc68‍\ud83c\udf3e', + 'man_firefighter':'\ud83d\udc68‍\ud83d\ude92', + 'man_health_worker':'\ud83d\udc68‍\u2695\ufe0f', + 'man_in_tuxedo':'\ud83e\udd35', + 'man_judge':'\ud83d\udc68‍\u2696\ufe0f', + 'man_juggling':'\ud83e\udd39‍\u2642\ufe0f', + 'man_mechanic':'\ud83d\udc68‍\ud83d\udd27', + 'man_office_worker':'\ud83d\udc68‍\ud83d\udcbc', + 'man_pilot':'\ud83d\udc68‍\u2708\ufe0f', + 'man_playing_handball':'\ud83e\udd3e‍\u2642\ufe0f', + 'man_playing_water_polo':'\ud83e\udd3d‍\u2642\ufe0f', + 'man_scientist':'\ud83d\udc68‍\ud83d\udd2c', + 'man_shrugging':'\ud83e\udd37‍\u2642\ufe0f', + 'man_singer':'\ud83d\udc68‍\ud83c\udfa4', + 'man_student':'\ud83d\udc68‍\ud83c\udf93', + 'man_teacher':'\ud83d\udc68‍\ud83c\udfeb', + 'man_technologist':'\ud83d\udc68‍\ud83d\udcbb', + 'man_with_gua_pi_mao':'\ud83d\udc72', + 'man_with_turban':'\ud83d\udc73', + 'tangerine':'\ud83c\udf4a', + 'mans_shoe':'\ud83d\udc5e', + 'mantelpiece_clock':'\ud83d\udd70', + 'maple_leaf':'\ud83c\udf41', + 'martial_arts_uniform':'\ud83e\udd4b', + 'mask':'\ud83d\ude37', + 'massage_woman':'\ud83d\udc86', + 'massage_man':'\ud83d\udc86‍\u2642\ufe0f', + 'meat_on_bone':'\ud83c\udf56', + 'medal_military':'\ud83c\udf96', + 'medal_sports':'\ud83c\udfc5', + 'mega':'\ud83d\udce3', + 'melon':'\ud83c\udf48', + 'memo':'\ud83d\udcdd', + 'men_wrestling':'\ud83e\udd3c‍\u2642\ufe0f', + 'menorah':'\ud83d\udd4e', + 'mens':'\ud83d\udeb9', + 'metal':'\ud83e\udd18', + 'metro':'\ud83d\ude87', + 'microphone':'\ud83c\udfa4', + 'microscope':'\ud83d\udd2c', + 'milk_glass':'\ud83e\udd5b', + 'milky_way':'\ud83c\udf0c', + 'minibus':'\ud83d\ude90', + 'minidisc':'\ud83d\udcbd', + 'mobile_phone_off':'\ud83d\udcf4', + 'money_mouth_face':'\ud83e\udd11', + 'money_with_wings':'\ud83d\udcb8', + 'moneybag':'\ud83d\udcb0', + 'monkey':'\ud83d\udc12', + 'monkey_face':'\ud83d\udc35', + 'monorail':'\ud83d\ude9d', + 'moon':'\ud83c\udf14', + 'mortar_board':'\ud83c\udf93', + 'mosque':'\ud83d\udd4c', + 'motor_boat':'\ud83d\udee5', + 'motor_scooter':'\ud83d\udef5', + 'motorcycle':'\ud83c\udfcd', + 'motorway':'\ud83d\udee3', + 'mount_fuji':'\ud83d\uddfb', + 'mountain':'\u26f0', + 'mountain_biking_man':'\ud83d\udeb5', + 'mountain_biking_woman':'\ud83d\udeb5‍\u2640\ufe0f', + 'mountain_cableway':'\ud83d\udea0', + 'mountain_railway':'\ud83d\ude9e', + 'mountain_snow':'\ud83c\udfd4', + 'mouse':'\ud83d\udc2d', + 'mouse2':'\ud83d\udc01', + 'movie_camera':'\ud83c\udfa5', + 'moyai':'\ud83d\uddff', + 'mrs_claus':'\ud83e\udd36', + 'muscle':'\ud83d\udcaa', + 'mushroom':'\ud83c\udf44', + 'musical_keyboard':'\ud83c\udfb9', + 'musical_note':'\ud83c\udfb5', + 'musical_score':'\ud83c\udfbc', + 'mute':'\ud83d\udd07', + 'nail_care':'\ud83d\udc85', + 'name_badge':'\ud83d\udcdb', + 'national_park':'\ud83c\udfde', + 'nauseated_face':'\ud83e\udd22', + 'necktie':'\ud83d\udc54', + 'negative_squared_cross_mark':'\u274e', + 'nerd_face':'\ud83e\udd13', + 'neutral_face':'\ud83d\ude10', + 'new':'\ud83c\udd95', + 'new_moon':'\ud83c\udf11', + 'new_moon_with_face':'\ud83c\udf1a', + 'newspaper':'\ud83d\udcf0', + 'newspaper_roll':'\ud83d\uddde', + 'next_track_button':'\u23ed', + 'ng':'\ud83c\udd96', + 'no_good_man':'\ud83d\ude45‍\u2642\ufe0f', + 'no_good_woman':'\ud83d\ude45', + 'night_with_stars':'\ud83c\udf03', + 'no_bell':'\ud83d\udd15', + 'no_bicycles':'\ud83d\udeb3', + 'no_entry':'\u26d4\ufe0f', + 'no_entry_sign':'\ud83d\udeab', + 'no_mobile_phones':'\ud83d\udcf5', + 'no_mouth':'\ud83d\ude36', + 'no_pedestrians':'\ud83d\udeb7', + 'no_smoking':'\ud83d\udead', + 'non-potable_water':'\ud83d\udeb1', + 'nose':'\ud83d\udc43', + 'notebook':'\ud83d\udcd3', + 'notebook_with_decorative_cover':'\ud83d\udcd4', + 'notes':'\ud83c\udfb6', + 'nut_and_bolt':'\ud83d\udd29', + 'o':'\u2b55\ufe0f', + 'o2':'\ud83c\udd7e\ufe0f', + 'ocean':'\ud83c\udf0a', + 'octopus':'\ud83d\udc19', + 'oden':'\ud83c\udf62', + 'office':'\ud83c\udfe2', + 'oil_drum':'\ud83d\udee2', + 'ok':'\ud83c\udd97', + 'ok_hand':'\ud83d\udc4c', + 'ok_man':'\ud83d\ude46‍\u2642\ufe0f', + 'ok_woman':'\ud83d\ude46', + 'old_key':'\ud83d\udddd', + 'older_man':'\ud83d\udc74', + 'older_woman':'\ud83d\udc75', + 'om':'\ud83d\udd49', + 'on':'\ud83d\udd1b', + 'oncoming_automobile':'\ud83d\ude98', + 'oncoming_bus':'\ud83d\ude8d', + 'oncoming_police_car':'\ud83d\ude94', + 'oncoming_taxi':'\ud83d\ude96', + 'open_file_folder':'\ud83d\udcc2', + 'open_hands':'\ud83d\udc50', + 'open_mouth':'\ud83d\ude2e', + 'open_umbrella':'\u2602\ufe0f', + 'ophiuchus':'\u26ce', + 'orange_book':'\ud83d\udcd9', + 'orthodox_cross':'\u2626\ufe0f', + 'outbox_tray':'\ud83d\udce4', + 'owl':'\ud83e\udd89', + 'ox':'\ud83d\udc02', + 'package':'\ud83d\udce6', + 'page_facing_up':'\ud83d\udcc4', + 'page_with_curl':'\ud83d\udcc3', + 'pager':'\ud83d\udcdf', + 'paintbrush':'\ud83d\udd8c', + 'palm_tree':'\ud83c\udf34', + 'pancakes':'\ud83e\udd5e', + 'panda_face':'\ud83d\udc3c', + 'paperclip':'\ud83d\udcce', + 'paperclips':'\ud83d\udd87', + 'parasol_on_ground':'\u26f1', + 'parking':'\ud83c\udd7f\ufe0f', + 'part_alternation_mark':'\u303d\ufe0f', + 'partly_sunny':'\u26c5\ufe0f', + 'passenger_ship':'\ud83d\udef3', + 'passport_control':'\ud83d\udec2', + 'pause_button':'\u23f8', + 'peace_symbol':'\u262e\ufe0f', + 'peach':'\ud83c\udf51', + 'peanuts':'\ud83e\udd5c', + 'pear':'\ud83c\udf50', + 'pen':'\ud83d\udd8a', + 'pencil2':'\u270f\ufe0f', + 'penguin':'\ud83d\udc27', + 'pensive':'\ud83d\ude14', + 'performing_arts':'\ud83c\udfad', + 'persevere':'\ud83d\ude23', + 'person_fencing':'\ud83e\udd3a', + 'pouting_woman':'\ud83d\ude4e', + 'phone':'\u260e\ufe0f', + 'pick':'\u26cf', + 'pig':'\ud83d\udc37', + 'pig2':'\ud83d\udc16', + 'pig_nose':'\ud83d\udc3d', + 'pill':'\ud83d\udc8a', + 'pineapple':'\ud83c\udf4d', + 'ping_pong':'\ud83c\udfd3', + 'pisces':'\u2653\ufe0f', + 'pizza':'\ud83c\udf55', + 'place_of_worship':'\ud83d\uded0', + 'plate_with_cutlery':'\ud83c\udf7d', + 'play_or_pause_button':'\u23ef', + 'point_down':'\ud83d\udc47', + 'point_left':'\ud83d\udc48', + 'point_right':'\ud83d\udc49', + 'point_up':'\u261d\ufe0f', + 'point_up_2':'\ud83d\udc46', + 'police_car':'\ud83d\ude93', + 'policewoman':'\ud83d\udc6e‍\u2640\ufe0f', + 'poodle':'\ud83d\udc29', + 'popcorn':'\ud83c\udf7f', + 'post_office':'\ud83c\udfe3', + 'postal_horn':'\ud83d\udcef', + 'postbox':'\ud83d\udcee', + 'potable_water':'\ud83d\udeb0', + 'potato':'\ud83e\udd54', + 'pouch':'\ud83d\udc5d', + 'poultry_leg':'\ud83c\udf57', + 'pound':'\ud83d\udcb7', + 'rage':'\ud83d\ude21', + 'pouting_cat':'\ud83d\ude3e', + 'pouting_man':'\ud83d\ude4e‍\u2642\ufe0f', + 'pray':'\ud83d\ude4f', + 'prayer_beads':'\ud83d\udcff', + 'pregnant_woman':'\ud83e\udd30', + 'previous_track_button':'\u23ee', + 'prince':'\ud83e\udd34', + 'princess':'\ud83d\udc78', + 'printer':'\ud83d\udda8', + 'purple_heart':'\ud83d\udc9c', + 'purse':'\ud83d\udc5b', + 'pushpin':'\ud83d\udccc', + 'put_litter_in_its_place':'\ud83d\udeae', + 'question':'\u2753', + 'rabbit':'\ud83d\udc30', + 'rabbit2':'\ud83d\udc07', + 'racehorse':'\ud83d\udc0e', + 'racing_car':'\ud83c\udfce', + 'radio':'\ud83d\udcfb', + 'radio_button':'\ud83d\udd18', + 'radioactive':'\u2622\ufe0f', + 'railway_car':'\ud83d\ude83', + 'railway_track':'\ud83d\udee4', + 'rainbow':'\ud83c\udf08', + 'rainbow_flag':'\ud83c\udff3\ufe0f‍\ud83c\udf08', + 'raised_back_of_hand':'\ud83e\udd1a', + 'raised_hand_with_fingers_splayed':'\ud83d\udd90', + 'raised_hands':'\ud83d\ude4c', + 'raising_hand_woman':'\ud83d\ude4b', + 'raising_hand_man':'\ud83d\ude4b‍\u2642\ufe0f', + 'ram':'\ud83d\udc0f', + 'ramen':'\ud83c\udf5c', + 'rat':'\ud83d\udc00', + 'record_button':'\u23fa', + 'recycle':'\u267b\ufe0f', + 'red_circle':'\ud83d\udd34', + 'registered':'\u00ae\ufe0f', + 'relaxed':'\u263a\ufe0f', + 'relieved':'\ud83d\ude0c', + 'reminder_ribbon':'\ud83c\udf97', + 'repeat':'\ud83d\udd01', + 'repeat_one':'\ud83d\udd02', + 'rescue_worker_helmet':'\u26d1', + 'restroom':'\ud83d\udebb', + 'revolving_hearts':'\ud83d\udc9e', + 'rewind':'\u23ea', + 'rhinoceros':'\ud83e\udd8f', + 'ribbon':'\ud83c\udf80', + 'rice':'\ud83c\udf5a', + 'rice_ball':'\ud83c\udf59', + 'rice_cracker':'\ud83c\udf58', + 'rice_scene':'\ud83c\udf91', + 'right_anger_bubble':'\ud83d\uddef', + 'ring':'\ud83d\udc8d', + 'robot':'\ud83e\udd16', + 'rocket':'\ud83d\ude80', + 'rofl':'\ud83e\udd23', + 'roll_eyes':'\ud83d\ude44', + 'roller_coaster':'\ud83c\udfa2', + 'rooster':'\ud83d\udc13', + 'rose':'\ud83c\udf39', + 'rosette':'\ud83c\udff5', + 'rotating_light':'\ud83d\udea8', + 'round_pushpin':'\ud83d\udccd', + 'rowing_man':'\ud83d\udea3', + 'rowing_woman':'\ud83d\udea3‍\u2640\ufe0f', + 'rugby_football':'\ud83c\udfc9', + 'running_man':'\ud83c\udfc3', + 'running_shirt_with_sash':'\ud83c\udfbd', + 'running_woman':'\ud83c\udfc3‍\u2640\ufe0f', + 'sa':'\ud83c\ude02\ufe0f', + 'sagittarius':'\u2650\ufe0f', + 'sake':'\ud83c\udf76', + 'sandal':'\ud83d\udc61', + 'santa':'\ud83c\udf85', + 'satellite':'\ud83d\udce1', + 'saxophone':'\ud83c\udfb7', + 'school':'\ud83c\udfeb', + 'school_satchel':'\ud83c\udf92', + 'scissors':'\u2702\ufe0f', + 'scorpion':'\ud83e\udd82', + 'scorpius':'\u264f\ufe0f', + 'scream':'\ud83d\ude31', + 'scream_cat':'\ud83d\ude40', + 'scroll':'\ud83d\udcdc', + 'seat':'\ud83d\udcba', + 'secret':'\u3299\ufe0f', + 'see_no_evil':'\ud83d\ude48', + 'seedling':'\ud83c\udf31', + 'selfie':'\ud83e\udd33', + 'shallow_pan_of_food':'\ud83e\udd58', + 'shamrock':'\u2618\ufe0f', + 'shark':'\ud83e\udd88', + 'shaved_ice':'\ud83c\udf67', + 'sheep':'\ud83d\udc11', + 'shell':'\ud83d\udc1a', + 'shield':'\ud83d\udee1', + 'shinto_shrine':'\u26e9', + 'ship':'\ud83d\udea2', + 'shirt':'\ud83d\udc55', + 'shopping':'\ud83d\udecd', + 'shopping_cart':'\ud83d\uded2', + 'shower':'\ud83d\udebf', + 'shrimp':'\ud83e\udd90', + 'signal_strength':'\ud83d\udcf6', + 'six_pointed_star':'\ud83d\udd2f', + 'ski':'\ud83c\udfbf', + 'skier':'\u26f7', + 'skull':'\ud83d\udc80', + 'skull_and_crossbones':'\u2620\ufe0f', + 'sleeping':'\ud83d\ude34', + 'sleeping_bed':'\ud83d\udecc', + 'sleepy':'\ud83d\ude2a', + 'slightly_frowning_face':'\ud83d\ude41', + 'slightly_smiling_face':'\ud83d\ude42', + 'slot_machine':'\ud83c\udfb0', + 'small_airplane':'\ud83d\udee9', + 'small_blue_diamond':'\ud83d\udd39', + 'small_orange_diamond':'\ud83d\udd38', + 'small_red_triangle':'\ud83d\udd3a', + 'small_red_triangle_down':'\ud83d\udd3b', + 'smile':'\ud83d\ude04', + 'smile_cat':'\ud83d\ude38', + 'smiley':'\ud83d\ude03', + 'smiley_cat':'\ud83d\ude3a', + 'smiling_imp':'\ud83d\ude08', + 'smirk':'\ud83d\ude0f', + 'smirk_cat':'\ud83d\ude3c', + 'smoking':'\ud83d\udeac', + 'snail':'\ud83d\udc0c', + 'snake':'\ud83d\udc0d', + 'sneezing_face':'\ud83e\udd27', + 'snowboarder':'\ud83c\udfc2', + 'snowflake':'\u2744\ufe0f', + 'snowman':'\u26c4\ufe0f', + 'snowman_with_snow':'\u2603\ufe0f', + 'sob':'\ud83d\ude2d', + 'soccer':'\u26bd\ufe0f', + 'soon':'\ud83d\udd1c', + 'sos':'\ud83c\udd98', + 'sound':'\ud83d\udd09', + 'space_invader':'\ud83d\udc7e', + 'spades':'\u2660\ufe0f', + 'spaghetti':'\ud83c\udf5d', + 'sparkle':'\u2747\ufe0f', + 'sparkler':'\ud83c\udf87', + 'sparkles':'\u2728', + 'sparkling_heart':'\ud83d\udc96', + 'speak_no_evil':'\ud83d\ude4a', + 'speaker':'\ud83d\udd08', + 'speaking_head':'\ud83d\udde3', + 'speech_balloon':'\ud83d\udcac', + 'speedboat':'\ud83d\udea4', + 'spider':'\ud83d\udd77', + 'spider_web':'\ud83d\udd78', + 'spiral_calendar':'\ud83d\uddd3', + 'spiral_notepad':'\ud83d\uddd2', + 'spoon':'\ud83e\udd44', + 'squid':'\ud83e\udd91', + 'stadium':'\ud83c\udfdf', + 'star':'\u2b50\ufe0f', + 'star2':'\ud83c\udf1f', + 'star_and_crescent':'\u262a\ufe0f', + 'star_of_david':'\u2721\ufe0f', + 'stars':'\ud83c\udf20', + 'station':'\ud83d\ude89', + 'statue_of_liberty':'\ud83d\uddfd', + 'steam_locomotive':'\ud83d\ude82', + 'stew':'\ud83c\udf72', + 'stop_button':'\u23f9', + 'stop_sign':'\ud83d\uded1', + 'stopwatch':'\u23f1', + 'straight_ruler':'\ud83d\udccf', + 'strawberry':'\ud83c\udf53', + 'stuck_out_tongue':'\ud83d\ude1b', + 'stuck_out_tongue_closed_eyes':'\ud83d\ude1d', + 'stuck_out_tongue_winking_eye':'\ud83d\ude1c', + 'studio_microphone':'\ud83c\udf99', + 'stuffed_flatbread':'\ud83e\udd59', + 'sun_behind_large_cloud':'\ud83c\udf25', + 'sun_behind_rain_cloud':'\ud83c\udf26', + 'sun_behind_small_cloud':'\ud83c\udf24', + 'sun_with_face':'\ud83c\udf1e', + 'sunflower':'\ud83c\udf3b', + 'sunglasses':'\ud83d\ude0e', + 'sunny':'\u2600\ufe0f', + 'sunrise':'\ud83c\udf05', + 'sunrise_over_mountains':'\ud83c\udf04', + 'surfing_man':'\ud83c\udfc4', + 'surfing_woman':'\ud83c\udfc4‍\u2640\ufe0f', + 'sushi':'\ud83c\udf63', + 'suspension_railway':'\ud83d\ude9f', + 'sweat':'\ud83d\ude13', + 'sweat_drops':'\ud83d\udca6', + 'sweat_smile':'\ud83d\ude05', + 'sweet_potato':'\ud83c\udf60', + 'swimming_man':'\ud83c\udfca', + 'swimming_woman':'\ud83c\udfca‍\u2640\ufe0f', + 'symbols':'\ud83d\udd23', + 'synagogue':'\ud83d\udd4d', + 'syringe':'\ud83d\udc89', + 'taco':'\ud83c\udf2e', + 'tada':'\ud83c\udf89', + 'tanabata_tree':'\ud83c\udf8b', + 'taurus':'\u2649\ufe0f', + 'taxi':'\ud83d\ude95', + 'tea':'\ud83c\udf75', + 'telephone_receiver':'\ud83d\udcde', + 'telescope':'\ud83d\udd2d', + 'tennis':'\ud83c\udfbe', + 'tent':'\u26fa\ufe0f', + 'thermometer':'\ud83c\udf21', + 'thinking':'\ud83e\udd14', + 'thought_balloon':'\ud83d\udcad', + 'ticket':'\ud83c\udfab', + 'tickets':'\ud83c\udf9f', + 'tiger':'\ud83d\udc2f', + 'tiger2':'\ud83d\udc05', + 'timer_clock':'\u23f2', + 'tipping_hand_man':'\ud83d\udc81‍\u2642\ufe0f', + 'tired_face':'\ud83d\ude2b', + 'tm':'\u2122\ufe0f', + 'toilet':'\ud83d\udebd', + 'tokyo_tower':'\ud83d\uddfc', + 'tomato':'\ud83c\udf45', + 'tongue':'\ud83d\udc45', + 'top':'\ud83d\udd1d', + 'tophat':'\ud83c\udfa9', + 'tornado':'\ud83c\udf2a', + 'trackball':'\ud83d\uddb2', + 'tractor':'\ud83d\ude9c', + 'traffic_light':'\ud83d\udea5', + 'train':'\ud83d\ude8b', + 'train2':'\ud83d\ude86', + 'tram':'\ud83d\ude8a', + 'triangular_flag_on_post':'\ud83d\udea9', + 'triangular_ruler':'\ud83d\udcd0', + 'trident':'\ud83d\udd31', + 'triumph':'\ud83d\ude24', + 'trolleybus':'\ud83d\ude8e', + 'trophy':'\ud83c\udfc6', + 'tropical_drink':'\ud83c\udf79', + 'tropical_fish':'\ud83d\udc20', + 'truck':'\ud83d\ude9a', + 'trumpet':'\ud83c\udfba', + 'tulip':'\ud83c\udf37', + 'tumbler_glass':'\ud83e\udd43', + 'turkey':'\ud83e\udd83', + 'turtle':'\ud83d\udc22', + 'tv':'\ud83d\udcfa', + 'twisted_rightwards_arrows':'\ud83d\udd00', + 'two_hearts':'\ud83d\udc95', + 'two_men_holding_hands':'\ud83d\udc6c', + 'two_women_holding_hands':'\ud83d\udc6d', + 'u5272':'\ud83c\ude39', + 'u5408':'\ud83c\ude34', + 'u55b6':'\ud83c\ude3a', + 'u6307':'\ud83c\ude2f\ufe0f', + 'u6708':'\ud83c\ude37\ufe0f', + 'u6709':'\ud83c\ude36', + 'u6e80':'\ud83c\ude35', + 'u7121':'\ud83c\ude1a\ufe0f', + 'u7533':'\ud83c\ude38', + 'u7981':'\ud83c\ude32', + 'u7a7a':'\ud83c\ude33', + 'umbrella':'\u2614\ufe0f', + 'unamused':'\ud83d\ude12', + 'underage':'\ud83d\udd1e', + 'unicorn':'\ud83e\udd84', + 'unlock':'\ud83d\udd13', + 'up':'\ud83c\udd99', + 'upside_down_face':'\ud83d\ude43', + 'v':'\u270c\ufe0f', + 'vertical_traffic_light':'\ud83d\udea6', + 'vhs':'\ud83d\udcfc', + 'vibration_mode':'\ud83d\udcf3', + 'video_camera':'\ud83d\udcf9', + 'video_game':'\ud83c\udfae', + 'violin':'\ud83c\udfbb', + 'virgo':'\u264d\ufe0f', + 'volcano':'\ud83c\udf0b', + 'volleyball':'\ud83c\udfd0', + 'vs':'\ud83c\udd9a', + 'vulcan_salute':'\ud83d\udd96', + 'walking_man':'\ud83d\udeb6', + 'walking_woman':'\ud83d\udeb6‍\u2640\ufe0f', + 'waning_crescent_moon':'\ud83c\udf18', + 'waning_gibbous_moon':'\ud83c\udf16', + 'warning':'\u26a0\ufe0f', + 'wastebasket':'\ud83d\uddd1', + 'watch':'\u231a\ufe0f', + 'water_buffalo':'\ud83d\udc03', + 'watermelon':'\ud83c\udf49', + 'wave':'\ud83d\udc4b', + 'wavy_dash':'\u3030\ufe0f', + 'waxing_crescent_moon':'\ud83c\udf12', + 'wc':'\ud83d\udebe', + 'weary':'\ud83d\ude29', + 'wedding':'\ud83d\udc92', + 'weight_lifting_man':'\ud83c\udfcb\ufe0f', + 'weight_lifting_woman':'\ud83c\udfcb\ufe0f‍\u2640\ufe0f', + 'whale':'\ud83d\udc33', + 'whale2':'\ud83d\udc0b', + 'wheel_of_dharma':'\u2638\ufe0f', + 'wheelchair':'\u267f\ufe0f', + 'white_check_mark':'\u2705', + 'white_circle':'\u26aa\ufe0f', + 'white_flag':'\ud83c\udff3\ufe0f', + 'white_flower':'\ud83d\udcae', + 'white_large_square':'\u2b1c\ufe0f', + 'white_medium_small_square':'\u25fd\ufe0f', + 'white_medium_square':'\u25fb\ufe0f', + 'white_small_square':'\u25ab\ufe0f', + 'white_square_button':'\ud83d\udd33', + 'wilted_flower':'\ud83e\udd40', + 'wind_chime':'\ud83c\udf90', + 'wind_face':'\ud83c\udf2c', + 'wine_glass':'\ud83c\udf77', + 'wink':'\ud83d\ude09', + 'wolf':'\ud83d\udc3a', + 'woman':'\ud83d\udc69', + 'woman_artist':'\ud83d\udc69‍\ud83c\udfa8', + 'woman_astronaut':'\ud83d\udc69‍\ud83d\ude80', + 'woman_cartwheeling':'\ud83e\udd38‍\u2640\ufe0f', + 'woman_cook':'\ud83d\udc69‍\ud83c\udf73', + 'woman_facepalming':'\ud83e\udd26‍\u2640\ufe0f', + 'woman_factory_worker':'\ud83d\udc69‍\ud83c\udfed', + 'woman_farmer':'\ud83d\udc69‍\ud83c\udf3e', + 'woman_firefighter':'\ud83d\udc69‍\ud83d\ude92', + 'woman_health_worker':'\ud83d\udc69‍\u2695\ufe0f', + 'woman_judge':'\ud83d\udc69‍\u2696\ufe0f', + 'woman_juggling':'\ud83e\udd39‍\u2640\ufe0f', + 'woman_mechanic':'\ud83d\udc69‍\ud83d\udd27', + 'woman_office_worker':'\ud83d\udc69‍\ud83d\udcbc', + 'woman_pilot':'\ud83d\udc69‍\u2708\ufe0f', + 'woman_playing_handball':'\ud83e\udd3e‍\u2640\ufe0f', + 'woman_playing_water_polo':'\ud83e\udd3d‍\u2640\ufe0f', + 'woman_scientist':'\ud83d\udc69‍\ud83d\udd2c', + 'woman_shrugging':'\ud83e\udd37‍\u2640\ufe0f', + 'woman_singer':'\ud83d\udc69‍\ud83c\udfa4', + 'woman_student':'\ud83d\udc69‍\ud83c\udf93', + 'woman_teacher':'\ud83d\udc69‍\ud83c\udfeb', + 'woman_technologist':'\ud83d\udc69‍\ud83d\udcbb', + 'woman_with_turban':'\ud83d\udc73‍\u2640\ufe0f', + 'womans_clothes':'\ud83d\udc5a', + 'womans_hat':'\ud83d\udc52', + 'women_wrestling':'\ud83e\udd3c‍\u2640\ufe0f', + 'womens':'\ud83d\udeba', + 'world_map':'\ud83d\uddfa', + 'worried':'\ud83d\ude1f', + 'wrench':'\ud83d\udd27', + 'writing_hand':'\u270d\ufe0f', + 'x':'\u274c', + 'yellow_heart':'\ud83d\udc9b', + 'yen':'\ud83d\udcb4', + 'yin_yang':'\u262f\ufe0f', + 'yum':'\ud83d\ude0b', + 'zap':'\u26a1\ufe0f', + 'zipper_mouth_face':'\ud83e\udd10', + 'zzz':'\ud83d\udca4', + + /* special emojis :P */ + 'octocat': ':octocat:', + 'showdown': 'S' +}; + +/** + * Created by Estevao on 31-05-2015. + */ + +/** + * Showdown Converter class + * @class + * @param {object} [converterOptions] + * @returns {Converter} + */ +showdown.Converter = function (converterOptions) { + 'use strict'; + + var + /** + * Options used by this converter + * @private + * @type {{}} + */ + options = {}, + + /** + * Language extensions used by this converter + * @private + * @type {Array} + */ + langExtensions = [], + + /** + * Output modifiers extensions used by this converter + * @private + * @type {Array} + */ + outputModifiers = [], + + /** + * Event listeners + * @private + * @type {{}} + */ + listeners = {}, + + /** + * The flavor set in this converter + */ + setConvFlavor = setFlavor, + + /** + * Metadata of the document + * @type {{parsed: {}, raw: string, format: string}} + */ + metadata = { + parsed: {}, + raw: '', + format: '' + }; + + _constructor(); + + /** + * Converter constructor + * @private + */ + function _constructor () { + converterOptions = converterOptions || {}; + + for (var gOpt in globalOptions) { + if (globalOptions.hasOwnProperty(gOpt)) { + options[gOpt] = globalOptions[gOpt]; + } + } + + // Merge options + if (typeof converterOptions === 'object') { + for (var opt in converterOptions) { + if (converterOptions.hasOwnProperty(opt)) { + options[opt] = converterOptions[opt]; + } + } + } else { + throw Error('Converter expects the passed parameter to be an object, but ' + typeof converterOptions + + ' was passed instead.'); + } + + if (options.extensions) { + showdown.helper.forEach(options.extensions, _parseExtension); + } + } + + /** + * Parse extension + * @param {*} ext + * @param {string} [name=''] + * @private + */ + function _parseExtension (ext, name) { + + name = name || null; + // If it's a string, the extension was previously loaded + if (showdown.helper.isString(ext)) { + ext = showdown.helper.stdExtName(ext); + name = ext; + + // LEGACY_SUPPORT CODE + if (showdown.extensions[ext]) { + console.warn('DEPRECATION WARNING: ' + ext + ' is an old extension that uses a deprecated loading method.' + + 'Please inform the developer that the extension should be updated!'); + legacyExtensionLoading(showdown.extensions[ext], ext); + return; + // END LEGACY SUPPORT CODE + + } else if (!showdown.helper.isUndefined(extensions[ext])) { + ext = extensions[ext]; + + } else { + throw Error('Extension "' + ext + '" could not be loaded. It was either not found or is not a valid extension.'); + } + } + + if (typeof ext === 'function') { + ext = ext(); + } + + if (!showdown.helper.isArray(ext)) { + ext = [ext]; + } + + var validExt = validate(ext, name); + if (!validExt.valid) { + throw Error(validExt.error); + } + + for (var i = 0; i < ext.length; ++i) { + switch (ext[i].type) { + + case 'lang': + langExtensions.push(ext[i]); + break; + + case 'output': + outputModifiers.push(ext[i]); + break; + } + if (ext[i].hasOwnProperty('listeners')) { + for (var ln in ext[i].listeners) { + if (ext[i].listeners.hasOwnProperty(ln)) { + listen(ln, ext[i].listeners[ln]); + } + } + } + } + + } + + /** + * LEGACY_SUPPORT + * @param {*} ext + * @param {string} name + */ + function legacyExtensionLoading (ext, name) { + if (typeof ext === 'function') { + ext = ext(new showdown.Converter()); + } + if (!showdown.helper.isArray(ext)) { + ext = [ext]; + } + var valid = validate(ext, name); + + if (!valid.valid) { + throw Error(valid.error); + } + + for (var i = 0; i < ext.length; ++i) { + switch (ext[i].type) { + case 'lang': + langExtensions.push(ext[i]); + break; + case 'output': + outputModifiers.push(ext[i]); + break; + default:// should never reach here + throw Error('Extension loader error: Type unrecognized!!!'); + } + } + } + + /** + * Listen to an event + * @param {string} name + * @param {function} callback + */ + function listen (name, callback) { + if (!showdown.helper.isString(name)) { + throw Error('Invalid argument in converter.listen() method: name must be a string, but ' + typeof name + ' given'); + } + + if (typeof callback !== 'function') { + throw Error('Invalid argument in converter.listen() method: callback must be a function, but ' + typeof callback + ' given'); + } + + if (!listeners.hasOwnProperty(name)) { + listeners[name] = []; + } + listeners[name].push(callback); + } + + function rTrimInputText (text) { + var rsp = text.match(/^\s*/)[0].length, + rgx = new RegExp('^\\s{0,' + rsp + '}', 'gm'); + return text.replace(rgx, ''); + } + + /** + * Dispatch an event + * @private + * @param {string} evtName Event name + * @param {string} text Text + * @param {{}} options Converter Options + * @param {{}} globals + * @returns {string} + */ + this._dispatch = function dispatch (evtName, text, options, globals) { + if (listeners.hasOwnProperty(evtName)) { + for (var ei = 0; ei < listeners[evtName].length; ++ei) { + var nText = listeners[evtName][ei](evtName, text, this, options, globals); + if (nText && typeof nText !== 'undefined') { + text = nText; + } + } + } + return text; + }; + + /** + * Listen to an event + * @param {string} name + * @param {function} callback + * @returns {showdown.Converter} + */ + this.listen = function (name, callback) { + listen(name, callback); + return this; + }; + + /** + * Converts a markdown string into HTML + * @param {string} text + * @returns {*} + */ + this.makeHtml = function (text) { + //check if text is not falsy + if (!text) { + return text; + } + + var globals = { + gHtmlBlocks: [], + gHtmlMdBlocks: [], + gHtmlSpans: [], + gUrls: {}, + gTitles: {}, + gDimensions: {}, + gListLevel: 0, + hashLinkCounts: {}, + langExtensions: langExtensions, + outputModifiers: outputModifiers, + converter: this, + ghCodeBlocks: [], + metadata: { + parsed: {}, + raw: '', + format: '' + } + }; + + // This lets us use ¨ trema as an escape char to avoid md5 hashes + // The choice of character is arbitrary; anything that isn't + // magic in Markdown will work. + text = text.replace(/¨/g, '¨T'); + + // Replace $ with ¨D + // RegExp interprets $ as a special character + // when it's in a replacement string + text = text.replace(/\$/g, '¨D'); + + // Standardize line endings + text = text.replace(/\r\n/g, '\n'); // DOS to Unix + text = text.replace(/\r/g, '\n'); // Mac to Unix + + // Stardardize line spaces + text = text.replace(/\u00A0/g, ' '); + + if (options.smartIndentationFix) { + text = rTrimInputText(text); + } + + // Make sure text begins and ends with a couple of newlines: + text = '\n\n' + text + '\n\n'; + + // detab + text = showdown.subParser('detab')(text, options, globals); + + /** + * Strip any lines consisting only of spaces and tabs. + * This makes subsequent regexs easier to write, because we can + * match consecutive blank lines with /\n+/ instead of something + * contorted like /[ \t]*\n+/ + */ + text = text.replace(/^[ \t]+$/mg, ''); + + //run languageExtensions + showdown.helper.forEach(langExtensions, function (ext) { + text = showdown.subParser('runExtension')(ext, text, options, globals); + }); + + // run the sub parsers + text = showdown.subParser('metadata')(text, options, globals); + text = showdown.subParser('hashPreCodeTags')(text, options, globals); + text = showdown.subParser('githubCodeBlocks')(text, options, globals); + text = showdown.subParser('hashHTMLBlocks')(text, options, globals); + text = showdown.subParser('hashCodeTags')(text, options, globals); + text = showdown.subParser('stripLinkDefinitions')(text, options, globals); + text = showdown.subParser('blockGamut')(text, options, globals); + text = showdown.subParser('unhashHTMLSpans')(text, options, globals); + text = showdown.subParser('unescapeSpecialChars')(text, options, globals); + + // attacklab: Restore dollar signs + text = text.replace(/¨D/g, '$$'); + + // attacklab: Restore tremas + text = text.replace(/¨T/g, '¨'); + + // render a complete html document instead of a partial if the option is enabled + text = showdown.subParser('completeHTMLDocument')(text, options, globals); + + // Run output modifiers + showdown.helper.forEach(outputModifiers, function (ext) { + text = showdown.subParser('runExtension')(ext, text, options, globals); + }); + + // update metadata + metadata = globals.metadata; + return text; + }; + + /** + * Converts an HTML string into a markdown string + * @param src + * @param [HTMLParser] A WHATWG DOM and HTML parser, such as JSDOM. If none is supplied, window.document will be used. + * @returns {string} + */ + this.makeMarkdown = this.makeMd = function (src, HTMLParser) { + + // replace \r\n with \n + src = src.replace(/\r\n/g, '\n'); + src = src.replace(/\r/g, '\n'); // old macs + + // due to an edge case, we need to find this: > < + // to prevent removing of non silent white spaces + // ex: this is sparta + src = src.replace(/>[ \t]+¨NBSP;<'); + + if (!HTMLParser) { + if (window && window.document) { + HTMLParser = window.document; + } else { + throw new Error('HTMLParser is undefined. If in a webworker or nodejs environment, you need to provide a WHATWG DOM and HTML such as JSDOM'); + } + } + + var doc = HTMLParser.createElement('div'); + doc.innerHTML = src; + + var globals = { + preList: substitutePreCodeTags(doc) + }; + + // remove all newlines and collapse spaces + clean(doc); + + // some stuff, like accidental reference links must now be escaped + // TODO + // doc.innerHTML = doc.innerHTML.replace(/\[[\S\t ]]/); + + var nodes = doc.childNodes, + mdDoc = ''; + + for (var i = 0; i < nodes.length; i++) { + mdDoc += showdown.subParser('makeMarkdown.node')(nodes[i], globals); + } + + function clean (node) { + for (var n = 0; n < node.childNodes.length; ++n) { + var child = node.childNodes[n]; + if (child.nodeType === 3) { + if (!/\S/.test(child.nodeValue)) { + node.removeChild(child); + --n; + } else { + child.nodeValue = child.nodeValue.split('\n').join(' '); + child.nodeValue = child.nodeValue.replace(/(\s)+/g, '$1'); + } + } else if (child.nodeType === 1) { + clean(child); + } + } + } + + // find all pre tags and replace contents with placeholder + // we need this so that we can remove all indentation from html + // to ease up parsing + function substitutePreCodeTags (doc) { + + var pres = doc.querySelectorAll('pre'), + presPH = []; + + for (var i = 0; i < pres.length; ++i) { + + if (pres[i].childElementCount === 1 && pres[i].firstChild.tagName.toLowerCase() === 'code') { + var content = pres[i].firstChild.innerHTML.trim(), + language = pres[i].firstChild.getAttribute('data-language') || ''; + + // if data-language attribute is not defined, then we look for class language-* + if (language === '') { + var classes = pres[i].firstChild.className.split(' '); + for (var c = 0; c < classes.length; ++c) { + var matches = classes[c].match(/^language-(.+)$/); + if (matches !== null) { + language = matches[1]; + break; + } + } + } + + // unescape html entities in content + content = showdown.helper.unescapeHTMLEntities(content); + + presPH.push(content); + pres[i].outerHTML = ''; + } else { + presPH.push(pres[i].innerHTML); + pres[i].innerHTML = ''; + pres[i].setAttribute('prenum', i.toString()); + } + } + return presPH; + } + + return mdDoc; + }; + + /** + * Set an option of this Converter instance + * @param {string} key + * @param {*} value + */ + this.setOption = function (key, value) { + options[key] = value; + }; + + /** + * Get the option of this Converter instance + * @param {string} key + * @returns {*} + */ + this.getOption = function (key) { + return options[key]; + }; + + /** + * Get the options of this Converter instance + * @returns {{}} + */ + this.getOptions = function () { + return options; + }; + + /** + * Add extension to THIS converter + * @param {{}} extension + * @param {string} [name=null] + */ + this.addExtension = function (extension, name) { + name = name || null; + _parseExtension(extension, name); + }; + + /** + * Use a global registered extension with THIS converter + * @param {string} extensionName Name of the previously registered extension + */ + this.useExtension = function (extensionName) { + _parseExtension(extensionName); + }; + + /** + * Set the flavor THIS converter should use + * @param {string} name + */ + this.setFlavor = function (name) { + if (!flavor.hasOwnProperty(name)) { + throw Error(name + ' flavor was not found'); + } + var preset = flavor[name]; + setConvFlavor = name; + for (var option in preset) { + if (preset.hasOwnProperty(option)) { + options[option] = preset[option]; + } + } + }; + + /** + * Get the currently set flavor of this converter + * @returns {string} + */ + this.getFlavor = function () { + return setConvFlavor; + }; + + /** + * Remove an extension from THIS converter. + * Note: This is a costly operation. It's better to initialize a new converter + * and specify the extensions you wish to use + * @param {Array} extension + */ + this.removeExtension = function (extension) { + if (!showdown.helper.isArray(extension)) { + extension = [extension]; + } + for (var a = 0; a < extension.length; ++a) { + var ext = extension[a]; + for (var i = 0; i < langExtensions.length; ++i) { + if (langExtensions[i] === ext) { + langExtensions[i].splice(i, 1); + } + } + for (var ii = 0; ii < outputModifiers.length; ++i) { + if (outputModifiers[ii] === ext) { + outputModifiers[ii].splice(i, 1); + } + } + } + }; + + /** + * Get all extension of THIS converter + * @returns {{language: Array, output: Array}} + */ + this.getAllExtensions = function () { + return { + language: langExtensions, + output: outputModifiers + }; + }; + + /** + * Get the metadata of the previously parsed document + * @param raw + * @returns {string|{}} + */ + this.getMetadata = function (raw) { + if (raw) { + return metadata.raw; + } else { + return metadata.parsed; + } + }; + + /** + * Get the metadata format of the previously parsed document + * @returns {string} + */ + this.getMetadataFormat = function () { + return metadata.format; + }; + + /** + * Private: set a single key, value metadata pair + * @param {string} key + * @param {string} value + */ + this._setMetadataPair = function (key, value) { + metadata.parsed[key] = value; + }; + + /** + * Private: set metadata format + * @param {string} format + */ + this._setMetadataFormat = function (format) { + metadata.format = format; + }; + + /** + * Private: set metadata raw text + * @param {string} raw + */ + this._setMetadataRaw = function (raw) { + metadata.raw = raw; + }; +}; + +/** + * Turn Markdown link shortcuts into XHTML tags. + */ +showdown.subParser('anchors', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('anchors.before', text, options, globals); + + var writeAnchorTag = function (wholeMatch, linkText, linkId, url, m5, m6, title) { + if (showdown.helper.isUndefined(title)) { + title = ''; + } + linkId = linkId.toLowerCase(); + + // Special case for explicit empty url + if (wholeMatch.search(/\(? ?(['"].*['"])?\)$/m) > -1) { + url = ''; + } else if (!url) { + if (!linkId) { + // lower-case and turn embedded newlines into spaces + linkId = linkText.toLowerCase().replace(/ ?\n/g, ' '); + } + url = '#' + linkId; + + if (!showdown.helper.isUndefined(globals.gUrls[linkId])) { + url = globals.gUrls[linkId]; + if (!showdown.helper.isUndefined(globals.gTitles[linkId])) { + title = globals.gTitles[linkId]; + } + } else { + return wholeMatch; + } + } + + //url = showdown.helper.escapeCharacters(url, '*_', false); // replaced line to improve performance + url = url.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback); + + var result = ''; + + return result; + }; + + // First, handle reference-style links: [link text] [id] + text = text.replace(/\[((?:\[[^\]]*]|[^\[\]])*)] ?(?:\n *)?\[(.*?)]()()()()/g, writeAnchorTag); + + // Next, inline-style links: [link text](url "optional title") + // cases with crazy urls like ./image/cat1).png + text = text.replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]?<([^>]*)>(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g, + writeAnchorTag); + + // normal cases + text = text.replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]??(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g, + writeAnchorTag); + + // handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link test][1] + // or [link test](/foo) + text = text.replace(/\[([^\[\]]+)]()()()()()/g, writeAnchorTag); + + // Lastly handle GithubMentions if option is enabled + if (options.ghMentions) { + text = text.replace(/(^|\s)(\\)?(@([a-z\d]+(?:[a-z\d.-]+?[a-z\d]+)*))/gmi, function (wm, st, escape, mentions, username) { + if (escape === '\\') { + return st + mentions; + } + + //check if options.ghMentionsLink is a string + if (!showdown.helper.isString(options.ghMentionsLink)) { + throw new Error('ghMentionsLink option must be a string'); + } + var lnk = options.ghMentionsLink.replace(/\{u}/g, username), + target = ''; + if (options.openLinksInNewWindow) { + target = ' rel="noopener noreferrer" target="¨E95Eblank"'; + } + return st + '' + mentions + ''; + }); + } + + text = globals.converter._dispatch('anchors.after', text, options, globals); + return text; +}); + +// url allowed chars [a-z\d_.~:/?#[]@!$&'()*+,;=-] + +var simpleURLRegex = /([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+?\.[^'">\s]+?)()(\1)?(?=\s|$)(?!["<>])/gi, + simpleURLRegex2 = /([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+?)([.!?,()\[\]])?(\1)?(?=\s|$)(?!["<>])/gi, + delimUrlRegex = /()<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)()>()/gi, + simpleMailRegex = /(^|\s)(?:mailto:)?([A-Za-z0-9!#$%&'*+-/=?^_`{|}~.]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?=$|\s)/gmi, + delimMailRegex = /<()(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, + + replaceLink = function (options) { + 'use strict'; + return function (wm, leadingMagicChars, link, m2, m3, trailingPunctuation, trailingMagicChars) { + link = link.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback); + var lnkTxt = link, + append = '', + target = '', + lmc = leadingMagicChars || '', + tmc = trailingMagicChars || ''; + if (/^www\./i.test(link)) { + link = link.replace(/^www\./i, 'http://www.'); + } + if (options.excludeTrailingPunctuationFromURLs && trailingPunctuation) { + append = trailingPunctuation; + } + if (options.openLinksInNewWindow) { + target = ' rel="noopener noreferrer" target="¨E95Eblank"'; + } + return lmc + '' + lnkTxt + '' + append + tmc; + }; + }, + + replaceMail = function (options, globals) { + 'use strict'; + return function (wholeMatch, b, mail) { + var href = 'mailto:'; + b = b || ''; + mail = showdown.subParser('unescapeSpecialChars')(mail, options, globals); + if (options.encodeEmails) { + href = showdown.helper.encodeEmailAddress(href + mail); + mail = showdown.helper.encodeEmailAddress(mail); + } else { + href = href + mail; + } + return b + '' + mail + ''; + }; + }; + +showdown.subParser('autoLinks', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('autoLinks.before', text, options, globals); + + text = text.replace(delimUrlRegex, replaceLink(options)); + text = text.replace(delimMailRegex, replaceMail(options, globals)); + + text = globals.converter._dispatch('autoLinks.after', text, options, globals); + + return text; +}); + +showdown.subParser('simplifiedAutoLinks', function (text, options, globals) { + 'use strict'; + + if (!options.simplifiedAutoLink) { + return text; + } + + text = globals.converter._dispatch('simplifiedAutoLinks.before', text, options, globals); + + if (options.excludeTrailingPunctuationFromURLs) { + text = text.replace(simpleURLRegex2, replaceLink(options)); + } else { + text = text.replace(simpleURLRegex, replaceLink(options)); + } + text = text.replace(simpleMailRegex, replaceMail(options, globals)); + + text = globals.converter._dispatch('simplifiedAutoLinks.after', text, options, globals); + + return text; +}); + +/** + * These are all the transformations that form block-level + * tags like paragraphs, headers, and list items. + */ +showdown.subParser('blockGamut', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('blockGamut.before', text, options, globals); + + // we parse blockquotes first so that we can have headings and hrs + // inside blockquotes + text = showdown.subParser('blockQuotes')(text, options, globals); + text = showdown.subParser('headers')(text, options, globals); + + // Do Horizontal Rules: + text = showdown.subParser('horizontalRule')(text, options, globals); + + text = showdown.subParser('lists')(text, options, globals); + text = showdown.subParser('codeBlocks')(text, options, globals); + text = showdown.subParser('tables')(text, options, globals); + + // We already ran _HashHTMLBlocks() before, in Markdown(), but that + // was to escape raw HTML in the original Markdown source. This time, + // we're escaping the markup we've just created, so that we don't wrap + //

      tags around block-level tags. + text = showdown.subParser('hashHTMLBlocks')(text, options, globals); + text = showdown.subParser('paragraphs')(text, options, globals); + + text = globals.converter._dispatch('blockGamut.after', text, options, globals); + + return text; +}); + +showdown.subParser('blockQuotes', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('blockQuotes.before', text, options, globals); + + // add a couple extra lines after the text and endtext mark + text = text + '\n\n'; + + var rgx = /(^ {0,3}>[ \t]?.+\n(.+\n)*\n*)+/gm; + + if (options.splitAdjacentBlockquotes) { + rgx = /^ {0,3}>[\s\S]*?(?:\n\n)/gm; + } + + text = text.replace(rgx, function (bq) { + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + bq = bq.replace(/^[ \t]*>[ \t]?/gm, ''); // trim one level of quoting + + // attacklab: clean up hack + bq = bq.replace(/¨0/g, ''); + + bq = bq.replace(/^[ \t]+$/gm, ''); // trim whitespace-only lines + bq = showdown.subParser('githubCodeBlocks')(bq, options, globals); + bq = showdown.subParser('blockGamut')(bq, options, globals); // recurse + + bq = bq.replace(/(^|\n)/g, '$1 '); + // These leading spaces screw with

       content, so we need to fix that:
      +    bq = bq.replace(/(\s*
      [^\r]+?<\/pre>)/gm, function (wholeMatch, m1) {
      +      var pre = m1;
      +      // attacklab: hack around Konqueror 3.5.4 bug:
      +      pre = pre.replace(/^  /mg, '¨0');
      +      pre = pre.replace(/¨0/g, '');
      +      return pre;
      +    });
      +
      +    return showdown.subParser('hashBlock')('
      \n' + bq + '\n
      ', options, globals); + }); + + text = globals.converter._dispatch('blockQuotes.after', text, options, globals); + return text; +}); + +/** + * Process Markdown `
      ` blocks.
      + */
      +showdown.subParser('codeBlocks', function (text, options, globals) {
      +  'use strict';
      +
      +  text = globals.converter._dispatch('codeBlocks.before', text, options, globals);
      +
      +  // sentinel workarounds for lack of \A and \Z, safari\khtml bug
      +  text += '¨0';
      +
      +  var pattern = /(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g;
      +  text = text.replace(pattern, function (wholeMatch, m1, m2) {
      +    var codeblock = m1,
      +        nextChar = m2,
      +        end = '\n';
      +
      +    codeblock = showdown.subParser('outdent')(codeblock, options, globals);
      +    codeblock = showdown.subParser('encodeCode')(codeblock, options, globals);
      +    codeblock = showdown.subParser('detab')(codeblock, options, globals);
      +    codeblock = codeblock.replace(/^\n+/g, ''); // trim leading newlines
      +    codeblock = codeblock.replace(/\n+$/g, ''); // trim trailing newlines
      +
      +    if (options.omitExtraWLInCodeBlocks) {
      +      end = '';
      +    }
      +
      +    codeblock = '
      ' + codeblock + end + '
      '; + + return showdown.subParser('hashBlock')(codeblock, options, globals) + nextChar; + }); + + // strip sentinel + text = text.replace(/¨0/, ''); + + text = globals.converter._dispatch('codeBlocks.after', text, options, globals); + return text; +}); + +/** + * + * * Backtick quotes are used for spans. + * + * * You can use multiple backticks as the delimiters if you want to + * include literal backticks in the code span. So, this input: + * + * Just type ``foo `bar` baz`` at the prompt. + * + * Will translate to: + * + *

      Just type foo `bar` baz at the prompt.

      + * + * There's no arbitrary limit to the number of backticks you + * can use as delimters. If you need three consecutive backticks + * in your code, use four for delimiters, etc. + * + * * You can use spaces to get literal backticks at the edges: + * + * ... type `` `bar` `` ... + * + * Turns to: + * + * ... type `bar` ... + */ +showdown.subParser('codeSpans', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('codeSpans.before', text, options, globals); + + if (typeof text === 'undefined') { + text = ''; + } + text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, + function (wholeMatch, m1, m2, m3) { + var c = m3; + c = c.replace(/^([ \t]*)/g, ''); // leading whitespace + c = c.replace(/[ \t]*$/g, ''); // trailing whitespace + c = showdown.subParser('encodeCode')(c, options, globals); + c = m1 + '' + c + ''; + c = showdown.subParser('hashHTMLSpans')(c, options, globals); + return c; + } + ); + + text = globals.converter._dispatch('codeSpans.after', text, options, globals); + return text; +}); + +/** + * Create a full HTML document from the processed markdown + */ +showdown.subParser('completeHTMLDocument', function (text, options, globals) { + 'use strict'; + + if (!options.completeHTMLDocument) { + return text; + } + + text = globals.converter._dispatch('completeHTMLDocument.before', text, options, globals); + + var doctype = 'html', + doctypeParsed = '\n', + title = '', + charset = '\n', + lang = '', + metadata = ''; + + if (typeof globals.metadata.parsed.doctype !== 'undefined') { + doctypeParsed = '\n'; + doctype = globals.metadata.parsed.doctype.toString().toLowerCase(); + if (doctype === 'html' || doctype === 'html5') { + charset = ''; + } + } + + for (var meta in globals.metadata.parsed) { + if (globals.metadata.parsed.hasOwnProperty(meta)) { + switch (meta.toLowerCase()) { + case 'doctype': + break; + + case 'title': + title = '' + globals.metadata.parsed.title + '\n'; + break; + + case 'charset': + if (doctype === 'html' || doctype === 'html5') { + charset = '\n'; + } else { + charset = '\n'; + } + break; + + case 'language': + case 'lang': + lang = ' lang="' + globals.metadata.parsed[meta] + '"'; + metadata += '\n'; + break; + + default: + metadata += '\n'; + } + } + } + + text = doctypeParsed + '\n\n' + title + charset + metadata + '\n\n' + text.trim() + '\n\n'; + + text = globals.converter._dispatch('completeHTMLDocument.after', text, options, globals); + return text; +}); + +/** + * Convert all tabs to spaces + */ +showdown.subParser('detab', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('detab.before', text, options, globals); + + // expand first n-1 tabs + text = text.replace(/\t(?=\t)/g, ' '); // g_tab_width + + // replace the nth with two sentinels + text = text.replace(/\t/g, '¨A¨B'); + + // use the sentinel to anchor our regex so it doesn't explode + text = text.replace(/¨B(.+?)¨A/g, function (wholeMatch, m1) { + var leadingText = m1, + numSpaces = 4 - leadingText.length % 4; // g_tab_width + + // there *must* be a better way to do this: + for (var i = 0; i < numSpaces; i++) { + leadingText += ' '; + } + + return leadingText; + }); + + // clean up sentinels + text = text.replace(/¨A/g, ' '); // g_tab_width + text = text.replace(/¨B/g, ''); + + text = globals.converter._dispatch('detab.after', text, options, globals); + return text; +}); + +showdown.subParser('ellipsis', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('ellipsis.before', text, options, globals); + + text = text.replace(/\.\.\./g, '…'); + + text = globals.converter._dispatch('ellipsis.after', text, options, globals); + + return text; +}); + +/** + * Turn emoji codes into emojis + * + * List of supported emojis: https://github.com/showdownjs/showdown/wiki/Emojis + */ +showdown.subParser('emoji', function (text, options, globals) { + 'use strict'; + + if (!options.emoji) { + return text; + } + + text = globals.converter._dispatch('emoji.before', text, options, globals); + + var emojiRgx = /:([\S]+?):/g; + + text = text.replace(emojiRgx, function (wm, emojiCode) { + if (showdown.helper.emojis.hasOwnProperty(emojiCode)) { + return showdown.helper.emojis[emojiCode]; + } + return wm; + }); + + text = globals.converter._dispatch('emoji.after', text, options, globals); + + return text; +}); + +/** + * Smart processing for ampersands and angle brackets that need to be encoded. + */ +showdown.subParser('encodeAmpsAndAngles', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('encodeAmpsAndAngles.before', text, options, globals); + + // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + // http://bumppo.net/projects/amputator/ + text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, '&'); + + // Encode naked <'s + text = text.replace(/<(?![a-z\/?$!])/gi, '<'); + + // Encode < + text = text.replace(/ + text = text.replace(/>/g, '>'); + + text = globals.converter._dispatch('encodeAmpsAndAngles.after', text, options, globals); + return text; +}); + +/** + * Returns the string, with after processing the following backslash escape sequences. + * + * attacklab: The polite way to do this is with the new escapeCharacters() function: + * + * text = escapeCharacters(text,"\\",true); + * text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); + * + * ...but we're sidestepping its use of the (slow) RegExp constructor + * as an optimization for Firefox. This function gets called a LOT. + */ +showdown.subParser('encodeBackslashEscapes', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('encodeBackslashEscapes.before', text, options, globals); + + text = text.replace(/\\(\\)/g, showdown.helper.escapeCharactersCallback); + text = text.replace(/\\([`*_{}\[\]()>#+.!~=|-])/g, showdown.helper.escapeCharactersCallback); + + text = globals.converter._dispatch('encodeBackslashEscapes.after', text, options, globals); + return text; +}); + +/** + * Encode/escape certain characters inside Markdown code runs. + * The point is that in code, these characters are literals, + * and lose their special Markdown meanings. + */ +showdown.subParser('encodeCode', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('encodeCode.before', text, options, globals); + + // Encode all ampersands; HTML entities are not + // entities within a Markdown code span. + text = text + .replace(/&/g, '&') + // Do the angle bracket song and dance: + .replace(//g, '>') + // Now, escape characters that are magic in Markdown: + .replace(/([*_{}\[\]\\=~-])/g, showdown.helper.escapeCharactersCallback); + + text = globals.converter._dispatch('encodeCode.after', text, options, globals); + return text; +}); + +/** + * Within tags -- meaning between < and > -- encode [\ ` * _ ~ =] so they + * don't conflict with their use in Markdown for code, italics and strong. + */ +showdown.subParser('escapeSpecialCharsWithinTagAttributes', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('escapeSpecialCharsWithinTagAttributes.before', text, options, globals); + + // Build a regex to find HTML tags. + var tags = /<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi, + comments = /-]|-[^>])(?:[^-]|-[^-])*)--)>/gi; + + text = text.replace(tags, function (wholeMatch) { + return wholeMatch + .replace(/(.)<\/?code>(?=.)/g, '$1`') + .replace(/([\\`*_~=|])/g, showdown.helper.escapeCharactersCallback); + }); + + text = text.replace(comments, function (wholeMatch) { + return wholeMatch + .replace(/([\\`*_~=|])/g, showdown.helper.escapeCharactersCallback); + }); + + text = globals.converter._dispatch('escapeSpecialCharsWithinTagAttributes.after', text, options, globals); + return text; +}); + +/** + * Handle github codeblocks prior to running HashHTML so that + * HTML contained within the codeblock gets escaped properly + * Example: + * ```ruby + * def hello_world(x) + * puts "Hello, #{x}" + * end + * ``` + */ +showdown.subParser('githubCodeBlocks', function (text, options, globals) { + 'use strict'; + + // early exit if option is not enabled + if (!options.ghCodeBlocks) { + return text; + } + + text = globals.converter._dispatch('githubCodeBlocks.before', text, options, globals); + + text += '¨0'; + + text = text.replace(/(?:^|\n)(?: {0,3})(```+|~~~+)(?: *)([^\s`~]*)\n([\s\S]*?)\n(?: {0,3})\1/g, function (wholeMatch, delim, language, codeblock) { + var end = (options.omitExtraWLInCodeBlocks) ? '' : '\n'; + + // First parse the github code block + codeblock = showdown.subParser('encodeCode')(codeblock, options, globals); + codeblock = showdown.subParser('detab')(codeblock, options, globals); + codeblock = codeblock.replace(/^\n+/g, ''); // trim leading newlines + codeblock = codeblock.replace(/\n+$/g, ''); // trim trailing whitespace + + codeblock = '
      ' + codeblock + end + '
      '; + + codeblock = showdown.subParser('hashBlock')(codeblock, options, globals); + + // Since GHCodeblocks can be false positives, we need to + // store the primitive text and the parsed text in a global var, + // and then return a token + return '\n\n¨G' + (globals.ghCodeBlocks.push({text: wholeMatch, codeblock: codeblock}) - 1) + 'G\n\n'; + }); + + // attacklab: strip sentinel + text = text.replace(/¨0/, ''); + + return globals.converter._dispatch('githubCodeBlocks.after', text, options, globals); +}); + +showdown.subParser('hashBlock', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('hashBlock.before', text, options, globals); + text = text.replace(/(^\n+|\n+$)/g, ''); + text = '\n\n¨K' + (globals.gHtmlBlocks.push(text) - 1) + 'K\n\n'; + text = globals.converter._dispatch('hashBlock.after', text, options, globals); + return text; +}); + +/** + * Hash and escape elements that should not be parsed as markdown + */ +showdown.subParser('hashCodeTags', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('hashCodeTags.before', text, options, globals); + + var repFunc = function (wholeMatch, match, left, right) { + var codeblock = left + showdown.subParser('encodeCode')(match, options, globals) + right; + return '¨C' + (globals.gHtmlSpans.push(codeblock) - 1) + 'C'; + }; + + // Hash naked + text = showdown.helper.replaceRecursiveRegExp(text, repFunc, ']*>', '', 'gim'); + + text = globals.converter._dispatch('hashCodeTags.after', text, options, globals); + return text; +}); + +showdown.subParser('hashElement', function (text, options, globals) { + 'use strict'; + + return function (wholeMatch, m1) { + var blockText = m1; + + // Undo double lines + blockText = blockText.replace(/\n\n/g, '\n'); + blockText = blockText.replace(/^\n/, ''); + + // strip trailing blank lines + blockText = blockText.replace(/\n+$/g, ''); + + // Replace the element text with a marker ("¨KxK" where x is its key) + blockText = '\n\n¨K' + (globals.gHtmlBlocks.push(blockText) - 1) + 'K\n\n'; + + return blockText; + }; +}); + +showdown.subParser('hashHTMLBlocks', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('hashHTMLBlocks.before', text, options, globals); + + var blockTags = [ + 'pre', + 'div', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'table', + 'dl', + 'ol', + 'ul', + 'script', + 'noscript', + 'form', + 'fieldset', + 'iframe', + 'math', + 'style', + 'section', + 'header', + 'footer', + 'nav', + 'article', + 'aside', + 'address', + 'audio', + 'canvas', + 'figure', + 'hgroup', + 'output', + 'video', + 'p' + ], + repFunc = function (wholeMatch, match, left, right) { + var txt = wholeMatch; + // check if this html element is marked as markdown + // if so, it's contents should be parsed as markdown + if (left.search(/\bmarkdown\b/) !== -1) { + txt = left + globals.converter.makeHtml(match) + right; + } + return '\n\n¨K' + (globals.gHtmlBlocks.push(txt) - 1) + 'K\n\n'; + }; + + if (options.backslashEscapesHTMLTags) { + // encode backslash escaped HTML tags + text = text.replace(/\\<(\/?[^>]+?)>/g, function (wm, inside) { + return '<' + inside + '>'; + }); + } + + // hash HTML Blocks + for (var i = 0; i < blockTags.length; ++i) { + + var opTagPos, + rgx1 = new RegExp('^ {0,3}(<' + blockTags[i] + '\\b[^>]*>)', 'im'), + patLeft = '<' + blockTags[i] + '\\b[^>]*>', + patRight = ''; + // 1. Look for the first position of the first opening HTML tag in the text + while ((opTagPos = showdown.helper.regexIndexOf(text, rgx1)) !== -1) { + + // if the HTML tag is \ escaped, we need to escape it and break + + + //2. Split the text in that position + var subTexts = showdown.helper.splitAtIndex(text, opTagPos), + //3. Match recursively + newSubText1 = showdown.helper.replaceRecursiveRegExp(subTexts[1], repFunc, patLeft, patRight, 'im'); + + // prevent an infinite loop + if (newSubText1 === subTexts[1]) { + break; + } + text = subTexts[0].concat(newSubText1); + } + } + // HR SPECIAL CASE + text = text.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, + showdown.subParser('hashElement')(text, options, globals)); + + // Special case for standalone HTML comments + text = showdown.helper.replaceRecursiveRegExp(text, function (txt) { + return '\n\n¨K' + (globals.gHtmlBlocks.push(txt) - 1) + 'K\n\n'; + }, '^ {0,3}', 'gm'); + + // PHP and ASP-style processor instructions ( and <%...%>) + text = text.replace(/(?:\n\n)( {0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, + showdown.subParser('hashElement')(text, options, globals)); + + text = globals.converter._dispatch('hashHTMLBlocks.after', text, options, globals); + return text; +}); + +/** + * Hash span elements that should not be parsed as markdown + */ +showdown.subParser('hashHTMLSpans', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('hashHTMLSpans.before', text, options, globals); + + function hashHTMLSpan (html) { + return '¨C' + (globals.gHtmlSpans.push(html) - 1) + 'C'; + } + + // Hash Self Closing tags + text = text.replace(/<[^>]+?\/>/gi, function (wm) { + return hashHTMLSpan(wm); + }); + + // Hash tags without properties + text = text.replace(/<([^>]+?)>[\s\S]*?<\/\1>/g, function (wm) { + return hashHTMLSpan(wm); + }); + + // Hash tags with properties + text = text.replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g, function (wm) { + return hashHTMLSpan(wm); + }); + + // Hash self closing tags without /> + text = text.replace(/<[^>]+?>/gi, function (wm) { + return hashHTMLSpan(wm); + }); + + /*showdown.helper.matchRecursiveRegExp(text, ']*>', '', 'gi');*/ + + text = globals.converter._dispatch('hashHTMLSpans.after', text, options, globals); + return text; +}); + +/** + * Unhash HTML spans + */ +showdown.subParser('unhashHTMLSpans', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('unhashHTMLSpans.before', text, options, globals); + + for (var i = 0; i < globals.gHtmlSpans.length; ++i) { + var repText = globals.gHtmlSpans[i], + // limiter to prevent infinite loop (assume 10 as limit for recurse) + limit = 0; + + while (/¨C(\d+)C/.test(repText)) { + var num = RegExp.$1; + repText = repText.replace('¨C' + num + 'C', globals.gHtmlSpans[num]); + if (limit === 10) { + console.error('maximum nesting of 10 spans reached!!!'); + break; + } + ++limit; + } + text = text.replace('¨C' + i + 'C', repText); + } + + text = globals.converter._dispatch('unhashHTMLSpans.after', text, options, globals); + return text; +}); + +/** + * Hash and escape
       elements that should not be parsed as markdown
      + */
      +showdown.subParser('hashPreCodeTags', function (text, options, globals) {
      +  'use strict';
      +  text = globals.converter._dispatch('hashPreCodeTags.before', text, options, globals);
      +
      +  var repFunc = function (wholeMatch, match, left, right) {
      +    // encode html entities
      +    var codeblock = left + showdown.subParser('encodeCode')(match, options, globals) + right;
      +    return '\n\n¨G' + (globals.ghCodeBlocks.push({text: wholeMatch, codeblock: codeblock}) - 1) + 'G\n\n';
      +  };
      +
      +  // Hash 
      
      +  text = showdown.helper.replaceRecursiveRegExp(text, repFunc, '^ {0,3}]*>\\s*]*>', '^ {0,3}\\s*
      ', 'gim'); + + text = globals.converter._dispatch('hashPreCodeTags.after', text, options, globals); + return text; +}); + +showdown.subParser('headers', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('headers.before', text, options, globals); + + var headerLevelStart = (isNaN(parseInt(options.headerLevelStart))) ? 1 : parseInt(options.headerLevelStart), + + // Set text-style headers: + // Header 1 + // ======== + // + // Header 2 + // -------- + // + setextRegexH1 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n={2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n=+[ \t]*\n+/gm, + setextRegexH2 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n-+[ \t]*\n+/gm; + + text = text.replace(setextRegexH1, function (wholeMatch, m1) { + + var spanGamut = showdown.subParser('spanGamut')(m1, options, globals), + hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"', + hLevel = headerLevelStart, + hashBlock = '' + spanGamut + ''; + return showdown.subParser('hashBlock')(hashBlock, options, globals); + }); + + text = text.replace(setextRegexH2, function (matchFound, m1) { + var spanGamut = showdown.subParser('spanGamut')(m1, options, globals), + hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"', + hLevel = headerLevelStart + 1, + hashBlock = '' + spanGamut + ''; + return showdown.subParser('hashBlock')(hashBlock, options, globals); + }); + + // atx-style headers: + // # Header 1 + // ## Header 2 + // ## Header 2 with closing hashes ## + // ... + // ###### Header 6 + // + var atxStyle = (options.requireSpaceBeforeHeadingText) ? /^(#{1,6})[ \t]+(.+?)[ \t]*#*\n+/gm : /^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm; + + text = text.replace(atxStyle, function (wholeMatch, m1, m2) { + var hText = m2; + if (options.customizedHeaderId) { + hText = m2.replace(/\s?\{([^{]+?)}\s*$/, ''); + } + + var span = showdown.subParser('spanGamut')(hText, options, globals), + hID = (options.noHeaderId) ? '' : ' id="' + headerId(m2) + '"', + hLevel = headerLevelStart - 1 + m1.length, + header = '' + span + ''; + + return showdown.subParser('hashBlock')(header, options, globals); + }); + + function headerId (m) { + var title, + prefix; + + // It is separate from other options to allow combining prefix and customized + if (options.customizedHeaderId) { + var match = m.match(/\{([^{]+?)}\s*$/); + if (match && match[1]) { + m = match[1]; + } + } + + title = m; + + // Prefix id to prevent causing inadvertent pre-existing style matches. + if (showdown.helper.isString(options.prefixHeaderId)) { + prefix = options.prefixHeaderId; + } else if (options.prefixHeaderId === true) { + prefix = 'section-'; + } else { + prefix = ''; + } + + if (!options.rawPrefixHeaderId) { + title = prefix + title; + } + + if (options.ghCompatibleHeaderId) { + title = title + .replace(/ /g, '-') + // replace previously escaped chars (&, ¨ and $) + .replace(/&/g, '') + .replace(/¨T/g, '') + .replace(/¨D/g, '') + // replace rest of the chars (&~$ are repeated as they might have been escaped) + // borrowed from github's redcarpet (some they should produce similar results) + .replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g, '') + .toLowerCase(); + } else if (options.rawHeaderId) { + title = title + .replace(/ /g, '-') + // replace previously escaped chars (&, ¨ and $) + .replace(/&/g, '&') + .replace(/¨T/g, '¨') + .replace(/¨D/g, '$') + // replace " and ' + .replace(/["']/g, '-') + .toLowerCase(); + } else { + title = title + .replace(/[^\w]/g, '') + .toLowerCase(); + } + + if (options.rawPrefixHeaderId) { + title = prefix + title; + } + + if (globals.hashLinkCounts[title]) { + title = title + '-' + (globals.hashLinkCounts[title]++); + } else { + globals.hashLinkCounts[title] = 1; + } + return title; + } + + text = globals.converter._dispatch('headers.after', text, options, globals); + return text; +}); + +/** + * Turn Markdown link shortcuts into XHTML tags. + */ +showdown.subParser('horizontalRule', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('horizontalRule.before', text, options, globals); + + var key = showdown.subParser('hashBlock')('
      ', options, globals); + text = text.replace(/^ {0,2}( ?-){3,}[ \t]*$/gm, key); + text = text.replace(/^ {0,2}( ?\*){3,}[ \t]*$/gm, key); + text = text.replace(/^ {0,2}( ?_){3,}[ \t]*$/gm, key); + + text = globals.converter._dispatch('horizontalRule.after', text, options, globals); + return text; +}); + +/** + * Turn Markdown image shortcuts into tags. + */ +showdown.subParser('images', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('images.before', text, options, globals); + + var inlineRegExp = /!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g, + crazyRegExp = /!\[([^\]]*?)][ \t]*()\([ \t]?<([^>]*)>(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(?:(["'])([^"]*?)\6))?[ \t]?\)/g, + base64RegExp = /!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g, + referenceRegExp = /!\[([^\]]*?)] ?(?:\n *)?\[([\s\S]*?)]()()()()()/g, + refShortcutRegExp = /!\[([^\[\]]+)]()()()()()/g; + + function writeImageTagBase64 (wholeMatch, altText, linkId, url, width, height, m5, title) { + url = url.replace(/\s/g, ''); + return writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title); + } + + function writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title) { + + var gUrls = globals.gUrls, + gTitles = globals.gTitles, + gDims = globals.gDimensions; + + linkId = linkId.toLowerCase(); + + if (!title) { + title = ''; + } + // Special case for explicit empty url + if (wholeMatch.search(/\(? ?(['"].*['"])?\)$/m) > -1) { + url = ''; + + } else if (url === '' || url === null) { + if (linkId === '' || linkId === null) { + // lower-case and turn embedded newlines into spaces + linkId = altText.toLowerCase().replace(/ ?\n/g, ' '); + } + url = '#' + linkId; + + if (!showdown.helper.isUndefined(gUrls[linkId])) { + url = gUrls[linkId]; + if (!showdown.helper.isUndefined(gTitles[linkId])) { + title = gTitles[linkId]; + } + if (!showdown.helper.isUndefined(gDims[linkId])) { + width = gDims[linkId].width; + height = gDims[linkId].height; + } + } else { + return wholeMatch; + } + } + + altText = altText + .replace(/"/g, '"') + //altText = showdown.helper.escapeCharacters(altText, '*_', false); + .replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback); + //url = showdown.helper.escapeCharacters(url, '*_', false); + url = url.replace(showdown.helper.regexes.asteriskDashAndColon, showdown.helper.escapeCharactersCallback); + var result = '' + altText + 'x "optional title") + + // base64 encoded images + text = text.replace(base64RegExp, writeImageTagBase64); + + // cases with crazy urls like ./image/cat1).png + text = text.replace(crazyRegExp, writeImageTag); + + // normal cases + text = text.replace(inlineRegExp, writeImageTag); + + // handle reference-style shortcuts: ![img text] + text = text.replace(refShortcutRegExp, writeImageTag); + + text = globals.converter._dispatch('images.after', text, options, globals); + return text; +}); + +showdown.subParser('italicsAndBold', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('italicsAndBold.before', text, options, globals); + + // it's faster to have 3 separate regexes for each case than have just one + // because of backtracing, in some cases, it could lead to an exponential effect + // called "catastrophic backtrace". Ominous! + + function parseInside (txt, left, right) { + /* + if (options.simplifiedAutoLink) { + txt = showdown.subParser('simplifiedAutoLinks')(txt, options, globals); + } + */ + return left + txt + right; + } + + // Parse underscores + if (options.literalMidWordUnderscores) { + text = text.replace(/\b___(\S[\s\S]*?)___\b/g, function (wm, txt) { + return parseInside (txt, '', ''); + }); + text = text.replace(/\b__(\S[\s\S]*?)__\b/g, function (wm, txt) { + return parseInside (txt, '', ''); + }); + text = text.replace(/\b_(\S[\s\S]*?)_\b/g, function (wm, txt) { + return parseInside (txt, '', ''); + }); + } else { + text = text.replace(/___(\S[\s\S]*?)___/g, function (wm, m) { + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + text = text.replace(/__(\S[\s\S]*?)__/g, function (wm, m) { + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + text = text.replace(/_([^\s_][\s\S]*?)_/g, function (wm, m) { + // !/^_[^_]/.test(m) - test if it doesn't start with __ (since it seems redundant, we removed it) + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + } + + // Now parse asterisks + if (options.literalMidWordAsterisks) { + text = text.replace(/([^*]|^)\B\*\*\*(\S[\s\S]*?)\*\*\*\B(?!\*)/g, function (wm, lead, txt) { + return parseInside (txt, lead + '', ''); + }); + text = text.replace(/([^*]|^)\B\*\*(\S[\s\S]*?)\*\*\B(?!\*)/g, function (wm, lead, txt) { + return parseInside (txt, lead + '', ''); + }); + text = text.replace(/([^*]|^)\B\*(\S[\s\S]*?)\*\B(?!\*)/g, function (wm, lead, txt) { + return parseInside (txt, lead + '', ''); + }); + } else { + text = text.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g, function (wm, m) { + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + text = text.replace(/\*\*(\S[\s\S]*?)\*\*/g, function (wm, m) { + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + text = text.replace(/\*([^\s*][\s\S]*?)\*/g, function (wm, m) { + // !/^\*[^*]/.test(m) - test if it doesn't start with ** (since it seems redundant, we removed it) + return (/\S$/.test(m)) ? parseInside (m, '', '') : wm; + }); + } + + + text = globals.converter._dispatch('italicsAndBold.after', text, options, globals); + return text; +}); + +/** + * Form HTML ordered (numbered) and unordered (bulleted) lists. + */ +showdown.subParser('lists', function (text, options, globals) { + 'use strict'; + + /** + * Process the contents of a single ordered or unordered list, splitting it + * into individual list items. + * @param {string} listStr + * @param {boolean} trimTrailing + * @returns {string} + */ + function processListItems (listStr, trimTrailing) { + // The $g_list_level global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. + // + // We do this because when we're not inside a list, we want to treat + // something like this: + // + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. + // + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. + // + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". + globals.gListLevel++; + + // trim trailing blank lines: + listStr = listStr.replace(/\n{2,}$/, '\n'); + + // attacklab: add sentinel to emulate \z + listStr += '¨0'; + + var rgx = /(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0| {0,3}([*+-]|\d+[.])[ \t]+))/gm, + isParagraphed = (/\n[ \t]*\n(?!¨0)/.test(listStr)); + + // Since version 1.5, nesting sublists requires 4 spaces (or 1 tab) indentation, + // which is a syntax breaking change + // activating this option reverts to old behavior + if (options.disableForced4SpacesIndentedSublists) { + rgx = /(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0|\2([*+-]|\d+[.])[ \t]+))/gm; + } + + listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4, taskbtn, checked) { + checked = (checked && checked.trim() !== ''); + + var item = showdown.subParser('outdent')(m4, options, globals), + bulletStyle = ''; + + // Support for github tasklists + if (taskbtn && options.tasklists) { + bulletStyle = ' class="task-list-item" style="list-style-type: none;"'; + item = item.replace(/^[ \t]*\[(x|X| )?]/m, function () { + var otp = '
    • a
    • + // instead of: + //
      • - - a
      + // So, to prevent it, we will put a marker (¨A)in the beginning of the line + // Kind of hackish/monkey patching, but seems more effective than overcomplicating the list parser + item = item.replace(/^([-*+]|\d\.)[ \t]+[\S\n ]*/g, function (wm2) { + return '¨A' + wm2; + }); + + // m1 - Leading line or + // Has a double return (multi paragraph) or + // Has sublist + if (m1 || (item.search(/\n{2,}/) > -1)) { + item = showdown.subParser('githubCodeBlocks')(item, options, globals); + item = showdown.subParser('blockGamut')(item, options, globals); + } else { + // Recursion for sub-lists: + item = showdown.subParser('lists')(item, options, globals); + item = item.replace(/\n$/, ''); // chomp(item) + item = showdown.subParser('hashHTMLBlocks')(item, options, globals); + + // Colapse double linebreaks + item = item.replace(/\n\n+/g, '\n\n'); + if (isParagraphed) { + item = showdown.subParser('paragraphs')(item, options, globals); + } else { + item = showdown.subParser('spanGamut')(item, options, globals); + } + } + + // now we need to remove the marker (¨A) + item = item.replace('¨A', ''); + // we can finally wrap the line in list item tags + item = '' + item + '\n'; + + return item; + }); + + // attacklab: strip sentinel + listStr = listStr.replace(/¨0/g, ''); + + globals.gListLevel--; + + if (trimTrailing) { + listStr = listStr.replace(/\s+$/, ''); + } + + return listStr; + } + + function styleStartNumber (list, listType) { + // check if ol and starts by a number different than 1 + if (listType === 'ol') { + var res = list.match(/^ *(\d+)\./); + if (res && res[1] !== '1') { + return ' start="' + res[1] + '"'; + } + } + return ''; + } + + /** + * Check and parse consecutive lists (better fix for issue #142) + * @param {string} list + * @param {string} listType + * @param {boolean} trimTrailing + * @returns {string} + */ + function parseConsecutiveLists (list, listType, trimTrailing) { + // check if we caught 2 or more consecutive lists by mistake + // we use the counterRgx, meaning if listType is UL we look for OL and vice versa + var olRgx = (options.disableForced4SpacesIndentedSublists) ? /^ ?\d+\.[ \t]/gm : /^ {0,3}\d+\.[ \t]/gm, + ulRgx = (options.disableForced4SpacesIndentedSublists) ? /^ ?[*+-][ \t]/gm : /^ {0,3}[*+-][ \t]/gm, + counterRxg = (listType === 'ul') ? olRgx : ulRgx, + result = ''; + + if (list.search(counterRxg) !== -1) { + (function parseCL (txt) { + var pos = txt.search(counterRxg), + style = styleStartNumber(list, listType); + if (pos !== -1) { + // slice + result += '\n\n<' + listType + style + '>\n' + processListItems(txt.slice(0, pos), !!trimTrailing) + '\n'; + + // invert counterType and listType + listType = (listType === 'ul') ? 'ol' : 'ul'; + counterRxg = (listType === 'ul') ? olRgx : ulRgx; + + //recurse + parseCL(txt.slice(pos)); + } else { + result += '\n\n<' + listType + style + '>\n' + processListItems(txt, !!trimTrailing) + '\n'; + } + })(list); + } else { + var style = styleStartNumber(list, listType); + result = '\n\n<' + listType + style + '>\n' + processListItems(list, !!trimTrailing) + '\n'; + } + + return result; + } + + /** Start of list parsing **/ + text = globals.converter._dispatch('lists.before', text, options, globals); + // add sentinel to hack around khtml/safari bug: + // http://bugs.webkit.org/show_bug.cgi?id=11231 + text += '¨0'; + + if (globals.gListLevel) { + text = text.replace(/^(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm, + function (wholeMatch, list, m2) { + var listType = (m2.search(/[*+-]/g) > -1) ? 'ul' : 'ol'; + return parseConsecutiveLists(list, listType, true); + } + ); + } else { + text = text.replace(/(\n\n|^\n?)(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm, + function (wholeMatch, m1, list, m3) { + var listType = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol'; + return parseConsecutiveLists(list, listType, false); + } + ); + } + + // strip sentinel + text = text.replace(/¨0/, ''); + text = globals.converter._dispatch('lists.after', text, options, globals); + return text; +}); + +/** + * Parse metadata at the top of the document + */ +showdown.subParser('metadata', function (text, options, globals) { + 'use strict'; + + if (!options.metadata) { + return text; + } + + text = globals.converter._dispatch('metadata.before', text, options, globals); + + function parseMetadataContents (content) { + // raw is raw so it's not changed in any way + globals.metadata.raw = content; + + // escape chars forbidden in html attributes + // double quotes + content = content + // ampersand first + .replace(/&/g, '&') + // double quotes + .replace(/"/g, '"'); + + content = content.replace(/\n {4}/g, ' '); + content.replace(/^([\S ]+): +([\s\S]+?)$/gm, function (wm, key, value) { + globals.metadata.parsed[key] = value; + return ''; + }); + } + + text = text.replace(/^\s*«««+(\S*?)\n([\s\S]+?)\n»»»+\n/, function (wholematch, format, content) { + parseMetadataContents(content); + return '¨M'; + }); + + text = text.replace(/^\s*---+(\S*?)\n([\s\S]+?)\n---+\n/, function (wholematch, format, content) { + if (format) { + globals.metadata.format = format; + } + parseMetadataContents(content); + return '¨M'; + }); + + text = text.replace(/¨M/g, ''); + + text = globals.converter._dispatch('metadata.after', text, options, globals); + return text; +}); + +/** + * Remove one level of line-leading tabs or spaces + */ +showdown.subParser('outdent', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('outdent.before', text, options, globals); + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + text = text.replace(/^(\t|[ ]{1,4})/gm, '¨0'); // attacklab: g_tab_width + + // attacklab: clean up hack + text = text.replace(/¨0/g, ''); + + text = globals.converter._dispatch('outdent.after', text, options, globals); + return text; +}); + +/** + * + */ +showdown.subParser('paragraphs', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('paragraphs.before', text, options, globals); + // Strip leading and trailing lines: + text = text.replace(/^\n+/g, ''); + text = text.replace(/\n+$/g, ''); + + var grafs = text.split(/\n{2,}/g), + grafsOut = [], + end = grafs.length; // Wrap

      tags + + for (var i = 0; i < end; i++) { + var str = grafs[i]; + // if this is an HTML marker, copy it + if (str.search(/¨(K|G)(\d+)\1/g) >= 0) { + grafsOut.push(str); + + // test for presence of characters to prevent empty lines being parsed + // as paragraphs (resulting in undesired extra empty paragraphs) + } else if (str.search(/\S/) >= 0) { + str = showdown.subParser('spanGamut')(str, options, globals); + str = str.replace(/^([ \t]*)/g, '

      '); + str += '

      '; + grafsOut.push(str); + } + } + + /** Unhashify HTML blocks */ + end = grafsOut.length; + for (i = 0; i < end; i++) { + var blockText = '', + grafsOutIt = grafsOut[i], + codeFlag = false; + // if this is a marker for an html block... + // use RegExp.test instead of string.search because of QML bug + while (/¨(K|G)(\d+)\1/.test(grafsOutIt)) { + var delim = RegExp.$1, + num = RegExp.$2; + + if (delim === 'K') { + blockText = globals.gHtmlBlocks[num]; + } else { + // we need to check if ghBlock is a false positive + if (codeFlag) { + // use encoded version of all text + blockText = showdown.subParser('encodeCode')(globals.ghCodeBlocks[num].text, options, globals); + } else { + blockText = globals.ghCodeBlocks[num].codeblock; + } + } + blockText = blockText.replace(/\$/g, '$$$$'); // Escape any dollar signs + + grafsOutIt = grafsOutIt.replace(/(\n\n)?¨(K|G)\d+\2(\n\n)?/, blockText); + // Check if grafsOutIt is a pre->code + if (/^]*>\s*]*>/.test(grafsOutIt)) { + codeFlag = true; + } + } + grafsOut[i] = grafsOutIt; + } + text = grafsOut.join('\n'); + // Strip leading and trailing lines: + text = text.replace(/^\n+/g, ''); + text = text.replace(/\n+$/g, ''); + return globals.converter._dispatch('paragraphs.after', text, options, globals); +}); + +/** + * Run extension + */ +showdown.subParser('runExtension', function (ext, text, options, globals) { + 'use strict'; + + if (ext.filter) { + text = ext.filter(text, globals.converter, options); + + } else if (ext.regex) { + // TODO remove this when old extension loading mechanism is deprecated + var re = ext.regex; + if (!(re instanceof RegExp)) { + re = new RegExp(re, 'g'); + } + text = text.replace(re, ext.replace); + } + + return text; +}); + +/** + * These are all the transformations that occur *within* block-level + * tags like paragraphs, headers, and list items. + */ +showdown.subParser('spanGamut', function (text, options, globals) { + 'use strict'; + + text = globals.converter._dispatch('spanGamut.before', text, options, globals); + text = showdown.subParser('codeSpans')(text, options, globals); + text = showdown.subParser('escapeSpecialCharsWithinTagAttributes')(text, options, globals); + text = showdown.subParser('encodeBackslashEscapes')(text, options, globals); + + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + text = showdown.subParser('images')(text, options, globals); + text = showdown.subParser('anchors')(text, options, globals); + + // Make links out of things like `` + // Must come after anchors, because you can use < and > + // delimiters in inline links like [this](). + text = showdown.subParser('autoLinks')(text, options, globals); + text = showdown.subParser('simplifiedAutoLinks')(text, options, globals); + text = showdown.subParser('emoji')(text, options, globals); + text = showdown.subParser('underline')(text, options, globals); + text = showdown.subParser('italicsAndBold')(text, options, globals); + text = showdown.subParser('strikethrough')(text, options, globals); + text = showdown.subParser('ellipsis')(text, options, globals); + + // we need to hash HTML tags inside spans + text = showdown.subParser('hashHTMLSpans')(text, options, globals); + + // now we encode amps and angles + text = showdown.subParser('encodeAmpsAndAngles')(text, options, globals); + + // Do hard breaks + if (options.simpleLineBreaks) { + // GFM style hard breaks + // only add line breaks if the text does not contain a block (special case for lists) + if (!/\n\n¨K/.test(text)) { + text = text.replace(/\n+/g, '
      \n'); + } + } else { + // Vanilla hard breaks + text = text.replace(/ +\n/g, '
      \n'); + } + + text = globals.converter._dispatch('spanGamut.after', text, options, globals); + return text; +}); + +showdown.subParser('strikethrough', function (text, options, globals) { + 'use strict'; + + function parseInside (txt) { + if (options.simplifiedAutoLink) { + txt = showdown.subParser('simplifiedAutoLinks')(txt, options, globals); + } + return '' + txt + ''; + } + + if (options.strikethrough) { + text = globals.converter._dispatch('strikethrough.before', text, options, globals); + text = text.replace(/(?:~){2}([\s\S]+?)(?:~){2}/g, function (wm, txt) { return parseInside(txt); }); + text = globals.converter._dispatch('strikethrough.after', text, options, globals); + } + + return text; +}); + +/** + * Strips link definitions from text, stores the URLs and titles in + * hash references. + * Link defs are in the form: ^[id]: url "optional title" + */ +showdown.subParser('stripLinkDefinitions', function (text, options, globals) { + 'use strict'; + + var regex = /^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm, + base64Regex = /^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm; + + // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug + text += '¨0'; + + var replaceFunc = function (wholeMatch, linkId, url, width, height, blankLines, title) { + linkId = linkId.toLowerCase(); + if (url.match(/^data:.+?\/.+?;base64,/)) { + // remove newlines + globals.gUrls[linkId] = url.replace(/\s/g, ''); + } else { + globals.gUrls[linkId] = showdown.subParser('encodeAmpsAndAngles')(url, options, globals); // Link IDs are case-insensitive + } + + if (blankLines) { + // Oops, found blank lines, so it's not a title. + // Put back the parenthetical statement we stole. + return blankLines + title; + + } else { + if (title) { + globals.gTitles[linkId] = title.replace(/"|'/g, '"'); + } + if (options.parseImgDimensions && width && height) { + globals.gDimensions[linkId] = { + width: width, + height: height + }; + } + } + // Completely remove the definition from the text + return ''; + }; + + // first we try to find base64 link references + text = text.replace(base64Regex, replaceFunc); + + text = text.replace(regex, replaceFunc); + + // attacklab: strip sentinel + text = text.replace(/¨0/, ''); + + return text; +}); + +showdown.subParser('tables', function (text, options, globals) { + 'use strict'; + + if (!options.tables) { + return text; + } + + var tableRgx = /^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|¨0)/gm, + //singeColTblRgx = /^ {0,3}\|.+\|\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n(?: {0,3}\|.+\|\n)+(?:\n\n|¨0)/gm; + singeColTblRgx = /^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm; + + function parseStyles (sLine) { + if (/^:[ \t]*--*$/.test(sLine)) { + return ' style="text-align:left;"'; + } else if (/^--*[ \t]*:[ \t]*$/.test(sLine)) { + return ' style="text-align:right;"'; + } else if (/^:[ \t]*--*[ \t]*:$/.test(sLine)) { + return ' style="text-align:center;"'; + } else { + return ''; + } + } + + function parseHeaders (header, style) { + var id = ''; + header = header.trim(); + // support both tablesHeaderId and tableHeaderId due to error in documentation so we don't break backwards compatibility + if (options.tablesHeaderId || options.tableHeaderId) { + id = ' id="' + header.replace(/ /g, '_').toLowerCase() + '"'; + } + header = showdown.subParser('spanGamut')(header, options, globals); + + return '' + header + '\n'; + } + + function parseCells (cell, style) { + var subText = showdown.subParser('spanGamut')(cell, options, globals); + return '' + subText + '\n'; + } + + function buildTable (headers, cells) { + var tb = '\n\n\n', + tblLgn = headers.length; + + for (var i = 0; i < tblLgn; ++i) { + tb += headers[i]; + } + tb += '\n\n\n'; + + for (i = 0; i < cells.length; ++i) { + tb += '\n'; + for (var ii = 0; ii < tblLgn; ++ii) { + tb += cells[i][ii]; + } + tb += '\n'; + } + tb += '\n
      \n'; + return tb; + } + + function parseTable (rawTable) { + var i, tableLines = rawTable.split('\n'); + + for (i = 0; i < tableLines.length; ++i) { + // strip wrong first and last column if wrapped tables are used + if (/^ {0,3}\|/.test(tableLines[i])) { + tableLines[i] = tableLines[i].replace(/^ {0,3}\|/, ''); + } + if (/\|[ \t]*$/.test(tableLines[i])) { + tableLines[i] = tableLines[i].replace(/\|[ \t]*$/, ''); + } + // parse code spans first, but we only support one line code spans + tableLines[i] = showdown.subParser('codeSpans')(tableLines[i], options, globals); + } + + var rawHeaders = tableLines[0].split('|').map(function (s) { return s.trim();}), + rawStyles = tableLines[1].split('|').map(function (s) { return s.trim();}), + rawCells = [], + headers = [], + styles = [], + cells = []; + + tableLines.shift(); + tableLines.shift(); + + for (i = 0; i < tableLines.length; ++i) { + if (tableLines[i].trim() === '') { + continue; + } + rawCells.push( + tableLines[i] + .split('|') + .map(function (s) { + return s.trim(); + }) + ); + } + + if (rawHeaders.length < rawStyles.length) { + return rawTable; + } + + for (i = 0; i < rawStyles.length; ++i) { + styles.push(parseStyles(rawStyles[i])); + } + + for (i = 0; i < rawHeaders.length; ++i) { + if (showdown.helper.isUndefined(styles[i])) { + styles[i] = ''; + } + headers.push(parseHeaders(rawHeaders[i], styles[i])); + } + + for (i = 0; i < rawCells.length; ++i) { + var row = []; + for (var ii = 0; ii < headers.length; ++ii) { + if (showdown.helper.isUndefined(rawCells[i][ii])) { + + } + row.push(parseCells(rawCells[i][ii], styles[ii])); + } + cells.push(row); + } + + return buildTable(headers, cells); + } + + text = globals.converter._dispatch('tables.before', text, options, globals); + + // find escaped pipe characters + text = text.replace(/\\(\|)/g, showdown.helper.escapeCharactersCallback); + + // parse multi column tables + text = text.replace(tableRgx, parseTable); + + // parse one column tables + text = text.replace(singeColTblRgx, parseTable); + + text = globals.converter._dispatch('tables.after', text, options, globals); + + return text; +}); + +showdown.subParser('underline', function (text, options, globals) { + 'use strict'; + + if (!options.underline) { + return text; + } + + text = globals.converter._dispatch('underline.before', text, options, globals); + + if (options.literalMidWordUnderscores) { + text = text.replace(/\b___(\S[\s\S]*?)___\b/g, function (wm, txt) { + return '' + txt + ''; + }); + text = text.replace(/\b__(\S[\s\S]*?)__\b/g, function (wm, txt) { + return '' + txt + ''; + }); + } else { + text = text.replace(/___(\S[\s\S]*?)___/g, function (wm, m) { + return (/\S$/.test(m)) ? '' + m + '' : wm; + }); + text = text.replace(/__(\S[\s\S]*?)__/g, function (wm, m) { + return (/\S$/.test(m)) ? '' + m + '' : wm; + }); + } + + // escape remaining underscores to prevent them being parsed by italic and bold + text = text.replace(/(_)/g, showdown.helper.escapeCharactersCallback); + + text = globals.converter._dispatch('underline.after', text, options, globals); + + return text; +}); + +/** + * Swap back in all the special characters we've hidden. + */ +showdown.subParser('unescapeSpecialChars', function (text, options, globals) { + 'use strict'; + text = globals.converter._dispatch('unescapeSpecialChars.before', text, options, globals); + + text = text.replace(/¨E(\d+)E/g, function (wholeMatch, m1) { + var charCodeToReplace = parseInt(m1); + return String.fromCharCode(charCodeToReplace); + }); + + text = globals.converter._dispatch('unescapeSpecialChars.after', text, options, globals); + return text; +}); + +showdown.subParser('makeMarkdown.blockquote', function (node, globals) { + 'use strict'; + + var txt = ''; + if (node.hasChildNodes()) { + var children = node.childNodes, + childrenLength = children.length; + + for (var i = 0; i < childrenLength; ++i) { + var innerTxt = showdown.subParser('makeMarkdown.node')(children[i], globals); + + if (innerTxt === '') { + continue; + } + txt += innerTxt; + } + } + // cleanup + txt = txt.trim(); + txt = '> ' + txt.split('\n').join('\n> '); + return txt; +}); + +showdown.subParser('makeMarkdown.codeBlock', function (node, globals) { + 'use strict'; + + var lang = node.getAttribute('language'), + num = node.getAttribute('precodenum'); + return '```' + lang + '\n' + globals.preList[num] + '\n```'; +}); + +showdown.subParser('makeMarkdown.codeSpan', function (node) { + 'use strict'; + + return '`' + node.innerHTML + '`'; +}); + +showdown.subParser('makeMarkdown.emphasis', function (node, globals) { + 'use strict'; + + var txt = ''; + if (node.hasChildNodes()) { + txt += '*'; + var children = node.childNodes, + childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + txt += '*'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.header', function (node, globals, headerLevel) { + 'use strict'; + + var headerMark = new Array(headerLevel + 1).join('#'), + txt = ''; + + if (node.hasChildNodes()) { + txt = headerMark + ' '; + var children = node.childNodes, + childrenLength = children.length; + + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + } + return txt; +}); + +showdown.subParser('makeMarkdown.hr', function () { + 'use strict'; + + return '---'; +}); + +showdown.subParser('makeMarkdown.image', function (node) { + 'use strict'; + + var txt = ''; + if (node.hasAttribute('src')) { + txt += '![' + node.getAttribute('alt') + ']('; + txt += '<' + node.getAttribute('src') + '>'; + if (node.hasAttribute('width') && node.hasAttribute('height')) { + txt += ' =' + node.getAttribute('width') + 'x' + node.getAttribute('height'); + } + + if (node.hasAttribute('title')) { + txt += ' "' + node.getAttribute('title') + '"'; + } + txt += ')'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.links', function (node, globals) { + 'use strict'; + + var txt = ''; + if (node.hasChildNodes() && node.hasAttribute('href')) { + var children = node.childNodes, + childrenLength = children.length; + txt = '['; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + txt += ']('; + txt += '<' + node.getAttribute('href') + '>'; + if (node.hasAttribute('title')) { + txt += ' "' + node.getAttribute('title') + '"'; + } + txt += ')'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.list', function (node, globals, type) { + 'use strict'; + + var txt = ''; + if (!node.hasChildNodes()) { + return ''; + } + var listItems = node.childNodes, + listItemsLenght = listItems.length, + listNum = node.getAttribute('start') || 1; + + for (var i = 0; i < listItemsLenght; ++i) { + if (typeof listItems[i].tagName === 'undefined' || listItems[i].tagName.toLowerCase() !== 'li') { + continue; + } + + // define the bullet to use in list + var bullet = ''; + if (type === 'ol') { + bullet = listNum.toString() + '. '; + } else { + bullet = '- '; + } + + // parse list item + txt += bullet + showdown.subParser('makeMarkdown.listItem')(listItems[i], globals); + ++listNum; + } + + // add comment at the end to prevent consecutive lists to be parsed as one + txt += '\n\n'; + return txt.trim(); +}); + +showdown.subParser('makeMarkdown.listItem', function (node, globals) { + 'use strict'; + + var listItemTxt = ''; + + var children = node.childNodes, + childrenLenght = children.length; + + for (var i = 0; i < childrenLenght; ++i) { + listItemTxt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + // if it's only one liner, we need to add a newline at the end + if (!/\n$/.test(listItemTxt)) { + listItemTxt += '\n'; + } else { + // it's multiparagraph, so we need to indent + listItemTxt = listItemTxt + .split('\n') + .join('\n ') + .replace(/^ {4}$/gm, '') + .replace(/\n\n+/g, '\n\n'); + } + + return listItemTxt; +}); + + + +showdown.subParser('makeMarkdown.node', function (node, globals, spansOnly) { + 'use strict'; + + spansOnly = spansOnly || false; + + var txt = ''; + + // edge case of text without wrapper paragraph + if (node.nodeType === 3) { + return showdown.subParser('makeMarkdown.txt')(node, globals); + } + + // HTML comment + if (node.nodeType === 8) { + return '\n\n'; + } + + // process only node elements + if (node.nodeType !== 1) { + return ''; + } + + var tagName = node.tagName.toLowerCase(); + + switch (tagName) { + + // + // BLOCKS + // + case 'h1': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 1) + '\n\n'; } + break; + case 'h2': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 2) + '\n\n'; } + break; + case 'h3': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 3) + '\n\n'; } + break; + case 'h4': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 4) + '\n\n'; } + break; + case 'h5': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 5) + '\n\n'; } + break; + case 'h6': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.header')(node, globals, 6) + '\n\n'; } + break; + + case 'p': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.paragraph')(node, globals) + '\n\n'; } + break; + + case 'blockquote': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.blockquote')(node, globals) + '\n\n'; } + break; + + case 'hr': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.hr')(node, globals) + '\n\n'; } + break; + + case 'ol': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.list')(node, globals, 'ol') + '\n\n'; } + break; + + case 'ul': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.list')(node, globals, 'ul') + '\n\n'; } + break; + + case 'precode': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.codeBlock')(node, globals) + '\n\n'; } + break; + + case 'pre': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.pre')(node, globals) + '\n\n'; } + break; + + case 'table': + if (!spansOnly) { txt = showdown.subParser('makeMarkdown.table')(node, globals) + '\n\n'; } + break; + + // + // SPANS + // + case 'code': + txt = showdown.subParser('makeMarkdown.codeSpan')(node, globals); + break; + + case 'em': + case 'i': + txt = showdown.subParser('makeMarkdown.emphasis')(node, globals); + break; + + case 'strong': + case 'b': + txt = showdown.subParser('makeMarkdown.strong')(node, globals); + break; + + case 'del': + txt = showdown.subParser('makeMarkdown.strikethrough')(node, globals); + break; + + case 'a': + txt = showdown.subParser('makeMarkdown.links')(node, globals); + break; + + case 'img': + txt = showdown.subParser('makeMarkdown.image')(node, globals); + break; + + default: + txt = node.outerHTML + '\n\n'; + } + + // common normalization + // TODO eventually + + return txt; +}); + +showdown.subParser('makeMarkdown.paragraph', function (node, globals) { + 'use strict'; + + var txt = ''; + if (node.hasChildNodes()) { + var children = node.childNodes, + childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + } + + // some text normalization + txt = txt.trim(); + + return txt; +}); + +showdown.subParser('makeMarkdown.pre', function (node, globals) { + 'use strict'; + + var num = node.getAttribute('prenum'); + return '
      ' + globals.preList[num] + '
      '; +}); + +showdown.subParser('makeMarkdown.strikethrough', function (node, globals) { + 'use strict'; + + var txt = ''; + if (node.hasChildNodes()) { + txt += '~~'; + var children = node.childNodes, + childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + txt += '~~'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.strong', function (node, globals) { + 'use strict'; + + var txt = ''; + if (node.hasChildNodes()) { + txt += '**'; + var children = node.childNodes, + childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals); + } + txt += '**'; + } + return txt; +}); + +showdown.subParser('makeMarkdown.table', function (node, globals) { + 'use strict'; + + var txt = '', + tableArray = [[], []], + headings = node.querySelectorAll('thead>tr>th'), + rows = node.querySelectorAll('tbody>tr'), + i, ii; + for (i = 0; i < headings.length; ++i) { + var headContent = showdown.subParser('makeMarkdown.tableCell')(headings[i], globals), + allign = '---'; + + if (headings[i].hasAttribute('style')) { + var style = headings[i].getAttribute('style').toLowerCase().replace(/\s/g, ''); + switch (style) { + case 'text-align:left;': + allign = ':---'; + break; + case 'text-align:right;': + allign = '---:'; + break; + case 'text-align:center;': + allign = ':---:'; + break; + } + } + tableArray[0][i] = headContent.trim(); + tableArray[1][i] = allign; + } + + for (i = 0; i < rows.length; ++i) { + var r = tableArray.push([]) - 1, + cols = rows[i].getElementsByTagName('td'); + + for (ii = 0; ii < headings.length; ++ii) { + var cellContent = ' '; + if (typeof cols[ii] !== 'undefined') { + cellContent = showdown.subParser('makeMarkdown.tableCell')(cols[ii], globals); + } + tableArray[r].push(cellContent); + } + } + + var cellSpacesCount = 3; + for (i = 0; i < tableArray.length; ++i) { + for (ii = 0; ii < tableArray[i].length; ++ii) { + var strLen = tableArray[i][ii].length; + if (strLen > cellSpacesCount) { + cellSpacesCount = strLen; + } + } + } + + for (i = 0; i < tableArray.length; ++i) { + for (ii = 0; ii < tableArray[i].length; ++ii) { + if (i === 1) { + if (tableArray[i][ii].slice(-1) === ':') { + tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii].slice(-1), cellSpacesCount - 1, '-') + ':'; + } else { + tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount, '-'); + } + } else { + tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount); + } + } + txt += '| ' + tableArray[i].join(' | ') + ' |\n'; + } + + return txt.trim(); +}); + +showdown.subParser('makeMarkdown.tableCell', function (node, globals) { + 'use strict'; + + var txt = ''; + if (!node.hasChildNodes()) { + return ''; + } + var children = node.childNodes, + childrenLength = children.length; + + for (var i = 0; i < childrenLength; ++i) { + txt += showdown.subParser('makeMarkdown.node')(children[i], globals, true); + } + return txt.trim(); +}); + +showdown.subParser('makeMarkdown.txt', function (node) { + 'use strict'; + + var txt = node.nodeValue; + + // multiple spaces are collapsed + txt = txt.replace(/ +/g, ' '); + + // replace the custom ¨NBSP; with a space + txt = txt.replace(/¨NBSP;/g, ' '); + + // ", <, > and & should replace escaped html entities + txt = showdown.helper.unescapeHTMLEntities(txt); + + // escape markdown magic characters + // emphasis, strong and strikethrough - can appear everywhere + // we also escape pipe (|) because of tables + // and escape ` because of code blocks and spans + txt = txt.replace(/([*_~|`])/g, '\\$1'); + + // escape > because of blockquotes + txt = txt.replace(/^(\s*)>/g, '\\$1>'); + + // hash character, only troublesome at the beginning of a line because of headers + txt = txt.replace(/^#/gm, '\\#'); + + // horizontal rules + txt = txt.replace(/^(\s*)([-=]{3,})(\s*)$/, '$1\\$2$3'); + + // dot, because of ordered lists, only troublesome at the beginning of a line when preceded by an integer + txt = txt.replace(/^( {0,3}\d+)\./gm, '$1\\.'); + + // +, * and -, at the beginning of a line becomes a list, so we need to escape them also (asterisk was already escaped) + txt = txt.replace(/^( {0,3})([+-])/gm, '$1\\$2'); + + // images and links, ] followed by ( is problematic, so we escape it + txt = txt.replace(/]([\s]*)\(/g, '\\]$1\\('); + + // reference URIs must also be escaped + txt = txt.replace(/^ {0,3}\[([\S \t]*?)]:/gm, '\\[$1]:'); + + return txt; +}); + +var root = this; + +// AMD Loader +if (typeof define === 'function' && define.amd) { + define(function () { + 'use strict'; + return showdown; + }); + +// CommonJS/nodeJS Loader +} else if (typeof module !== 'undefined' && module.exports) { + module.exports = showdown; + +// Regular Browser loader +} else { + root.showdown = showdown; +} +}).call(this); + +//# sourceMappingURL=showdown.js.map diff --git a/octoprint_octolapse/static/js/webcams/mjpg_streamer/raspi_cam_v2.js b/octoprint_octolapse/static/js/webcams/mjpg_streamer/raspi_cam_v2.js index 1c6d69af..061d47d7 100644 --- a/octoprint_octolapse/static/js/webcams/mjpg_streamer/raspi_cam_v2.js +++ b/octoprint_octolapse/static/js/webcams/mjpg_streamer/raspi_cam_v2.js @@ -1,7 +1,7 @@ /* ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2019 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published diff --git a/octoprint_octolapse/templates/octolapse_dialog.jinja2 b/octoprint_octolapse/templates/octolapse_dialog.jinja2 new file mode 100644 index 00000000..be4c4c75 --- /dev/null +++ b/octoprint_octolapse/templates/octolapse_dialog.jinja2 @@ -0,0 +1,66 @@ + + diff --git a/octoprint_octolapse/templates/octolapse_dialog_rendering_in_process.jinja2 b/octoprint_octolapse/templates/octolapse_dialog_rendering_in_process.jinja2 new file mode 100644 index 00000000..6daee418 --- /dev/null +++ b/octoprint_octolapse/templates/octolapse_dialog_rendering_in_process.jinja2 @@ -0,0 +1,59 @@ + + + + + + + + + + + + + diff --git a/octoprint_octolapse/templates/octolapse_dialog_rendering_unfinished.jinja2 b/octoprint_octolapse/templates/octolapse_dialog_rendering_unfinished.jinja2 new file mode 100644 index 00000000..da597278 --- /dev/null +++ b/octoprint_octolapse/templates/octolapse_dialog_rendering_unfinished.jinja2 @@ -0,0 +1,96 @@ + + + + + + + + diff --git a/octoprint_octolapse/templates/octolapse_dialog_timelapse_files.jinja2 b/octoprint_octolapse/templates/octolapse_dialog_timelapse_files.jinja2 new file mode 100644 index 00000000..dd729999 --- /dev/null +++ b/octoprint_octolapse/templates/octolapse_dialog_timelapse_files.jinja2 @@ -0,0 +1,91 @@ + + + + + + + +{% include "octolapse_file_browser.jinja2" %} + diff --git a/octoprint_octolapse/templates/octolapse_file_browser.jinja2 b/octoprint_octolapse/templates/octolapse_file_browser.jinja2 new file mode 100644 index 00000000..4a61359b --- /dev/null +++ b/octoprint_octolapse/templates/octolapse_file_browser.jinja2 @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + diff --git a/octoprint_octolapse/templates/octolapse_helpers.jinja2 b/octoprint_octolapse/templates/octolapse_helpers.jinja2 new file mode 100644 index 00000000..3d4856b2 --- /dev/null +++ b/octoprint_octolapse/templates/octolapse_helpers.jinja2 @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/octoprint_octolapse/templates/octolapse_navbar.jinja2 b/octoprint_octolapse/templates/octolapse_navbar.jinja2 index d7a1c8a6..e064d61d 100644 --- a/octoprint_octolapse/templates/octolapse_navbar.jinja2 +++ b/octoprint_octolapse/templates/octolapse_navbar.jinja2 @@ -1,7 +1,7 @@ -
      - + diff --git a/octoprint_octolapse/templates/octolapse_profile_dialog_add_edit.jinja2 b/octoprint_octolapse/templates/octolapse_profile_dialog_add_edit.jinja2 index f2d6c885..5b990a83 100644 --- a/octoprint_octolapse/templates/octolapse_profile_dialog_add_edit.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profile_dialog_add_edit.jinja2 @@ -1,7 +1,7 @@ @@ -75,7 +74,7 @@  |   |  + data-bind="attr: {href: '/plugin/octolapse/downloadFile?type=profile&apikey=' + UI_API_KEY + '&guid=' + guid() + '&profile_type=' + $root.profileTypeName()}" download="">  |  @@ -84,7 +83,7 @@ -
      +
      diff --git a/octoprint_octolapse/templates/octolapse_profiles_camera.jinja2 b/octoprint_octolapse/templates/octolapse_profiles_camera.jinja2 index 3253f32e..32c3b073 100644 --- a/octoprint_octolapse/templates/octolapse_profiles_camera.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profiles_camera.jinja2 @@ -1,7 +1,7 @@ + + + + + + If required, enter a password to be sent with the camera snapshot request. Attention Be sure you are using HTTPS when setting a password and that your camera URL is secure to keep your password secure! + +
      +
      + + +
      @@ -149,36 +244,43 @@
      - + MS - -
      +
      +
      Octolapse will wait this long (in milliseconds) to get an image from your camera, else it will timeout and the print will continue. The timeout doesn't start until after the camera delay specified above.
      -
      +
      - - -
      + +
      +
      Optionally rotate, mirror, or flip or transpose your snapshots. Requires some additional power from your CPU. Not recommended for slower hardware. Does NOT affect your camera stream and is NOT affected by webcam rotation in the Octoprint webcam settings.
      @@ -187,54 +289,112 @@

      Custom Camera Scripts

      - +
      +
      Gcode Scripts
      +
      + +
      + + +
      + Gcode sent before the snapshot is taken. +
      +
      +
      + +
      + + +
      + Gcode sent after the snapshot is taken. +
      +
      +
      - - - -
      + +
      + +
      Octolapse will execute any script path entered here at the start of a print.
      - - -
      + +
      + +
      Octolapse will execute any script path entered here after the printer has stabilized but before taking a snapshot.
      - - -
      + +
      + +
      Octolapse will execute any script path entered here after a snapshot is complete.
      - - -
      + +
      + +
      Octolapse will execute any script path entered here right before rendering. This is useful to transfer images from a remote location (DSLR or printer), or to apply custom filters.
      - - -
      + +
      + +
      Octolapse will execute any script path entered here after rendering is complete.
      +
      + +
      + + + +
      + Octolapse will execute any script path entered here after the print has completed. +
      +
      @@ -246,7 +406,7 @@

      It is highly recommended that you take the time to configure these settings since they can make a big difference in the quality of your timelapse, and reduce the required snapshot delay.

      -

      Custom image preferences are only available for USB web-cameras running on mjpg_streamer (the raspberry pi camera module is not currently supported). Consider using a custom 'Before Print Start Script' or try using a web camera running on mjpg_streamer.

      +

      Custom image preferences are only available when using the 'Webcam' Camera Type setting above. It may be possible to use the 'Before Print Start Script' script above to configure your camera.

      @@ -255,9 +415,9 @@
      Enable webcam image adjustments. Currently this only works with mjpg-streamer.
      @@ -265,92 +425,50 @@ - -
      -
      - - -
      - Loading the webcam stream... +
      +
      +
      -
      -
      -
      - -
      - -
      -
      -
      +
      {% include "octolapse_profiles_camera_webcam.jinja2" %} diff --git a/octoprint_octolapse/templates/octolapse_profiles_camera_webcam.jinja2 b/octoprint_octolapse/templates/octolapse_profiles_camera_webcam.jinja2 index b041e02d..8e6e25ca 100644 --- a/octoprint_octolapse/templates/octolapse_profiles_camera_webcam.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profiles_camera_webcam.jinja2 @@ -1,7 +1,7 @@ -{% include "/webcams/mjpg_streamer/logitech_c920.jinja2" %} -{% include "/webcams/mjpg_streamer/raspi_cam_v2.jinja2" %} +{% include "plugin_octolapse/webcams/mjpg_streamer/logitech_c920.jinja2" %} +{% include "plugin_octolapse/webcams/mjpg_streamer/raspi_cam_v2.jinja2" %} +{% include "plugin_octolapse/webcams/mjpg_streamer/logitech_c250.jinja2" %} diff --git a/octoprint_octolapse/templates/octolapse_profiles_library.jinja2 b/octoprint_octolapse/templates/octolapse_profiles_library.jinja2 index 879e015a..a6fcc134 100644 --- a/octoprint_octolapse/templates/octolapse_profiles_library.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profiles_library.jinja2 @@ -1,3 +1,26 @@ + diff --git a/octoprint_octolapse/templates/octolapse_profiles_debug.jinja2 b/octoprint_octolapse/templates/octolapse_profiles_logging.jinja2 similarity index 62% rename from octoprint_octolapse/templates/octolapse_profiles_debug.jinja2 rename to octoprint_octolapse/templates/octolapse_profiles_logging.jinja2 index 67992a28..d30a006b 100644 --- a/octoprint_octolapse/templates/octolapse_profiles_debug.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profiles_logging.jinja2 @@ -1,7 +1,7 @@ - diff --git a/octoprint_octolapse/templates/octolapse_profiles_printer.jinja2 b/octoprint_octolapse/templates/octolapse_profiles_printer.jinja2 index 3a58fb6b..c2d44efe 100644 --- a/octoprint_octolapse/templates/octolapse_profiles_printer.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profiles_printer.jinja2 @@ -1,7 +1,7 @@ - {% include "octolapse_profiles_printer_slicer_other.jinja2" %} {% include "octolapse_profiles_printer_slicer_slic3r_pe.jinja2" %} diff --git a/octoprint_octolapse/templates/octolapse_profiles_printer_automatic.jinja2 b/octoprint_octolapse/templates/octolapse_profiles_printer_automatic.jinja2 index 6a81ae23..eaa9960d 100644 --- a/octoprint_octolapse/templates/octolapse_profiles_printer_automatic.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profiles_printer_automatic.jinja2 @@ -1,3 +1,26 @@ + diff --git a/octoprint_octolapse/templates/octolapse_profiles_printer_slicer_cura.jinja2 b/octoprint_octolapse/templates/octolapse_profiles_printer_slicer_cura.jinja2 index b4f7e87d..c33be5fb 100644 --- a/octoprint_octolapse/templates/octolapse_profiles_printer_slicer_cura.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profiles_printer_slicer_cura.jinja2 @@ -1,173 +1,230 @@ - diff --git a/octoprint_octolapse/templates/octolapse_profiles_printer_slicer_simplify3d.jinja2 b/octoprint_octolapse/templates/octolapse_profiles_printer_slicer_simplify3d.jinja2 index 0eb18c43..6c224f07 100644 --- a/octoprint_octolapse/templates/octolapse_profiles_printer_slicer_simplify3d.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profiles_printer_slicer_simplify3d.jinja2 @@ -1,128 +1,188 @@ - diff --git a/octoprint_octolapse/templates/octolapse_profiles_stabilization.jinja2 b/octoprint_octolapse/templates/octolapse_profiles_stabilization.jinja2 index 8fba27e0..83f660a9 100644 --- a/octoprint_octolapse/templates/octolapse_profiles_stabilization.jinja2 +++ b/octoprint_octolapse/templates/octolapse_profiles_stabilization.jinja2 @@ -1,7 +1,7 @@ + + diff --git a/octoprint_octolapse/templates/octolapse_snapshot_plan.jinja2 b/octoprint_octolapse/templates/octolapse_snapshot_plan.jinja2 new file mode 100644 index 00000000..9fbc684f --- /dev/null +++ b/octoprint_octolapse/templates/octolapse_snapshot_plan.jinja2 @@ -0,0 +1,149 @@ + + + diff --git a/octoprint_octolapse/templates/octolapse_status_extruder.jinja2 b/octoprint_octolapse/templates/octolapse_status_extruder.jinja2 index 3b5b3ebb..97c89adc 100644 --- a/octoprint_octolapse/templates/octolapse_status_extruder.jinja2 +++ b/octoprint_octolapse/templates/octolapse_status_extruder.jinja2 @@ -1,7 +1,7 @@ - diff --git a/octoprint_octolapse/templates/octolapse_status_triggers.jinja2 b/octoprint_octolapse/templates/octolapse_status_triggers.jinja2 index 986e1322..b696aa9f 100644 --- a/octoprint_octolapse/templates/octolapse_status_triggers.jinja2 +++ b/octoprint_octolapse/templates/octolapse_status_triggers.jinja2 @@ -1,7 +1,7 @@ + - - diff --git a/octoprint_octolapse/templates/octolapse_tab.jinja2 b/octoprint_octolapse/templates/octolapse_tab.jinja2 index 6677fb5d..381d6d22 100644 --- a/octoprint_octolapse/templates/octolapse_tab.jinja2 +++ b/octoprint_octolapse/templates/octolapse_tab.jinja2 @@ -1,7 +1,7 @@ +
      +
      +
      +
      - - {% include "octolapse_status_triggers.jinja2" %} - {% include "octolapse_status_extruder.jinja2" %} - {% include "octolapse_status_position.jinja2" %} - {% include "octolapse_status_printer_state.jinja2" %} - {% include "octolapse_status_snapshot_plan.jinja2" %} - {% include "octolapse_tab_latest_snapshot.jinja2" %} - {% include "octolapse_tab_settings_current.jinja2" %} - {% include "octolapse_tab_webcam_settings_popup.jinja2" %} - {% include "octolapse_tab_snapshot_plan_preview_popup.jinja2" %} - - +{% include "octolapse_status_triggers.jinja2" %} +{% include "octolapse_status_extruder.jinja2" %} +{% include "octolapse_status_position.jinja2" %} +{% include "octolapse_status_printer_state.jinja2" %} +{% include "octolapse_status_snapshot_plan.jinja2" %} +{% include "octolapse_snapshot_plan.jinja2" %} +{% include "octolapse_tab_latest_snapshot.jinja2" %} +{% include "octolapse_tab_settings_current.jinja2" %} +{% include "octolapse_tab_webcam_settings_popup.jinja2" %} +{% include "octolapse_tab_snapshot_plan_preview_popup.jinja2" %} +{% include "octolapse_dialog_rendering_unfinished.jinja2" %} +{% include "octolapse_dialog_rendering_in_process.jinja2" %} +{% include "octolapse_dialog_timelapse_files.jinja2" %} +{% include "octolapse_dialog.jinja2" %} +{% include "octolapse_helpers.jinja2" %} diff --git a/octoprint_octolapse/templates/octolapse_tab_latest_snapshot.jinja2 b/octoprint_octolapse/templates/octolapse_tab_latest_snapshot.jinja2 index eb4baa1f..09fbb724 100644 --- a/octoprint_octolapse/templates/octolapse_tab_latest_snapshot.jinja2 +++ b/octoprint_octolapse/templates/octolapse_tab_latest_snapshot.jinja2 @@ -1,7 +1,7 @@ diff --git a/octoprint_octolapse/templates/octolapse_tab_settings_current.jinja2 b/octoprint_octolapse/templates/octolapse_tab_settings_current.jinja2 index 38eeb055..56faf6da 100644 --- a/octoprint_octolapse/templates/octolapse_tab_settings_current.jinja2 +++ b/octoprint_octolapse/templates/octolapse_tab_settings_current.jinja2 @@ -1,7 +1,7 @@ diff --git a/octoprint_octolapse/templates/octolapse_tab_settings_current_cameras.jinja2 b/octoprint_octolapse/templates/octolapse_tab_settings_current_cameras.jinja2 index 9ef6c9a0..1b4ab92c 100644 --- a/octoprint_octolapse/templates/octolapse_tab_settings_current_cameras.jinja2 +++ b/octoprint_octolapse/templates/octolapse_tab_settings_current_cameras.jinja2 @@ -1,36 +1,66 @@ - + diff --git a/octoprint_octolapse/templates/octolapse_tab_snapshot_plan_preview_popup.jinja2 b/octoprint_octolapse/templates/octolapse_tab_snapshot_plan_preview_popup.jinja2 index 4c000b80..e40356a3 100644 --- a/octoprint_octolapse/templates/octolapse_tab_snapshot_plan_preview_popup.jinja2 +++ b/octoprint_octolapse/templates/octolapse_tab_snapshot_plan_preview_popup.jinja2 @@ -1,7 +1,7 @@ diff --git a/octoprint_octolapse/templates/webcams/mjpg_streamer/logitech_c920.jinja2 b/octoprint_octolapse/templates/webcams/mjpg_streamer/logitech_c920.jinja2 index 4c4b9f88..fa401101 100644 --- a/octoprint_octolapse/templates/webcams/mjpg_streamer/logitech_c920.jinja2 +++ b/octoprint_octolapse/templates/webcams/mjpg_streamer/logitech_c920.jinja2 @@ -11,34 +11,34 @@ General Settings
      -    +   
      - +
      - +
      -    +   
      - +
      @@ -47,10 +47,10 @@
      -    +   
      - +
      @@ -59,10 +59,10 @@
      -    +   
      - +
      @@ -78,15 +78,15 @@
      -    +   
      - +
      @@ -101,16 +101,16 @@
      -    +   
      - +
      AUTO @@ -126,13 +126,13 @@
      -    +   
      +
      @@ -158,10 +158,10 @@
      -    +   
      - +
      @@ -175,7 +175,7 @@
      @@ -187,10 +187,10 @@ Pan, Tilt, and Zoom
      -    +   
      - +
      @@ -199,10 +199,10 @@
      -    +   
      - +
      @@ -211,10 +211,10 @@
      -    +   
      - +
      @@ -229,7 +229,7 @@ Misc
      -    +   
      +
      @@ -23,10 +23,10 @@
      -    +   
      - +
      @@ -35,10 +35,10 @@
      -    +   
      - +
      @@ -47,10 +47,10 @@
      -    +   
      - +
      @@ -59,10 +59,10 @@
      -    +   
      - +
      @@ -71,10 +71,10 @@
      -    +   
      - +
      @@ -83,13 +83,12 @@
      -
      Effects
      -    +   
      +
      disabled @@ -122,22 +121,22 @@
      -    +   
      - +
      @@ -151,7 +150,7 @@ Balance
      -    +   
      +
      @@ -177,10 +176,10 @@
      -    +   
      - +
      @@ -195,7 +194,7 @@ Codec Controls
      -    +   
      +
      @@ -221,16 +220,16 @@
      -    +   
      - +
      @@ -239,7 +238,7 @@
      -    +   
      Exposure
      -    +   
      -    +   
      - +
      @@ -313,15 +312,15 @@
      -    +   
      - +
      @@ -332,14 +331,14 @@
      -    +   
      +
      @@ -367,7 +366,7 @@
      -    +   
      v0.4.0rc... Always + # 0.4.0rc... > 0.4.0rc.dev... + # 0.4.0rc2... > 0.4.0rc1... + + # test stripping off commit level version info + test_version = NumberedVersion('0.4.0rc1+u.dec65f5') + # make sure identical version numbers are considered to be equal + assert ( + NumberedVersion('0.4.0rc1') == NumberedVersion('0.4.0rc1') + ) + + # make sure V prefixes are ignored version numbers are considered to be equal + assert ( + NumberedVersion('v0.4.0rc1') == NumberedVersion('0.4.0rc1') + ) + assert ( + NumberedVersion('V0.4.0rc1') == NumberedVersion('0.4.0rc1') + ) + assert ( + NumberedVersion('0.4.0rc1') == NumberedVersion('v0.4.0rc1') + ) + assert ( + NumberedVersion('0.4.0rc1') == NumberedVersion('V0.4.0rc1') + ) + assert not ( + NumberedVersion('v0.4.0rc1') < NumberedVersion('0.4.0rc1') + ) + + # make sure that rc is always greater than rcX.devX + assert ( + NumberedVersion('0.4.0rc1.dev1') < NumberedVersion('0.4.0rc1') + ) + assert not ( + NumberedVersion('0.4.0rc1.dev1') > NumberedVersion('0.4.0rc1') + ) + assert not ( + NumberedVersion('0.4.0rc1.dev1') == NumberedVersion('0.4.0rc1') + ) + assert ( + NumberedVersion('0.4.0rc1') > NumberedVersion('0.4.0rc1.dev1') + ) + assert not ( + NumberedVersion('0.4.0rc1') < NumberedVersion('0.4.0rc1.dev1') + ) + assert not ( + NumberedVersion('0.4.0rc1') == NumberedVersion('0.4.0rc1.dev1') + ) + + + # Make sure that release versions are always greater than non-release versions + assert ( + NumberedVersion('0.4.0') > NumberedVersion('0.4.0rc1') + ) + assert not ( + NumberedVersion('0.4.0') < NumberedVersion('0.4.0rc1') + ) + assert not ( + NumberedVersion('0.4.0') == NumberedVersion('0.4.0rc1') + ) + assert ( + NumberedVersion('0.4.0rc1') < NumberedVersion('0.4.0') + ) + assert not ( + NumberedVersion('0.4.0rc1') > NumberedVersion('0.4.0') + ) + assert not ( + NumberedVersion('0.4.0rc1') == NumberedVersion('0.4.0') + ) + + # make sure that higher tag versions are always considered newer than lower ones + assert ( + NumberedVersion('0.4.1') > NumberedVersion('0.4.0') + ) + assert not ( + NumberedVersion('0.4.1') < NumberedVersion('0.4.0') + ) + assert not( + NumberedVersion('0.4.1') == NumberedVersion('0.4.0') + ) + assert ( + NumberedVersion('0.4.0') < NumberedVersion('0.4.1') + ) + assert not ( + NumberedVersion('0.4.0') > NumberedVersion('0.4.1') + ) + assert not ( + NumberedVersion('0.4.0') == NumberedVersion('0.4.1') + ) + + + # make sure that clean versions are always considered older than dirty versions + assert ( + NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') > NumberedVersion("v0.4.0rc1.dev5+11.g3ffd305") + ) + assert not ( + NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') < NumberedVersion("v0.4.0rc1.dev5+11.g3ffd305") + ) + assert not ( + NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') == NumberedVersion("v0.4.0rc1.dev5+11.g3ffd305") + ) + assert ( + NumberedVersion("v0.4.0rc1.dev5+11.g3ffd305") < NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') + ) + assert not ( + NumberedVersion("v0.4.0rc1.dev5+11.g3ffd305") > NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') + ) + assert not ( + NumberedVersion("v0.4.0rc1.dev5+11.g3ffd305") == NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') + ) + + + # make sure that versions with more commits are higher than those with lower + assert ( + NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') > NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305.dirty") + ) + assert not ( + NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') < NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305.dirty") + ) + assert not ( + NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') == NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305.dirty") + ) + assert ( + NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305.dirty") < NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') + ) + assert not ( + NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305.dirty") > NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') + ) + assert not ( + NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305.dirty") == NumberedVersion('v0.4.0rc1.dev5+11.g3ffd305.dirty') + ) + + + + # When there is not development info within the version, the comparison is ambiguous. Make them equal + assert ( + NumberedVersion('v0.4.0rc1.dev5+u.g3ffd305') == NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305") + ) + assert ( + NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305") == NumberedVersion('v0.4.0rc1.dev5+u.g3ffd305') + ) + + # when there is full tag information (i.e. installed from a release), it is older + assert ( + NumberedVersion('v0.4.0rc1.dev5') < NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305") + ) + assert not ( + NumberedVersion('v0.4.0rc1.dev5') > NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305") + ) + assert not ( + NumberedVersion('v0.4.0rc1.dev5') == NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305") + ) + assert ( + NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305") > NumberedVersion('v0.4.0rc1.dev5') + ) + assert not ( + NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305") < NumberedVersion('v0.4.0rc1.dev5') + ) + assert not ( + NumberedVersion("v0.4.0rc1.dev5+10.g3ffd305") == NumberedVersion('v0.4.0rc1.dev5') + ) + + assert ( + NumberedVersion("v0.4.0rc1.dev2") < NumberedVersion('v0.4.0rc1.dev3') + ) + + assert ( + NumberedVersion('v0.4.0rc1.dev3') > NumberedVersion("v0.4.0rc1.dev2") + ) + + assert ( + NumberedVersion('v0.4.0') > NumberedVersion("v0.4.0rc1.dev2") + ) + assert ( + NumberedVersion("v0.4.0rc1.dev2") < NumberedVersion('v0.4.0') + ) diff --git a/octoprint_octolapse/test/test_octolapseplugin.py b/octoprint_octolapse/test/test_octolapseplugin.py index 0204845e..52b514a1 100644 --- a/octoprint_octolapse/test/test_octolapseplugin.py +++ b/octoprint_octolapse/test/test_octolapseplugin.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published diff --git a/octoprint_octolapse/test/test_parsing.py b/octoprint_octolapse/test/test_parsing.py index 24718390..be98fbab 100644 --- a/octoprint_octolapse/test/test_parsing.py +++ b/octoprint_octolapse/test/test_parsing.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -23,7 +23,7 @@ import time import unittest -from octoprint_octolapse.gcode_parser import Commands, Response +from octoprint_octolapse.gcode_commands import Commands, Response class TestParsing(unittest.TestCase): @@ -44,6 +44,28 @@ def test_unknown_word(self): def test_LocationResponse(self): line = 'ok X:150.0 Y:150.0 Z:0.7 E:0.0' parsed = Response.parse_position_line(line) + self.assertTrue(parsed) + if parsed: + # we don't know T or F when printing from SD since + # there's no way to query it from the firmware and + # no way to track it ourselves when not streaming + # the file - this all sucks sooo much + + x = parsed.get("x") + y = parsed.get("y") + z = parsed.get("z") + e = None + if "e" in parsed: + e = parsed.get("e") + return {'x': x, 'y': y, 'z': z, 'e': e, } + + def test_LocationResponse_E3D_ToolChanger(self): + # Note that C is between Z and E, which doesn't yet work + line = 'X:272.500 Y:140.000 Z:15.000 C:4.600 E:0.000 E0:0.0 Count 66000 21200 24000 7360 Machine 272.500 140.000 15.000 4.600 Bed comp 0.000' + # This adjusted format DOES work currently + #line = 'X:272.500 Y:140.000 Z:15.000 E:0.000 E0:0.0 C:4.600 Count 66000 21200 24000 7360 Machine 272.500 140.000 15.000 4.600 Bed comp 0.000' + parsed = Response.parse_position_line(line) + self.assertTrue(parsed) if parsed: # we don't know T or F when printing from SD since # there's no way to query it from the firmware and diff --git a/octoprint_octolapse/test/test_position.py b/octoprint_octolapse/test/test_position.py index a3617a10..1095c281 100644 --- a/octoprint_octolapse/test/test_position.py +++ b/octoprint_octolapse/test/test_position.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -24,7 +24,7 @@ import unittest import pprint from tempfile import NamedTemporaryFile -from octoprint_octolapse.gcode_parser import Commands +from octoprint_octolapse.gcode_commands import Commands from octoprint_octolapse.position import Pos from octoprint_octolapse.position import Position from octoprint_octolapse.settings import OctolapseSettings, PrinterProfile, SlicerSettings, Slic3rPeSettings @@ -156,108 +156,6 @@ def test_wipe(self): - # send the appropriate start gcodes - - def test_position_error(self): - """Test the IsInBounds function to make sure the program will not attempt to operate after being told to move - out of bounds. """ - position = Position(self.Settings, self.OctoprintPrinterProfile, False) - - # Initial test, should return false without any coordinates - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # home the axis and test - position.update(Commands.parse("G28")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - - # X axis tests - # reset, set relative extruder and absolute xyz, home the axis and test again - position = Position(self.Settings, self.OctoprintPrinterProfile, False) - position.update(Commands.parse("M83")) - position.update(Commands.parse("G90")) - position.update(Commands.parse("G28")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move out of bounds min - position.update(Commands.parse("G0 x-0.0001")) - self.assertTrue(position.has_position_error()) - self.assertTrue(position.position_error() is not None) - # move back in bounds - position.update(Commands.parse("G0 x0.0")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move to middle - position.update(Commands.parse("G0 x125")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move to max - position.update(Commands.parse("G0 x250")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move out of bounds max - position.update(Commands.parse("G0 x250.0001")) - self.assertTrue(position.has_position_error()) - self.assertTrue(position.position_error() is not None) - - # Y axis tests - # reset, set relative extruder and absolute xyz, home the axis and test again - position = Position(self.Settings, self.OctoprintPrinterProfile, False) - position.update(Commands.parse("M83")) - position.update(Commands.parse("G90")) - position.update(Commands.parse("G28")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move out of bounds min - position.update(Commands.parse("G0 y-0.0001")) - self.assertTrue(position.has_position_error()) - self.assertTrue(position.position_error() is not None) - # move back in bounds - position.update(Commands.parse("G0 y0.0")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move to middle - position.update(Commands.parse("G0 y100")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move to max - position.update(Commands.parse("G0 y200")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move out of bounds max - position.update(Commands.parse("G0 y200.0001")) - self.assertTrue(position.has_position_error()) - self.assertTrue(position.position_error() is not None) - - # Z axis tests - # reset, home the axis and test again - # reset, set relative extruder and absolute xyz, home the axis and test again - position = Position(self.Settings, self.OctoprintPrinterProfile, False) - position.update(Commands.parse("M83")) - position.update(Commands.parse("G90")) - position.update(Commands.parse("G28")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move out of bounds min - position.update(Commands.parse("G0 z-0.0001")) - self.assertTrue(position.has_position_error()) - self.assertTrue(position.position_error() is not None) - # move back in bounds - position.update(Commands.parse("G0 z0.0")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move to middle - position.update(Commands.parse("G0 z100")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move to max - position.update(Commands.parse("G0 z200")) - self.assertFalse(position.has_position_error()) - self.assertIsNone(position.position_error()) - # move out of bounds max - position.update(Commands.parse("G0 z200.0001")) - self.assertTrue(position.has_position_error()) - self.assertTrue(position.position_error() is not None) def test_reset(self): """Test init state.""" diff --git a/octoprint_octolapse/test/test_render.py b/octoprint_octolapse/test/test_render.py index fee788c9..7435afb5 100644 --- a/octoprint_octolapse/test/test_render.py +++ b/octoprint_octolapse/test/test_render.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -32,8 +32,14 @@ from random import randint from shutil import rmtree from tempfile import mkdtemp, NamedTemporaryFile +import errno +# Recent versions of Pillow changed the case of the import +# Why!? +try: + from pil import Image +except ImportError: + from PIL import Image -from PIL import Image from mock import Mock from octoprint_octolapse.render import TimelapseRenderJob, Render, Rendering @@ -50,7 +56,14 @@ def createSnapshotDir(n, capture_template=None, size=(50, 50), write_metadata=Tr # Make the temp folder. dir = mkdtemp() + '/' # Make sure any nested folders specified by the capture template exist. - os.makedirs(os.path.dirname("{0}{1}".format(dir, capture_template) % 0)) + try: + os.makedirs(os.path.dirname("{0}{1}".format(dir, capture_template) % 0)) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + raise + # Make images and save them with the correct names. random.seed(0) @@ -82,7 +95,7 @@ def createRenderingJob(self, rendering): self.on_render_error = Mock(return_value=None) return TimelapseRenderJob(job_id=self.rendering_job_id, rendering=rendering, - debug=self.octolapse_settings.current_debug_profile(), + logging=self.octolapse_settings.current_logging_profile(), print_filename=self.print_name, capture_dir=self.snapshot_dir_path, snapshot_filename_format=self.capture_template, @@ -143,7 +156,7 @@ def setUp(self): self.print_end_time = 100 # Create fake snapshots. - self.capture_template = get_snapshot_filename(self.print_name, self.print_start_time, SnapshotNumberFormat) + self.capture_template = get_snapshot_filename(self.print_name, SnapshotNumberFormat) self.data_directory = mkdtemp() self.octoprint_timelapse_folder = mkdtemp() diff --git a/octoprint_octolapse/test/test_settings.py b/octoprint_octolapse/test/test_settings.py index aece6eea..9c938d32 100644 --- a/octoprint_octolapse/test/test_settings.py +++ b/octoprint_octolapse/test/test_settings.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published diff --git a/octoprint_octolapse/test/test_settings_slicer.py b/octoprint_octolapse/test/test_settings_slicer.py index bf831c2f..c4a6ccf5 100644 --- a/octoprint_octolapse/test/test_settings_slicer.py +++ b/octoprint_octolapse/test/test_settings_slicer.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -44,4 +44,4 @@ def test_json_file_load(self): with open(file_path) as settingsJson: data = json.load(settingsJson) loaded_settings = OctolapseSettings.create_from("c:\\temp\\temp.log", "0.3.4", data) - self.assertNotEqual(loaded_settings.profiles.debug[0].name, "Default Debug") + self.assertNotEqual(loaded_settings.profiles.logging[0].name, "Default Logging") diff --git a/octoprint_octolapse/test/test_snapshotGcode.py b/octoprint_octolapse/test/test_snapshotGcode.py index 5fceee91..8278ff93 100644 --- a/octoprint_octolapse/test/test_snapshotGcode.py +++ b/octoprint_octolapse/test/test_snapshotGcode.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -25,11 +25,11 @@ from tempfile import NamedTemporaryFile from octoprint_octolapse.extruder import Extruder -from octoprint_octolapse.gcode import SnapshotGcodeGenerator +from octoprint_octolapse.stabilization_gcode import SnapshotGcodeGenerator from octoprint_octolapse.settings import OctolapseSettings from octoprint_octolapse.position import Position from octoprint_octolapse.trigger import Triggers -from octoprint_octolapse.gcode_parser import ParsedCommand, Commands +from octoprint_octolapse.gcode_commands import ParsedCommand, Commands from octoprint_octolapse.settings import PrinterProfile from octoprint_octolapse.test.testing_utilities import get_printer_profile diff --git a/octoprint_octolapse/test/test_timelapse.py b/octoprint_octolapse/test/test_timelapse.py index 490e6085..45836fc0 100644 --- a/octoprint_octolapse/test/test_timelapse.py +++ b/octoprint_octolapse/test/test_timelapse.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -24,7 +24,7 @@ import time import unittest -from octoprint_octolapse.gcode import SnapshotGcode +from octoprint_octolapse.stabilization_gcode import SnapshotGcode from octoprint_octolapse.position import Position from octoprint_octolapse.settings import OctolapseSettings from octoprint_octolapse.timelapse import Timelapse, TimelapseState @@ -323,11 +323,6 @@ def test_IsTriggering_GcodeTrigger(self): self.assertTrue(triggeringTrigger == self.Timelapse_GcodeTrigger.Triggers[0]) - # test with position error - self.Timelapse_GcodeTrigger.Position.get_position(0).has_position_error = True - self.assertTrue(self.Timelapse_GcodeTrigger.IsTriggering( - snapshotCommand) is None) - def test_IsTriggering_TimerTrigger(self): self.Settings.profiles.current_snapshot().gcode_trigger_enabled = False self.Settings.profiles.current_snapshot().layer_trigger_enabled = False @@ -661,7 +656,7 @@ def test_GcodeQueuing_Triggering_Nonsnapshot_command(self): self.assertTrue(self.Timelapse_GcodeTrigger.OctoprintPrinter.IsPaused) def test_GcodeSent_IgnoredStates(self): - """Tests WaitingForTrigger, RequestingReturnPosition, SendingSnapshotGcode, TakingSnapshot, RequestingSnapshotPosition, SendingReturnGcode, all of which should be ignored except for a debug message.""" + """Tests WaitingForTrigger, RequestingReturnPosition, SendingSnapshotGcode, TakingSnapshot, RequestingSnapshotPosition, SendingReturnGcode, all of which should be ignored except for a logging message.""" snapshotCommand = "snap" self.Settings.profiles.current_printer().snapshot_command = snapshotCommand self.Settings.profiles.current_snapshot().gcode_trigger_enabled = True diff --git a/octoprint_octolapse/test/test_trigger.py b/octoprint_octolapse/test/test_trigger.py index 9e84b506..9baf1086 100644 --- a/octoprint_octolapse/test/test_trigger.py +++ b/octoprint_octolapse/test/test_trigger.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published diff --git a/octoprint_octolapse/test/test_trigger_gcode.py b/octoprint_octolapse/test/test_trigger_gcode.py index 258743fb..4e6d0b2d 100644 --- a/octoprint_octolapse/test/test_trigger_gcode.py +++ b/octoprint_octolapse/test/test_trigger_gcode.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published diff --git a/octoprint_octolapse/test/test_trigger_layer.py b/octoprint_octolapse/test/test_trigger_layer.py index 83e82b9e..41c8b003 100644 --- a/octoprint_octolapse/test/test_trigger_layer.py +++ b/octoprint_octolapse/test/test_trigger_layer.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published diff --git a/octoprint_octolapse/test/test_trigger_timer.py b/octoprint_octolapse/test/test_trigger_timer.py index 50219004..dce3f74a 100644 --- a/octoprint_octolapse/test/test_trigger_timer.py +++ b/octoprint_octolapse/test/test_trigger_timer.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published diff --git a/octoprint_octolapse/test/test_utility.py b/octoprint_octolapse/test/test_utility.py index 9b21bb38..f4d26e97 100644 --- a/octoprint_octolapse/test/test_utility.py +++ b/octoprint_octolapse/test/test_utility.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -22,8 +22,9 @@ ################################################################################## import unittest - +from octoprint_octolapse._version import get_versions import octoprint_octolapse.utility as utility +from octoprint_octolapse_setuptools import NumberedVersion class TestUtility(unittest.TestCase): @@ -254,6 +255,19 @@ def test_isclose(self): self.assertTrue(utility.is_close(113.33847, 113.34, 0.005)) self.assertTrue(utility.is_close(119.9145519, 119.91, 0.005)) + def test_numbered_version(self): + fallback_version = NumberedVersion.clean_version("0.4.0rc3") + plugin_version = NumberedVersion.clean_version(get_versions()['version']) + #if plugin_version == "0+unknown" or NumberedVersion(plugin_version) < NumberedVersion(fallback_version): + if plugin_version == "0+unknown": + plugin_version = fallback_version + try: + # This generates version in the following form: + # 0.4.0rc1+?.GUID_GOES_HERE + plugin_version += "+u." + versioneer.get_versions()['full-revisionid'][0:7] + except: + pass + self.assertEqual(plugin_version, fallback_version) if __name__ == '__main__': suite = unittest.TestLoader().loadTestsFromTestCase(TestUtility) diff --git a/octoprint_octolapse/test/testing_utilities.py b/octoprint_octolapse/test/testing_utilities.py index 798af4ac..807799ee 100644 --- a/octoprint_octolapse/test/testing_utilities.py +++ b/octoprint_octolapse/test/testing_utilities.py @@ -1,3 +1,26 @@ +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## + + def get_printer_profile(): return { "default_firmware_retractions_zhop": False, @@ -14,7 +37,6 @@ def get_printer_profile(): "min_y": 0.0, "axis_speed_display_units": "mm-min", "auto_position_detection_commands": "", - "abort_out_of_bounds": True, "name": "Prusa Mk2/Mk2S Multi Material", "min_z": 0.0, "z_hop_speed": 7200.0, diff --git a/octoprint_octolapse/timelapse.py b/octoprint_octolapse/timelapse.py index ddaf4324..2f8e3eed 100644 --- a/octoprint_octolapse/timelapse.py +++ b/octoprint_octolapse/timelapse.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -24,81 +24,86 @@ import threading import time import uuid -from six.moves import queue - +# remove unused usings +# from six import iteritems, string_types +# Remove python 2 support +# from six.moves import queue +import queue as queue +import os import octoprint_octolapse.utility as utility -from octoprint_octolapse.gcode import SnapshotGcodeGenerator, SnapshotGcode -from octoprint_octolapse.gcode_parser import Commands, ParsedCommand, Response +from octoprint_octolapse.stabilization_gcode import SnapshotGcodeGenerator, SnapshotGcode +from octoprint_octolapse.gcode_commands import Commands, Response +from octoprint_octolapse.gcode_processor import ParsedCommand from octoprint_octolapse.position import Position -from octoprint_octolapse.render import RenderError, RenderingProcessor, RenderingCallbackArgs, RenderJobInfo, TimelapseRenderJob from octoprint_octolapse.settings import PrinterProfile, OctolapseSettings from octoprint_octolapse.snapshot import CaptureSnapshot, SnapshotJobInfo, SnapshotError from octoprint_octolapse.trigger import Triggers +import octoprint_octolapse.error_messages as error_messages import octoprint_octolapse.stabilization_preprocessing as preprocessing -import GcodePositionProcessor +from octoprint_octolapse.gcode_processor import GcodeProcessor # create the module level logger from octoprint_octolapse.log import LoggingConfigurator logging_configurator = LoggingConfigurator() logger = logging_configurator.get_logger(__name__) + +class TimelapseStartException(Exception): + def __init__(self, message, type): + super(TimelapseStartException, self).__init__() + self.message = message + self.type = type + + class Timelapse(object): def __init__( - self, get_current_octolapse_settings, octoprint_printer, data_folder, timelapse_folder, - on_print_started=None, on_print_start_failed=None, - on_snapshot_start=None, on_snapshot_end=None, on_new_thumbnail_available=None, - on_prerender_start=None, on_render_start=None, on_render_success=None, on_render_error=None, - on_render_end=None, on_timelapse_stopping=None, on_timelapse_stopped=None, - on_state_changed=None, on_timelapse_end=None, - on_snapshot_position_error=None, on_position_error=None + self, get_current_octolapse_settings, octoprint_printer, data_folder, default_settings_folder, + octoprint_settings, + on_print_started=None, on_print_start_failed=None, + on_snapshot_start=None, on_snapshot_end=None, on_new_thumbnail_available=None, + on_post_processing_error_callback=None, on_timelapse_stopping=None, + on_timelapse_stopped=None, on_state_changed=None, on_timelapse_end=None, on_snapshot_position_error=None, + on_rendering_start=None ): # config variables - These don't change even after a reset self.state_update_period_seconds = 1 self._data_folder = data_folder + self._temporary_folder = get_current_octolapse_settings().main_settings.get_temporary_directory(self._data_folder) self.get_current_octolapse_settings = get_current_octolapse_settings self._settings = self.get_current_octolapse_settings() # type: OctolapseSettings + self._octoprint_settings = octoprint_settings self._octoprint_printer = octoprint_printer - self._default_timelapse_directory = timelapse_folder + self._default_settings_folder = default_settings_folder self._print_start_callback = on_print_started self._print_start_failed_callback = on_print_start_failed - self._prerender_start_callback = on_prerender_start - self._render_start_callback = on_render_start - self._render_success_callback = on_render_success - self._render_error_callback = on_render_error - self._render_end_callback = on_render_end self._snapshot_start_callback = on_snapshot_start self._snapshot_complete_callback = on_snapshot_end self._new_thumbnail_available_callback = on_new_thumbnail_available + self._on_post_processing_error_callback = on_post_processing_error_callback self._timelapse_stopping_callback = on_timelapse_stopping self._timelapse_stopped_callback = on_timelapse_stopped self._state_changed_callback = on_state_changed self._timelapse_end_callback = on_timelapse_end self._snapshot_position_error_callback = on_snapshot_position_error - self._position_error_callback = on_position_error + self._on_rendering_start_callback = on_rendering_start self._commands = Commands() # used to parse and generate gcode self._triggers = None self._print_end_status = "Unknown" - self._last_state_changed_message_time = None - self._last_position_error_message_time = None - self._position_error_message_thread = None - self.has_position_errors_to_report = False - self.position_errors_to_report = None + self._last_state_changed_message_time = 0 # Settings that may be different after StartTimelapse is called self._octoprint_printer_profile = None self._current_job_info = None - self._ffmpeg_path = None self._stabilization = None self._trigger = None + self._trigger_profile = None self._gcode = None self._printer = None self._capture_snapshot = None - self._rendering_processor = None self._position = None self._state = TimelapseState.Idle - self._is_test_mode = False - self._snapshot_command = None + self._test_mode_enabled = False # State Tracking that should only be reset when starting a timelapse self._has_been_stopped = False self._timelapse_stop_requested = False @@ -110,6 +115,7 @@ def __init__( self._position_payload = None self._position_timeout_long = 600.0 self._position_timeout_short = 60.0 + self._position_timeout_very_short = 5.0 self._position_signal = threading.Event() self._position_signal.set() @@ -117,10 +123,11 @@ def __init__( self._snapshot_success = False # It shouldn't take more than 5 seconds to take a snapshot! self._snapshot_timeout = 5.0 - self._snapshot_signal = threading.Event() - self._snapshot_signal.set() self._most_recent_snapshot_payload = None + self._stabilization_signal = threading.Event() + self._stabilization_signal.set() + self._current_profiles = {} self._current_file_line = 0 @@ -128,27 +135,38 @@ def __init__( self.current_snapshot_plan_index = 0 self.current_snapshot_plan = None # type: preprocessing.SnapshotPlan self.is_realtime = True + self.was_started = False # snapshot thread queue self._snapshot_task_queue = queue.Queue(maxsize=1) - self._rendering_task_queue = queue.Queue(maxsize=0) self._reset() + def validate_snapshot_command(self, command_string): + # there needs to be at least one non-comment non-whitespace character for the gcode command to work. + parsed_command = GcodeProcessor.parse(command_string) + return len(parsed_command.gcode)>0 + def get_snapshot_count(self): if self._capture_snapshot is None: - return 0 - return self._capture_snapshot.SnapshotsTotal + return 0, 0 + return self._capture_snapshot.SnapshotsTotal, self._capture_snapshot.ErrorsTotal def get_current_profiles(self): return self._current_profiles + def get_current_settings(self): + return self._settings + def get_current_state(self): return self._state def start_timelapse( - self, settings, overridable_printer_profile_settings, ffmpeg_path, + self, settings, overridable_printer_profile_settings, gcode_file_path, snapshot_plans=None ): + logger.debug( + "Starting the timelapse with the current configuration." + ) # we must supply the settings first! Else reset won't work properly. self._reset() # in case the settings have been destroyed and recreated @@ -156,7 +174,7 @@ def start_timelapse( # ToDo: all cloning should be removed after this point. We already have a settings object copy. # Also, we no longer need the original settings since we can use the global OctolapseSettings.Logger now self._printer = self._settings.profiles.current_printer() - self._snapshot_command = self._printer.snapshot_command + self._temporary_folder = self._settings.main_settings.get_temporary_directory(self._data_folder) self._stabilization = self._settings.profiles.current_stabilization() self._trigger_profile = self._settings.profiles.current_trigger() self.snapshot_plans = snapshot_plans @@ -172,29 +190,17 @@ def start_timelapse( self.RequiresLocationDetectionAfterHome = False self.overridable_printer_profile_settings = overridable_printer_profile_settings - self._ffmpeg_path = ffmpeg_path - self._current_job_info = utility.TimelapseJobInfo( job_guid=uuid.uuid4(), print_start_time=time.time(), - print_file_name=utility.get_filename_from_full_path(gcode_file_path) + print_file_name=utility.get_filename_from_full_path(gcode_file_path), + print_file_extension=utility.get_extension_from_full_path(gcode_file_path), ) - - if self._rendering_processor is None: - self._rendering_processor = RenderingProcessor( - self._rendering_task_queue, - self._data_folder, - self._default_timelapse_directory, - self._on_prerender_start, - self._on_render_start, - self._on_render_success, - self._on_render_error, - self._on_render_end - ) - self._rendering_processor.start() - - self._gcode = SnapshotGcodeGenerator( - self._settings, self.overridable_printer_profile_settings) + # save the timelapse job info + self._current_job_info.save(self._temporary_folder) + # save the rendering settings for use by the RenderingProcessor for this timelapse + self._settings.save_rendering_settings(self._temporary_folder, self._current_job_info.JobGuid) + self._gcode = SnapshotGcodeGenerator(self._settings, self.overridable_printer_profile_settings) self._capture_snapshot = CaptureSnapshot( self._settings, @@ -202,7 +208,9 @@ def start_timelapse( self._settings.profiles.active_cameras(), self._current_job_info, self.send_gcode_for_camera, - self._new_thumbnail_available_callback + self._new_thumbnail_available_callback, + self._on_post_processing_error_callback, + ) self._position = Position( @@ -210,47 +218,65 @@ def start_timelapse( self._settings.profiles.current_trigger(), self.overridable_printer_profile_settings ) - self._state = TimelapseState.WaitingForTrigger - self._is_test_mode = self._settings.profiles.current_debug_profile().is_test_mode + self._test_mode_enabled = self._settings.main_settings.test_mode_enabled self._triggers = Triggers(self._settings) self._triggers.create() # take a snapshot of the current settings for use in the Octolapse Tab self._current_profiles = self._settings.profiles.get_profiles_dict() + # test the position request + if not self._test_position_request(): + message = "Your printer does not support M114, and is incompatible with Octolapse." + if not self._settings.main_settings.cancel_print_on_startup_error: + message += " Continue on failure is enabled so your print will continue, but the timelapse has been " \ + "aborted." + raise TimelapseStartException(message, 'm114_not_supported') + self._state = TimelapseState.WaitingForTrigger + self.was_started = True + logger.debug( + "The timelapse configuration is set, waiting to stop the job-on-hold lock." + ) - def on_position_received(self, payload): - # added new position request sent flag so that we can prevent position requests NOT from Octolapse from - # triggering a snapshot. - if self._position_request_sent: - self._position_request_sent = False - logger.info("Octolapse has received a position request response.") - if self._state in [ - TimelapseState.AcquiringLocation, TimelapseState.TakingSnapshot, TimelapseState.WaitingToEndTimelapse, - TimelapseState.WaitingToRender - ]: - # set flag to false so that it can be triggered again after the next M114 sent by Octolapse - self._position_payload = payload - else: - self._position_payload = None - logger.info( - "Octolapse was not in the correct state to receive a position update. StateId:{0}", - self._state - ) - self._position_signal.set() - else: - logger.info("Octolapse has received an position response but did not request one. Ignoring.") + _stabilization_gcode_tags = { + 'snapshot-init', + 'snapshot-start', + 'snapshot-gcode', + 'snapshot-return', + 'snapshot-end' + } + + def get_current_temporary_folder(self): + return self._temporary_folder + + class StabilizationGcodeStateException(Exception): + pass def send_snapshot_gcode_array(self, gcode_array, tags): self._octoprint_printer.commands(gcode_array, tags=tags) - def send_gcode_for_camera(self, gcode_array, timeout): - self.get_position_async( - start_gcode=gcode_array, timeout=timeout, tags={'camera-gcode'} - ) + def send_gcode_for_camera(self, gcode_array, timeout, wait_for_completion=True, tags=None): + if tags is None: + tags = {'camera-gcode'} + if wait_for_completion: + self.get_position_async( + start_gcode=gcode_array, timeout=timeout, tags=tags + ) + else: + self.send_snapshot_gcode_array(gcode_array, tags=tags) + + def _test_position_request(self): + logger.info("Testing M114 Support.") + if self.get_position_async(timeout=self._position_timeout_short, no_wait=True): + return True + return False # requests a position from the printer (m400-m114), and can send optional gcode before the position request. # this ensures any gcode sent in the start_gcode parameter will be executed before the function returns. - def get_position_async(self, start_gcode=None, timeout=None, tags=None): + _position_acquisition_array_wait = ["M400", "M114"] + _position_acquisition_no_wait = ["M400", "M114"] + + def get_position_async(self, start_gcode=None, timeout=None, tags=None, no_wait=False): + self._position_payload = None if timeout is None: timeout = self._position_timeout_long @@ -259,71 +285,120 @@ def get_position_async(self, start_gcode=None, timeout=None, tags=None): # Warning, we can only request one position at a time! if self._position_signal.is_set(): self._position_signal.clear() + if tags is None: + tags = set() - # build the staret commands - commands_to_send = ["M400", "M114"] # send any code that is to be run before the position request if start_gcode is not None and len(start_gcode) > 0: - commands_to_send = start_gcode + commands_to_send - - if self._state in [ - TimelapseState.TakingSnapshot, TimelapseState.AcquiringLocation, TimelapseState.WaitingToEndTimelapse, - TimelapseState.WaitingToRender - ]: - if tags is None: - tags = {'current-position'} - self.send_snapshot_gcode_array(commands_to_send, tags) + self.send_snapshot_gcode_array(start_gcode, tags) + tags = set(['wait-for-position']) + + if no_wait: + self.send_snapshot_gcode_array(["M114"], tags) else: - logger.warning( - "Warning: The printer was not in the expected state to send octolapse gcode. State:%s", - self._state - ) - return None + self.send_snapshot_gcode_array(["M400", "M114"], tags) event_is_set = self._position_signal.wait(timeout) - if not event_is_set: # we ran into a timeout while waiting for a fresh position - logger.info("Warning: A timeout occurred while requesting the current position.") - + logger.warning("Warning: A timeout occurred while requesting the current position.") return None - return self._position_payload - def _take_snapshots(self): + def on_position_received(self, payload): + # added new position request sent flag so that we can prevent position requests NOT from Octolapse from + # triggering a snapshot. + if self._position_request_sent: + self._position_request_sent = False + logger.info("Octolapse has received a position request response.") + # set flag to false so that it can be triggered again after the next M114 sent by Octolapse + self._position_payload = payload + self._position_signal.set() + else: + logger.info("Octolapse has received an position response but did not request one. Ignoring.") + + def _take_snapshots(self, metadata): snapshot_payload = { "success": False, "error": "Waiting on thread to signal, aborting" } - # start the snapshot logger.info("Taking a snapshot.") self._snapshot_task_queue.join() self._snapshot_task_queue.put("snapshot_job") try: - results = self._capture_snapshot.take_snapshots() + results = self._capture_snapshot.take_snapshots(metadata, no_wait=not self._stabilization.wait_for_moves_to_finish) finally: self._snapshot_task_queue.get() self._snapshot_task_queue.task_done() # todo - notify client here # todo - maintain snapshot number separately for each camera! succeeded = len(results) > 0 - errors = [] + errors = {} + error_count = 0 for result in results: assert(isinstance(result, SnapshotJobInfo)) if not result.success: succeeded = False - errors.append(result.error) + error_count += 1 + if isinstance(result.error, SnapshotError): + error_message = result.error.message + # remove python 2 support + # elif isinstance(result.error, string_types): + elif isinstance(result.error, str): + error_message = result.error + + if result.job_type not in errors: + errors[result.job_type] = {"error": error_message, 'count': 1} + else: + previous_error = errors[result.job_type] + previous_error["error"] += error_message + previous_error["count"] += 1 + snapshot_payload["success"] = succeeded - # todo: format this so the errors look better error_message = "" - if len(errors) == 1: - if isinstance(errors[0], SnapshotError): - error_message = errors[0].message - else: - error_message = "An unexpected error occurred while taking snapshots. See plugin_octolapse.log for details." - elif len(errors) > 1: - error_message = "{0} errors occurred while taking snapshots. See plugin_octolapse.log for details.".format(len(errors)) + if error_count == 1: + # remove python 2 support + # for key, value in iteritems(errors): + for key, value in errors.items(): + error = value["error"] + if key == 'before-snapshot': + error_message = "Before Snapshot Script Error: {0}" + elif key == 'after-snapshot': + error_message = "After Snapshot Script Error: {0}" + else: + error_message = "{0}" + + error_message = error_message.format(error) + + elif error_count > 1: + before_snapshot_error_count = False + after_snapshot_error_count = False + snapshot_error_count = False + # remove python 2 support + # for key, value in iteritems(errors): + for key, value in errors.items(): + if key == 'before-snapshot': + before_snapshot_error_count = value["count"] + elif key == 'after-snapshot': + after_snapshot_error_count = value["count"] + else: + snapshot_error_count = value["count"] + + error_message = "Multiple errors occurred:" + if before_snapshot_error_count > 0: + error_message += "{0}{1} Before Snapshot Error{2}".format( + os.linesep, before_snapshot_error_count, "s" if before_snapshot_error_count > 1 else "" + ) + if snapshot_error_count > 0: + error_message += "{0}{1} Snapshot Error{2}".format( + os.linesep, snapshot_error_count, "s" if snapshot_error_count > 1 else "" + ) + if after_snapshot_error_count > 0: + error_message += "{0}{1} After Snapshot Error{2}".format( + os.linesep, after_snapshot_error_count, "s" if after_snapshot_error_count > 1 else "" + ) + error_message += "{0}See plugin_octolapse.log for details.".format(os.linesep) snapshot_payload["error"] = error_message @@ -332,9 +407,7 @@ def _take_snapshots(self): return snapshot_payload - def _take_timelapse_snapshot_precalculated( - self, parsed_command - ): + def _take_timelapse_snapshot_precalculated(self): timelapse_snapshot_payload = { "snapshot_position": None, "return_position": None, @@ -353,9 +426,6 @@ def _take_timelapse_snapshot_precalculated( # save the gcode fo the payload timelapse_snapshot_payload["snapshot_gcode"] = snapshot_gcode - if self._gcode.has_snapshot_position_errors: - timelapse_snapshot_payload["error"] = self._gcode.Snapshotposition_errors - if snapshot_gcode is None: logger.warning("No snapshot gcode was generated.") return timelapse_snapshot_payload @@ -363,50 +433,74 @@ def _take_timelapse_snapshot_precalculated( assert (isinstance(snapshot_gcode, SnapshotGcode)) # If we have any initialization gcodes, send them before waiting for moves to finish - # (in case we are tracking item) if len(snapshot_gcode.InitializationGcode) > 0: - logger.info("Sending initialization gcode.") - self.send_snapshot_gcode_array(snapshot_gcode.InitializationGcode, {'snapshot-start'}) - - # start building up a list of gcodes to send, starting with (appropriately) the start gcode - gcodes_to_send = snapshot_gcode.StartGcode - + logger.info("Queuing %d initialization commands.", len(snapshot_gcode.InitializationGcode)) + self.send_snapshot_gcode_array(snapshot_gcode.InitializationGcode, {'snapshot-init'}) + + # If we have any start gcodes (lift/retract), send them before waiting for moves to finish + if len(snapshot_gcode.StartGcode) > 0: + logger.info("Queuing %d start commands.", len(snapshot_gcode.StartGcode)) + self.send_snapshot_gcode_array(snapshot_gcode.StartGcode, {'snapshot-start'}) + + ## Send the snapshot gcodes, making sure to send an M400+M114 before taking any snapshots + gcodes_to_send = [] + # loop through the snapshot commands and build up gocdes_to_send array, only sending the current commands + # once we hit the snapshot command. for gcode in snapshot_gcode.snapshot_commands: - if utility.is_snapshot_command(gcode, self._printer.snapshot_command): - snapshot_position = None - if len(gcodes_to_send) > 0: - snapshot_position = self.get_position_async( - start_gcode=gcodes_to_send, - tags={'snapshot-gcode'} + if gcode == "{0} {1}".format(PrinterProfile.OCTOLAPSE_COMMAND, PrinterProfile.DEFAULT_OCTOLAPSE_SNAPSHOT_COMMAND): + if self._stabilization.wait_for_moves_to_finish: + logger.debug( + "Queuing %d snapshot commands, an M400 and an M114 command. Note that the actual snapshot command is never sent.", + len(gcodes_to_send) ) - gcodes_to_send = [] + snapshot_position = self.get_position_async(start_gcode=gcodes_to_send, tags={'snapshot-gcode'}) if snapshot_position is None: has_error = True logger.error( "The snapshot position is None. Either the print has cancelled or a timeout has been " "reached. " ) - # don't send any more gcode if we're cancelling - if self._octoprint_printer.get_state_id() == "CANCELLING": - return None + else: + logger.debug( + "Queuing %d snapshot commands. Not waiting for moves to finish.", + len(gcodes_to_send) + ) + snapshot_position = None + self.send_snapshot_gcode_array(gcodes_to_send, {'snapshot-gcode'}) + gcodes_to_send = [] + # TODO: ALLOW MULTIPLE PAYLOADS timelapse_snapshot_payload["snapshot_position"] = snapshot_position # take a snapshot - timelapse_snapshot_payload["snapshot_payload"] = self._take_snapshots() + timelapse_snapshot_payload["snapshot_payload"] = self._take_snapshots(self.current_snapshot_plan.get_snapshot_metadata()) else: gcodes_to_send.append(gcode) - # return the printhead to the start position - gcodes_to_send.extend(snapshot_gcode.ReturnCommands + snapshot_gcode.EndGcode) if len(gcodes_to_send) > 0: - if self._state == TimelapseState.TakingSnapshot: - logger.info( - "Sending snapshot return and end gcode.") - self.send_snapshot_gcode_array(gcodes_to_send, {"snapshot-end"}) + logger.info("Queuing remaining %d snapshot commands.", len(snapshot_gcode.StartGcode)) + self.send_snapshot_gcode_array(gcodes_to_send, {"snapshot-gcode"}) + # return the printhead to the starting position by sending the return commands + if len(snapshot_gcode.ReturnCommands) > 0: + logger.info("Queuing %d return commands.", len(snapshot_gcode.ReturnCommands)) + self.send_snapshot_gcode_array(snapshot_gcode.ReturnCommands, {"snapshot-return"}) + + # send any end gcodes, including deretract, delift, axis mode corrections, etc + if len(snapshot_gcode.EndGcode) > 0: + logger.info("Queuing %d end commands.", len(snapshot_gcode.EndGcode)) + self.send_snapshot_gcode_array(snapshot_gcode.EndGcode, {"snapshot-end"}) + + if self._state != TimelapseState.TakingSnapshot: + logger.warning( + "The timelapse state was expected to TakingSnapshots, but was equal to {0}".format(self._state) + ) # we've completed the procedure, set success - timelapse_snapshot_payload["success"] = not has_error and not self._gcode.has_snapshot_position_errors + timelapse_snapshot_payload["success"] = not has_error + except Timelapse.StabilizationGcodeStateException as e: + logger.exception("The timelapse was in the wrong state to take a snapshot.") + timelapse_snapshot_payload["success"] = False + timelapse_snapshot_payload["error"] = "The timelapse was stopped in the middle of a snapshot. Skipping." except Exception as e: logger.exception("Failed to take a snapshot for the provided snapshot plan.") timelapse_snapshot_payload["error"] = "An unexpected error was encountered while running the timelapse " \ @@ -461,7 +555,7 @@ def to_state_dict(self, include_timelapse_start_data=False): "printer_state": printer_state_dict, "trigger_state": trigger_state, "trigger_type": "real-time" if self.is_realtime else "pre-calculated", - "snapshot_plan": snapshot_plan + "snapshot_plan": snapshot_plan, } return state_dict @@ -487,10 +581,21 @@ def stop_snapshots(self, message=None, error=False): timelapse_stopped_callback_thread.start() return True - def release_job_on_hold_lock(self): + def release_job_on_hold_lock(self, force=False, reset=False, parsed_command=None): + if parsed_command is not None: + self._octoprint_printer.commands([parsed_command.gcode], tags={"before_release_job_lock"}) + if self.job_on_hold: - self._octoprint_printer.set_job_on_hold(False) - self.job_on_hold = False + if force or (self._stabilization_signal.is_set() and self._position_signal.is_set()): + logger.debug("Releasing job-on-hold lock.") + if self._octoprint_printer.is_operational(): + try: + self._octoprint_printer.set_job_on_hold(False) + except RuntimeError as e: + logger.exception("Unable to release job lock. It's likely that the printer was disconnected.") + self.job_on_hold = False + if reset: + self._reset() def on_print_failed(self): if self._state != TimelapseState.Idle: @@ -506,21 +611,6 @@ def on_print_disconnected(self): def on_print_cancelling(self): self._state = TimelapseState.Cancelling - requests_in_progress = False - if not self._position_signal.is_set(): - logger.error("The print is cancelling, but a position request is in progress.") - self._position_payload = None - self._position_signal.set() - requests_in_progress = True - if not self._snapshot_signal.is_set(): - logger.error("The print is cancelling, but a snapshot request is in progress.") - self._most_recent_snapshot_payload = None - self._snapshot_signal.set() - requests_in_progress = True - if not requests_in_progress: - self._octoprint_printer.set_job_on_hold(False) - self.job_on_hold = False - def on_print_canceled(self): if self._state != TimelapseState.Idle: @@ -530,28 +620,18 @@ def on_print_completed(self): if self._state != TimelapseState.Idle: self.end_timelapse("COMPLETED") + def on_print_ended(self): + self.snapshot_plans = [] + def end_timelapse(self, print_status): self._print_end_status = print_status try: - if self._current_job_info is None or self._current_job_info.PrintStartTime is None: - self._reset() - elif self._state in [ - TimelapseState.WaitingForTrigger, TimelapseState.WaitingToRender, TimelapseState.WaitingToEndTimelapse, - TimelapseState.Cancelling - ]: - if not self._render_timelapse(self._print_end_status): - if self._render_error_callback is not None: - error = RenderError('timelapse_start', "The render_start function returned false") - render_end_callback_thread = threading.Thread( - target=self._render_error_callback, args=[None, error] - ) - render_end_callback_thread.daemon = True - render_end_callback_thread.start() - self._reset() - if self._state != TimelapseState.Idle: - self._state = TimelapseState.WaitingToEndTimelapse - + # See if there are enough snapshots to start renderings + snapshot_count, error_count = self.get_snapshot_count() + if snapshot_count > 1: + self._render_timelapse(self._print_end_status) + self._reset() except Exception as e: logger.exception("Failed to end the timelapse") @@ -587,16 +667,14 @@ def is_timelapse_active(self): return False return True - def get_is_rendering(self): - if self._rendering_processor is None: - return False - return self._rendering_processor.is_processing() + def get_is_test_mode_active(self): + return self._test_mode_enabled def get_is_taking_snapshot(self): return self._snapshot_task_queue.qsize() > 0 def on_print_start(self, parsed_command): - return self._print_start_callback(parsed_command) + self._print_start_callback(parsed_command) def on_print_start_failed(self, message): self._print_start_failed_callback(message) @@ -608,45 +686,45 @@ def on_gcode_queuing(self, command_string, cmd_type, gcode, tags): return None, if not self.is_timelapse_active(): - if utility.is_snapshot_command(command_string, self._snapshot_command): - if self._settings.profiles.current_printer().suppress_snapshot_command_always: - logger.info( - "Snapshot command %s detected while octolapse was disabled." - " Suppressing command.".format(command_string) - ) - return None, - else: - logger.info( - "Snapshot command %s detected while octolapse was disabled. Not suppressing since " - "'suppress_snapshot_command_always' is false.", - command_string - ) - # if the timelapse is not active, exit without changing any gcode - return None + current_printer = self._settings.profiles.current_printer() + if ( + current_printer is not None and + current_printer.suppress_snapshot_command_always and + current_printer.is_snapshot_command(command_string) + ): + logger.info( + "Snapshot command %s detected while octolapse was disabled." + " Suppressing command.".format(command_string) + ) + return None, + else: + # if the timelapse is not active, exit without changing any gcode + return None self.check_current_line_number(tags) - if tags is not None and "plugin:octolapse" in tags: + if not ( + tags is not None and + "plugin:octolapse" in tags and self.log_octolapse_gcode(logger.debug, "queuing", command_string, tags) - else: - logger.verbose("Queuing Command: %s", command_string) + ): + logger.verbose("Queuing: %s", command_string) if self.is_realtime: return_value = self.process_realtime_gcode(command_string, tags) parsed_command = self._position.current_pos.parsed_command else: - parsed_command_cpp = GcodePositionProcessor.Parse(command_string.encode('ascii', errors="replace")) - if parsed_command_cpp: - parsed_command = ParsedCommand.create_from_cpp_parsed_command(parsed_command_cpp) - else: - parsed_command = ParsedCommand(None, None, command_string) - + parsed_command = GcodeProcessor.parse(command_string) return_value = self.process_pre_calculated_gcode(parsed_command, tags) # notify any callbacks self._send_state_changed_message() - if return_value == (None,) or utility.is_snapshot_command(command_string, self._snapshot_command): + if ( + return_value == (None,) or ( + self._printer.is_snapshot_command(command_string) + ) + ): return None, if parsed_command is not None and parsed_command.cmd is not None: @@ -663,7 +741,7 @@ def on_gcode_queuing(self, command_string, cmd_type, gcode, tags): return Commands.to_string(parsed_command) # look for test mode - if self._is_test_mode and self._state >= TimelapseState.WaitingForTrigger: + if self._test_mode_enabled and self._state >= TimelapseState.WaitingForTrigger: return self._commands.alter_for_test_mode(parsed_command) # Send the original unaltered command @@ -680,6 +758,14 @@ def process_pre_calculated_gcode(self, parsed_command, tags): if self.current_snapshot_plan is None: return None current_file_line = self.get_current_file_line(tags) + # skip plans if we need to in case any were missed. + if self.current_snapshot_plan.file_gcode_number < current_file_line: + while ( + self.current_snapshot_plan.file_gcode_number < current_file_line and + len(self.snapshot_plans) > self.current_snapshot_plan_index + ): + self.set_next_snapshot_plan() + if ( self._state == TimelapseState.WaitingForTrigger and self._octoprint_printer.is_printing() @@ -688,10 +774,11 @@ def process_pre_calculated_gcode(self, parsed_command, tags): # time to take a snapshot! if self.current_snapshot_plan.triggering_command.gcode != parsed_command.gcode: logger.error( - "The snapshot plan position (%s:%s) does not match the actual position (%s:%s)! " + "The snapshot plan position (gcode number: %s, gcode:%s, line number: %s) does not match the actual position (gcode number: %s, gcode: %s)! " "Aborting Snapshot, moving to next plan.", self.current_snapshot_plan.file_gcode_number, - self.current_snapshot_plan.parsed_command.gcode, + self.current_snapshot_plan.triggering_command.gcode, + self.current_snapshot_plan.file_line_number, current_file_line, parsed_command.gcode ) @@ -699,11 +786,15 @@ def process_pre_calculated_gcode(self, parsed_command, tags): return None if self._octoprint_printer.set_job_on_hold(True): + logger.debug("Setting job-on-hold lock.") # this was set to 'False' earlier. Why? self.job_on_hold = True # We are triggering, take a snapshot self._state = TimelapseState.TakingSnapshot - # take the snapshot on a new thread + + # take the snapshot on a new thread, making sure to set a signal so we know when it is finished + if not self._stabilization_signal.is_set(): + self._stabilization_signal.clear() thread = threading.Thread( target=self.acquire_snapshot_precalculated, args=[parsed_command] ) @@ -723,8 +814,8 @@ def process_realtime_gcode(self, gcode, tags): try: # get the position state in case it has changed # if there has been a position or extruder state change, inform any listener - - self._position.update(gcode) + file_line_number = self.get_current_file_line(tags) + self._position.update(gcode, file_line_number=file_line_number) parsed_command = self._position.current_pos.parsed_command # if this code is snapshot gcode, simply return it to the printer. @@ -746,22 +837,16 @@ def process_realtime_gcode(self, gcode, tags): self._state = TimelapseState.AcquiringLocation if self._octoprint_printer.set_job_on_hold(True): + logger.debug("Setting job-on-hold lock.") self.job_on_hold = True thread = threading.Thread(target=self.acquire_position, args=[parsed_command]) thread.daemon = True thread.start() return None, - elif ( - self._position.current_pos.has_position_error and - self._state != TimelapseState.AcquiringLocation - ): - # There are position errors, report them! - self._on_position_error() elif (self._state == TimelapseState.WaitingForTrigger - and self._octoprint_printer.is_printing() - and not self._position.current_pos.has_position_error): + and self._octoprint_printer.is_printing()): # update the triggers with the current position - self._triggers.update(self._position, parsed_command) + self._triggers.update(self._position) # see if at least one trigger is triggering _first_triggering = self.get_first_triggering() @@ -769,6 +854,7 @@ def process_realtime_gcode(self, gcode, tags): if _first_triggering: # get the job lock if self._octoprint_printer.set_job_on_hold(True): + logger.debug("Setting job-on-hold lock.") self.job_on_hold = True # We are triggering, take a snapshot self._state = TimelapseState.TakingSnapshot @@ -779,7 +865,10 @@ def process_realtime_gcode(self, gcode, tags): self.current_snapshot_plan = self._gcode.create_snapshot_plan( self._position, _first_triggering) - # take the snapshot on a new thread + # take the snapshot on a new thread, making sure to set a signal so we know when it + # is finished + if not self._stabilization_signal.is_set(): + self._stabilization_signal.clear() thread = threading.Thread( target=self.acquire_snapshot_precalculated, args=[parsed_command] ) @@ -787,7 +876,7 @@ def process_realtime_gcode(self, gcode, tags): thread.start() # undo the position update since we'll be suppressing this command - self._position.undo_update() + #self._position.undo_update() # suppress the current command, we'll send it later return None, @@ -815,11 +904,25 @@ def detect_timelapse_start(self, command_string, tags): self._state == TimelapseState.Idle and self.get_current_octolapse_settings().main_settings.is_octolapse_enabled and ( - {'trigger:comm.start_print', 'trigger:comm.reset_line_numbers'} <= tags or - {'script:beforePrintStarted', 'trigger:comm.send_gcode_script'} <= tags + ( + 'trigger:comm.start_print' in tags and + ( + 'trigger:comm.reset_line_numbers' in tags or + command_string.startswith("M23") + ) + ) or {'script:beforePrintStarted', 'trigger:comm.send_gcode_script'} <= tags ) and self._octoprint_printer.is_printing() ): + if command_string.startswith("M23"): + # SD print, can't do anything about it. Send a warning + error = error_messages.get_error(["init", "cant_print_from_sd"]) + logger.info(error["description"]) + self.on_print_start_failed([error]) + # continue with the print since we can't stop it + return None + if self._octoprint_printer.set_job_on_hold(True): + logger.debug("Setting job-on-hold lock.") self.job_on_hold = True self._state = TimelapseState.Initializing @@ -830,41 +933,40 @@ def detect_timelapse_start(self, command_string, tags): ) # parse the command string try: - fast_cmd = GcodePositionProcessor.Parse(command_string.encode('ascii', errors="replace")) + parsed_command = GcodeProcessor.parse(command_string) except ValueError as e: - logger.exception(e) + self._state = TimelapseState.Idle + logger.exception("Unable to parse the command string.") # if we don't return NONE here, we will have problems with the print! return None except Exception as e: - logger.exception(e) + self._state = TimelapseState.Idle + logger.exception("An unexpected exception occurred while trying to parse the command string.") # TODO: REMOVE THIS BECAUSE IT'S TOO BROAD! - # if we don't return NONE here, we will have problems with the print! raise e - if fast_cmd: - parsed_command = ParsedCommand(fast_cmd[0], fast_cmd[1], command_string) - else: - parsed_command = ParsedCommand(None, None, command_string) - - # call the synchronous callback on_print_start - if self.on_print_start(parsed_command): - if self._state == TimelapseState.WaitingForTrigger: - # set the current line to 0 so that the plugin checks for line 1 below after startup. - self._current_file_line = 0 - self._octoprint_printer.set_job_on_hold(False) - self.job_on_hold = False - else: - return None, + # start a thread to start the timelapse + + def run_on_print_start_callback(parsed_command): + self.on_print_start(parsed_command) + + thread = threading.Thread( + target=run_on_print_start_callback, args=[parsed_command] + ) + thread.daemon = True + thread.start() + return None, else: self.on_print_start_failed( - "Unable to start timelapse, failed to acquire a job lock. Print start failed." + error_messages['timelapse']['cannot_aquire_job_lock'] ) return None def preprocessing_finished(self, parsed_command): if parsed_command is not None: self.send_snapshot_gcode_array([parsed_command.gcode], {'pre-processing-end'}) - self._octoprint_printer.set_job_on_hold(False) + self.release_job_on_hold_lock() + logger.debug("Releasing job-on-hold lock.") self.job_on_hold = False @staticmethod @@ -877,6 +979,16 @@ def get_current_file_line(tags): return int(actual_file_line) return None + @staticmethod + def get_current_file_position(tags): + # check the current line number + if 'source:file' in tags: + for tag in tags: + if len(tag) > 9 and tag.startswith("filepos:"): + actual_file_position = tag[8:] + return int(actual_file_position) + return None + def check_current_line_number(self, tags): # check the current line number if 'source:file' in tags: @@ -899,13 +1011,13 @@ def check_for_non_metric_errors(self): is_metric = self._position.current_pos.is_metric has_error = False error_message = "" - if is_metric is None and self._position.current_pos.has_position_error: + if is_metric is None: has_error = True error_message = "The printer profile requires an explicit G21 command before any position " \ "altering/setting commands, including any home commands. Stopping timelapse, " \ "but continuing the print. " - elif not is_metric and self._position.current_pos.has_position_error: + elif not is_metric: has_error = True if self._printer.units_default == "inches": error_message = "The printer profile uses 'inches' as the default unit of measurement. In order to" \ @@ -989,6 +1101,7 @@ def acquire_position(self, parsed_command): finally: self._octoprint_printer.set_job_on_hold(False) + logger.debug("Releasing job-on-hold lock.") self.job_on_hold = False def acquire_snapshot_precalculated(self, parsed_command): @@ -1000,9 +1113,7 @@ def acquire_snapshot_precalculated(self, parsed_command): snapshot_callback_thread.start() # take the snapshot - self._most_recent_snapshot_payload = self._take_timelapse_snapshot_precalculated( - parsed_command - ) + self._most_recent_snapshot_payload = self._take_timelapse_snapshot_precalculated() if self._most_recent_snapshot_payload is None: logger.error("acquire_snapshot received a null payload.") @@ -1027,43 +1138,65 @@ def acquire_snapshot_precalculated(self, parsed_command): if not self.is_realtime: self.set_next_snapshot_plan() self._octoprint_printer.set_job_on_hold(False) + logger.debug("Releasing job-on-hold lock.") self.job_on_hold = False + self._stabilization_signal.set() def on_gcode_sending(self, cmd, tags): if cmd == "M114" and 'plugin:octolapse' in tags: - logger.verbose("The position request is being sent") + logger.debug("The position request is being sent") self._position_request_sent = True - elif tags is not None and "plugin:octolapse" in tags: - self.log_octolapse_gcode(logger.verbose, "sending", cmd, tags) + elif self._state == TimelapseState.Idle: + return + elif not ( + tags is not None + and "plugin:octolapse" in tags + and self.log_octolapse_gcode(logger.verbose, "sending", cmd, tags) + ): + logger.verbose("Sending: %s", cmd) def on_gcode_sent(self, cmd, cmd_type, gcode, tags={}): - if tags is not None and "plugin:octolapse" in tags: - self.log_octolapse_gcode(logger.debug, "sent", cmd, tags) - else: - logger.verbose("Sent to printer: %s", cmd) + if self._state == TimelapseState.Idle: + return + if not ( + tags is not None + and "plugin:octolapse" in tags + and self.log_octolapse_gcode(logger.debug, "sent", cmd, tags) + ): + logger.debug("Sent: %s", cmd) def on_gcode_received(self, line): - logger.verbose("Received from printer: %s", line) if self._position_request_sent: payload = Response.check_for_position_request(line) if payload: self.on_position_received(payload) - + elif self._state != TimelapseState.Idle: + logger.verbose("Received: %s", line) return line def log_octolapse_gcode(self, logf, msg, cmd, tags): if "acquire-position" in tags: logf("Acquire snapshot position gcode - %s: %s", msg, cmd) + elif "snapshot-init" in tags: + logf("Snapshot gcode INIT - %s: %s", msg, cmd) elif "snapshot-start" in tags: - logf("Snapshot gcode START - %s: %s", msg, cmd) + logf("Snapshot gcode START - %s: %s", msg, cmd) elif "snapshot-gcode" in tags: - logf("Snapshot gcode - %s: %s", msg, cmd) + logf("Snapshot gcode SNAPSHOT - %s: %s", msg, cmd) + elif "snapshot-return" in tags: + logf("Snapshot gcode RETURN - %s: %s", msg, cmd) elif "snapshot-end" in tags: - logf("Snapshot gcode END - %s: %s", msg, cmd) + logf("Snapshot gcode END - %s: %s", msg, cmd) + elif "wait-for-position" in tags: + logf("Waiting for moves to complete before continuing - %s: %s", msg, cmd) elif "pre-processing-end" in tags: logf("Pre processing finished gcode - %s: %s", msg, cmd) elif "current-position" in tags: logf("Current position gcode - %s: %s", msg, cmd) + elif "before-snapshot-gcode" in tags: + logf("Before snapshot gcode - %s: %s", msg, cmd) + elif "after-snapshot-gcode" in tags: + logf("After snapshot gcode - %s: %s", msg, cmd) elif "camera-gcode" in tags: logf("Camera gcode - %s: %s", msg, cmd) elif "force_xyz_axis" in tags: @@ -1072,6 +1205,10 @@ def log_octolapse_gcode(self, logf, msg, cmd, tags): logf("Force E axis mode gcode - %s: %s", msg, cmd) elif "preview-stabilization" in tags: logf("Preview stabilization gcode - %s: %s", msg, cmd) + else: + return False + return True + # internal functions #################### @@ -1079,22 +1216,14 @@ def _send_state_changed_message(self): """Notifies any callbacks about any changes contained in the dictionaries. If you send a dict here the client will get a message, so check the settings to see if they are subscribed to notifications before populating the dictinaries!""" + try: - if self._last_state_changed_message_time is not None and self._state != TimelapseState.Idle: - # do not send more than 1 per second - - time_since_last_update = time.time() - self._last_state_changed_message_time - if time_since_last_update < self.state_update_period_seconds: - if self._position.current_pos.has_position_error: - self.has_position_errors_to_report = True - self.position_errors_to_report = self._position.current_pos.has_position_error + if self._last_state_changed_message_time + 1 > time.time(): return - - try: # Notify any callbacks if self._state_changed_callback is not None: - def send_real_time_change_message(has_printer_state_error): + def send_real_time_change_message(): trigger_change_list = None position_change_dict = None printer_state_change_dict = None @@ -1102,19 +1231,18 @@ def send_real_time_change_message(has_printer_state_error): trigger_changes_dict = None # Get the changes - if self._settings.main_settings.show_trigger_state_changes: + if self.get_current_octolapse_settings().main_settings.show_trigger_state_changes: trigger_change_list = self._triggers.state_to_list() - if self._settings.main_settings.show_position_changes: + if self.get_current_octolapse_settings().main_settings.show_position_changes: position_change_dict = self._position.to_position_dict() update_printer_state = ( - self._settings.main_settings.show_printer_state_changes - or has_printer_state_error + self.get_current_octolapse_settings().main_settings.show_printer_state_changes ) if update_printer_state: printer_state_change_dict = self._position.to_state_dict() - if self._settings.main_settings.show_extruder_state_changes: + if self.get_current_octolapse_settings().main_settings.show_extruder_state_changes: extruder_change_dict = self._position.current_pos.to_extruder_state_dict() # if there are any state changes, send them @@ -1147,7 +1275,7 @@ def send_real_time_change_message(has_printer_state_error): self._state_changed_callback(change_dict) def send_pre_calculated_change_message(): - if not self._settings.main_settings.show_snapshot_plan_information: + if not self.get_current_octolapse_settings().main_settings.show_snapshot_plan_information: return # if there are any state changes, send them change_dict = { @@ -1163,12 +1291,7 @@ def send_pre_calculated_change_message(): self._state_changed_callback(change_dict) if self.is_realtime: - position_errors = False - if self.has_position_errors_to_report: - position_errors = self.position_errors_to_report - elif self._position.current_pos.has_position_error: - position_errors = self._position.current_pos.position_error - send_real_time_change_message(position_errors) + send_real_time_change_message() else: send_pre_calculated_change_message() @@ -1188,44 +1311,13 @@ def _is_trigger_waiting(self): return True return False - def _on_position_error(self): - # rate limited position error notification - delay_seconds = 0 - # if another thread is trying to send the message, stop it - if self._position_error_message_thread is not None and self._position_error_message_thread.isAlive(): - self._position_error_message_thread.cancel() - - if self._last_position_error_message_time is not None: - # do not send more than 1 per second - time_since_last_update = time.time() - self._last_position_error_message_time - if time_since_last_update < 1: - delay_seconds = 1 - time_since_last_update - if delay_seconds < 0: - delay_seconds = 0 - - message = self._position.current_pos.position_error - logger.error(message) - - def _send_position_error(position_error_message): - self._position_error_callback(position_error_message) - self._last_position_error_message_time = time.time() - - # Send a delayed message - self._position_error_message_thread = threading.Timer( - delay_seconds, - _send_position_error, - [message] - - ) - self._position_error_message_thread.daemon = True - self._position_error_message_thread.start() - def _on_trigger_snapshot_complete(self, snapshot_payload): if self._snapshot_complete_callback is not None: payload = { "success": snapshot_payload["success"], "error": snapshot_payload["error"], "snapshot_count": self._capture_snapshot.SnapshotsTotal, + "snapshot_failed_count": self._capture_snapshot.ErrorsTotal, "snapshot_payload": snapshot_payload["snapshot_payload"], } @@ -1236,67 +1328,18 @@ def _on_trigger_snapshot_complete(self, snapshot_payload): snapshot_complete_callback_thread.start() def _render_timelapse(self, print_end_state): - # make sure we have a non null TimelapseSettings object. We may have terminated the timelapse for some reason - if self._rendering_processor is not None: - if self._settings.profiles.current_rendering().enabled: - # If we are still taking snapshots, wait for them all to finish - if self.get_is_taking_snapshot(): - logger.info("Snapshot jobs are running, waiting for them to finish before rendering.") - self._snapshot_task_queue.join() - logger.info("Snapshot jobs queue has completed, starting to render.") - self._current_job_info.PrintEndTime = time.time() - self._current_job_info.PrintEndState = print_end_state - for camera in self._settings.profiles.active_cameras(): - rendering_job_info = RenderJobInfo( - self._current_job_info, - self._data_folder, - camera, - self._settings.profiles.current_rendering(), - self._ffmpeg_path - ) - self._rendering_task_queue.put(rendering_job_info) - return True - return False - - def _on_prerender_start(self, payload): - assert (isinstance(payload, RenderingCallbackArgs)) - logger.info("Started prerendering the timelapse. JobId: %s", payload.JobId) - - # notify the caller - if self._prerender_start_callback is not None: - self._prerender_start_callback(payload) - - def _on_render_start(self, payload): - assert (isinstance(payload, RenderingCallbackArgs)) - logger.info("Started rendering/synchronizing the timelapse. JobId: %s", payload.JobId) - - # notify the caller - if self._render_start_callback is not None: - self._render_start_callback(payload) - - def _on_render_success(self, payload): - assert (isinstance(payload, RenderingCallbackArgs)) - logger.info("Completed rendering. JobId: %s", payload.JobId) - - if self._settings.profiles.current_rendering().cleanup_after_render_complete: - self._capture_snapshot.clean_snapshots(payload.SnapshotDirectory, payload.JobDirectory) - - if self._render_success_callback is not None: - self._render_success_callback(payload) - - def _on_render_error(self, payload, error): - assert (isinstance(payload, RenderingCallbackArgs)) - logger.info("Completed rendering. JobId: %s", payload.JobId) - - if self._settings.profiles.current_rendering().cleanup_after_render_fail: - self._capture_snapshot.clean_snapshots(payload.SnapshotDirectory, payload.JobDirectory) - - if self._render_error_callback is not None: - self._render_error_callback(payload, error) - - def _on_render_end(self): - logger.info("Completed all pending rendering jobs.") - self._render_end_callback() + if self.was_started: + # If we are still taking snapshots, wait for them all to finish + if self.get_is_taking_snapshot(): + logger.info("Snapshot jobs are running, waiting for them to finish before rendering.") + self._snapshot_task_queue.join() + logger.info("Snapshot jobs queue has completed, starting to render.") + # todo: update print job info + self._current_job_info.PrintEndTime = time.time() + self._current_job_info.PrintEndState = print_end_state + self._current_job_info.save(self._temporary_folder) + for camera in self._settings.profiles.active_cameras(): + self._on_rendering_start_callback(self._current_job_info.JobGuid, camera.guid, self._temporary_folder) def _reset(self): self._state = TimelapseState.Idle @@ -1305,11 +1348,11 @@ def _reset(self): self._triggers.reset() self.CommandIndex = -1 - self._last_state_changed_message_time = None + self._last_state_changed_message_time = 0 self._current_job_info = None self._snapshotGcodes = None self._positionRequestAttempts = 0 - self._is_test_mode = False + self._test_mode_enabled = False self._position_request_sent = False # A list of callbacks who want to be informed when a timelapse ends @@ -1324,13 +1367,13 @@ def _reset(self): "snapshot": "", "rendering": "", "camera": "", - "debug_profile": "" + "logging_profile": "" } # fetch position private variables self._position_payload = None self._position_signal.set() - self._snapshot_signal.set() self._current_job_info = None + self.was_started = False def _reset_snapshot(self): self._state = TimelapseState.WaitingForTrigger diff --git a/octoprint_octolapse/trigger.py b/octoprint_octolapse/trigger.py index 81e7e87e..721d30fd 100644 --- a/octoprint_octolapse/trigger.py +++ b/octoprint_octolapse/trigger.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -36,35 +36,31 @@ class Triggers(object): TRIGGER_TYPE_IN_PATH = 'in-path' def __init__(self, settings): - self.trigger_profile = None self._triggers = [] self.reset() self._settings = settings self.name = "Unknown" - self.printer = None def count(self): return len(self._triggers) def reset(self): - self.trigger_profile = None self._triggers = [] def create(self): self.reset() - self.printer = self._settings.profiles.current_printer() - self.trigger_profile = self._settings.profiles.current_trigger() - self.name = self.trigger_profile.name + trigger_profile = self._settings.profiles.current_trigger() + self.name = trigger_profile.name # create the triggers # If the gcode trigger is enabled, add it - if self.trigger_profile.trigger_subtype == TriggerProfile.GCODE_TRIGGER_TYPE: + if trigger_profile.trigger_subtype == TriggerProfile.GCODE_TRIGGER_TYPE: # Add the trigger to the list self._triggers.append(GcodeTrigger(self._settings)) # If the layer trigger is enabled, add it - elif self.trigger_profile.trigger_subtype == TriggerProfile.LAYER_TRIGGER_TYPE: + elif trigger_profile.trigger_subtype == TriggerProfile.LAYER_TRIGGER_TYPE: self._triggers.append(LayerTrigger(self._settings)) # If the layer trigger is enabled, add it - elif self.trigger_profile.trigger_subtype == TriggerProfile.TIMER_TRIGGER_TYPE: + elif trigger_profile.trigger_subtype == TriggerProfile.TIMER_TRIGGER_TYPE: self._triggers.append(TimerTrigger(self._settings)) def resume(self): @@ -77,10 +73,10 @@ def pause(self): if type(trigger) == TimerTrigger: trigger.pause() - def update(self, position, parsed_command): + def update(self, position): # the previous command (not just the current) MUST have homed positions else # we may have some null coordinates. - #if not position.current_pos.has_homed_position: + #if not position.current_pos.has_definite_position: # return ## Note: I think we need to add waits to handle the above """Update all triggers and return any that are triggering""" @@ -89,15 +85,11 @@ def update(self, position, parsed_command): for current_trigger in self._triggers: # determine what type the current trigger is and update appropriately if isinstance(current_trigger, GcodeTrigger): - current_trigger.update(position, parsed_command) + current_trigger.update(position) elif isinstance(current_trigger, TimerTrigger): current_trigger.update(position) elif isinstance(current_trigger, LayerTrigger): current_trigger.update(position) - - # Make sure there are no position errors (unknown position, out of bounds, etc) - if position.current_pos.has_position_error: - logger.error("A trigger has a position error:%s", position.current_pos.position_error) # see if the current trigger is triggering, indicting that a snapshot should be taken except Exception as e: logger.exception("Failed to update the snapshot triggers.") @@ -160,7 +152,7 @@ def __init__(self, state=None): self.is_waiting_on_zhop = False if state is None else state.is_waiting_on_zhop self.is_waiting_on_extruder = False if state is None else state.is_waiting_on_extruder self.has_changed = False if state is None else state.has_changed - self.is_homed = False if state is None else state.is_homed + self.has_definite_position = False if state is None else state.has_definite_position def to_dict(self, trigger): return { @@ -174,7 +166,7 @@ def to_dict(self, trigger): "is_waiting_on_extruder": self.is_waiting_on_extruder, "has_changed": self.has_changed, "require_zhop": trigger.require_zhop, - "is_homed": self.is_homed, + "has_definite_position": self.has_definite_position, "trigger_count": trigger.trigger_count } @@ -195,7 +187,7 @@ def is_equal(self, state): and self.is_home_position_wait == state.is_home_position_wait and self.is_waiting_on_zhop == state.is_waiting_on_zhop and self.is_waiting_on_extruder == state.is_waiting_on_extruder - and self.is_homed == state.is_homed): + and self.has_definite_position == state.has_definite_position): return True return False @@ -206,12 +198,20 @@ def __init__(self, octolapse_settings, max_states=5): self._settings = octolapse_settings self.printer = self._settings.profiles.current_printer() self.trigger_profile = self._settings.profiles.current_trigger() - self.type = 'Trigger' self._state_history = [] self._max_states = max_states self.extruder_triggers = None self.trigger_count = 0 + self.snapshots_enabled = True + + def update(self, position): + parsed_command = position.current_pos.parsed_command + if parsed_command.is_octolapse_command: + if "STOP-SNAPSHOTS" in parsed_command.parameters: + self.snapshots_enabled = False + elif "START-SNAPSHOTS" in parsed_command.parameters: + self.snapshots_enabled = True def name(self): return self.trigger_profile.name + " Trigger" @@ -284,10 +284,9 @@ class GcodeTrigger(Trigger): def __init__(self, octolapse_settings): # call parent constructor super(GcodeTrigger, self).__init__(octolapse_settings) - self.snapshot_command = self.printer.snapshot_command self.type = "gcode" self.require_zhop = self.trigger_profile.require_zhop - + self.snapshot_command = octolapse_settings.profiles.current_printer().snapshot_command if self.trigger_profile.extruder_state_requirements_enabled: self.extruder_triggers = ExtruderTriggers( self.trigger_profile.trigger_on_extruding_start, @@ -324,16 +323,19 @@ def __init__(self, octolapse_settings): message = "Creating Gcode Trigger - Gcode Command:%s, require_zhop:%s" logger.info( message, - self.printer.snapshot_command, + self.snapshot_command, self.trigger_profile.require_zhop ) # add an initial state self.add_state(GcodeTriggerState()) - def update(self, position, parsed_command): + def update(self, position): + super(GcodeTrigger, self).update(position) + parsed_command = position.current_pos.parsed_command """If the provided command matches the trigger command, sets is_triggered to true, else false""" try: + # get the last state to use as a starting point for the update # if there is no state, this will return the default state state = self.get_state(0) @@ -345,32 +347,59 @@ def update(self, position, parsed_command): state = GcodeTriggerState(state) # reset any variables that must be reset each update state.reset_state() + + # set the trigger position. It should be the previous position, not the current + trigger_position = position.previous_pos + # Don't update the trigger if we don't have a homed axis - # Make sure to use the previous value so the homing operation can complete - if not position.current_pos.has_homed_position: + if not trigger_position.has_definite_position: state.is_triggered = False - state.is_homed = False + state.has_definite_position = False else: - state.is_homed = True + state.has_definite_position = trigger_position.has_definite_position # check to see if we are in the proper position to take a snapshot # set is in position - state.is_in_position = position.current_pos.is_in_position and position.current_pos.is_in_bounds + state.is_in_position = trigger_position.is_in_position and trigger_position.is_in_bounds state.in_path_position = position.current_pos.in_path_position - if self.snapshot_command.lower() == parsed_command.gcode.lower(): - state.is_waiting = True + if self.printer.is_snapshot_command(parsed_command.gcode): + if self.snapshots_enabled: + state.is_waiting = True + else: + logger.info("GcodeTrigger - A snapshot was detected, but snapshots were disabled via " + "@Octolapse stop-snapshots.") if state.is_waiting: - if position.is_extruder_triggered(self.extruder_triggers): - if not position.previous_pos.has_homed_position: - state.is_waiting_for_homed_position = True - logger.debug("GcodeTrigger - Triggering - Waiting for the previous position to be homed.") - elif self.require_zhop and not position.current_pos.is_zhop: + if position.is_previous_extruder_triggered(self.extruder_triggers): + if not trigger_position.has_definite_position: + state.is_waiting_for_definite_position = True + logger.debug("GcodeTrigger - Triggering - Waiting for a definite previous position.") + elif self.require_zhop and not trigger_position.is_zhop: state.is_waiting_on_zhop = True logger.debug("GcodeTrigger - Waiting on ZHop.") + elif not trigger_position.is_in_bounds: + logger.debug("GcodeTrigger - Waiting for in-bounds position.") elif not state.is_in_position and not state.in_path_position: # Make sure the previous X,Y is in position logger.debug("GcodeTrigger - Waiting on Position.") + elif not trigger_position.last_extrusion_height: + logger.debug( + "GcodeTrigger - Waiting for at least one extrusion on a previous layer." + ) + elif not utility.greater_than_or_equal( + trigger_position.z, trigger_position.last_extrusion_height + ): + # The extruder is below the last extrusion height, do not take a snapshot else we might + # run into the part! + logger.debug( + "GcodeTrigger - Waiting for extruder to move above the highest extrusion point." + ) + elif not self.snapshots_enabled: + # Snapshot have been disabled by an octolapse gcode command + logger.debug( + "GcodeTrigger - Waiting for snapshots to be enabled via @Octolapse start-snapshots " + "command. " + ) else: state.is_triggered = True self.trigger_count += 1 @@ -495,6 +524,7 @@ def __init__(self, octolapse_settings): def update(self, position): """Updates the layer monitor position. x, y and z may be absolute, but e must always be relative""" + super(LayerTrigger, self).update(position) try: # get the last state to use as a starting point for the update # if there is no state, this will return the default state @@ -508,16 +538,20 @@ def update(self, position): # reset any variables that must be reset each update state.reset_state() + + # set the trigger position. It should be the previous position, not the current + trigger_position = position.previous_pos # Don't update the trigger if we don't have a homed axis - # Make sure to use the previous value so the homing operation can complete - if not position.current_pos.has_homed_position: + if not trigger_position.has_definite_position: state.is_triggered = False - state.is_homed = False + state.has_definite_position = False else: - state.is_homed = True + state.has_definite_position = True # set is in position - state.is_in_position = position.current_pos.is_in_position and position.current_pos.is_in_bounds + state.is_in_position = trigger_position.is_in_position and trigger_position.is_in_bounds + # the in path position will be our CURRENT POSITION not the trigger position + # (which is the previous position) state.in_path_position = position.current_pos.in_path_position # calculate height increment changed @@ -526,16 +560,16 @@ def update(self, position): and self.height_increment > 0 and position.current_pos.is_layer_change and ( - state.current_increment * self.height_increment < position.current_pos.height or + state.current_increment * self.height_increment < trigger_position.height or state.current_increment == 0 ) ): - new_increment = int(math.ceil(position.current_pos.height/self.height_increment)) + new_increment = int(math.ceil(trigger_position.height/self.height_increment)) if new_increment <= state.current_increment: message = ( - "Layer Trigger - Warning - The height increment was expected to increase, but it did not." + "Layer Trigger - Warning - The height increment was expected to increase, but it did not." " Height Increment:%s, Current Increment:%s, Calculated Increment:%s" ) logger.warning( @@ -556,7 +590,7 @@ def update(self, position): "Layer Trigger - Height Increment:%s, Current Increment:%s, Height: %s", self.height_increment, state.current_increment, - position.current_pos.height + trigger_position.height ) # see if we've encountered a layer or height change @@ -566,8 +600,9 @@ def update(self, position): state.is_waiting = True else: + # see if the CURRENT position is a layer change if position.current_pos.is_layer_change: - state.layer = position.current_pos.layer + state.layer = trigger_position.layer state.is_layer_change_wait = True state.is_layer_change = True state.is_waiting = True @@ -575,7 +610,7 @@ def update(self, position): if state.is_height_change_wait or state.is_layer_change_wait or state.is_waiting: state.is_waiting = True # see if the extruder is triggering - is_extruder_triggering = position.is_extruder_triggered(self.extruder_triggers) + is_extruder_triggering = position.is_previous_extruder_triggered(self.extruder_triggers) if not is_extruder_triggering: state.is_waiting_on_extruder = True if state.is_height_change_wait: @@ -583,15 +618,36 @@ def update(self, position): elif state.is_layer_change_wait: logger.debug("LayerTrigger - Layer change triggering, waiting on extruder.") else: - if not position.previous_pos.has_homed_position: - state.is_waiting_for_homed_position = True - logger.debug("LayerTrigger - Triggering - Waiting for the previous position to be homed.") - elif self.require_zhop and not position.current_pos.is_zhop: + if not trigger_position.has_definite_position: + state.is_waiting_for_definite_position = True + logger.debug("LayerTrigger - Triggering - Waiting for a definite previous position.") + elif self.require_zhop and not trigger_position.is_zhop: state.is_waiting_on_zhop = True logger.debug("LayerTrigger - Triggering - Waiting on ZHop.") + elif not trigger_position.is_in_bounds: + logger.debug("GcodeTrigger - Waiting for in-bounds position.") elif not state.is_in_position and not state.in_path_position: # Make sure the previous X,Y is in position logger.debug("LayerTrigger - Waiting on Position.") + elif not trigger_position.last_extrusion_height: + # this should never be hit, but just in case! + logger.debug( + "LayerTrigger - Waiting for at least one extrusion on a previous layer." + ) + elif utility.less_than( + trigger_position.z, trigger_position.last_extrusion_height + ): + # The extruder is below the last extrusion height, do not take a snapshot else we might + # run into the part! + logger.debug( + "LayerTrigger - Waiting for extruder to move above the highest extrusion point." + ) + elif not self.snapshots_enabled: + # Snapshot have been disabled by an octolapse gcode command + logger.debug( + "LayerTrigger - Waiting for snapshots to be enabled via @Octolapse start-snapshots " + "command. " + ) else: if state.is_height_change_wait: logger.debug("LayerTrigger - Height change triggering.") @@ -736,6 +792,7 @@ def resume(self): state.pause_time = None def update(self, position): + super(TimerTrigger, self).update(position) try: # get the last state to use as a starting point for the update # if there is no state, this will return the default state @@ -750,19 +807,20 @@ def update(self, position): state.reset_state() state.is_triggered = False + # set the trigger position. It should be the previous position, not the current + trigger_position = position.previous_pos # Don't update the trigger if we don't have a homed axis - # Make sure to use the previous value so the homing operation can complete - if not position.current_pos.has_homed_position: + if not trigger_position.has_definite_position: state.is_triggered = False - state.is_homed = False + state.has_definite_position = False else: - state.is_homed = True + state.has_definite_position = True # record the current time to keep things consistant current_time = time.time() # set is in position - state.is_in_position = position.current_pos.is_in_position and position.current_pos.is_in_bounds + state.is_in_position = trigger_position.is_in_position and trigger_position.is_in_bounds state.in_path_position = position.current_pos.in_path_position # if the trigger start time is null, set it now. @@ -790,17 +848,36 @@ def update(self, position): state.is_waiting = True # see if the exturder is in the right position - if position.is_extruder_triggered(self.extruder_triggers): - if not position.previous_pos.has_homed_position: - state.is_waiting_for_homed_position = True - logger.debug("TimerTrigger - Triggering - Waiting for the previous position to be homed.") - if self.require_zhop and not position.current_pos.is_zhop: + if position.is_previous_extruder_triggered(self.extruder_triggers): + if not trigger_position.has_definite_position: + state.is_waiting_for_definite_position = True + logger.debug("TimerTrigger - Triggering - Waiting for a definite previous position.") + if self.require_zhop and not trigger_position.is_zhop: logger.debug("TimerTrigger - Waiting on ZHop.") state.is_waiting_on_zhop = True + elif not trigger_position.is_in_bounds: + logger.debug("TimerTrigger - Waiting for in-bounds position.") elif not state.is_in_position and not state.in_path_position: # Make sure the previous X,Y is in position - logger.debug("TimerTrigger - Waiting on Position.") + elif not trigger_position.last_extrusion_height: + logger.debug( + "TimerTrigger - Waiting for at least one extrusion on a previous layer." + ) + elif not utility.greater_than_or_equal( + trigger_position.z, trigger_position.last_extrusion_height + ): + # The extruder is below the last extrusion height, do not take a snapshot else we might + # run into the part! + logger.debug( + "TimerTrigger - Waiting for extruder to move above the highest extrusion point." + ) + elif not self.snapshots_enabled: + # Snapshot have been disabled by an octolapse gcode command + logger.debug( + "TimerTrigger - Waiting for snapshots to be enabled via @Octolapse start-snapshots " + "command. " + ) else: # Is Triggering self.trigger_count += 1 diff --git a/octoprint_octolapse/utility.py b/octoprint_octolapse/utility.py index 63682a98..4ba46d72 100644 --- a/octoprint_octolapse/utility.py +++ b/octoprint_octolapse/utility.py @@ -1,7 +1,7 @@ # coding=utf-8 ################################################################################## # Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. -# Copyright (C) 2017 Brad Hochgesang +# Copyright (C) 2023 Brad Hochgesang ################################################################################## # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published @@ -22,23 +22,36 @@ ################################################################################## from __future__ import unicode_literals import math +import uuid import ntpath import os import re import subprocess import sys import time +import urllib import traceback import threading -import psutil -from threading import Timer +import json +import shutil +import errno +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry +# Todo: Determin if this is still necessary. +try: + from slugify import Slugify +except ImportError: + from octoprint.vendor.awesome_slugify import Slugify +_SLUGIFY = Slugify() +_SLUGIFY.safe_chars = "-_.()[] " # create the module level logger from octoprint_octolapse.log import LoggingConfigurator logging_configurator = LoggingConfigurator() logger = logging_configurator.get_logger(__name__) - +from threading import Timer FLOAT_MATH_EQUALITY_RANGE = 0.0000001 def get_float(value, default): @@ -87,7 +100,7 @@ def get_string(value, default): # global for bitrate regex octoprint_ffmpeg_bitrate_regex = re.compile( - "^\d+[KkMm]$", re.IGNORECASE) + r"^\d+[KkMm]$", re.IGNORECASE) def get_bitrate(value, default): @@ -105,12 +118,89 @@ def is_sequence(arg): hasattr(arg, "__iter__")) -def get_filename_from_full_path(path): + + +def sanitize_filename(filename): + if filename is None: + return None + if u"/" in filename or u"\\" in filename: + raise ValueError("name must not contain / or \\") + + result = _SLUGIFY(filename).replace(u" ", u"_") + if result and result != u"." and result != u".." and result[0] == u".": + # hidden files under *nix + result = result[1:] + return result + + +def split_all(path): + allparts = [] + while 1: + parts = os.path.split(path) + if parts[0] == path: # sentinel for absolute paths + allparts.insert(0, parts[0]) + break + elif parts[1] == path: # sentinel for relative paths + allparts.insert(0, parts[1]) + break + else: + path = parts[0] + allparts.insert(0, parts[1]) + return allparts + + +def get_directory_from_full_path(path): + return os.path.dirname(path) + + +def get_filename_from_full_path(path): basename = ntpath.basename(path) head, tail = ntpath.split(basename) file_name = tail or ntpath.basename(head) - return os.path.splitext(file_name)[0] + split_filename = os.path.splitext(file_name) + if len(split_filename) > 0: + return split_filename[0] + return "" + + +def remove_extension_from_filename(filename): + return os.path.splitext(filename)[0] + + +def get_extension_from_full_path(path): + return get_extension_from_filename(ntpath.basename(path)) + + +def get_extension_from_filename(filename): + head, tail = ntpath.split(filename) + file_name = tail or ntpath.basename(head) + split_filename = os.path.splitext(file_name) + if len(split_filename) > 1: + extension = split_filename[1] + if len(split_filename) > 1: + return extension[1:] + return "" + + +def get_collision_free_filepath(path): + filename = get_filename_from_full_path(path) + directory = get_directory_from_full_path(path) + extension = get_extension_from_full_path(path) + + original_filename = filename + file_number = 0 + # Check to see if the file exists, if it does add a number to the end and continue + while os.path.isfile( + os.path.join( + directory, + "{0}.{1}".format(filename, extension) + ) + ): + file_number += 1 + filename = "{0}_{1}".format(original_filename, file_number) + + return os.path.join(directory, "{0}.{1}".format(filename, extension)) def greater_than_or_close(a, b, abs_tol): @@ -121,8 +211,12 @@ def greater_than(a, b): return a - b > FLOAT_MATH_EQUALITY_RANGE +def less_than(a, b): + return b - a > FLOAT_MATH_EQUALITY_RANGE + + def less_than_or_equal(a, b): - return a < b or is_equal(a,b) + return a < b or is_equal(a, b) def greater_than_or_equal(a, b): @@ -132,6 +226,7 @@ def greater_than_or_equal(a, b): def is_equal(a,b): return a - b < FLOAT_MATH_EQUALITY_RANGE + def less_than_or_close(a, b, abs_tol): return a - b < abs_tol @@ -146,12 +241,6 @@ def is_close(a, b, abs_tol=0.01000): def round_to_float_equality_range(n): return (n * 10000000 + 0.00000005)//1 / 10000000.0 - #return round(n / 0.0000001) * 0.0000001 - #answer = math.floor(n*10000000)/10000000 if n > 0 else math.ceil(n*10000000)/10000000 - return round(n,8) - - #return answer - # slow - return round(n,6) def round_to(n, precision): @@ -166,36 +255,91 @@ def round_up(value): return -(-value // 1.0) -def get_temp_snapshot_driectory_template(): - return "{0}{1}{2}{3}".format("{DATADIRECTORY}", os.sep, "tempsnapshots", os.sep) +_snapshot_archive_default_directory = "snapshot_archive" + + +def get_default_snapshot_archive_directory_name(): + return _snapshot_archive_default_directory + + +_temporary_snapshot_subdirectory = "octolapse_snapshots_tmp" + + +def get_temporary_snapshot_directory(temporary_directory): + return os.path.join(temporary_directory, _temporary_snapshot_subdirectory) + + +def get_temporary_snapshot_job_path(temporary_directory, job_guid): + return os.path.join( + get_temporary_snapshot_directory(temporary_directory), + job_guid) + + +def get_temporary_snapshot_job_camera_path(temporary_directory, job_guid, camera_guid): + return os.path.join( + get_temporary_snapshot_job_path(temporary_directory, job_guid), + camera_guid) + + +_temporary_rendering_subdirectory = "octolapse_rendering_tmp" + + +def get_temporary_rendering_directory(temporary_directory): + return os.path.join(temporary_directory, _temporary_rendering_subdirectory) -def get_snapshot_directory(data_directory): - return "{0}{1}{2}{3}".format(data_directory, os.sep, "snapshots", os.sep) +_temporary_archive_subdirectory = "octolapse_archive_tmp" +def get_temporary_archive_directory(temporary_directory): + return os.path.join(temporary_directory, _temporary_archive_subdirectory) -def get_snapshot_filename_template(): - return os.path.join("{FILENAME}") +def get_temporary_archive_path(temporary_directory): + file_name = "{0}.zip".format(uuid.uuid4()) + return os.path.join(get_temporary_archive_directory(temporary_directory), file_name) -def get_rendering_directory_from_data_directory(data_directory): - return get_rendering_directory_template().replace("{DATADIRECTORY}", data_directory) +snapshot_archive_extension = "zip" -def get_latest_snapshot_download_path(data_directory, camera_guid, base_folder=None): - if not camera_guid or camera_guid == "undefined" and base_folder : +def get_snapshot_archive_filename(rendering_filename): + return "{0}.{1}".format(rendering_filename, snapshot_archive_extension) + + +no_archive_filename = "no-archive.octolapse" + + +def create_no_archive_file(temporary_directory, job_guid, camera_guid): + camera_job_directory = get_temporary_snapshot_job_camera_path(temporary_directory, job_guid, camera_guid) + # create the directory if it does not exist + if not os.path.isdir(camera_job_directory): + os.makedirs(camera_job_directory) + + # create the no archive file path + no_archive_file_path = os.path.join(camera_job_directory, no_archive_filename) + # create the file + with open(no_archive_file_path, mode='w'): + pass # nothing to do, just create the file + + +def has_no_archive_file(temporary_directory, job_guid, camera_guid): + camera_job_directory = get_temporary_snapshot_job_camera_path(temporary_directory, job_guid, camera_guid) + no_archive_file_path = os.path.join(camera_job_directory, no_archive_filename) + return os.path.isfile(no_archive_file_path) + +def get_latest_snapshot_download_path(temporary_directory, camera_guid, base_folder=None): + if (not camera_guid or camera_guid == "undefined") and base_folder: return get_images_download_path(base_folder, "no-camera-selected.png") - return "{0}{1}".format(get_snapshot_directory(data_directory), "latest_{0}.jpeg".format(camera_guid)) + return os.path.join(get_temporary_snapshot_directory(temporary_directory), "latest_{0}.jpeg".format(camera_guid)) -def get_latest_snapshot_thumbnail_download_path(data_directory, camera_guid, base_folder=None): - if not camera_guid or camera_guid == "undefined" and base_folder : +def get_latest_snapshot_thumbnail_download_path(temporary_directory, camera_guid, base_folder=None): + if (not camera_guid or camera_guid == "undefined") and base_folder: return get_images_download_path(base_folder, "no-camera-selected.png") - return "{0}{1}".format(get_snapshot_directory(data_directory), "latest_thumb_{0}.jpeg".format(camera_guid)) + return os.path.join(get_temporary_snapshot_directory(temporary_directory), "latest_thumb_{0}.jpeg".format(camera_guid)) def get_images_download_path(base_folder, file_name): - return "{0}{1}data{2}{3}{4}{5}".format(base_folder, os.sep, os.sep, "Images", os.sep, file_name) + return os.path.join(base_folder, "data", "Images", file_name) def get_error_image_download_path(base_folder): @@ -206,51 +350,70 @@ def get_no_snapshot_image_download_path(base_folder): return get_images_download_path(base_folder, "no_snapshot.png") -def get_rendering_directory_template(): - return "{0}{1}{2}{3}".format("{DATADIRECTORY}", os.sep, "timelapses", os.sep) - - def get_rendering_base_filename_template(): return "{FILENAME}_{DATETIMESTAMP}" -def get_rendering_filename(template, tokens): - template.format(**tokens) - - def get_rendering_base_filename(print_name, print_start_time, print_end_time=None): file_template = get_rendering_base_filename_template() file_template = file_template.replace("{FILENAME}", get_string(print_name, "")) file_template = file_template.replace( "{DATETIMESTAMP}", time.strftime("%Y%m%d%H%M%S", time.localtime(time.time()))) file_template = file_template.replace( - "{PRINTSTARTTIME}", time.strftime("%Y%m%d%H%M%S", time.localtime(print_start_time))) - if print_end_time is not None: - file_template = file_template.replace( - "{PRINTENDTIME}", time.strftime("%Y%m%d%H%M%S", time.localtime(print_end_time))) + "{PRINTSTARTTIME}", + "UNKNOWN" if not print_start_time else time.strftime("%Y%m%d%H%M%S", time.localtime(print_start_time)) + ) + file_template = file_template.replace( + "{PRINTENDTIME}", + "UNKNOWN" if not print_end_time else time.strftime("%Y%m%d%H%M%S", time.localtime(print_end_time)) + ) return file_template -def get_snapshot_filename(print_name, print_start_time, snapshot_number): - file_template = get_snapshot_filename_template() \ - .format(FILENAME=get_string(print_name, ""), - DATETIMESTAMP="{0:d}".format(math.trunc(round(time.time(), 2) * 100)), - PRINTSTARTTIME="{0:d}".format(math.trunc(round(print_start_time, 2) * 100))) - return "{0}{1}.{2}".format(file_template, format_snapshot_number(snapshot_number), "jpg") +snapshot_file_extensions = ["jpg"] + + +default_snapshot_extension = snapshot_file_extensions[0] -def get_pre_roll_snapshot_filename(print_name, print_start_time, snapshot_number): - file_template = get_snapshot_filename_template() \ - .format(FILENAME=get_string(print_name, ""), - DATETIMESTAMP="{0:d}".format(math.trunc(round(time.time(), 2) * 100)), - PRINTSTARTTIME="{0:d}".format(math.trunc(round(print_start_time, 2) * 100))) + +def is_valid_snapshot_extension(extension): + return extension.lower() in snapshot_file_extensions + + +temporary_extension = "tmp" + + +def is_valid_temporary_extension(extension): + return extension.lower() == temporary_extension + + +def get_snapshot_filename(print_name, snapshot_number): + return "{0}{1}.{2}".format( + print_name, + format_snapshot_number(snapshot_number), + default_snapshot_extension + ) + + +def get_pre_roll_snapshot_filename(print_name, snapshot_number): return "{0}{1}_{2}.{3}".format( - file_template, + print_name, format_snapshot_number(snapshot_number), format_snapshot_number(snapshot_number), "jpg") -SnapshotNumberFormat = "%06d" +SnaphotNumberDigits = 6 +SnapshotNumberFormat = "%0{0}d".format(SnaphotNumberDigits) + + +def get_snapshot_number_from_path(path): + try: + if path.upper().endswith(".JPG") and len(path) > SnaphotNumberDigits + 4: + return int(path[len(path)-SnaphotNumberDigits-4:len(path)-4]) + except ValueError: + pass + return -1 def format_snapshot_number(number): @@ -261,31 +424,6 @@ def format_snapshot_number(number): return number -def get_snapshot_temp_directory(data_directory): - directory_template = get_temp_snapshot_driectory_template() - directory_template = directory_template.replace( - "{DATADIRECTORY}", data_directory) - - return directory_template - - -def get_rendering_directory(data_directory, print_name, print_start_time, output_extension, print_end_time=None): - directory_template = get_rendering_directory_template() - directory_template = directory_template.replace( - "{FILENAME}", get_string(print_name, "")) - directory_template = directory_template.replace( - "{OUTPUTFILEEXTENSION}", get_string(output_extension, "")) - directory_template = directory_template.replace("{PRINTSTARTTIME}", - "{0:d}".format(math.trunc(round(print_start_time, 2) * 100))) - if print_end_time is not None: - directory_template = directory_template.replace("{PRINTENDTIME}", - "{0:d}".format(math.trunc(round(print_end_time, 2) * 100))) - directory_template = directory_template.replace( - "{DATADIRECTORY}", data_directory) - - return directory_template - - def seconds_to_hhmmss(seconds): hours = seconds // (60 * 60) seconds %= (60 * 60) @@ -293,9 +431,6 @@ def seconds_to_hhmmss(seconds): seconds %= 60 return "%02i:%02i:%02i" % (hours, minutes, seconds) -class SafeDict(dict): - def __missing__(self, key): - return '{' + key + '}' def get_currently_printing_file_path(octoprint_printer): if octoprint_printer is not None: @@ -364,13 +499,6 @@ def clamp(v, v_min, v_max): raise ValueError("We've not implemented circular bed stuff yet!") -def is_snapshot_command(command_string, snapshot_command): - # note that self.Printer.snapshot_command is stripped of comments. - if snapshot_command is not None and len(snapshot_command)>0: - return command_string.lower() == snapshot_command.lower() - return False - - def get_intersections_circle(x1, y1, x2, y2, c_x, c_y, c_radius): # Finds any intersections as well as the closest point on or within the circle to the center (cx, cy) intersections = [] @@ -511,262 +639,396 @@ def get_intersections_rectangle(x1, y1, x2, y2, rect_x1, rect_y1, rect_x2, rect_ return False -def exception_to_string(e): - trace_back = sys.exc_info()[2] - if trace_back is None: - return "{}".format(e) - tb_lines = traceback.format_exception(e.__class__, e, trace_back) - return ''.join(tb_lines) - - def get_system_fonts(base_directory): - """Retrieves a list of fonts for any operating system. Note that this may not be a complete list of fonts discoverable on the system. - :returns A list of filepaths to fonts available on the system.""" + """Retrieves a list of fonts for any operating system. Note that this may not be a complete list of fonts + discoverable on the system. + :returns A list of filepaths to fonts available on the system.""" font_paths = [] font_names = set() # first add all of our supplied fonts default_font_path = os.path.join(base_directory, "data", "fonts", "DejaVu") + logger.info("Searching for default fonts at: %s", default_font_path) for f in os.listdir(default_font_path): - if f.endswith(".ttf"): + font_path = os.path.join(default_font_path, f) + logger.verbose("Checking for font at path: %s", f) + if os.path.isfile(font_path) and f.endswith(".ttf"): + logger.debug("Found font at path: %s", f) font_names.add(f) font_paths.append(os.path.join(default_font_path, f)) if sys.platform == "linux" or sys.platform == "linux2" or sys.platform == "darwin": # Linux and OS X. - linux_font_paths = subprocess.check_output("fc-list --format %{file}\\n".split()).split('\n') + # Try 1 - fails for some + # linux_font_paths = subprocess.check_output("fc-list --format %{file}\\n".split()).split('\n') + # Try 2 - fails for others.... + # linux_font_paths = str(subprocess.check_output(["fc-list", "--format", "%{file}\n"]), 'UTF-8').split('\n') + + fc_list_output = subprocess.check_output(["fc-list", "--format", "%{file}\n"], universal_newlines=True) + if isinstance(fc_list_output, bytes): + fc_list_output = fc_list_output.decode("UTF-8") + + logger.verbose("Searching for fonts within:\n %s", fc_list_output) + + linux_font_paths = list(filter(None, fc_list_output.split('\n'))) for f in linux_font_paths: + logger.verbose("Checking for font at path: %s", f) font_name = os.path.basename(f) if not font_name in font_names: - font_names.add(f) + logger.debug("Font found at path: %s", f) + font_names.add(font_name) font_paths.append(f) elif sys.platform == "win32" or sys.platform == "cygwin": # Windows. - for f in os.listdir(os.path.join(os.environ['WINDIR'], "fonts")): + windows_font_path = os.path.join(os.environ['WINDIR'], "fonts") + logger.info("Searching for windows fonts at: %s", windows_font_path) + for f in os.listdir(windows_font_path): if f.endswith(".ttf"): if not f in font_names: + logger.debug("Font found at path: %s", f) font_names.add(f) - font_paths.append(os.path.join(os.environ['WINDIR'], f)) + font_paths.append(os.path.join(os.environ['WINDIR'], "fonts", f)) else: - raise NotImplementedError('Unsupported operating system.') - - def sort_fonts(a, b): - a_name = os.path.basename(a).lower() - b_name = os.path.basename(b).lower() - if a_name > b_name: - return 1 - elif a_name == b_name: - return 0 - else: - return -1 + logger.info("Don't know how to search for fonts on a Mac, sorry.") + pass + # sort the fonts - font_paths.sort(sort_fonts) + font_paths.sort(key=(lambda b: os.path.basename(b).lower())) return font_paths - -class POpenWithTimeout(object): - class ProcessError(Exception): - def __init__(self, error_type, message, cause=None): - super(POpenWithTimeout.ProcessError, self).__init__() - self.error_type = error_type - self.cause = cause if cause is not None else None - self.message = message - - def __str__(self): - if self.cause is None: - return "{}: {}".format(self.error_type, self.message) - if isinstance(self.cause, list): - if len(self.cause) > 1: - error_string = "{}: {} - Inner Exceptions".format(self.error_type, self.message) - error_count = 1 - for cause in self.cause: - error_string += "\n {}: {} Exception - {}".format(error_count, type(cause).__name__, cause) - error_count += 1 - return error_string - elif len(self.cause) == 1: - return "{}: {} - Inner Exception: {}".format(self.error_type, self.message, self.cause[0]) - return "{}: {} - Inner Exception: {}".format(self.error_type, self.message, self.cause) - - lock = threading.Lock() - - def __init__(self): - self.proc = None - self.stdout = '' - self.stderr = '' - self.completed = False - self.exception = None - self.subprocess_kill_exceptions = [] - self.kill_exceptions = None - - def kill(self): - if self.proc is None: - return - try: - process = psutil.Process(self.proc.pid) - for proc in process.children(recursive=True): - try: - proc.kill() - except psutil.NoSuchProcess: - # the process must have completed - pass - except (psutil.Error,psutil.AccessDenied, psutil.ZombieProcess) as e: - self.kill_exceptions.append(e) - process.kill() - except psutil.NoSuchProcess: - # the process must have completed - pass - except (psutil.Error, psutil.AccessDenied, psutil.ZombieProcess) as e: - self.kill_exceptions = e - - def get_exceptions(self): - if ( - self.exception is None - and (self.subprocess_kill_exceptions is None or len(self.subprocess_kill_exceptions) == 0) - and self.kill_exceptions is None - ): - return None - causes = [] - error_type = None - error_message = None - if self.exception is not None: - error_type = 'script-execution-error' - error_message = 'An error occurred curing the execution of a custom script.' - causes.append(self.exception) - if self.kill_exceptions is not None: - if error_type is None: - error_type = 'script-kill-error' - error_message = 'A custom script timed out, and an error occurred while terminating the process.' - causes.append(self.kill_exceptions) - if len(self.subprocess_kill_exceptions) > 0: - if error_type is None: - error_type = 'script-subprocess-kill-error' - error_message = 'A custom script timed out, and an error occurred while terminating one of its ' \ - 'subprocesses.' - for cause in self.subprocess_kill_exceptions: - causes.append(cause) - - return POpenWithTimeout.ProcessError( - error_type, - error_message, - cause=causes) - - # run a command with the provided args, timeout in timeout_seconds - def run(self, args, timeout_seconds): - - # Create, start and run the process and fill in stderr and stdout - def execute_process(args): - # get the lock so that we can start the process without encountering a timeout - self.lock.acquire() - try: - # don't start the process if we've already timed out - if not self.completed: - self.proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - else: - return - except (OSError, subprocess.CalledProcessError) as e: - self.exception = e - self.completed = True - (exc_stdout, exc_stderr) = self.proc.communicate() - self.stdout = exc_stdout - self.stderr = exc_stderr - return - finally: - self.lock.release() - - (t_stdout, t_stderr) = self.proc.communicate() - self.lock.acquire() - try: - if not self.completed: - self.stdout = t_stdout - self.stderr = t_stderr - self.completed = True - finally: - self.lock.release() - - thread = threading.Thread(target=execute_process, args=[args]) - # start the thread - thread.start() - # join the thread with a timeout - thread.join(timeout=timeout_seconds) - # check to see if the thread is alive - if thread.is_alive(): - self.lock.acquire() - try: - if not self.completed: - if self.proc is not None: - self.kill() - (p_stdout, p_stderr) = self.proc.communicate() - self.stdout = p_stdout - self.stderr = p_stderr + '- The snapshot script timed out in {0} seconds.'.format(timeout_seconds) - self.completed = True - except AttributeError: - # It's possible that the process is killed AFTER we check for self.proc is None - # catch that here and pass - pass - finally: - self.lock.release() - - - if self.proc is not None: - # raise any exceptions that were caught - exceptions = self.get_exceptions() - if exceptions is not None: - raise exceptions - return self.proc.returncode - else: - self.stderr = 'The process does not exist' - return -100 - -def run_command_with_timeout(args, timeout_sec): - """Execute `cmd` in a subprocess and enforce timeout `timeout_sec` seconds. - Return subprocess exit code on natural completion of the subprocess. - Raise an exception if timeout expires before subprocess completes.""" +def get_directory_size(root, recurse=False): + total_size = 0 + try: + if not os.path.isdir(root): + return 0 + for name in os.listdir(root): + file_path = os.path.join(root, name) + if os.path.isfile(file_path): + total_size += os.path.getsize(file_path) + elif recurse and os.path.isdir(file_path): + total_size += get_directory_size(file_path, recurse) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + logger.exception(e) + return total_size + + +def get_file_creation_date(path): + if sys.platform == "win32": + # getctime always returns the creation date of a file, exactly what we want. + # in Linux this could be the st_ctime, which is always later than or equal to the + # modification date, so only trust this for windows. + return os.path.getctime(path) + # get file statistics + stat = os.stat(path) + if hasattr(stat, 'st_birthtime'): + # Not all OSs support st_birthtime, which is an actual creation date + return stat.st_birthtime + + # for any other OS get the last content modification date. + return stat.st_mtime + + +# MUCH faster than the standard shutil.copy +def fast_copy(src, dst, buffer_size=1024 * 1024 * 1): + # Optimize the buffer for small files + file_size = os.path.getsize(src) + + buffer_size = min(buffer_size, file_size) + if buffer_size == 0: + buffer_size = 1024 + + with open(src, 'rb') as fin: + with open(dst, 'wb') as fout: + shutil.copyfileobj(fin, fout, buffer_size) + + +# function to walk a directory and return all contained subdirectories +def walk_directories(root): + for name in os.listdir(root): + if os.path.isdir(os.path.join(root, name)): + yield name + + +# an iterable function to walk all files in a given directory and return a list of files with metadata +def walk_files(root, filter_function=None): + '''Iterable function, returns a list of dicts, each including name, extension, size, and date (creation date if + possible, else modified date)). Can be filtered to only include files with extensions in the provided set. ''' + for name in os.listdir(root): + file_path = os.path.join(root, name) + if os.path.isfile(file_path): + extension = get_extension_from_filename(name) + + if filter_function and not filter_function(root, name, extension): + continue + yield { + 'name': name, + 'extension': extension, + 'size': os.path.getsize(file_path), + 'date': get_file_creation_date(file_path) + } + + +FILE_TYPE_SNAPSHOT_ARCHIVE = "snapshot_archive" +FILE_TYPE_TIMELAPSE_OCTOLAPSE = "timelapse_octolapse" +FILE_TYPE_TIMELAPSE_OCTOPRINT = "timelapse_octoprint" + + +def get_file_info(file_path): + name = os.path.basename(file_path) + extension = get_extension_from_filename(name) + return { + 'name': name, + 'extension': extension, + 'size': os.path.getsize(file_path) if os.path.isfile(file_path) else 0, + 'date': get_file_creation_date(file_path) + } + + +# Handle windows specific stuff (TODO: need to move other code here too) +def is_windows(): + return sys.platform.startswith('win') + + +# Handle windows errors that do not exist on Linux (why???) +# Windows Exceptions in Linux +try: + from exceptions import WindowsError +except ImportError: + class WindowsError(OSError): pass + + +ERROR_WINDOWS_DIRECTORY_NOT_EMPTY = 145 +ERROR_WINDOWS_DIRECTORY_NOT_EMPTY_RETRIES = 10 +ERROR_WINDOWS_DIRECTORY_NOT_EMPTY_RETRY_MS = 1.0/1000.0 + + +def rmtree(path): + """ + Windows doesn't seem to be able to reliably immediately delete a path. It often works after waiting + for a small amount of time. I added retry to this function so that it works more reliably on windows. + :param path: + :return: + """ + num_tries = 0 + while True: + try: + shutil.rmtree(path) + break + except WindowsError as e: + if not e.winerror != ERROR_WINDOWS_DIRECTORY_NOT_EMPTY: + raise e + num_tries += 1 + if num_tries < ERROR_WINDOWS_DIRECTORY_NOT_EMPTY_RETRIES: + time.sleep(ERROR_WINDOWS_DIRECTORY_NOT_EMPTY_RETRY_MS) + else: + raise e - proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +ERROR_WINDOWS_FILE_IS_IN_USE = 32 +ERROR_WINDOWS_FILE_IS_IN_USE_RETRIES = 10 +ERROR_WINDOWS_FILE_IS_IN_USE_RETRY_SECONDS = 1.0/1000.0 - if timeout_sec is not None: - timer = Timer(timeout_sec, proc.kill) +def remove(path): + """For some reason closed files are sometimes open if you try to remove them immediately after a file operation. + This happens occationally when using Pillow to save images. I added this function so that windows can retry + a delete before failing with an exception. + """ + num_tries = 0 + while True: try: - timer.start() - (stdout, stderr) = proc.communicate() - finally: - timer.cancel() - else: - (stdout, stderr) = proc.communicate() - # Process completed naturally - return exit code - return proc.returncode, stdout, stderr + os.remove(path) + break + except WindowsError as e: + if e.winerror != ERROR_WINDOWS_FILE_IS_IN_USE: + raise e + num_tries += 1 + if num_tries < ERROR_WINDOWS_FILE_IS_IN_USE_RETRIES: + time.sleep(ERROR_WINDOWS_FILE_IS_IN_USE_RETRY_SECONDS) + else: + raise e + +def move(src, dst): + """For some reason closed files are sometimes open if you try to remove them immediately after a file operation. + This happens occationally when using Pillow to save images. I added this function so that windows can retry + a delete before failing with an exception. + """ + num_tries = 0 + while True: + try: + shutil.move(src, dst) + break + except WindowsError as e: + if e.winerror != ERROR_WINDOWS_FILE_IS_IN_USE: + raise e + num_tries += 1 + if num_tries < ERROR_WINDOWS_FILE_IS_IN_USE_RETRIES: + time.sleep(ERROR_WINDOWS_FILE_IS_IN_USE_RETRY_SECONDS) + else: + raise e class TimelapseJobInfo(object): + timelapse_info_file_name = "timelapse_info.json" + + @staticmethod + def is_timelapse_info_file(file_name): + return file_name.lower() == TimelapseJobInfo.timelapse_info_file_name + def __init__( self, job_info=None, job_guid=None, print_start_time=None, print_end_time=None, - print_end_state=None, print_file_name=None + print_end_state="INCOMPLETE", print_file_name="UNKNOWN", print_file_extension=None ): if job_info is None: - self.JobGuid = "{}".format(job_guid) + self.JobGuid = None if job_guid is None else "{}".format(job_guid) self.PrintStartTime = print_start_time self.PrintEndTime = print_end_time self.PrintEndState = print_end_state self.PrintFileName = print_file_name - + self.PrintFileExtension = print_file_extension else: self.JobGuid = job_info.JobGuid self.PrintStartTime = job_info.PrintStartTime self.PrintEndTime = job_info.PrintEndTime self.PrintEndState = job_info.PrintEndState self.PrintFileName = job_info.PrintFileName + self.PrintFileExtension = job_info.PrintFileExtension + + @staticmethod + def load(data_folder, print_job_guid, camera_guid=None): + file_directory = get_temporary_snapshot_job_path(data_folder, print_job_guid) + file_path = os.path.join(file_directory, TimelapseJobInfo.timelapse_info_file_name) + try: + with open(file_path, 'r') as timelapse_info: + data = json.load(timelapse_info) + return TimelapseJobInfo.from_dict(data) + except (OSError, IOError, ValueError) as e: + logger.exception("Unable to load TimelapseJobInfo from %s.", file_path) + info = TimelapseJobInfo() + info.PrintEndState = "UNKNOWN" + info.JobGuid = print_job_guid + if camera_guid is not None: + snapshot_path = os.path.join(file_directory, camera_guid) + if os.path.exists(snapshot_path): + # look for a jpg from which to extract the print name + for name in os.listdir(snapshot_path): + camera_file_path = os.path.join(snapshot_path, name) + extension = get_extension_from_filename(name) + + if ( + os.path.isfile(camera_file_path) and + is_valid_snapshot_extension(extension) and + name.endswith(".{0}".format(extension)) + ): + if len(name) > 10: + test_image_number_string = name[len(name)-10:len(name)-4] + try: + int(test_image_number_string) + info.PrintFileName = name[0:len(name)-10] + break + except ValueError: + pass + info.PrintFileName = name[0:len(name) - 4] + break + return info + + def save(self, temporary_directory): + file_directory = get_temporary_snapshot_job_path(temporary_directory, self.JobGuid) + file_path = os.path.join(file_directory, TimelapseJobInfo.timelapse_info_file_name) + if not os.path.exists(file_directory): + os.makedirs(file_directory) + with open(file_path, 'w') as timelapse_info: + json.dump(self.to_dict(), timelapse_info) + + def to_dict(self): + return { + "job_guid": self.JobGuid, + "print_start_time": self.PrintStartTime, + "print_end_time": self.PrintEndTime, + "print_end_state": self.PrintEndState, + "print_file_name": self.PrintFileName, + "print_file_extension": self.PrintFileExtension + } + + @staticmethod + def from_dict(dict_obj): + return TimelapseJobInfo( + job_guid=dict_obj["job_guid"], + print_start_time=dict_obj["print_start_time"], + print_end_time=dict_obj["print_end_time"], + print_end_state=dict_obj["print_end_state"], + print_file_name=dict_obj["print_file_name"], + print_file_extension=dict_obj["print_file_extension"], + ) + + +if sys.version_info < (3, 0): + def unquote(value): + return urllib.unquote(value) +else: + def unquote(value): + return urllib.parse.unquote(value) class RecurringTimerThread(threading.Thread): - def __init__(self, interval_seconds, callback, cancel_event): + def __init__(self, interval_seconds, callback, cancel_event, first_run_delay_seconds=None): threading.Thread.__init__(self) - self.interval_seconds = interval_seconds - self.callback = callback - self.stopped = cancel_event + self._interval_seconds = interval_seconds + self._callback = callback + self._cancel_event = cancel_event + self._trigger = threading.Event() + self._first_run_delay_seconds = first_run_delay_seconds def run(self): - while not self.stopped.wait(self.interval_seconds): - self.callback() + if self._first_run_delay_seconds is not None: + time.sleep(self._first_run_delay_seconds) + self._callback() + while not self._cancel_event.wait(self._interval_seconds): + self._callback() + + +class SafeDict(dict): + def __missing__(self, key): + return '{' + key + '}' + + +# retry utility adapted from https://www.peterbe.com/plog/best-practice-with-retries-with-requests +def requests_retry_session( + retries=3, + backoff_factor=0.3, + status_forcelist=(500, 502, 504), + session=None, +): + session = session or requests.Session() + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session + + +class JsonSerializable(object): + + def __str__(self): + return json.dumps(self, default=JsonSerializable.json_dumper, + sort_keys=True) + + @staticmethod + def json_dumper(obj): + to_json = getattr(obj, "to_json", None) + if callable(to_json): + return obj.to_json() + to_dict = getattr(obj, "to_dict", None) + if callable(to_dict): + return obj.to_dict() + return obj.__dict__ + diff --git a/octoprint_octolapse_setuptools/__init__.py b/octoprint_octolapse_setuptools/__init__.py new file mode 100644 index 00000000..688b8faf --- /dev/null +++ b/octoprint_octolapse_setuptools/__init__.py @@ -0,0 +1,280 @@ +# coding=utf-8 +################################################################################## +# Octolapse - A plugin for OctoPrint used for making stabilized timelapse videos. +# Copyright (C) 2023 Brad Hochgesang +################################################################################## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see the following: +# https://github.com/FormerLurker/Octolapse/blob/master/LICENSE +# +# You can contact the author either through the git-hub repository, or at the +# following email address: FormerLurker@pm.me +################################################################################## + +from distutils import version +import functools + + +@functools.total_ordering +class NumberedVersion(version.LooseVersion): + # This is the current plugin version, not including any versioneer info, + # which could be earlier or later + CurrentVersion = "0.4.5" + # This is the CurrentVersion last time the settings were migrated. + CurrentSettingsVersion = "0.4.3" + ''' + Prerelease tags will ALWAYS compare as less than a version with the same initial tags if the prerelease tag + exists. + ''' + def __init__(self, vstring=None, pre_release_tags=['rc'], development_tags=['dev']): + # trim vstring + if vstring != None: + vstring = vstring.strip() + self.pre_release_tags = pre_release_tags + self.development_tags = development_tags + self.original_string = vstring + self._tag_version = [] + self._pre_release_version = [] + self._development_version = [] + self._commit_version = [] + self.commit_version_string = '' + self.is_pre_release = False + self.is_development = False + self.has_commit_info = False + self.commits_ahead = 0 + self.commit_guid = '' + self.is_dirty = False + + # strip off any leading V or v + if len(vstring) > 0 and vstring[0].upper() == "V": + vstring = vstring[1:] + + # find any pluses that contain commit level info + if '+' in vstring: + index = vstring.find('+') + # make sure there is info after the '+' + if index < len(vstring) - 1: + self.commit_version_string = vstring[index+1:] + # strip off the plus symbox from the vstring + vstring = vstring[:index] + version.LooseVersion.__init__(self, vstring) + + def parse(self, vstring): + version.LooseVersion.parse(self, vstring) + # save the version without commit level info + # set index = 0 + index = 0 + tag_length = len(self.version) + # determine tag version + for i in range(index, tag_length): + index = i + seg = self.version[i] + if seg in self.pre_release_tags: + self.is_pre_release = True + break + self._tag_version.append(seg) + # determine pre release version + for i in range(index + 1, tag_length): + index = i + seg = self.version[i] + if seg in self.development_tags: + self.is_development = True + index += 1 # skip the dev bit + break + self._pre_release_version.append(seg) + # determine development version + for i in range(index, tag_length): + index = i + seg = self.version[i] + self._development_version.append(seg) + + if len(self.commit_version_string) > 0: + self.commits_ahead = None # we don't know how many commits we have yet + # We have commit version info, let's add it to our version + prel_segments = self.commit_version_string.split('.') + # get num commits ahead + if len(prel_segments) > 0: + if prel_segments[0] != 'u': + try: + self.commits_ahead = int(prel_segments[0]) + except ValueError: + # If you can't parse this, commits_ahead will be None + pass + # get commit guid + if len(prel_segments) > 1: + guid = prel_segments[1] + if len(guid) == 8: + self.commit_guid = guid + self.has_commit_info = True + + # find out if the current version is 'dirty' yuck! + if len(prel_segments) > 2: + self.is_dirty = prel_segments[2] == 'dirty' + + # add the new segments + if self.has_commit_info: + # add the commit level stuff to the version segments + # Add the commit info separator (+) + self._commit_version.append("+") + # next, add the number of commits we are ahead + self._commit_version.append("u" if self.commits_ahead is None else self.commits_ahead) + # next add the guid + self._commit_version.append(self.commit_guid) + # if the version is dirty, add that segment + if self.is_dirty: + self._commit_version.append("dirty") + self.version.extend(self._commit_version) + + def __repr__(self): + return "{cls} ('{vstring}', {prerel_tags})" \ + .format(cls=self.__class__.__name__, vstring=str(self), prerel_tags=list(self.prerel_tags.keys())) + + def __str__(self): + return self.original_string + + def __lt__(self, other): + """ + Compare versions and return True if the current version is less than other + """ + # first compare tags using LooseVersion + cur_version = self._tag_version + other_version = other._tag_version + if cur_version < other_version: + return True + if cur_version > other_version: + return False + + # cur_version == other_version, compare pre-release + if self.is_pre_release and not other.is_pre_release: + # the current version is a pre-release, but other is not. + return True + + if other.is_pre_release and not self.is_pre_release: + # other is a pre-release, but the current version is not. + return False + + cur_version = self.pre_release_tags + other_version = other.pre_release_tags + if cur_version < other_version: + return True + if cur_version > other_version: + return False + + # now compare development versions + if self.is_development and not other.is_development: + # the current version is development, but other is not. + return True + + if other.is_development and not self.is_development: + # other is a development, but the current version is not. + return False + # Both versions are development, compare dev versions + + cur_version = self._development_version + other_version = other._development_version + if cur_version < other_version: + return True + if cur_version > other_version: + return False + + # Development versions are the same, now compare commit info + # First, if either version has no 'commits_ahead', they are considered equal, return false + if self.commits_ahead is None or other.commits_ahead is None: + return False + + if self.commits_ahead < other.commits_ahead: + return True + if other.commits_ahead < self.commits_ahead: + return False + + # we are the same number of commits ahead, so just check dirty (dirty > not dirty) + if other.is_dirty and not self.is_dirty: + return True + return False + + def __eq__(self, other): + return not (self < other) and not (self > other) + + def __gt__(self, other): + """ + Compare versions and return True if the current version is greater than other + """ + # first compare tags using LooseVersion + cur_version = self._tag_version + other_version = other._tag_version + if cur_version > other_version: + return True + if cur_version < other_version: + return False + + # cur_version == other_version, compare pre-release + if self.is_pre_release and not other.is_pre_release: + # the current version is a pre-release, but other is not. + return False + + if other.is_pre_release and not self.is_pre_release: + # other is a pre-release, but the current version is not. + return True + + cur_version = self.pre_release_tags + other_version = other.pre_release_tags + if cur_version > other_version: + return True + if cur_version < other_version: + return False + + # now compare development versions + if self.is_development and not other.is_development: + # the current version is development, but other is not. + return False + + if other.is_development and not self.is_development: + # other is a development, but the current version is not. + return True + # Both versions are development, compare dev versions + cur_version = self._development_version + other_version = other._development_version + if cur_version > other_version: + return True + if cur_version < other_version: + return False + + # Development versions are the same, now compare commit info + # First, if either version has no 'commits_ahead', they are considered equal, return false + if self.commits_ahead is None or other.commits_ahead is None: + return False + + if self.commits_ahead > other.commits_ahead: + return True + if other.commits_ahead > self.commits_ahead: + return False + + # we are the same number of commits ahead, so just check dirty (dirty > not dirty) + if self.is_dirty and not other.is_dirty: + return True + return False + + @staticmethod + def clean_version(version): + if version is None or len(version) == 0: + return "0+unknown" + + version = version.lower() + + if version[0] == "v": + version = version[1:] + + return version + + + diff --git a/requirements.txt b/requirements.txt index a1dc4637..48b60e33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,3 @@ # # works as expected. Requirements can be found in setup.py. ### - -. diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..000d455f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,13 @@ + +# See the docstring in versioneer.py for instructions. Note that you must +# re-run 'versioneer.py setup' after changing this section, and commit the +# resulting files. + +[versioneer] +VCS = git +style = pep440 +versionfile_source = octoprint_octolapse/_version.py +versionfile_build = octoprint_octolapse/_version.py +tag_prefix = +parentdir_prefix = + diff --git a/setup.py b/setup.py index dcab78e5..c310a8eb 100644 --- a/setup.py +++ b/setup.py @@ -1,163 +1,151 @@ # coding=utf-8 -from distutils.core import Extension -from distutils.command.build_ext import build_ext +from setuptools import setup, Extension +from setuptools.command.build_ext import build_ext import sys -import os -######################################################################################################################## -# The plugin's identifier, has to be unique -plugin_identifier = "octolapse" -# The plugin's python package, should be "octoprint_", has to be unique -plugin_package = "octoprint_octolapse" -# The plugin's human readable name. Can be overwritten within OctoPrint's internal data via __plugin_name__ in the -# plugin module -plugin_name = "Octolapse" -# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "0.4.0rc1.dev0" -# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin -# module -plugin_description = """Create stabilized timelapses of your 3d prints. Highly customizable, loads of presets, lots of fun.""" -# The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module -plugin_author = "Brad Hochgesang" -# The plugin's author's mail address. -plugin_author_email = "FormerLurker@pm.me" - -# The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module -plugin_url = "https://github.com/FormerLurker/Octolapse" - -# The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module -plugin_license = "AGPLv3" - -# Any additional requirements besides OctoPrint should be listed here -plugin_requires = ["pillow", "sarge", "six", "OctoPrint>1.3.8", "psutil", "file_read_backwards", "setuptools>=6.0"] - -# uncomment to enable faulthandler. -# Also need to uncomment faulthandler lines on the top of plugin_octolapse/__init__.py -# plugin_requires.append("faulthandler>=3.1") -# - -# TODO: Get fontconfig to work -#from sys import platform -#if platform == "linux" or platform == "linux2": -# plugin_requires.append("enum34") -# plugin_requires.append("fontconfig") - -# -------------------------------------------------------------------------------------------------------------------- -# More advanced options that you usually shouldn't have to touch follow after this point -# --------------------------------------d------------------------------------------------------------------------------ +import platform -# Additional package data to install for this plugin. The subfolders "templates", "static" and "translations" will -# already be installed automatically if they exist. Note that if you add something here you'll also need to update -# MANIFEST.in to match to ensure that python setup.py sdist produces a source distribution that contains all your -# files. This is sadly due to how python's setup.py works, see also http://stackoverflow.com/a/14159430/2028598 -plugin_additional_data = [ - 'data/*.json', - 'data/images/*.png', - 'data/images/*.jpeg', - 'data/lib/c/*.cpp', - 'data/lib/c/*.h', - 'data/webcam_types/*' -] -# Any additional python packages you need to install with your plugin that are not contained in .* -plugin_additional_packages = [] - -# Any python packages within .* you do NOT want to install with your plugin -plugin_ignored_packages = [] - -# Additional parameters for the call to setuptools.setup. If your plugin wants to register additional entry points, -# define dependency links or other things like that, this is the place to go. Will be merged recursively with the -# default setup parameters as provided by octoprint_setuptools.create_plugin_setup_parameters using -# octoprint.util.dict_merge. -# -# Example: plugin_requires = ["someDependency==dev"] additional_setup_parameters = {"dependency_links": [ -# "https://github.com/someUser/someRepo/archive/master.zip#egg=someDependency-dev"]} - -copt = { - 'msvc': ['/Ox', '/fp:fast', '/GS', '/GL', '/analyze', '/Gy', '/Oi', '/MD', '/EHsc', '/Ot'], - 'mingw32': ['-fopenmp', '-O3', '-ffast-math', '-march=native', '-std=c++11'], - 'gcc': ['-O3', '-std=c++11'] -} - -lopt = { - 'mingw32': ['-fopenmp'], - #'msvc': ['/DEBUG'] +# Versioneer import with fallback +try: + import versioneer + cmdclass = versioneer.get_cmdclass() +except (ImportError, AttributeError): + print("Warning: versioneer not available, using fallback version") + cmdclass = {} + def get_version(): + return "0.4.51" + def get_cmdclass(): + return {} + class _versioneer: + get_version = staticmethod(get_version) + get_cmdclass = staticmethod(get_cmdclass) + versioneer = _versioneer + +# Compiler options +compiler_opts = { + 'extra_compile_args': [], + 'extra_link_args': [], + 'define_macros': [('DEBUG_chardet', '1'), ('IS_PYTHON_EXTENSION', '1')] } +# Platform-specific compiler flags +if sys.platform == 'win32': + compiler_opts['extra_compile_args'].extend(['/std:c++14', '/permissive-']) +elif sys.platform == 'darwin': + compiler_opts['extra_compile_args'].extend(['-std=c++14', '-mmacosx-version-min=10.9']) +else: + compiler_opts['extra_compile_args'].append('-std=c++14') -class build_ext_subclass( build_ext ): +class build_ext_subclass(build_ext): def build_extensions(self): - c = self.compiler.compiler_type - print("Compiling Octolapse Parser Extension with {0}".format(c)) - if c in copt: - for e in self.extensions: - e.extra_compile_args = copt[c] - if c in lopt: - for e in self.extensions: - e.extra_link_args = lopt[c] + # Detect compiler type + compiler_type = self.compiler.compiler_type if hasattr(self.compiler, 'compiler_type') else 'unix' + + print(f"Compiling Octolapse Parser Extension with {compiler_type}.") + + for ext in self.extensions: + ext.extra_compile_args.extend(compiler_opts['extra_compile_args']) + ext.extra_link_args.extend(compiler_opts['extra_link_args']) + ext.define_macros.extend(compiler_opts['define_macros']) + build_ext.build_extensions(self) -## Build our c++ parser extension -plugin_ext_sources = [ - 'octoprint_octolapse/data/lib/c/gcode_position_processor.cpp', - 'octoprint_octolapse/data/lib/c/gcode_parser.cpp', - 'octoprint_octolapse/data/lib/c/gcode_position.cpp', - 'octoprint_octolapse/data/lib/c/parsed_command.cpp', - 'octoprint_octolapse/data/lib/c/parsed_command_parameter.cpp', - 'octoprint_octolapse/data/lib/c/position.cpp', - 'octoprint_octolapse/data/lib/c/python_helpers.cpp', - 'octoprint_octolapse/data/lib/c/snapshot_plan.cpp', - 'octoprint_octolapse/data/lib/c/snapshot_plan_step.cpp', - 'octoprint_octolapse/data/lib/c/stabilization.cpp', - 'octoprint_octolapse/data/lib/c/stabilization_results.cpp', - 'octoprint_octolapse/data/lib/c/stabilization_smart_layer.cpp', - 'octoprint_octolapse/data/lib/c/stabilization_snap_to_print.cpp', - 'octoprint_octolapse/data/lib/c/logging.cpp', - 'octoprint_octolapse/data/lib/c/utilities.cpp', - 'octoprint_octolapse/data/lib/c/trigger_position.cpp' -] -cpp_gcode_parser = Extension( - 'GcodePositionProcessor', - sources=plugin_ext_sources, - language="c++" -) - - -additional_setup_parameters = { - "ext_modules": [cpp_gcode_parser], - "cmdclass": {"build_ext": build_ext_subclass} -} - -######################################################################################################################## - -from setuptools import setup - -try: - import octoprint_setuptools -except: - print("Could not import OctoPrint's setuptools, are you sure you are running that under " - "the same python installation that OctoPrint is installed under?") - import sys - - sys.exit(-1) - -setup_parameters = octoprint_setuptools.create_plugin_setup_parameters( - identifier=plugin_identifier, - package=plugin_package, - name=plugin_name, - version=plugin_version, - description=plugin_description, - author=plugin_author, - mail=plugin_author_email, - url=plugin_url, - license=plugin_license, - requires=plugin_requires, - additional_packages=plugin_additional_packages, - ignored_packages=plugin_ignored_packages, - additional_data=plugin_additional_data, +# Update cmdclass with our custom build_ext +cmdclass['build_ext'] = build_ext_subclass + +def package_data_dirs(source, sub_folders): + dirs = [source] + for folder in sub_folders: + dirs.append(source + '/' + folder) + + ret = [] + for d in dirs: + for dirname, _, files in os.walk(d): + for filename in files: + filepath = os.path.join(dirname, filename) + ret.append(filepath[len(source)+1:]) + return ret + +def get_requirements(requirements_path): + requirements = [] + with open(requirements_path, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and line != '.': + requirements.append(line) + return requirements +import os +requirements = get_requirements('requirements.txt') + +setup( + name="Octolapse", + version=versioneer.get_version(), + cmdclass=cmdclass, + description="Create a stabilized timelapse of your 3D prints.", + long_description="Octolapse is designed to make stabilized timelapses of your prints with as little hassle as possible, and it's extremely configurable.", + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: GNU Affero General Public License v3', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Topic :: System :: Monitoring' + ], + author="Brad Hochgesang", + author_email="FormerLurker@pm.me", + url="https://github.com/FormerLurker/Octolapse/", + license="GNU Affero General Public License v3", + packages=["octoprint_octolapse", + "octoprint_octolapse_setuptools"], + package_data={ + "octoprint_octolapse": package_data_dirs( + 'octoprint_octolapse', + ['data', 'static', 'templates'] + ) + }, + include_package_data=True, + zip_safe=False, + install_requires=requirements, + extras_require={ + 'develop': [ + 'mock>=2.0.0', + ] + }, + entry_points={ + "octoprint.plugin": [ + "octolapse = octoprint_octolapse" + ] + }, + ext_modules=[ + Extension( + 'GcodePositionProcessor', + sources=[ + 'octoprint_octolapse/data/lib/c/gcode_parser.cpp', + 'octoprint_octolapse/data/lib/c/extruder.cpp', + 'octoprint_octolapse/data/lib/c/gcode_position.cpp', + 'octoprint_octolapse/data/lib/c/position.cpp', + 'octoprint_octolapse/data/lib/c/stabilization.cpp', + 'octoprint_octolapse/data/lib/c/stabilization_smart_layer.cpp', + 'octoprint_octolapse/data/lib/c/stabilization_smart_gcode.cpp', + 'octoprint_octolapse/data/lib/c/stabilization_results.cpp', + 'octoprint_octolapse/data/lib/c/trigger_position.cpp', + 'octoprint_octolapse/data/lib/c/snapshot_plan.cpp', + 'octoprint_octolapse/data/lib/c/snapshot_plan_step.cpp', + 'octoprint_octolapse/data/lib/c/parsed_command.cpp', + 'octoprint_octolapse/data/lib/c/parsed_command_parameter.cpp', + 'octoprint_octolapse/data/lib/c/gcode_comment_processor.cpp', + 'octoprint_octolapse/data/lib/c/gcode_position_processor.cpp', + 'octoprint_octolapse/data/lib/c/python_helpers.cpp', + 'octoprint_octolapse/data/lib/c/logging.cpp', + 'octoprint_octolapse/data/lib/c/utilities.cpp', + ], + include_dirs=['octoprint_octolapse/data/lib/c'], + language='c++', + ) + ] ) - -if len(additional_setup_parameters): - from octoprint.util import dict_merge - setup_parameters = dict_merge(setup_parameters, additional_setup_parameters) - -setup(**setup_parameters) diff --git a/versioneer.py b/versioneer.py new file mode 100644 index 00000000..5fc0f401 --- /dev/null +++ b/versioneer.py @@ -0,0 +1,1822 @@ + +# Version: 0.18 + +"""The Versioneer - like a rocketeer, but for versions. + +The Versioneer +============== + +* like a rocketeer, but for versions! +* https://github.com/warner/python-versioneer +* Brian Warner +* License: Public Domain +* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy +* [![Latest Version] +(https://pypip.in/version/versioneer/badge.svg?style=flat) +](https://pypi.python.org/pypi/versioneer/) +* [![Build Status] +(https://travis-ci.org/warner/python-versioneer.png?branch=master) +](https://travis-ci.org/warner/python-versioneer) + +This is a tool for managing a recorded version number in distutils-based +python projects. The goal is to remove the tedious and error-prone "update +the embedded version string" step from your release process. Making a new +release should be as easy as recording a new tag in your version-control +system, and maybe making new tarballs. + + +## Quick Install + +* `pip install versioneer` to somewhere to your $PATH +* add a `[versioneer]` section to your setup.cfg (see below) +* run `versioneer install` in your source tree, commit the results + +## Version Identifiers + +Source trees come from a variety of places: + +* a version-control system checkout (mostly used by developers) +* a nightly tarball, produced by build automation +* a snapshot tarball, produced by a web-based VCS browser, like github's + "tarball from tag" feature +* a release tarball, produced by "setup.py sdist", distributed through PyPI + +Within each source tree, the version identifier (either a string or a number, +this tool is format-agnostic) can come from a variety of places: + +* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows + about recent "tags" and an absolute revision-id +* the name of the directory into which the tarball was unpacked +* an expanded VCS keyword ($Id$, etc) +* a `_version.py` created by some earlier build step + +For released software, the version identifier is closely related to a VCS +tag. Some projects use tag names that include more than just the version +string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool +needs to strip the tag prefix to extract the version identifier. For +unreleased software (between tags), the version identifier should provide +enough information to help developers recreate the same tree, while also +giving them an idea of roughly how old the tree is (after version 1.2, before +version 1.3). Many VCS systems can report a description that captures this, +for example `git describe --tags --dirty --always` reports things like +"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the +0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has +uncommitted changes. + +The version identifier is used for multiple purposes: + +* to allow the module to self-identify its version: `myproject.__version__` +* to choose a name and prefix for a 'setup.py sdist' tarball + +## Theory of Operation + +Versioneer works by adding a special `_version.py` file into your source +tree, where your `__init__.py` can import it. This `_version.py` knows how to +dynamically ask the VCS tool for version information at import time. + +`_version.py` also contains `$Revision$` markers, and the installation +process marks `_version.py` to have this marker rewritten with a tag name +during the `git archive` command. As a result, generated tarballs will +contain enough information to get the proper version. + +To allow `setup.py` to compute a version too, a `versioneer.py` is added to +the top level of your source tree, next to `setup.py` and the `setup.cfg` +that configures it. This overrides several distutils/setuptools commands to +compute the version when invoked, and changes `setup.py build` and `setup.py +sdist` to replace `_version.py` with a small static file that contains just +the generated version data. + +## Installation + +See [INSTALL.md](./INSTALL.md) for detailed installation instructions. + +## Version-String Flavors + +Code which uses Versioneer can learn about its version string at runtime by +importing `_version` from your main `__init__.py` file and running the +`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can +import the top-level `versioneer.py` and run `get_versions()`. + +Both functions return a dictionary with different flavors of version +information: + +* `['version']`: A condensed version string, rendered using the selected + style. This is the most commonly used value for the project's version + string. The default "pep440" style yields strings like `0.11`, + `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section + below for alternative styles. + +* `['full-revisionid']`: detailed revision identifier. For Git, this is the + full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". + +* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the + commit date in ISO 8601 format. This will be None if the date is not + available. + +* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that + this is only accurate if run in a VCS checkout, otherwise it is likely to + be False or None + +* `['error']`: if the version string could not be computed, this will be set + to a string describing the problem, otherwise it will be None. It may be + useful to throw an exception in setup.py if this is set, to avoid e.g. + creating tarballs with a version string of "unknown". + +Some variants are more useful than others. Including `full-revisionid` in a +bug report should allow developers to reconstruct the exact code being tested +(or indicate the presence of local changes that should be shared with the +developers). `version` is suitable for display in an "about" box or a CLI +`--version` output: it can be easily compared against release notes and lists +of bugs fixed in various releases. + +The installer adds the following text to your `__init__.py` to place a basic +version in `YOURPROJECT.__version__`: + + from ._version import get_versions + __version__ = get_versions()['version'] + del get_versions + +## Styles + +The setup.cfg `style=` configuration controls how the VCS information is +rendered into a version string. + +The default style, "pep440", produces a PEP440-compliant string, equal to the +un-prefixed tag name for actual releases, and containing an additional "local +version" section with more detail for in-between builds. For Git, this is +TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags +--dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the +tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and +that this commit is two revisions ("+2") beyond the "0.11" tag. For released +software (exactly equal to a known tag), the identifier will only contain the +stripped tag, e.g. "0.11". + +Other styles are available. See [details.md](details.md) in the Versioneer +source tree for descriptions. + +## Debugging + +Versioneer tries to avoid fatal errors: if something goes wrong, it will tend +to return a version of "0+unknown". To investigate the problem, run `setup.py +version`, which will run the version-lookup code in a verbose mode, and will +display the full contents of `get_versions()` (including the `error` string, +which may help identify what went wrong). + +## Known Limitations + +Some situations are known to cause problems for Versioneer. This details the +most significant ones. More can be found on Github +[issues page](https://github.com/warner/python-versioneer/issues). + +### Subprojects + +Versioneer has limited support for source trees in which `setup.py` is not in +the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are +two common reasons why `setup.py` might not be in the root: + +* Source trees which contain multiple subprojects, such as + [Buildbot](https://github.com/buildbot/buildbot), which contains both + "master" and "slave" subprojects, each with their own `setup.py`, + `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI + distributions (and upload multiple independently-installable tarballs). +* Source trees whose main purpose is to contain a C library, but which also + provide bindings to Python (and perhaps other langauges) in subdirectories. + +Versioneer will look for `.git` in parent directories, and most operations +should get the right version string. However `pip` and `setuptools` have bugs +and implementation details which frequently cause `pip install .` from a +subproject directory to fail to find a correct version string (so it usually +defaults to `0+unknown`). + +`pip install --editable .` should work correctly. `setup.py install` might +work too. + +Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in +some later version. + +[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking +this issue. The discussion in +[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the +issue from the Versioneer side in more detail. +[pip PR#3176](https://github.com/pypa/pip/pull/3176) and +[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve +pip to let Versioneer work correctly. + +Versioneer-0.16 and earlier only looked for a `.git` directory next to the +`setup.cfg`, so subprojects were completely unsupported with those releases. + +### Editable installs with setuptools <= 18.5 + +`setup.py develop` and `pip install --editable .` allow you to install a +project into a virtualenv once, then continue editing the source code (and +test) without re-installing after every change. + +"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a +convenient way to specify executable scripts that should be installed along +with the python package. + +These both work as expected when using modern setuptools. When using +setuptools-18.5 or earlier, however, certain operations will cause +`pkg_resources.DistributionNotFound` errors when running the entrypoint +script, which must be resolved by re-installing the package. This happens +when the install happens with one version, then the egg_info data is +regenerated while a different version is checked out. Many setup.py commands +cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into +a different virtualenv), so this can be surprising. + +[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes +this one, but upgrading to a newer version of setuptools should probably +resolve it. + +### Unicode version strings + +While Versioneer works (and is continually tested) with both Python 2 and +Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. +Newer releases probably generate unicode version strings on py2. It's not +clear that this is wrong, but it may be surprising for applications when then +write these strings to a network connection or include them in bytes-oriented +APIs like cryptographic checksums. + +[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates +this question. + + +## Updating Versioneer + +To upgrade your project to a new release of Versioneer, do the following: + +* install the new Versioneer (`pip install -U versioneer` or equivalent) +* edit `setup.cfg`, if necessary, to include any new configuration settings + indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install` in your source tree, to replace + `SRC/_version.py` +* commit any changed files + +## Future Directions + +This tool is designed to make it easily extended to other version-control +systems: all VCS-specific components are in separate directories like +src/git/ . The top-level `versioneer.py` script is assembled from these +components by running make-versioneer.py . In the future, make-versioneer.py +will take a VCS name as an argument, and will construct a version of +`versioneer.py` that is specific to the given VCS. It might also take the +configuration arguments that are currently provided manually during +installation by editing setup.py . Alternatively, it might go the other +direction and include code from all supported VCS systems, reducing the +number of intermediate scripts. + + +## License + +To make Versioneer easier to embed, all its code is dedicated to the public +domain. The `_version.py` that it creates is also in the public domain. +Specifically, both are released under the Creative Commons "Public Domain +Dedication" license (CC0-1.0), as described in +https://creativecommons.org/publicdomain/zero/1.0/ . + +""" + +from __future__ import print_function +try: + import configparser +except ImportError: + import ConfigParser as configparser +import errno +import json +import os +import re +import subprocess +import sys + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_root(): + """Get the project root directory. + + We require that all commands are run from the project root, i.e. the + directory that contains setup.py, setup.cfg, and versioneer.py . + """ + root = os.path.realpath(os.path.abspath(os.getcwd())) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + # allow 'python path/to/setup.py COMMAND' + root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) + setup_py = os.path.join(root, "setup.py") + versioneer_py = os.path.join(root, "versioneer.py") + if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + err = ("Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND').") + raise VersioneerBadRootError(err) + try: + # Certain runtime workflows (setup.py install/develop in a setuptools + # tree) execute all dependencies in a single python process, so + # "versioneer" may be imported multiple times, and python's shared + # module-import table will cache the first one. So we can't use + # os.path.dirname(__file__), as that will find whichever + # versioneer.py was first imported, even in later projects. + me = os.path.realpath(os.path.abspath(__file__)) + me_dir = os.path.normcase(os.path.splitext(me)[0]) + vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) + if me_dir != vsr_dir: + print("Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(me), versioneer_py)) + except NameError: + pass + return root + + +def get_config_from_root(root): + """Read the project setup.cfg file to determine Versioneer config.""" + # This might raise EnvironmentError (if setup.cfg is missing), or + # configparser.NoSectionError (if it lacks a [versioneer] section), or + # configparser.NoOptionError (if it lacks "VCS="). See the docstring at + # the top of versioneer.py for instructions on writing your setup.cfg . + setup_cfg = os.path.join(root, "setup.cfg") + parser = configparser.ConfigParser() + with open(setup_cfg, "r") as f: + parser.read_file(f) + VCS = parser.get("versioneer", "VCS") # mandatory + + def get(parser, name): + if parser.has_option("versioneer", name): + return parser.get("versioneer", name) + return None + cfg = VersioneerConfig() + cfg.VCS = VCS + cfg.style = get(parser, "style") or "" + cfg.versionfile_source = get(parser, "versionfile_source") + cfg.versionfile_build = get(parser, "versionfile_build") + cfg.tag_prefix = get(parser, "tag_prefix") + if cfg.tag_prefix in ("''", '""'): + cfg.tag_prefix = "" + cfg.parentdir_prefix = get(parser, "parentdir_prefix") + cfg.verbose = get(parser, "verbose") + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +# these dictionaries contain VCS-specific tools +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + print("stdout was %s" % stdout) + return None, p.returncode + return stdout, p.returncode + + +LONG_VERSION_PY['git'] = ''' +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.18 (https://github.com/warner/python-versioneer) + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" + git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" + git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "%(STYLE)s" + cfg.tag_prefix = "%(TAG_PREFIX)s" + cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" + cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %%s" %% dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %%s" %% (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %%s (error)" %% dispcmd) + print("stdout was %%s" %% stdout) + return None, p.returncode + return stdout, p.returncode + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %%s but none started with prefix %%s" %% + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %%d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%%s', no digits" %% ",".join(refs - tags)) + if verbose: + print("likely tags: %%s" %% ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %%s" %% r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %%s not under git control" %% root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%%s*" %% tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%%s'" + %% describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%%s' doesn't start with prefix '%%s'" + print(fmt %% (full_tag, tag_prefix)) + pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" + %% (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%%d" %% pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%%d" %% pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%%s'" %% style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +def get_versions(): + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", "date": None} +''' + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs - tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def do_vcs_install(manifest_in, versionfile_source, ipy): + """Git-specific installation logic for Versioneer. + + For Git, this means creating/changing .gitattributes to mark _version.py + for export-subst keyword substitution. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + files = [manifest_in, versionfile_source] + if ipy: + files.append(ipy) + try: + me = __file__ + if me.endswith(".pyc") or me.endswith(".pyo"): + me = os.path.splitext(me)[0] + ".py" + versioneer_file = os.path.relpath(me) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) + present = False + try: + f = open(".gitattributes", "r") + for line in f.readlines(): + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + f.close() + except EnvironmentError: + pass + if not present: + f = open(".gitattributes", "a+") + f.write("%s export-subst\n" % versionfile_source) + f.close() + files.append(".gitattributes") + run_command(GITS, ["add", "--"] + files) + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +SHORT_VERSION_PY = """ +# This file was generated by 'versioneer.py' (0.18) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +import json + +version_json = ''' +%s +''' # END VERSION_JSON + + +def get_versions(): + return json.loads(version_json) +""" + + +def versions_from_file(filename): + """Try to determine the version from _version.py if present.""" + try: + with open(filename) as f: + contents = f.read() + except EnvironmentError: + raise NotThisMethod("unable to read _version.py") + mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + raise NotThisMethod("no version_json in _version.py") + return json.loads(mo.group(1)) + + +def write_to_version_file(filename, versions): + """Write the given version number to the given _version.py file.""" + os.unlink(filename) + contents = json.dumps(versions, sort_keys=True, + indent=1, separators=(",", ": ")) + with open(filename, "w") as f: + f.write(SHORT_VERSION_PY % contents) + + print("set %s to '%s'" % (filename, versions["version"])) + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +class VersioneerBadRootError(Exception): + """The project root directory is unknown or missing key files.""" + + +def get_versions(verbose=False): + """Get the project version from whatever source is available. + + Returns dict with two keys: 'version' and 'full'. + """ + if "versioneer" in sys.modules: + # see the discussion in cmdclass.py:get_cmdclass() + del sys.modules["versioneer"] + + root = get_root() + cfg = get_config_from_root(root) + + assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" + handlers = HANDLERS.get(cfg.VCS) + assert handlers, "unrecognized VCS '%s'" % cfg.VCS + verbose = verbose or cfg.verbose + assert cfg.versionfile_source is not None, \ + "please set versioneer.versionfile_source" + assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" + + versionfile_abs = os.path.join(root, cfg.versionfile_source) + + # extract version from first of: _version.py, VCS command (e.g. 'git + # describe'), parentdir. This is meant to work for developers using a + # source checkout, for users of a tarball created by 'setup.py sdist', + # and for users of a tarball/zipball created by 'git archive' or github's + # download-from-tag feature or the equivalent in other VCSes. + + get_keywords_f = handlers.get("get_keywords") + from_keywords_f = handlers.get("keywords") + if get_keywords_f and from_keywords_f: + try: + keywords = get_keywords_f(versionfile_abs) + ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) + if verbose: + print("got version from expanded keyword %s" % ver) + return ver + except NotThisMethod: + pass + + try: + ver = versions_from_file(versionfile_abs) + if verbose: + print("got version from file %s %s" % (versionfile_abs, ver)) + return ver + except NotThisMethod: + pass + + from_vcs_f = handlers.get("pieces_from_vcs") + if from_vcs_f: + try: + pieces = from_vcs_f(cfg.tag_prefix, root, verbose) + ver = render(pieces, cfg.style) + if verbose: + print("got version from VCS %s" % ver) + return ver + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + if verbose: + print("got version from parentdir %s" % ver) + return ver + except NotThisMethod: + pass + + if verbose: + print("unable to compute version") + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, "error": "unable to compute version", + "date": None} + + +def get_version(): + """Get the short version string for this project.""" + return get_versions()["version"] + + +def get_cmdclass(): + """Get the custom setuptools/distutils subclasses used by Versioneer.""" + if "versioneer" in sys.modules: + del sys.modules["versioneer"] + # this fixes the "python setup.py develop" case (also 'install' and + # 'easy_install .'), in which subdependencies of the main project are + # built (using setup.py bdist_egg) in the same python process. Assume + # a main project A and a dependency B, which use different versions + # of Versioneer. A's setup.py imports A's Versioneer, leaving it in + # sys.modules by the time B's setup.py is executed, causing B to run + # with the wrong versioneer. Setuptools wraps the sub-dep builds in a + # sandbox that restores sys.modules to it's pre-build state, so the + # parent is protected against the child's "import versioneer". By + # removing ourselves from sys.modules here, before the child build + # happens, we protect the child from the parent's versioneer too. + # Also see https://github.com/warner/python-versioneer/issues/52 + + cmds = {} + + # we add "version" to both distutils and setuptools + from distutils.core import Command + + class cmd_version(Command): + description = "report generated version string" + user_options = [] + boolean_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + vers = get_versions(verbose=True) + print("Version: %s" % vers["version"]) + print(" full-revisionid: %s" % vers.get("full-revisionid")) + print(" dirty: %s" % vers.get("dirty")) + print(" date: %s" % vers.get("date")) + if vers["error"]: + print(" error: %s" % vers["error"]) + cmds["version"] = cmd_version + + # we override "build_py" in both distutils and setuptools + # + # most invocation pathways end up running build_py: + # distutils/build -> build_py + # distutils/install -> distutils/build ->.. + # setuptools/bdist_wheel -> distutils/install ->.. + # setuptools/bdist_egg -> distutils/install_lib -> build_py + # setuptools/install -> bdist_egg ->.. + # setuptools/develop -> ? + # pip install: + # copies source tree to a tempdir before running egg_info/etc + # if .git isn't copied too, 'git describe' will fail + # then does setup.py bdist_wheel, or sometimes setup.py install + # setup.py egg_info -> ? + + # we override different "build_py" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.build_py import build_py as _build_py + else: + from distutils.command.build_py import build_py as _build_py + + class cmd_build_py(_build_py): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_py.run(self) + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if cfg.versionfile_build: + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_py"] = cmd_build_py + + if "cx_Freeze" in sys.modules: # cx_freeze enabled? + from cx_Freeze.dist import build_exe as _build_exe + # nczeczulin reports that py2exe won't like the pep440-style string + # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. + # setup(console=[{ + # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION + # "product_version": versioneer.get_version(), + # ... + + class cmd_build_exe(_build_exe): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _build_exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["build_exe"] = cmd_build_exe + del cmds["build_py"] + + if 'py2exe' in sys.modules: # py2exe enabled? + try: + from py2exe.distutils_buildexe import py2exe as _py2exe # py3 + except ImportError: + from py2exe.build_exe import py2exe as _py2exe # py2 + + class cmd_py2exe(_py2exe): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _py2exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["py2exe"] = cmd_py2exe + + # we override different "sdist" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.sdist import sdist as _sdist + else: + from distutils.command.sdist import sdist as _sdist + + class cmd_sdist(_sdist): + def run(self): + versions = get_versions() + self._versioneer_generated_versions = versions + # unless we update this, the command will keep using the old + # version + self.distribution.metadata.version = versions["version"] + return _sdist.run(self) + + def make_release_tree(self, base_dir, files): + root = get_root() + cfg = get_config_from_root(root) + _sdist.make_release_tree(self, base_dir, files) + # now locate _version.py in the new base_dir directory + # (remembering that it may be a hardlink) and replace it with an + # updated value + target_versionfile = os.path.join(base_dir, cfg.versionfile_source) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, + self._versioneer_generated_versions) + cmds["sdist"] = cmd_sdist + + return cmds + + +CONFIG_ERROR = """ +setup.cfg is missing the necessary Versioneer configuration. You need +a section like: + + [versioneer] + VCS = git + style = pep440 + versionfile_source = src/myproject/_version.py + versionfile_build = myproject/_version.py + tag_prefix = + parentdir_prefix = myproject- + +You will also need to edit your setup.py to use the results: + + import versioneer + setup(version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), ...) + +Please read the docstring in ./versioneer.py for configuration instructions, +edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. +""" + +SAMPLE_CONFIG = """ +# See the docstring in versioneer.py for instructions. Note that you must +# re-run 'versioneer.py setup' after changing this section, and commit the +# resulting files. + +[versioneer] +#VCS = git +#style = pep440 +#versionfile_source = +#versionfile_build = +#tag_prefix = +#parentdir_prefix = + +""" + +INIT_PY_SNIPPET = """ +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions +""" + + +def do_setup(): + """Main VCS-independent setup function for installing Versioneer.""" + root = get_root() + try: + cfg = get_config_from_root(root) + except (EnvironmentError, configparser.NoSectionError, + configparser.NoOptionError) as e: + if isinstance(e, (EnvironmentError, configparser.NoSectionError)): + print("Adding sample versioneer config to setup.cfg", + file=sys.stderr) + with open(os.path.join(root, "setup.cfg"), "a") as f: + f.write(SAMPLE_CONFIG) + print(CONFIG_ERROR, file=sys.stderr) + return 1 + + print(" creating %s" % cfg.versionfile_source) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), + "__init__.py") + if os.path.exists(ipy): + try: + with open(ipy, "r") as f: + old = f.read() + except EnvironmentError: + old = "" + if INIT_PY_SNIPPET not in old: + print(" appending to %s" % ipy) + with open(ipy, "a") as f: + f.write(INIT_PY_SNIPPET) + else: + print(" %s unmodified" % ipy) + else: + print(" %s doesn't exist, ok" % ipy) + ipy = None + + # Make sure both the top-level "versioneer.py" and versionfile_source + # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so + # they'll be copied into source distributions. Pip won't be able to + # install the package without this. + manifest_in = os.path.join(root, "MANIFEST.in") + simple_includes = set() + try: + with open(manifest_in, "r") as f: + for line in f: + if line.startswith("include "): + for include in line.split()[1:]: + simple_includes.add(include) + except EnvironmentError: + pass + # That doesn't cover everything MANIFEST.in can do + # (http://docs.python.org/2/distutils/sourcedist.html#commands), so + # it might give some false negatives. Appending redundant 'include' + # lines is safe, though. + if "versioneer.py" not in simple_includes: + print(" appending 'versioneer.py' to MANIFEST.in") + with open(manifest_in, "a") as f: + f.write("include versioneer.py\n") + else: + print(" 'versioneer.py' already in MANIFEST.in") + if cfg.versionfile_source not in simple_includes: + print(" appending versionfile_source ('%s') to MANIFEST.in" % + cfg.versionfile_source) + with open(manifest_in, "a") as f: + f.write("include %s\n" % cfg.versionfile_source) + else: + print(" versionfile_source already in MANIFEST.in") + + # Make VCS-specific changes. For git, this means creating/changing + # .gitattributes to mark _version.py for export-subst keyword + # substitution. + do_vcs_install(manifest_in, cfg.versionfile_source, ipy) + return 0 + + +def scan_setup_py(): + """Validate the contents of setup.py against Versioneer's expectations.""" + found = set() + setters = False + errors = 0 + with open("setup.py", "r") as f: + for line in f.readlines(): + if "import versioneer" in line: + found.add("import") + if "versioneer.get_cmdclass()" in line: + found.add("cmdclass") + if "versioneer.get_version()" in line: + found.add("get_version") + if "versioneer.VCS" in line: + setters = True + if "versioneer.versionfile_source" in line: + setters = True + if len(found) != 3: + print("") + print("Your setup.py appears to be missing some important items") + print("(but I might be wrong). Please make sure it has something") + print("roughly like the following:") + print("") + print(" import versioneer") + print(" setup( version=versioneer.get_version(),") + print(" cmdclass=versioneer.get_cmdclass(), ...)") + print("") + errors += 1 + if setters: + print("You should remove lines like 'versioneer.VCS = ' and") + print("'versioneer.versionfile_source = ' . This configuration") + print("now lives in setup.cfg, and should be removed from setup.py") + print("") + errors += 1 + return errors + + +if __name__ == "__main__": + cmd = sys.argv[1] + if cmd == "setup": + errors = do_setup() + errors += scan_setup_py() + if errors: + sys.exit(1)