diff options
| -rw-r--r-- | PrusaSlicer/TinyMaker.ini | 175 | ||||
| -rwxr-xr-x | sl1-to-tinymaker.py | 273 |
2 files changed, 448 insertions, 0 deletions
diff --git a/PrusaSlicer/TinyMaker.ini b/PrusaSlicer/TinyMaker.ini new file mode 100644 index 0000000..425c759 --- /dev/null +++ b/PrusaSlicer/TinyMaker.ini @@ -0,0 +1,175 @@ +# generated by PrusaSlicer 2.9.5 on 2026-05-23 at 14:26:24 UTC +absolute_correction = 0 +area_fill = 35 +bed_custom_model = +bed_custom_texture = +bed_shape = 0x0,40.8x0,40.8x30.6,0x30.6 +bottle_cost = 0 +bottle_volume = 1000 +bottle_weight = 1 +branchingsupport_base_diameter = 4 +branchingsupport_base_height = 1 +branchingsupport_base_safety_distance = 1 +branchingsupport_buildplate_only = 0 +branchingsupport_critical_angle = 45 +branchingsupport_head_front_diameter = 0.4 +branchingsupport_head_penetration = 0.2 +branchingsupport_head_width = 1 +branchingsupport_max_bridge_length = 5 +branchingsupport_max_bridges_on_pillar = 2 +branchingsupport_max_pillar_link_distance = 10 +branchingsupport_max_weight_on_model = 10 +branchingsupport_object_elevation = 5 +branchingsupport_pillar_connection_mode = dynamic +branchingsupport_pillar_diameter = 1 +branchingsupport_pillar_widening_factor = 0.5 +branchingsupport_small_pillar_diameter_percent = 50% +compatible_prints = +compatible_prints_condition = +default_sla_material_profile = TinyMaker +default_sla_print_profile = TinyMaker +delay_after_exposure = 0,0 +delay_before_exposure = 3,3 +delay_to_reflood = 0,0 +display_height = 30.6 +display_mirror_x = 1 +display_mirror_y = 0 +display_orientation = landscape +display_pixels_x = 320 +display_pixels_y = 240 +display_width = 40.8 +dynamic_delay_before_profile = disabled,disabled +dynamic_delay_before_timeout = 50,50 +dynamic_tilt_down_profile = disabled,disabled +dynamic_tilt_up_profile = disabled,disabled +elefant_foot_compensation = 0.2 +elefant_foot_min_width = 0.2 +exposure_time = 14 +faded_layers = 8 +fast_tilt_time = 5 +gamma_correction = 1 +high_viscosity_tilt_time = 10 +hollowing_closing_distance = 2 +hollowing_enable = 0 +hollowing_min_thickness = 3 +hollowing_quality = 0.5 +host_type = prusalink +initial_exposure_time = 35 +initial_layer_height = 0.3 +layer_height = 0.05 +material_colour = #29B2B2 +material_correction = 1,1,1 +material_correction_x = 1 +material_correction_y = 1 +material_correction_z = 1 +material_density = 1 +material_notes = +material_ow_absolute_correction = nil +material_ow_branchingsupport_critical_angle = nil +material_ow_branchingsupport_head_front_diameter = nil +material_ow_branchingsupport_head_penetration = nil +material_ow_branchingsupport_head_width = nil +material_ow_branchingsupport_max_bridge_length = nil +material_ow_branchingsupport_max_bridges_on_pillar = nil +material_ow_branchingsupport_max_pillar_link_distance = nil +material_ow_branchingsupport_pillar_diameter = nil +material_ow_branchingsupport_small_pillar_diameter_percent = nil +material_ow_elefant_foot_compensation = nil +material_ow_faded_layers = 8 +material_ow_pad_wall_slope = nil +material_ow_support_critical_angle = nil +material_ow_support_head_front_diameter = nil +material_ow_support_head_penetration = nil +material_ow_support_head_width = nil +material_ow_support_max_bridge_length = nil +material_ow_support_max_bridges_on_pillar = nil +material_ow_support_max_pillar_link_distance = nil +material_ow_support_pillar_diameter = nil +material_ow_support_points_density_relative = nil +material_ow_support_small_pillar_diameter_percent = nil +material_print_speed = fast +material_type = Tough +material_uuid = +material_vendor = (Unknown) +max_exposure_time = 120 +max_initial_exposure_time = 300 +max_print_height = 60 +min_exposure_time = 0.8 +min_initial_exposure_time = 0.8 +output_filename_format = [input_filename_base].zip +pad_around_object = 0 +pad_around_object_everywhere = 0 +pad_brim_size = 1.6 +pad_enable = 1 +pad_max_merge_distance = 50 +pad_object_connector_penetration = 0.3 +pad_object_connector_stride = 10 +pad_object_connector_width = 0.5 +pad_object_gap = 1 +pad_wall_height = 0 +pad_wall_slope = 90 +pad_wall_thickness = 2 +physical_printer_settings_id = +print_host = +printer_model = TinyMaker +printer_notes = +printer_settings_id = TinyMaker +printer_technology = SLA +printer_variant = default +printer_vendor = +printhost_apikey = +printhost_cafile = +printing_temperature = nil +relative_correction = 1,1 +relative_correction_x = 1 +relative_correction_y = 1 +relative_correction_z = 1 +sla_archive_format = SL1 +sla_material_settings_id = TinyMaker +sla_output_precision = 0.001 +sla_print_settings_id = TinyMaker +slice_closing_radius = 0.049 +slicing_mode = regular +slow_tilt_time = 8 +support_base_diameter = 4 +support_base_height = 1 +support_base_safety_distance = 1 +support_buildplate_only = 0 +support_critical_angle = 45 +support_enforcers_only = 0 +support_head_front_diameter = 0.4 +support_head_penetration = 0.2 +support_head_width = 1 +support_max_bridge_length = 15 +support_max_bridges_on_pillar = 3 +support_max_pillar_link_distance = 10 +support_max_weight_on_model = 10 +support_object_elevation = 5 +support_pillar_connection_mode = dynamic +support_pillar_diameter = 1 +support_pillar_widening_factor = 0.5 +support_points_density_relative = 100 +support_small_pillar_diameter_percent = 50% +support_tree_type = default +supports_enable = 1 +thumbnails = 224x168/PNG +tilt_down_cycles = 1,1 +tilt_down_delay = 0,0 +tilt_down_finish_speed = layer1750,layer1750 +tilt_down_finish_speed_slx = layer160,layer160 +tilt_down_initial_speed = layer1750,layer1750 +tilt_down_initial_speed_slx = layer160,layer160 +tilt_down_offset_delay = 0,0 +tilt_down_offset_steps = 0,0 +tilt_up_cycles = 1,1 +tilt_up_delay = 0,0 +tilt_up_finish_speed = layer1750,layer1750 +tilt_up_finish_speed_slx = layer160,layer160 +tilt_up_initial_speed = move8000,move8000 +tilt_up_initial_speed_slx = layer160,layer160 +tilt_up_offset_delay = 0,0 +tilt_up_offset_steps = 1200,1200 +tower_hop_height = 0,0 +tower_speed = layer22,layer22 +use_tilt = 1,1 +zcorrection_layers = 0 diff --git a/sl1-to-tinymaker.py b/sl1-to-tinymaker.py new file mode 100755 index 0000000..d2f959a --- /dev/null +++ b/sl1-to-tinymaker.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2026 Sophia Pearson <codergal89@gmail.com> + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +from tkinter import filedialog +from tkinter.ttk import * + +from argparse import ArgumentParser +from configparser import ConfigParser, UNNAMED_SECTION +from dataclasses import dataclass +from glob import glob +from zipfile import ZipFile +from io import TextIOWrapper + +import logging +import os.path +import shutil +import sys + + +PROGRAM_NAME = "sl1-to-tinymaker" +LOGGER = logging.getLogger(PROGRAM_NAME) + +STANDARD_PREABLE = """ +;START_GCODE_BEGIN +G21; use metric units +G90; perform absolute movements +M106 S0; turn the light off +G28 Z0; home to Z0 + +; START_GCODE_END +""" + +STANDARD_LAYER = """ +;LAYER_START:{layer} +;currPos:{bottom_height} +M6054 "{slice_name}"; show the slice +G0 Z{top_height:.6f} F{speed_raise:.6f}; raise the platform +G0 Z{bottom_height:.6f} F{speed_lower:.6f}; lower the platform into position +G4 P200; dwell to stabilize +M106 S255.000000; turn the light on +G4 P{time_in_ms}; expose +M106 S0; turn the light off + +;LAYER_END +""" + +STANDARD_POSTAMBLE = """ +;END_GCODE_BEGIN +M106 S0; turn the light off +G1 Z60.000000 F25; raise the platform +M18; disable all steppers + +;END_GCODE_END +""" + + +@dataclass +class PrintConfiguration: + """A simple DTO to handle print configurations""" + + exposure_time: float + first_layer_exposure_time: float + layer_height: float + initial_layers: int + + +def select_file() -> str: + """ + Ask the user to select an archive file + + :return: The path to the selected file. + """ + + result = filedialog.askopenfilename( + title="Open sliced archive", + filetypes=(("sl1 archives", "*.zip *.sl1"),) + ) + return result if result else None + +def read_print_configuration(archive: ZipFile) -> PrintConfiguration: + """ + Read the configuration from an SL1 config.ini format + + :param ZipFile archive: The SL1 arhcive + :return: The parsed print configuration. + :rtype: PrintConfiguration + """ + + with TextIOWrapper(archive.open("config.ini", "r"), encoding="utf-8") as config_file: + parser = ConfigParser(allow_unnamed_section=True) + parser.read_file(config_file) + + layer_time = float(parser.get(UNNAMED_SECTION, "expTime")) + first_layer_time = float(parser.get(UNNAMED_SECTION, "expTimeFirst")) + layer_height = float(parser.get(UNNAMED_SECTION, "layerHeight")) + initial_layers = int(parser.get(UNNAMED_SECTION, "numFade")) + + return PrintConfiguration(layer_time, first_layer_time, layer_height, initial_layers) + + +def collect_slices(archive: ZipFile) -> list[str]: + """ + Collect all layer slices. + + :param ZipFile archive: The SL1 archive + :return: a list of slice filenames. + :rtype: list[str] + """ + + all_files = archive.namelist() + png_files = filter(lambda x : x.endswith(".png") , all_files) + slice_files = filter(lambda x : not x.startswith("thumbnail/"), png_files) + + return list(slice_files) + + +def write_gcode(root: str, config: PrintConfiguration, slice_count: int) -> None: + """ + Write the required print gcode for the print. + + :param str root: The path the root of converted print. + :param PrintConfiguration config: The configuration for this print. + :param int slice_count: The number of slices in this archive. + """ + + if not os.path.exists(root) or not os.path.isdir(root): + raise FileNotFoundError(f"'{root}' does not exist") + + gcode_file_path = f"{root}/run.gcode" + + with open(gcode_file_path, "w") as gcode_file: + gcode_file.write(STANDARD_PREABLE) + + for layer in range(slice_count): + if layer < config.initial_layers: + layer_time = config.first_layer_exposure_time + speed_raise = 40.0 + speed_lower = 50.0 + else: + layer_time = config.exposure_time + speed_raise = 40.0 + speed_lower = 150.0 + + gcode_file.write(STANDARD_LAYER.format( + bottom_height=(layer + 1) * config.layer_height, + layer=layer, + slice_name=f"{layer + 1}.png", + speed_lower = speed_lower, + speed_raise = speed_raise, + time_in_ms=int(1000 * layer_time), + top_height=2 + (layer + 1) * config.layer_height, + ) + ) + + gcode_file.write(STANDARD_POSTAMBLE) + + +def extract_slices(root: str, archive: ZipFile, slices: list[str]) -> None: + """ + Copy the slice files from an extracted SL1 archive to a TinyMaker compatible folder. + + :param str root: The path the root of converted print. + :param ZipFile arhive: The SL1 archive. + :param list[str] slices: The names of the slice images in the archive. + """ + + if not os.path.exists(root) or not os.path.isdir(root): + raise FileNotFoundError(f"'{root}' does not exist") + + for x, slice_name in enumerate(slices, 1): + slice_info = archive.getinfo(slice_name) + slice_info.filename = f"{x}.png" + archive.extract(slice_info, root) + + +def main(): + """The main application""" + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(message)s", + datefmt='%m/%d/%Y %I:%M:%S %p', + ) + + parser = ArgumentParser( + prog=PROGRAM_NAME, + description="Convert SL1 archives to TinyMaker compatible print folders.", + ) + parser.add_argument("file", help="The SL1 archive to convert", nargs="?", type=str) + args = parser.parse_args() + + selected_file = args.file if args.file else select_file() + + if not selected_file: + LOGGER.error("No file was selected") + return 1 + + full_file_path = os.path.abspath(selected_file) + + if not os.path.isfile(full_file_path): + LOGGER.error(f"File not found: {full_file_path}") + return 1 + + with ZipFile(full_file_path, "r") as archive: + LOGGER.info(f"Converting {full_file_path} to TinyMaker folder") + + try: + config = read_print_configuration(archive) + LOGGER.info(f"Successfully read configuration in '{full_file_path}'") + LOGGER.info(f"exposure time is {config.exposure_time}s ({config.first_layer_exposure_time}s for the first layer)") + LOGGER.info(f"layer height is {config.layer_height}") + LOGGER.info(f"base layer count is {config.initial_layers}") + except Exception as e: + LOGGER.error(f"Failed to read configuration in '{print_root}': {e}") + return 1 + + try: + slices = collect_slices(archive) + LOGGER.info(f"Found {len(slices)} slices in '{full_file_path}'") + except Exception as e: + LOGGER.error(f"Failed to collect slices from '{full_file_path}': {e}") + return 2 + + try: + output_root = f"{os.path.splitext(full_file_path)[0]}-tinymaker" + os.mkdir(output_root) + LOGGER.info(f"Created output directory '{output_root}'") + except Exception as e: + LOGGER.error(f"Failed to create output directory '{output_root}': {e}") + return 3 + + try: + write_gcode(output_root, config, len(slices)) + LOGGER.info(f"Generated gcode in '{output_root}'") + except Exception as e: + LOGGER.error(f"Failed to write gcode in '{output_root}': {e}") + return 4 + + try: + extract_slices(output_root, archive, slices) + LOGGER.info(f"Extracted slices to '{output_root}'") + except Exception as e: + LOGGER.error(f"Failed to extract slices to '{output_root}': {e}") + return 5 + + +if __name__ == "__main__": + sys.exit(main()) |
