aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-03-16 17:00:55 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-03-16 17:00:55 +0100
commit4c50201a2048b7ffcc1b7086c9617112cb540501 (patch)
tree06f3be45401fc20432f7ef7974d51c8bc7f143b8
parentf2251311a8523b6c24839ccd1e6834ef065c679f (diff)
downloadQtXpl2-4c50201a2048b7ffcc1b7086c9617112cb540501.tar.gz
QtXpl2-4c50201a2048b7ffcc1b7086c9617112cb540501.zip
README.md, docs/guide.md (FR) — full API documentation
-rw-r--r--README.md167
-rw-r--r--docs/guide.md480
2 files changed, 647 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f82cf68
--- /dev/null
+++ b/README.md
@@ -0,0 +1,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`) |
+| `QtXpl2Demo` | `bin/QtXpl2Demo` | Interactive demo app |
+| `Xpl2MockServer` | `bin/Xpl2MockServer` | Mock JI2 server for development |
+
+## Quick start
+
+```bash
+# Terminal 1 — start the mock server
+./build/bin/Xpl2MockServer
+
+# Terminal 2 — run the demo
+./build/bin/QtXpl2Demo
+```
+
+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
+ demo/
+ 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-server/
+ 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
diff --git a/docs/guide.md b/docs/guide.md
new file mode 100644
index 0000000..32ed2eb
--- /dev/null
+++ b/docs/guide.md
@@ -0,0 +1,480 @@
+# Guide d'utilisation QtXpl2
+
+Ce guide explique comment utiliser le module QML `Xpl2` pour piloter un Jetting Controller Alchemie depuis une interface Qt Quick.
+
+## Import et connexion
+
+```qml
+import Xpl2
+
+// Xpl2Client est un singleton — pas besoin de l'instancier.
+// Configurer l'adresse du contrôleur :
+Xpl2Client.host = "192.168.1.100"
+
+// Se connecter / se déconnecter :
+Xpl2Client.connectToServer()
+Xpl2Client.disconnectFromServer()
+```
+
+### Propriétés de connexion
+
+| Propriété | Type | Description |
+|---|---|---|
+| `host` | string | Adresse IP du Jetting Controller (défaut : `"127.0.0.1"`) |
+| `connected` | bool | `true` quand les 3 ports TCP sont connectés |
+| `controllerId` | int | ID du contrôleur (renseigné après `getJcVersion()`) |
+| `firmwareVersion` | string | Version firmware du contrôleur |
+| `hardwareVersion` | string | Version hardware du contrôleur |
+| `printheadCount` | int | Nombre de têtes connectées |
+
+### Signaux généraux
+
+| Signal | Description |
+|---|---|
+| `errorOccurred(string error)` | Erreur de connexion ou de commande |
+| `statusMessage(string message)` | Message de log pour chaque réponse reçue |
+| `shuttingDown()` | Le serveur est en train de s'éteindre |
+
+---
+
+## Informations version
+
+### Obtenir la version du contrôleur
+
+```qml
+Xpl2Client.getJcVersion()
+```
+
+La réponse met à jour les propriétés `controllerId`, `firmwareVersion`, `hardwareVersion` et `printheadCount`. Le signal `jcVersionReceived()` est émis.
+
+### Obtenir la version d'une tête
+
+```qml
+Xpl2Client.getPhVersion(3) // tête n°3
+```
+
+**Signal :** `phVersionReceived(controllerId, printheadId, mcuFirmwareVersion, mcuHardwareVersion, mcuFirmwareVariant, fpgaFirmwareVersion, fpgaHardwareVersion, bootloaderVersion)`
+
+```qml
+Connections {
+ target: Xpl2Client
+ function onPhVersionReceived(cid, phId, mcuFw, mcuHw, mcuVar, fpgaFw, fpgaHw, boot) {
+ console.log("Tête", phId, "— MCU:", mcuFw, "FPGA:", fpgaFw)
+ }
+}
+```
+
+---
+
+## Contrôle du jetting
+
+### Activer / désactiver le jetting
+
+```qml
+// Toutes les têtes
+Xpl2Client.jettingAllOn() // → signal jettingAllOnResult(cid, success)
+Xpl2Client.jettingOff() // → signal jettingOffResult(cid, success)
+
+// Avec un masque de buses (180 caractères, 12 par tête × 15 têtes)
+// Chaque caractère : 'F' = jet actif, '0' = jet inactif
+Xpl2Client.jettingOn("FFFF..." ) // → signal jettingOnResult(cid, success)
+
+// Une seule tête avec masque (12 caractères)
+Xpl2Client.phJettingOn(3, "FFFFFFFFFFFF") // → signal phJettingOnResult(cid, phId, success)
+Xpl2Client.phJettingOff(3) // → signal phJettingOffResult(cid, phId, success)
+```
+
+### LEDs d'identification
+
+```qml
+// LED du contrôleur
+Xpl2Client.jcIdLedOn() // → signal jcIdLedOnResult(cid, success)
+Xpl2Client.jcIdLedOff() // → signal jcIdLedOffResult(cid, success)
+
+// LED d'une tête
+Xpl2Client.phIdLedOn(3) // → signal phIdLedOnResult(cid, phId, success)
+Xpl2Client.phIdLedOff(3) // → signal phIdLedOffResult(cid, phId, success)
+```
+
+### Calibration
+
+```qml
+// Calibrer toutes les têtes
+Xpl2Client.jcCalibration() // → signal jcCalibrationResult(cid, success)
+
+// Calibrer une tête
+Xpl2Client.phCalibration(3) // → signal phCalibrationResult(cid, phId, success)
+
+// Récupérer les données de calibration (48 fréquences par tête, -1 = non calibré)
+Xpl2Client.phCalibrationData(3) // → signal phCalibrationDataReceived(cid, phId, frequencies)
+Xpl2Client.phCalibrationRawData(3) // → signal phCalibrationRawDataReceived(cid, phId, frequencies)
+
+// Fréquence de base calibrée
+Xpl2Client.phCalibratedBaseFrequency(3) // → signal phCalibratedBaseFrequencyReceived(cid, phId, baseFreq, activeBaseFreq)
+```
+
+### Codes de défaut
+
+```qml
+// Réinitialiser les codes de défaut
+Xpl2Client.jcResetFaultCodes() // → signal jcResetFaultCodesResult(cid, success)
+Xpl2Client.phResetFaultCodes(3) // → signal phResetFaultCodesResult(cid, phId, success)
+```
+
+### Désactivation de buses
+
+```qml
+// Masque de 12 caractères : '0' = buse active, autre = buse désactivée
+// "000000000000" réactive toutes les buses
+Xpl2Client.phNozzlesDisabled(3, "000000000000") // → signal phNozzlesDisabledResult(cid, phId, success)
+```
+
+---
+
+## Messages de statut
+
+Le contrôleur peut envoyer périodiquement des messages de statut détaillés.
+
+### Démarrer / arrêter les messages
+
+```qml
+// Niveau 1 = basique, niveau 2 = étendu
+// Intervalle en millisecondes
+Xpl2Client.jcStatusMessagingStart(1, 1000) // JC, niveau 1, toutes les secondes
+Xpl2Client.jcStatusMessagingStop()
+
+Xpl2Client.phStatusMessagingStart(2, 500) // Têtes, niveau 2, toutes les 500ms
+Xpl2Client.phStatusMessagingStop()
+```
+
+**Signaux de confirmation :**
+- `jcStatusMessagingStartResult(cid, statusLevel, sendIntervalMs, success)`
+- `jcStatusMessagingStopResult(cid, success)`
+- `phStatusMessagingStartResult(cid, statusLevel, sendIntervalMs, success)`
+- `phStatusMessagingStopResult(cid, success)`
+
+### Recevoir les statuts
+
+Les messages de statut arrivent sous forme d'objets structurés avec des propriétés nommées.
+
+```qml
+Connections {
+ target: Xpl2Client
+
+ function onJcStatusReceived(status) {
+ // status est un objet Xpl2JcStatus
+ tempLabel.text = status.temperature.toFixed(1) + "°C"
+ cpuLabel.text = status.cpuPercentageBusy.toFixed(1) + "%"
+ }
+
+ function onPhStatusReceived(status) {
+ // status est un objet Xpl2PhStatus
+ console.log("Tête", status.printheadId, "temp:", status.temperature)
+ }
+}
+```
+
+### Champs du statut JC (`Xpl2JcStatus`)
+
+#### Niveau 1
+
+| Propriété | Type | Description |
+|---|---|---|
+| `controllerId` | int | ID du contrôleur |
+| `statusLevel` | int | Niveau de statut (1 ou 2) |
+| `cpuPercentageBusy` | float | Charge CPU (%) |
+| `rail5V` | float | Tension rail 5V |
+| `railCanBus8V` | float | Tension bus CAN 8V |
+| `temperature` | float | Température (°C) |
+| `humidity` | float | Humidité (%) |
+| `busCurrent` | float | Courant bus (A) |
+| `onTimeSeconds` | int | Temps de fonctionnement (s) |
+
+#### Niveau 2 (inclut le niveau 1 +)
+
+| Propriété | Type | Description |
+|---|---|---|
+| `ipAddress` | string | Adresse IP |
+| `eFuseVoltage` | float | Tension eFuse (V) |
+| `eFuseBusEnabled` | bool | Bus eFuse activé |
+| `busPowerEnabled` | bool | Alimentation bus activée |
+| `busPowerOk` | bool | Alimentation bus OK |
+| `switchValue` | int | Valeur du commutateur |
+| `firmwareVersion` | string | Version firmware |
+| `hardwareVersion` | string | Version hardware |
+| `indicator0` .. `indicator5` | bool | Indicateurs 0 à 5 |
+
+### Champs du statut PH (`Xpl2PhStatus`)
+
+#### Niveau 1
+
+| Propriété | Type | Description |
+|---|---|---|
+| `controllerId` | int | ID du contrôleur |
+| `statusLevel` | int | Niveau de statut |
+| `printheadId` | int | ID de la tête |
+| `temperature` | float | Température (°C) |
+| `humidity` | float | Humidité (%) |
+| `mcuTemperature` | float | Température MCU (°C) |
+| `pdsVoltage` | float | Tension PDS (V) |
+| `mdsVoltage` | float | Tension MDS (V) |
+| `systemVoltage` | float | Tension système (V) |
+| `eFuseCurrent` | float | Courant eFuse (A) |
+| `nozzleCurrent` | float | Courant buses (A) |
+| `vdd` | float | Tension VDD (V) |
+| `temperatureTrip` | bool | Seuil température dépassé |
+| `pdsOverVoltageTrip` | bool | Surtension PDS |
+| `pdsUnderVoltageTrip` | bool | Sous-tension PDS |
+| `pdsSupplyErrorTrip` | bool | Erreur alimentation PDS |
+| `mdsOverVoltageTrip` | bool | Surtension MDS |
+| `mdsUnderVoltageTrip` | bool | Sous-tension MDS |
+| `supplyOverVoltageTrip` | bool | Surtension alimentation |
+| `supplyUnderVoltageTrip` | bool | Sous-tension alimentation |
+| `eFuseOverCurrentTrip` | bool | Surintensité eFuse |
+| `eFuseInputVoltageErrorTrip` | bool | Erreur tension entrée eFuse |
+| `eFuseFaultTrip` | bool | Défaut eFuse |
+| `flashFaultyTrip` | bool | Défaut flash |
+| `flashChecksumErrorTrip` | bool | Erreur checksum flash |
+| `dutyCycle` | float | Rapport cyclique (%) |
+| `pwmFrequency` | float | Fréquence PWM (Hz) |
+| `drive` | int | Drive |
+| `nozzleDriveFrequency` | float | Fréquence drive buses (Hz) |
+| `nozzleDriveDutyCycle` | float | Rapport cyclique drive buses (%) |
+| `onTimeSeconds` | int | Temps de fonctionnement (s) |
+
+#### Niveau 2 (inclut le niveau 1 +)
+
+| Propriété | Type | Description |
+|---|---|---|
+| `accelerometerId` | int | ID accéléromètre |
+| `mcuId` | string | ID MCU |
+| `flashMemoryId` | string | ID mémoire flash |
+| `temperatureSensorSerialNumber` | int | N° série capteur température |
+| `mcuHardwareVersion` | string | Version hardware MCU |
+| `mcuFirmwareVersion` | string | Version firmware MCU |
+| `mcuFirmwareVariant` | string | Variante firmware MCU |
+| `fpgaHardwareVersion` | string | Version hardware FPGA |
+| `fpgaFirmwareVersion` | string | Version firmware FPGA |
+| `bootloaderVersion` | string | Version bootloader |
+| `maxAllowedTemperature` | float | Température max autorisée (°C) |
+| `pdsVoltageMax` | float | Tension PDS max (V) |
+| `pdsVoltageMin` | float | Tension PDS min (V) |
+| `pdsVoltageSetting` | float | Réglage tension PDS (V) |
+| `mdsVoltageMax` | float | Tension MDS max (V) |
+| `mdsVoltageMin` | float | Tension MDS min (V) |
+| `eFuseCurrentMax` | float | Courant eFuse max (A) |
+| `measuredHardwareVersion` | string | Version hardware mesurée |
+| `gyroX`, `gyroY`, `gyroZ` | int | Gyroscope X/Y/Z |
+| `accelerationX`, `accelerationY`, `accelerationZ` | int | Accélération X/Y/Z |
+| `purge`, `purgeState`, `purgeDelay`, `purgeCounter` | int | Paramètres de purge |
+| `cleaningStartPeriod`, `cleaningEndPeriod`, `cleaningStepPeriod`, `cleaningPeriod` | int | Paramètres de nettoyage |
+
+---
+
+## Configuration
+
+### ID des têtes
+
+```qml
+Xpl2Client.phSetId(3) // → signal phSetIdResult(cid, phId, success)
+Xpl2Client.phDeassignId() // → signal phDeassignIdResult(cid, success)
+```
+
+### Paramètres de jetting
+
+```qml
+// Pour tout le contrôleur
+Xpl2Client.jcSetJettingParams(dutyCycle, pwmFreq, drive, nozzleDriveFreq, nozzleDriveDutyCycle)
+// → signal jcSetJettingParamsResult(cid, dutyCycle, pwmFreq, drive, nozzleDriveFreq, nozzleDriveDutyCycle, result)
+
+// Pour une tête
+Xpl2Client.phSetJettingParams(phId, dutyCycle, pwmFreq, drive, nozzleDriveFreq, nozzleDriveDutyCycle)
+// → signal phSetJettingParamsResult(cid, phId, dutyCycle, pwmFreq, drive, nozzleDriveFreq, nozzleDriveDutyCycle, result)
+
+// Lire les paramètres d'une tête
+Xpl2Client.phGetJettingParams(3)
+// → signal phGetJettingParamsResult(cid, phId, dutyCycle, pwmFreq, drive, nozzleDriveFreq, nozzleDriveDutyCycle, result)
+```
+
+Le paramètre `result` est un `JettingParamsResult` :
+
+| Valeur | Signification |
+|---|---|
+| `1` (Ok) | Succès |
+| `0` (Failed) | Échec |
+| `-1` (DutyCycle) | Rapport cyclique invalide |
+| `-2` (PwmFrequency) | Fréquence PWM invalide |
+| `-3` (Drive) | Drive invalide |
+| `-4` (NozzleDriveFrequency) | Fréquence drive buses invalide |
+| `-5` (NozzleDriveDutyCycle) | Rapport cyclique drive buses invalide |
+
+### Calibration
+
+```qml
+Xpl2Client.jcSaveCalibration() // → jcSaveCalibrationResult(cid, success)
+Xpl2Client.phSaveCalibration(3) // → phSaveCalibrationResult(cid, phId, success)
+Xpl2Client.jcResetCalibration() // → jcResetCalibrationResult(cid, success)
+Xpl2Client.phResetCalibration(3) // → phResetCalibrationResult(cid, phId, success)
+```
+
+### Purge
+
+```qml
+// Configurer : intervalle (ms) et durée de jet (ms)
+// La durée doit être inférieure à l'intervalle
+Xpl2Client.jcSetPurgeSettings(5000, 200) // → jcSetPurgeSettingsResult(cid, interval, time, success)
+Xpl2Client.jcSwitchOffPurge() // → jcSwitchOffPurgeResult(cid, success)
+```
+
+### Setter / Getter générique
+
+Permet de lire ou écrire n'importe quel paramètre par son ID (voir Appendice D du protocole).
+
+```qml
+// Écrire un paramètre du contrôleur
+// saveNewValue : true pour sauvegarder en mémoire permanente
+Xpl2Client.jcSetter(true, 4, "35.0") // setter ID 4 = température
+// → signal jcSetterResult(cid, saveNewValue, setterId, newValue, result)
+
+// Écrire un paramètre d'une tête
+Xpl2Client.phSetter(3, false, 23, "50.0") // tête 3, setter ID 23 = duty cycle
+// → signal phSetterResult(cid, phId, saveNewValue, setterId, newValue, result)
+
+// Lire un paramètre du contrôleur
+// getSavedValue : true pour la valeur sauvegardée, false pour la valeur courante
+Xpl2Client.jcGetter(false, 4)
+// → signal jcGetterResult(cid, savedValue, getterId, currentValue, success)
+
+// Lire un paramètre d'une tête
+Xpl2Client.phGetter(3, false, 1)
+// → signal phGetterResult(cid, phId, savedValue, getterId, currentValue, success)
+```
+
+Le `result` des setters est un `SetterResult` :
+
+| Valeur | Signification |
+|---|---|
+| `1` (Ok) | Succès |
+| `0` (Failed) | Échec |
+| `-1` (IncorrectNewValue) | Valeur incorrecte |
+
+### Sauvegarde et réinitialisation
+
+```qml
+// Sauvegarder tous les réglages
+Xpl2Client.jcSaveAllPrintheadSettings() // → jcSaveAllPrintheadSettingsResult(cid, success)
+Xpl2Client.phSaveSettings(3) // → phSaveSettingsResult(cid, phId, success)
+
+// Réinitialiser les réglages
+Xpl2Client.jcResetSettingsAllPrintheads() // → jcResetSettingsAllPrintheadsResult(cid, success)
+Xpl2Client.phResetAllSettings(3) // → phResetAllSettingsResult(cid, phId, success)
+```
+
+### Redémarrage et arrêt
+
+```qml
+Xpl2Client.jcRebootAllPrintheads() // → jcRebootAllPrintheadsResult(cid, success)
+Xpl2Client.phReboot(3) // → phRebootResult(cid, phId, success)
+Xpl2Client.jcResetControllerSoftware() // → jcResetControllerSoftwareResult(cid, success)
+Xpl2Client.jcRestart() // → jcRestartResult(cid, success)
+Xpl2Client.jcShutdown() // → jcShutdownResult(cid, success)
+```
+
+---
+
+## Imaging
+
+Les commandes d'imaging sont envoyées sur le port 2 (9111).
+
+### Démarrer / arrêter l'impression
+
+```qml
+Xpl2Client.imagingStart(1.0) // vitesse d'impression
+// → signal imagingReply(imageLines) — nombre de lignes d'image à envoyer
+
+Xpl2Client.imagingStop()
+// → signal imagingStopResult(success)
+```
+
+### Masques d'image
+
+Les masques définissent quelles buses sont actives pour chaque ligne d'image. Le masque fait 12 caractères par tête (180 caractères pour 15 têtes).
+
+```qml
+// Envoyer un masque : m0 démarre, m1 termine et déclenche l'envoi
+Xpl2Client.imageMaskStart("FFFFFFFFFFFF...") // 180 caractères
+Xpl2Client.imageMaskEnd("fin_du_message")
+// → signal imagingReply(imageLines)
+```
+
+### Masques de rapport cyclique
+
+Même principe avec 2 caractères hexadécimaux par tête.
+
+```qml
+Xpl2Client.dutyCycleMaskStart("FF...")
+Xpl2Client.dutyCycleMaskEnd("fin")
+// → signal imagingReply(imageLines)
+```
+
+### Compteur d'images
+
+Vérifie le nombre de lignes d'image restantes. Utilisé avant d'arrêter l'impression pour s'assurer que toutes les lignes ont été envoyées.
+
+```qml
+Xpl2Client.imageCount()
+// → signal imagingReply(imageLines) — 0 signifie que tout est envoyé
+```
+
+---
+
+## Événements serveur
+
+Ces signaux sont émis automatiquement quand le serveur envoie des événements.
+
+### Erreurs
+
+```qml
+Connections {
+ target: Xpl2Client
+
+ // Erreur du contrôleur (codes 3000+)
+ function onJcErrorCode(controllerId, errorCode, params) {
+ console.log("Erreur JC:", errorCode, params)
+ }
+
+ // Erreur d'une tête (codes 4000+)
+ function onPhErrorCode(controllerId, printheadId, errorCode, params) {
+ console.log("Erreur tête", printheadId, ":", errorCode)
+ }
+
+ // Erreur d'imaging (codes 5000+)
+ function onImgErrorCode(controllerId, errorCode, params) { }
+
+ // Erreur de statut (codes 6000+)
+ function onStatusErrorCode(controllerId, errorCode, params) { }
+}
+```
+
+### Connexion des têtes
+
+```qml
+Connections {
+ target: Xpl2Client
+
+ function onPhConnectionChanged(controllerId, printheadId, connected) {
+ console.log("Tête", printheadId, connected ? "connectée" : "déconnectée")
+ }
+}
+```
+
+### Arrêt du serveur
+
+```qml
+Connections {
+ target: Xpl2Client
+ function onShuttingDown() {
+ console.log("Le serveur s'éteint")
+ }
+}
+```