Open Source Blueberry Production 3D CAD

From Open Source Ecology
Revision as of 02:35, 19 December 2025 by Marcin (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search


HintLightbulb.png Hint: This is a freecad macro for generating v0.1 of pot with recirculation and irrigation built in.


  1. FreeCAD Python Script for OSE-POT-25L Hydroponic Pot
  2. Open FreeCAD, go to View > Panels > Python Console
  3. Then: exec(open("/path/to/this/file.py").read())
  4. Or use Macro > Macros > Create and paste this code

import FreeCAD as App import Part import math

  1. Create new document

if App.ActiveDocument:

   App.closeDocument(App.ActiveDocument.Name)

doc = App.newDocument("OSE_POT_25L")

  1. ============================================
  2. DIMENSIONS (all in mm)
  3. ============================================

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

  1. Reservoir and false bottom

FALSE_BOTTOM_HEIGHT = 45 # from base interior RESERVOIR_VOLUME = 2 # liters

  1. Drip ring

DRIP_RING_DIA = 340 # inner channel diameter DRIP_CHANNEL_SIZE = 8 # 8x8mm channel NUM_DRIP_OUTLETS = 8 DRIP_OUTLET_DIA = 2

  1. Air slots

NUM_AIR_SLOTS = 52 AIR_SLOT_WIDTH = 3 AIR_SLOT_HEIGHT = 25 AIR_SLOT_START_HEIGHT = 60 # from base

  1. Drain

DRAIN_DIA = 15 DRAIN_BOSS_DIA = 30 DRAIN_BOSS_HEIGHT = 5

  1. ============================================
  2. HELPER FUNCTIONS
  3. ============================================

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
  1. ============================================
  2. CREATE MAIN POT BODY
  3. ============================================

print("Creating main pot body...")

  1. Outer shell (tapered)

outer_shell = make_tapered_cylinder(

   BOTTOM_OUTER_DIA / 2,
   TOP_OUTER_DIA / 2,
   HEIGHT

)

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

)

  1. Create hollow pot

pot_body = outer_shell.cut(inner_cavity)

  1. ============================================
  2. CREATE FALSE BOTTOM PLATFORM
  3. ============================================

print("Creating false bottom...")

  1. Calculate inner diameter at false bottom height
  2. Linear interpolation for tapered pot

ratio = FALSE_BOTTOM_HEIGHT / HEIGHT false_bottom_inner_dia = BOTTOM_INNER_DIA + (TOP_INNER_DIA - BOTTOM_INNER_DIA) * ratio

  1. 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)

)

  1. 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)
  1. Cut drainage holes from false bottom

for hole in drainage_holes:

   false_bottom = false_bottom.cut(hole)
  1. ============================================
  2. CREATE AIR SLOTS
  3. ============================================

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)
  1. Cut air slots from pot body

for slot in air_slots_compound:

   pot_body = pot_body.cut(slot)
  1. ============================================
  2. CREATE DRAIN FITTING
  3. ============================================

print("Creating drain fitting...")

  1. Drain boss (reinforced area around drain)

drain_boss = Part.makeCylinder(

   DRAIN_BOSS_DIA / 2,
   DRAIN_BOSS_HEIGHT,
   App.Vector(0, 0, 0)

)

  1. Drain hole through base

drain_hole = Part.makeCylinder(

   DRAIN_DIA / 2,
   WALL_THICKNESS + DRAIN_BOSS_HEIGHT + 2,
   App.Vector(0, 0, -1)

)

  1. Add boss to pot, then cut hole

pot_body = pot_body.fuse(drain_boss) pot_body = pot_body.cut(drain_hole)

  1. ============================================
  2. CREATE DRIP RING (simplified representation)
  3. ============================================

print("Creating drip ring...")

  1. Drip ring channel (torus at top of pot)

drip_ring_z = HEIGHT - DRIP_CHANNEL_SIZE - 5

  1. 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)

)

  1. 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)
  1. Fuse drip outlets to ring

for outlet in drip_outlets:

   drip_ring = drip_ring.fuse(outlet)
  1. ============================================
  2. CREATE DOCUMENT OBJECTS
  3. ============================================

print("Adding parts to document...")

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

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

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

  1. ============================================
  2. CREATE CROSS-SECTION VIEW HELPER
  3. ============================================

print("Creating cross-section plane...")

  1. 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)

)

  1. 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)

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

  1. ============================================
  2. RECOMPUTE AND SET VIEW
  3. ============================================

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)

  1. Fit view to show all

if App.GuiUp:

   import FreeCADGui
   FreeCADGui.activeDocument().activeView().viewIsometric()
   FreeCADGui.SendMsgToActiveView("ViewFit")