> For the complete documentation index, see [llms.txt](https://mapir.gitbook.io/chloros/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mapir.gitbook.io/chloros/reference-cli-and-sdk/cli-reference.md).

# Chloros CLI Reference

**Version:** 1.2.0 **Generated:** 2026-07-29 19:19 · **Revised:** 2026-08-30 **Audience:** Optimised for LLM consumption; human-readable. **Scope:** Every user-facing subcommand of `chloros-cli`, with options and copy-pasteable examples.

This document is the complete reference for the `chloros-cli` command-line tool that ships with MAPIR Chloros. It is intentionally exhaustive so that an LLM (or human) can compose any supported workflow from the listings below without inspecting the source code.

If you only need the highlights, jump to:

* [Five-Minute Quickstart](#five-minute-quickstart)
* [LATTICE Camera First-Connect Workflow](#lattice-camera-first-connect-workflow)
* [DAQ Sensor First-Connect Workflow](#daq-sensor-first-connect-workflow)
* [Smart-AE / Smart-Capture](#smart-ae--smart-capture)
* [Capture Modes, Recorders & Offline Reprocess](#capture-modes-recorders--offline-reprocess)

***

## Conventions

* All commands are prefixed `chloros-cli`. On Windows the binary is `chloros-cli.exe`; on Linux/Jetson it is `chloros-cli`.
* Optional arguments are shown as `--flag`. Required positional arguments are shown without brackets.
* Where a default is given, omitting the flag uses that value.
* The CLI is a thin HTTP client over the Chloros backend (Flask server on `127.0.0.1:5000`). The backend is auto-started by most commands. `CHLOROS_BACKEND_URL=<url>` points the **`lattice`**, **`project`**, and **`daq pool-*`** command families at a remote backend — the core commands (`process`, `login`, `logout`, `status`, `export-status`, `time-sync`, `selftest`) deliberately pin `http://127.0.0.1:<port>` and ignore it (the IPv4 literal avoids Windows' `localhost`→`::1` \~2 s-per-request penalty). See [Environment Variables](#environment-variables).
* A Chloros+ account login is required for all SDK/CLI calls (run `chloros-cli login` once per machine; cached in `~/.chloros/`).
* Examples use Linux paths; on Windows replace `/home/user/...` with `C:/Users/.../...`.

***

## Top-Level Synopsis

```
chloros-cli [global options] COMMAND [command options]
```

### Global Options

| Flag                 | Description                                                        |
| -------------------- | ------------------------------------------------------------------ |
| `--backend-exe PATH` | Override the auto-detected backend executable.                     |
| `--port N`           | Backend HTTP port (default: `5000`).                               |
| `-v, --verbose`      | Enable verbose output.                                             |
| `--restart`          | Force-restart the backend (kills any running `backend_server.py`). |
| `--version`          | Print version (`Chloros CLI 1.2.0`).                               |
| `--help`             | Show top-level help.                                               |

### Commands Index

| Command                                                                                                                                                  | Purpose                                                     |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| [`process`](#chloros-cli-process)                                                                                                                        | Process a folder of Survey3 or LATTICE captures end-to-end. |
| [`login`](#chloros-cli-login)                                                                                                                            | Authenticate this machine with a Chloros+ account.          |
| [`logout`](#chloros-cli-logout)                                                                                                                          | Clear cached credentials.                                   |
| [`status`](#chloros-cli-status)                                                                                                                          | Show current license / authentication status.               |
| [`export-status`](#chloros-cli-export-status)                                                                                                            | Live Thread-4 export progress during a `process` run.       |
| [`language`](#chloros-cli-language)                                                                                                                      | Set or list CLI display language (38 supported).            |
| [`set-project-folder`](#project-folder-commands) / [`get-project-folder`](#project-folder-commands) / [`reset-project-folder`](#project-folder-commands) | Default project folder (shared with the GUI).               |
| [`update`](#chloros-cli-update)                                                                                                                          | Check for and install CLI updates (Linux/Jetson).           |
| [`selftest`](#chloros-cli-selftest)                                                                                                                      | System diagnostics + smoke tests.                           |
| [`time-sync`](#chloros-cli-time-sync)                                                                                                                    | PTP grandmaster status / control.                           |
| [`lattice`](#chloros-cli-lattice)                                                                                                                        | LATTICE camera control & capture (45+ subcommands).         |
| [`daq`](#chloros-cli-daq)                                                                                                                                | DAQ spectral-sensor control (DAQ-U / DAQ-M / DAQ-E).        |
| [`project`](#chloros-cli-project)                                                                                                                        | Open and drive a saved Chloros project (cameras + DAQs).    |

***

## Installation

`chloros-cli` ships inside the Chloros desktop installer on every supported platform — there is no separate CLI download. Installing the platform package adds `chloros-cli` to your `PATH` alongside the desktop app and the backend binary it drives.

Latest downloads: [`https://mapir.gitbook.io/chloros/download`](https://mapir.gitbook.io/chloros/download)

> The installer also ships convenience launcher scripts (`Chloros_CLI.bat` / `Chloros_CLI.ps1`, `Launch_CLI.*`, `chloros-cli.sh`) that open a ready-to-use CLI shell; they are covered in the [CLI User Guide](/chloros/cli.md) and not duplicated here.

### Windows (.exe)

1. Download the Windows installer from the download page.
2. Run `Chloros-Setup-x.y.z.exe` and follow the wizard. Default install path is `C:\Program Files\Chloros\` (the CLI lands in `C:\Program Files\Chloros\cli\`, which the installer adds to PATH).
3. Open a new terminal (`cmd.exe`, PowerShell, or Windows Terminal) so the updated `PATH` is picked up.

```powershell
chloros-cli --version
```

The installer automatically adds `chloros-cli.exe` to your system `PATH` and bundles the Arena SDK runtime needed for LATTICE cameras.

### Linux amd64 (.deb)

For Ubuntu 22.04 LTS or newer / Debian-based x86\_64 workstations.

> **Ubuntu 20.04 is not supported.** The package's dependency list is derived from what the backend actually links against, and that includes `libc6 (>= 2.34)`; focal ships glibc 2.31. `apt` refuses the install rather than letting it fail at runtime.

```bash
sudo dpkg -i chloros-amd64.deb
sudo apt-get install -f         # only if dpkg reports missing dependencies
chloros-cli --version
```

The .deb installs:

* `chloros-cli` to `/usr/bin/chloros-cli`
* The compiled backend to `/usr/lib/chloros/chloros-backend`
* The Arena SDK runtime (for LATTICE cameras)
* Denoiser models, calibration bundles, and update-channel config

### Linux arm64 — Jetson (JetPack 6)

```bash
sudo dpkg -i chloros-arm64-jp6.deb
sudo apt-get install -f
chloros-cli --version
```

Same layout as the amd64 .deb, with a CUDA build tuned for Jetson Orin / Orin NX / Orin Nano.

### Authenticate Once Per Machine

Every platform requires a one-time Chloros+ login before SDK/CLI calls work:

```bash
chloros-cli login user@example.com 'YourPassword'
```

Credentials are cached in `~/.chloros/user_session.json`.

### Verify the Install

```bash
chloros-cli --version           # prints "Chloros CLI 1.2.0"
chloros-cli selftest            # full 7-step diagnostic (backend, GPU, models, CUDA)
chloros-cli status              # shows license tier + logged-in user
```

> **Chloros+ subscription required.** The CLI requires an active Chloros+ plan. **Copper** is the entry Chloros+ tier — every paid Chloros+ tier has CLI/SDK access; only the free **Iron** tier does not. (Plan-id map: `0`=Iron/free, `1`=Copper, `2`=Bronze, `3`=Silver, `4`=Gold.) Upgrade at [`https://cloud.mapir.camera/pricing`](https://cloud.mapir.camera/pricing).
>
> This floor is enforced by the backend, not just by the CLI: an SDK/CLI-flagged request without a paid plan is rejected with `403 PLAN_UPGRADE_REQUIRED`, whether it comes from `chloros-cli`, the Python SDK, or a hand-rolled HTTP client. A logged-out caller gets `401 AUTH_REQUIRED` instead. Access works offline for the plan's grace period (30 days monthly, to expiry for yearly) and stops when that lapses; `chloros-cli status` keeps working so the reason is visible (it is the one SDK/CLI route exempt from the tier gate — `GET /api/license-status`).

***

## Five-Minute Quickstart

```bash
# 1. Sign in once on this machine
chloros-cli login user@example.com 'YourPassword'

# 2. Survey3 / LATTICE folder → finished radiance + NDVI in one call
chloros-cli process "/home/user/captures/flight_001" \
  --vignette --reflectance --indices NDVI NDRE GNDVI

# 3. Take a single LATTICE photo with the first camera found
chloros-cli lattice capture -o output/

# 4. Connect a 4-cam LATTICE array with the GUI's smart-prep flow
chloros-cli lattice array-connect \
  --serials 213800234,214000533,214701288,214701292

# 5. Read a spectrum from a connected DAQ-U
chloros-cli daq pool-connect --port COM3
chloros-cli daq pool-latest --sensor-id CB-7C-A8-2E-5F   # id from 'daq pool-list'
```

***

## `chloros-cli process`

Process a folder of images through the full Chloros pipeline (target detection → calibration → vignette → reflectance → index export).

### Synopsis

```
chloros-cli process INPUT [OPTIONS]
```

### Positional Arguments

| Argument | Description                                                                                       |
| -------- | ------------------------------------------------------------------------------------------------- |
| `INPUT`  | Path to input folder containing `.raw + .jpg` (Survey3), `.tif/.tiff` (LATTICE), or `.dng` files. |

### Common Options

| Flag                                                 | Default                                                                                           | Description                                                                                                                                                                                                                                                                                                  |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `-o, --output PATH`                                  | a new timestamped folder under your default project path (`~/Chloros Projects` unless configured) | Project folder to create or reuse. If the folder already holds a `project.json`, a `_1`/`_2` sibling is created instead of overwriting.                                                                                                                                                                      |
| `-n, --project-name NAME`                            | auto (timestamp)                                                                                  | Project name.                                                                                                                                                                                                                                                                                                |
| `--debayer {standard,texture-aware}`                 | `standard`                                                                                        | `texture-aware` uses a Chloros+ neural debayer; slower but higher quality.                                                                                                                                                                                                                                   |
| `--vignette / --no-vignette`                         | `--vignette`                                                                                      | Vignette correction.                                                                                                                                                                                                                                                                                         |
| `--reflectance / --no-reflectance`                   | `--reflectance`                                                                                   | Reflectance calibration (uses panel target if found, NIST per-serial cal for LATTICE). For LATTICE multispectral this doubles as the reflectance **product** toggle — see [Per-Product Export Toggles](#per-product-export-toggles-lattice-multispectral).                                                   |
| `--ppk`                                              | off                                                                                               | Apply PPK GNSS corrections from sidecar files.                                                                                                                                                                                                                                                               |
| `--exposure-pin-1 MODEL`                             | off                                                                                               | Pin a Survey3 dual-camera rig's "pin-1" model.                                                                                                                                                                                                                                                               |
| `--exposure-pin-2 MODEL`                             | off                                                                                               | Pin the "pin-2" model.                                                                                                                                                                                                                                                                                       |
| `--recal-interval SECONDS`                           | 0                                                                                                 | Force re-running calibration math every N seconds of capture-time.                                                                                                                                                                                                                                           |
| `--timezone-offset HOURS`                            | local                                                                                             | Override timezone offset baked into output metadata.                                                                                                                                                                                                                                                         |
| `--format FORMAT`                                    | `TIFF (16-bit)`                                                                                   | One of `TIFF (16-bit)`, `TIFF (32-bit, Percent)`, `PNG (8-bit)`, `JPG (8-bit)`.                                                                                                                                                                                                                              |
| `--indices NAME [NAME ...]`                          | none                                                                                              | Vegetation indices (`NDVI`, `NDRE`, `GNDVI`, `EVI`, `SAVI`, `OSAVI`, `CIG`, …).                                                                                                                                                                                                                              |
| `--input-level {auto,raw,debayered,processed}`       | `auto`                                                                                            | Force the pipeline entry point for LATTICE TIFFs (Survey3 .raw is unaffected). Also the escape hatch that lets a capture with **no raw** be processed at all — see [What a captures folder looks like](#what-a-captures-folder-looks-like).                                                                  |
| `--debayered / --no-debayered`                       | on                                                                                                | Emit the linear debayered product (`Debayered_Images`). See [Per-Product Export Toggles](#per-product-export-toggles-lattice-multispectral).                                                                                                                                                                 |
| `--preview / --no-preview`                           | on                                                                                                | Emit the display preview (`Preview_Images`): RGB = white balance (DAQ-illuminant when available, else gray-world) + gamma; multispec = false-colour stretch.                                                                                                                                                 |
| `--radiance / --no-radiance`                         | on                                                                                                | Emit float32 radiance (`Radiance_Images`, W/m²/sr/nm).                                                                                                                                                                                                                                                       |
| `--reflectance-source {daq,target,auto}`             | `auto`                                                                                            | Reference for the LATTICE reflectance product: `auto` = QA-passing in-frame target is the absolute reference, DAQ-downwelling (ρ = π·L/E) fallback; `target` = strict (no DAQ substitution); `daq` = DAQ-authoritative. See [Per-Product Export Toggles](#per-product-export-toggles-lattice-multispectral). |
| `--target-reflectance-dir DIR`                       | none                                                                                              | Directory of per-unit **measured** target reflectance scans (`<serial>.csv`); falls back to the nominal T3/T4P spectra on a miss.                                                                                                                                                                            |
| `--array-alignment / --no-array-alignment`           | on                                                                                                | LATTICE arrays: apply the module-to-module alignment stamped in each capture's `Chloros:Alignment*` XMP to every processed product (debayered / preview / radiance / reflectance / index). No-op for images without the tags.                                                                                |
| `--array-alignment-crop / --no-array-alignment-crop` | crop                                                                                              | Crop aligned exports to the array's common-overlap region so all modules share one footprint; `--no-…` keeps the full sensor canvas (black fill outside the source).                                                                                                                                         |
| `--array-alignment-interp {bilinear,nearest,cubic}`  | `bilinear`                                                                                        | Resampling for the alignment warp. `nearest` preserves exact source DNs (no inter-pixel mixing of radiometric values).                                                                                                                                                                                       |

### Target-Detection Options

| Flag                        | Description                                                          |
| --------------------------- | -------------------------------------------------------------------- |
| `--min-target-size PIXELS`  | Minimum panel-target size (px) for detector.                         |
| `--target-clustering 0-100` | Clustering sensitivity.                                              |
| `--target / --targets`      | Treat the input folder as target-panel-only (skip survey detection). |

### Examples

```bash
# Simplest: defaults are good for most surveys
chloros-cli process "/home/user/images/survey_001"

# Multi-index with explicit format
chloros-cli process "/home/user/images/survey_001" \
  --vignette \
  --reflectance \
  --format "TIFF (32-bit, Percent)" \
  --indices NDVI NDRE GNDVI OSAVI

# Texture-aware debayer for highest quality (Chloros+ only)
chloros-cli process "/home/user/images/survey_001" \
  --debayer texture-aware \
  --indices NDVI

# Process LATTICE captures explicitly (auto-detects from EXIF normally)
chloros-cli process "/home/user/captures/lattice_flight" \
  --input-level processed

# LATTICE multispectral → float32 radiance only (no DAQ downwelling needed)
chloros-cli process "/home/user/captures/lattice_flight" \
  --no-debayered --no-preview --no-reflectance

# LATTICE reflectance anchored to an in-frame target (strict, no DAQ fallback),
# with per-unit measured target scans looked up by serial
chloros-cli process "/home/user/captures/lattice_flight" \
  --reflectance-source target --target-reflectance-dir "/home/user/target_scans"

# LATTICE array capture: keep native geometry (ignore stamped alignment)
chloros-cli process "/home/user/captures/array_flight" \
  --no-array-alignment

# Aligned, uncropped, value-preserving resampling
chloros-cli process "/home/user/captures/array_flight" \
  --no-array-alignment-crop --array-alignment-interp nearest

# Save to a custom output location with a project name
chloros-cli process "C:/input" -o "C:/output" -n "Field_A_2026-05-26"
```

### Per-Product Export Toggles (LATTICE multispectral)

LATTICE processing fans out into **every applicable product in one pass**. The four per-type toggles — `--debayered`, `--preview`, `--radiance`, `--reflectance` — are all **ON by default**; use the `--no-<type>` form to drop one. RGB master cams only ever emit debayered + preview (no per-band radiance/reflectance), so `--radiance`/`--reflectance` are no-ops for them. The toggles are ignored for Survey3 `.raw` (which follows the standard reflectance/target path). *(The old `--radiometric-output {reflectance,radiance,sensor-response}` flag was **removed** and replaced by these toggles; there is no `sensor-response` level anymore.)*

| Product         | Output                                                                                  | DAQ downwelling needed?                                              |
| --------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `--debayered`   | Linear demosaic (`Debayered_Images`).                                                   | No.                                                                  |
| `--preview`     | Display preview (`Preview_Images`): RGB = WB + gamma; multispec = false-colour stretch. | No.                                                                  |
| `--radiance`    | float32 W/m²/sr/nm from the full radiometric chain (`Radiance_Images`).                 | No.                                                                  |
| `--reflectance` | uint16 reflectance ρ (`32768` = 1.0), Pix4D-ready.                                      | **Yes**, unless a QA-passing in-frame target anchors it (see below). |

`--reflectance-source` picks the reflectance reference: **`auto`** (default) makes a QA-passing in-frame target the **absolute reference** — the target-anchored empirical-line chains are cross-scored on held-out panels and the measured winner applied — falling back to the DAQ downwelling divide (ρ = π·L/E) when no target is present or QA fails; **`target`** is strict (no DAQ substitution); **`daq`** opts out to the DAQ-authoritative behaviour. Target geometry (ArUco / fixed-ROI / strip) comes from the project target config; `--target-reflectance-dir DIR` holds per-unit **measured** scans (`<serial>.csv`) looked up by the target unit's serial/QR, with the nominal T3/T4P spectra as the fallback.

The DAQ reflectance path resolves the **timestamp-matched downwelling** automatically from a recorded **`.daq`** (DAQ-U/M/E) **or a DAQ-M native `.csv`** found alongside the imagery. If a per-camera or DAQ calibration bundle isn't cached locally, the pipeline **auto-fetches it from AWS** on first use (requires internet once; cached under `~/.chloros/`).

#### Reading reflectance pixels (Pix4D / Metashape / your own scripts)

Reflectance is stored as integer DN, and **the DN that means ρ = 1.0 depends on the source camera**:

| Source              | ρ = 1.0 is                  | How to tell                                             |
| ------------------- | --------------------------- | ------------------------------------------------------- |
| LATTICE (M3C / M3M) | `32768` (headroom to ρ 2.0) | XMP `Chloros:PixelScale=32768` is stamped on the file.  |
| Survey3             | `65535` (clipped at ρ 1.0)  | No `Chloros:*` XMP tags — that absence *is* the signal. |

**Read `Chloros:PixelScale` and divide by it** rather than assuming a constant. The tag is defined in the uint16 domain, so it stays `32768` across output formats that rescale — `TIFF (16-bit)`, `PNG (8-bit)`, `JPG (8-bit)` and `TIFF (32-bit, Percent)` are all self-describing (normalise the stored dtype back to uint16 first: ×257 from 8-bit, ×65535 from float).

> **One case carries no scale, by design.** When an 8-bit-source capture (BayerRG8) is written as 8-bit TIFF, the pipeline *clips* to 0..255 instead of rescaling, so every value above ρ≈0.008 flattens to 255 and no scale describes the file. Chloros deliberately omits both `Chloros:PixelScale` and the `MicaSense:RadiometricCalibration` tuple there, and logs why. **If the tag is absent on a LATTICE reflectance file, do not assume a scale — re-export at 16-bit or 32-bit** rather than dividing pixels that were never divisible.

#### EXIF carried through to the export

`process` copies the source capture's **GPS block and its ExifIFD** onto every product, so an export carries `FocalLength`, `FNumber`, `ExposureTime`, `ISO`, `DateTimeOriginal` and `CameraSerialNumber` alongside the georeferencing.

**`FocalLength` is not optional for photogrammetry.** Pix4D solves ground sample distance from focal length plus altitude; with the tag absent it falls back to a wildly wrong scale. On one 49-capture orange-grove flight the missing tag turned a 411 m × 160 m site into a reconstructed 47.8 km × 13 km one — a 455 MP ortho of mostly nodata, which then read as a tiling problem and a BigTIFF problem before anyone checked the GSD. If your ortho comes out at an implausible scale, run `exiftool -FocalLength` over the exported product first.

The copy is deliberately **not** `-all:all`: IFD0's structural tags break LATTICE output when copied, and `ExifImageWidth` / `ExifImageHeight` are excluded because they describe the *source* capture — an export that was ever resized would otherwise carry dimensions contradicting its own raster. XMP is written directly rather than copied, because ExifTool discards same-invocation XMP tags when the XMP block is copied (which would drop the MAPIR calibration tags).

### Where the outputs land

Products are written **under the project folder, grouped by camera and then by file format**:

```
<project>/
└── LATT-M3M-L41-F550/                  # one folder per camera model+lens+filter
    ├── tiff16/
    │   ├── Reflectance_Calibrated_Images/
    │   ├── Debayered_Images/
    │   ├── Preview_Images/
    │   └── <INDEX>_Index_Images/        # e.g. NDVI_Index_Images
    └── tiff32/
        └── Radiance_Images/             # float32 radiance always lands here
```

The camera folder is `LATT-<sensor>-<lens>-F<filter>` for LATTICE (matching the capture's EXIF `Model`) and `<model>_<filter>` for Survey3 — two cameras sharing a sensor and filter but differing in lens keep separate trees, because vignette, field of view and distortion differ. The format folder follows `--format`: `tiff16`, `tiff8`, `png8`, `jpg8`, or `tiff32` for `TIFF (32-bit, Percent)`.

> **Every exported product keeps the SOURCE file's name.** A radiance export of `capture_…_raw.tif` is still called `capture_…_raw.tif` — it just lives in `tiff32/Radiance_Images/`. **The folder identifies the product, not the filename**, so globbing for `*radiance*.tif` finds nothing; match on the directory instead.

### Light-sensor recordings — calibrated `.daq` + `.csv`

`process` also handles the `.daq` recordings in your input folder, and it does **not** need any imagery to do it: a DAQ-U / DAQ-M / DAQ-E flown on its own is a complete capture, and a folder holding only `.daq` files is a valid input.

A DAQ can be recorded **without** its calibration — that is what the public [`chloros_scripts`](https://github.com/mapircamera/chloros_scripts) recorders (`record_daq.py`) do by default: they write raw sensor counts and stamp the file so Chloros fetches that sensor's factory calibration **by serial** (local cache first, then MAPIR Cloud) and applies it. `process` writes the result back out:

```
<project>/
└── Light Sensor/
    ├── <name>_calibrated.daq        # reprocessable archive, declares its bundle
    └── <name>_calibrated.csv        # W/m^2/nm per reading + photometric columns
```

The `.csv` carries one row per reading: UTC timestamp, integration time, total power, photopic/scotopic lux, PPFD (and its blue/green/red split), peak wavelength, then the full spectrum on the sensor's own wavelength grid. The `.daq` re-imports without being calibrated a second time.

On success the run reports `Light-sensor products written: N (calibrated .daq + .csv)`. The parenthetical describes what was actually written, so it reads `(RAW COUNTS — this sensor has no calibration bundle)` for a bundle-less sensor and `(N calibrated, M raw counts)` for a folder holding both. The backend's own `[DAQ-EXPORT]` and `[RUN-SUMMARY]` headlines derive their wording the same way — none of the three can call a raw export calibrated.

A DAQ-U / DAQ-M / DAQ-E recording whose calibration bundle cannot be fetched — you are offline, or that sensor has no calibration on file — is **skipped with a reason** on a `[DAQ-EXPORT]` line, never written out as a "calibrated" file holding raw counts. Connect to the internet and re-run. The reason is the one the reader actually established for that file (unreadable schema, no bundle, a write error), and the run summary lists **distinct** reasons — twenty files skipped for one cause read as one cause, not twenty repetitions of it.

#### DAQ-A recordings export as raw counts

The **DAQ-A** family predates the per-serial bundle system and has no calibration bundle to fetch — it is calibrated in the field against a reflectance target instead, which is why it never needed one. Refusing those recordings left them with no way to get their numbers out at all, so they export under a **different name**:

```
<project>/
└── Light Sensor/
    ├── <name>_raw.daq        # NOT _calibrated
    └── <name>_raw.csv        # raw spectral sensor counts, NOT irradiance
```

A different filename rather than a flag inside the file, because the claim has to survive being emailed on as a bare name. The `.csv` header states `raw spectral sensor counts (NOT irradiance)` and warns that the values are comparable **within** the file — which is exactly what target-based calibration uses them for — and not across sensors. The power-dependent photometric columns (total power, photopic and scotopic lux, PPFD) are written **NULL** rather than integrated from counts, and the run summary says `RAW COUNTS` so "exported" in a log cannot be read as irradiance.

Legacy **v1.01 / v1.02** recordings (a DAQ-A-SD writes these) carry no per-reading epoch, only the file's write time. The image↔downwelling matcher still refuses them — matching a frame against a write time would be wrong invisibly — but the exporter reads them, and the CSV prints `clock=daq_created_on` so the product states which clock it is on.

### Notes

* `process` automatically detects whether your folder is Survey3, LATTICE, or mixed.
* Progress streams over Server-Sent Events; the CLI shows live per-thread progress (Detecting, Analyzing, Processing, Exporting).
* For Linux/Jetson, the CLI checks swap and may warn before processing large folders. Texture-aware debayer also auto-applies a GPU frequency cap on low-power Jetsons (Nano, Orin Nano).
* On success the run reports how many image products it wrote (`Image products written: N`).

#### A run that writes no images fails

If you asked for products and the run wrote **none** — only `project.json` and `calibration_data.json` — `process` treats that as a failure: it prints `Processing finished but wrote no image products.` and **exits non-zero**, so a script can detect it. The message names the project folder and the usual causes:

* the input folder wasn't recognised as a capture (check the layout and `--input-level`), or
* every requested product was skipped as inapplicable for those cameras (e.g. asking for radiance/reflectance from RGB-only cameras).

Re-run with `--verbose` and check the backend log for `[LATTICE-EXPORT]` / `[EXPORT-CHECK]` lines, which explain per-camera skips that don't otherwise reach the CLI's output.

A deliberate metadata-only run — every product toggle off and no `--indices` — is still a **success**, because an empty image output is the correct result there.

So is a **light-sensor-only run**: a folder of `.daq` recordings has no imagery to export by definition, and the run is judged on the calibrated `.daq` / `.csv` it wrote instead.

***

## `chloros-cli login`

Authenticate this machine with a Chloros+ cloud account. Credentials are cached securely in `~/.chloros/user_session.json`.

```
chloros-cli login EMAIL PASSWORD
```

### Examples

```bash
chloros-cli login user@example.com 'YourPassword'

# Passwords containing $ should use SINGLE quotes
chloros-cli login user@example.com 'my$ecret$pass'
```

> **PowerShell `$$` mangling is auto-corrected.** In double quotes PowerShell expands `$$` (stripping it from, or duplicating parts of, the password). On a 401 the CLI automatically retries with `$$` re-appended, then with a de-duplicated half of the password; if a retry succeeds it logs you in and prints the correct single-quote syntax to use next time.

> **Headless/scripted use: no cached session means an interactive prompt, not a fast failure.** Any backend-spawning command (`process`, `status`, `export-status`, `time-sync`, …) run without a cached license/session drops into an interactive `Email:` / `Password:` prompt on stdin before proceeding. An unattended job with no cached session will therefore hang waiting for input — run `chloros-cli login EMAIL PASSWORD` once per machine before scheduling headless work.

***

## `chloros-cli logout`

Clears the cached session and forces a fresh login on the next call.

```bash
chloros-cli logout
```

***

## `chloros-cli status`

Show the current license tier (Iron/Copper/Bronze/Silver/Gold), authenticated user, and device-binding count.

```bash
chloros-cli status
```

***

## `chloros-cli export-status`

Poll the live Thread-4 export progress. Safe to call **during** a `process` run from another shell.

```bash
chloros-cli export-status
```

***

## `chloros-cli language`

Set the CLI's display language (38 supported, including CJK, RTL, and Indic). Falls back gracefully to English on legacy consoles that can't render the script.

```
chloros-cli language [LANG_CODE] [--list]
```

### Examples

```bash
# List all available languages
chloros-cli language --list

# Switch to Spanish
chloros-cli language es

# Show the currently-active language
chloros-cli language
```

***

## Project Folder Commands

These manage the default project folder location (shared with the GUI).

```bash
chloros-cli set-project-folder "/home/user/Chloros Projects"
chloros-cli get-project-folder
chloros-cli reset-project-folder
```

***

## `chloros-cli update`

Linux / Jetson only. Checks `version_url` from `/etc/chloros/update.conf` and offers to download + install the matching `.deb`.

```bash
chloros-cli update            # check + install
chloros-cli update --check    # check only
```

On Linux/Jetson the CLI also runs an **automatic update check on every startup** (non-blocking, never delays the command): it reads `/etc/chloros/update.conf`, caches the result for 1 hour in `~/.chloros/update_cache.json`, and prints `Update available: vX.Y.Z / Run: chloros-cli update` when a newer version exists. Silently skipped on any error and on Windows.

***

## `chloros-cli selftest`

Runs a 7-step smoke test: version, port availability, backend startup, `/api/test`, `/api/system-info` (GPU/CUDA/PyTorch), denoiser model presence, CUDA+denoiser readiness.

```bash
chloros-cli selftest
```

***

## `chloros-cli time-sync`

PTP grandmaster status and control. The Chloros host runs the PTP grandmaster; LATTICE cams and DAQ-E units slave to it for cross-device timestamps.

| Subcommand                                 | Description                                                                     |
| ------------------------------------------ | ------------------------------------------------------------------------------- |
| `status`                                   | Show grandmaster state, BMCA priorities, clock identity.                        |
| `peers`                                    | List slaves seen via Delay\_Req (cameras + DAQ-E sensors).                      |
| `cameras`                                  | Per-camera PTP health (`PtpStatus`, `PtpOffsetFromMaster`, `PtpMeanPathDelay`). |
| `restart`                                  | Restart the grandmaster process.                                                |
| `set-priority --priority1 N --priority2 N` | Override BMCA priorities.                                                       |

### Examples

```bash
chloros-cli time-sync status
chloros-cli time-sync peers
chloros-cli time-sync cameras
chloros-cli time-sync restart
chloros-cli time-sync set-priority --priority1 1 --priority2 1
```

***

## `chloros-cli lattice`

LATTICE camera control. Every subcommand routes through the Chloros backend; the backend owns the cam pool so subsequent CLI calls reuse the same open handle.

### Common Options (shared by most subcommands)

| Flag                                                   | Description                                                                                                                                                                              |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-d, --device N`                                       | Camera index (default: 0).                                                                                                                                                               |
| `-s, --serial SN`                                      | Specific serial; overrides `--device`.                                                                                                                                                   |
| `--serials SN1,SN2,…`                                  | Comma-separated serials for multi-cam operation.                                                                                                                                         |
| `--all`                                                | Operate on every discovered camera.                                                                                                                                                      |
| `--exposure US`                                        | Exposure time in microseconds.                                                                                                                                                           |
| `--gain DB`                                            | Gain in dB.                                                                                                                                                                              |
| `--pixel-format FMT`                                   | e.g. `BayerRG8`, `BayerRG12`.                                                                                                                                                            |
| `--width N` / `--height N`                             | Image dimensions.                                                                                                                                                                        |
| `--preset {default,high_quality,high_speed,triggered}` | Apply a settings preset. All free-run except `triggered`, which arms the camera for a hardware edge on Line 2 — with nothing driving that line it will wait forever rather than capture. |
| `-o, --output DIR`                                     | Output directory (default: `output`).                                                                                                                                                    |
| `--packet-size {auto,jumbo,standard,N}`                | GVSP packet size. `auto` runs ICMP+GVSP probes; `jumbo` = 9000; `standard` = 1500.                                                                                                       |

### LATTICE Camera First-Connect Workflow

```bash
# 1. Discover cameras on the network
chloros-cli lattice info

# 2. Single-cam smoke test: capture one frame.
#    By default this saves EVERY export type applicable to the cam
#    (raw, debayered, radiance, reflectance, preview). Pass e.g.
#    `--processing debayered` to save just one.
chloros-cli lattice capture -o output/

# 3. Connect a synchronized array (RECOMMENDED ENTRY POINT for arrays).
#    This is the same "smart-prep" flow the Chloros GUI uses:
#      - Network capability probe (ICMP DF ping + GVSP probe)
#      - Tier auto-pick (sim-emit / ftd-stagger / slip)
#      - Auto-shrink frame size to fit the wire
#      - PTP enabled by default
#      - Per-cam pixel format auto-pick
#      - AE seeding from the cam's saved state
#      - GPIO trigger config on Line2
chloros-cli lattice array-connect \
  --serials 213800234,214000533,214701288,214701292

# 4. Capture one synced frame group from the live array.
#    Defaults to --processing all (one file per export type per cam);
#    pass a single level to narrow it, e.g. --processing reflectance.
chloros-cli lattice array-capture --processing reflectance -o output/

# 5. Live-preview one cam in your browser
chloros-cli lattice viewer --serial 213800234

# 6. Tear down when done
chloros-cli lattice array-disconnect
```

### Subcommand Reference

#### Discovery & Information

| Subcommand                                                                                                                                                             | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `lattice info`                                                                                                                                                         | List connected cameras (vendor, model, serial, IP, MAC).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `lattice probe [--pixel-format FMT] [--json] [--no-discover]`                                                                                                          | Analyse host system for optimal camera configuration. `--no-discover` skips camera discovery (faster, NIC-only analysis).                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `lattice network [--fix] [--estimate] [--cameras N]`                                                                                                                   | Check/fix NIC settings; estimate bandwidth/FPS.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `lattice network-analysis --master SN --slaves SN1,SN2,… [--width N] [--height N] [--pixel-format FMT] [--binning N] [--force-tier TIER] [--backend-url URL] [--json]` | Stable-schema backend network-capability + array recommendation (returns `status` ∈ `ok` / `auto_shrunk` / `auto_capped_fps` / `needs_force_slip` / `error`). `auto_capped_fps` keeps the requested resolution but caps the target fps — read `recommended.recommended_target_fps` and pass it as the connect target; treat it as success, not an error.                                                                                                                                                                                   |
| `lattice analyze-array [--models M1,M2,…] [--binning N] [--n-active N] [--width N] [--height N] [--pixel-format FMT] [--force-tier TIER] [--json]`                     | What-if analysis without opening cameras. **`--n-active` is the total number of cameras on the wire, not just this array's** — raise it when standalone cameras stream concurrently, or the wire budget is computed against a demand that under-counts them (default: `len(--models)`). Always prints the aggregate `Wire budget:` (MB/s demanded vs collision-safe ceiling) and `Max cameras:` lines, and flags `** OVER-SUBSCRIBED **` when the array over-subscribes the wire — see [Array fps & burst model](#array-fps--burst-model). |
| `lattice gpu`                                                                                                                                                          | Show GPU status.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `lattice firmware [--update] [--force] [-y\|--yes]`                                                                                                                    | Check or update camera firmware. Local `.fwa` selection is pinned: the file in `firmware/<MODEL_PREFIX>/` matching the build's `MIN_FIRMWARE_VERSION` is flashed when present (highest version only as a fallback), so a newer vendor image staged on disk is inert until that pin is bumped — deliberately newer releases reach units via the signed AWS manifest, which is preferred when newer.                                                                                                                                         |
| `lattice presets [--apply NAME]`                                                                                                                                       | List or apply camera presets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `lattice status`                                                                                                                                                       | Live camera status.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

#### Capture

| Subcommand                                                                                                           | Purpose                                                                                                                                                                                                                                                                                                                                                        |
| -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lattice capture [--format tiff\|png\|jpg] [--jpeg-quality N] [--processing LEVEL] [--levels L1,L2,…] [--force-daq]` | Single frame. **Saves every export type by default** (`--processing all`); see [Capture Export Levels](#capture-export-levels-the-all-default). `--levels` saves an explicit subset (overrides `--processing`); `--force-daq` writes the assigned DAQ reading as a `.daq` sidecar even on a raw-only grab. `--jpeg-quality` = JPEG quality 1–100 (default 95). |
| `lattice continuous [--format tiff\|png\|jpg] [--jpeg-quality N] [--queue-depth N]`                                  | Stream to disk until Ctrl+C.                                                                                                                                                                                                                                                                                                                                   |
| `lattice viewer [--brightness N] [--ae-damping F] [--frame-rate FPS]`                                                | Browser-based live MJPEG preview. `--ae-damping` sets auto-exposure damping (0.4–100).                                                                                                                                                                                                                                                                         |

#### Sensor Tuning

| Subcommand                                                                                                          | Purpose                                                                                                                                                                                                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lattice configure [--get N1 N2…] [--set N=V N=V…] [--dump] [--json]`                                               | Read/write any GenICam node.                                                                                                                                                                                                                                                                                                          |
| `lattice exposure [--auto] [--auto-once] [--off] [--set US] [--brightness N] [--damping F] [--upper-limit US]`      | Exposure & AE.                                                                                                                                                                                                                                                                                                                        |
| `lattice gain [--auto] [--off] [--set DB]`                                                                          | Gain & auto-gain.                                                                                                                                                                                                                                                                                                                     |
| `lattice resolution [--set WxH] [--offset X,Y] [--binning N]`                                                       | Sensor ROI & binning. Binning is always **Average** — see below.                                                                                                                                                                                                                                                                      |
| `lattice format [--set FMT] [--list]`                                                                               | Pixel format.                                                                                                                                                                                                                                                                                                                         |
| `lattice trigger [--mode On\|Off] [--source SRC] [--delay-us US] [--activation EDGE] [--list-sources] [--software]` | Hardware/software trigger.                                                                                                                                                                                                                                                                                                            |
| `lattice white-balance [--auto] [--off] [--red R] [--blue B]` (no flags = one-shot WB)                              | WB operations. RGB/Bayer cams only; a no-op (skipped) on mono M3M.                                                                                                                                                                                                                                                                    |
| `lattice color-profile [--set raw\|linear\|natural\|enhanced\|custom_temp] [--cct K] [--get]`                       | RGB display colour pipeline. `natural` (default) is the cheap live finish; `enhanced` adds defringe + vibrance + CLAHE local contrast for the full hub-parity look at \~2× the per-frame finish cost, so a lower **live** framerate — saved captures always get the full finish either way. RGB/Bayer cams only; skipped on mono M3M. |
| `lattice color [--saturation N] [--contrast N] [--reset] [--get]`                                                   | Display saturation/contrast (RGB filter cams). Skipped on mono M3M.                                                                                                                                                                                                                                                                   |
| `lattice filter [--set NAME] [--list]`                                                                              | Set the cam's filter model (`RGN-IMX265`, `OCN`, `NGB`, …).                                                                                                                                                                                                                                                                           |
| `lattice power [--sleep]`                                                                                           | Probe power/thermal nodes; toggle low-power idle.                                                                                                                                                                                                                                                                                     |

> **Binning is always Average.** `lattice resolution --binning N` sets Average binning and does not offer a mode switch. Sum binning multiplies every DN by N², costing roughly two stops of highlight headroom per binning step; Chloros divides that factor back out before the radiometric chain, but pixels Sum drove into the sensor's ceiling are clipped and cannot be recovered. It buys nothing Average does not — an index is scale-invariant and the radiance scalars are calibrated at the Average scale. If a camera arrives already set to Sum (left there by another tool, or by a factory reset), Chloros detects it from a node readback and compensates; running `lattice resolution --binning N` puts it back on Average.

#### Calibration & Sensors

| Subcommand                                                                                 | Purpose                                       |
| ------------------------------------------------------------------------------------------ | --------------------------------------------- |
| `lattice calibrate [--filter NAME] [--attempts N] [--save PATH]`                           | Calibrate from a reflectance target.          |
| `lattice dls [--connect] [--spectrum] [--irradiance] [--mac MAC] [--filter NAME] [--json]` | Built-in downwelling-light-sensor commands.   |
| `lattice vignette --input DIR --output DIR [--lens-model KEY]`                             | Apply vignette correction to existing images. |

#### Multi-Camera (Transient Sessions)

| Subcommand                                                                     | Purpose                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lattice multi-info`                                                           | List all cameras with sync roles.                                                                                                                                                                           |
| `lattice multi-capture [--format FMT] [--jpeg-quality N] [--processing LEVEL]` | One synced frame from each cam. Saves **all export types by default** when a persistent array is connected; the transient no-array fallback is **debayered only** (run `array-connect` first for the rest). |
| `lattice multi-stream [--fps F] [--count N] [--format FMT] [--jpeg-quality N]` | Stream synced frames (transient).                                                                                                                                                                           |
| `lattice multi-test [--count N]`                                               | GPIO sync timing test.                                                                                                                                                                                      |
| `lattice multi-detect [--line LINE] [--json]`                                  | Auto-detect GPIO master/slave wiring.                                                                                                                                                                       |

#### Alignment

| Subcommand                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lattice align-calibrate [--method orb\|akaze\|phase\|checkerboard\|manual] [--model translation\|rigid\|affine\|homography] [--frames N] [--checkerboard RxC] [--points PATH] [--reference SN] [--save PATH] [--preview] [--vignette] [--prefilter none\|gradient\|clahe\|blur\|hist_match] [--rms-threshold-px N]` — plus detector/matcher knobs `[--max-features N] [--ratio-threshold F] [--matcher bf\|flann] [--knn-k N]`, RANSAC knobs `[--ransac-threshold-px F] [--ransac-iters N] [--ransac-confidence F]`, multi-frame combination `[--averaging mean\|median\|inlier_weighted]`, geometric constraints `[--lock-rotation] [--lock-scale] [--lock-axis x\|y]`, spatial restriction `[--roi X0,Y0,X1,Y1] [--mask PATH]`, and per-slave overrides `[--per-cam-override SN:KEY=VALUE]` (repeatable) | Compute alignment profile from live cameras. `--prefilter` defaults to `gradient` (edge map; matches the GUI/array aligner — edges survive across spectral bands). `--matcher flann` pays off above \~5000 features; `--averaging median` is robust to one bad capture, `inlier_weighted` weights by match count; `--lock-scale` projects to the nearest rotation (no scale), `--lock-axis` zeroes one translation component; `--mask` applies to every camera (use `--per-cam-override` for per-cam settings, e.g. `--per-cam-override 214701292:method=phase`). `--rms-threshold-px` refuses to save a calibration whose reprojection RMS exceeds the gate. |
| `lattice align-apply --profile PATH [--format tiff\|png] [--bit-depth 8\|12\|16] [--bands NAMES] [--order NAMES] [--gpu\|--no-gpu] [--no-crop] [--per-camera] [--per-band] [--vignette] [--interpolation nearest\|linear\|cubic\|lanczos] [--border-mode constant\|replicate\|reflect\|wrap] [--border-value N]`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | Capture one aligned multi-band frame. `--bit-depth` defaults to matching the camera; `--no-crop` keeps the full frame (pad with black); `--interpolation` (default `linear`) and `--border-mode`/`--border-value` (default `constant`/0) control the CPU warp — the GPU path is bilinear regardless.                                                                                                                                                                                                                                                                                                                                                          |
| `lattice align-stream --profile PATH [--fps F] [--count N] [--bit-depth 8\|12\|16] [--bands NAMES] [--order NAMES] [--gpu\|--no-gpu] [--no-crop] [--per-band] [--vignette] [--interpolation nearest\|linear\|cubic\|lanczos] [--border-mode MODE] [--border-value N]`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Stream aligned multi-band frames (same warp knobs as `align-apply`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `lattice align-info --profile PATH [--json]`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Display profile details.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `lattice align-reorder --profile PATH [--order NAMES] [--enable SERIALS] [--disable SERIALS]`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Change layer ordering.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

#### Index / Vegetation Maths

```bash
# Offline: compute NDVI from an aligned multi-band TIFF
chloros-cli lattice index --input aligned.tif --preset NDVI \
  --output ndvi.tif --colorize --gradient RdYlGn

# Live: discover array, calibrate alignment, capture, compute index, in one go
chloros-cli lattice index --live --profile align.json --preset NDVI \
  --save-multiband -o output/
```

Full flag set: `--input PATH | --live --profile PATH`, `--preset NAME` (NDVI/NDRE/EVI/SAVI/GNDVI/…), `--formula EXPR`, `--channel SYM=BAND` (repeatable), `--capture-level raw|debayered|radiance|reflectance|unknown` (override the capture level recorded in the source TIFF; default: read from TIFF metadata), `--output PATH`, `--output-format all|raw|tif|colorized|lut|png`, `--gradient NAME|JSON`, `--vmin/--vmax/--percentile LO,HI`, `--bg-mode clip|transparent|indexColor|backgroundColor`, `--colorize`, `--list-presets`, `--list-gradients`. With `--live` the alignment warp knobs also apply: `--save-multiband`, `--gpu/--no-gpu`, `--no-crop`, `--bit-depth 8|12|16`, `--vignette`, `--interpolation nearest|linear|cubic|lanczos`, `--border-mode constant|replicate|reflect|wrap`, `--border-value N`.

> **`--channel` symbols are case-sensitive.** The symbol side must match the preset's channel names exactly (presets use lowercase, e.g. NDVI = `red`,`nir` — check `--list-presets`), and the band side must match a band name in the aligned stack (or be a 0-based band index in offline mode). `--channel red=Red_660 --channel nir=NIR_850` works; `--channel RED=660` fails with a `channel_map missing entries` error.

#### Persistent Connections (Smart-Prep, GUI-Equivalent Flow)

These commands keep cameras open in the backend pool across CLI invocations.

| Subcommand                                                                                                                                                                                                                                     | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lattice cam-connect [--serial SN]`                                                                                                                                                                                                            | Add one cam to the pool (single-cam, no array).                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `lattice cam-disconnect [--serial SN] [--all]`                                                                                                                                                                                                 | Release.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `lattice cam-list`                                                                                                                                                                                                                             | List cams in the pool.                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| **`lattice array-connect`**                                                                                                                                                                                                                    | **Connect a persistent synchronized array (THE recommended entry point).** Runs the full GUI smart-prep flow.                                                                                                                                                                                                                                                                                                                                                     |
| `lattice array-disconnect [--array-id ID] [--all]`                                                                                                                                                                                             | Release an array.                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `lattice array-list`                                                                                                                                                                                                                           | List connected arrays.                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `lattice array-status [--array-id ID]`                                                                                                                                                                                                         | Live fps, PTP, last error.                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `lattice array-capture [--processing LEVEL\|all] [--levels L1,L2,…] [--aligned\|--no-aligned] [--index\|--no-index] [--force-daq] [--smart] [--fastest] [--compression deflate\|none] [--continuous\|--interval S] [--count N] [--duration S]` | One synced capture from the live array — Single / Continuous / Interval / Fastest. **Defaults to `all`** (one file per applicable export type per cam). Skipped cams (e.g. RGB excluded from radiance/reflectance) are reported with `Skipped: SN:<serial> (<reason>)`; the DAQ reading used for reflectance is saved alongside and reported with `DAQ: <path>`. See [Capture Modes, Recorders & Offline Reprocess](#capture-modes-recorders--offline-reprocess). |
| `lattice array-record [--fps F] [--duration S] [--gif] [--gif-only]`                                                                                                                                                                           | Record the live combined-index view to video/GIF (monitoring-grade; needs the combined stream open).                                                                                                                                                                                                                                                                                                                                                              |
| `lattice array-burst [--duration S] [--max-frames N] [--build] [--products …]`                                                                                                                                                                 | High-fps raw-Bayer burst (analysis-grade; reprocess offline).                                                                                                                                                                                                                                                                                                                                                                                                     |
| `lattice array-build-video --burst-dir DIR [--products …] [--fps F] [--save-tiffs] [--gif]`                                                                                                                                                    | Reprocess a saved raw burst into calibrated video(s).                                                                                                                                                                                                                                                                                                                                                                                                             |

**`array-connect` Options**

| Flag                                                                                  | Default                                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--serials SN1,SN2,…`                                                                 | auto-discover all LATTICE cams (needs ≥2) | First serial is the MASTER. When omitted, discovery filters to LATTICE (`TRI032*`) models and connects all of them.                                                                                                                                                                                                                                                                                                                                               |
| `--line {Line0,Line2,Line3}`                                                          | `Line2`                                   | GPIO sync line.                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `--target-fps F`                                                                      | auto                                      | Master trigger fire rate.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `--force-tier {sim-capture-sim-emit, sim-capture-ftd-stagger, slip-emit-and-capture}` | auto                                      | Override the tier picker.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `--wire-ceiling-mbps MB_PER_S`                                                        | auto-detected                             | **The host's sustained wire budget, in MB/s — the number the whole array allocation hangs off.** Lower it when the array reports GVSP-corrupt frames: the auto value is derived from the NIC's advertised link rate, which over-states USB adapters, thin PCIe lanes and busy shared fabrics. Persisted in the project's array capture block, so a reopen / CLI / SDK reconnect restores it. See [Array health](#array-health--which-subsystem-is-losing-frames). |
| `--binning {1,2,4}`                                                                   | auto                                      | Hardware binning.                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `--no-recommend`                                                                      | off                                       | Skip the network-analysis step.                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `--no-ptp`                                                                            | off                                       | Disable PTP (cross-cam timestamps are then **not** comparable).                                                                                                                                                                                                                                                                                                                                                                                                   |

### Smart-AE / Smart-Capture

LATTICE arrays run continuous AE in the background as soon as they're connected, but a freshly-pointed scene takes a moment to converge. `array-capture --smart` is the **packaged convenience**: it waits for AE to settle across every cam in the array, then triggers the capture. Use it when you change scenes mid-session.

```bash
# Connect once, then take settled captures whenever you re-point the rig
chloros-cli lattice array-connect --serials SN1,SN2,SN3,SN4
chloros-cli lattice array-capture --smart --processing reflectance -o pose_a/
# (move the rig)
chloros-cli lattice array-capture --smart --processing reflectance -o pose_b/
```

The settle policy is conservative by default: 5 s timeout, 1.5 s stability window, ±5 % exposure spread tolerance. Tune via the SDK (`ArrayHandle.capture_smart(settle_timeout_s=…, stability_window_s=…, exposure_tolerance_pct=…)`) if you need different behaviour from automation.

### Capture Export Levels (the `all` default)

As of this release, `lattice capture`, `lattice multi-capture`, and `lattice array-capture` **default to `--processing all`** — one saved file per export type that applies to each camera, matching the GUI's "Capture All" behaviour. The levels are:

| Level                 | Output                                                                                                                                                  | Applies to                                                                                       |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `raw`                 | Single-channel Bayer (mono cams: the single band) straight from the sensor.                                                                             | All cams.                                                                                        |
| `debayered`           | 3-channel BGR demosaic (mono cams: 1-channel grayscale).                                                                                                | All cams.                                                                                        |
| `radiance`            | float32 W/m²/sr/nm via the full radiometric chain.                                                                                                      | Multispectral (M3C/M3M) only — **skipped for RGB-filter cams**.                                  |
| `reflectance`         | uint16 ρ (`32768` = 1.0), Pix4D-ready.                                                                                                                  | Multispectral only, and **only when a DAQ is bound + the cam is calibrated**; otherwise skipped. |
| `preview` / `display` | Full GUI-preview chain (CCM + WB + gamma per the cam's profile). `lattice capture` names this `preview`; `array-capture`/`multi-capture` use `display`. | All cams.                                                                                        |

Pass a single level to save just that one (`--processing debayered`). When you ask for `all`, levels that don't apply to a given cam are skipped (and reported), not errored — an unconnected or uncalibrated cam still gets `raw` / `debayered` / `preview`.

For any reflectance frame, the DAQ downwelling reading actually used is written to a **`.daq`** sidecar next to the imagery (so the capture can be re-processed later) and reported on a `DAQ:` line.

### What a captures folder looks like

Each export type lands in its **own subfolder** under `-o`, so a multi-level capture never mixes types:

```
output/
├── raw/           capture_<ts>_SN<serial>_raw.tif
├── debayered/     capture_<ts>_SN<serial>_debayered.tif
├── radiance/      capture_<ts>_SN<serial>_radiance.tif
├── reflectance/   capture_<ts>_SN<serial>_reflectance.tif
├── preview/       capture_<ts>_SN<serial>_display.tif
├── index/         per-camera vegetation-index (LUT) render, when --index is on
├── composite/     array foreground/background live-view composite, when produced
└── *.daq          the downwelling reading matched to the capture
```

`<ts>` is the capture timestamp and `<serial>` the camera serial, so one synced group shares a timestamp across cameras. **Note the one asymmetry:** the `display` level is stored in a folder named `preview/` while the files themselves keep `_display` in the name — folder and suffix differ for that level only. Unknown levels fall back to a folder of their own name, and if the subfolder can't be created the file is written to the output root rather than being lost.

**Re-processing a captures folder:** point `chloros-cli process` at the **captures root** (`output/`). `process` normally imports only the folder you name, but when that folder holds no images and does have subfolders it descends automatically — so the root's level subfolders and the root `.daq` are all picked up in one go. Every level of a capture imports as a single image with the other levels available as modes, rather than as one image per level.

Naming a **level subfolder** directly (e.g. `output/raw/`) also works. Doing so leaves the root `.daq` behind, so copy or point the DAQ reading in alongside when you re-derive a radiometric product from `raw/` — otherwise the timestamp match has nothing to resolve against.

**Processing always starts from `raw`.** Within each capture the raw frame is the pipeline source; `debayered`, `radiance`, `reflectance` and `preview` come along as viewable modes but are never fed back through the pipeline. Re-processing a derived product would re-apply vignette, CCM and radiance math that is already baked into its pixels, so Chloros declines rather than double-processing. Two consequences worth knowing:

* The `index/` and `composite/` renders are **never** processed. They are outputs, not captures — an NDVI LUT render has no meaningful radiance interpretation.
* A captures folder exported **without** `raw` (e.g. `array-capture --processing reflectance`) has no legitimate pipeline source. Those captures import and display normally, but `process` skips them and says so:

  ```
  [IMPORT-LEVEL] Skipping 4 already-processed file(s) with no raw source: capture_…_reflectance.tif
  [IMPORT-LEVEL] Processing starts from raw. Re-capture with --processing raw, or force an entry
                 point with --input-level.
  ```

  If you genuinely need to push a derived product through — a hub session captured with `demosaic` on, or a legacy folder — `--input-level {raw,debayered,processed}` forces the entry point and overrides the skip. That flag is the deliberate escape hatch; `auto` (the default) never processes a capture that has no raw.

### Skipped Captures in Mixed-Filter Arrays

When you mix RGB and multispectral cams in one array, `array-capture --processing radiance` (or `reflectance`) saves the multispec frames and **skips** the RGB cams — per-Bayer radiance is not meaningful for a broadband sensor. The CLI prints each saved file (with its export level), each `.daq` written, and each skip explicitly, so the file count isn't surprising:

```
  Saved: output/sync_…_SN213800234.tif [reflectance] (SN:213800234, fid:1)
  Saved: output/sync_…_SN214000533.tif [reflectance] (SN:214000533, fid:1)
  Saved: output/sync_…_SN214701288.tif [reflectance] (SN:214701288, fid:1)
  DAQ:   output/sync_…_daq-e-54b5e0.daq
  Skipped: SN:214701292 (reflectance-not-applicable-to-rgb-cam filter=RGB)

  3 synchronized frames captured. (1 skipped)
```

Skip-reason tokens follow the pattern `<level>-not-applicable-to-rgb-cam`. Reflectance can also skip with `reflectance-skipped-no-fresh-dls` / `reflectance-skipped-bound-daq-unavailable (…)`, and with `dls-uncalibrated-band-<nm>` when the band lies mostly outside the DAQ light sensor's radiometrically calibrated range (\~374–974 nm) — among shipping SKUs only F988, whose supported path is the reflectance-panel workflow.

Use `--processing debayered` (or `display`) to include every cam regardless of filter type, or the default `all` to get every applicable level per cam in one shot.

***

## Capture Modes, Recorders & Offline Reprocess

These all operate on a **persistent array** (run `array-connect` first). They mirror the GUI capture panel.

### `array-capture` modes

`array-capture` is a single command with four shutter modes plus a set of export toggles:

| Mode                   | Flag           | Behaviour                                                                                                                                                                                                                                   |
| ---------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Single** *(default)* | (none)         | One synced capture group, then exit.                                                                                                                                                                                                        |
| **Continuous**         | `--continuous` | Back-to-back passes until `Ctrl+C`, `--count N`, or `--duration S`.                                                                                                                                                                         |
| **Interval**           | `--interval S` | One pass every `S` seconds (measured from the start of each pass), same bounds.                                                                                                                                                             |
| **Fastest**            | `--fastest`    | Raw-only + the assigned DAQ reading + the combined-index composite; skips the radiance/reflectance/display math so the frame lands fast. Implies `--processing raw --force-daq`. Reprocess the saved `.daq` into calibrated products later. |

Export toggles (combine with any mode; all share the GUI/SDK endpoint):

| Flag                           | Effect                                                                                                                                                                                                                                                                                  |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--processing LEVEL`           | Single export level, or `all` (default).                                                                                                                                                                                                                                                |
| `--levels L1,L2,…`             | Explicit subset of export types (e.g. `raw,radiance,reflectance`); **overrides `--processing`**.                                                                                                                                                                                        |
| `--aligned` / `--no-aligned`   | Warp every member's non-raw export to the array's [alignment profile](#alignment) (co-registered). Raw stays unwarped but carries the transform in metadata. Falls back to unaligned (with a warning) if the array has no profile.                                                      |
| `--index` / `--no-index`       | Save / skip the per-cam vegetation-index overlay where one is configured. Default: render it.                                                                                                                                                                                           |
| `--force-daq`                  | Save the assigned DAQ/DLS reading as a `.daq` sidecar even when no chosen level needs it (e.g. a raw-only grab), so the frames can be reprocessed into reflectance/index offline.                                                                                                       |
| `--smart`                      | Wait for AE to settle across all cams before triggering (see [Smart-AE / Smart-Capture](#smart-ae--smart-capture)).                                                                                                                                                                     |
| `--compression {deflate,none}` | TIFF pixel compression. `deflate` (default) = lossless zlib L1 + horizontal predictor, \~4.1 MB per full-res frame; `none` = uncompressed, \~5× faster write at \~6.3 MB per frame — use for maximum sustained rate when disk allows. Both are lossless and read identically on import. |

> **Single-write TIFF + the sustained-rate model.** Captures are written in **one** tifffile pass carrying pixels + XMP + IFD0 Make/Model (measured on full-res Mono12: 36 ms compressed / 6.5 ms uncompressed, vs \~148 ms for the old write-then-ExifTool rewrite); the only remaining ExifTool work (EXIF sub-IFD polish) runs on an async background worker, and a frame is complete and import-ready even if it never runs. Note that DEFLATE compression holds the Python GIL, so compressed writes do **not** parallelize across the per-cam writer threads — sustained 8-cam full-res capture at sensor rate (\~10.4 fps) requires `--compression none` **and** NVMe-class disk (\~500 MB/s of sustained writes). The same knob is exposed as `compression` on `POST /api/camera/array/capture`.

```bash
# Interval timelapse: one reflectance pass every 10 s for 5 minutes
chloros-cli lattice array-capture --interval 10 --duration 300 \
  --processing reflectance -o timelapse/

# Fastest grab for a moving rig — raw + .daq now, calibrate later
chloros-cli lattice array-capture --fastest -o flightline/

# Co-registered multi-band export (drop the index overlay)
chloros-cli lattice array-capture --processing reflectance --aligned --no-index -o out/
```

### `array-record` — combined-index video/GIF (monitoring-grade)

Records whatever the **live combined-index view** is showing to an `.avi` (and optionally a `.gif`). Because it taps the live composite, the combined stream must be open (e.g. the array is being previewed in the GUI) for frames to land. It polls progress every 2 s and stops on `--duration`, `Ctrl+C`, or when the recorder self-ends.

```bash
# 30-second combined-index clip at 10 fps, plus a GIF
chloros-cli lattice array-record --duration 30 --fps 10 --gif -o monitoring/
```

| Flag               | Default      | Description                                   |
| ------------------ | ------------ | --------------------------------------------- |
| `--array-id ID`    | only array   | Target array (omit if just one is connected). |
| `-o, --output DIR` | `output`     | Output directory (backend-local).             |
| `--fps F`          | `10`         | Recording frame rate.                         |
| `--duration S`     | until Ctrl+C | Auto-stop after `S` seconds.                  |
| `--gif`            | off          | Also write an animated GIF.                   |
| `--gif-only`       | off          | Write only a GIF (no `.avi`).                 |

### `array-burst` — raw-Bayer high-fps burst (analysis-grade)

Reads the grab loop's synced-group buffer directly — **no calibration chain, no exiftool, no live view needed** — so it runs at the camera's full grab rate. Writes raw frames + a per-frame manifest + one `.daq` per distinct DLS reading under `<output>/bursts/<base>/`. Reprocess offline (next command), or pass `--build` to do it immediately on stop.

```bash
# 5-second raw burst, then build the combined index video in one shot
chloros-cli lattice array-burst --duration 5 --build \
  --products combined:index --fps 10 -o capture/
```

| Flag               | Default          | Description                                                                    |
| ------------------ | ---------------- | ------------------------------------------------------------------------------ |
| `--array-id ID`    | only array       | Target array.                                                                  |
| `-o, --output DIR` | `output`         | Output directory (burst lands in `<DIR>/bursts/<base>/`).                      |
| `--duration S`     | until Ctrl+C     | Auto-stop after `S` seconds.                                                   |
| `--max-frames N`   | unbounded        | Auto-stop after `N` raw frames.                                                |
| `--build`          | off              | After stopping, immediately reprocess the burst (same as `array-build-video`). |
| `--products …`     | `combined:index` | With `--build`: which video(s) to build (see below).                           |
| `--fps F`          | `10`             | With `--build`: output video fps.                                              |
| `--save-tiffs`     | off              | With `--build`: also save per-frame calibrated TIFFs.                          |
| `--gif`            | off              | With `--build`: also write animated GIF(s).                                    |

### `array-build-video` — offline reprocess a saved burst

Time-matches each raw frame to the nearest saved `.daq` reading and pushes it through the **same radiance / reflectance / index chain as the import pipeline**, rendering one or more videos.

`--products` is a comma list of `kind:level` items, where `kind` ∈ `per_cam` | `combined` and `level` ∈ `radiance` | `reflectance` | `index`. A bare `level` (no `kind:`) defaults to `per_cam`. Default is `combined:index`.

```bash
# Per-cam reflectance video for every member + one combined NDVI video
chloros-cli lattice array-build-video \
  --burst-dir "capture/bursts/2026-06-24_141500" \
  --products per_cam:reflectance,combined:index \
  --fps 10 --save-tiffs
```

| Flag              | Default          | Description                                                  |
| ----------------- | ---------------- | ------------------------------------------------------------ |
| `--burst-dir DIR` | (required)       | Path to the burst folder (`…/bursts/<base>/`).               |
| `--products …`    | `combined:index` | `kind:level` list, as above.                                 |
| `--fps F`         | `10`             | Output video fps.                                            |
| `--save-tiffs`    | off              | Also save per-frame calibrated TIFFs alongside the video(s). |
| `--gif`           | off              | Also write animated GIF(s).                                  |

> **Pick the right recorder.** `array-record` is *monitoring-grade* — it captures the live composite as-displayed and needs the stream open. `array-burst` → `array-build-video` is *analysis-grade* — it saves raw sensor data at full rate and reconstructs calibrated radiance/reflectance/index videos afterward, with no live view required.

### Mono (M3M) Single-Band Cameras

The **M3M** line is the mono sibling of the Bayer **M3C**: one narrowband interference filter per camera (`M3M-<lens>-F<wavelength>`, e.g. `M3M-L87-F685`), so the sensor delivers a **single grayscale band** with no Bayer mosaic. There is nothing to demosaic, no inter-channel crosstalk to unmix, and no white balance to set — the whole RGB-display colour pipeline simply doesn't apply.

What that means on the CLI:

* **`lattice white-balance`, `lattice color-profile`, `lattice color`** detect a mono cam and **skip with a one-line message** instead of pushing meaningless settings. They still run normally against an RGB/Bayer M3C cam in the same session.
* **`lattice calibrate` / `process --reflectance` / `array-capture --processing radiance`** still work — radiance and reflectance are *per-band* radiometric maps and are perfectly well-defined for one band. Mono frames carry an **identity** sensor-response matrix (no 3×3 unmix), so the plane passes through the calibration math untouched.
* **A single mono cam cannot produce a vegetation index.** NDVI/NDRE/etc. need at least two bands (e.g. Red + NIR). To get an index from mono hardware, point **several** M3M cams at different wavelengths, align them into one multi-band stack, and index *that*:

```bash
# Red (660) + NIR (850) mono pair -> aligned 2-band stack -> NDVI
chloros-cli lattice array-connect --serials SN_RED,SN_NIR
chloros-cli lattice index --live --profile align.json \
  --preset NDVI --channel red=Red_660 --channel nir=NIR_850 \
  --save-multiband -o output/
```

`--channel` symbols must match the preset's channel names **exactly** (case-sensitive; NDVI's are lowercase `red`,`nir` — see `--list-presets`), and the band side names a band in the aligned stack (offline mode also accepts 0-based band indices, e.g. `--channel red=0 --channel nir=1`).

The discriminator throughout the stack is the `M3M` token in the model string (it never appears in an `M3C` string), surfaced to the GUI/SDK as `is_mono`.

***

## Host NIC Setup & Tuning (LATTICE arrays)

LATTICE cameras stream GVSP over the host's Ethernet adapter, so for multi-camera arrays the adapter's **driver** and **receive-ring size** matter as much as the link rate. Wrong settings surface as a `FRAMES WILL DROP` / `Reduce ROI to enable` gate in the Array Settings panel (and in `lattice network-analysis` / the SDK's `analyze_array_network()`), even when the cameras themselves are healthy.

### USB 10GbE adapters — Realtek RTL8157 ("Realtek USB 10GbE Family Controller")

| Item                                             | Required value                                   | Why it matters                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------------------------------ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Driver version**                               | **≥ v10.67 (Jan 2026)**, INF `rtump64x64sta.inf` | The legacy **2016** driver (v10.65, `rtump64x64.inf`) mishandles power-down and bugchecks with **`DRIVER_POWER_STATE_FAILURE` (BSOD `0x9F`)** on shutdown/restart/sleep. The transition hangs (\~5 min timeout), the user force-powers-off, and repeated unclean shutdowns **corrupt the WMI repository** (PowerShell/tools begin failing with `Invalid class`) and **wedge the USB stack** on the next boot (NIC won't enable; USB drives stop enumerating). Update from realtek.com (or the dongle vendor) before relying on clean restarts.                                                                                           |
| **Receive Buffers** — keyword `ReceiveBufferLen` | **256** (driver max)                             | The NIC RX ring. The driver default of **32** leaves only \~0.26 MB usable ring — far too small for a multi-cam burst — so the array panel reports `Sim-emit burst … exceeds NIC RX ring usable capacity 0.26 MB` and blocks connect. At **256** the ring is large (**\~13.5 MB measured on the lab 10GbE host**), giving the RX pipeline real headroom for multi-cam GVSP bursts. (Whether a given config actually *connects* is decided by two checks — the **drain-aware** admittance check and the **aggregate over-subscription** check — not a raw burst-vs-ring compare; see [Array fps & burst model](#array-fps--burst-model).) |
| **Receive URBs** — keyword `PendingReceives`     | **64** (max)                                     | USB request blocks in flight; raise alongside Receive Buffers for burst absorption.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| **Jumbo Frame** — keyword `*JumboPacket`         | **9014**                                         | Needed for 9000-byte GVSP packets (6× fewer packets/frame than 1500).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |

> ⚠️ **A NIC driver update RESETS these advanced properties to defaults.** After updating or replacing the adapter driver, **re-apply** `ReceiveBufferLen=256` and `PendingReceives=64`, or the array panel will gate again even though "nothing changed in the hardware." This is the #1 cause of a previously-working rig suddenly refusing to connect.

Apply from an **elevated** PowerShell (substitute your adapter name, e.g. `"Ethernet 5"`):

```powershell
Set-NetAdapterAdvancedProperty -Name "Ethernet 5" -RegistryKeyword ReceiveBufferLen -RegistryValue 256
Set-NetAdapterAdvancedProperty -Name "Ethernet 5" -RegistryKeyword PendingReceives  -RegistryValue 64
Get-NetAdapterAdvancedProperty  -Name "Ethernet 5" -RegistryKeyword ReceiveBufferLen,PendingReceives   # verify
```

> **`lattice network --fix` covers USB 10GbE adapters.** It now detects the adapter type and tunes the correct receive-ring keyword: `*ReceiveBuffers`→2048 for PCIe NICs (Intel I219, etc.), or `ReceiveBufferLen`→256 + `PendingReceives`→64 for the Realtek **USB** 10GbE controller (which does not expose `*ReceiveBuffers`). Targets are clamped to each driver's reported max (`NumericParameterMaxValue`), so it never writes an out-of-range value. Run it from an **elevated** terminal; like any registry-based tune, the change takes effect after an adapter restart or reboot. The manual `Set-NetAdapterAdvancedProperty` commands above remain a fine alternative — they apply live (re-bind the adapter) without a restart.

### Network basics (all LATTICE links)

* **Addressing:** link-local `169.254.0.0/16` (GigE Vision LLA). The host takes a static `169.254.x.x/16`; cameras + DAQ-E self-assign in the same range. No DHCP/gateway required.
* **Packet size:** prefer jumbo (9000), but let the auto-probe find it — it re-measures on every connect and already looks past the camera's 1500-byte ICMP cap via a GVSP probe, so it lands on jumbo wherever the wire truly carries it. Pin with `CHLOROS_GVSP_PACKET_SIZE_FORCE=9000` only when you know better than the probe, and prefer per-command over permanent: a pin skips the probe, so if the path can't actually carry 9000 **every** capture times out with `SC_ERR_TIMEOUT -1011` (see [Environment Variables](#environment-variables)).
* **RX ring scales with `ReceiveBufferLen`:** at the default `32` the usable ring is \~0.26 MB (too small for any multi-cam burst); at the max `256` it is large (\~13.5 MB measured on the lab 10GbE host), giving real headroom. Whether a config connects is then decided by the drain-aware admittance check **and** the aggregate over-subscription check below — not a raw burst-vs-ring compare.

### Array fps & burst model

How to read the Array Settings panel (and `lattice analyze-array` / the SDK's `analyze_array_network`):

* **Burst is summed per-camera at each cam's real pixel format.** Mono **M3M** cams stream **Mono12 (2 B/px)**; **M3C** Bayer cams stream 8- or 12-bit (TRI032S silently emits BayerRG12 even when BayerRG8 is requested). So a 4-cam full-res frame is **\~12.6 MB if all 8-bit but \~25 MB with three 12-bit mono cams**. The projection resolves each cam's format from its model (identity cache), so the burst matches what the wire actually carries — not a one-size BayerRG8 assumption.
* **A USB Ethernet adapter is capped at 200 MB/s regardless of its nameplate.** The efficiency table that turns a link rate into a sustained figure is PCIe-derived; a USB NIC advertises its *Ethernet* link rate but is bounded by the USB bus and its driver. A USB 10GbE dongle used to score \~1063 MB/s "sustained" — a number that was never probed — and the resulting pacing corrupted 6–18 % of frames while still reporting a healthy target fps. USB-attached NICs are now capped at **200 MB/s** as an absolute (the limit is the bus, so it does not scale with the nameplate; a USB 1 GbE adapter derives \~80 MB/s and is unaffected). `wire_ceiling_source` on the capability record says so in words, and `nic_is_usb` flags it. Override either way with `--wire-ceiling-mbps`.
* **Admittance is drain-aware, not whole-burst-vs-ring.** A simultaneous burst only has to fit the *transient backlog* = `max(0, Σ per-cam arrival − host drain) × emit_window`, not the entire burst. On a fast-host / slow-cam fabric (a **PCIe** 10G host + 4× 1 GbE cams: arrival ≈ 320 MB/s, drain ≈ 1063 MB/s) the host drains faster than the cams fill, backlog ≈ 0, so full-res sim-emit **admits** even though the 25 MB burst exceeds the 13.5 MB ring. Put the same four cameras behind a **USB** 10GbE adapter and the drain is 200 MB/s, not 1063 — arrival outruns it, and the loss shows up as corrupt frames rather than as a lower frame rate. On a 1 GbE host the cams' 31.25 MB/s DLThr floor makes arrival outrun the drain → it correctly **blocks** (for *this* class of block, reduce ROI or use binning ≥ 2). Admittance is one of **two** connect gates — the other is the aggregate over-subscription check below.
* **Projected fps is a conservative serial-retrieve ceiling.** The host grab loop currently pulls each cam's buffer **serially** (\~one per-cam emit window each), so the cycle is bounded by `max(readout+emit, N × emit)` with per-cam emit clamped to the camera's **access link** (1 GbE ≈ 80 MB/s), not the host uplink. For a 4-cam full-res 12-bit array that's **\~2.8 fps**, matching the measured \~2.7–3.0. fps is deliberately **exposure-independent**, so in dim scenes actual can dip slightly below the ceiling as exposure lengthens. The serial retrieve is the real fps limiter; parallelizing it would raise the ceiling toward the single-emit rate.
* **Aggregate over-subscription is a hard connect blocker.** Per-cam bandwidth allocation floors at **8 MB/s** (`ARRAY_PER_CAM_FLOOR_BPS`), so once the floor clamps, aggregate demand (`per_cam × N`) can exceed the **collision-safe wire ceiling** (`sustained × sim_emit_factor`). Practical full-res ceilings on 1 GbE: **6 cams at 1500 MTU, 9 with jumbo**. This ceiling is a property of the wire and the floor alone — it is **independent of frame size**, so **binning and smaller ROI do NOT help** (they lower bytes per *frame*, not the GevSCPD-paced bytes per *second*); the only remedies are fewer cameras, jumbo frames end-to-end, or a faster NIC. The symptom would be GVSP packet loss, not a graceful fps reduction, so `analyze-array` zeroes the achievable-fps figures and prints `** OVER-SUBSCRIBED **`, and `array-connect` with a pinned resolution **refuses to connect** (the walk-down otherwise bins frames down, which does not clear this class of block either). `CHLOROS_ARRAY_ALLOW_OVERSUBSCRIBED=1` downgrades the refusal to a loud warning for bench work — see [Environment Variables](#environment-variables).

### Array health — which subsystem is losing frames

A connected array's `GET /api/camera/array/<array_id>/capability` carries a live `health` block, re-evaluated on a rolling **10-second** window. It splits frame loss into the two causes that need opposite fixes, rather than reporting one "incomplete" rate that names neither:

| Field                                                | What it means                                                                 | Which subsystem                                       |
| ---------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------- |
| `gvsp_corrupt_rate_pct` (per serial)                 | The frame **arrived and was structurally bad** — GVSP packet loss.            | **Network**: wire budget, pacing, NIC RX ring, MTU    |
| `never_arrived_rate_pct` (per serial)                | The frame **never came at all** — the camera didn't fire, or nothing left it. | **Trigger / sync**: M8 cable, `--line`, `TriggerMode` |
| `worst_gvsp_corrupt_pct` / `worst_never_arrived_pct` | The worst camera's rate for each.                                             | —                                                     |
| `per_cam_rate_pct`                                   | Combined incomplete rate per camera (both causes together).                   | —                                                     |
| `stable_for_seconds`                                 | How long every camera has stayed under 0.01 %.                                | —                                                     |

Above 5 % the backend logs an `[array-health <id>] WARN` line naming the split — on the first breach, on a severity-band change, once a minute while it persists, and once when it clears. The corrupt half prints `[gvsp-corrupt <SN>]` on the first hit per camera and reason, then a roll-up every 60 s. Every evaluation still lands in the backend log file; the counters move on every buffer regardless of what is printed.

The same record reports the number the whole allocation hangs off:

| Field                      | What it means                                                                                                                                                             |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wire_ceiling_mbps`        | The host's sustained wire budget in force, MB/s.                                                                                                                          |
| `wire_ceiling_source`      | Where that number came from, in words — e.g. `USB-capped 200 MB/s (was theoretical 1062; PnPDeviceID=USB\VID_0BDA&PID_815A)` or `user override 120 MB/s (auto said 200)`. |
| `wire_ceiling_is_user_set` | `true` when `--wire-ceiling-mbps` (or the GUI's **Wire Budget** field) set it.                                                                                            |
| `nic_is_usb`               | `true` for a USB Ethernet adapter — see the 200 MB/s cap above.                                                                                                           |

**Reading it:** non-zero `gvsp_corrupt_rate_pct` with `never_arrived_rate_pct` at 0 means triggering and cable sync are perfect and 100 % of the loss is on the network path — lower `--wire-ceiling-mbps` and reconnect. The reverse pattern points at the sync cable or the trigger line instead.

> **`--target-fps` is not the lever for corrupt frames.** GevSCPD pacing is written once at connect, so lowering the trigger rate changes the duty cycle and not the simultaneous-emit burst rate. A measured 5× demand cut produced no improvement; dropping the wire ceiling from 240 to 200 MB/s took the same rig from 10.4 % corrupt to 0.00 %.

> **Mid-stream auto-shrink is unavailable on TRI032S firmware.** A running array cannot fix this itself; disconnect and reconnect so the connect-time picker can re-plan with the new ceiling.

### Symptom → fix

| Symptom (Array Settings / connect / `analyze_array_network`)                                                                                      | Cause                                                                                                                                           | Fix                                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FRAMES WILL DROP … exceeds NIC RX ring usable capacity 0.26 MB`, `Reduce ROI to enable`                                                          | `ReceiveBufferLen` reset to 32 (typically after a driver update)                                                                                | Set `ReceiveBufferLen`→256, `PendingReceives`→64; reopen the panel (restart the backend if it cached the old ring size)                                                                                                 |
| Restart/shutdown hangs; later `Invalid class` WMI errors, NIC won't enable, USB drives missing                                                    | Old 2016 Realtek USB 10GbE driver → BSOD `0x9F` → forced power-offs                                                                             | Update the adapter driver to ≥ v10.67 (2026), then re-apply the receive-ring settings above                                                                                                                             |
| Connect succeeds but returns sub-native resolution                                                                                                | Smart-prep auto-shrank the frame to fit the wire                                                                                                | Upgrade the link / accept the shrink / `--force-tier slip-emit-and-capture`                                                                                                                                             |
| Array reports a healthy target fps but delivers a fraction of it; `health.gvsp_corrupt_rate_pct` non-zero, `never_arrived_rate_pct` 0             | The host's inferred wire budget over-states what it actually sustains (typical on a USB Ethernet adapter, a thin PCIe lane, or a shared fabric) | Reconnect with a lower `--wire-ceiling-mbps` and re-check the health block. **Not** `--target-fps` — GevSCPD pacing is fixed at connect                                                                                 |
| Cameras missing from published groups; `health.never_arrived_rate_pct` non-zero, `gvsp_corrupt_rate_pct` 0                                        | Trigger / sync path — the cams are not firing, not a network problem                                                                            | Check the M8 sync cable and `--line`; confirm every member is armed (`TriggerMode=On`)                                                                                                                                  |
| `** OVER-SUBSCRIBED **` / `Wire budget` exceeded in `analyze-array`, or connect refusal with pinned resolution (`array over-subscribes the wire`) | Aggregate per-cam demand (8 MB/s floor × N cams) exceeds the collision-safe wire ceiling — 6 cams full-res on 1 GbE @1500 MTU, 9 with jumbo     | Fewer cameras, jumbo frames end-to-end, or a faster NIC. **ROI/binning will NOT help** (the ceiling is frame-size-independent). `CHLOROS_ARRAY_ALLOW_OVERSUBSCRIBED=1` overrides on the bench (accepts the packet loss) |

***

## `chloros-cli daq`

Spectral-sensor commands. Two classes:

* **`pool-*`** — thin HTTP clients that drive the sensor through the backend's persistent pool. **This is the supported path, and the only one present in the shipped CLI.** The backend owns the transport, so the GUI, the CLI and SDK scripts all share one live handle instead of fighting over the serial port.
* **Everything else** (`test`, `record`, `live`, `stream`, `connect`, `info`, `net`, `ota`, `sample-rate`, `calibrate`, `serve`, `ws`, `udp`, `mqtt`, `reflectance`, `login`, `logout`, `status`) — direct hardware access, documented below for completeness. These need the `daq` Python package, which is **not included in any shipped artifact**: the compiled CLI excludes it (`scripts/Build-CLI.ps1` sets `--nofollow-import-to=daq`, and the transports `pyserial` / `bleak` / `zeroconf` with it), and the PyPI SDK package does not contain it either. They run only from a source checkout, so treat them as a MAPIR-internal development path rather than something to reach for.
* **`discover` / `list`** straddle the two: they are direct-hardware commands from a source checkout, but on a shipped build they fall back to `pool-discover` and the backend performs the scan. So scanning works everywhere — which matters because it is the only way to learn a DAQ-M's BLE MAC.

> **`chloros-cli daq --help`** (and `-h` / `help`) lists the `pool-*` subcommands — help is deliberately routed to the pool client so it reflects the commands that actually run. If you invoke a direct-hardware subcommand on a shipped build it exits with an explicit error naming the missing package and pointing you back at `pool-*`; nothing fails silently. (`discover` / `list` are the exception — they reroute to `pool-discover` and just work.)
>
> **Everything a customer needs is reachable through `pool-*`** — connect, stream, record calibrated `.daq` files, and swap cap profiles. The DAQ can also be driven from Python with `chloros_sdk.connect_daq_sensor()`, which uses the same pooled path.

### DAQ Sensor First-Connect Workflow

```bash
# 1. Smart-detect any DAQ on this machine (Ethernet → BLE → USB precedence)
chloros-cli daq connect

# 2. Detailed scan: every transport, showing the address to connect with.
#    This is how you find a DAQ-M's BLE MAC — unlike a DAQ-E hostname or a
#    DAQ-U COM port, a MAC isn't printed on the device or listed by the OS.
chloros-cli daq discover                      # or: daq pool-discover
chloros-cli daq discover --only ble           # BLE only
chloros-cli daq discover --json               # machine-readable

# 3. Open a persistent pool session (handle stays alive across CLI calls)
chloros-cli daq pool-connect           # smart-detect
chloros-cli daq pool-connect --port COM3                       # DAQ-U on a specific COM port
chloros-cli daq pool-connect --mac AA:BB:CC:DD:EE:FF           # DAQ-M by BLE MAC
chloros-cli daq pool-connect --eth-host daq-e-xxx.local        # DAQ-E by hostname

# 4. List what's in the pool, including the sensor_id you'll use next
#    (DAQ-U ids look like 'CB-7C-A8-2E-5F'; DAQ-E ids like 'daq-e-def330')
chloros-cli daq pool-list

# 5. Read the latest spectrum frame
chloros-cli daq pool-latest --sensor-id CB-7C-A8-2E-5F

# 6. Record a calibrated .daq file for 60s
chloros-cli daq pool-record --sensor-id CB-7C-A8-2E-5F --duration 60 \
  -o ~/Documents/spectra --device-name "field-A"

# 7. Release
chloros-cli daq pool-disconnect --sensor-id CB-7C-A8-2E-5F
```

### `pool-*` Reference

| Subcommand                                                                                     | Purpose                                                                                                                                                                                                                                                                                                              |
| ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `daq pool-connect` (smart-detect)                                                              | Open a sensor in the backend pool.                                                                                                                                                                                                                                                                                   |
| `daq pool-connect --port PORT`                                                                 | DAQ-U on a specific serial port.                                                                                                                                                                                                                                                                                     |
| `daq pool-connect --ble`                                                                       | DAQ-M over BLE, MAC auto-scanned.                                                                                                                                                                                                                                                                                    |
| `daq pool-connect --mac MAC`                                                                   | DAQ-M over BLE at a known MAC (implies `--ble`).                                                                                                                                                                                                                                                                     |
| `daq pool-connect --eth-host HOST`                                                             | DAQ-E over Ethernet at a known host.                                                                                                                                                                                                                                                                                 |
| `daq pool-connect --eth`                                                                       | DAQ-E over Ethernet, host auto-discovered (mDNS + ARP fallback; works from a cold ARP cache on Windows and Linux).                                                                                                                                                                                                   |
| `daq pool-connect --integration-time MS --frame-avg N --no-ae`                                 | Tune integration window / AE state.                                                                                                                                                                                                                                                                                  |
| `daq pool-connect --no-stream`                                                                 | Connect but do not start streaming yet (resume with `pool-stream --start`).                                                                                                                                                                                                                                          |
| `daq pool-connect --cap-id {none, fov_15, fov_30, fov_45, fov_60, fov_90, sunshine_cosine}`    | Cap-correction profile. Default at the backend is `sunshine_cosine`.                                                                                                                                                                                                                                                 |
| `daq pool-discover [--only usb,ble,eth] [--timeout SEC] [--json]`                              | Scan every transport for sensors you could connect to, without connecting. **This is how you find a DAQ-M's BLE MAC.** `daq discover` / `daq list` route here automatically in shipped builds. Sensors already open in the pool are not listed — a connected DAQ-M stops advertising — so use `pool-list` for those. |
| `daq pool-list`                                                                                | Show every sensor in the backend pool.                                                                                                                                                                                                                                                                               |
| `daq pool-disconnect --sensor-id ID [--all]`                                                   | Release.                                                                                                                                                                                                                                                                                                             |
| `daq pool-latest --sensor-id ID [--recent N] [--json]`                                         | Most-recent N spectrum frames.                                                                                                                                                                                                                                                                                       |
| `daq pool-stream --sensor-id ID [--start \| --stop]`                                           | Resume / pause streaming.                                                                                                                                                                                                                                                                                            |
| `daq pool-record --sensor-id ID [--duration SEC] [--output DIR] [--device-name NAME] [--stop]` | Start / stop a .daq recording.                                                                                                                                                                                                                                                                                       |
| `daq pool-set-cap --sensor-id ID --cap-id CAP`                                                 | Swap the cap-correction profile at runtime.                                                                                                                                                                                                                                                                          |

### Direct-Hardware Subcommands (source checkout only — not in shipped builds)

> Listed for completeness. These require the `daq` Python package plus `pyserial` / `bleak` / `zeroconf`, none of which ship in the compiled CLI or the PyPI SDK — they run only from a MAPIR source checkout. **If you are using a released Chloros build, use the `pool-*` commands above instead**; they cover connect, stream, record and cap selection.

```bash
chloros-cli daq test --port COM3                           # Verify connection
chloros-cli daq connect --eth                              # Smart-detect over ETH
chloros-cli daq info --eth-host daq-e-xxx.local            # Device summary as JSON
chloros-cli daq discover --only usb,ble --timeout 5        # Scan local interfaces
chloros-cli daq list                                       # Alias of discover
# ^ discover/list are the exception in this section: in a shipped build they
#   fall back to `pool-discover` (the backend does the scan), so they work
#   without a source checkout. The only difference is that the fallback needs
#   the Chloros backend running, as all pool-* commands do.

# Streaming JSON Lines to stdout (pipeable)
chloros-cli daq stream --port COM3 --format jsonl --photometrics

# Record to .daq for 60 seconds
chloros-cli daq record --port COM3 --duration 60 -o ~/Documents/spectra/

# Live spectrum visualization in a window
chloros-cli daq live --port COM3 --record

# Dual-sensor reflectance (ambient + object) → JSON Lines
chloros-cli daq reflectance \
  --ambient-eth-host daq-e-field.local \
  --object-eth-host daq-e-canopy.local \
  --record -o ~/Documents/reflectance/

# Convenience: pick integration_time + frame_avg for a target rate
chloros-cli daq sample-rate --port COM3 --target-hz 5

# Calibration profile management
chloros-cli daq calibrate --port COM3 --list
chloros-cli daq calibrate --port COM3 --set field_calibration_2026_05

# DAQ-E network config (mDNS auto-discovers the host)
chloros-cli daq net --eth-host daq-e-xxx.local set-ip --mode static --ip 192.168.2.20
chloros-cli daq net --eth-host daq-e-xxx.local set-name "sky-sensor"
chloros-cli daq net --eth-host daq-e-xxx.local set-ptp --enabled true --domain 0
chloros-cli daq net --eth-host daq-e-xxx.local set-auto-stream true          # auto-stream on boot
chloros-cli daq net --eth-host daq-e-xxx.local set-require-signature         # require factory-signed cal (fw v1.6.0+; refused while the held cal is unsigned)
chloros-cli daq net --eth-host daq-e-xxx.local set-time                      # push host clock (refused when PTP SLAVE)
chloros-cli daq net --eth-host daq-e-xxx.local set-auth-token --current "" --new "s3cret"   # control-channel auth ("" new = disable)
chloros-cli daq net --eth-host daq-e-xxx.local set-ota-password "newpass"    # change OTA password (min 4 chars)
chloros-cli daq net --eth-host daq-e-xxx.local factory-reset                 # clear all NVS settings and reboot
chloros-cli daq net --eth-host daq-e-xxx.local reboot

# OTA firmware update
chloros-cli daq ota --eth-host daq-e-xxx.local \
  --firmware daq_e_1.21.bin --password mapir-daq-e

# Bridge spectra to other protocols
chloros-cli daq serve --port COM3 --tcp-port 9000           # TCP JSON-lines
chloros-cli daq ws    --port COM3 --ws-port 9001            # WebSocket
chloros-cli daq udp   --port COM3 --udp-port 9002           # UDP broadcast
chloros-cli daq mqtt  --port COM3 --broker mqtt.example.com --topic daq/spectrum
```

***

## `chloros-cli project`

Open, connect to, and drive a saved Chloros project (a folder with `cameras.json` + `sensors.json` + `project.json`). Everything routes through the backend so the GUI and CLI produce identical hardware state.

### Subcommand Reference

| Subcommand                                                                                                                                                                                                                                    | Purpose                                                                                                                                                                                                                                                                              |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `project open PATH`                                                                                                                                                                                                                           | Print the project's device manifest (cameras, arrays, sensors).                                                                                                                                                                                                                      |
| `project devices PATH [--reconnect]`                                                                                                                                                                                                          | List or re-run discovery.                                                                                                                                                                                                                                                            |
| `project connect PATH [--cameras-only] [--sensors-only]`                                                                                                                                                                                      | Connect every saved camera / array / sensor.                                                                                                                                                                                                                                         |
| `project capture PATH NAME [-o DIR] [--format FMT] [--exposure US] [--gain DB] [--prefix P]`                                                                                                                                                  | Single capture from a named camera or array.                                                                                                                                                                                                                                         |
| `project burst PATH NAME [-n N] [-i S] [-o DIR] [--format FMT] [--exposure US] [--gain DB] [--prefix P]`                                                                                                                                      | N-frame burst from a named camera or array (`-n/--count` default 5; `-i/--interval` inter-frame seconds, default 0). Array bursts de-dup repeated synced groups (staleness watchdog) so a partial-cycling array can't hand back N copies of one frame; prints per-iteration results. |
| `project stream PATH NAME [-n N] [--fps F] [-o DIR] [--format FMT] [--exposure US] [--gain DB] [--poll-interval S]`                                                                                                                           | Stream-to-disk via a backend job. `--poll-interval` = seconds between `/stats` polls (default 2.0).                                                                                                                                                                                  |
| `project sensor read PATH NAME [--json]`                                                                                                                                                                                                      | Latest spectrum frame.                                                                                                                                                                                                                                                               |
| `project sensor log PATH NAME --seconds SEC [-o DIR] [--device-name NAME]`                                                                                                                                                                    | Record .daq.                                                                                                                                                                                                                                                                         |
| `project run PATH RECIPE.yaml`                                                                                                                                                                                                                | Execute a YAML/JSON capture recipe. `--dry-run` validates without running.                                                                                                                                                                                                           |
| `project align calibrate PATH NAME [--method M] [--model M] [--frames N] [--reference SN] [--max-features N] [--ratio-threshold F] [--ransac-threshold-px F] [--min-matches N] [--max-reproj-err-px F] [--checkerboard RxC] [--name PROFILE]` | Compute alignment for an array — see [the flag table below](#project-align-calibrate-options).                                                                                                                                                                                       |
| `project align status PATH NAME [--json]`                                                                                                                                                                                                     | Print current alignment profile.                                                                                                                                                                                                                                                     |
| `project align clear PATH NAME`                                                                                                                                                                                                               | Drop the cached profile.                                                                                                                                                                                                                                                             |
| `project align tweak PATH NAME --serial SN --dx N --dy N --rotation-deg N --scale N`                                                                                                                                                          | Nudge one slave's transform.                                                                                                                                                                                                                                                         |
| `project align export PATH NAME --to FILE`                                                                                                                                                                                                    | Save the profile to JSON.                                                                                                                                                                                                                                                            |
| `project align import PATH NAME --from FILE [--no-validate]`                                                                                                                                                                                  | Load a saved profile.                                                                                                                                                                                                                                                                |

#### `project align calibrate` Options

| Flag                                                                             | Default       | Description                                                                                                                                                                                |
| -------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--method {feature_orb, feature_akaze, phase_correlation, checkerboard, manual}` | `feature_orb` | Alignment method. **These spellings differ from `lattice align-calibrate`**, which takes the short forms `orb` / `akaze` / `phase`; the two commands are not interchangeable on this flag. |
| `--model {translation, rigid, affine, homography}`                               | `affine`      | Transform model to fit.                                                                                                                                                                    |
| `--frames N`                                                                     | `1`           | Synced frame snapshots to average.                                                                                                                                                         |
| `--reference SN`                                                                 | the master    | Reference camera serial; every other member is warped onto it.                                                                                                                             |
| `--max-features N`                                                               | `5000`        | ORB feature-count cap.                                                                                                                                                                     |
| `--ratio-threshold F`                                                            | `0.75`        | Lowe's ratio test.                                                                                                                                                                         |
| `--ransac-threshold-px F`                                                        | `3.0`         | RANSAC inlier threshold.                                                                                                                                                                   |
| `--min-matches N`                                                                | `15`          | **Quality gate** — refuse the solve below this many inlier matches.                                                                                                                        |
| `--max-reproj-err-px F`                                                          | `4.0`         | **Quality gate** — refuse the solve above this RMS reprojection error.                                                                                                                     |
| `--checkerboard RxC`                                                             | —             | Board geometry for `--method checkerboard`, e.g. `9x6`.                                                                                                                                    |
| `--name PROFILE`                                                                 | empty         | Profile name embedded in the saved JSON. **Not the array name** — that is the positional `NAME`.                                                                                           |

The two quality gates are the reason a calibrate can succeed at solving and still decline to save: a profile that fails either one would silently mis-register every later capture, so it is refused rather than persisted.

### Examples

```bash
# Open a project and see what it knows about
chloros-cli project open "/home/user/Chloros Projects/Field_A"

# Connect everything saved in the project
chloros-cli project connect "/home/user/Chloros Projects/Field_A"

# Capture from a named camera (defined in cameras.json)
chloros-cli project capture "/home/user/Chloros Projects/Field_A" FrontLeft \
  -o output/ --format tiff

# Capture from a named array
chloros-cli project capture "/home/user/Chloros Projects/Field_A" main_rig \
  -o output/ --format tiff

# Capture with overrides
chloros-cli project capture "/home/user/Chloros Projects/Field_A" main_rig \
  --exposure 5000

# Read a spectrum
chloros-cli project sensor read "/home/user/Chloros Projects/Field_A" Sky --json

# Record a DAQ log
chloros-cli project sensor log "/home/user/Chloros Projects/Field_A" Sky \
  --seconds 120 -o ~/Documents/spectra/

# Align an array (live)
chloros-cli project align calibrate "/home/user/Chloros Projects/Field_A" main_rig
chloros-cli project align status "/home/user/Chloros Projects/Field_A" main_rig

# Run a recipe
chloros-cli project run "/home/user/Chloros Projects/Field_A" recipe.yaml
```

### Recipe DSL

`project run RECIPE.yaml` accepts a YAML or JSON file describing a sequence of actions:

```yaml
# recipe.yaml
overrides:
  cameras:
    FrontLeft:
      exposure_us: 5000
      target_brightness: 80

stop_on_error: true
actions:
  - apply:
      name: FrontLeft
      settings:
        exposure_auto: "Off"
        gain: 6.0
        gain_auto: "Off"
  - wait: 2s
  - capture:
      name: FrontLeft
      output: pose_a/
      format: tiff
  - stream:
      name: main_rig
      count: 60
      fps: 5
      output: stream/
  - burst:
      name: main_rig
      count: 10
      interval: 0.5
      output: burst_a/
      format: tiff
  - sensor:
      name: Sky
      action: read
```

Supported actions: `apply`, `wait`, `capture`, `stream`, `burst`, `sensor`. The `burst` action takes `name` (required), `count` (default 5), `interval` (seconds, default 0), `output`, `format`, and `settings` (same per-camera settings shape as `apply`); array bursts use the same fresh-synced-group watchdog as `project burst`.

Run it:

```bash
chloros-cli project run "/path/to/project" recipe.yaml

# Dry-run to validate without firing hardware
chloros-cli project run "/path/to/project" recipe.yaml --dry-run
```

***

## Environment Variables

| Variable                             | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CHLOROS_BACKEND_URL`                | Override the backend URL (default `http://127.0.0.1:5000`) — **honoured only by the `lattice`, `project`, and `daq pool-*` command families.** The core commands (`process`, `login`, `logout`, `status`, `export-status`, `time-sync`, `selftest`) pin `http://127.0.0.1:<port>` and ignore this variable (the IPv4 literal sidesteps the Windows `localhost`→`::1` \~2 s-per-request penalty), so they always target the local machine. |
| `CHLOROS_ARRAY_ALLOW_OVERSUBSCRIBED` | `1` downgrades the array over-subscription connect refusal (aggregate per-cam demand > collision-safe wire ceiling with `pin_resolution`) to a loud warning-and-proceed, accepting GVSP packet loss. Bench use only — see [Array fps & burst model](#array-fps--burst-model).                                                                                                                                                             |
| `CHLOROS_CLI_MODE`                   | Set by the CLI itself; tells the backend to enable parallel processing.                                                                                                                                                                                                                                                                                                                                                                   |
| `CHLOROS_GVSP_PROBE_FALLBACK`        | `0` skips the GVSP fallback probe (ICMP results only). **This turns jumbo off, it doesn't just quieten the log** — the camera answers DF pings only up to 1500 on every path, so this probe is the only thing that can detect jumbo. Saves \~1 s per camera per connect; costs \~1.45× wire ceiling if the network *could* have carried jumbo. The SDK warns when you set it.                                                             |
| `CHLOROS_GVSP_PACKET_SIZE_FORCE`     | Pin GVSP packet size to N bytes; skips probing entirely. Prefer per-command (`CHLOROS_GVSP_PACKET_SIZE_FORCE=9000 chloros-cli …`) over setting it permanently: a pinned size stops adapting to the network in front of it, and pinning 9000 on a path that can't carry jumbo makes **every** capture time out with `SC_ERR_TIMEOUT -1011`.                                                                                                |
| `TMPDIR` (Linux)                     | Override Nuitka onefile extraction dir. The CLI auto-uses `/mnt/ssd/tmp` if present.                                                                                                                                                                                                                                                                                                                                                      |

***

## Exit Codes

| Code  | Meaning                                   |
| ----- | ----------------------------------------- |
| `0`   | Success.                                  |
| `1`   | Generic failure (most subcommand errors). |
| `2`   | Argument error.                           |
| `130` | Interrupted by Ctrl+C.                    |

***

## Troubleshooting Pointers

* **"Login required"** → Run `chloros-cli login EMAIL PASSWORD` once on this machine.
* **"backend unreachable"** → Start the Chloros desktop app, or run the backend binary directly (`chloros-backend`), or check `CHLOROS_BACKEND_URL` if remote.
* **`lattice` commands fail with "LATTICE camera drivers not found"** → The Arena SDK runtime isn't installed; the CLI ships with `win32api` bundled on Windows but the C runtime is part of the GUI installer.
* **Array connect / Array Settings shows "FRAMES WILL DROP" or "Reduce ROI to enable"** → Host NIC receive ring is too small (commonly reset to 32 after a NIC driver update). See [Host NIC Setup & Tuning](#host-nic-setup--tuning-lattice-arrays) — set `ReceiveBufferLen=256`, `PendingReceives=64`.
* **Machine hangs on restart/shutdown, then WMI `Invalid class` / NIC won't enable / USB drives missing** → Outdated USB 10GbE adapter driver causing `DRIVER_POWER_STATE_FAILURE` (BSOD `0x9F`). Update the adapter driver — see [Host NIC Setup & Tuning](#host-nic-setup--tuning-lattice-arrays).
* **Jetson swap warning** → Add file-backed swap; the CLI prints the exact `fallocate` / `swapon` commands.
* **DAQ direct commands missing** → Expected: the shipped `chloros-cli` deliberately excludes the `daq` package, so only `pool-*` is present (the PyPI SDK does not carry it either). Use `pool-*`, which drives the same sensor through the backend, or `chloros_sdk.connect_daq_sensor()` from Python.

***

## See Also

* [Python SDK Reference](/chloros/reference-cli-and-sdk/sdk-reference.md) — programmatic equivalent of every CLI command.
* [DAQ Sensor Guide](/chloros/daq-light-sensors/daq.md) — sensor-specific wiring + calibration.
* Online docs: `https://mapir.gitbook.io/chloros/cli`
