QtXpl2
Qt 6 static library and QML module for communicating with Alchemie Jetting Interface 2 (JI2) controllers over TCP. Implements the full XPL2 printhead remote protocol (56 command tokens) with a typed, signal-based API designed for Qt Quick applications.
Requirements
- Qt 6.10.2 (Core, Network, Qml, Quick)
- CMake 3.16+
- Ninja (recommended)
Build
qt-cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel
Produces four targets:
| Target | Binary | Description |
|---|---|---|
QtXpl2 |
libQtXpl2.a |
Static library + QML module (import Xpl2) |
JettingProxy |
bin/JettingProxy |
TCP relay between one controller and N clients |
JettingInterfaceDemo |
bin/JettingInterfaceDemo |
Interactive demo app |
MockJettingController |
bin/MockJettingController |
Mock JI2 server for development |
Quick start
# Terminal 1 — start the proxy
./build/bin/JettingProxy
# Terminal 2 — start the mock server (connects to proxy on 9110-9112)
./build/bin/MockJettingController
# Terminal 3 — run the demo (connects to proxy on 9210-9212)
./build/bin/JettingInterfaceDemo
Click Connect in the demo, then explore the Commands and Status tabs. Multiple demo instances can connect simultaneously.
CLI flags
| Binary | Flag | Description |
|---|---|---|
JettingInterfaceDemo |
--printheads N |
Number of printheads in the demo UI (default 10) |
JettingProxy |
--controller-port P |
Base port for controller side (default 9110) |
JettingProxy |
--client-port P |
Base port for client side (default 9210) |
| All | --wire-debug |
Log raw wire bytes / forwarded frames |
Architecture
Proxy architecture
A JettingProxy relay sits between the controller and N client instances:
MockController ──(9110/9111/9112)──→ JettingProxy ←──(9210/9211/9212)── Xpl2Client #1
←──(9210/9211/9212)── Xpl2Client #2
The proxy broadcasts controller frames to all clients and forwards client frames to the controller. KA_PING is handled independently on both sides (never forwarded).
Three-port TCP protocol
The JI2 protocol uses three independent TCP channels:
| Controller port | Client port | Name | Purpose |
|---|---|---|---|
| 9110 | 9210 | Command | GS_, CN_, CF_ commands and responses |
| 9111 | 9211 | Imaging | m0-m6 image masks, m2/m4 start/stop, n replies |
| 9112 | 9212 | Status | EV_ events, status messaging |
Xpl2Client connects out to the proxy on the client ports as a QML singleton.
Protocol implementation
All 56 protocol tokens are implemented across 6 categories:
| Category | Count | Direction | Examples |
|---|---|---|---|
| GS_ (Status) | 2 | Request/reply | getJcVersion(), getPhVersion() |
| CN_ (Control) | 17 | Request/reply | jettingAllOn(), jcCalibration(), phIdLedOn() |
| CF_ (Config) | 20 | Request/reply | jcSetter(), phGetJettingParams(), jcShutdown() |
| Imaging (m/n) | 8 | Request/reply | imagingStart(), imageMaskEnd(), imageCount() |
| KA_ (Keepalive) | 1 | Automatic | Invisible to QML, handled internally |
| EV_ (Events) | 8 | Server-push | jcStatusReceived(), phErrorCode(), shuttingDown() |
Data-driven dispatch
Response handling uses a static dispatch table (QHash<QByteArray, ResponseEntry>) mapping each command token to its response shape, minimum parameter count, and signal emitter. Adding a new command is one table entry:
{ "CN_JETTING_ALL_ON", { ResponseShape::JcSuccess, 2,
[](auto *s, const auto &p) {
emit s->jettingAllOnResult(p[0].toInt(), p[1].toInt() == 1);
} } },
QML usage
import Xpl2
// Xpl2Client is a singleton — no instantiation needed
Button {
text: "Connect"
onClicked: {
Xpl2Client.host = "192.168.1.100"
Xpl2Client.commandPort = 9210
Xpl2Client.connectToProxy()
}
}
// Bind to properties
Label {
text: "FW: " + Xpl2Client.firmwareVersion
}
// React to signals
Connections {
target: Xpl2Client
function onJcStatusReceived(status) {
// status is an Xpl2JcStatus Q_GADGET
tempLabel.text = status.temperature.toFixed(1) + "°C"
cpuLabel.text = status.cpuPercentageBusy.toFixed(1) + "%"
}
function onPhErrorCode(controllerId, printheadId, errorCode, params) {
console.log("PH", printheadId, "error:", errorCode)
}
}
Status gadgets
Periodic status messages are parsed into structured types with named properties:
Xpl2JcStatus— 21 fields (7 at level 1, 14 more at level 2): CPU%, voltages, temperature, humidity, indicators, firmware versionsXpl2PhStatus— 59 fields (28 at level 1, 31 more at level 2): temperatures, voltages, trip flags, jetting params, gyro/accelerometer, purge state
Extended result enums
Configuration commands that can fail on specific parameters return typed enums visible in QML:
JettingParamsResult—Ok(1),Failed(0),DutyCycle(-1) throughNozzleDriveDutyCycle(-5)SetterResult—Ok(1),Failed(0),IncorrectNewValue(-1)
Project structure
QtXpl2/
src/
Xpl2Client.h/cpp # QML singleton — typed API, dispatch table
Xpl2Protocol.h/cpp # Wire framing, enums (Q_NAMESPACE)
Xpl2JcStatus.h/cpp # Q_GADGET for JC status (Appendix A)
Xpl2PhStatus.h/cpp # Q_GADGET for PH status (Appendix B)
CMakeLists.txt
jetting-interface/
Main.qml # App shell: connection, tabs, console
CommandsPage.qml # GS/CN/CF/imaging buttons, printhead list
StatusPage.qml # Status messaging controls, live displays
DebugConsole.qml # Dark log panel
main.cpp
CMakeLists.txt
jetting-proxy/
JettingProxy.h/cpp # Transparent TCP relay (1 controller ↔ N clients)
main.cpp
CMakeLists.txt
mock-jetting-controller/
MockServer.h/cpp # Canned responses, periodic status emission
main.cpp
CMakeLists.txt
docs/
protocol.pdf # Alchemie JI2-JC protocol specification
Jetting proxy
The proxy (JettingProxy) is a transparent TCP relay that enables multiple clients to share a single controller connection. It operates at the raw frame level — it reads LF-terminated lines and forwards them without parsing the protocol. KA_PING is handled independently on both sides to prevent N clients from each replying to the controller.
Mock server
The mock server responds to all 56 protocol tokens with canned data. It connects to the proxy (or directly to a listening client) on ports 9110-9112. When status messaging is started (CN_JC_STATUS_MESSAGING_START / CN_PH_STATUS_MESSAGING_START), it emits periodic EV_STATUS_MSG_JC / EV_STATUS_MSG_PH events at the requested interval, alternating between two value sets so updates are visually apparent in the demo.
Conventions
- C++17, GNU code style (
.clang-format) - QML:
.qmlformat.ini,pragma ComponentBehavior: Bound, no unqualified access - Doc-comments on declarations (headers), not definitions (.cpp)
- No external dependencies beyond Qt 6.10.2
- Prefer named functions/slots over lambdas
