aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 518acbc62cc82c60b2117d5fb36a19390c84b851 (plain)
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# 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 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

```bash
# 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:

```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.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 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
  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