1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
# 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
```bash
qt-cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel
```
Produces three targets:
| Target | Binary | Description |
|---|---|---|
| `QtXpl2` | `libQtXpl2.a` | Static library + QML module (`import Xpl2`) |
| `JettingInterfaceDemo` | `bin/JettingInterfaceDemo` | Interactive demo app |
| `MockJettingController` | `bin/MockJettingController` | Mock JI2 server for development |
## Quick start
```bash
# Terminal 1 — start the mock server
./build/bin/MockJettingController
# Terminal 2 — run the demo
./build/bin/JettingInterfaceDemo
```
The demo connects to `127.0.0.1` by default. Click **Connect**, then explore the **Commands** and **Status** tabs.
### CLI flags
| Flag | Description |
|---|---|
| `--printheads N` | Number of printheads in the demo UI (default 10) |
| `--wire-debug` | Log raw wire bytes for both client and mock server |
## Architecture
### Three-port TCP client
The JI2 protocol uses three independent TCP sockets:
| Port | Name | Purpose |
|---|---|---|
| 9110 | Command | GS_, CN_, CF_ commands and responses |
| 9111 | Imaging | m0-m6 image masks, m2/m4 start/stop, n replies |
| 9112 | Status | KA_PING keepalive, EV_ events, status messaging |
`Xpl2Client` manages all three connections 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:
```cpp
{ "CN_JETTING_ALL_ON", { ResponseShape::JcSuccess, 2,
[](auto *s, const auto &p) {
emit s->jettingAllOnResult(p[0].toInt(), p[1].toInt() == 1);
} } },
```
## QML usage
```qml
import Xpl2
// Xpl2Client is a singleton — no instantiation needed
Button {
text: "Connect"
onClicked: {
Xpl2Client.host = "192.168.1.100"
Xpl2Client.connectToServer()
}
}
// 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 versions
- **`Xpl2PhStatus`** — 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) through `NozzleDriveDutyCycle` (-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
mock-jetting-controller/
MockServer.h/cpp # Canned responses, periodic status emission
main.cpp
CMakeLists.txt
docs/
protocol.pdf # Alchemie JI2-JC protocol specification
```
## Mock server
The mock server responds to all 56 protocol tokens with canned data. 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
|