#!/usr/bin/env bash # export_for_msla.sh — KiCad Gerber export + pm4n generation for Anycubic Photon Mono 4 # # Usage: # ./export_for_msla.sh [OPTIONS] # # Options: # --layers LAYER,LAYER,... KiCad layer names to export (default: F.Cu) # --invert LAYER,LAYER,... Layers to invert (comma-separated) # --mirror LAYER,LAYER,... Layers to mirror (comma-separated) # --exposure SECONDS Exposure time in seconds (default: 60) # --dummy FILE Dummy .pm4n template (default: Dummy.pm4n beside this script) # --out DIR Output directory (default: ./output) # --dpmm N Render resolution in dots/mm (default: native 58.824) # --pos X,Y Board position on LCD in mm (default: centred) # --preview-scale N Downsample preview PNG by N (default: 1 = full resolution) # --verbose Print detailed progress # -h, --help Show this help # # Normal output: one output filepath per layer. # Verbose output: full progress from KiCad and the converter. # # Example: # ./export_for_msla.sh --invert F.Cu,B.Mask --mirror F.Cu,F.Mask panel/board.kicad_pcb # # KiCad layer name → Gerber filename stem: # F.Cu → F_Cu B.Cu → B_Cu # F.Mask → F_Mask B.Mask → B_Mask # F.SilkS → F_Silkscreen B.SilkS → B_Silkscreen # F.Paste → F_Paste B.Paste → B_Paste # Edge.Cuts → Edge_Cuts # Front → Front Back → Back set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PYTHON="${PYTHON:-python3}" CONVERTER="$SCRIPT_DIR/gerber_to_pm4n.py" # ---- defaults ---- LAYERS="F.Cu" INVERT_LAYERS="" MIRROR_LAYERS="" EXPOSURE="60" DUMMY="$SCRIPT_DIR/Dummy.pm4n" OUT_DIR="./output" DPMM="" POS="" VERBOSE=0 PREVIEW_SCALE="" # ---- helpers ---- usage() { grep '^#' "$0" | sed 's/^# \{0,1\}//' exit 0 } log() { [[ $VERBOSE -eq 1 ]] && echo "$@" || true; } contains() { local list="$1" item="$2" echo "$list" | tr ',' '\n' | grep -qx "$item" } layer_to_filename() { case "$1" in F.Cu) echo "Front" ;; B.Cu) echo "Back" ;; F.SilkS) echo "F_Silkscreen" ;; B.SilkS) echo "B_Silkscreen" ;; Edge.Cuts) echo "Edge_Cuts" ;; *) echo "${1//./_}" ;; esac } # ---- parse arguments ---- PCB_FILE="" while [[ $# -gt 0 ]]; do case "$1" in --layers) LAYERS="$2"; shift 2 ;; --invert) INVERT_LAYERS="$2"; shift 2 ;; --mirror) MIRROR_LAYERS="$2"; shift 2 ;; --exposure) EXPOSURE="$2"; shift 2 ;; --dummy) DUMMY="$2"; shift 2 ;; --out) OUT_DIR="$2"; shift 2 ;; --dpmm) DPMM="$2"; shift 2 ;; --pos) POS="$2"; shift 2 ;; --preview-scale) PREVIEW_SCALE="$2"; shift 2 ;; --verbose) VERBOSE=1; shift ;; -h|--help) usage ;; -*) echo "ERROR: unknown option: $1" >&2; exit 1 ;; *) PCB_FILE="$1"; shift ;; esac done [[ -z "$PCB_FILE" ]] && { echo "ERROR: no .kicad_pcb file specified" >&2; exit 1; } [[ ! -f "$PCB_FILE" ]] && { echo "ERROR: file not found: $PCB_FILE" >&2; exit 1; } [[ ! -f "$DUMMY" ]] && { echo "ERROR: Dummy.pm4n not found: $DUMMY" >&2; exit 1; } BOARD_NAME="$(basename "$PCB_FILE" .kicad_pcb)" GERBERS_DIR="$OUT_DIR/gerbers" PM4N_DIR="$OUT_DIR/pm4n" mkdir -p "$GERBERS_DIR" "$PM4N_DIR" # ---- Step 1: export Gerbers ---- log "=== Exporting Gerbers ===" log " Board: $PCB_FILE" log " Layers: $LAYERS" kicad-cli pcb export gerbers \ --output "$GERBERS_DIR" \ --layers "$LAYERS" \ --no-protel-ext \ --subtract-soldermask \ --no-netlist \ "$PCB_FILE" \ 2>/dev/null log "" # ---- Step 2: convert to pm4n ---- log "=== Converting to .pm4n ===" IFS=',' read -ra LAYER_LIST <<< "$LAYERS" for LAYER in "${LAYER_LIST[@]}"; do LAYER_STEM="$(layer_to_filename "$LAYER")" GBR_FILE="$GERBERS_DIR/${BOARD_NAME}-${LAYER_STEM}.gbr" if [[ ! -f "$GBR_FILE" ]]; then echo "WARNING: Gerber not found for layer $LAYER (expected: $GBR_FILE)" >&2 continue fi OUT_PM4N="$PM4N_DIR/${BOARD_NAME}-${LAYER_STEM}.pm4n" FLAGS=() contains "$INVERT_LAYERS" "$LAYER" && FLAGS+=(--invert) contains "$MIRROR_LAYERS" "$LAYER" && FLAGS+=(--mirror) [[ -n "$DPMM" ]] && FLAGS+=(--dpmm "$DPMM") [[ -n "$POS" ]] && FLAGS+=(--pos "$POS") [[ -n "$PREVIEW_SCALE" ]] && FLAGS+=(--preview-scale "$PREVIEW_SCALE") [[ $VERBOSE -eq 1 ]] && FLAGS+=(--verbose) log " $LAYER → $OUT_PM4N [${FLAGS[*]:-} exposure=${EXPOSURE}s]" "$PYTHON" "$CONVERTER" \ "$DUMMY" \ "$GBR_FILE" \ --output "$OUT_PM4N" \ --exposure "$EXPOSURE" \ "${FLAGS[@]}" done log "=== Done ==="