It’s been a while
It’s like that sometimes. Moved house again… anyway, without further ado…
Roughly modeled the pumps for the large format cell in FreeCAD (a while ago…)

Improving hardware documentation with FreeCAD, ForgeJo/Codeberg, Markdown…
Went to FOSDEM 2026 and was very impressed by OpenFlexure’s documentation, and Julian Stirling’s presentation on it (we also gave a talk there!). A key takeaway from it that resonated with me was his “no photos in the documentation” point—since any minor change requires taking new photos of the entire build process, a laborious and time-intensive process. He noted that even slightly out-of-date photos with, say, different colors for a part, were enough to throw off people following the instructions and reduce their confidence. They use a CI/CD process to render the steps in their documentation, so that everything is built automatically. Julian made the GitBuilding tool which OpenFlexure and our FBRC project both use to render hardware build docs with markdown. They use OpenSCAD primarily, while I use FreeCAD, so the implementation of rendering will be different.
A good time to remember this XKCD…

I think FBRC is at a point where it makes sense to implement CI/CD in our docs, I am struggling to keep them up-to-date with the pace of development and the fact that the photo-taking process takes a very long time.
Also, unfortunately, the FreeCAD Render workbench (not built-in) is looking for a new maintainer, so I will not be relying on it (plus it would have heavy dependencies in a CI/CD process, and is probably overkill/not worth the trouble. KISS!) The TechDraw workbench is built-in, however, so I am more confident to use it and build a pipeline around it.
I also saw this great MangoJelly tutorial video on making nice exploded assemblies with TechDraw. I thought about making FreeCAD take screenshots of different build steps but it was a bit janky and pixelated/hard to adapt for web devices. TechDraw can output SVGs which of course scale much better than screenshots and look much crisper.
I’ll point out there are also tools in development like https://osh-autodoc.org but, I am trying to minimize dependencies, and this would conflict with our use of GitBuilding for the actual docs. It would be hard to make it work for our system, it is more for IKEA-style build instructions.
With TechDraw I can quickly make SVGs like this and it’s easy to automate:

I think I can color in different components as well, and I plan to make TechDraw pages for each step of the assembly process.
CI/CD implementation in ForgeJo/Codeberg
This is beyond my comfort zone. I am not a code monkey, and most info out there is for GitHub (and I unsurprisingly am not a fan of Microsoft/GAFAM). Stephen Hawes and the Opulo team have done some great work on CI/CD for their pick-and-place machine which is modeled in FreeCAD, and provides a great starting point, but it is way more involved than what FBRC needs now. And it’s for GitHub. (also of note, their AutoBOM project).
Until now I have avoided FreeCAD Python scripting and headless FreeCAD, so this is my first time doing either. The documentation on the FreeCAD wiki is a bit “meh” so I just started messing around. This is what I have that works now:
# run script from terminal with `<path-to-FreeCAD> --console export-stl.py`
import Mesh, Import
# Endplate (Hole)
FreeCAD.openDocument('endplate-hole.FCStd')
__objs__ = []
__objs__.append(FreeCAD.getDocument("endplate_hole").getObject("Body004"))
if hasattr(Mesh, "exportOptions"):
options = Mesh.exportOptions(u"../exports/Endplate-Hole.stl")
Mesh.export(__objs__, u"../exports/Endplate-Hole.stl", options)
else:
Mesh.export(__objs__, u"../exports/Endplate-Hole.stl")
del __objs__
App.closeDocument(App.ActiveDocument.Name)
# Endplate (Pin)
FreeCAD.openDocument('endplate-pin.FCStd')
__objs__ = []
__objs__.append(FreeCAD.getDocument("endplate_pin").getObject("Body004"))
if hasattr(Mesh, "exportOptions"):
options = Mesh.exportOptions(u"../exports/Endplate-Pin.stl")
Mesh.export(__objs__, u"../exports/Endplate-Pin.stl", options)
else:
Mesh.export(__objs__, u"../exports/Endplate-Pin.stl")
del __objs__
App.closeDocument(App.ActiveDocument.Name)
# Membrane Frame
FreeCAD.openDocument('membrane-frame.FCStd')
__objs__ = []
__objs__.append(FreeCAD.getDocument("membrane_frame").getObject("Body007"))
if hasattr(Mesh, "exportOptions"):
options = Mesh.exportOptions(u"../exports/Membrane-Frame.stl")
Mesh.export(__objs__, u"../exports/Membrane-Frame.stl", options)
else:
Mesh.export(__objs__, u"../exports/Membrane-Frame.stl")
del __objs__
App.closeDocument(App.ActiveDocument.Name)
# Flow Frame
FreeCAD.openDocument('flow-frame.FCStd')
__objs__ = []
__objs__.append(FreeCAD.getDocument("flow_frame").getObject("Body"))
if hasattr(Mesh, "exportOptions"):
options = Mesh.exportOptions(u"../exports/Flow-Frame.stl")
Mesh.export(__objs__, u"../exports/Flow-Frame.stl", options)
else:
Mesh.export(__objs__, u"../exports/Flow-Frame.stl")
del __objs__
App.closeDocument(App.ActiveDocument.Name)
# Double Reservoir
try:
FreeCAD.openDocument('double-reservoir.FCStd')
except ModuleNotFoundError:
print("uh oh")
finally:
__objs__ = []
__objs__.append(FreeCAD.getDocument("double_reservoir").getObject("Connect"))
if hasattr(Mesh, "exportOptions"):
options = Mesh.exportOptions(u"../exports/Double-Reservoir.stl")
Mesh.export(__objs__, u"../exports/Double-Reservoir.stl", options)
else:
Mesh.export(__objs__, u"../exports/Double-Reservoir.stl")
del __objs__
App.closeDocument(App.ActiveDocument.Name)
# Jig
FreeCAD.openDocument('jig.FCStd')
__objs__ = []
__objs__.append(FreeCAD.getDocument("jig").getObject("Body"))
if hasattr(Mesh, "exportOptions"):
options = Mesh.exportOptions(u"../exports/Jig.stl")
Mesh.export(__objs__, u"../exports/Jig.stl", options)
else:
Mesh.export(__objs__, u"../exports/Jig.stl")
del __objs__
App.closeDocument(App.ActiveDocument.Name)
# Cell Assembly Tool
FreeCAD.openDocument('cell-assembly-tool.FCStd')
__objs__ = []
__objs__.append(FreeCAD.getDocument("cell_assembly_tool").getObject("Body005"))
if hasattr(Mesh, "exportOptions"):
options = Mesh.exportOptions(u"../exports/Cell-Assembly-Tool.stl")
Mesh.export(__objs__, u"../exports/Cell-Assembly-Tool.stl", options)
else:
Mesh.export(__objs__, u"../exports/Cell-Assembly-Tool.stl")
del __objs__
App.closeDocument(App.ActiveDocument.Name)
# Current Collector .STEP
FreeCAD.openDocument('outer-current-collector.FCStd')
__objs__ = []
__objs__.append(FreeCAD.getDocument("outer_current_collector").getObject("Body002"))
if hasattr(Import, "exportOptions"):
options = Import.exportOptions(u"../exports/Current-Collector.step")
Import.export(__objs__, u"../exports/Current-Collector.step", options)
else:
Import.export(__objs__, u"../exports/Current-Collector.step")
del __objs__
App.closeDocument(App.ActiveDocument.Name)
## Assembly
FreeCAD.openDocument('assembly.FCStd')
Gui.activeDocument().activeView().viewIsometric()
Gui.SendMsgToActiveView("ViewFit")
Gui.activeDocument().activeView().saveImage('../exports/assembly.webp',2400,1600,'Transparent')
exit()It automates the tedious process of selecting the correct bodies in FreeCAD and exporting them to STLs/STEPs each time there is a minor update, it can be run in CI/CD, and it should work with TechDraw so I can automate the rendering of multiple assembly steps (in time!).
To find the right commands, I turned on the Python console view in FreeCAD as I manually performed the exports. Then, with the code shown from the console, I copied the correct commands into the script and ran FreeCAD in console mode from the command line (note, Gui commands like screenshots don’t work in console mode!).
As far as doing this in Codeberg/ForgeJo, I have a test.yaml file draft that can build the GitBuilding documentation (no FreeCAD yet):
on:
workflow_dispatch:
jobs:
test:
runs-on: [codeberg-tiny, codeberg-tiny-lazy]
steps:
- name: checkout code
uses: actions/checkout@v4
- name: build docs
run: |
python3 -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install gitbuilding
cd docs
gitbuilding build-htmlAnyway, that’s a snapshot of what I’m working on. Also, this is all in the context of some changes I’m making to our dev kit described here.
Citation
@online{smith2026,
author = {Smith, Kirk Pollard},
title = {Improving Hardware Documentation with {FreeCAD,}
{ForgeJo/Codeberg,} {Markdown...} / {Lab} {Notebook} {Entry} \#6},
date = {2026-03-18},
url = {https://dualpower.supply/posts/lab-notebook-6/},
langid = {en}
}