Skip to content

feat(toupcam): global reset mode (ENABLE_TOUPCAM_GLOBAL_RESET) and packed 12-bit (MONO12PACKED) - #705

Merged
Alpaca233 merged 5 commits into
Cephla-Lab:masterfrom
Alpaca233:feature/toupcam-global-reset-mono12packed
Oct 17, 2026
Merged

Alpaca233 merged 5 commits into
Cephla-Lab:masterfrom
Alpaca233:feature/toupcam-global-reset-mono12packed

Conversation

@Alpaca233

@Alpaca233 Alpaca233 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Ports the ToupCam global reset mode and packed 12-bit (MONO12PACKED) support that was developed on the bench (against an older master, on an ITR10CMOS26000KMA) onto current master, with a _def switch for global reset.

Global reset mode

  • New flag ENABLE_TOUPCAM_GLOBAL_RESET = False in control/_def.py (set enable_toupcam_global_reset = True under [GENERAL] in the machine ini to turn it on). Default off, so existing ToupCam setups are unchanged.
  • Available on every ToupCam model: when enabled, the driver turns TOUPCAM_OPTION_GLOBAL_RESET_MODE on in hardware trigger mode and off otherwise. With global reset every row starts exposing at once, so the strobe time and trigger delay are 0, but the rows keep integrating until read out, so the image is only flat if the light is off before the readout starts. Only hardware trigger mode does that (the microcontroller pulses the illumination for exactly the exposure); software trigger and continuous mode keep the light on through the readout, where global reset would shade the image top to bottom and halve the frame rate.
  • The option is changed with the stream paused (via _pause_streaming, which also gates triggers). A camera that rejects the option (E_NOTIMPL, or any error on the startup write) logs it once and is not asked again; a later write that fails for any other reason leaves the driver's state matching the camera's and raises.
  • A resolution with no rolling-shutter timing entry (the ITR10's 6208x4168) now yields a zero strobe plus a one-time warning instead of a ZeroDivisionError.
  • The precise-framerate reset is factored into _reset_precise_framerate so both early returns still reset it.

MONO12PACKED (ITR10CMOS26000KMA only)

  • New CameraPixelFormat.MONO12PACKED (bit depth 12) and ToupcamCameraModel.ITR10CMOS26000KMA.
  • The ITR10 joins the driver's _DELIVERED_PIXEL_FORMAT table as (MONO12PACKED, MONO12): above 8 bits it offers and runs only the packed 12-bit mode unbinned (the camera has RAW8, RAW16 and RAW12PACK, no plain RAW12) and the 16-bit mode carrying 12-bit data when binned, where the packed mode is not available. Any mono request above 8 bits on the ITR10 resolves to those, the same way MONO16 resolves to MONO12 when binned on the ITR3. The mode is tied to the model; no SDK capability flag is consulted.
  • The mode is selected with TOUPCAM_OPTION_PIXEL_FORMAT = RAW12PACK; setting BITDEPTH takes the camera back to RAW8/RAW16. set_binning leaves the packed mode before resizing and re-enters it after.
  • PullImageV2 unpacks to [0, 4095]; the frame callback shifts up 4 bits so frames sit on the same left-aligned scale as MONO12 (max_value = 65520), so intensities don't jump when binning changes.
  • Black level in the packed mode is scaled by 16 (12-bit SDK scale); the 16-bit modes keep the ADC-depth rule from Remove napari live view; port overexposure indicator and alignment (with auto-align) to the pyqtgraph viewer #627.
  • A pixel-format change, including the ITR10's binning transitions, keeps the black level last set: the driver stores it on the 8-bit scale (the config default at startup), the way it already stores the exposure time and pixel format, and re-sends it on the new mode's scale after the switch. On master a pixel-format change reset it to the config default.
  • Other ToupCam models are unchanged: their format list and delivered-format behaviour are exactly as on master.

No GUI changes: the camera settings widget already re-lists and re-selects the delivered format after a pixel-format or binning change.

Not ported (out of scope here, in the same bench file)

The bench driver also carried ITR10 trigger-robustness changes: waiting for an in-flight frame before exposure/gain changes, restarting the stream when a trigger times out, pausing the stream on every acquisition-mode change, and re-pointing the trigger source at software. Those are separate behaviour changes and are not planned for this PR.

Verification

  • New unit tests: tests/control/test_camera_toupcam_global_reset.py (strobe shortcut, unknown-resolution fallback, the _def gate, hardware trigger only, stream pause ordering, unsupported camera, failed later writes) and additions to tests/control/test_camera_toupcam_pixel_format.py (ITR10 rows in the delivered-format and offered-format tables, the SDK mode a 12-bit request selects per binning, the leave/enter ordering around a binning change, black level factor, black level kept across binning and pixel-format changes, packed frame scaling through the real _on_frame_callback).
  • pytest tests/control/test_camera_toupcam_*.py tests/control/test_multi_camera_models.py tests/squid tests/control/test_def.py tests/control/test_gui_camera_settings_restore.py: 441 pass, 1 skip. tests/control/test_widgets.py: 160 pass; the one failure (test_check_ram_available_with_error_dialog_insufficient_ram) fails identically on clean master in this environment.
  • Not verified on hardware in this PR. The behaviour matches the bench implementation that was run on an ITR10CMOS26000KMA, with two things worth a quick check on the ITR10 before merge: the black-level factor for MONO12PACKED (16, the 12-bit SDK scale, rather than the ADC-depth rule), and the binned entry of the table (MONO12 in the 16-bit mode), which is taken from the bench driver's packed-vs-MONO12 scaling rather than the low-bits check the table's comment asks for.

🤖 Generated with Claude Code

Alpaca233 and others added 2 commits October 16, 2026 13:51
…cked 12-bit (MONO12PACKED)

Global reset: every row starts exposing at once, so triggered frames need no
rolling-shutter strobe delay. Opt-in through the new ENABLE_TOUPCAM_GLOBAL_RESET
flag in _def (settable from the ini). The driver turns it on in the trigger
modes and off in continuous mode, where it would halve the frame rate and shade
the image under steady light. A camera that rejects the option is not asked
again. A resolution without a rolling-shutter timing entry (the
ITR10CMOS26000KMA's 6208x4168) now gets a zero strobe with a warning instead of
a division by zero.

MONO12PACKED: 12-bit pixels packed on the wire (TOUPCAM_PIXELFORMAT_RAW12PACK)
to save USB bandwidth, on cameras with TOUPCAM_FLAG_RAW12PACK. The SDK unpacks
them to uint16 and the driver shifts them up 4 bits so frames sit on the same
left-aligned scale as MONO12. The mode is only valid at 1x1 binning, so 12-bit
resolves to MONO12PACKED at 1x1 and MONO12 at any other binning; set_binning
leaves the packed mode before resizing and re-enters it after.

Also adds ToupcamCameraModel.ITR10CMOS26000KMA and the PIXEL_FORMAT /
GLOBAL_RESET_MODE options to the startup option dump.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…2PACK capability probe

The ITR10CMOS26000KMA joins _DELIVERED_PIXEL_FORMAT as (MONO12PACKED, MONO12), so above 8 bits it offers and
runs only the packed mode unbinned and the 16-bit mode binned, the same way the ITR3 relabels MONO16 to MONO12
when binned. The SDK flag check and _pixel_format_for_binning go away; other models keep master's format list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Alpaca233
Alpaca233 requested a balanced review from Copilot October 17, 2026 01:06
…fter review

- The strobe calculation resets the precise framerate once, on the rolling-shutter path only; under global
  reset it no longer touches the SDK on every exposure change.
- Global reset state starts unknown so the first write always lands, replacing the startup probe of the
  option. _set_global_reset takes the acquisition mode, so the trigger-modes-only policy lives in one place.
- The packed 12-bit frame is shifted in place on the processed copy instead of allocating the full frame a
  second time per frame.
- _refresh_pixel_format was dead (every writer already stores the delivered format); _configure_camera reuses
  _change_pixel_format; set_binning tests both conditions on locals rather than on a side effect.
- The ToupCam tests share one camera skeleton, capabilities builder and frame-callback harness
  (tests/control/toupcam_test_utils.py) instead of three copies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Software-trigger illumination, black-level preservation, and failed global-reset transitions need correction.

3 open issues
What changed in this PR

Adds optional ToupCam global reset and ITR10 packed 12-bit support to the camera driver.

Changes:

  • Adds configurable global reset and unknown-resolution timing fallback.
  • Adds packed-format selection, binning transitions, and frame scaling.
  • Expands driver tests and shares camera test helpers.
File Description
software/​tests/​control/​toupcam_test_utils.py Adds shared fake-camera builders.
software/​tests/​control/​test_camera_toupcam_pixel_format.py Tests packed formats, binning, and scaling.
software/​tests/​control/​test_camera_toupcam_global_reset.py Tests reset configuration and timing behavior.
software/​tests/​control/​test_camera_toupcam_buffer_race.py Reuses shared callback helpers.
software/​squid/​config.py Adds ITR10 and MONO12PACKED definitions.
software/​control/​camera_toupcam.py Implements global reset and packed-format handling.
software/​control/​_def.py Adds the opt-in global-reset flag.

Review effort: Balanced

Comment thread software/control/camera_toupcam.py Outdated
the frame rate (measured on an ITR10CMOS26000KMA). The option is changed with the stream stopped, so a
change costs a stream restart.
"""
enabled = acquisition_mode != CameraAcquisitionMode.CONTINUOUS and ENABLE_TOUPCAM_GLOBAL_RESET
Comment on lines +598 to +604
except toupcam.HRESULTException as ex:
self._log.info(
f"Global reset mode not supported by this camera: {control.toupcam_exceptions.explain(ex)}"
)
self._global_reset_supported = False
self._global_reset_enabled = False
return
Comment on lines +833 to +841
old_format = self._pixel_format
new_format = self._delivered_format(old_format, (binning_factor_x, binning_factor_y))
# MONO12PACKED only works at 1x1, so leave it before resizing and enter it after.
if old_format == CameraPixelFormat.MONO12PACKED and new_format != old_format:
self._change_pixel_format(new_format)
self._raw_set_resolution(width, height)
if new_format == CameraPixelFormat.MONO12PACKED and new_format != old_format:
self._change_pixel_format(new_format)
self._pixel_format = new_format
Alpaca233 and others added 2 commits October 16, 2026 19:53
…on failed writes, keep black level

Addresses the Copilot review on Cephla-Lab#705.

- Global reset is only enabled in hardware trigger mode. In software trigger mode
  the illumination stays on until the frame has been read (and between frames in
  live), so the rows read out last integrate longer and the image shades top to
  bottom, the same as in continuous mode. Only hardware trigger pulses the light
  for exactly the exposure.
- A global reset write that fails after a successful one no longer marks the
  option unsupported and the mode off: only E_NOTIMPL does, any other error keeps
  the last known state (which still matches the camera) and raises. The first
  write at startup stays forgiving so a camera that answers an unknown option
  with another code can still open.
- A pixel format change, including the ITR10's binning transitions, keeps the
  current black level instead of re-applying the config default: it is read back
  in user units before the SDK mode switch and re-sent on the new scale after.
  The default is applied at startup only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n a mode switch

- The driver keeps the black level last set (the config default at startup) on
  the 8-bit scale, the way it already keeps the exposure time and pixel format,
  and re-sends it after an SDK mode switch. This drops the read-back, the
  startup-only parameter on _change_pixel_format and one SDK call per switch.
- The reason global reset is hardware-trigger only is explained once, in
  _set_global_reset; the _def comment, the strobe comment and the tests state
  the policy only.
- The two first-write-failure tests are one parametrized test; the constructor
  comment on when the option is marked unsupported is brought up to date.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Alpaca233
Alpaca233 merged commit 4b61045 into Cephla-Lab:master Oct 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants