rushabhcodes/ESP32-E-Reader

A compact ESP32-C3 e-reader PCB integrating LiPo charging/protection, USB-C, 3.3 V regulation, e-paper display bias generation, microSD storage, and tactile controls.

Version
1.0.85
License
unset
Stars
3

scripts/inspect-print-toolpaths.py

"""Inspect reference G-code bounds, layer coverage and spring toolpaths.

Run after check-printability.py. Draws actual extruded XY moves at selected
heights; it does not simulate deposited plastic or predict fatigue/fit.
"""
from pathlib import Path
from collections import defaultdict
import hashlib
import json
import math
import re
from PIL import Image, ImageDraw, ImageFont

ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "checks/printability"
ASSETS = ROOT / "assets/enclosure"
SPEC = json.loads((ASSETS / "display-connection.json").read_text())
CASE_Y = SPEC["case"]["centerY"]
COLORS = {"model": "#236a9e", "support": "#b27c26", "bridge": "#a63963"}

def read_moves(path):
    layers = defaultdict(list)
    xyz = [0., 0., 0.]
    e = 0.
    relative_e = False
    relative_xyz = False
    kind = ""
    for line in path.read_text().splitlines():
        if line.startswith(";TYPE:"):
            kind = line[6:].strip().lower()
        code = line.split(";")[0].strip()
        if not code:
            continue
        cmd = code.split()[0]
        words = {k: float(v) for k, v in re.findall(r"([XYZE])(-?(?:\d+(?:\.\d*)?|\.\d+))", code)}
        if cmd in ("M82", "M83"):
            relative_e = cmd == "M83"
        if cmd in ("G90", "G91"):
            relative_xyz = cmd == "G91"
        if cmd == "G92" and "E" in words:
            e = words["E"]
        if cmd not in ("G0", "G1", "G2", "G3"):
            continue
        previous = xyz[:]
        for i, axis in enumerate("XYZ"):
            if axis in words:
                xyz[i] = words[axis] + (xyz[i] if relative_xyz else 0)
        delta = 0.
        if "E" in words:
            delta = words["E"] if relative_e else words["E"] - e
            e = words["E"] if not relative_e else e + words["E"]
        if delta <= 0 or math.dist(previous[:2], xyz[:2]) < 1e-6 or not kind:
            continue
        assert cmd in ("G0", "G1"), "Arc toolpaths need a different inspector"
        layers[round(xyz[2], 3)].append((previous[:2], xyz[:2], kind))
    return dict(sorted(layers.items()))

def covers(moves, bounds):
    # Presence of an actual extrusion in a region; not a strength calculation.
    return any(bounds[0] <= (a[0] + b[0]) / 2 <= bounds[1]
               and bounds[2] <= (a[1] + b[1]) / 2 <= bounds[3]
               and "support" not in kind for a, b, kind in moves)

data = {}
for name in ("front-chassis", "rear-cover", "reader-print-plate"):
    path = OUT / (name + ".gcode")
    layers = read_moves(path)
    points = [p for moves in layers.values() for a, b, _ in moves for p in (a, b)]
    bounds = [[min(p[i] for p in points), max(p[i] for p in points)] for i in (0, 1)]
    assert all(0 <= low <= high <= 220 for low, high in bounds), (name, bounds)
    assert min(layers) == .2 and all(0 < z <= 250 for z in layers), name
    assert not any(b - a > .401 for a, b in zip(layers, list(layers)[1:])), name
    data[name] = {"gcode_sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
                  "extrusion_bounds_xy_mm": bounds, "extrusion_z_range_mm": [min(layers), max(layers)],
                  "extrusion_layers": len(layers), "extruded_xy_moves": sum(map(len, layers.values())),
                  "reference_bed_bounds_pass": True}
    if name == "front-chassis":
        spring_layers = {}
        # Centered STL: original world Y maps to print Y + (110 - CASE_Y).
        for x in SPEC["controls"]["navigationX"]:
            box = (110 + x - 4.5, 110 + x + 4.5, 110 - CASE_Y - 46.8, 110 - CASE_Y - 45.8)
            levels = [z for z, moves in layers.items() if 1.0 <= z <= 1.9 and covers(moves, box)]
            assert len(levels) >= 4, ("Key spring lost during slicing", x, levels)
            spring_layers[str(x)] = levels
        data[name]["key_leaf_toolpath_layers"] = spring_layers
        screen_leaves = {}
        for side in (-1, 1):
            for tip in SPEC['case']['screenClipTipY']:
                x, y = 110 + side * 29.3, 110 - CASE_Y + tip + 7
                levels = [z for z, moves in layers.items() if 2.7 <= z <= 3.9
                          and covers(moves, (x - .3, x + .3, y - 3, y + 3))]
                assert len(levels) >= 4, ("Screen leaf lost", side, tip, levels)
                screen_leaves[f"{side},{tip}"] = levels
        data[name]["six_screen_leaf_toolpath_layers"] = screen_leaves
    if name == "rear-cover":
        snaps = {}
        for side in (-1, 1):
            for tip in (-5., 24.):
                x, y = 110 + side * 32.6, 110 + CASE_Y - tip - 7.05
                levels = [z for z, moves in layers.items() if 3.0 <= z <= 6.3
                          and covers(moves, (x - .3, x + .3, y - 3, y + 3))]
                assert len(levels) >= 10, ("Cover snap leaf lost", side, tip, levels)
                snaps[f"{side},{tip}"] = levels
        data[name]["four_cover_snap_toolpath_layers"] = snaps
        supports = {}
        for x in (-12., 12.):
            levels = [z for z, moves in layers.items() if 12 <= z <= 14.2
                      and covers(moves, (110 + x - 3, 110 + x + 3, 110 + CASE_Y - SPEC["case"]["topSupportY"] - .4, 110 + CASE_Y - SPEC["case"]["topSupportY"] + .4))]
            assert len(levels) >= 8, ("Upper screen support lost", x, levels)
            supports[str(x)] = levels
        data[name]["two_upper_screen_support_toolpath_layers"] = supports
    if name == "reader-print-plate":
        continue
    selected = (.2, 1.4, 2.6, 3.6) if name == "front-chassis" else (.2, 5.8, 7.6, 10.2)
    canvas = Image.new("RGB", (1360, 690), "#f5f2ed")
    draw = ImageDraw.Draw(canvas)
    font = ImageFont.truetype("/usr/share/fonts/TTF/DejaVuSans.ttf", 16)
    small = ImageFont.truetype("/usr/share/fonts/TTF/DejaVuSans.ttf", 12)
    draw.text((22, 16), name + " — actual reference toolpaths", font=font, fill="#222222")
    draw.text((22, 44), "Blue: model   Gold: removable supports   Pink: bridges | 0.4 mm nozzle / 0.2 mm layers / PETG", font=small, fill="#444444")
    for column, target in enumerate(selected):
        z = min(layers, key=lambda level: abs(level - target))
        ox, oy = 22 + 335 * column, 110
        draw.text((ox, 76), f"Layer at Z {z:.2f} mm", font=font, fill="#222222")
        for a, b, kind in layers[z]:
            color = COLORS["support" if "support" in kind else "bridge" if "bridge" in kind else "model"]
            draw.line([(ox + (p[0] - 68) * 3.7, oy + (177 - p[1]) * 3.7) for p in (a, b)], fill=color, width=2)
    draw.text((22, 645), "Reference slice preview. Select and validate your own printer preset before printing.", font=small, fill="#444444")
    canvas.save(OUT / (name + "-toolpaths.png"))
report = {"status": "reference_toolpaths_inspected", "parts_and_plate": data,
          "scope": "Actual extrusion bounds, layer coverage, four key springs, six screen leaves, four cover snaps and two upper screen supports checked for toolpath presence. Selected layers rendered for visual review. Support removal, printed fit, snap force and fatigue need a prototype; this is not machine-ready G-code."}
(ASSETS / "stl/toolpath-check.json").write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps(report, indent=2))