Skip to content

feat(capture): add Fujifilm Bluetooth trigger connector (optional fuji-ble backend) - #652

Open
rohanpandula wants to merge 1 commit into
marcinz606:mainfrom
rohanpandula:fuji-ble-connector
Open

feat(capture): add Fujifilm Bluetooth trigger connector (optional fuji-ble backend)#652
rohanpandula wants to merge 1 commit into
marcinz606:mainfrom
rohanpandula:fuji-ble-connector

Conversation

@rohanpandula

Copy link
Copy Markdown
Contributor

Summary

This is the thin connector from the standalone fuji-ble-negpy-trigger package into NegPy — the same shape as #615 (coolscanpy) and the way NegPy already consumes python-sane (plain Scan panel) and gphoto2 (Camera Scanning): an optional dependency, imported lazily, entirely absent by default.

It lets a Fujifilm GFX body be triggered over Bluetooth LE for automated RGB film scanning. The reason it exists: Fujifilm bodies stick in a gphoto2 tethered-capture state and refuse to reconnect, so gphoto2 triggering is unusable on them. Here the shutter fires over BLE and the RAW arrives via the camera's WiFi "PC Auto Save" into a watched folder — no gphoto2 capture session, so nothing gets stuck.

It is intentionally scoped to the glue. No sidebar UI in this PR — that lands stacked on top (#follow-up). This PR adds nothing a reviewer has to maintain beyond a small adapter, a backend seam in the worker, and the tests behind them.

What it adds

  • fuji-ble optional dependency group in pyproject.toml, mirroring the existing scanner (python-sane) and camera (gphoto2) groups. Pins fuji-ble-negpy-trigger to an immutable commit, exactly as the coolscan-roll group pins coolscanpy. uv sync --group fuji-ble opts in; the rest of the app never sees it. macOS/Linux only (bleak/CoreBluetooth).
  • negpy/infrastructure/capture/fuji_ble.py — the only module that imports fujitrigger. Exposes available() (a cheap importlib.util.find_spec check, mirroring ScanSidebar._sane_available()), make_camera(), discover(), and pair(). It accepts and returns plain dicts — no fujitrigger type escapes this module — and translates vendor errors to plain RuntimeError with the original preserved as __cause__, the same contract SaneBackend and GphotoCamera already honour.
  • Camera protocol gains is_open() (base.py) so the worker treats every backend uniformly — gphoto2 USB and the Bluetooth trigger both report it, and the worker never calls a backend-specific method to ask. This is the seam: a backend is selected and constructed through the adapter, not special-cased.
  • CaptureWorker backend seamset_camera_backend() arms usb (gphoto2, unchanged) or fuji_ble; the BLE camera is built through fuji_ble.make_camera() and live-view/settings/calibration gate themselves off via capabilities (the remote carries only the shutter). The worker stores the Fuji config as an opaque dict it never interprets.
  • build.py — bundles fujitrigger + bleak (both imported lazily, so PyInstaller would miss them) and adds NSBluetoothAlwaysUsageDescription to the macOS app's Info.plist when the group is installed, mirroring the gphoto2 optional-extra handling.
  • tests/test_capture_fuji_ble.py — injects a fake fujitrigger module, so no Bluetooth hardware and no real package are needed (the same approach feat(roll): add coolscanpy roll-scanning backend adapter (optional dependency) #615 uses for coolscanpy).

The integration point

fuji_ble.py is the one place fujitrigger is imported. Everything above it — the worker, and the sidebar in the follow-up — talks only through its functions and plain dicts. Re-pointing the device code later means changing only this module.

Hardware & validation

Validated against one setup: Fujifilm GFX 100 II on macOS — paired over BLE (CoreBluetooth-managed SMP bond), completed the secure handshake, fired the shutter, and reconnected by serial across the body's rotating pre-bond addresses. The protocol is ported from the furble firmware (MIT). Every other GFX/X body is untested.

Verification

  • tests/test_capture_fuji_ble.py + tests/test_capture_worker.py pass against the fake-fujitrigger fixture with no hardware (and the full suite: 2545 passed).
  • make lint and make type pass.
  • Feature is fully inert when fuji-ble-negpy-trigger is absent — available() gates every entry point.

Review note

Opened without the sidebar so the boundary shape can be reviewed on its own. The pairing UI (Trigger selector, Pair button, drop-folder picker) is ready as a stacked follow-up. Feedback on the adapter boundary and the is_open()/backend seam is what I'm most looking for.

…e backend)

Thin connector from the standalone fuji-ble-negpy-trigger package into NegPy,
mirroring how python-sane and gphoto2 are consumed: an optional dependency,
imported lazily, entirely absent by default. NegPy keeps only the glue
(capture/fuji_ble.py); all Fuji BLE device code lives in the standalone package
(pinned to an immutable commit, like coolscanpy).

- fuji_ble.py is the only module importing fujitrigger; accepts/returns plain
  dicts, translates vendor errors to RuntimeError with __cause__ preserved
- Camera protocol gains is_open() so the worker treats every backend uniformly
  (gphoto2 USB and Bluetooth trigger both report it) — no vendor special-casing
- CaptureWorker gains a backend seam (set_camera_backend / detect / pair) that
  builds the BLE camera through the adapter and gates live-view/settings off via
  capabilities (the remote carries only the shutter)
- build.py bundles fujitrigger+bleak and adds NSBluetoothAlwaysUsageDescription
  when the fuji-ble group is installed
- adapter tests inject a fake fujitrigger module (no hardware), as marcinz606#615 does

No sidebar UI here — that lands stacked on top.
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