Skip to content

Add detailed logging and clarify manual vs automated specifications #39

Add detailed logging and clarify manual vs automated specifications

Add detailed logging and clarify manual vs automated specifications #39

Workflow file for this run

name: OpenSCAD Render and Export
on:
push:
paths:
- 'LifeTrac-v25/mechanical_design/**/*.scad'
- '.github/workflows/openscad-render.yml'
pull_request:
paths:
- 'LifeTrac-v25/mechanical_design/**/*.scad'
workflow_dispatch:
jobs:
render:
runs-on: ubuntu-latest
name: Generate Renders and Outputs
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install OpenSCAD
run: |
sudo apt-get update
sudo apt-get install -y openscad xvfb x11-utils
openscad --version
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x16 &
XVFB_PID=$!
echo "XVFB_PID=$XVFB_PID" >> $GITHUB_ENV
# Wait for Xvfb to be ready (max 10 seconds)
for i in {1..10}; do
if xdpyinfo -display :99 >/dev/null 2>&1; then
echo "Xvfb is ready on display :99"
break
fi
if [ $i -eq 10 ]; then
echo "Timeout waiting for Xvfb to start"
exit 1
fi
sleep 1
done
echo "DISPLAY=:99" >> $GITHUB_ENV
- name: Create output directories
run: |
cd LifeTrac-v25/mechanical_design
mkdir -p output/{dxf,renders,animations}
- name: Generate assembly.png and cnclayout.svg
run: |
cd LifeTrac-v25/mechanical_design
# Generate assembly.png (1024x768 matching CEB-Press example)
echo "Generating assembly.png..."
openscad -o assembly.png \
--camera=3000,3000,2000,0,0,500 \
--imgsize=1024,768 \
--projection=p \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
# Generate cnclayout.svg (2D projection for CNC cutting)
echo "Generating cnclayout.svg..."
openscad -o cnclayout.svg \
--projection=o \
--viewall \
cnclayout.scad
echo "✓ assembly.png and cnclayout.svg generated successfully"
ls -lh assembly.png cnclayout.svg
- name: Render preview images
run: |
cd LifeTrac-v25/mechanical_design
# Main assembly view
echo "Rendering main assembly..."
openscad -o output/renders/lifetrac_v25_main.png \
--camera=3000,3000,2000,0,0,500 \
--imgsize=1920,1080 \
--projection=p \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
# Front view
echo "Rendering front view..."
openscad -o output/renders/lifetrac_v25_front.png \
--camera=0,3000,1000,0,0,500 \
--imgsize=1920,1080 \
--projection=p \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
# Side view
echo "Rendering side view..."
openscad -o output/renders/lifetrac_v25_side.png \
--camera=3000,0,1000,0,0,500 \
--imgsize=1920,1080 \
--projection=p \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
# Top view
echo "Rendering top view..."
openscad -o output/renders/lifetrac_v25_top.png \
--camera=0,0,3000,0,0,500 \
--imgsize=1920,1080 \
--projection=o \
--colorscheme=Nature \
openscad/lifetrac_v25.scad
echo "Preview images rendered successfully"
- name: Render module examples
run: |
cd LifeTrac-v25/mechanical_design
# Render each module's example
for module in modules/*.scad; do
filename=$(basename "$module" .scad)
echo "Rendering $filename examples..."
openscad -o "output/renders/${filename}_examples.png" \
--camera=500,500,500,0,0,0 \
--imgsize=1600,900 \
--projection=p \
--colorscheme=Nature \
"$module"
done
- name: Generate animation frames
run: |
cd LifeTrac-v25/mechanical_design
echo "Generating animation frames..."
mkdir -p output/animations/frames
# Generate 36 frames for animation (every 10 degrees)
for i in {0..35}; do
t=$(awk "BEGIN {print $i/36}")
printf "Frame %d (t=%.4f)\n" $i $t
openscad -o "output/animations/frames/frame_$(printf %03d $i).png" \
--camera=3000,3000,2000,0,0,500 \
--imgsize=1280,720 \
--projection=p \
--colorscheme=Nature \
-D "\$t=$t" \
openscad/lifetrac_v25.scad
done
echo "Animation frames generated"
- name: Create animation GIF
run: |
sudo apt-get install -y imagemagick
cd LifeTrac-v25/mechanical_design/output/animations
echo "Creating animation GIF of arm movement..."
convert -delay 10 -loop 0 frames/frame_*.png lifetrac_v25_animation.gif
# Copy to parent directory for easy access
cp lifetrac_v25_animation.gif ../../lifetrac_v25_animation.gif
echo "✓ Animation GIF created successfully"
ls -lh ../../lifetrac_v25_animation.gif
- name: Generate summary report
run: |
cd LifeTrac-v25/mechanical_design
cat > output/RENDER_REPORT.md << 'EOF'
# OpenSCAD Render Report
**Generated:** $(date)
**Commit:** ${{ github.sha }}
**Branch:** ${{ github.ref_name }}
## Files Generated
### Preview Renders
- Main assembly view
- Front view
- Side view
- Top view
- Module examples (5 modules)
### Animations
- 36 animation frames (1280x720 PNG)
- Animated GIF showing arm movement (lifetrac_v25_animation.gif)
### CNC Files
- assembly.png (3D render of complete assembly)
- cnclayout.svg (2D layout for CNC cutting)
## Next Steps
1. Review rendered images in the artifacts
2. View lifetrac_v25_animation.gif to see arm movement
3. Use cnclayout.svg for CNC plasma cutting
4. Use animation frames to create videos
5. Export DXF files for individual parts using export_all_cnc_parts.sh
6. Check the Structural Analysis workflow for design validation results
---
Generated by GitHub Actions
EOF
- name: Upload render artifacts
uses: actions/upload-artifact@v4
with:
name: openscad-renders
path: |
LifeTrac-v25/mechanical_design/output/renders/
LifeTrac-v25/mechanical_design/output/RENDER_REPORT.md
retention-days: 90
- name: Upload animation artifacts
uses: actions/upload-artifact@v4
with:
name: openscad-animations
path: LifeTrac-v25/mechanical_design/output/animations/
retention-days: 90
- name: Extract specifications and update README
run: |
cd LifeTrac-v25
echo "Extracting specifications from OpenSCAD file..."
# Create Python script to extract specs
cat > /tmp/extract_specs.py << 'PYTHON_EOF'
import re
import sys
# Read the OpenSCAD file
try:
with open('mechanical_design/openscad/lifetrac_v25.scad', 'r') as f:
content = f.read()
except FileNotFoundError:
print("ERROR: OpenSCAD file not found", file=sys.stderr)
sys.exit(1)
# Extract key dimensions with error handling
def extract_value(pattern, content, name):
try:
match = re.search(pattern, content)
if match:
return float(match.group(1))
print(f"WARNING: Could not find {name} in OpenSCAD file", file=sys.stderr)
return None
except (ValueError, AttributeError) as e:
print(f"ERROR: Failed to extract {name}: {e}", file=sys.stderr)
return None
MACHINE_WIDTH = extract_value(r'MACHINE_WIDTH\s*=\s*(\d+)', content, 'MACHINE_WIDTH')
MACHINE_LENGTH = extract_value(r'MACHINE_LENGTH\s*=\s*(\d+)', content, 'MACHINE_LENGTH')
MACHINE_HEIGHT = extract_value(r'MACHINE_HEIGHT\s*=\s*(\d+)', content, 'MACHINE_HEIGHT')
WHEEL_DIAMETER = extract_value(r'WHEEL_DIAMETER\s*=\s*(\d+)', content, 'WHEEL_DIAMETER')
GROUND_CLEARANCE = extract_value(r'GROUND_CLEARANCE\s*=\s*(\d+)', content, 'GROUND_CLEARANCE')
HYDRAULIC_PRESSURE_PSI = extract_value(r'HYDRAULIC_PRESSURE_PSI\s*=\s*(\d+)', content, 'HYDRAULIC_PRESSURE_PSI')
LIFT_CYLINDER_BORE = extract_value(r'LIFT_CYLINDER_BORE\s*=\s*([\d.]+)', content, 'LIFT_CYLINDER_BORE')
# Validate that all required values were found
required_values = {
'MACHINE_WIDTH': MACHINE_WIDTH,
'MACHINE_LENGTH': MACHINE_LENGTH,
'MACHINE_HEIGHT': MACHINE_HEIGHT,
'WHEEL_DIAMETER': WHEEL_DIAMETER,
'GROUND_CLEARANCE': GROUND_CLEARANCE,
'HYDRAULIC_PRESSURE_PSI': HYDRAULIC_PRESSURE_PSI,
'LIFT_CYLINDER_BORE': LIFT_CYLINDER_BORE
}
missing_values = [k for k, v in required_values.items() if v is None]
if missing_values:
print(f"ERROR: Missing required values: {', '.join(missing_values)}", file=sys.stderr)
print("The following patterns failed to match:", file=sys.stderr)
for val in missing_values:
print(f" - {val}", file=sys.stderr)
print("Skipping README update to avoid breaking existing content", file=sys.stderr)
print("If OpenSCAD file structure changed, update the regex patterns in this workflow", file=sys.stderr)
sys.exit(0) # Exit gracefully without updating
# Conservative lift capacity estimate based on typical loader geometry
# NOTE: This is a simplified estimate. The actual lift capacity calculation in the
# OpenSCAD file uses complex lever arm geometry and moment calculations which would
# require additional extraction logic. For now, we use a conservative estimate.
# If design changes significantly, this value should be reviewed and updated.
ESTIMATED_LIFT_CAPACITY_KG = 1200
ESTIMATED_LIFT_CAPACITY_LBS = ESTIMATED_LIFT_CAPACITY_KG * 2.205
# Read current README
with open('README.md', 'r') as f:
readme = f.read()
# Build the specifications table
specs_table = f"""| Specification | Value (mm) | Value (inches) | Value (meters) |
|--------------|------------|----------------|----------------|
| **Overall Width** | {int(MACHINE_WIDTH):,} mm | {MACHINE_WIDTH/25.4:.1f}" | {MACHINE_WIDTH/1000:.2f} m |
| **Overall Length** | {int(MACHINE_LENGTH):,} mm | {MACHINE_LENGTH/25.4:.1f}" | {MACHINE_LENGTH/1000:.2f} m |
| **Height to Frame Top** | {int(MACHINE_HEIGHT):,} mm | {MACHINE_HEIGHT/25.4:.1f}" | {MACHINE_HEIGHT/1000:.2f} m |
| **Ground Clearance** | {int(GROUND_CLEARANCE)} mm | {GROUND_CLEARANCE/25.4:.1f}" | {GROUND_CLEARANCE/1000:.2f} m |
| **Wheel Diameter** | {int(WHEEL_DIAMETER)} mm | {WHEEL_DIAMETER/25.4:.1f}" | {WHEEL_DIAMETER/1000:.2f} m |"""
perf_table = f"""| Specification | Value |
|--------------|-------|
| **Hydraulic System Pressure** | {int(HYDRAULIC_PRESSURE_PSI):,} PSI |
| **Estimated Lift Capacity** | ~{int(ESTIMATED_LIFT_CAPACITY_KG):,} kg (~{int(ESTIMATED_LIFT_CAPACITY_LBS):,} lbs) |
| **Drive Configuration** | All-wheel drive (4 hydraulic motors) |
| **Cylinder Bore (Lift)** | {LIFT_CYLINDER_BORE} mm ({LIFT_CYLINDER_BORE/25.4:.1f}") |"""
# Update the specs in README using regex with validation
original_readme = readme
# Update technical specifications table
updated_readme = re.sub(
r'(\| Specification \| Value \(mm\) \| Value \(inches\) \| Value \(meters\) \|.*?\n)(\| \*\*Wheel Diameter\*\* \|[^\n]+)',
specs_table,
readme,
count=1,
flags=re.DOTALL
)
if updated_readme == readme:
print("WARNING: Technical specifications table pattern not found, trying alternative pattern", file=sys.stderr)
# Try simpler pattern that looks for the table header
pattern_start = readme.find('### Technical Specifications')
if pattern_start != -1:
# Find the end of the table
table_start = readme.find('| Specification | Value (mm)', pattern_start)
if table_start != -1:
# Find next section header or blank line after table
table_end = readme.find('\n\n###', table_start)
if table_end == -1:
table_end = readme.find('\n\n| Specification | Value |', table_start)
if table_end != -1:
# Extract table and replace
old_table = readme[table_start:readme.find('\n', readme.rfind('|', table_start, table_end))+1]
updated_readme = readme.replace(old_table, specs_table + '\n', 1)
readme = updated_readme
# Update performance specifications table
updated_readme = re.sub(
r'(### Performance Specifications\s*\n\n\| Specification \| Value \|.*?\n)(\| \*\*Cylinder Bore \(Lift\)\*\* \|[^\n]+)',
f'### Performance Specifications\n\n{perf_table}',
readme,
count=1,
flags=re.DOTALL
)
if updated_readme == readme:
print("WARNING: Performance specifications table pattern not found", file=sys.stderr)
else:
readme = updated_readme
# Only write if changes were made
if readme != original_readme:
try:
with open('README.md', 'w') as f:
f.write(readme)
print("✓ README specifications updated successfully")
except IOError as e:
print(f"ERROR: Failed to write README: {e}", file=sys.stderr)
sys.exit(1)
else:
print("INFO: No changes needed to README specifications")
print(f" Width: {int(MACHINE_WIDTH)} mm")
print(f" Length: {int(MACHINE_LENGTH)} mm")
print(f" Height: {int(MACHINE_HEIGHT)} mm")
print(f" Lift Capacity: ~{int(ESTIMATED_LIFT_CAPACITY_KG)} kg")
PYTHON_EOF
python3 /tmp/extract_specs.py
echo "README specifications updated"
- name: Commit and push rendered outputs
run: |
cd LifeTrac-v25
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
# Add the generated files (mechanical_design images and updated README)
git add mechanical_design/assembly.png mechanical_design/cnclayout.svg mechanical_design/lifetrac_v25_animation.gif README.md
# Check if there are changes
if ! git diff --staged --quiet; then
git commit -m "Update rendered outputs and specifications [skip ci]"
git push --force-with-lease origin HEAD || {
echo "INFO: git push failed. This is expected on PRs from forks."
echo "The files are available as workflow artifacts."
}
else
echo "No changes to rendered output files or README"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Cleanup Xvfb
if: always()
run: |
if [ -n "$XVFB_PID" ]; then
echo "Stopping Xvfb process (PID: $XVFB_PID)"
# Try graceful shutdown first
kill -TERM $XVFB_PID 2>/dev/null || true
# Wait up to 5 seconds for process to terminate
for i in {1..5}; do
if ! kill -0 $XVFB_PID 2>/dev/null; then
echo "Xvfb stopped gracefully"
break
fi
sleep 1
done
# Force kill if still running
kill -KILL $XVFB_PID 2>/dev/null || true
echo "Xvfb force stopped"
fi
- name: Comment on PR with results
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const comment = `## OpenSCAD Render Results 🎨
✅ Rendering completed successfully!
### Generated Outputs
- 📸 Preview renders (main, front, side, top views)
- 🎬 Animation frames (36 frames)
- 🎞️ Animated GIF of arm movement (lifetrac_v25_animation.gif)
- 🔧 Module examples
- 🖼️ Assembly image and CNC layout (assembly.png, cnclayout.svg)
### Artifacts
Download the generated files from the workflow artifacts:
- \`openscad-renders\` - Preview images
- \`openscad-animations\` - Animation frames and GIF
### Committed Files
The following files have been committed to the repository:
- \`assembly.png\` - 3D assembly render
- \`cnclayout.svg\` - 2D CNC cutting layout
- \`lifetrac_v25_animation.gif\` - Animation showing arm movement
_Note: Check the **OpenSCAD Structural Analysis** workflow for design validation and structural analysis results._
**Workflow Run:** [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});