Qt6/C++ GUI for the inkjet test rig. Runs on a Raspberry Pi connected to a Galil DMC-4080-C022 motion controller and a JetForge (Added Scientific) Xaar printhead controller, with stroboscopic imaging via an IDS UI-3370CP camera and a Teensy 4.0 strobe driver.
| Component | Interface | Notes |
|---|---|---|
| Galil DMC-4080-C022 | Ethernet (192.168.42.x) | Axes A (X carriage) and D (reservoir height) |
| JetForge (Added Scientific) | USB serial | Xaar 128-nozzle printhead controller |
| Air solenoid valve | Galil extended I/O bit 22 | Quick purge |
| IDS UI-3370CP camera | USB3 | Stroboscopic imaging |
| Strobe Teensy 4.0 | USB serial | Custom PCB; firmware in Teensy/ |
The following assumes Raspberry Pi OS (Bookworm / trixie-based) on a Pi 4 or Pi 5, freshly flashed and SSH-accessible.
sudo apt update && sudo apt full-upgrade -ysudo apt install -y \
git cmake ninja-build \
qt6-base-dev qt6-serialport-dev \
libgl-devGalil publishes a Debian APT repository, but it uses SHA1 signatures which Debian trixie rejects by default. Work around this:
# 3a — Add Galil's repo with the required override flags.
# (The file likely already exists; check before adding it.)
sudo tee /etc/apt/sources.list.d/galil-release.list <<'EOF'
deb [trusted=yes arch=arm64] http://apt.galil.com/galil stable main
EOF
# 3b — Install
sudo apt update
sudo apt install -y gclib gcapsdIf apt complains about GPG / SHA1: the
trusted=yesflag above bypasses the signature check entirely. This is acceptable on a lab-internal machine. You can also import Galil's key manually if you prefer stricter checking — see Galil's online documentation.
Verify the install:
ls /usr/include/gclib.h # should exist
ls /usr/lib/libgclib.so # or /usr/lib/aarch64-linux-gnu/libgclib.soThe DMC-4080 ships with a default IP of 192.168.0.42. It can be
reconfigured, but the simplest approach is to put the Pi on the same
192.168.42.x subnet (or whichever subnet the controller is on).
# Replace 192.168.42.10 with an address not already in use on that subnet.
sudo nmcli connection modify "Wired connection 1" \
ipv4.method manual \
ipv4.addresses "192.168.42.10/24" \
ipv4.gateway "" \
ipv4.dns ""
sudo nmcli connection up "Wired connection 1"Test reachability:
ping 192.168.42.100 # the controller's IPIf the controller was given a different IP during commissioning, update the address in the GUI's Galil Connection bar before clicking Connect.
The DMC-4080 ships with motor type set to servo (MT=-1). The FSL30
linear stages use stepper motors. If program.dmc has not yet been
uploaded to the controller, you need to configure this manually once.
Connect via telnet:
telnet 192.168.42.100Then send:
MTA=2 (X-axis: stepper, active-low step)
MTD=2 (D-axis: stepper, active-low step)
SHA (enable X motor)
SHD (enable D motor)
Once program.dmc is uploaded (see §8), #AUTO does this at every power-up
and the manual step is no longer needed.
Axis letter note: The Galil accepts both the axis letter (
A,D) and the axis name (X). This project uses the letter form throughout.SHAandSHXare equivalent;SHDhas no name alias.
Download the IDS peak installer for Linux ARM64 from https://en.ids-imaging.com/ids-peak.html.
# Unpack and run the installer (name will differ by version):
chmod +x ids_peak_*_arm64.run
sudo ./ids_peak_*_arm64.runThe SDK installs to /opt/ids/peak by default. If you choose a different
path, pass it to CMake:
cmake -DIDS_PEAK_ROOT=/your/path ..After installing, check that the camera is recognised:
lsusb | grep IDSgit clone https://github.com/YOUR-ORG/TestRig.git
cd TestRig
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -G Ninja
ninjaThe resulting binary is build/TestRig.
The program.dmc file configures the controller at power-up. Upload it
once after commissioning; the controller stores it in non-volatile memory.
From the GUI: (not yet implemented — use Galil tools for now)
From Galil Composer (Windows only): File → Download → select program.dmc.
From gcapsd / telnet:
# Use gclib's program download utility if available:
gcapsd -a 192.168.42.100 -d program.dmc
# Or paste the contents line-by-line into telnet.After uploading, reset the controller (RS in telnet or power-cycle) and
verify the motors energise automatically.
./build/TestRig-
Connect to Galil — enter the controller IP in the connection bar and click Connect. The status dot turns green. Both axis motor-enable buttons update to reflect the motor state.
-
Home the axes (optional but recommended) — click Home (Rev Limit → Zero) on each axis in the Axes tab. The carriage jogs slowly toward its reverse limit switch; when the limit trips, position is defined as 0 mm.
-
Connect to JetForge — go to the Printhead tab, enter the serial port (e.g.
/dev/ttyACM0), and click Connect. See the note below about identifying which Teensy is which. -
Power on the printhead — click Power On.
-
Load a bitmap — click Browse and select a
.bmpfile. The image is converted to a bitstream and sent to the printhead. -
Print — use the Test Jet button for a single encoder-triggered pass at a predefined location, or the jog controls to position manually and then trigger a pass.
Two Teensy devices appear on USB: the JetForge (Added Scientific Xaar printhead controller) and the strobe Teensy (custom PCB for the LED flash). Both enumerate as PJRC serial ports.
To tell them apart:
- Plug in one at a time and note which port appears (
ls /dev/ttyACM*). - Or: send a command in the JetForge's serial command field and watch which port responds.
The JetForge goes in the Printhead tab port field. The strobe Teensy goes in the Camera/Strobe tab Teensy Connection section.
The quick-purge solenoid is controlled by a Galil digital output.
To change the bit number, edit the #define value in src/printer.h:
#define SOLENOID_BIT 22The JetForge receives a print-start trigger and a direction signal from the
Galil's extended I/O. The default bit assignments (which may need to be
changed) are in src/printer.h:
#define MJ_START_BIT 18 // pin 18
#define MJ_DIR_BIT 32 // pin 32Verify these against the test rig's actual wiring before the first print pass. If the bits are wrong, the printhead will fire at the wrong time or not at all.
Both axes use the same FUYU FSL30 linear stage with a Nema 14 stepper and a 2 mm pitch leadscrew at 256 microsteps:
MICROSTEPS_PER_MM = 200 steps/rev × 256 microsteps × (1 rev / 2 mm) = 25 600 cnt/mm
When the X carriage is replaced with the longer stage used on the custom
printer, update X_CNTS_PER_MM in src/printer.h if the new stage has a
different pitch or microstepping setting. The D-axis constant
(D_CNTS_PER_MM) is independent and does not need to change.
- Confirm the
#AUTOprogram has been uploaded and the controller was reset. - Check
MTin telnet:MG _MTAshould return2. If it returns-1,MTX=2has not been applied — uploadprogram.dmcand reset. - The controller needs
SHbefore it will acceptBG. If the GUI connects before#AUTOfinishes (rare), click Enable Motor manually in the Axes tab.
- Confirm the static IP is set on
eth0and the Pi can ping the controller. - Confirm
gclibandgcapsdare installed (dpkg -l gclib gcapsd). - The DMC-4080-C022 default port is 23 (telnet). If your network blocks 23, configure the controller to use a different port via Galil Composer.
- Install IDS peak for ARM64 (see §6).
- Check
lsusbshows the camera. - Run
ids_peak_cockpitto verify the camera opens with the IDS tools before trying the GUI.
- This cannot happen — each USB device gets its own
/dev/ttyACMxnode. Re-scan with the Rescan button in the Teensy Connection group after plugging in or unplugging.
TestRig/
├── CMakeLists.txt
├── program.dmc DMC controller startup program
├── README.md
└── src/
├── printer.h/cpp Axis enum, constants, CMD namespace
├── galilcontroller.h/cpp Multi-axis gclib wrapper
├── asyncserialdevice.h/cpp Serial base class (from custom printer)
├── mfjdrv.h JetDrive command definitions (reference)
├── mjdriver.h/cpp Added Scientific (JetForge) serial driver
├── printheadwidget.h/cpp Printhead control tab
├── cameracontroller.h/cpp IDS peak wrapper (pimpl)
├── cameraworker.h/cpp Acquisition thread
├── camerawidget.h/cpp Camera settings UI
├── liveviewwidget.h/cpp Live frame display
├── strobewidget.h/cpp Start/Stop acquisition
├── arduinocontroller.h/cpp Teensy serial protocol
├── arduinowidget.h/cpp Strobe timing UI
└── mainwindow.h/cpp Main window + Axes tab