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 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
```

View File

@@ -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

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
# 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

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
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