summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--PrusaSlicer/TinyMaker.ini175
-rwxr-xr-xsl1-to-tinymaker.py273
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())