Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ devices = pychdk.list_devices()
# Connect to a camera
cam = pychdk.ChdkDevice(devices[0])

# Check CHDK version
# Check which version of the CHDK PTP protocol the camera speaks.
# This is the protocol version only — not the camera's firmware, and not
# the CHDK build on the card. There is no dedicated method for those;
# for the build, run CHDK's own Lua:
# cam.lua_execute("return get_buildinfo()")
major, minor = cam._chdk.get_version()
print(f"CHDK {major}.{minor}")
print(f"CHDK PTP protocol {major}.{minor}")

# Execute Lua on the camera
result = cam.lua_execute("return 2 + 2")
Expand Down Expand Up @@ -156,7 +160,7 @@ tools/

## Known limitations

- **Remote capture on A2500**: The A2500 CHDK port is alpha-level. Streaming remote capture (`shoot(stream=True)`) may fail with PTP error `0x2002`. The library falls back to SD card capture (`shoot()`) which triggers the shutter but stores images on the card rather than streaming them back.
- **Remote capture on A2500 is unverified**: `tools/flash_chdk.py` installs CHDK 1.6.1 r6315 for A2500 firmware 1.00A, and this library's streaming remote capture has never been run against that camera. Earlier versions of this file called the port "alpha-level"; that is not supported by the build we install — `camera_list.csv` in release-1_6 carries a `BETA_STATUS` column, 34 of its 397 entries are marked `ALPHA` or `BETA`, and the A2500's entry (`a2500,100a,,,`) is not one of them. The same sentence reported a `0x2002` failure on this camera; we have no record of observing it, so treat that as a possibility to test rather than a known behaviour. What the code does establish is that if streaming does fail, the library does not fall back — the exception reaches the caller and nothing else is attempted. Nor does the exception tell you what state the camera is in: the failure can come from the script submission, before `init_usb_capture` has run at all, or from a capture that initialized and was then cancelled. So whether USB remote capture is still enabled afterwards, and whether an ordinary `shoot()` would recover the picture, are both unestablished. Recovery is an open question for the bench, not a documented workaround.
- **macOS only** for `tools/flash_chdk.py`. The library itself works on macOS and Linux.
- **Canon cameras only** — CHDK is Canon-specific. Device discovery defaults to Canon's USB vendor ID (`0x04A9`).

Expand Down
280 changes: 280 additions & 0 deletions RELEASE-NOTES-v0.1.3.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions docs/chdk-flasher.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ A single-file Python CLI tool that downloads, formats, and flashes CHDK firmware

## Safety

- Only lists removable, external disks — internal drives are filtered out via `diskutil info -plist` checking for `RemovableMedia` and `Internal` keys
- Requires explicit `y` confirmation before formatting
- If multiple removable disks are found, asks user to pick
- Lists only whole disks that `diskutil info -plist` reports as `RemovableMedia` and `VirtualOrPhysical: Physical`, which excludes fixed drives, disk images and synthesised volumes. There is no `Internal` check, and none is wanted: a built-in SD slot is internal and is exactly what we want to flash from. The filter does not distinguish an SD card from any other removable medium, so an external USB drive can appear in the list — the confirmation prompt below, which names the disk, is the check on that
- Requires explicit `y` confirmation before formatting, on every path
- If multiple removable disks are found, asks user to pick — and then asks the same confirmation, naming the chosen disk. Selection is not consent
- If no removable disks found, tells user to insert a card and exits

## Boot sector patching
Expand Down Expand Up @@ -54,8 +54,8 @@ Writes `ODD\n` or `EVEN\n` to `OWN.TXT` on the card root. This is how Captua ide
$ python3 tools/flash_chdk.py

Downloading CHDK 1.6.1-6315 for A2500... (cached at ~/.cache/pychdk/a2500-100a-1.6.1-6315-full.zip)
Found removable disk: /dev/disk4 (SDCARD, 16GB, FAT32)
This will ERASE /dev/disk4. Continue? [y/N] y
Found removable disk: /dev/disk4 (SDCARD, 16.0GB)
This will ERASE /dev/disk4 (SDCARD, 16.0GB) and everything on it. Continue? [y/N] y
Formatting /dev/disk4 as FAT32...
Extracting CHDK files to /Volumes/CHDK_A2500...
Patching boot sector...
Expand Down
8 changes: 6 additions & 2 deletions examples/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ def main():
except Exception as e:
print(f"Streaming capture failed: {e}")
print("Trying non-streaming capture...")
cam.shoot(download_after=False)
print("Shot taken (saved to SD card)")
# This path waits for the shoot script to finish, so the script
# ran; it does not fetch anything back, and the library never
# checks that a file was written.
cam.shoot()
print("shoot() script finished; the image, if one was written, "
"is on the camera's card")

cam.close()
print("Done.")
Expand Down
7 changes: 6 additions & 1 deletion examples/test_two_cameras.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,13 @@ def main():
except Exception as e:
print(f"Streaming capture failed: {e}")
print("Trying non-streaming capture...")
# do_return=False submits the script and does not wait on it,
# so at this point all that is known is that each camera was
# asked to shoot. Whether a shutter fired, and whether a file
# reached either card, is not known here.
mc.execute_all("shoot()", do_return=False)
print("Shots taken (saved to SD cards)")
print("shoot() submitted to each camera; not waited on, so "
"whether a picture reached either card is unknown here")

mc.close()
print("Done.")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pychdk"
version = "0.1.2"
version = "0.1.3"
description = "Pure Python CHDK PTP camera control"
requires-python = ">=3.11"
dependencies = [
Expand Down
2 changes: 1 addition & 1 deletion src/pychdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Pure Python CHDK PTP camera control."""
import importlib

__version__ = "0.1.2"
__version__ = "0.1.3"

__all__ = [
"ChdkDevice", "list_devices", "DeviceInfo", "install_signal_handlers",
Expand Down
43 changes: 40 additions & 3 deletions src/pychdk/chdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,22 @@ class ScriptFlag(IntEnum):
FLUSH = 0x200


# Live view transfer flags: which block(s) GetDisplayData should send.
# These are the LV_TFR_* values in CHDK's core/live_view.h. They matter
# because they are not optional: live_view_get_data tests each flag
# before it adds the matching block, so a request with no flag set
# comes back as the header and the framebuffer descriptions and no
# pixel data at all (core/live_view.c). CHDK's own protocol note says
# as much — "The frame buffer descriptions are returned regardless of
# whether the data is available" (core/live_view.h).
#
# Read from the CHDK sources named above, not confirmed against a
# camera.
LV_TFR_VIEWPORT = 0x01
LV_TFR_BITMAP = 0x04
LV_TFR_PALETTE = 0x08
LV_TFR_BITMAP_OPACITY = 0x10

# Remote capture format bits
REMOTE_CAP_JPEG = 0x01
REMOTE_CAP_RAW = 0x02
Expand Down Expand Up @@ -360,8 +376,23 @@ def remote_capture_is_ready(self):
poll can arrive before init_usb_capture has executed and get
this answer perfectly legitimately. It is a failure only once
the script has had its chance — which the caller knows and this
method does not. A caller seeing it after the script has ended
is looking at an initialization that never happened.
method does not.

Even then it establishes less than it looks like it does. All a
caller seeing it after the script has ended knows is that
remote capture is not initialized NOW, and there are at least
two ways to arrive there that this status does not separate:
init_usb_capture never ran, or it ran and the capture was
cancelled afterwards. CHDK clears the capture target after its
own download timeout — the set_remotecap_timeout documentation
says "following a timeout, RemoteCaptureIsReady and
RemoteCaptureGetData will behave as if remote capture were not
initialized" (modules/luascript.c) — and after certain
chunk-selection errors, both by way of remotecap_reset
(core/remotecap.c). A host-side transfer failure on its own
does not establish that the reset happened: the PTP handler
does not check what send_data returned (core/ptp.c). So read
this as "not initialized", not as "never initialized".

Returns:
Tuple of (is_ready, status). status is REMOTE_CAP_NOTSET
Expand Down Expand Up @@ -498,7 +529,13 @@ def wait_for_script(self, timeout=30.0, script_id=None):
raise TimeoutError(f"Script still running after {timeout}s")

def drain_messages(self):
"""Drain all pending messages from the script message queue."""
"""Read up to fifty pending messages off the script queue.

A bounded attempt, not a guarantee. It stops early when the
camera reports the queue empty, and stops regardless at fifty.
It returns nothing either way, so a caller cannot tell which of
the two happened and must not assume the queue is now empty.
"""
for _ in range(50):
_, has_msgs = self.get_script_status()
if not has_msgs:
Expand Down
Loading
Loading