feat(capture): add Fujifilm Bluetooth trigger connector (optional fuji-ble backend) - #652
Open
rohanpandula wants to merge 1 commit into
Open
feat(capture): add Fujifilm Bluetooth trigger connector (optional fuji-ble backend)#652rohanpandula wants to merge 1 commit into
rohanpandula wants to merge 1 commit into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the thin connector from the standalone
fuji-ble-negpy-triggerpackage into NegPy — the same shape as #615 (coolscanpy) and the way NegPy already consumespython-sane(plain Scan panel) andgphoto2(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-bleoptional dependency group inpyproject.toml, mirroring the existingscanner(python-sane) andcamera(gphoto2) groups. Pinsfuji-ble-negpy-triggerto an immutable commit, exactly as the coolscan-roll group pins coolscanpy.uv sync --group fuji-bleopts in; the rest of the app never sees it. macOS/Linux only (bleak/CoreBluetooth).negpy/infrastructure/capture/fuji_ble.py— the only module that importsfujitrigger. Exposesavailable()(a cheapimportlib.util.find_speccheck, mirroringScanSidebar._sane_available()),make_camera(),discover(), andpair(). It accepts and returns plain dicts — nofujitriggertype escapes this module — and translates vendor errors to plainRuntimeErrorwith the original preserved as__cause__, the same contractSaneBackendandGphotoCameraalready honour.Cameraprotocol gainsis_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.CaptureWorkerbackend seam —set_camera_backend()armsusb(gphoto2, unchanged) orfuji_ble; the BLE camera is built throughfuji_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— bundlesfujitrigger+bleak(both imported lazily, so PyInstaller would miss them) and addsNSBluetoothAlwaysUsageDescriptionto 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 fakefujitriggermodule, 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.pyis the one placefujitriggeris 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
furblefirmware (MIT). Every other GFX/X body is untested.Verification
tests/test_capture_fuji_ble.py+tests/test_capture_worker.pypass against the fake-fujitriggerfixture with no hardware (and the full suite: 2545 passed).make lintandmake typepass.fuji-ble-negpy-triggeris 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.