Open Source Blueberry Production 3D CAD
- FreeCAD Python Script for OSE-POT-25L Hydroponic Pot
- Open FreeCAD, go to View > Panels > Python Console
- Then: exec(open("/path/to/this/file.py").read())
- Or use Macro > Macros > Create and paste this code
import FreeCAD as App import Part import math
- Create new document
if App.ActiveDocument:
App.closeDocument(App.ActiveDocument.Name)
doc = App.newDocument("OSE_POT_25L")
- ============================================
- DIMENSIONS (all in mm)
- ============================================
TOP_OUTER_DIA = 370 BOTTOM_OUTER_DIA = 320 HEIGHT = 280 WALL_THICKNESS = 3
TOP_INNER_DIA = TOP_OUTER_DIA - 2 * WALL_THICKNESS BOTTOM_INNER_DIA = BOTTOM_OUTER_DIA - 2 * WALL_THICKNESS
- Reservoir and false bottom
FALSE_BOTTOM_HEIGHT = 45 # from base interior RESERVOIR_VOLUME = 2 # liters
- Drip ring
DRIP_RING_DIA = 340 # inner channel diameter DRIP_CHANNEL_SIZE = 8 # 8x8mm channel NUM_DRIP_OUTLETS = 8 DRIP_OUTLET_DIA = 2
- Air slots
NUM_AIR_SLOTS = 52 AIR_SLOT_WIDTH = 3 AIR_SLOT_HEIGHT = 25 AIR_SLOT_START_HEIGHT = 60 # from base
- Drain
DRAIN_DIA = 15 DRAIN_BOSS_DIA = 30 DRAIN_BOSS_HEIGHT = 5
- ============================================
- HELPER FUNCTIONS
- ============================================
def make_tapered_cylinder(bottom_radius, top_radius, height, pos_z=0):
"""Create a tapered cylinder (frustum) using a cone""" # FreeCAD cone: Radius1 is bottom, Radius2 is top cone = Part.makeCone(bottom_radius, top_radius, height) cone.translate(App.Vector(0, 0, pos_z)) return cone
def make_air_slot(slot_width, slot_height, wall_thickness, radius, angle, start_z):
"""Create a single air slot as a box positioned on the pot wall""" # Create slot box slot = Part.makeBox(wall_thickness + 2, slot_width, slot_height) # Position at radius slot.translate(App.Vector(radius - 1, -slot_width/2, start_z)) # Rotate around Z axis slot.rotate(App.Vector(0, 0, 0), App.Vector(0, 0, 1), angle) return slot
- ============================================
- CREATE MAIN POT BODY
- ============================================
print("Creating main pot body...")
- Outer shell (tapered)
outer_shell = make_tapered_cylinder(
BOTTOM_OUTER_DIA / 2, TOP_OUTER_DIA / 2, HEIGHT
)
- Inner cavity (tapered) - subtract to make hollow
inner_cavity = make_tapered_cylinder(
BOTTOM_INNER_DIA / 2, TOP_INNER_DIA / 2, HEIGHT - WALL_THICKNESS, # Leave bottom thickness WALL_THICKNESS # Start above base
)
- Create hollow pot
pot_body = outer_shell.cut(inner_cavity)
- ============================================
- CREATE FALSE BOTTOM PLATFORM
- ============================================
print("Creating false bottom...")
- Calculate inner diameter at false bottom height
- Linear interpolation for tapered pot
ratio = FALSE_BOTTOM_HEIGHT / HEIGHT false_bottom_inner_dia = BOTTOM_INNER_DIA + (TOP_INNER_DIA - BOTTOM_INNER_DIA) * ratio
- False bottom disk with drainage holes
false_bottom = Part.makeCylinder(
false_bottom_inner_dia / 2 - 2, # Slightly smaller for fit 3, # 3mm thick platform App.Vector(0, 0, WALL_THICKNESS + FALSE_BOTTOM_HEIGHT - 3)
)
- Add drainage holes to false bottom (grid pattern)
drainage_holes = [] hole_spacing = 25 for x in range(-int(false_bottom_inner_dia/3), int(false_bottom_inner_dia/3), hole_spacing):
for y in range(-int(false_bottom_inner_dia/3), int(false_bottom_inner_dia/3), hole_spacing):
if x*x + y*y < (false_bottom_inner_dia/2 - 20)**2: # Within radius
hole = Part.makeCylinder(
4, # 8mm diameter holes
5,
App.Vector(x, y, WALL_THICKNESS + FALSE_BOTTOM_HEIGHT - 4)
)
drainage_holes.append(hole)
- Cut drainage holes from false bottom
for hole in drainage_holes:
false_bottom = false_bottom.cut(hole)
- ============================================
- CREATE AIR SLOTS
- ============================================
print("Creating air slots...")
air_slots_compound = [] angle_step = 360 / NUM_AIR_SLOTS
for i in range(NUM_AIR_SLOTS):
angle = i * angle_step
# Calculate radius at slot height (accounting for taper)
slot_mid_height = AIR_SLOT_START_HEIGHT + AIR_SLOT_HEIGHT / 2
ratio = slot_mid_height / HEIGHT
slot_radius = (BOTTOM_OUTER_DIA / 2) + ((TOP_OUTER_DIA - BOTTOM_OUTER_DIA) / 2) * ratio
slot = make_air_slot(
AIR_SLOT_WIDTH,
AIR_SLOT_HEIGHT,
WALL_THICKNESS,
slot_radius,
angle,
AIR_SLOT_START_HEIGHT
)
air_slots_compound.append(slot)
- Cut air slots from pot body
for slot in air_slots_compound:
pot_body = pot_body.cut(slot)
- ============================================
- CREATE DRAIN FITTING
- ============================================
print("Creating drain fitting...")
- Drain boss (reinforced area around drain)
drain_boss = Part.makeCylinder(
DRAIN_BOSS_DIA / 2, DRAIN_BOSS_HEIGHT, App.Vector(0, 0, 0)
)
- Drain hole through base
drain_hole = Part.makeCylinder(
DRAIN_DIA / 2, WALL_THICKNESS + DRAIN_BOSS_HEIGHT + 2, App.Vector(0, 0, -1)
)
- Add boss to pot, then cut hole
pot_body = pot_body.fuse(drain_boss) pot_body = pot_body.cut(drain_hole)
- ============================================
- CREATE DRIP RING (simplified representation)
- ============================================
print("Creating drip ring...")
- Drip ring channel (torus at top of pot)
drip_ring_z = HEIGHT - DRIP_CHANNEL_SIZE - 5
- Create torus for drip ring channel
drip_ring = Part.makeTorus(
DRIP_RING_DIA / 2, # Major radius DRIP_CHANNEL_SIZE / 2, # Minor radius (channel size) App.Vector(0, 0, drip_ring_z)
)
- Create drip outlets
drip_outlets = [] for i in range(NUM_DRIP_OUTLETS):
angle = i * (360 / NUM_DRIP_OUTLETS)
angle_rad = math.radians(angle)
x = (DRIP_RING_DIA / 2) * math.cos(angle_rad)
y = (DRIP_RING_DIA / 2) * math.sin(angle_rad)
# Outlet tube pointing down
outlet = Part.makeCylinder(
DRIP_OUTLET_DIA / 2,
15, # Outlet length
App.Vector(x, y, drip_ring_z - 15)
)
drip_outlets.append(outlet)
- Fuse drip outlets to ring
for outlet in drip_outlets:
drip_ring = drip_ring.fuse(outlet)
- ============================================
- CREATE DOCUMENT OBJECTS
- ============================================
print("Adding parts to document...")
- Main pot body
pot_obj = doc.addObject("Part::Feature", "Pot_Body") pot_obj.Shape = pot_body pot_obj.ViewObject.ShapeColor = (0.3, 0.3, 0.35) # Dark gray (biochar color) pot_obj.ViewObject.Transparency = 0
- False bottom (separate part for clarity)
fb_obj = doc.addObject("Part::Feature", "False_Bottom") fb_obj.Shape = false_bottom fb_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.45) # Slightly lighter fb_obj.ViewObject.Transparency = 0
- Drip ring (highlight color for visibility)
dr_obj = doc.addObject("Part::Feature", "Drip_Ring") dr_obj.Shape = drip_ring dr_obj.ViewObject.ShapeColor = (0.2, 0.5, 0.7) # Blue tint for visibility dr_obj.ViewObject.Transparency = 30
- ============================================
- CREATE CROSS-SECTION VIEW HELPER
- ============================================
print("Creating cross-section plane...")
- Create a cutting plane for cross-section view
cutting_box = Part.makeBox(
TOP_OUTER_DIA, TOP_OUTER_DIA / 2 + 50, HEIGHT + 20, App.Vector(-TOP_OUTER_DIA/2, 0, -10)
)
- Create cross-section version of pot
pot_section = pot_body.cut(cutting_box) fb_section = false_bottom.cut(cutting_box) dr_section = drip_ring.cut(cutting_box)
- Add cross-section objects (initially hidden)
pot_section_obj = doc.addObject("Part::Feature", "Pot_CrossSection") pot_section_obj.Shape = pot_section pot_section_obj.ViewObject.ShapeColor = (0.3, 0.3, 0.35) pot_section_obj.ViewObject.Visibility = False
fb_section_obj = doc.addObject("Part::Feature", "FalseBottom_CrossSection") fb_section_obj.Shape = fb_section fb_section_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.45) fb_section_obj.ViewObject.Visibility = False
dr_section_obj = doc.addObject("Part::Feature", "DripRing_CrossSection") dr_section_obj.Shape = dr_section dr_section_obj.ViewObject.ShapeColor = (0.2, 0.5, 0.7) dr_section_obj.ViewObject.Visibility = False
- ============================================
- RECOMPUTE AND SET VIEW
- ============================================
doc.recompute()
print("") print("=" * 50) print("OSE-POT-25L Model Created Successfully!") print("=" * 50) print("") print("OBJECTS IN MODEL:") print(" - Pot_Body: Main pot with air slots and drain") print(" - False_Bottom: Perforated platform") print(" - Drip_Ring: Irrigation ring with 8 outlets") print("") print("CROSS-SECTION VIEWS (hidden by default):") print(" - Pot_CrossSection") print(" - FalseBottom_CrossSection") print(" - DripRing_CrossSection") print("") print("TO VIEW CROSS-SECTION:") print(" 1. Hide: Pot_Body, False_Bottom, Drip_Ring") print(" 2. Show: *_CrossSection objects") print("") print("TO EXPORT FOR RENDERING:") print(" File > Export > Select format (STEP, STL, etc.)") print("") print("KEY DIMENSIONS:") print(f" Top diameter: {TOP_OUTER_DIA} mm") print(f" Bottom diameter: {BOTTOM_OUTER_DIA} mm") print(f" Height: {HEIGHT} mm") print(f" Wall thickness: {WALL_THICKNESS} mm") print(f" Air slots: {NUM_AIR_SLOTS} x {AIR_SLOT_WIDTH}mm") print(f" Drip outlets: {NUM_DRIP_OUTLETS} x Ø{DRIP_OUTLET_DIA}mm") print(f" Drain: Ø{DRAIN_DIA}mm") print("=" * 50)
- Fit view to show all
if App.GuiUp:
import FreeCADGui
FreeCADGui.activeDocument().activeView().viewIsometric()
FreeCADGui.SendMsgToActiveView("ViewFit")