OpenCAL drives a computed axial lithography printer from a Raspberry Pi
Opensource CAL 3D printer with configuration flexibility based on commercially available optical components and 3D printed components. Intended for research purposes.
At a glance
- What is it?
- OpenCAL is the open-source software for a computed axial lithography printer, a volumetric and layer-less form of resin printing where a projector and a spinning vial do the shaping instead of a moving print head. The firmware runs headless on a Raspberry Pi 5 and is driven entirely through a 20x4 character LCD and a rotary encoder, with every pin and calibration value in one JSON file.
- Who is it for?
- Adopt OpenCAL if you are building a CAL printer for research, since it is an open implementation with a published list of parts and a configuration file that expects you to match your own hardware. Skip it if you want a desktop resin printer, because this is a specific volumetric technique with its own optics and mechanics, and if you are not prepared to source the parts.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 130 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Volumetric printing, which is a different machine
OpenCAL is open-source software for building and operating a computed axial lithography, or CAL, 3D printer, described as a volumetric, layer-less resin printing technique.
That phrase describes the whole difference between this and an ordinary resin printer. A conventional printer exposes one layer at a time and moves the build platform or the light source through the resin. A CAL printer instead illuminates the entire volume of resin in the vial at once, projecting an image that has already been computed to account for how light scatters and refracts through the liquid on its way to the target point. The result is a solid that forms in the resin rather than being built up slice by slice.
The consequences for the machine are large. There is no Z axis to step, so there is no bed and no layer height to calibrate. The light source is a projector, which has to be calibrated to the exact optical path and the geometry of the vial. And the resin needs to be moved, because the computation assumes a known distribution of the photoinitiator, which means rotating the vial during the exposure rather than leaving it static.
The project is intended for research purposes, and the description says so plainly: an open-source CAL 3D printer with configuration flexibility based on commercially available optical components and 3D printed components.
That last clause is the interesting constraint. The design goal was not a bespoke optical bench but a machine built from parts you can buy and parts you can print, and the software is open because the research depends on the machine being modifiable.
The firmware runs on a Raspberry Pi 5 and orchestrates all hardware needed for a CAL print job, with all parameters tunable through one configuration file. It is designed to run headless on a Raspberry Pi 5 and is controlled through a hardware LCD and encoder interface.
The project is Python, with releases v0.2.2 from 2026-05-13, v0.2.1 from 2026-05-05 and v0.1.11 from 2026-05-01, and the last push was on 2026-06-15.
The hardware list is a parts list you can buy
The hardware requirements table reads like a parts list, and each entry has been picked deliberately rather than generically.
The computer is a Raspberry Pi 5 with 2GB or more of RAM, and the storage is a microSD card of at least 12GB. The motor controller is a Pololu Tic T249 over USB, driving a NEMA 17 stepper that was tested with a 17HE19-2004S rated at 2.0A. The LED array is an 8 by 8 SK6812 RGBW NeoPixel matrix, 64 LEDs in total.
The display is a 20 by 4 I2C LCD with a PCF8574 backpack at address 0x27, and the encoder is a rotary encoder with a push button on GPIO 5, 6 and 19. The camera is a Raspberry Pi Camera Module 3 with the IMX708 sensor. The projector is any HDMI projector, tested with a NexiGo Nova Mini.
The projector line is worth pausing on, because it says something about the ambition. Any HDMI projector is the specification, and the tested unit is a small portable one. That is only possible because the projector is driven as a video source rather than as a controlled light source, with the precision coming from the computation of the projected pattern and from calibration rather than from the projector's own optics.
The software dependencies installed automatically on the pre-built image are listed separately and they map onto the hardware one for one. Pololu ticcmd is the command-line tool for the Tic motor controller. pi5neo handles NeoPixel and SK6812 control over SPI. picamera2 and libcamera are the Raspberry Pi camera stack. mpv is the video playback for the projector. And udiskie does USB drive automounting.
That last one explains a design decision that appears later. The LCD menu offers print from USB, manual control with image capture to USB, and a prompt for saving video to USB, so the machine is designed to be driven from a USB stick with no network involved.
Nine components, and one file that describes the hardware
The key components list is the architecture, and it is worth reading as a set of responsibilities rather than as a directory listing.
The LCD GUI is a state-machine menu controller for the 20x4 I2C LCD display, and it drives all user interaction via the rotary encoder: file selection, settings, manual motor and LED control, alignment, and print start and stop. The word state machine is the key. A character LCD with four lines and one knob has no room for a general interface, so every screen is a state and every input is a transition, and that constraint shapes the whole interaction model.
The Pygame GUI manages the projector display for precise interactive visuals during calibration and printing. So the Pi has two graphical surfaces: the character LCD for the operator and a full graphical one driven through the projector, which is what the user looks at during alignment.
The print controller orchestrates a full print job: it spins up the stepper motor, activates the LEDs, plays the video via mpv, and records via the camera. The hardware controller initialises all hardware at startup, and failures are caught individually so the system continues in degraded mode rather than crashing. That last clause matters on a machine with this many peripherals, where one misbehaving LED board should not prevent you from calibrating the motor.
Then the three device-specific controllers. The stepper controller drives the Pololu Tic T249 over USB. The projector controller is mpv-based, with crop and zoom calibration for the print resin vial, which is the physical adjustment that ties the projected image to the vial. The camera controller is picamera2-based, handling capture and H264 video recording.
And the two configuration files. config.json is the single source of truth for all GPIO pins, I2C addresses, LED counts, camera type, default RPM, and projector calibration values. tic_settings.yaml holds the Pololu Tic T249 motor controller settings, being the current limit, step mode and acceleration, and they are applied automatically on every startup.
That last detail is a nice piece of engineering. Pushing the motor settings to the controller on every boot means the device always has the configured limits even if it was reflashed or replaced, so the safe current limit is not a one-time setup step you can forget.
The GUI features are listed as five groups: print from USB with the RPM read from the filename, manual control for LEDs, the stepper motor and image capture to USB, a settings menu with an alignment tool, a vial width finder, calibration images and a USB video save prompt, power options for restart, power off and killing the GUI, and an about screen with credits.
Flashing the pre-built image
The quick start path is the pre-built Raspberry Pi 5 image, and it is the shortest route to a working machine.
The image is distributed as opencal_pi5.img.gz. You download it, download and install Raspberry Pi Imager, then in the Imager you click Choose OS and Use custom and select the image file, click Choose Storage and select your microSD card of at least 12GB, click Write, and wait. Then you insert the card into the Raspberry Pi 5 and power on.
The system boots directly into OpenCAL. The default login username is opencal, and the password is set on first login, where you are prompted to change it.
The note about what is already configured is the reassuring part. The pre-built image already has the systemd service enabled and all hardware configured, so you do not need to run any additional setup commands. The only things intentionally cleared from the image for security are the WiFi credentials and the SSH host keys, and the host keys are regenerated automatically on first boot.
Clearing the WiFi credentials means the image ships without someone's network, which is the right default for something distributed as a file download, and clearing the SSH host keys is the same hygiene you would expect from a fresh install of any operating system.
WiFi is set up through a file rather than a prompt, by placing a wifi.txt file in the boot partition with the contents:
SSID=YourNetworkName
PASS=YourPasswordThat is a deliberate choice for a headless machine. Since the interface is an LCD and an encoder, there is no display to type a password into at first boot, so the network details have to arrive through the SD card's boot partition before the machine starts.
The image is hosted on Google Drive, which is worth noting as a practical detail rather than a technical one, since it is the kind of hosting choice that affects how people can fetch it.
Building from source and running it as a service
The source path requires a Raspberry Pi 5 running Raspberry Pi OS Bookworm, and it is a short sequence.
# Clone the repo
git clone https://github.com/computed-axial-lithography/OpenCAL.git
cd OpenCAL
# Install Python dependencies
pip install -r requirements.txt --break-system-packages
# Install Pololu Tic software (ticcmd)
# Download the ARM64 package from https://www.pololu.com/docs/0J71/3.2
# and install it with: sudo dpkg -i pololu-tic-*.deb
# Install udiskie for USB automounting
sudo apt install udiskieThe Pololu step needs a manual download, because the ARM64 package is fetched from Pololu's own site and installed with dpkg. The udiskie install is a plain apt call, and the pip install uses the break-system-packages flag, which is what a fresh Bookworm image needs since it ships with an externally managed environment.
The service setup is the part that explains how the program is started. The documentation enables a user systemd service to start on boot, and writes two unit files into the user's systemd directory.
The first is a path unit, which is a systemd mechanism for starting something when a filesystem path appears:
[Unit]
Description=Start OpenCAL when Wayland compositor is ready
[Path]
PathExists=%t/wayland-0
[Install]
WantedBy=default.targetThe path it watches is the Wayland socket, and the description says it starts OpenCAL when the Wayland compositor is ready. That is the correct dependency to express, because the Pygame GUI needs a display and starting it before the compositor has created its socket would fail.
The second is the service itself:
[Unit]
Description=OpenCAL Printer Controller
[Service]
Type=simple
WorkingDirectory=/home/opencal/OpenCAL
ExecStart=/usr/bin/python3 -m opencal
Environment=SDL_VIDEODRIVER=wayland
Environment=WAYLAND_DISPLAY=wayland-0
Environment=XDG_RUNTIME_DIR=/run/user/1000
Environment=DISPLAY=:0
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=5
TimeoutStopSec=10The service is a simple type with a working directory, running the package as a module with python3. The environment block sets SDL_VIDEODRIVER to wayland and the Wayland display name, and also sets XDG_RUNTIME_DIR and DISPLAY, which suggests the code can fall back to X11 in some situations.
The logging and restart behaviour are the parts worth noticing. Standard output and standard error both go to the journal, so there is one place to look for logs. Restart is on-failure with a five second delay, which is the standard way to keep a hardware orchestration service alive after a transient error, and the stop timeout is ten seconds, which is there so the stepper and the projector can be shut down cleanly rather than having the process killed mid-move.
The enabling commands are the usual trio: enable the path unit, start it, and enable lingering for the user with loginctl, so the service starts on boot even without an active login session.
Configuration, the naming convention and what is still open
Everything about the hardware is described in one file, opencal/utils/config.json, and the documentation tells you to edit it to match your hardware.
{
"stepper_motor": {
"driver_mode": "tic_usb",
"A_pin": 12,
"B_pin": 13,
"default_rpm": 9The stepper_motor section shows the shape of the whole file. There is a driver_mode, which here is tic_usb, so the choice of motor driver is configuration rather than code. There are the A_pin and B_pin values, which are the encoder pins for the rotary knob. And there is a default_rpm, which is the speed the vial is rotated at during a print.
That last value is the one with physical meaning. The computation of a CAL exposure depends on the photoinitiator being distributed in a known way, and the rotation speed is part of that assumption, which is why it is in the same file as the pin assignments rather than buried in a constant.
The RPM is also read from the filename when you print from USB. The GUI feature list says print from USB automatically reads RPM from filename if present, and there is a video file naming convention section for it. That is a small design decision with a real justification: it lets you keep a library of calibration videos at different rotation speeds and pick the right one by dropping the file on a USB stick, with no typing on a character LCD.
The settings menu carries the calibration tools: an alignment tool, a vial width finder, calibration images and a prompt for saving video to USB. The vial width finder is there because the vial geometry is an input to the computation, and the projector controller has crop and zoom calibration for the same reason.
The packaging is a standard Python project with a pyproject.toml. It requires Python 3.11 or newer, and the dependencies are the hardware libraries: gpiozero for the general GPIO interface, lgpio for the low-level pin access, pi5neo for the LED array, rplcd for the I2C display, smbus2 for the I2C bus, ticlib and pyusb for the motor controller, and ipython. The package data section explicitly includes the two configuration files so they ship with the package. The linting is configured with ruff at a line length of 100.
The repository also carries the tools that are separate from the package proper: camera_test.py, make_calibration.py and vial_width.py at the root, a setup.sh, an apt-requirements.txt, a docs directory and an assets directory.
The project states that it is in active development and that feedback and contributions are welcome, with a Discord server and a readthedocs documentation site, and there is a separate VAMToolbox documentation link for generating CAL-compatible print files, which is the other half of the workflow since the software that computes the pattern to display lives outside this repository.
Editorial conclusion
Adopt OpenCAL if you are building a CAL printer for research, since it is an open implementation with a published list of parts and a configuration file that expects you to match your own hardware. Skip it if you want a desktop resin printer, because this is a specific volumetric technique with its own optics and mechanics, and if you are not prepared to source the parts. Verify first which LED array and projector you have, since the configuration records their calibration values, that your Raspberry Pi 5 has at least 2GB of RAM and a 12GB card, and whether you want the pre-built image or to build from source, since the image ships with the service already enabled.
Frequently asked questions
What is OpenCAL and what kind of printer does it drive?
OpenCAL is open-source software for building and operating a computed axial lithography 3D printer, a volumetric and layer-less resin printing technique. It runs headless on a Raspberry Pi 5 and orchestrates the hardware for a print job, with all parameters tunable through opencal/utils/config.json, and it is controlled through a 20 by 4 I2C LCD and a rotary encoder rather than a screen.
What hardware does OpenCAL need?
A Raspberry Pi 5 with 2GB or more of RAM, a microSD card of at least 12GB, a Pololu Tic T249 motor controller over USB, a NEMA 17 stepper tested with a 17HE19-2004S rated at 2.0A, an 8 by 8 SK6812 RGBW NeoPixel matrix of 64 LEDs, a 20 by 4 I2C LCD with a PCF8574 backpack at address 0x27, a rotary encoder with a push button on GPIO 5, 6 and 19, a Raspberry Pi Camera Module 3 with the IMX708 sensor, and any HDMI projector, tested with a NexiGo Nova Mini.
How do I install OpenCAL?
The quickest route is to flash the pre-built image: download opencal_pi5.img.gz, use Raspberry Pi Imager with Choose OS then Use custom to select the image, Choose Storage to pick a microSD card of at least 12GB, and write it. The image already has the systemd service enabled and the hardware configured, with the login username opencal and the password set at first login. To build from source on Raspberry Pi OS Bookworm, clone the repository, run `pip install -r requirements.txt --break-system-packages`, install Pololu ticcmd from the ARM64 package and udiskie with apt, then enable the user systemd units.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/computed-axial-lithography-opencal-firmware)Was this useful?