# v0.3.7

- **The `exp_rel_*` ceiling is a 16-bit Timer 1 overflow, and is now derived rather
  than guessed.** The relative exposure scales the *device's own* absolute exposure
  time into a 16-bit count:

      timer = exposure_time * exp_rel / 100      (mod 65536)

  so the usable maximum is `65535 * 100 // exposure_time` -- 1598 for the 4100 a
  ProScan 10T reports. Past it the register wraps and the exposure *drops*, and a
  wrapped result below 100 meets the same floor that makes `exp_rel=50` behave as
  100. A binary search puts the step at exactly 1598/1599, for all three channels,
  which is where the product crosses 65535. The model predicts every measurement
  taken to within 4%:

      exp_rel   1086   1500  |  1598  1599  |  2200  3300  5000  7500  10000
      predicted x10.9  x15.0 | x15.98 x1.00 | x6.02 x1.03 x2.05 x11.1  x4.09
      measured  x10.5  x14.7 |   clip x1.00 | x5.79 x1.06 x2.01 x10.7  x3.94

  New `option.max_relative_exposure(exposure_time)`; `MAX_TESTED_RELATIVE_EXPOSURE`
  (a hard-coded 1500, itself a raised guess from 1086) is replaced by
  `MAX_RELATIVE_EXPOSURE`, the ceiling implied by `OBSERVED_EXPOSURE_TIME`, used by
  `OptionsTable.validate()` which has no device reading available
- `auto_exp` clamps each channel to the ceiling the device's own reported exposure
  time implies, per channel, and reports the shortfall. A scanner that has not
  warmed up reports no exposure time at all, which says nothing about the register
  it uses, so the observed ceiling is assumed and said out loud rather than
  abandoning the correction
- This also explains why `exp_time_*` looked inert but is not irrelevant: the device
  ignores what is written to it and keeps its own value, and that value is the
  multiplicand deciding where `exp_rel_*` overflows

# v0.3.6

- **`auto_exp` now meters, and it works.** It runs a preview pass at the device's
  preview resolution with `exp_rel_*` at 100, takes the 99th percentile of each
  colour plane, and scales that channel's relative exposure to land it at 85% of
  full scale. Per channel, no cap on the correction beyond the measured-linear
  range, and no dependency on anything the device reports about itself. On a PIE
  ProScan 10T against a colour negative it derives 247/563/1086, which puts all
  three channels within 1% of each other at ~88% of full scale with nothing
  clipped -- where untouched they sit at 4.39 : 1.93 : 1.00 with red at 34%
- **`exp_rel_*` is the exposure control on this hardware, and `gain_*`,
  `exp_time_*` and `light` are inert.** Measured by sweeping each in isolation at
  300 dpi / 16-bit: gain 10/30/60, exp_time 500/1500/2500/2937/6000/10000 and
  light 4/5/6/7 all produce the same image to within 0.2%, while `exp_rel_*` is
  linear to within 4% and clamps at the bottom -- 50 behaves as 100. The top is set
  by a Timer 1 overflow, explained in v0.3.7. `exp_rel_*` is the
  per-line integration period, so the line rate halves as it doubles. SANE
  hard-codes all three to 100 and never varies them (`pieusb.c:878-882`), which is
  why neither backend could ever expose this film
- **`auto_exp` no longer adopts the device's own calibration.** `gain_*`,
  `exp_time_*`, `offset_*` and `light` from GET GAIN OFFSET are inert, so the
  v0.3.5 behaviour was a no-op that cost a command. `Scanner._adopt_device_calibration`
  and `_set_from_device` are gone; `CALIBRATION_OPTIONS` is replaced by
  `METERED_OPTIONS`, the three `exp_rel_*` names, which are restored after the scan
  that derived them. The v0.3.5 note that the light byte was "worth roughly the
  entire exposure range" was wrong -- it came from a comparison confounded by
  resolution. Sending 0 was still incorrect and 4 is still the right default
- Shading correction counts and warns about clamped samples. Clipping is not
  uniform: the per-column gain is >1 where the lamp falls off, so edge columns
  reach the ceiling at a lower raw value than centre columns, and highlights near
  full scale clip column by column and read as vertical banding rather than flat
  white. Measured at `exp_rel` 400, red clipped 12.7% of samples, concentrated at
  the left and right edges with 55 percentage points of column-to-column swing
- `OptionsTable.validate()` no longer warns on every `exp_rel_*` away from 100 --
  it is meant to be moved. It now warns only above the Timer 1 ceiling (see v0.3.7)
  or below 100, where the device clamps and the setting does nothing. Warned about
  rather than refused, since the real ceiling depends on a value only Scanner reads
- The per-scan settings line now includes the relative exposures, since `auto_exp`
  restores them afterwards and the log is the only record of what a scan ran with
- The high-resolution stall is a device characteristic with no host-side fix: the
  host is unattended for 1.9% of a 5000 dpi scan, the device's buffer never fills
  (it drains to 0 every fifth read), and the C backend stalls identically at
  matched bit depth. It is also self-correcting -- raising `exp_rel_*` slows the
  line rate to match the readout, and a 5000 dpi scan at x11.1 took 150 s against
  190 s at 100%, because the reversals stop

# v0.3.5

- **`auto_exp` is now "scan with the calibration the scanner reports for itself"**,
  and no longer runs a preview pass. It reads GET GAIN OFFSET and sends the gain,
  exposure, offset and light it finds there -- `SCAN_CALIBRATION_AUTO`
  (`pieusb_specific.c:1988`), which is the C backend's *default* calibration mode
  (`:733`) and what its working scans run with. The firmware optimises those values
  per channel while warming up, on this lamp at its current temperature, so they
  beat anything derivable from outside. Without `auto_exp` the options are sent
  exactly as set, unchanged
- **Removed the "calibration from preview" port**, along with `pieusb.calibration`
  and the extra pass it cost. That path divides by `settings.saturationLevel`, and
  this hardware never populates it: GET GAIN OFFSET reports `0-0-0` for it cold and
  warm alike -- confirmed on a PIE ProScan 10T, in the C backend's own captured
  response (`pieusb_scancmd.c:1136-1146`), and in a SANE trace of the C reading it.
  A zero there makes `dg` 0, and `updateGain2` turns a `dg` of 0 into gain 0 and
  exposure 0. There was nothing for a preview to meter against, so there is no
  longer a preview. `ScanPhase.METERING` is kept for compatibility but never
  emitted; a scan now reports one sweep of the phases, not two
- **Fixed the light level being sent as 0 on every scan.** Byte 15 of SET GAIN
  OFFSET is the lamp level; it was hard-coded to 0 with a `# Light, maybe in SANE
  is 5?` comment. The C never sends 0 -- AUTO echoes back what GET GAIN OFFSET
  reported at byte 75 (`pieusb_scancmd.c:1048`, `1111`), and the DEFAULT and OPTIONS
  modes send `DEFAULT_LIGHT`/`OPT_LIGHT`, both 4. The documented band is 4..7: the
  firmware starts at 7 or 6 and decrements as the lamp warms, settling at 4, where
  "the scanner produces stable scans" (`pieusb_scancmd.h:208-213`). This
  under-exposed *every* scan, manual ones included. Measured on hardware: a scan at
  the *minimum* exposure (2937) with light 4 reaches the same level as one at
  maximum exposure with light 0 -- the byte was worth roughly the entire exposure
  range
- New `light` option, default 4, so the lamp level is settable and appears in the
  option table. `auto_exp` adopts the scanner's own value over it
- `extraEntries` (byte 16) and `doubleTimes` (byte 17) are both sent as 0, which is
  what a backend trace of a working C scan shows going out with the device's own
  gain and exposure. Not the `DEFAULT_ADDITIONAL_ENTRIES` of 1 from
  `pieusb_specific.h:108`: only the DEFAULT and OPTIONS calibration modes assign
  that, while AUTO leaves the field alone because `get_gain_offset()` does not
  decode it. The SET GAIN OFFSET payload now matches a captured C payload byte for
  byte
- A cold scanner reports zeros for its whole calibration -- exposure
  `(0, 0, 0, 4100)`, gain `(0, 0, 0, 15)`, light 0 -- and fills the real values in
  during its first scan. `auto_exp` detects that, warns, and scans with the options
  as set rather than adopting zeros. Practical consequence: the first `auto_exp`
  scan of a session is not calibrated, so scan twice and discard the first
- Every scan logs the exposure, gain, offset and light it actually sent, at INFO.
  `auto_exp` restores those options afterwards, so the log is the only place the
  values a scan ran with can be seen after the fact
- `tests/exercise.py`: new `--gain-offset` prints the scanner's own saturation
  levels, exposure times, gain, offset and light (pair with `--dry-run` for the
  readout alone). Console logging now shows INFO, which is where the line above
  lands; `-v` is still needed for the per-command DEBUG chatter
- `tests/exercise.py` also writes `<prefix>_rgb.tif` (and `_ir.tif`) next to the
  .npy: uncompressed TIFF at the scan's own bit depth, pixels exactly as scanned.
  Hand-rolled, because the package has no image-library dependency and Pillow
  cannot write 16-bit RGB TIFF -- it drops the low byte of every sample, and
  mis-reads such files too, which is worth knowing if you have been inspecting
  scans through it

# v0.3.3

- Relaxed python and numpy minimum requirements

# v0.3.2

- Made it possible to set the resolution to the maximum reported by the inquiry

# v0.3.1

- **Breaking:** `UpdateData.scanned_lines`/`total_lines` are replaced by a single
  `progress` float, 0.0 -> 1.0 within the current `phase`. Line counts only ever
  meant anything during SCANNING, which left every other phase reporting `None`
  and each consumer writing the same division; a fraction is what a progress
  indicator wants and it now applies to every phase. CALIBRATING reports the
  shading read's fraction and WARMING_UP its progress through the START SCAN
  retry budget; phases whose length the device does not announce report 0.0
- **Breaking:** the `calibrate` option is replaced by `reuse_calibration`, with
  the opposite polarity and the same default behaviour (`calibrate=True` is
  `reuse_calibration=False`, still the default). The old name promised something
  it could not deliver: skipping calibration is a request, not a decision, and
  the option now says so
- Skipping calibration is honoured, where before it changed the SET MODE quality
  bit and nothing else. A pass acquires a shading reference unless
  `reuse_calibration` is set AND one is already cached; the scanner can still
  refuse, which it signals by answering START SCAN with MUST_CALIBRATE, and then
  the pass calibrates anyway. `ScanPhase.CALIBRATING` is reported only when a
  pass actually calibrates
- Shading references are cached on the `Scanner` for the life of the device
  session, so a pass that skips calibration corrects from the one an earlier pass
  read. Following the C backend, which keeps `shading_ref`/`shading_mean` on the
  open device handle (`pieusb_specific.h:292-294`). Practical consequence: keep
  one `Scanner` open across a batch, since a fresh one starts with a cold cache
  and calibrates on its first scan regardless of the option. The cache is dropped
  if the device starts reporting a different shading width, and an unusable read
  keeps the previous reference rather than falling back to raw pixels
- With `auto_exp`, the metering pass calibrates and the real pass reuses that
  reference: one calibration per scan instead of two
- `set_options()` takes `skip_shading_analysis` as a keyword argument, since
  whether a pass can skip depends on the Scanner's cache and not on an option
  alone
- The scan sequence proper is now `Scanner._scan_pass(started, emit)`, shared by
  the real scan and the auto-exposure metering pass, which differ only in the
  options in force and in the `emit` callback that labels their progress.
  Replaces the `_phase_override` attribute that relabelled updates inside
  `_emit()`
- Implemented the `auto_exp` option, which until now warned and did nothing. It
  runs a preview pass at the device's preview resolution, measures the 99th
  percentile of each colour plane against the CCD saturation levels, and derives
  new `gain_*`/`exp_*` settings before the real scan. A port of the SANE
  backend's "calibration from preview" path (`sanei_pieusb_analyze_preview`,
  `sanei_pieusb_set_gain_offset`, `updateGain2`), in the new
  `pieusb.calibration` module. The preview pass reports its progress under a new
  `ScanPhase.METERING` so it is distinguishable from the real scan
- `_get_gain_offset()` now also returns the `saturation_level` triple that
  auto-exposure meters against (GET GAIN OFFSET byte 54)
- **Breaking:** split the exposure options, which conflated two independent
  device controls behind one name. `exp_r`/`exp_g`/`exp_b`/`exp_i` are gone;
  there are now two families:
  - `exp_time_r`/`_g`/`_b`/`_i` -- ABSOLUTE integration time in Timer 1 counts,
    carried by SET GAIN OFFSET. This is the real exposure control and the one
    auto-exposure moves. Its default changes from 100 to 2937 (SANE's
    DEFAULT_EXPOSURE, the value the firmware itself falls back to) and its upper
    bound is the inquiry maximum times 4 -- without which the device's own
    default is outside the range the device reports
  - `exp_rel_r`/`_g`/`_b` -- RELATIVE exposure percentage, sent by the
    SCSI_EXPOSURE write. No infrared entry, because the device has none. **Leave
    this at 100.** SANE hard-codes it and exposes no option for it, so no other
    value has been exercised against this hardware; it is redundant with
    `exp_time_*`; and auto-exposure's saturation reference assumes it is at 100.
    `OptionsTable.validate()` warns once per scan if it is moved
  The previous single option fed *both* commands, so the value written to the
  SET GAIN OFFSET exposure time was the relative percentage's 100 rather than an
  exposure time. Existing callers setting `exp_*` must rename to `exp_time_*`
- Added `Unit.PERCENT` and `Unit.TIMER_COUNTS`; exposure times are not
  expressible in microseconds without the Timer 1 clock rate, which the device
  does not report

# 0.2.0

- Added wait() API to block until the scan is finished
- Added close(), which cancels a running scan and waits for the worker before
  releasing the USB interface. `with Scanner(...)` now goes through it, so
  leaving the block mid-scan no longer closes the device under the worker
- Fixed Scanner construction: the option attribute accessors recursed on
  `self.params` before the option table existed, so `Scanner(info)` always
  raised RecursionError
- Reading an option attribute returns its value rather than the internal
  Parameter, and an unknown name raises AttributeError rather than KeyError
- Setting an option while a scan is running raises ScanInProgress

# 0.1.0

Initial release
