- Rust 76.7%
- Python 18.6%
- HTML 2.2%
- Shell 1.7%
- Dockerfile 0.8%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .tasks | ||
| docker | ||
| docs | ||
| examples | ||
| lab | ||
| src | ||
| tests | ||
| vendor | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| cliff.toml | ||
| compose.helios.yml | ||
| compose.lab.yml | ||
| compose.yml | ||
| copyme.settings.toml | ||
| Dockerfile | ||
| README.md | ||
| ROADMAP.md | ||
| run.py | ||
| VERSION.txt | ||
LJ rust
Laser tracer for LJ: it reads frames from Redis, optimizes them for the mirrors (galvos) and sends them to a Helios or Etherdream DAC.
Docker
Every example in this README runs in Docker: you only need docker (with compose) and python3 on the host. ./run.py builds the images and runs the tracer, Valkey (Redis-compatible), the tests and the renders in containers.
$ cp copyme.settings.toml settings.toml # edit the DAC and redis_url
$ ./run.py docker --dac dummy --config ./settings.toml
$ ./run.py --help
What the optimizer changes
Frames as a client sends them, through the old angle_correction, and through optimize, drawn by a simulated galvo (2.5 kHz mirrors, 30 kpps):
| as sent | angle_correction |
optimize |
|
|---|---|---|---|
| star | ![]() |
![]() |
![]() |
| ILDA test pattern | ![]() |
![]() |
![]() |
| 100 small stars | ![]() |
![]() |
![]() |
How to read them: thin grey lines are the shape asked for, the coloured line is where the beam goes with the laser on, dashed grey lines are blank jumps, faint dots are the points sent to the DAC (orange lit, grey blank).
- As sent, the mirrors cannot keep up: the star is a squiggle in the middle, and the jumps between small stars are lit.
angle_correctionholds the corners, but the beam overshoots every tip, and the ILDA pattern needs 14 000 points (2 frames per second).optimizebrakes before corners, waits for the mirrors after jumps, orders the strokes to shorten jumps, and fits the frame to the point budget.
Settings and tuning per laser: docs/optimizer.md.
Crashcourse
Without Docker, on a host with Rust and libudev-dev (./run.py docker does the same in containers):
# Copy and edit the settings file
$ cp copyme.settings.toml settings.toml
# Populate the redis database
$ cargo run --example populate_redis
# Run
$ cargo run --release
Examples
All of these run in Docker through ./run.py (./run.py --help, ./run.py <command> --help).
# Run tracer + Valkey (Redis-compatible, data in the lj_rust_valkey-data volume) with the DAC of your settings file
$ ./run.py docker --dac helios --config ./settings.toml
$ ./run.py docker --dac etherdream --config ./settings.toml -d # in the background
$ ./run.py down # stop; --wipe also deletes the Valkey data
# Run the cargo binary against an already running Redis
$ ./run.py raw --config ./settings.toml
# See what a command would run, without running it
$ ./run.py --dry-run docker --dac dummy --config copyme.settings.toml
Checking drawings without a laser:
# Render every test frame (as sent, angle_correction, optimize) to PNG and compare with tests/visual.
# Prints a directory with the renders, the references and the differences, for review.
$ ./run.py visual
$ ./run.py visual --update # after an intended change: review, then accept the new references
# One HTML page with every frame at several mirror speeds and damping, with quality numbers
$ ./run.py report --out report/optimizer.html
Tests:
# Gate: fmt, clippy, tests with coverage (Redis tests against a throwaway Valkey), run.py tests
# (CI also runs visual and test)
$ ./run.py check --lcov lcov.info
$ ./run.py fmt # format the sources
# End to end: Dummy DAC, throwaway Valkey on port 6380
$ ./run.py test
# Optimizer time per frame, and stress frames (huge, noisy, many strokes, broken input);
# the lj_rust:check image is built by ./run.py check
$ docker run --rm --entrypoint cargo lj_rust:check run --release --example optimizer_bench
$ docker run --rm --entrypoint cargo lj_rust:check run --release --example optimizer_stress
# What each optimizer setting does when pushed too far (ranges in docs/optimizer.md)
$ docker run --rm --entrypoint cargo lj_rust:check run --release --example optimizer_sweep
# Run your own frame (Redis line format) as sent, through angle_correction and optimize,
# with an optional profile override; SVGs go to the mounted directory
$ docker run --rm -v "$PWD/out:/out" --entrypoint cargo lj_rust:check run --release --example optimizer_frame -- tests/fixtures/client_frame_1440.ron /out "(accel: 60000.0)"
Hardware lab
Draws test frames on a real laser through LJ's Redis keys, films them with a webcam and writes a YAML record per frame. It needs a Helios on USB, a V4L2 camera and someone at the laser: it asks once to confirm the area is clear before drawing, then for an accept or reject per frame (a reason only on reject, optional).
# LJ (web UI on http://127.0.0.1:8080), a throwaway Valkey, the tracer on the Helios
$ ./run.py lab up --camera /dev/video4
# Fit the geometry to the test target (lab/calibration-A4.md, on the wall in the projection):
# small crosses, a homography, the EDH written to /EDH/0 as LJ does, the outline checked in mm.
# Lights the laser. Rerun after moving the projector or the sheet, and after any LJ restart.
$ ./run.py lab calibrate --camera /dev/video4
# Built-in frames: simulator shapes, stress frames, the ILDA test pattern
$ ./run.py lab list
# A series of frames (every built-in but `dot`), one keystroke per frame: Enter accepts, r rejects
$ ./run.py lab suite
$ ./run.py lab suite -- --patterns square,star,ilda --scale 0.5
# One frame; its folder (images, frame, record.yaml) is printed at the end
$ ./run.py lab run -- run --pattern ilda --scale 0.6
$ ./run.py lab run -- --help
# Stop; LJ's settings (calibration) stay in a volume, --wipe deletes them
$ ./run.py lab down
Measuring tools (OpenCV, in the analysis image) are in lab/analysis/: laser_on_target.py (a figure in mm on the sheet), figure_vs_frame.py (a drawn frame against the frame sent: deviation, coverage, leak), lens.py (camera lens calibration).
The tracer settings come from docker/lab.settings.toml (--config to use another file). The lab sets black before the tracer starts, after each run, on error and on Ctrl-C. The degenerate frame (non-finite points) is refused unless the settings include optimize.
Docker notes
- The tracer runs on the host network: Etherdream discovery is a UDP broadcast.
- Valkey listens on
127.0.0.1only, on the port ofredis_url. - Helios: install the udev rules on the host.
Documentation
- docs/optimizer.md: optimizer settings, tuning a laser, known limits.
- docs/adr: decisions (ADR); docs/agdr: implementation decisions (AgDR).








