Close
0%
0%

PLANTINATOR 2026 - Seedling Transplanting Machine

Currently creating a motion simulated CAD drawing with an LLM

Similar projects worth following
Winter project - design and build a seedling transplanting machine for the WEEDINATOR. Rather than using FreeCAD manually, I'm using FreCAD friendly Python macros created using an LLM. So far this seems to be between 10 and 20 times faster and it has all kinds of bonuses such as mathematically derived rotations and translations and checking for moving parts interfering with each other. The whole machine will be fairly complex - can the LLM handle the whole thing and what kind of subscription will i need?

The plan is to create a simulated FreeCAD model so that all the geometry can be checked before going ahead into the fabrication stage. Parts will be made and arranged by means of Python macros created with the help of an LLM. So far so good.

  • Mechanism Successfully Plants a Flower

    GOAT INDUSTRIES • 2 hours ago • 0 comments

    All the basic features of the mechanism are now complete. The machine now accepts a seedling into one of the carousel tubes and plants it accurately into the soil. The seedling produces a nice yellow flower.

  • Mechanism Comes Together

    GOAT INDUSTRIES • 2 days ago • 0 comments

    Nearly 3 weeks into the project and the main components for the system are now in place, with a second duck bill ready to put in position. There's also some kind of flap disc needed under the carousel tubes to quickly release the seedlings into the duck bill hopper. The single carousel should be able to feed both duck bill systems simultaneously, which makes the design simpler.

  • Animated Seedling Transplanter Assembly

    GOAT INDUSTRIES • 6 days ago • 0 comments

    The duck bill is now opened by a hydraulic master cylinder and slave, similar to a vehicle clutch. Cables would have been a bit too tightly curved. The pushrod on the master cylinder wont tolerate sideways forces, so it needs an inline lateral interface for it to work of the cam disc. The duck bills need a couple of springs to bring them back to the closed position. The axle between the two drive wheels had to be cut out and raised up via chain and sprockets (coloured green) to allow the main duck bill arm to move without putting a horrible kink in it. The LLM managed to get a flexible pipe between the master and slave, which is pretty cool.

    FreeCAD Macro: https://github.com/paddygoat/PLANTINATOR_2026/blob/main/CAD/Duck_bill_and_lugged_wheels_assembly/dual_lugged_wheels_sprockets_chain__duck_bill_106_bushed_cam_slider.FCMacro

  • Testing the Geometry of the Duck bill Mechanism

    GOAT INDUSTRIES • 09/25/2026 at 20:40 • 0 comments

    This mechanism was created and tuned with an LLM to primarily try and get the transplanting distance as close to 285mm as possible, it's currently 289.6 and make sure the duck bill, shown in blue, does not try and 'plough the earth' ie it's speed when it penetrates the soil is as close to zero relative to the soil in the left - right axis. Other than that, I was able to do further tuning to get the tip of the duck bill to trace an oval pathway shown in dashed blue and get that trace upright rather than slanting to one side.

    The whole of this diagram is a Macro and is basically created through simple maths, though far too extensive for any normal human to put together manually. Any of the components shown can be exported as .dxf etc for laser cutting or other formats for 3D printing.

    Compared to trying to create this mechanism by 'normal' CAD usage, I'd estimate using the LLM is now about 100 times faster than manually trying to tune the various arm lengths. For example, the LLM was able to move the various pivot points to get the duck bill tip pathway vertical and get the duck bill to match the soil speed in minutes rather than days.

    The code is now about 10,000 lines long, mostly occupied by a 8,000 line base 64 encoded chunk representing another imported file. Without this import, the code would be about 3,000 lines long. See the Github repo for the latest files, just import the .FCMacro files as a macro into FreeCAD and you can see and the animation as above. If you want to edit the file - use an LLM !!

  • Duck-bill Mechanism Update

    GOAT INDUSTRIES • 09/21/2026 at 11:01 • 0 comments

    Part of a planting machine this assembly was modeled with the help of an LLM by using Python macros. The Macro is mechanically reproducible in real life and real, accurate dimensions can be extracted from it by importing it into FreeCAD. All this is only 850 lines of code !!!!!!!

    The video below was produced from the macro by the LLM using Blender, which it installed on my PC in a Python environment. Took 12 hours to render 300 frames.

    # Standalone comparison: complete closed and 20-degree mechanism in side view.
    # Both poses share identical fixed pivot locations. All dimensions in mm.
    import FreeCAD as App
    import os
    
    # Root profile trimmed for at least 1 mm band clearance through 25 degrees.
    # 0 = outer hole; 1, 2, 3 move inward in 15 mm steps.
    # Selecting another hole also relocates the rear mount to retain a vertical start.
    CLEVIS_HOLE_INDEX = 0
    
    def build_pose(requested_angle):
        # FreeCAD macro: four-panel demi-cone; dimensions in mm.
        # Standalone: uses only built-in FreeCAD/Part and Python modules.
        # Dimensions refer to the theoretical sheet mid-surface.
        import math
        import os
        import FreeCAD as App
        import Part
        
        SLANT_LENGTH = 180.0
        RIM_CHORD = 50.0                 # straight chord, NOT circular arc length
        THICKNESS = 2.0
        SLOT_WIDTH = 0.3                 # nominal material removed before bending
        TIP_KEEP = 30.0
        RIM_KEEP = 20.0                 # measured along each bend from its rim corner
        SLOTS_PER_BEND = 3
        PANELS = 4
        FLANGE_WIDTH = 25.0             # horizontal distance normal to each straight rim edge
        FLANGE_THICKNESS = 2.0
        BRACKET_WIDTH = 20.0
        BRACKET_LENGTH = 60.0          # extended 10 mm below the lowest hole
        BRACKET_THICKNESS = 3.0
        BRACKET_TOP_RADIUS = 5.0
        BRACKET_HOLE_DIAMETER = 6.0
        BRACKET_HOLE_FROM_TOP = 10.0
        BRACKET_HOLE_SPACING = 15.0     # centre-to-centre downwards
        BRACKET_HOLE_COUNT = 3
        BRACKET_BELOW_FLANGE = 3.0
        MOUNT_SLOT_LENGTH = 20.2        # along Y, perpendicular to open-back line
        MOUNT_SLOT_WIDTH = 3.2          # along X
        MOUNT_SLOT_X = 63.5             # mirrored centres at +/- X
        MOUNT_SLOT_Y = 45.0             # from open-back line Y=0; estimated from screenshot
        # Blank means save beside this macro (fallback: FreeCAD macro directory).
        OUTPUT_DIR = ""
        # Illustrative female rod ends, not a manufacturer's dimensioned part.
        ROD_END_EYE_OD = 16.0
        ROD_END_HOUSING_WIDTH = 6.0
        ROD_END_BALL_WIDTH = 8.0
        ROD_END_BALL_RADIUS = 5.0
        ROD_END_EYE_TO_MOUTH = 25.0
        ROD_END_SHANK_OD = 9.0
        ROD_THREAD_DIAMETER = 6.0
        ROD_THREAD_ENGAGEMENT = 10.0
        ROD_END_SPACER = 1.0
        PIVOT_OFFSET = 20.0         # each half outward: 40 mm additional separation
        PIVOT_BOSS_RADIUS = 12.0    # 9 mm material beyond the 6 mm hole
        PLAIN_BAR_LENGTH = 150.0
        PLAIN_BAR_DIAMETER = 6.0
        CYL_BASE_Y=-201.0  # corrected cylinder: retain 7 mm initial extension
        CYL_BASE_X=63.5
        CYL_BASE_HEIGHT_OFFSET=0.0   # rear pin level with clevis pin at zero degrees
        CYL_STROKE=25.0              # retained operating envelope from previous model
        CYL_REAR_PIN_DIAMETER=10.0   # catalogue rear bore is 10; clevis pin stays 6
        RIGHT_TIE_SPACER=1.0         # standard tie-rod mounting restored; clevis uses other top hole
        OPENING_ANGLE_DEG = requested_angle         # original (+Y) half; driven half solved from fixed links
        
        L = SLANT_LENGTH
        C = RIM_CHORD
        if PANELS != 4 or SLOTS_PER_BEND < 1:
            raise ValueError("This macro requires four panels and at least one slot.")
        if not (0 < C < 2*L and 0 < TIP_KEEP < L and 0 < RIM_KEEP < L-TIP_KEEP and THICKNESS > 0 and SLOT_WIDTH > 0):
            raise ValueError("Invalid dimensions.")
        alpha = 2 * math.asin(C / (2*L))
        radius = C / (2*math.sin(math.pi/(2*PANELS)))
        if radius >= L:
            raise ValueError("Rim too large for the sloping length.")
        height = math.sqrt(L*L - radius*radius)
        # Equal slot lengths and equal internal bridges; both end margins stay intact.
        pitch_piece = (L - TIP_KEEP - RIM_KEEP) / (2*SLOTS_PER_BEND - 1)
        intervals = [(TIP_KEEP + 2*i*pitch_piece,
                      TIP_KEEP + (2*i+1)*pitch_piece) for i in range(SLOTS_PER_BEND)]
        angles = [math.pi/2 + (i-PANELS/2)*alpha for i in range(PANELS+1)]
        V = App.Vector
        origin = V(0,0,0)
     rim = [V(L*math.cos(a),...
    Read more »

  • Duck-bill Mechanism for Inserting Seedlings into the Ground

    GOAT INDUSTRIES • 09/19/2026 at 21:51 • 0 comments

    This device is called a Duck's Bill and moves down to the ground and releases the seedling enclosed within it at exactly the right moment. Some designs use a cable to do this and using a hydraulic actuator does seem like a bit of an over kill.
    To see the mechanism, just copy the following code into a python file and upload into FreeCAD as a macro. If you want to remove the second, 20 degrees overlay, upload the file to LLM and ask it: "Remove the 20 degrees overlay".

    # Standalone comparison: complete closed and 20-degree mechanism in side view.
    # Both poses share identical fixed pivot locations. All dimensions in mm.
    import FreeCAD as App
    import os
    
    # 0 = outer hole; 1, 2, 3 move inward in 15 mm steps.
    # Selecting another hole also relocates the rear mount to retain a vertical start.
    CLEVIS_HOLE_INDEX = 0
    
    def build_pose(requested_angle):
        # FreeCAD macro: four-panel demi-cone; dimensions in mm.
        # Standalone: uses only built-in FreeCAD/Part and Python modules.
        # Dimensions refer to the theoretical sheet mid-surface.
        import math
        import os
        import FreeCAD as App
        import Part
        
        SLANT_LENGTH = 180.0
        RIM_CHORD = 50.0                 # straight chord, NOT circular arc length
        THICKNESS = 2.0
        SLOT_WIDTH = 0.3                 # nominal material removed before bending
        TIP_KEEP = 30.0
        RIM_KEEP = 20.0                 # measured along each bend from its rim corner
        SLOTS_PER_BEND = 3
        PANELS = 4
        FLANGE_WIDTH = 25.0             # horizontal distance normal to each straight rim edge
        FLANGE_THICKNESS = 2.0
        BRACKET_WIDTH = 20.0
        BRACKET_LENGTH = 60.0          # extended 10 mm below the lowest hole
        BRACKET_THICKNESS = 3.0
        BRACKET_TOP_RADIUS = 5.0
        BRACKET_HOLE_DIAMETER = 6.0
        BRACKET_HOLE_FROM_TOP = 10.0
        BRACKET_HOLE_SPACING = 15.0     # centre-to-centre downwards
        BRACKET_HOLE_COUNT = 3
        BRACKET_BELOW_FLANGE = 3.0
        MOUNT_SLOT_LENGTH = 20.2        # along Y, perpendicular to open-back line
        MOUNT_SLOT_WIDTH = 3.2          # along X
        MOUNT_SLOT_X = 63.5             # mirrored centres at +/- X
        MOUNT_SLOT_Y = 45.0             # from open-back line Y=0; estimated from screenshot
        # Blank means save beside this macro (fallback: FreeCAD macro directory).
        OUTPUT_DIR = ""
        # Illustrative female rod ends, not a manufacturer's dimensioned part.
        ROD_END_EYE_OD = 16.0
        ROD_END_HOUSING_WIDTH = 6.0
        ROD_END_BALL_WIDTH = 8.0
        ROD_END_BALL_RADIUS = 5.0
        ROD_END_EYE_TO_MOUTH = 25.0
        ROD_END_SHANK_OD = 9.0
        ROD_THREAD_DIAMETER = 6.0
        ROD_THREAD_ENGAGEMENT = 10.0
        ROD_END_SPACER = 1.0
        PIVOT_OFFSET = 20.0         # each half outward: 40 mm additional separation
        PIVOT_BOSS_RADIUS = 12.0    # 9 mm material beyond the 6 mm hole
        PLAIN_BAR_LENGTH = 150.0
        PLAIN_BAR_DIAMETER = 6.0
        CYL_BASE_Y=-201.0  # corrected cylinder: retain 7 mm initial extension
        CYL_BASE_X=63.5
        CYL_BASE_HEIGHT_OFFSET=0.0   # rear pin level with clevis pin at zero degrees
        CYL_STROKE=25.0              # retained operating envelope from previous model
        CYL_REAR_PIN_DIAMETER=10.0   # catalogue rear bore is 10; clevis pin stays 6
        RIGHT_TIE_SPACER=1.0         # standard tie-rod mounting restored; clevis uses other top hole
        OPENING_ANGLE_DEG = requested_angle         # original (+Y) half; driven half solved from fixed links
        
        L = SLANT_LENGTH
        C = RIM_CHORD
        if PANELS != 4 or SLOTS_PER_BEND < 1:
            raise ValueError("This macro requires four panels and at least one slot.")
        if not (0 < C < 2*L and 0 < TIP_KEEP < L and 0 < RIM_KEEP < L-TIP_KEEP and THICKNESS > 0 and SLOT_WIDTH > 0):
            raise ValueError("Invalid dimensions.")
        alpha = 2 * math.asin(C / (2*L))
        radius = C / (2*math.sin(math.pi/(2*PANELS)))
        if radius >= L:
            raise ValueError("Rim too large for the sloping length.")
        height = math.sqrt(L*L - radius*radius)
        # Equal slot lengths and equal internal bridges; both end margins stay intact.
        pitch_piece = (L - TIP_KEEP - RIM_KEEP) / (2*SLOTS_PER_BEND - 1)
        intervals = [(TIP_KEEP + 2*i*pitch_piece,
                      TIP_KEEP + (2*i+1)*pitch_piece) for i in range(SLOTS_PER_BEND)]
        angles = [math.pi/2 + (i-PANELS/2)*alpha for i in range(PANELS+1)]
        V = App.Vector
        origin = V(0,0,0)
        rim = [V(L*math.cos(a), L*math.sin(a), 0) for a in angles]
        
        def face...
    Read more »

  • From a FreeCAD macro test to a dedicated machine simulator

    GOAT INDUSTRIES • 09/16/2026 at 10:41 • 0 comments

    The first PLANTINATOR log was a simple test: could a Python macro generate sprockets and a moving chain in FreeCAD without having to draw and position everything manually? That established a useful starting point. The model has now grown into a more detailed wheel and chain assembly, with a dedicated Qt5 application for inspecting the geometry and movement.

    The eventual objective is a transplanting machine. This simplified duckbill assembly illustrates the direction of the project:

    The immediate work is on the supporting drive mechanism and the tools for developing it. The current CAD code includes two lugged wheels on a shared shaft, three sprockets, and individual chain components. The wheels include stepped discs, rims, bosses, lug holes, chamfers, and simplified locking fasteners. The current sprocket combination is 28, 15, and 21 teeth, with a 58-pitch chain at 12.7 mm pitch—an extension of the earlier two-sprocket experiment.

    Much of the interest here is in how the machine is being designed. With an LLM such as GPT Astra helping to write and revise the Python, a mechanical change can start as an ordinary description: increase the wheel spacing, alter a lug dimension, or keep the wheel locked to the rotation of its driving sprocket. The resulting code expresses those requirements through parameters, geometry functions, and placement calculations.

    That is particularly useful when one change affects several parts. Moving the wheels further apart also affects the shared shaft. Changing a repeated lug feature should update every lug consistently. Encoding those relationships makes revisions repeatable and avoids having to select and reposition each affected object by hand. FreeCAD has its own parametric tools, of course; Python provides another practical way to manage the repetition and mathematical relationships in this assembly.

    The LLM still needs clear instructions and its output needs checking. Specifying which dimensions can change, which relationships must remain fixed, and which direction an offset refers to is far more useful than asking it to “improve the mechanism.” Small changes followed by visual inspection make errors easier to spot.

    The dedicated Qt5 viewer is intended to make that inspection loop quicker. Repeatedly opening FreeCAD, running the macro, and arranging the view adds friction when the immediate question is simply whether a change looks right through a complete rotation. The viewer puts playback and camera controls alongside the mechanism, so it is easier to concentrate on the moving geometry.

    It uses FreeCAD’s Part geometry kernel to build the solids and Pivy/Coin with OpenGL to display their tessellated surfaces. Holes and chamfers are therefore part of the geometry being shown. Once the scene is built, playback updates component positions and rotations rather than constructing every solid again for each frame.

    The controls include pause, reverse motion through signed speed, stepping by one chain pitch, and a position slider. There are standard camera views, orbit and zoom controls, plus close-ups for the lugs and chain. These should make it easier to examine awkward positions instead of judging the mechanism from one attractive angle.

    There is also a route back into the full FreeCAD environment. Running the supplied `FreeCAD_Simulator.FCMacro` creates native document objects from the bundled model. The document can then be saved as an `.FCStd` file and developed further, adding the supporting frame, bearing mounts, brackets, and other static parts around the mechanism.

    One detail to keep straight: the standalone viewer’s dimension loader reads numeric parameters from a file; it does not execute arbitrary geometry code. Accepted parameter changes need to be carried into the bundled source before generating the matching FreeCAD document.

    The next stage is to continue refining the geometry and checking its movement before committing to fabrication. The animation...

    Read more »

  • Testing Macro Drawings

    GOAT INDUSTRIES • 09/12/2026 at 14:04 • 0 comments

    First Macro drawing for FreeCad is just to check it can be done easily. It works. Here is the first Macro:

    import FreeCAD as App
    import Part
    import math
    try:
        from PySide import QtCore
    except ImportError:
        from PySide6 import QtCore
    
    doc_name = "Chain_Sprocket_13T_15T_Simulation"
    doc = App.newDocument(doc_name)
    
    # --- 1. System Parameters ---
    t1 = 13 # Left Sprocket Teeth
    t2 = 15 # Right Sprocket Teeth
    pitch = 9.525
    roller_diameter = 6.35
    bore_diameter = 10.0
    plate_thickness = 5.3
    num_links = 32 # Increased chain length to accommodate larger sprocket
    
    # Chain Link Specs
    inner_width = 5.72
    pin_diameter = 3.28
    link_plate_thickness = 1.3
    
    # Calculated Radii
    R1 = pitch / (2 * math.sin(math.pi / t1))
    R2 = pitch / (2 * math.sin(math.pi / t2))
    root1 = R1 - (roller_diameter / 2.0)
    root2 = R2 - (roller_diameter / 2.0)
    
    # --- 2. Iterative Solver for Exact Center Distance (C) ---
    # A closed chain requires a geometrically perfect loop length.
    L_target = num_links * pitch
    C = 85.0 # Initial guess
    for _ in range(50): # Newton-Raphson method for exact distance
        theta = math.asin((R2 - R1) / C)
        L_calc = 2 * C * math.cos(theta) + R1 * (math.pi - 2*theta) + R2 * (math.pi + 2*theta)
        C -= (L_calc - L_target) / (2 * math.cos(theta))
    
    # Final Path Geometry Variables
    theta = math.asin((R2 - R1) / C)
    L_str = C * math.cos(theta)
    L_arc1 = R1 * (math.pi - 2 * theta)
    L_arc2 = R2 * (math.pi + 2 * theta)
    L_total = L_arc1 + L_arc2 + 2 * L_str
    
    # Tangent Points defining the path layout
    T1top = App.Vector(-R1 * math.sin(theta), R1 * math.cos(theta), 0)
    T1bot = App.Vector(-R1 * math.sin(theta), -R1 * math.cos(theta), 0)
    T2top = App.Vector(C - R2 * math.sin(theta), R2 * math.cos(theta), 0)
    T2bot = App.Vector(C - R2 * math.sin(theta), -R2 * math.cos(theta), 0)
    
    # --- 3. Dynamic Sprocket Generator ---
    def create_sprocket(name, teeth, R_pitch, R_root):
        R_outer = R_pitch + 2.3 # Scaled OD
        base_cylinder = Part.makeCylinder(R_outer, plate_thickness)
        scale_y = 0.85 
        
        # Parameterized Wedge Cutter to adapt to different radii
        p_root = App.Vector(R_root, 0, 0)
        p_A = App.Vector(R_pitch - 1.6, 2.77 * scale_y, 0)
        p_B = App.Vector(R_pitch - 1.6, -2.77 * scale_y, 0)
        p_M_up = App.Vector(R_pitch + 1.75, 4.5 * scale_y, 0)
        p_M_low = App.Vector(R_pitch + 1.75, -4.5 * scale_y, 0)
        p_3 = App.Vector(R_pitch + 5.1, 5.5 * scale_y, 0)
        p_4 = App.Vector(R_pitch + 5.1, -5.5 * scale_y, 0)
    
        arc_root = Part.Arc(p_B, p_root, p_A).toShape()
        arc_up = Part.Arc(p_A, p_M_up, p_3).toShape()
        arc_low = Part.Arc(p_4, p_M_low, p_B).toShape()
        line_out = Part.LineSegment(p_3, p_4).toShape()
    
        wedge_wire = Part.Wire([arc_root, arc_up, line_out, arc_low])
        wedge_solid = Part.Face(wedge_wire).extrude(App.Vector(0, 0, plate_thickness + 2.0))
        wedge_solid.translate(App.Vector(0, 0, -1.0))
    
        sprocket = base_cylinder
        for i in range(teeth):
            cutter = wedge_solid.copy()
            cutter.rotate(App.Vector(0, 0, 0), App.Vector(0, 0, 1), i * (360.0 / teeth))
            sprocket = sprocket.cut(cutter)
            
        bore = Part.makeCylinder(bore_diameter / 2.0, plate_thickness + 2.0)
        bore.translate(App.Vector(0, 0, -1.0))
        
        obj = doc.addObject("Part::Feature", name)
        obj.Shape = sprocket.cut(bore)
        obj.ViewObject.ShapeColor = (0.2, 0.2, 0.2)
        return obj
    
    sprocket_1 = create_sprocket("Sprocket_13T", t1, R1, root1)
    sprocket_2 = create_sprocket("Sprocket_15T", t2, R2, root2)
    sprocket_2.Placement.Base = App.Vector(C, 0, 0)
    
    # --- 4. Chain Link Geometry ---
    def create_link_plate(thickness, z_offset):
        r = 4.0
        cyl1 = Part.makeCylinder(r, thickness, App.Vector(0,0,z_offset))
        cyl2 = Part.makeCylinder(r, thickness, App.Vector(pitch,0,z_offset))
        box = Part.makeBox(pitch, r*2, thickness, App.Vector(0, -r, z_offset))
        return cyl1.fuse(cyl2).fuse(box)
    
    inner_plate_z1 = plate_thickness/2 + inner_width/2
    inner_plate_z2 = plate_thickness/2 - inner_width/2 - link_plate_thickness
    p1 = create_link_plate(link_plate_thickness, inner_plate_z1)
    p2 = create_link_plate(link_plate_thickness, inner_plate_z2)
    r1 = Part.makeCylinder(roller_diameter/2, inner_width,...
    Read more »

View all 8 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates