Created
November 22, 2025 09:32
-
-
Save sleemanj/8904818bee556cb43613e84fe996239b to your computer and use it in GitHub Desktop.
Bash script (using gerbv and standard tools) to add two holes to a gerber paste mask and drill file for alignment purposes. Created mostly by Gemini 3 AI, a couple of manual fixes are commented with "Fix". To use, execute this from within the directory containing your gerber files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ============================================================================= | |
| # PCB Tooling Hole Injector (Multi-Unit Support) | |
| # Adds drill hits and paste mask openings to existing manufacturing files. | |
| # Handles mixed Imperial/Metric units between Gerber and Drill files. | |
| # ============================================================================= | |
| # Ensure standard decimal formatting | |
| export LC_NUMERIC="C" | |
| # Check dependencies | |
| for cmd in gerbv bc awk sed grep; do | |
| if ! command -v $cmd &> /dev/null; then | |
| echo "Error: Required command '$cmd' not found. Please install it." | |
| exit 1 | |
| fi | |
| done | |
| # --- 1. Locate Files --- | |
| echo "Scanning directory for files..." | |
| GKO_FILE=$(find . -maxdepth 1 -iname "*.gko" | head -n1) | |
| DRILL_FILE=$(find . -maxdepth 1 -iname "*.txt" | head -n1) | |
| GTP_FILE=$(find . -maxdepth 1 -iname "*.gtp" | head -n1) | |
| GTL_FILE=$(find . -maxdepth 1 -iname "*.gtl" | head -n1) | |
| GBL_FILE=$(find . -maxdepth 1 -iname "*.gbl" | head -n1) | |
| if [[ -z "$GKO_FILE" || -z "$DRILL_FILE" || -z "$GTP_FILE" || -z "$GTL_FILE" || -z "$GBL_FILE" ]]; then | |
| echo "Error: Could not find all required files (.gko, .txt, .gtp, .gtl, .gbl)." | |
| exit 1 | |
| fi | |
| echo "Found:" | |
| echo " Outline: $GKO_FILE" | |
| echo " Drill: $DRILL_FILE" | |
| echo " Paste: $GTP_FILE" | |
| echo " Top: $GTL_FILE" | |
| echo " Bottom: $GBL_FILE" | |
| echo "" | |
| # --- 2. Analyze Units --- | |
| # A. Gerber Units (from GKO) | |
| UNITS_GKO=$(grep -a "^%MO" "$GKO_FILE" | head -n1) | |
| if [[ "$UNITS_GKO" == *"%MOMM"* ]]; then | |
| IS_GERBER_METRIC=1 | |
| echo "Gerber Units: Metric (mm)" | |
| else | |
| IS_GERBER_METRIC=0 | |
| echo "Gerber Units: Imperial (inches)" | |
| fi | |
| # B. Drill Units (from TXT) | |
| # Look for "METRIC" string as requested | |
| if grep -iq "METRIC" "$DRILL_FILE"; then | |
| IS_DRILL_METRIC=1 | |
| echo "Drill Units: Metric (Detected 'METRIC' tag)" | |
| else | |
| IS_DRILL_METRIC=0 | |
| echo "Drill Units: Imperial (Defaulting, 'METRIC' tag not found)" | |
| fi | |
| # --- 3. Analyze Board Outline (GKO) --- | |
| # Extract Format Specification for Gerbers (Coordinate parsing) | |
| FORMAT_LINE=$(grep -a "^%FS" "$GKO_FILE" | head -n1) | |
| GERBER_DECIMALS=4 | |
| if [[ $FORMAT_LINE =~ X[0-9]([0-9]) ]]; then | |
| GERBER_DECIMALS=${BASH_REMATCH[1]} | |
| fi | |
| GERBER_DIVIDER=$(echo "10^$GERBER_DECIMALS" | bc) | |
| echo "Gerber Format: $GERBER_DECIMALS decimals (Divisor: $GERBER_DIVIDER)" | |
| # Parse GKO for Bounding Box | |
| read min_x_raw min_y_raw max_x_raw max_y_raw <<< $(awk -v div="$GERBER_DIVIDER" ' | |
| BEGIN { minx=999999999; miny=999999999; maxx=-999999999; maxy=-999999999 } | |
| /^X[0-9-]*Y[0-9-]*/ { | |
| match($0, /X([0-9-]+)/, x) | |
| match($0, /Y([0-9-]+)/, y) | |
| val_x = x[1] + 0 | |
| val_y = y[1] + 0 | |
| if (val_x < minx) minx = val_x | |
| if (val_x > maxx) maxx = val_x | |
| if (val_y < miny) miny = val_y | |
| if (val_y > maxy) maxy = val_y | |
| } | |
| END { print minx, miny, maxx, maxy } | |
| ' "$GKO_FILE") | |
| # Convert Raw Gerber Integer Coords to Real Gerber Units | |
| G_MIN_X=$(echo "scale=6; $min_x_raw / $GERBER_DIVIDER" | bc) | |
| G_MIN_Y=$(echo "scale=6; $min_y_raw / $GERBER_DIVIDER" | bc) | |
| G_MAX_X=$(echo "scale=6; $max_x_raw / $GERBER_DIVIDER" | bc) | |
| G_MAX_Y=$(echo "scale=6; $max_y_raw / $GERBER_DIVIDER" | bc) | |
| # Calculate Dimensions in Gerber Units | |
| WIDTH_GU=$(echo "scale=6; $G_MAX_X - $G_MIN_X" | bc) | |
| HEIGHT_GU=$(echo "scale=6; $G_MAX_Y - $G_MIN_Y" | bc) | |
| # Display Dimensions to User (Always in mm) | |
| if [ "$IS_GERBER_METRIC" -eq 0 ]; then | |
| DISP_W=$(echo "scale=2; $WIDTH_GU * 25.4" | bc) | |
| DISP_H=$(echo "scale=2; $HEIGHT_GU * 25.4" | bc) | |
| # Determine Origin in MM for calculation base | |
| ORIGIN_X_MM=$(echo "scale=6; $G_MIN_X * 25.4" | bc) | |
| ORIGIN_Y_MM=$(echo "scale=6; $G_MIN_Y * 25.4" | bc) | |
| else | |
| DISP_W=$WIDTH_GU | |
| DISP_H=$HEIGHT_GU | |
| ORIGIN_X_MM=$G_MIN_X | |
| ORIGIN_Y_MM=$G_MIN_Y | |
| fi | |
| echo "------------------------------------------------" | |
| echo "Board Outline Dimensions: $DISP_W x $DISP_H mm" | |
| echo "------------------------------------------------" | |
| # --- 4. Input Loop --- | |
| while true; do | |
| echo "Enter offsets in Millimeters from Bottom-Left corner." | |
| read -p "Hole 1 X offset (mm): " h1_x_off | |
| read -p "Hole 1 Y offset (mm): " h1_y_off | |
| read -p "Hole 2 X offset (mm): " h2_x_off | |
| read -p "Hole 2 Y offset (mm): " h2_y_off | |
| read -p "Hole Diameter (mm) [default 3]: " h_diam | |
| h_diam=${h_diam:-3} | |
| # --- 5. Calculate Absolute Coordinates in MM --- | |
| # User Offset + Board Origin (in MM) = Absolute Position in MM | |
| ABS_H1_X_MM=$(echo "scale=6; $ORIGIN_X_MM + $h1_x_off" | bc) | |
| ABS_H1_Y_MM=$(echo "scale=6; $ORIGIN_Y_MM + $h1_y_off" | bc) | |
| ABS_H2_X_MM=$(echo "scale=6; $ORIGIN_X_MM + $h2_x_off" | bc) | |
| ABS_H2_Y_MM=$(echo "scale=6; $ORIGIN_Y_MM + $h2_y_off" | bc) | |
| # --- 6. Calculate GERBER Coordinates --- | |
| # Convert Absolute MM -> Gerber Units -> Integer Format | |
| mm_to_gerber_int() { | |
| local mm_val=$1 | |
| local result | |
| if [ "$IS_GERBER_METRIC" -eq 0 ]; then | |
| # MM to Inch | |
| result=$(echo "scale=6; $mm_val / 25.4" | bc) | |
| else | |
| result=$mm_val | |
| fi | |
| # Float to Scaled Integer (Rounding handled by printf %.0f via awk) | |
| echo "$result * $GERBER_DIVIDER" | bc | awk '{printf "%0.0f", $1}' | |
| } | |
| H1_X_GERBER=$(mm_to_gerber_int $ABS_H1_X_MM) | |
| H1_Y_GERBER=$(mm_to_gerber_int $ABS_H1_Y_MM) | |
| H2_X_GERBER=$(mm_to_gerber_int $ABS_H2_X_MM) | |
| H2_Y_GERBER=$(mm_to_gerber_int $ABS_H2_Y_MM) | |
| # Aperture size for Gerber | |
| if [ "$IS_GERBER_METRIC" -eq 0 ]; then | |
| APER_SIZE_GERBER=$(echo "scale=4; $h_diam / 25.4" | bc) | |
| else | |
| APER_SIZE_GERBER=$h_diam | |
| fi | |
| # --- 7. Calculate DRILL Coordinates --- | |
| # Convert Absolute MM -> Drill Units -> Explicit Decimal Format | |
| mm_to_drill_float() { | |
| local mm_val=$1 | |
| if [ "$IS_DRILL_METRIC" -eq 0 ]; then | |
| # MM to Inch | |
| echo "scale=5; $mm_val / 25.4" | bc | awk '{printf "%.5f", $0}' | |
| else | |
| # MM is MM | |
| echo "scale=3; $mm_val" | bc | awk '{printf "%.3f", $0}' | |
| fi | |
| } | |
| H1_X_DRILL=$(mm_to_drill_float $ABS_H1_X_MM) | |
| H1_Y_DRILL=$(mm_to_drill_float $ABS_H1_Y_MM) | |
| H2_X_DRILL=$(mm_to_drill_float $ABS_H2_X_MM) | |
| H2_Y_DRILL=$(mm_to_drill_float $ABS_H2_Y_MM) | |
| # Tool size for Drill | |
| # if [ "$IS_DRILL_METRIC" -eq 0 ]; then | |
| # TOOL_SIZE_DRILL=$(echo "scale=4; $h_diam / 25.4" | bc) | |
| # else | |
| # TOOL_SIZE_DRILL=$h_diam | |
| # fi | |
| # Fix - James; If the tool size isn't correctly formatted with 2 dp | |
| # gerbv doesn't interpret it correctly | |
| TOOL_SIZE_DRILL=$(mm_to_drill_float $h_diam) | |
| # --- 8. Generate Files --- | |
| WORK_DIR=$(mktemp -d /tmp/pcb_mod_XXXXXX) | |
| echo "Processing in $WORK_DIR..." | |
| cp "$GKO_FILE" "$WORK_DIR/" | |
| cp "$DRILL_FILE" "$WORK_DIR/" | |
| cp "$GTP_FILE" "$WORK_DIR/" | |
| cp "$GTL_FILE" "$WORK_DIR/" | |
| cp "$GBL_FILE" "$WORK_DIR/" | |
| TGT_GTP="$WORK_DIR/$(basename "$GTP_FILE")" | |
| TGT_DRILL="$WORK_DIR/$(basename "$DRILL_FILE")" | |
| # Modify Paste Mask (GERBER) | |
| # Add Aperture (D199) | |
| sed -i "/^%ADD/!b;n;a %ADD199C,$APER_SIZE_GERBER*%" "$TGT_GTP" | |
| # Add Flashes | |
| NEW_G_LINES="D199*\nX${H1_X_GERBER}Y${H1_Y_GERBER}D03*\nX${H2_X_GERBER}Y${H2_Y_GERBER}D03*" | |
| sed -i "s/^M02/$NEW_G_LINES\nM02/" "$TGT_GTP" | |
| # Modify Drill File (DRILL) | |
| # Add Tool (T99) | |
| # We use explicit decimals in the tool definition to be safe | |
| sed -i "/^%/i T99C$TOOL_SIZE_DRILL" "$TGT_DRILL" | |
| # Fix - James, the above also adds a line at the top of the file incorrectly | |
| cat "$TGT_DRILL" | tail -n +2 >"$TGT_DRILL.2" | |
| mv "$TGT_DRILL.2" "$TGT_DRILL" | |
| # Add Hits using Explicit Decimals | |
| DRILL_LINES="T99\nX${H1_X_DRILL}Y${H1_Y_DRILL}\nX${H2_X_DRILL}Y${H2_Y_DRILL}" | |
| sed -i "s/^M30/$DRILL_LINES\nM30/" "$TGT_DRILL" | |
| # --- 9. Visualize --- | |
| echo "Opening gerbv..." | |
| gerbv \ | |
| "$TGT_DRILL" \ | |
| "$TGT_GTP" \ | |
| "$WORK_DIR/$(basename "$GTL_FILE")" \ | |
| "$WORK_DIR/$(basename "$GBL_FILE")" \ | |
| "$WORK_DIR/$(basename "$GKO_FILE")" \ | |
| & | |
| GERBV_PID=$! | |
| # --- 10. Confirm --- | |
| read -p "Is the placement acceptable? (y/N): " confirm | |
| if ps -p $GERBV_PID > /dev/null; then | |
| kill $GERBV_PID 2>/dev/null | |
| fi | |
| if [[ "$confirm" =~ ^[Yy]$ ]]; then | |
| echo "Success! New files located at:" | |
| echo "$WORK_DIR" | |
| exit 0 | |
| else | |
| echo "Retrying..." | |
| rm -rf "$WORK_DIR" | |
| echo "" | |
| fi | |
| done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment