...
 
Commits (14)
......@@ -12,9 +12,14 @@
docs/*.zip
# Ignore automatically built files
Makefile
builds/
docs/Output/
Makefile
*.stl.d
ninja.build
build.ninja
.ninja_deps
.ninja_log
# Ignore IDE files
.vscode/
......
......@@ -6,20 +6,20 @@ stages:
build:
stage: build
image: ubuntu:18.04
before_script:
- apt-get update -qq
- apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates make
- apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates python3-pip
- add-apt-repository -y ppa:openscad/releases
- apt-get update -qq
- apt-get install -y -qq openscad
- pip3 install -r requirements.txt
script:
# Build STL files with OpenSCAD
- mkdir -p /root/.local/share
- python3 ./generate_makefile.py
- make -j7
- ./build.py
artifacts:
expire_in: 1 week
name: "${CI_PROJECT_NAME}-${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}"
......@@ -83,12 +83,12 @@ deploy:
# Install rsync if not already installed
- 'which rsync || ( apt-get update -y && apt-get install rsync -y )'
# Upload the builds folder to openflexure-microscope builds
# Upload the builds folder to openflexure-microscope builds
- rsync -hrvz -e ssh builds/ ci-user@openflexure.bath.ac.uk:/var/www/build/openflexure-microscope/${CI_COMMIT_REF_NAME} --delete
# Zip the builds folder and upload it to the openflexure-microscope build root
- 'which zip || ( apt-get update -y && apt-get install zip -y )'
- (cd builds && zip -r "../${CI_PROJECT_NAME}-${CI_COMMIT_REF_NAME}.zip" .)
- (cd builds && zip -r "../${CI_PROJECT_NAME}-${CI_COMMIT_REF_NAME}.zip" .)
- rsync -hrvz -e ssh "${CI_PROJECT_NAME}-${CI_COMMIT_REF_NAME}.zip" ci-user@openflexure.bath.ac.uk:/var/www/build/openflexure-microscope/
# Run update-latest.py on the build server
......
......@@ -2,10 +2,7 @@
The microscope is modelled using OpenSCAD. This means there's a collection of scripts that, when run, generate STL files that you can then print. The best way to get hold of the latest STL files is to generate them yourself. To do this, you will need [OpenSCAD](http://www.openscad.org/) (available for Windows, Mac and Linux). The simplest way to build all the necessary STL files is to use GNU Make. If you use Linux you will almost certainly have this, Mac OS should either have it or be able to get it from homebrew etc., and Windows you can get it through Cygwin or MinGW.
To build all the STL files (NB you don't need to print all of these, there are some alternatives), simply change directory to the root folder of this repository and run ``python generate_makefile.py`` and then ``make`. This will generate a makefile and run it to compile the files and put them in the ``builds/`` directory.
## Make on Windows
I was happy to find out that [you can use make in MSYS](https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058), which comes bundled with Git for Windows. If you install Git for Windows, you can then download a copy of the executable file for make and put it in the bin directory. For me, this was ``C:\Users\me\AppData\Local\Programs\Git\mingw64\bin`` though your installation may be different. NB to start MSYS on Windows, look for "Git Bash" in your Start menu.
To build all the STL files (NB you don't need to print all of these, there are some alternatives), simply change directory to the root folder of this repository and run ``pip3 install -r requirements.txt`` and then ``./build.py`. This will generate a Ninja build file and run it to compile the files and put them in the ``builds/`` directory.
## OpenSCAD command line
You'll need to make sure OpenSCAD is in your executable path so the build script can [run it from the command line](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Using_OpenSCAD_in_a_command_line_environment). This is probably the case on Linux, but on Windows I just ran ``PATH="$PATH:/c/Program Files/OpenSCAD/"`` before running make. A more permanent solution is to put that command line into a text file ``.bash_profile`` in your home directory (i.e. ``C:\Users\me\.bash_profile``). If it doesn't exist, just create a new text file with that as the only line. Once you've done this, OpenSCAD will automatically be added to your path every time you start MSYS (aka Git Bash). NB the filename does not have a ``.txt`` extension, and it won't work if you leave one there. Symlinking the OpenSCAD binary into your mingw64 binaries folder seems to break things like the OpenSCAD includes directory, so use the method above in preference if you can.
......
......@@ -53,4 +53,4 @@ Most of the Openflexure Microscope stuff lives on GitHub, under [my account](htt
* Some [characterisation scripts for analysing images of the USAF resolution test target](https://github.com/rwb27/usaf_analysis/)
## Compiling from source
If you want to print the current development version, you can compile the STL from the OpenSCAD files - but please still consult the documentation for quantities and tips on print settings, etc. You can use GNU Make to generate all the STL files (run ``python generate_makefile.py`` and then ``make`` in the root directory of the repository). More instructions, including hints for Windows users, are available in [COMPILE.md](COMPILE.md).
If you want to print the current development version, you can compile the STL from the OpenSCAD files - but please still consult the documentation for quantities and tips on print settings, etc. You can use Ninja build to generate all the STL files (run ``pip3 install -r requirements.txt`` and then ``./build.py`` in the root directory of the repository). More instructions are available in [COMPILE.md](COMPILE.md).
#!/usr/bin/env python3
from ninja import Writer, ninja as run_build
build_file = open("build.ninja", "w")
ninja = Writer(build_file, width=120)
ninja.rule(
"openscad", command="openscad $parameters $in -o $out -d $out.d", depfile="$out.d"
)
build_dir = "builds"
def p_string(name, value):
"""
Build an OpenScad parameter string from a variable name and value
Arguments:
name {[str]} -- Parameter name
value -- Parameter value
"""
# Convert bools to lowercase
if type(value) == bool:
value = str(value).lower()
# Wrap strings in quotes
elif type(value) == str:
value = f'"{value}"'
return "-D '{}={}'".format(name, value)
def stage_parameters(stage_size, sample_z):
"""
Return array of common stage parameters for a given size and sample z
Arguments:
stage_size {str} -- Stage size, e.g. "LS"
sample_z {int} -- Sample z position, default 65
"""
return [p_string("big_stage", (stage_size == "LS")), p_string("sample_z", sample_z)]
################################
### GENERAL, WIDELY USED OPTIONS
# All available microscope sizes
stage_size_options = ["LS"]
sample_z_options = [65]
# All permutations of microscope size
microscope_size_options = [
f"{stage_size}{sample_z}"
for stage_size in stage_size_options
for sample_z in sample_z_options
]
###################
### MICROSCOPE BODY
for stage_size in stage_size_options:
for sample_z in sample_z_options:
for beamsplitter in [True, False]:
for brim in [True, False]:
motors = True # Right now we never need to remove motor lugs
outputs = "{build_dir}/main_body_{stage_size}{sample_z}{motors}{beamsplitter}{brim}.stl".format(
build_dir=build_dir,
stage_size=stage_size,
sample_z=sample_z,
motors="-M" if motors else "",
beamsplitter="-BS" if beamsplitter else "",
brim="_brim" if brim else "",
)
parameters = [
*stage_parameters(stage_size, sample_z),
p_string("motor_lugs", motors),
p_string("beamsplitter", beamsplitter),
p_string("enable_smart_brim", brim),
]
ninja.build(
outputs,
rule="openscad",
inputs="openscad/main_body.scad",
variables={"parameters": " ".join(parameters)},
)
#################
### OPTICS MODULE
cameras = ["picamera_2", "logitech_c270", "m12"]
rms_lenses = [
"rms_f40d16",
"rms_f50d13",
"rms_infinity_f50d13",
] # NB: Only RMS lenses are compatible with the beamsplitter
optics_versions = [
("picamera_2", "pilens"),
("logitech_c270", "c270_lens"),
("m12", "m12_lens"),
] + [(camera, lens) for camera in cameras for lens in rms_lenses]
for sample_z in sample_z_options:
for (camera, lens) in optics_versions:
beamsplitter_options = [True, False] if lens in rms_lenses else [False]
for beamsplitter in beamsplitter_options:
outputs = "{build_dir}/optics_{camera}_{lens}{beamsplitter}.stl".format(
build_dir=build_dir,
camera=camera,
lens=lens,
beamsplitter="_beamsplitter" if beamsplitter else "",
)
parameters = [
p_string("sample_z", sample_z),
p_string("optics", lens),
p_string("camera", camera),
]
ninja.build(
outputs,
rule="openscad",
inputs="openscad/optics.scad",
variables={"parameters": " ".join(parameters)},
)
####################
### MICROSCOPE STAND
for stand_height in [30]:
for beamsplitter in [True, False]:
outputs = "{build_dir}/microscope_stand_{stand_height}{beamsplitter}.stl".format(
build_dir=build_dir,
stand_height=stand_height,
beamsplitter="-BS" if beamsplitter else "",
)
parameters = [
p_string("h", stand_height),
p_string("beamsplitter", beamsplitter),
]
ninja.build(
outputs,
rule="openscad",
inputs="openscad/microscope_stand.scad",
variables={"parameters": " ".join(parameters)},
)
ninja.build(
"builds/microscope_stand_no_pi.stl",
rule="openscad",
inputs="openscad/microscope_stand_no_pi.scad",
variables={"parameters": " ".join(parameters)},
)
########
### FEET
for foot_height in [15, 26]:
# Figure out some nice names for foot heights
if foot_height == 26:
version_name = "_tall"
elif foot_height == 15:
version_name = ""
else:
version_name = f"_{foot_height}"
outputs = "{build_dir}/feet{version}.stl".format(
build_dir=build_dir, version=version_name
)
parameters = [p_string("foot_height", foot_height)]
ninja.build(
outputs,
rule="openscad",
inputs="openscad/feet.scad",
variables={"parameters": " ".join(parameters)},
)
###################
### CAMERA PLATFORM
for stage_size in stage_size_options:
for sample_z in sample_z_options:
for version in ["picamera_2", "6led"]:
outputs = "{build_dir}/camera_platform_{version}_{stage_size}{sample_z}.stl".format(
build_dir=build_dir,
version=version,
stage_size=stage_size,
sample_z=sample_z,
)
parameters = [
*stage_parameters(stage_size, sample_z),
p_string("optics", "pilens"),
p_string("camera", version),
]
ninja.build(
outputs,
rule="openscad",
inputs="openscad/camera_platform.scad",
variables={"parameters": " ".join(parameters)},
)
###############
### LENS SPACER
for stage_size in stage_size_options:
for sample_z in sample_z_options:
outputs = "{build_dir}/lens_spacer_picamera_2_pilens_{stage_size}{sample_z}.stl".format(
build_dir=build_dir, stage_size=stage_size, sample_z=sample_z
)
parameters = [
*stage_parameters(stage_size, sample_z),
p_string("optics", "pilens"),
]
ninja.build(
outputs,
rule="openscad",
inputs="openscad/lens_spacer.scad",
variables={"parameters": " ".join(parameters)},
)
##################
### PICAMERA TOOLS
picamera_2_tools = ["cover", "gripper", "lens_gripper"]
for tool in picamera_2_tools:
outputs = f"builds/picamera_2_{tool}.stl"
inputs = f"openscad/cameras/picamera_2_{tool}.scad"
parameters = [p_string("camera", "picamera_2")]
ninja.build(
outputs,
rule="openscad",
inputs=inputs,
variables={"parameters": " ".join(parameters)},
)
#################
### SAMPLE RISERS
for riser_type in ["sample", "slide"]:
outputs = f"builds/{riser_type}_riser_LS10.stl"
inputs = f"openscad/{riser_type}_riser.scad"
parameters = [p_string("big_stage", True), p_string("h", 10)]
ninja.build(
outputs,
rule="openscad",
inputs=inputs,
variables={"parameters": " ".join(parameters)},
)
###############
### SMALL PARTS
parts = [
"actuator_assembly_tools",
"actuator_drilling_jig",
"back_foot",
"condenser",
"gears",
"illumination_dovetail",
"lens_tool",
"motor_driver_case",
"sample_clips",
"small_gears",
"thumbwheels",
]
for part in parts:
outputs = f"builds/{part}.stl"
inputs = f"openscad/{part}.scad"
ninja.build(outputs, rule="openscad", inputs=inputs)
###############
### RUN BUILD
build_file.close()
run_build()
#!python
import re
import argparse
import functools
"""This script generates the Makefile for the Openflexure Microscope.
It is intended to be run whenever you need a new makefile. The makefile lives in
the repository and is versioned, so most people never need to run this script.
"""
# This is used in a lot of places to determine various dimensions
microscope_sizes = ["LS65"]
# Every version of the body we're building
body_versions = [
size + motors + beamsplitter + brim
for size in microscope_sizes
for motors in ["-M"] # NB we only need versions with motor lugs!
for beamsplitter in ["", "-BS"]
for brim in ["", "_brim"]
]
# List of available cameras
cameras = ["picamera_2", "logitech_c270", "m12"]
# List of all available lenses
lenses = [
"pilens",
"c270_lens",
"m12_lens",
"rms_f40d16",
"rms_f50d13",
"rms_infinity_f50d13",
]
# List of lenses which can support a beamsplitter cube
bs_compatible_lenses = ["rms_f40d16", "rms_f50d13", "rms_infinity_f50d13"]
optics_versions_LS65 = ["picamera_2_pilens", "logitech_c270_c270_lens"]
optics_versions_LS65 += [
cam + "_" + lens + (beamsplitter if lens in bs_compatible_lenses else "")
for cam in cameras
for lens in lenses
if "rms" in lens
for beamsplitter in ["", "_beamsplitter"]
] + ["m12_m12_lens"]
optics_versions = [v + "_LS65" for v in optics_versions_LS65]
sample_riser_versions = ["LS10"]
slide_riser_versions = ["LS10"]
stand_versions = ["LS65-30", "LS65-30-BS"]
def body_parameters(version):
"""Retrieve the parameters we pass to OpenSCAD to generate the given body version."""
m = re.match("(LS|SS)([\d]{2})(-M)?(-BS)?(_brim)?", version)
if m is None:
raise ValueError(
"Error finding optics module parameters from version string '{}'".format(
version
)
)
p = {"motor_lugs": False, "beamsplitter": False, "sample_z": -1, "big_stage": None}
p["big_stage"] = m.group(1) == "LS"
p["sample_z"] = m.group(2)
p["motor_lugs"] = m.group(3) == "-M"
p["beamsplitter"] = m.group(4) == "-BS"
p["enable_smart_brim"] = m.group(5) == "_brim"
return p
def optics_module_parameters(version):
"""Figure out the parameters we need to generate the optics module"""
search_expression = "({cam})_({lens})_(beamsplitter_)?({body})".format(
cam="|".join(cameras), lens="|".join(lenses), body="|".join(microscope_sizes)
)
m = re.search(search_expression, version)
if m is None:
raise ValueError(
"Error finding optics module parameters from version string '{}'".format(
version
)
)
# Start with the parameters for a matching body geometry
p = body_parameters(m.group(4))
# Add camera and optics parameters
p.update({"camera": m.group(1), "optics": m.group(2)})
# Check for beamsplitter option
p["beamsplitter"] = m.group(3) == "beamsplitter_"
return p
def stand_parameters(version):
m = re.match(
"({body})-([\d]+)(-BS)?$".format(body="|".join(microscope_sizes)), version
)
if m is None:
raise ValueError(
"Error finding optics module parameters from version string '{}'".format(
version
)
)
p = body_parameters(m.group(1))
p["h"] = int(m.group(2))
p["beamsplitter"] = m.group(3) == "-BS"
return p
def riser_parameters(version):
"""extract the parameters for sample risers"""
m = re.match("(LS|SS)([\d]+)", version)
if m is None:
raise ValueError(
"Error finding optics module parameters from version string '{}'".format(
version
)
)
p = {}
p["big_stage"] = "LS" == m.group(1)
p["h"] = int(m.group(2))
return p
def openscad_recipe(**kwargs):
output = "\t" + "openscad -o $@"
for name, value in kwargs.items():
try:
float(value)
except ValueError:
# numbers and booleans are OK, but strings need to be double-quoted on the command line.
if value not in ["true", "false"]:
value = '"{}"'.format(value)
if type(value) == type(True): # need to convert boolean values
value = "true" if value else "false"
output += " -D '{name}={value}'".format(name=name, value=str(value))
output += " $<\n"
return output
def merge_dicts(*args):
"""Merge dictionaries together into a single output."""
out = args[0].copy()
for a in args[1:]:
out.update(a)
return out
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate the Openflexure Microscope makefile"
)
parser.add_argument(
"--version_numstring", help="Override the defined version string", default=None
)
args = parser.parse_args()
extra_defines = {}
if args.version_numstring:
extra_defines["version_numstring"] = args.version_numstring
def openscad_recipe_baked(**kwargs):
"""An openscad recipe, with additional definitions baked in."""
return openscad_recipe(**merge_dicts(extra_defines, kwargs))
with open("Makefile", "w") as makefile:
def M(line):
makefile.write(line + "\n")
M("# Makefile for the openflexure microscope")
M("# Generated by generate_makefile.py")
M(".PHONY: all cleanstl")
M("")
M("SOURCE = openscad")
M("OUTPUT = builds")
M("")
M("body_versions = " + " ".join(body_versions))
M("optics_versions = " + " ".join(optics_versions))
M("stand_versions = " + " ".join(stand_versions))
M("sample_riser_versions = " + " ".join(sample_riser_versions))
M("slide_riser_versions = " + " ".join(slide_riser_versions))
M("")
M("TOOLS := actuator_assembly_tools lens_tool")
M(
"TOOLS := $(TOOLS) picamera_2_cover picamera_2_gripper picamera_2_lens_gripper actuator_drilling_jig"
)
M(
"ACCESSORIES := picamera_2_cover $(sample_riser_versions:%=sample_riser_%) $(slide_riser_versions:%=slide_riser_%) $(stand_versions:%=microscope_stand_%) microscope_stand_no_pi motor_driver_case back_foot"
)
M("COMMONPARTS := feet feet_tall gears sample_clips small_gears thumbwheels")
M("BODIES := $(body_versions:%=main_body_%)")
M(
"OPTICS := $(optics_versions:%=optics_%) camera_platform_picamera_2_LS65 camera_platform_6led_LS65 lens_spacer_picamera_2_pilens_LS65 fl_cube"
)
M("ILLUMINATIONS := illumination_dovetail condenser reflection_illuminator")
M(
"ALLPARTS := $(COMMONPARTS) $(TOOLS) $(BODIES) $(ILLUMINATIONS) $(OPTICS) $(ACCESSORIES)"
)
M("ALLSTLFILES := $(ALLPARTS:%=$(OUTPUT)/%.stl)")
M("")
M("parameters_file := $(SOURCE)/microscope_parameters.scad")
M("utilities_file := $(SOURCE)/utilities.scad")
M(
"all_deps := $(parameters_file) $(utilities_file) #All targets depend on these"
)
M("")
M("all: $(ALLSTLFILES)")
M("")
M("cleanstl:")
M("\t" + "rm $(STLFILES)")
M("")
M("#parameter and utilities files affect everything")
M("$(OUTPUT)/%.stl: $(all_deps)")
M("")
M("main_body_dep_names := compact_nut_seat dovetail logo z_axis")
M("main_body_deps := $(main_body_dep_names:%=$(SOURCE)/%.scad)")
for version in body_versions:
M(
"$(OUTPUT)/main_body_"
+ version
+ ".stl: $(SOURCE)/main_body.scad $(main_body_deps)"
)
M(openscad_recipe_baked(**body_parameters(version)))
M("")
for size in microscope_sizes:
M(
"$(OUTPUT)/illumination_dovetail_"
+ size
+ ".stl: $(SOURCE)/illumination_dovetail.scad $(main_body_deps) $(SOURCE)/illumination.scad"
)
M(openscad_recipe_baked(**body_parameters(size)))
M(
"$(OUTPUT)/condenser_"
+ size
+ ".stl: $(SOURCE)/condenser.scad $(main_body_deps) $(SOURCE)/illumination.scad"
)
M(openscad_recipe_baked(**body_parameters(size)))
M("")
M("optics_dep_names := dovetail cameras/camera")
M("optics_deps := $(optics_dep_names:%=$(SOURCE)/%.scad)")
for version in optics_versions:
M(
"$(OUTPUT)/optics_"
+ version
+ ".stl: $(SOURCE)/optics.scad $(optics_deps)"
)
M(openscad_recipe_baked(**optics_module_parameters(version)))
M("$(OUTPUT)/fl_cube.stl: $(SOURCE)/fl_cube.scad $(optics_deps)")
M(openscad_recipe_baked(**optics_module_parameters(version)))
M("")
for b in microscope_sizes:
for n in ["camera_platform_picamera_2", "lens_spacer_picamera_2_pilens"]:
M(
"$(OUTPUT)/{}_{}.stl: $(SOURCE)/{}.scad $(optics_deps)".format(
n, b, n.split("_picamera_")[0]
)
)
M(
openscad_recipe_baked(
camera="picamera_2", optics="pilens", **body_parameters(b)
)
)
for b in microscope_sizes:
n = "camera_platform_6led"
M(
"$(OUTPUT)/{}_{}.stl: $(SOURCE)/{}.scad $(optics_deps)".format(
n, b, n.split("_6led")[0]
)
)
M(
openscad_recipe_baked(
camera="6led", optics="pilens", **body_parameters(b)
)
)
M("riser_dep_names := main_body")
M("riser_deps := $(optics_dep_names:%=$(SOURCE)/%.scad)")
for version in sample_riser_versions:
M(
"$(OUTPUT)/sample_riser_"
+ version
+ ".stl: $(SOURCE)/sample_riser.scad $(riser_deps)"
)
M(openscad_recipe_baked(**riser_parameters(version)))
for version in slide_riser_versions:
M(
"$(OUTPUT)/slide_riser_"
+ version
+ ".stl: $(SOURCE)/slide_riser.scad $(riser_deps)"
)
M(openscad_recipe_baked(**riser_parameters(version)))
M("")
M("stand_dep_names := main_body")
M("stand_deps := $(optics_dep_names:%=$(SOURCE)/%.scad)")
for version in stand_versions:
M(
"$(OUTPUT)/microscope_stand_"
+ version
+ ".stl: $(SOURCE)/microscope_stand.scad $(stand_deps)"
)
M(openscad_recipe_baked(**stand_parameters(version)))
M("")
M("$(OUTPUT)/picamera_2_%.stl: $(SOURCE)/cameras/picamera_2_%.scad $(all_deps)")
M(openscad_recipe_baked(camera="picamera_2"))
M("")
M("$(OUTPUT)/feet_tall.stl: $(SOURCE)/feet.scad $(all_deps)")
M(openscad_recipe_baked(foot_height=26))
M("")
M(
"$(OUTPUT)/actuator_assembly_tools.stl: $(SOURCE)/actuator_assembly_tools.scad $(all_deps)"
)
M(openscad_recipe_baked(foot_height=26))
M("")
M("$(OUTPUT)/%.stl: $(SOURCE)/%.scad $(all_deps)")
M(openscad_recipe_baked())
M("")