#!/usr/bin/env python import importlib.resources from tempfile import NamedTemporaryFile, TemporaryDirectory from pathlib import Path from quart import Quart, request, Response, send_file, abort from . import protoboard as pb from . import protoserve_data from ..utils import MM, Inch def extract_importlib(package): root = TemporaryDirectory() stack = [(importlib.resources.files(package), Path(root.name))] while stack: res, out = stack.pop() for item in res.iterdir(): item_out = out / item.name if item.is_file(): item_out.write_bytes(item.read_bytes()) else: assert item.is_dir() item_out.mkdir() stack.push((item, item_out)) return root static_folder = extract_importlib(protoserve_data) app = Quart(__name__, static_folder=static_folder.name) @app.route('/') async def index(): return await app.send_static_file('protoserve.html') def deserialize(obj, unit): pitch_x = float(obj.get('pitch_x', 1.27)) pitch_y = float(obj.get('pitch_y', 1.27)) clearance = float(obj.get('clearance', 0.2)) mil = lambda x: x/1000 if unit == Inch else x match obj['type']: case 'layout': if not obj.get('children'): return pb.EmptyProtoArea() proportions = [float(child['layout_prop']) for child in obj['children']] content = [deserialize(child, unit) for child in obj['children']] return pb.PropLayout(content, obj['direction'], proportions) case 'twoside': top, bottom = obj['children'] return pb.TwoSideLayout(deserialize(top, unit), deserialize(bottom, unit)) case 'placeholder': return pb.EmptyProtoArea() case 'smd': match obj['pad_shape']: case 'rect': pad = pb.SMDPad.rect(0, 0, pitch_x-clearance, pitch_y-clearance, paste=False, unit=unit) case 'circle': pad = pb.SMDPad.circle(0, 0, min(pitch_x, pitch_y)-clearance, paste=False, unit=unit) return pb.PatternProtoArea(pitch_x, pitch_y, obj=pad, unit=unit) case 'tht': hole_dia = mil(float(obj['hole_dia'])) match obj['plating']: case 'plated': oneside, plated = False, True case 'nonplated': oneside, plated = False, False case 'singleside': oneside, plated = True, False match obj['pad_shape']: case 'rect': pad = pb.THTPad.rect(0, 0, hole_dia, pitch_x-clearance, pitch_y-clearance, paste=False, plated=plated, unit=unit) case 'circle': pad = pb.THTPad.circle(0, 0, hole_dia, min(pitch_x, pitch_y)-clearance, paste=False, plated=plated, unit=unit) case 'obround': pad = pb.THTPad.obround(0, 0, hole_dia, pitch_x-clearance, pitch_y-clearance, paste=False, plated=plated, unit=unit) if oneside: pad.pad_bottom = None return pb.PatternProtoArea(pitch_x, pitch_y, obj=pad, unit=unit) case 'manhattan': return pb.PatternProtoArea(pitch_x, pitch_y, obj=pb.ManhattanPads(pitch_x, pitch_y, clearance, unit=unit), unit=unit) case 'powered': pitch = mil(float(obj.get('pitch', 2.54))) hole_dia = mil(float(obj['hole_dia'])) via_drill = mil(float(obj['via_hole_dia'])) via_dia = mil(float(obj['via_dia'])) trace_width = mil(float(obj['trace_width'])) # Force 1mm margin to avoid shorts when adjacent to planes such as that one in the RF THT proto. return pb.PatternProtoArea(pitch, pitch, pb.PoweredProto(pitch, hole_dia, clearance, via_size=via_drill, power_pad_dia=via_dia, trace_width=trace_width, unit=unit), margin=unit(1.0, MM), unit=unit) case 'flower': pitch = mil(float(obj.get('pitch', 2.54))) hole_dia = mil(float(obj['hole_dia'])) pattern_dia = mil(float(obj['pattern_dia'])) return pb.PatternProtoArea(2*pitch, 2*pitch, pb.THTFlowerProto(pitch, hole_dia, pattern_dia, unit=unit), unit=unit) case 'spiky': return pb.PatternProtoArea(2.54, 2.54, pb.SpikyProto(), unit=unit) case 'alio': pitch = mil(float(obj.get('pitch', 2.54))) drill = mil(float(obj.get('hole_dia', 0.9))) clearance = mil(float(obj.get('clearance', 0.3))) link_pad_width = mil(float(obj.get('link_pad_width', 1.1))) link_trace_width = mil(float(obj.get('link_trace_width', 0.5))) via_size = mil(float(obj.get('via_hole_dia', 0.4))) return pb.PatternProtoArea(pitch, pitch, pb.AlioCell( pitch=pitch, drill=drill, clearance=clearance, link_pad_width=link_pad_width, link_trace_width=link_trace_width, via_size=via_size ), margin=unit(1.5, MM), unit=unit) case 'rf': pitch = float(obj.get('pitch', 2.54)) hole_dia = float(obj['hole_dia']) via_dia = float(obj['via_dia']) via_drill = float(obj['via_hole_dia']) return pb.PatternProtoArea(pitch, pitch, pb.RFGroundProto(pitch, hole_dia, clearance, via_dia, via_drill, unit=MM), unit=MM) def to_board(obj): unit = Inch if obj.get('units' == 'us') else MM w = float(obj.get('width', unit(100, MM))) h = float(obj.get('height', unit(80, MM))) corner_radius = float(obj.get('round_corners', {}).get('radius', unit(1.5, MM))) holes = obj.get('mounting_holes', {}) mounting_hole_dia = float(holes.get('diameter', unit(3.2, MM))) mounting_hole_offset = float(holes.get('offset', unit(5, MM))) if obj.get('children'): try: content = deserialize(obj['children'][0], unit) except ValueError: return abort(400) else: content = [pb.EmptyProtoArea()] return pb.ProtoBoard(w, h, content, corner_radius=corner_radius, mounting_hole_dia=mounting_hole_dia, mounting_hole_offset=mounting_hole_offset, unit=unit) @app.route('/preview.svg', methods=['POST']) async def preview(): obj = await request.get_json() board = to_board(obj) return Response(str(board.pretty_svg()), mimetype='image/svg+xml') @app.route('/gerbers.zip', methods=['POST']) async def gerbers(): obj = await request.get_json() board = to_board(obj) with NamedTemporaryFile(suffix='.zip') as f: f = Path(f.name) board.layer_stack().save_to_zipfile(f) return Response(f.read_bytes(), mimetype='image/svg+xml') if __name__ == '__main__': app.run()