From bfb87fbee8f8e9394e4ecdb46837709c434118a8 Mon Sep 17 00:00:00 2001 From: Arnaud Morin Date: Sat, 5 Sep 2026 14:47:42 +0200 Subject: [PATCH] Add flight controller Signed-off-by: Arnaud Morin --- AGENT.md | 2 ++ README.md | 12 +++++--- build.sh | 2 +- fc.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ frame.py | 34 +++++++++++++++-------- 5 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 fc.py diff --git a/AGENT.md b/AGENT.md index 791a4cc..c914a58 100644 --- a/AGENT.md +++ b/AGENT.md @@ -115,6 +115,8 @@ from the stack — don't hand-patch them. .venv/bin/python3 spar.py .venv/bin/python3 camera_mount.py # printed accessory, not a frame body .venv/bin/python3 standoff.py # turned hardware, in the assembly +.venv/bin/python3 lollipop.py # printed antenna holder +.venv/bin/python3 fc.py # bought electronics, an envelope only .venv/bin/python3 frame.py # assembly, BOM, exports, renders ``` diff --git a/README.md b/README.md index 2abd220..1ba402e 100644 --- a/README.md +++ b/README.md @@ -76,13 +76,17 @@ trust the run over this copy of it. | spar | 4 | 2 mm | carbon fibre | 1.19 | 4.76 | | camera_mount | 1 | printed | TPU | 7.87 | 7.87 | | standoff | 4 | Ø3.5 mm | aluminium | 0.39 | 1.56 | -| lollipop | 1 | printed | TPU | 1.15 | 1.15 | -| **frame** | **24** | | | | **33.05** | +| lollipop | 1 | printed | TPU | 2.12 | 2.12 | +| fc | 1 | bought | electronics | 12.09 | 12.09 | +| **frame** | **25** | | | | **46.11** | -Carbon at 1.55 g/cm³, TPU at 1.21, aluminium at 2.70. +Carbon at 1.55 g/cm³, TPU at 1.21, aluminium at 2.70. The `fc` is not a part +you make — it is the envelope of a flight controller and 4-in-1 ESC stack, +modelled as a solid block of FR4 at 1.85 g/cm³ so the all-up figure is honest. +Swap in your own stack's real mass if you know it. **Not counted, and not modelled:** screws, motors, props, camera, VTX, -receiver, flight controller. The standoffs are the only hardware in the model. +receiver. The standoffs and the FC stack are the hardware the model carries. ### Hardware you supply diff --git a/build.sh b/build.sh index 64450fc..c2f7349 100755 --- a/build.sh +++ b/build.sh @@ -27,7 +27,7 @@ run() { uv run --no-sync python "$@"; } # Each part asserts it came out as one solid at import time, so a part that # broke stops the build here rather than quietly exporting a bad STL. -for part in plate arm motor_base spar camera_mount standoff lollipop; do +for part in plate arm motor_base spar camera_mount standoff lollipop fc; do run "$part.py" done diff --git a/fc.py b/fc.py new file mode 100644 index 0000000..d9484c7 --- /dev/null +++ b/fc.py @@ -0,0 +1,83 @@ +# @author Arnaud Morin +# SPDX-License-Identifier: Apache-2.0 +# + +import cadquery as cq + +# A flight controller is not a material, it is a stack: two boards, their +# spacers, and everything soldered to them. This models only the envelope, as +# one solid, so the density is FR4's -- bare PCB -- and the dense parts (FETs, +# inductors, connectors) stand in for the air between the boards. It lands the +# block at about 12 g, which is what a real 25.5 mm FC plus 4-in-1 ESC weighs. +PCB_DENSITY = 1.85e-3 # g/mm3, FR4 + + +def fc(): + + box = (cq.Workplane("XY") + .box(33, 33, 8, centered=(True, True, True)) + .edges('Z') + .fillet(5) + ) + + # Remove extra for standoff + cut = (cq.Workplane("XY") + .pushPoints([ + (25.5/2, 25.5/2), + (25.5/2, -25.5/2), + (-25.5/2, 25.5/2), + (-25.5/2, -25.5/2), + ]) + .circle(7) + .extrude(5) + .translate((0,0,1.6/2)) + ) + box = box.cut(cut).cut(cut.mirror("XY")) + + # Standoff + sta = (cq.Workplane("XY") + .pushPoints([ + (25.5/2, 25.5/2), + (25.5/2, -25.5/2), + (-25.5/2, 25.5/2), + (-25.5/2, -25.5/2), + ]) + .circle(2.5) + .extrude(5, both=True) + .translate((0,0,-2)) + ) + box = box.union(sta) + + # holes in standoff + cut = (cq.Workplane("XY") + .pushPoints([ + (25.5/2, 25.5/2), + (25.5/2, -25.5/2), + (-25.5/2, 25.5/2), + (-25.5/2, -25.5/2), + ]) + .circle(1.2) + .extrude(10, both=True) + ) + box = box.cut(cut) + + # Make Z 0 + box = box.translate((0,0,7)) + return box + + +result = fc() + +assert result.solids().size() == 1, "fc is not one solid" + +if "show_object" not in globals(): # running outside CQ-editor + def show_object(*args, **kwargs): + pass + +show_object(result) + + +if __name__ == "__main__": + + print("fc %.2f g" % (result.val().Volume() * PCB_DENSITY)) + diff --git a/frame.py b/frame.py index b031aa8..7d465a9 100644 --- a/frame.py +++ b/frame.py @@ -16,6 +16,7 @@ from camera_mount import result as CAMERA_MOUNT import standoff as SO from standoff import result as STANDOFF from lollipop import result as LOLLIPOP +from fc import result as FC, PCB_DENSITY OUT = "build" @@ -28,10 +29,9 @@ COLORS = { "spar": cq.Color(0.22, 0.62, 0.35), # green "camera_mount": cq.Color(0.95, 0.78, 0.09), # yellow TPU "standoff": cq.Color(0.75, 0.76, 0.78), # aluminium - "lollipop": cq.Color(0.95, 0.78, 0.09), # yellow TPU, as the mount + "lollipop": cq.Color(0.95, 0.78, 0.09), # yellow TPU + "fc": cq.Color(0.42, 0.28, 0.62), # PCB purple "prop": cq.Color(0.55, 0.55, 0.60), # opaque: a ring hides - # nothing, so there is no - # call for transparency } DENSITY = { @@ -40,6 +40,7 @@ DENSITY = { "camera_mount": TPU_DENSITY, "standoff": SO.ALU_DENSITY, "lollipop": TPU_DENSITY, + "fc": PCB_DENSITY, } # --- propellers ------------------------------------------------------------- @@ -77,6 +78,7 @@ PARTS = [ ("camera_mount", CAMERA_MOUNT, P.THICKNESS, "solid"), ("standoff", STANDOFF, SO.OD, "solid"), ("lollipop", LOLLIPOP, 3.5, "solid"), + ("fc", FC, 11.0, "solid"), ] @@ -90,9 +92,9 @@ def _loc(x, y, z, rot_z, flip=0.0): def pieces(): - """[(name, tag, solid, Location)] -- the 19 bodies, straight off the - placement helpers: 2 plates, 4 arms, 8 motor bases, 4 spars, 1 camera - mount.""" + """[(name, tag, solid, Location)] -- the 25 bodies, straight off the + placement helpers: 2 plates, 4 arms, 8 motor bases, 4 spars, 4 standoffs, + 1 camera mount, 1 antenna holder, 1 fc stack.""" out = [] for i, (x, y, z, rz) in enumerate(P.plate_placements()): out.append(("plate", "plate_%s" % ("bottom", "top")[i], @@ -121,6 +123,14 @@ def pieces(): # height that clears both plates. out.append(("lollipop", "lollipop_0", LOLLIPOP, _loc(0.0, -22.0, 7.0, 90.0))) + # fc: the stack stands on the bottom plate's top face, over the 25.5 square + # the plate is drilled for. fc.py puts its own origin at the underside of + # its posts, so that face is the whole placement. The 45 deg is not + # cosmetic: fc.py sets its posts on the diagonals at (+-12.75, +-12.75), + # while the plate drills the same 25.5 square with its holes on the axes at + # 25.5/sqrt(2) = 18.03. Same radius, turned an eighth of a turn, so without + # this the screws miss. + out.append(("fc", "fc_0", FC, _loc(0.0, 0.0, P.THICKNESS, 45.0))) return out @@ -175,11 +185,13 @@ def bom(ps): print(" %-12s %4d %6.1f %9.3f %9.3f" % (name, qty, thick, each, each * qty)) print(" %-12s %4d %6s %9s %9.3f" % ("FRAME", len(ps), "", "", total)) - print(" carbon at %.2f g/cm3, TPU camera_mount at %.2f g/cm3," - " aluminium standoffs at %.2f g/cm3;" - % (P.CF_DENSITY * 1e3, TPU_DENSITY * 1e3, SO.ALU_DENSITY * 1e3)) - print(" the standoffs are the only hardware counted -- no screws, motors" - " or electronics") + print(" carbon at %.2f g/cm3, TPU parts at %.2f g/cm3, aluminium" + " standoffs at %.2f g/cm3," % (P.CF_DENSITY * 1e3, TPU_DENSITY * 1e3, + SO.ALU_DENSITY * 1e3)) + print(" fc as a solid block of FR4 at %.2f g/cm3 -- an envelope, not a" + " board" % (PCB_DENSITY * 1e3,)) + print(" hardware counted is the standoffs and the fc stack -- no screws," + " motors, props or receiver") return total