From 702374dbe2b3b4128fa49f272472bd90914af3bd Mon Sep 17 00:00:00 2001 From: Seth Fischer Date: Sat, 24 Feb 2024 15:25:42 +1300 Subject: [PATCH] feat: command to generate rpi hat outline --- src/osr_mechanical/console/application.py | 36 ++++++++++ src/osr_mechanical/console/utilities.py | 6 ++ src/osr_mechanical/pcb/__init__.py | 4 ++ src/osr_mechanical/pcb/rpi_hat.py | 71 +++++++++++++++++++ tests/osr_mechanical/console/___init__.py | 1 + .../osr_mechanical/console/test_utilities.py | 13 ++++ 6 files changed, 131 insertions(+) create mode 100644 src/osr_mechanical/console/utilities.py create mode 100644 src/osr_mechanical/pcb/__init__.py create mode 100644 src/osr_mechanical/pcb/rpi_hat.py create mode 100644 tests/osr_mechanical/console/___init__.py create mode 100644 tests/osr_mechanical/console/test_utilities.py diff --git a/src/osr_mechanical/console/application.py b/src/osr_mechanical/console/application.py index 3cb6497..bffdd16 100644 --- a/src/osr_mechanical/console/application.py +++ b/src/osr_mechanical/console/application.py @@ -1,6 +1,8 @@ """Rover console command.""" +import importlib import logging +import tempfile from argparse import ArgumentParser, Namespace from base64 import b64encode from datetime import datetime @@ -8,6 +10,7 @@ from pathlib import Path from sys import stdout +from cadquery import exporters as cq_exporters from jinja2 import Environment, PackageLoader, select_autoescape from osr_mechanical import __version__ @@ -21,6 +24,7 @@ from osr_mechanical.console.dxf import dxf_import_export from osr_mechanical.console.exporters import ExportPNG from osr_mechanical.console.release import ReleaseBuilder +from osr_mechanical.console.utilities import snake_to_camel_case logging.basicConfig(encoding="utf-8", level=logging.INFO) logger = logging.getLogger("osr_mechanical.console") @@ -105,6 +109,27 @@ def export_bom(args: Namespace) -> None: exit(EX_OK) +def export_pcb_outline(args: Namespace) -> None: + """Export PCB outlines as DXF.""" + module_name = ( + f"osr_mechanical.pcb.{args.board}.{snake_to_camel_case(args.board)}Board" + ) + + module_name, class_name = module_name.rsplit(".", 1) + module = importlib.import_module(module_name) + container = getattr(module, class_name) + + pcb = container().cq_object + + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_file = Path(tmp_dir) / "tmp.dxf" + cq_exporters.export(pcb, str(tmp_file), "DXF") + + stdout.write(tmp_file.read_text()) + + exit(EX_OK) + + def build_parser() -> ArgumentParser: """Parse arguments.""" parser = ArgumentParser(prog="console", description="Rover console command.") @@ -207,6 +232,17 @@ def build_parser() -> ArgumentParser: ) parser_bom.set_defaults(func=export_bom) + parser_pcb_outline = subparsers.add_parser( + "pcb-outline", help="generate printed circuit board outlines" + ) + parser_pcb_outline.add_argument( + "--board", + required=True, + type=str, + help="board for which to generate outline", + ) + parser_pcb_outline.set_defaults(func=export_pcb_outline) + return parser diff --git a/src/osr_mechanical/console/utilities.py b/src/osr_mechanical/console/utilities.py new file mode 100644 index 0000000..28c20dc --- /dev/null +++ b/src/osr_mechanical/console/utilities.py @@ -0,0 +1,6 @@ +"""Utilities.""" + + +def snake_to_camel_case(snake_str: str) -> str: + """Convert a snake case string to camel case.""" + return "".join(x.capitalize() for x in snake_str.lower().split("_")) diff --git a/src/osr_mechanical/pcb/__init__.py b/src/osr_mechanical/pcb/__init__.py new file mode 100644 index 0000000..c3ec836 --- /dev/null +++ b/src/osr_mechanical/pcb/__init__.py @@ -0,0 +1,4 @@ +"""PCB mechanical designs. + +Mechanical designs for printed circuit boards such as board outlines. +""" diff --git a/src/osr_mechanical/pcb/rpi_hat.py b/src/osr_mechanical/pcb/rpi_hat.py new file mode 100644 index 0000000..44f5701 --- /dev/null +++ b/src/osr_mechanical/pcb/rpi_hat.py @@ -0,0 +1,71 @@ +"""Raspberry Pi HAT mechanical design.""" + +import cadquery as cq + +from osr_common.cq_containers import CqWorkplaneContainer + + +class RpiHatBoard(CqWorkplaneContainer): + """Raspberry Pi HAT+ board outline. + + See https://datasheets.raspberrypi.com/hat/hat-plus-specification.pdf + """ + + def __init__(self) -> None: + """Initialise RPi HAT+ board.""" + self.width = 65 + self.height = 56.5 + self.thickness = 1.5 + self.corner_radius = 3.5 + + self.csi_slot_width = 17 + self.csi_slot_height = 2 + + self.dsi_slot_height = 17 + self.dsi_slot_width = 5 + + self.mounting_hole_radius = 2.7 / 2 + self.mounting_hole_between_centers_x = 58 + self.mounting_hole_between_centers_y = 49 + + dsi_slot_loc_x = -self.width / 2 + self.dsi_slot_width / 2 + dsi_slot_offset_y = -0.5 + self.dsi_slot_loc = (dsi_slot_loc_x, dsi_slot_offset_y) + + self._cq_object = self._make() + + def outline(self) -> cq.Sketch: + """Create RPi HAT+ board outline.""" + sketch = ( + cq.Sketch() + .rect(self.width, self.height) + .vertices() + .tag("major_outline") + .reset() + .push([(-(self.width / 2) + 50, -(self.height / 2) + 11.5)]) + .slot(self.csi_slot_width, self.csi_slot_height, angle=90, mode="s") + .push([self.dsi_slot_loc]) + .rect(self.dsi_slot_width, self.dsi_slot_height, mode="s") + .reset() + .vertices("(>Y[2]) or (< cq.Workplane: + """Create RPi HAT+ board.""" + result = cq.Workplane("XY").placeSketch(self.outline()).extrude(self.thickness) + + return result diff --git a/tests/osr_mechanical/console/___init__.py b/tests/osr_mechanical/console/___init__.py new file mode 100644 index 0000000..de9cc1c --- /dev/null +++ b/tests/osr_mechanical/console/___init__.py @@ -0,0 +1 @@ +"""Test console app utilities.""" diff --git a/tests/osr_mechanical/console/test_utilities.py b/tests/osr_mechanical/console/test_utilities.py new file mode 100644 index 0000000..adc9b38 --- /dev/null +++ b/tests/osr_mechanical/console/test_utilities.py @@ -0,0 +1,13 @@ +"""Test console utilities.""" + +from osr_mechanical.console.utilities import snake_to_camel_case + + +class TestSnakeToCamelCase: + """Test snake to camel case utility.""" + + def test_lower_snake_case(self) -> None: + """Test valid snake case.""" + result = snake_to_camel_case("valid_snake_case") + + assert "ValidSnakeCase" == result