Skip to content

EOS R: HDR exposure bracketing + manual shutter control - #280

Open
jed2nd wants to merge 4 commits into
reticulatedpines:devfrom
jed2nd:r-hdr-bracketing
Open

EOS R: HDR exposure bracketing + manual shutter control#280
jed2nd wants to merge 4 commits into
reticulatedpines:devfrom
jed2nd:r-hdr-bracketing

Conversation

@jed2nd

@jed2nd jed2nd commented Jun 18, 2026

Copy link
Copy Markdown

What this does

Enables working exposure (HDR) bracketing on the EOS R (FW 1.8.0), plus the shutter-control and capture pieces it needs.

The core fix: PROP_SHUTTER encoding

On the R, PROP_SHUTTER is a 2-byte value whose high byte is the shutter as a signed Tv in 1/3-stop units (0 = 1", +3 = 0.5", -3 = 2", verified on camera). ML had been writing the value in the low byte, so the camera always saw Tv 0 and every frame came out 1" — which is why bracketing produced no exposure variation. This converts at the property boundary (write and read), leaving ML's internal raw scale unchanged.

Included

  • lens.c — hi-byte-Tv encode/decode; CONFIG_R capture via the CameraConductor remote-release event with an early return so bracket frames sequence cleanly; coerce out-of-range shutter writes to the camera limit.
  • shoot.c — detect the user's shot via the card file_number (the R's job_state notifications are unreliable) to fire the bracket; read the M-mode dial via a brief half-press so the bracket centers on the user's shutter; drop the MENU_MODE cancel check on R; auto frame count → fixed 5-frame symmetric bracket.
  • features.h / property_whitelist.h — enable HDR bracketing + intervalometer, allow PROP_SHUTTER.

Comparison with #279, and why this one

#279 fixes the single-capture "saving…" hang on the R using the same CameraConductor remote-release event. This PR carries an equivalent capture fix and goes further:

#279 This PR
Single-capture saving-hang fix ✅ remote-release ✅ same remote-release event
Multi-frame sequencing falls through to the generic post-capture waits adds a settle + early return that skips those waits — without it the R stalls after frame 1, so a bracket/intervalometer can't fire a sequence
PROP_SHUTTER encoding fix ✅ hi-byte Tv (without it every frame is 1")
HDR exposure bracketing
Dial-centered bracket + intervalometer
Hardware test coverage single capture full 11-point matrix (comment below)

So this is a superset: an equivalent remote-release capture fix, plus the early-return refinement and the shutter-encoding fix that multi-frame bracketing requires. It can replace #279, or be rebased onto it if you'd rather land #279 first — happy to do whichever.

(Minor: #279 wires the remote-release as a named stub SetEventIrRemoteReleaseBtn; this PR currently calls it by address 0xE0190215 with a comment — trivial to switch to the named stub if you prefer.)

Testing

Validated on an EOS R (FW 1.8.0) — full functional matrix in the comment below (single capture, 5-frame HDR bracket centered on the dial with varying exposures, intervalometer, clean power-off).

Known limitations

  • The R doesn't report the shutter dial to ML at idle, so the bracket reads it via a brief half-press at the start (a short pause before the first frame).
  • Bracketing from a very fast base (e.g. 1/2000) clamps the fast-end frames at 1/8000 (a physical limit), handled so no frame is wasted.
  • Auto frame-count is a fixed 5-frame bracket on the R (the histogram auto-metering path is unreliable on this body).

Enables working exposure (HDR) bracketing on the EOS R.

The key fix is the R's PROP_SHUTTER encoding: it is a 2-byte value whose
high byte is the shutter as a signed Tv in 1/3-stop units (0 = 1"). ML had
been writing the value in the low byte, so the camera always saw Tv 0 and
every frame came out 1". lens.c now encodes/decodes PROP_SHUTTER at the
property boundary, keeping ML's internal raw scale unchanged.

- lens.c: hi-byte-Tv encode/decode (ml_raw_to_r_shutter16 /
  r_shutter16_to_ml_raw); CONFIG_R capture via the CameraConductor
  remote-release event (SetEventIrRemoteReleaseBtn, 0xE0190215) with an
  early return so bracket frames sequence cleanly; coerce out-of-range
  shutter writes to the camera limit.
- shoot.c: detect the user's shot via the card file_number (the R's
  job_state notifications are unreliable) to fire the bracket; read the
  M-mode dial via a brief half-press so the bracket centers on the user's
  shutter; drop the MENU_MODE cancel check on R; treat auto frame count as
  a fixed 5-frame symmetric bracket.
- features.h: enable HDR bracketing + intervalometer for R.180.
- property_whitelist.h: allow PROP_SHUTTER.

Tested on EOS R (FW 1.8.0): single capture (clean finalize), 5-frame HDR
bracket centered on the dial with varying exposures, intervalometer, and
clean power-off.
@jed2nd

jed2nd commented Jun 18, 2026

Copy link
Copy Markdown
Author

Test matrix — EOS R, firmware 1.8.0 (7.3.9)

All run on hardware. Setup: M mode, manual focus, scene with tonal range.

# Feature Expected Result
1 Single photo + clean finalize Saved; no "saving…" hang at power-off ✅ PASS
2 Shutter value applies EXIF matches the set shutter ✅ PASS
3 HDR bracket — centered, 1/125 base, 5 frames 5 images stepping dark→bright, centered on 1/125 ✅ PASS
4 HDR bracket — 3 frames 3 images ✅ PASS
5 HDR bracket — fast base (1/2000) clamp Fast frames clamp at 1/8000, no wasted duplicate ✅ PASS — two frames landed at 1/8000 (−2EV and −4EV both exceed the limit; expected)
6 HDR bracket — centers on the dial (1/500) Centers on 1/500, not a fixed default ✅ PASS
7 HDR bracket — reliability ×3 Full set each time, clean power-off ✅ PASS
8 Exposure ordering All exposures present and correct ✅ PASS — fired in ML's sequence order (center, −, +, −−, ++), not monotonic brightness; HDR tools sort by EXIF
9 Intervalometer ~5 frames spaced ~3 s, Leave-Menu trigger ✅ PASS — 4 frames, triggered on menu leave
10 Single photo after bracketing Normal capture still works ✅ PASS
11 Clean shutdown No saving hang ✅ PASS

Net: single capture, correctly-centered 5-frame HDR bracket with varying exposures (verified across the 1/8000–30" range), intervalometer, and clean power-off all working.

@jed2nd

jed2nd commented Jun 18, 2026

Copy link
Copy Markdown
Author

Re-ran a smoke test on the final stripped branch build (the exact PR content, with the dev probes removed): single capture + clean power-off, 5-frame HDR bracket (centered on the dial, exposures stepping dark→bright), and intervalometer all confirmed working on the EOS R. No behavioral change from the matrix above.

@jed2nd

jed2nd commented Jun 18, 2026

Copy link
Copy Markdown
Author

@kitor @reticulatedpines

Prefer this PR over #279

Gate the R shutter-encoding and HDR-bracket adaptations on semantic
capability flags (CONFIG_PROP_SHUTTER_HI_BYTE, CONFIG_HDR_BRACKET_VIA_FILENUMBER)
defined in platform/R.180/internals.h, instead of scattered CONFIG_R.
Also: remove a leftover BRKCANCEL.TXT debug log, restore the shared
prop_set_rawshutter length (the R uses its own path), and use a named
SetEventIrRemoteReleaseBtn stub instead of a raw address.
@jed2nd

jed2nd commented Jun 18, 2026

Copy link
Copy Markdown
Author

Pushed a follow-up to reduce the R special-casing:

  • The shutter-encoding and HDR-bracket adaptations are now gated on semantic capability flags (CONFIG_PROP_SHUTTER_HI_BYTE, CONFIG_HDR_BRACKET_VIA_FILENUMBER) defined in platform/R.180/internals.h, instead of scattered CONFIG_R. CONFIG_R now appears only in the capture #elif (the existing per-body release chain alongside CONFIG_5D2/CONFIG_40D).
  • Removed a leftover BRKCANCEL.TXT debug log that had slipped through.
  • Restored the shared prop_set_rawshutter length to 4 (the R takes its own early-return path, so that change to the generic path was unintended).
  • Replaced the raw 0xE0190215 address with a named SetEventIrRemoteReleaseBtn stub.

Behavior-preserving for the R (the flags compile the same blocks); builds clean under -Werror.

jed2nd added 2 commits June 19, 2026 09:51
The R's PROP_ISO is a 4-byte value with the ISO code in byte 1 (val = code<<8),
code 15 = ISO 100, +3 per stop -- the same high-byte trick PROP_SHUTTER uses.
The R also broadcasts PROP_ISO on the dial, so read works once the handler
decodes byte 1. Mirrors the CONFIG_PROP_SHUTTER_HI_BYTE pattern.

- internals.h: CONFIG_PROP_ISO_BYTE1 capability flag.
- lens.c: ml_raw_to_r_iso / r_iso_to_ml_raw (ML APEX raw <-> R byte-1 code),
  r_set_rawiso; PROP_HANDLER(PROP_ISO) decodes byte 1; prop_set_rawiso routes
  through r_set_rawiso under the flag.
- property_whitelist.h: PROP_ISO out of prop_handler_deny[] (read + length cache)
  and into prop_write_allow[] (write).

Validated on an EOS R (FW 1.8.0): boots with PROP_ISO un-denied, ML reads the
dial ISO, and lens_set_rawiso sets ISO 100/400/800/3200 with exact read-back.
The R's PROP_APERTURE is a 2-byte value with the Av code in byte 1 (val =
code<<8), 3 per stop: code 9=f/2.8, 18=f/8, 27=f/22 = round((ml_raw-8)*3/8) --
the same byte-1 / *3/8 family as PROP_ISO (only the zero-offset differs). The R
also broadcasts PROP_APERTURE on the dial, so read works once the handler
decodes byte 1; writing byte 1 drives the lens and the R clamps to the lens
range itself.

- internals.h: CONFIG_PROP_APERTURE_BYTE1 capability flag.
- lens.c: ml_raw_to_r_aperture / r_aperture_to_ml_raw + r_set_rawaperture (write
  code<<8, len 2, no min/max coerce -- raw_aperture_min/max are 0 on the R, so
  the generic COERCE would clamp to [0,0]); PROP_HANDLER(PROP_APERTURE) decodes
  byte 1; prop_set_rawaperture routes through r_set_rawaperture under the flag.
- property_whitelist.h: PROP_APERTURE added to prop_write_allow (read handler was
  already registered -- not deny-listed).

Validated on an EOS R (FW 1.8.0): read tracks the dial, and lens_set_rawaperture
sets f/4/f/8/f/11 with exact read-back (APSET: raw 40/56/64 -> readback 40/56/64).
@jed2nd

jed2nd commented Jun 19, 2026

Copy link
Copy Markdown
Author

Expanded this PR to cover all three manual exposure controls on the R, not just shutter — ISO and aperture read/write now work too.

The interesting part: the R encodes all three exposure properties the same unusual way. The value sits in a high byte as an offset code at 3 units per stop, rather than ML's usual low-byte APEX raw — which is why naïve writes silently did nothing (ISO/aperture stuck at one value) or came out as a fixed 1" exposure (shutter):

Property Length Code location Encoding
PROP_SHUTTER 2 bytes byte 1 signed Tv·3, 0 = 1"
PROP_ISO 4 bytes byte 1 15 = ISO 100, +3/stop
PROP_APERTURE 2 bytes byte 1 9 = f/2.8, +3/stop

ML keeps its own APEX raw scale internally and converts only at the property boundary ((ml_raw − offset)·3/8 → high byte; the inverse on read). Each is behind its own capability flag — CONFIG_PROP_SHUTTER_HI_BYTE, CONFIG_PROP_ISO_BYTE1, CONFIG_PROP_APERTURE_BYTE1 — so nothing changes for other bodies. ISO and aperture also needed un-gating in property_whitelist.h: ISO was defensively deny-listed (a runtime slave proved the handler is safe on the R), and both were added to prop_write_allow. Aperture deliberately skips the usual min/max COERCE because the R doesn't report a lens range to ML (raw_aperture_min/max stay 0) — the camera clamps to the mounted lens itself.

Validated on an EOS R (FW 1.8.0): all three track the dial correctly and set correctly through the production lens_set_raw* path, with exact read-back (e.g. ISO set 100/400/800/3200 and aperture set f/4·f/8·f/11 both round-trip exactly).

jed2nd added a commit to jed2nd/magiclantern_simplified that referenced this pull request Jun 19, 2026
These were one-time reverse-engineering tools to map the R's exposure-property
encodings. All three props (shutter, ISO, aperture) are now validated and the
features are on PR reticulatedpines#280, so the probes have served their purpose. Removing them
(scale map / write sweep / format test / dial read / hi-byte map / read map /
set test, plus the runtime PROP_ISO and PROP_APERTURE probe slaves) drops
_bss_end from 0x143600 to 0x13f600 -- ~16KB reclaimed, restoring ~20KB of
user_mem budget margin (the probe build had been ~20 bytes over -> red LED).

Kept: the SF capture/active-read probes (Path 2) and the other capture probes.
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.

1 participant