Add flight controller

Signed-off-by: Arnaud Morin <arnaud.gfpv@mailops.fr>
This commit is contained in:
Arnaud Morin
2026-09-05 14:47:42 +02:00
parent 61025489a4
commit bfb87fbee8
5 changed files with 117 additions and 16 deletions

View File

@@ -115,6 +115,8 @@ from the stack — don't hand-patch them.
.venv/bin/python3 spar.py .venv/bin/python3 spar.py
.venv/bin/python3 camera_mount.py # printed accessory, not a frame body .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 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 .venv/bin/python3 frame.py # assembly, BOM, exports, renders
``` ```

View File

@@ -76,13 +76,17 @@ trust the run over this copy of it.
| spar | 4 | 2 mm | carbon fibre | 1.19 | 4.76 | | spar | 4 | 2 mm | carbon fibre | 1.19 | 4.76 |
| camera_mount | 1 | printed | TPU | 7.87 | 7.87 | | camera_mount | 1 | printed | TPU | 7.87 | 7.87 |
| standoff | 4 | Ø3.5 mm | aluminium | 0.39 | 1.56 | | standoff | 4 | Ø3.5 mm | aluminium | 0.39 | 1.56 |
| lollipop | 1 | printed | TPU | 1.15 | 1.15 | | lollipop | 1 | printed | TPU | 2.12 | 2.12 |
| **frame** | **24** | | | | **33.05** | | 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, **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 ### Hardware you supply

View File

@@ -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 # 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. # 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" run "$part.py"
done done

83
fc.py Normal file
View File

@@ -0,0 +1,83 @@
# @author Arnaud Morin <arnaud.gfpv@mailops.fr>
# 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))

View File

@@ -16,6 +16,7 @@ from camera_mount import result as CAMERA_MOUNT
import standoff as SO import standoff as SO
from standoff import result as STANDOFF from standoff import result as STANDOFF
from lollipop import result as LOLLIPOP from lollipop import result as LOLLIPOP
from fc import result as FC, PCB_DENSITY
OUT = "build" OUT = "build"
@@ -28,10 +29,9 @@ COLORS = {
"spar": cq.Color(0.22, 0.62, 0.35), # green "spar": cq.Color(0.22, 0.62, 0.35), # green
"camera_mount": cq.Color(0.95, 0.78, 0.09), # yellow TPU "camera_mount": cq.Color(0.95, 0.78, 0.09), # yellow TPU
"standoff": cq.Color(0.75, 0.76, 0.78), # aluminium "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 "prop": cq.Color(0.55, 0.55, 0.60), # opaque: a ring hides
# nothing, so there is no
# call for transparency
} }
DENSITY = { DENSITY = {
@@ -40,6 +40,7 @@ DENSITY = {
"camera_mount": TPU_DENSITY, "camera_mount": TPU_DENSITY,
"standoff": SO.ALU_DENSITY, "standoff": SO.ALU_DENSITY,
"lollipop": TPU_DENSITY, "lollipop": TPU_DENSITY,
"fc": PCB_DENSITY,
} }
# --- propellers ------------------------------------------------------------- # --- propellers -------------------------------------------------------------
@@ -77,6 +78,7 @@ PARTS = [
("camera_mount", CAMERA_MOUNT, P.THICKNESS, "solid"), ("camera_mount", CAMERA_MOUNT, P.THICKNESS, "solid"),
("standoff", STANDOFF, SO.OD, "solid"), ("standoff", STANDOFF, SO.OD, "solid"),
("lollipop", LOLLIPOP, 3.5, "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(): def pieces():
"""[(name, tag, solid, Location)] -- the 19 bodies, straight off the """[(name, tag, solid, Location)] -- the 25 bodies, straight off the
placement helpers: 2 plates, 4 arms, 8 motor bases, 4 spars, 1 camera placement helpers: 2 plates, 4 arms, 8 motor bases, 4 spars, 4 standoffs,
mount.""" 1 camera mount, 1 antenna holder, 1 fc stack."""
out = [] out = []
for i, (x, y, z, rz) in enumerate(P.plate_placements()): for i, (x, y, z, rz) in enumerate(P.plate_placements()):
out.append(("plate", "plate_%s" % ("bottom", "top")[i], out.append(("plate", "plate_%s" % ("bottom", "top")[i],
@@ -121,6 +123,14 @@ def pieces():
# height that clears both plates. # height that clears both plates.
out.append(("lollipop", "lollipop_0", LOLLIPOP, out.append(("lollipop", "lollipop_0", LOLLIPOP,
_loc(0.0, -22.0, 7.0, 90.0))) _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 return out
@@ -175,11 +185,13 @@ def bom(ps):
print(" %-12s %4d %6.1f %9.3f %9.3f" print(" %-12s %4d %6.1f %9.3f %9.3f"
% (name, qty, thick, each, each * qty)) % (name, qty, thick, each, each * qty))
print(" %-12s %4d %6s %9s %9.3f" % ("FRAME", len(ps), "", "", total)) print(" %-12s %4d %6s %9s %9.3f" % ("FRAME", len(ps), "", "", total))
print(" carbon at %.2f g/cm3, TPU camera_mount at %.2f g/cm3," print(" carbon at %.2f g/cm3, TPU parts at %.2f g/cm3, aluminium"
" aluminium standoffs at %.2f g/cm3;" " standoffs at %.2f g/cm3," % (P.CF_DENSITY * 1e3, TPU_DENSITY * 1e3,
% (P.CF_DENSITY * 1e3, TPU_DENSITY * 1e3, SO.ALU_DENSITY * 1e3)) SO.ALU_DENSITY * 1e3))
print(" the standoffs are the only hardware counted -- no screws, motors" print(" fc as a solid block of FR4 at %.2f g/cm3 -- an envelope, not a"
" or electronics") " board" % (PCB_DENSITY * 1e3,))
print(" hardware counted is the standoffs and the fc stack -- no screws,"
" motors, props or receiver")
return total return total