summaryrefslogtreecommitdiff
path: root/src/camera/CameraTable.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/camera/CameraTable.vala')
-rw-r--r--src/camera/CameraTable.vala417
1 files changed, 417 insertions, 0 deletions
diff --git a/src/camera/CameraTable.vala b/src/camera/CameraTable.vala
new file mode 100644
index 0000000..8466388
--- /dev/null
+++ b/src/camera/CameraTable.vala
@@ -0,0 +1,417 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public class DiscoveredCamera {
+ public GPhoto.Camera gcamera;
+ public string uri;
+ public string display_name;
+ public GLib.Icon? icon;
+
+ public DiscoveredCamera(GPhoto.Camera gcamera, string uri, string display_name, GLib.Icon? icon) {
+ this.gcamera = gcamera;
+ this.uri = uri;
+ this.display_name = display_name;
+ this.icon = icon;
+ }
+}
+
+public class CameraTable {
+ private const int UPDATE_DELAY_MSEC = 1000;
+
+ // list of subsystems being monitored for events
+ private const string[] SUBSYSTEMS = { "usb", "block", null };
+
+ private static CameraTable instance = null;
+
+ private GUdev.Client client = new GUdev.Client(SUBSYSTEMS);
+ private OneShotScheduler camera_update_scheduler = null;
+ private GPhoto.Context null_context = new GPhoto.Context();
+ private GPhoto.CameraAbilitiesList abilities_list;
+ private VolumeMonitor volume_monitor;
+
+ private Gee.HashMap<string, DiscoveredCamera> camera_map = new Gee.HashMap<string, DiscoveredCamera>();
+
+ public signal void camera_added(DiscoveredCamera camera);
+
+ public signal void camera_removed(DiscoveredCamera camera);
+
+ private CameraTable() {
+ camera_update_scheduler = new OneShotScheduler("CameraTable update scheduler",
+ on_update_cameras);
+
+ // listen for interesting events on the specified subsystems
+ client.uevent.connect(on_udev_event);
+ volume_monitor = VolumeMonitor.get();
+ volume_monitor.volume_changed.connect(on_volume_changed);
+ volume_monitor.volume_added.connect(on_volume_changed);
+
+ // because loading the camera abilities list takes a bit of time and slows down app
+ // startup, delay loading it (and notifying any observers) for a small period of time,
+ // after the dust has settled
+ Timeout.add(500, delayed_init);
+ }
+
+ private bool delayed_init() {
+ // We disable this here so cameras that are already connected at the time
+ // the application is launched don't interfere with normal navigation...
+ ((LibraryWindow) AppWindow.get_instance()).set_page_switching_enabled(false);
+
+ try {
+ init_camera_table();
+ } catch (GPhotoError err) {
+ warning("Unable to initialize camera table: %s", err.message);
+
+ return false;
+ }
+
+ try {
+ update_camera_table();
+ } catch (GPhotoError err) {
+ warning("Unable to update camera table: %s", err.message);
+ }
+
+ // ...and re-enable it here, so that cameras connected -after- the initial
+ // populating of the table will trigger a switch to the import page, as before.
+ ((LibraryWindow) AppWindow.get_instance()).set_page_switching_enabled(true);
+ return false;
+ }
+
+ public static CameraTable get_instance() {
+ if (instance == null)
+ instance = new CameraTable();
+
+ return instance;
+ }
+
+ public Gee.Iterable<DiscoveredCamera> get_cameras() {
+ return camera_map.values;
+ }
+
+ public int get_count() {
+ return camera_map.size;
+ }
+
+ public DiscoveredCamera? get_for_uri(string uri) {
+ return camera_map.get(uri);
+ }
+
+ private void do_op(GPhoto.Result res, string op) throws GPhotoError {
+ if (res != GPhoto.Result.OK)
+ throw new GPhotoError.LIBRARY("[%d] Unable to %s: %s", (int) res, op, res.as_string());
+ }
+
+ private void init_camera_table() throws GPhotoError {
+ do_op(GPhoto.CameraAbilitiesList.create(out abilities_list), "create camera abilities list");
+ do_op(abilities_list.load(null_context), "load camera abilities list");
+ }
+
+ private string[] get_all_usb_cameras() {
+ string[] cameras = new string[0];
+
+ GLib.List<GUdev.Device> device_list = client.query_by_subsystem(null);
+ foreach (GUdev.Device device in device_list) {
+ string device_file = device.get_device_file();
+ if(
+ // only keep devices that have a non-null device file and that
+ // have both the ID_GPHOTO2 and GPHOTO2_DRIVER properties set
+ (device_file != null) &&
+ (device.has_property("ID_GPHOTO2")) &&
+ (device.has_property("GPHOTO2_DRIVER"))
+ ) {
+ int camera_bus, camera_device;
+ // extract the bus and device IDs from the device file string
+ // TODO: is it safe to parse the absolute path or should we be
+ // smarter and use a regex to only pick up the end of the path?
+ if (device_file.scanf("/dev/bus/usb/%d/%d", out camera_bus, out camera_device) < 2) {
+ critical("get_all_usb_cameras: Failed to scanf device file %s", device_file);
+
+ continue;
+ }
+ string camera = "usb:%.3d,%.3d".printf(camera_bus, camera_device);
+ debug("USB camera detected at %s", camera);
+ cameras += camera;
+ }
+ }
+
+ return cameras;
+ }
+
+ // USB (or libusb) is a funny beast; if only one USB device is present (i.e. the camera),
+ // then a single camera is detected at port usb:. However, if multiple USB devices are
+ // present (including non-cameras), then the first attached camera will be listed twice,
+ // first at usb:, then at usb:xxx,yyy. If the usb: device is removed, another usb:xxx,yyy
+ // device will lose its full-path name and be referred to as usb: only.
+ //
+ // This function gleans the full port name of a particular port, even if it's the unadorned
+ // "usb:", by using GUdev.
+ private bool usb_esp(int current_camera_count, string[] usb_cameras, string port,
+ out string full_port) {
+ // sanity
+ assert(current_camera_count > 0);
+
+ debug("USB ESP: current_camera_count=%d port=%s", current_camera_count, port);
+
+ full_port = null;
+
+ // if GPhoto detects one camera, and USB reports one camera, all is swell
+ if (current_camera_count == 1 && usb_cameras.length == 1) {
+ full_port = usb_cameras[0];
+
+ debug("USB ESP: port=%s full_port=%s", port, full_port);
+
+ return true;
+ }
+
+ // with more than one camera, skip the mirrored "usb:" port
+ if (port == "usb:") {
+ debug("USB ESP: Skipping %s", port);
+
+ return false;
+ }
+
+ // parse out the bus and device ID
+ int bus, device;
+ if (port.scanf("usb:%d,%d", out bus, out device) < 2) {
+ critical("USB ESP: Failed to scanf %s", port);
+
+ return false;
+ }
+
+ foreach (string usb_camera in usb_cameras) {
+ int camera_bus, camera_device;
+ if (usb_camera.scanf("usb:%d,%d", out camera_bus, out camera_device) < 2) {
+ critical("USB ESP: Failed to scanf %s", usb_camera);
+
+ continue;
+ }
+
+ if ((bus == camera_bus) && (device == camera_device)) {
+ full_port = port;
+
+ debug("USB ESP: port=%s full_port=%s", port, full_port);
+
+ return true;
+ }
+ }
+
+ debug("USB ESP: No matching bus/device found for port=%s", port);
+
+ return false;
+ }
+
+ public static string get_port_uri(string port) {
+ return "gphoto2://[%s]/".printf(port);
+ }
+
+ public static string? get_port_path(string port) {
+ // Accepted format is usb:001,005
+ return port.has_prefix("usb:") ?
+ "/dev/bus/usb/%s".printf(port.substring(4).replace(",", "/")) : null;
+ }
+
+ private string? get_name_for_uuid(string uuid) {
+ foreach (Volume volume in volume_monitor.get_volumes()) {
+ if (volume.get_identifier(VolumeIdentifier.UUID) == uuid) {
+ return volume.get_name();
+ }
+ }
+ return null;
+ }
+
+ private GLib.Icon? get_icon_for_uuid(string uuid) {
+ foreach (Volume volume in volume_monitor.get_volumes()) {
+ if (volume.get_identifier(VolumeIdentifier.UUID) == uuid) {
+ return volume.get_icon();
+ }
+ }
+ return null;
+ }
+
+ private void update_camera_table() throws GPhotoError {
+ // need to do this because virtual ports come and go in the USB world (and probably others)
+ GPhoto.PortInfoList port_info_list;
+ do_op(GPhoto.PortInfoList.create(out port_info_list), "create port list");
+ do_op(port_info_list.load(), "load port list");
+
+ GPhoto.CameraList camera_list;
+ do_op(GPhoto.CameraList.create(out camera_list), "create camera list");
+ do_op(abilities_list.detect(port_info_list, camera_list, null_context), "detect cameras");
+
+ Gee.HashMap<string, string> detected_map = new Gee.HashMap<string, string>();
+
+ // walk the USB chain and find all PTP cameras; this is necessary for usb_esp
+ string[] usb_cameras = get_all_usb_cameras();
+
+ // go through the detected camera list and glean their ports
+ for (int ctr = 0; ctr < camera_list.count(); ctr++) {
+ string name;
+ do_op(camera_list.get_name(ctr, out name), "get detected camera name");
+
+ string port;
+ do_op(camera_list.get_value(ctr, out port), "get detected camera port");
+
+ debug("Detected %d/%d %s @ %s", ctr + 1, camera_list.count(), name, port);
+
+ // do some USB ESP, skipping ports that cannot be deduced
+ if (port.has_prefix("usb:")) {
+ string full_port;
+ if (!usb_esp(camera_list.count(), usb_cameras, port, out full_port))
+ continue;
+
+ port = full_port;
+ }
+
+ detected_map.set(port, name);
+ }
+
+ // find cameras that have disappeared
+ DiscoveredCamera[] missing = new DiscoveredCamera[0];
+ foreach (DiscoveredCamera camera in camera_map.values) {
+ GPhoto.PortInfo port_info;
+ string tmp_path;
+
+ do_op(camera.gcamera.get_port_info(out port_info),
+ "retrieve missing camera port information");
+
+#if WITH_GPHOTO_25
+ port_info.get_path(out tmp_path);
+#else
+ tmp_path = port_info.path;
+#endif
+
+ GPhoto.CameraAbilities abilities;
+ do_op(camera.gcamera.get_abilities(out abilities), "retrieve camera abilities");
+
+ if (detected_map.has_key(tmp_path)) {
+ debug("Found camera for %s @ %s in detected map", abilities.model, tmp_path);
+
+ continue;
+ }
+
+ debug("%s @ %s missing", abilities.model, tmp_path);
+
+ missing += camera;
+ }
+
+ // have to remove from hash map outside of iterator
+ foreach (DiscoveredCamera camera in missing) {
+ GPhoto.PortInfo port_info;
+ string tmp_path;
+
+ do_op(camera.gcamera.get_port_info(out port_info),
+ "retrieve missing camera port information");
+#if WITH_GPHOTO_25
+ port_info.get_path(out tmp_path);
+#else
+ tmp_path = port_info.path;
+#endif
+
+ GPhoto.CameraAbilities abilities;
+ do_op(camera.gcamera.get_abilities(out abilities), "retrieve missing camera abilities");
+
+ debug("Removing from camera table: %s @ %s", abilities.model, tmp_path);
+
+ camera_map.unset(get_port_uri(tmp_path));
+
+ camera_removed(camera);
+ }
+
+ // add cameras which were not present before
+ foreach (string port in detected_map.keys) {
+ string name = detected_map.get(port);
+ string display_name = null;
+ GLib.Icon? icon = null;
+ string uri = get_port_uri(port);
+
+ if (camera_map.has_key(uri)) {
+ // already known about
+ debug("%s @ %s already registered, skipping", name, port);
+
+ continue;
+ }
+
+ // Get display name for camera.
+ string path = get_port_path(port);
+ if (null != path) {
+ GUdev.Device device = client.query_by_device_file(path);
+ string serial = device.get_property("ID_SERIAL_SHORT");
+ if (null != serial) {
+ // Try to get the name and icon.
+ display_name = get_name_for_uuid(serial);
+ icon = get_icon_for_uuid(serial);
+ }
+ if (null == display_name) {
+ display_name = device.get_sysfs_attr("product");
+ }
+ if (null == display_name) {
+ display_name = device.get_property("ID_MODEL");
+ }
+ }
+ if (null == display_name) {
+ // Default to GPhoto detected name.
+ display_name = name;
+ }
+
+ int index = port_info_list.lookup_path(port);
+ if (index < 0)
+ do_op((GPhoto.Result) index, "lookup port %s".printf(port));
+
+ GPhoto.PortInfo port_info;
+ string tmp_path;
+
+ do_op(port_info_list.get_info(index, out port_info), "get port info for %s".printf(port));
+#if WITH_GPHOTO_25
+ port_info.get_path(out tmp_path);
+#else
+ tmp_path = port_info.path;
+#endif
+
+ // this should match, every time
+ assert(port == tmp_path);
+
+ index = abilities_list.lookup_model(name);
+ if (index < 0)
+ do_op((GPhoto.Result) index, "lookup camera model %s".printf(name));
+
+ GPhoto.CameraAbilities camera_abilities;
+ do_op(abilities_list.get_abilities(index, out camera_abilities),
+ "lookup camera abilities for %s".printf(name));
+
+ GPhoto.Camera gcamera;
+ do_op(GPhoto.Camera.create(out gcamera), "create camera object for %s".printf(name));
+ do_op(gcamera.set_abilities(camera_abilities), "set camera abilities for %s".printf(name));
+ do_op(gcamera.set_port_info(port_info), "set port info for %s on %s".printf(name, port));
+
+ debug("Adding to camera table: %s @ %s", name, port);
+
+ DiscoveredCamera camera = new DiscoveredCamera(gcamera, uri, display_name, icon);
+ camera_map.set(uri, camera);
+
+ camera_added(camera);
+ }
+ }
+
+ private void on_udev_event(string action, GUdev.Device device) {
+ debug("udev event: %s on %s", action, device.get_name());
+
+ // Device add/removes often arrive in pairs; this allows for a single
+ // update to occur when they come in all at once
+ camera_update_scheduler.after_timeout(UPDATE_DELAY_MSEC, true);
+ }
+
+ public void on_volume_changed(Volume volume) {
+ camera_update_scheduler.after_timeout(UPDATE_DELAY_MSEC, true);
+ }
+
+ private void on_update_cameras() {
+ try {
+ get_instance().update_camera_table();
+ } catch (GPhotoError err) {
+ warning("Error updating camera table: %s", err.message);
+ }
+ }
+}
+