summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cli.c108
-rw-r--r--src/cli.h31
-rw-r--r--src/device.c134
-rw-r--r--src/device.h188
-rw-r--r--src/gui_app.c127
-rw-r--r--src/gui_app.h33
-rw-r--r--src/gui_app_window.c513
-rw-r--r--src/gui_app_window.h40
-rw-r--r--src/image.c417
-rw-r--r--src/image.h121
-rw-r--r--src/main.c36
-rw-r--r--src/meson.build29
-rw-r--r--src/moticam.c482
-rw-r--r--src/moticam.h31
-rw-r--r--src/options.c102
-rw-r--r--src/options.h55
-rw-r--r--src/usb_source.c136
-rw-r--r--src/usb_source.h31
-rw-r--r--src/utils.c80
-rw-r--r--src/utils.h46
20 files changed, 2740 insertions, 0 deletions
diff --git a/src/cli.c b/src/cli.c
new file mode 100644
index 0000000..a5c20c7
--- /dev/null
+++ b/src/cli.c
@@ -0,0 +1,108 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <glib.h>
+
+#include "cli.h"
+#include "device.h"
+#include "utils.h"
+
+static bool
+cli_read_images(libusb_context *usb, struct device *device, int count,
+ bool raw, const char *out, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ if (!device_start(device, error))
+ return false;
+ bool ret = true;
+ for (int i = 0; ret && i < count;) {
+ /* Read image. */
+ GError *image_error = NULL;
+ struct image *image = device_read(device, &image_error);
+ if (!image && image_error) {
+ g_propagate_error(error, image_error);
+ ret = false;
+ } else if (!image) {
+ /* No image yet, handle events. */
+ int r = libusb_handle_events(usb);
+ if (r)
+ utils_fatal("unable to handle libusb events: %s",
+ libusb_strerror(r));
+ } else {
+ /* Image received. */
+ if (!raw && image->format != IMAGE_FORMAT_XBGR32) {
+ struct image *cimage = image_new(image->width, image->height,
+ image->width * 4, IMAGE_FORMAT_XBGR32);
+ image_convert(cimage, image);
+ image_unref(image);
+ image = cimage;
+ }
+ char *name = g_strdup_printf(out, i);
+ utils_info("write %s", name);
+ if (!image_save(image, name, error))
+ ret = false;
+ g_free(name);
+ image_unref(image);
+ i++;
+ }
+ }
+ if (!device_stop(device, ret ? error : NULL))
+ return false;
+ return ret;
+}
+
+int
+cli_run(struct options *options)
+{
+ libusb_context *usb;
+ int r = libusb_init(&usb);
+ if (r)
+ utils_fatal("unable to initialize libusb: %s", libusb_strerror(r));
+ GError *error = NULL;
+ struct device *device = device_open(usb, &error);
+ if (!device)
+ utils_fatal("unable to find device: %s", error->message);
+ const struct device_info *info = device_get_info(device);
+ if (options->exposure_ms > 0.0)
+ device_set_exposure(device, options->exposure_ms);
+ if (options->gain > 0.0)
+ device_set_gain(device, options->gain);
+ int width = -1, height = -1;
+ for (int i = 0; width == -1 && i < info->resolutions; i++) {
+ const struct device_info_resolution *ir = &info->resolution[i];
+ if ((options->width == -1 || options->width == ir->width)
+ && (options->height == -1 || options->height == ir->height)) {
+ width = ir->width;
+ height = ir->height;
+ }
+ }
+ if (width == -1)
+ utils_fatal("no matching resolution");
+ device_set_resolution(device, width, height, 0);
+ if (!cli_read_images(usb, device, options->count, options->raw,
+ options->out, &error))
+ utils_fatal("unable to read images: %s", error->message);
+ if (!device_close(device, &error))
+ utils_fatal("unable to close device: %s", error->message);
+ libusb_exit(usb);
+ return EXIT_SUCCESS;
+}
diff --git a/src/cli.h b/src/cli.h
new file mode 100644
index 0000000..01f2d8a
--- /dev/null
+++ b/src/cli.h
@@ -0,0 +1,31 @@
+#ifndef cli_h
+#define cli_h
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include "options.h"
+
+/* Run command line application, return status code. */
+int
+cli_run(struct options *options);
+
+#endif /* cli_h */
diff --git a/src/device.c b/src/device.c
new file mode 100644
index 0000000..09a94e3
--- /dev/null
+++ b/src/device.c
@@ -0,0 +1,134 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include "device.h"
+
+#include "moticam.h"
+
+static const struct device_driver *drivers[] = {
+ &moticam_device_driver,
+ NULL
+};
+
+GQuark
+device_error_quark(void)
+{
+ return g_quark_from_static_string("device-error-quark");
+}
+
+struct device *
+device_open(libusb_context *usb, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, NULL);
+ libusb_device **list;
+ libusb_device *found_usb_device = NULL;
+ char *found_device_name = NULL;
+ const struct device_driver *found_device_driver = NULL;
+ ssize_t cnt = libusb_get_device_list(usb, &list);
+ if (cnt < 0) {
+ g_set_error(error, DEVICE_ERROR, DEVICE_ERROR_LIST,
+ "can not list devices: %s", libusb_strerror(cnt));
+ return NULL;
+ }
+ for (ssize_t i = 0; !found_usb_device && i < cnt; i++) {
+ libusb_device *usb_device = list[i];
+ struct libusb_device_descriptor desc;
+ int r = libusb_get_device_descriptor(usb_device, &desc);
+ if (r) {
+ g_warning("can not get device descriptor: %s",
+ libusb_strerror(r));
+ } else {
+ for (const struct device_driver **dd = drivers;
+ !found_usb_device && *dd; dd++) {
+ char *device_name = (*dd)->usb_poll(&desc);
+ if (device_name) {
+ found_usb_device = usb_device;
+ found_device_name = device_name;
+ found_device_driver = *dd;
+ }
+ }
+ }
+ }
+ struct device *device = NULL;
+ if (found_usb_device) {
+ g_free(found_device_name);
+ device = found_device_driver->usb_open(usb, found_usb_device, error);
+ } else {
+ g_set_error(error, DEVICE_ERROR, DEVICE_ERROR_LIST,
+ "no device found");
+ }
+ libusb_free_device_list(list, 1);
+ return device;
+}
+
+const struct device_info *
+device_get_info(struct device *device)
+{
+ return device->get_info(device);
+}
+
+void
+device_set_exposure(struct device *device, double exposure_ms)
+{
+ device->set_exposure(device, exposure_ms);
+}
+
+void
+device_set_gain(struct device *device, double gain)
+{
+ device->set_gain(device, gain);
+}
+
+void
+device_set_resolution(struct device *device, int width, int height,
+ int stride)
+{
+ device->set_resolution(device, width, height, stride);
+}
+
+bool
+device_start(struct device *device, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ return device->start(device, error);
+}
+
+struct image *
+device_read(struct device *device, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ return device->read(device, error);
+}
+
+bool
+device_stop(struct device *device, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ return device->stop(device, error);
+}
+
+bool
+device_close(struct device *device, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ return device->close(device, error);
+}
diff --git a/src/device.h b/src/device.h
new file mode 100644
index 0000000..7028ebb
--- /dev/null
+++ b/src/device.h
@@ -0,0 +1,188 @@
+#ifndef device_h
+#define device_h
+/* Camicro - Microscope camera viewer.
+ *
+ * Device interface.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <libusb.h>
+#include <stdint.h>
+#include <glib.h>
+
+#include "image.h"
+
+/* Error domain for device related errors. */
+#define DEVICE_ERROR device_error_quark()
+
+struct device;
+
+/* USB poll function. If the device is handled by this driver, it should
+ * return a dynamically allocated string naming this device. */
+typedef char *(*device_driver_usb_poll_f)(
+ struct libusb_device_descriptor *desc);
+
+/* USB open function. The device driver previously declared that it handles
+ * this device. */
+typedef struct device *(*device_driver_usb_open_f)(
+ libusb_context *usb, libusb_device *usb_device, GError **error);
+
+/* Information on a device driver. */
+struct device_driver {
+ /* Driver name. */
+ const char *name;
+ /* Poll this driver for support for an USB device. */
+ device_driver_usb_poll_f usb_poll;
+ /* Open an USB device. */
+ device_driver_usb_open_f usb_open;
+};
+
+/* Information on a supported resolution. */
+struct device_info_resolution {
+ /* Image width. */
+ int width;
+ /* Image height. */
+ int height;
+};
+
+/* Informations on an open device. */
+struct device_info {
+ /* Exposure minimum and maximum values. */
+ double exposure_min_ms, exposure_max_ms;
+ /* Exposure default value. */
+ double exposure_default_ms;
+ /* Exposure increment between two settable values. */
+ double exposure_step_ms;
+ /* Gain minimum and maximum values. */
+ double gain_min, gain_max;
+ /* Gain default value. */
+ double gain_default;
+ /* Gain increment between two settable values. */
+ double gain_step;
+ /* Number of supported resolutions. */
+ int resolutions;
+ /* Table of supported resolutions. */
+ const struct device_info_resolution *resolution;
+};
+
+/* Retrieve informations. */
+typedef const struct device_info *(*device_get_info_f)(
+ struct device *device);
+
+/* Set new exposure value. */
+typedef void (*device_set_exposure_f)(
+ struct device *device, double exposure_ms);
+
+/* Set new gain value. */
+typedef void (*device_set_gain_f)(
+ struct device *device, double gain);
+
+/* Set resolution, must be called before first image read. If stride is zero,
+ * let the device decide. */
+typedef void (*device_set_resolution_f)(
+ struct device *device, int width, int height, int stride);
+
+/* Start streaming. */
+typedef bool (*device_start_f)(
+ struct device *device, GError **error);
+
+/* Return an image if one is available, else, return NULL. */
+typedef struct image *(*device_read_f)(
+ struct device *device, GError **error);
+
+/* Stop streaming. */
+typedef bool (*device_stop_f)(
+ struct device *device, GError **error);
+
+/* Close device. */
+typedef bool (*device_close_f)(
+ struct device *device, GError **error);
+
+/* Open device. */
+struct device {
+ /* Retrieve informations. */
+ device_get_info_f get_info;
+ /* Change exposure. */
+ device_set_exposure_f set_exposure;
+ /* Change gain. */
+ device_set_gain_f set_gain;
+ /* Change resolution. */
+ device_set_resolution_f set_resolution;
+ /* Start streaming. */
+ device_start_f start;
+ /* Read an image. */
+ device_read_f read;
+ /* Stop streaming. */
+ device_stop_f stop;
+ /* Close device. */
+ device_close_f close;
+};
+
+/* Error codes. */
+enum DeviceError {
+ /* Error listing devices. */
+ DEVICE_ERROR_LIST,
+ /* Generic USB error. */
+ DEVICE_ERROR_USB,
+};
+
+/* Return quark of error domain for device related errors. */
+GQuark
+device_error_quark(void);
+
+/* Find and open device. */
+struct device *
+device_open(libusb_context *usb, GError **error);
+
+/* Retrieve informations. */
+const struct device_info *
+device_get_info(struct device *device);
+
+/* Set new exposure value. */
+void
+device_set_exposure(struct device *device, double exposure_ms);
+
+/* Set new gain value. */
+void
+device_set_gain(struct device *device, double gain);
+
+/* Set resolution. */
+void
+device_set_resolution(struct device *device, int width, int height,
+ int stride);
+
+/* Start streaming. */
+bool
+device_start(struct device *device, GError **error);
+
+/* Read an image. */
+struct image *
+device_read(struct device *device, GError **error);
+
+/* Stop streaming. */
+bool
+device_stop(struct device *device, GError **error);
+
+/* Close device. */
+bool
+device_close(struct device *device, GError **error);
+
+#endif /* device_h */
diff --git a/src/gui_app.c b/src/gui_app.c
new file mode 100644
index 0000000..e7cc862
--- /dev/null
+++ b/src/gui_app.c
@@ -0,0 +1,127 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include "gui_app.h"
+#include "gui_app_window.h"
+#include "options.h"
+#include "usb_source.h"
+#include "utils.h"
+
+struct _GuiApp {
+ GtkApplication parent;
+};
+
+typedef struct _GuiAppPrivate GuiAppPrivate;
+
+struct _GuiAppPrivate {
+ libusb_context *usb;
+ GSource *usb_source;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE(GuiApp, gui_app, GTK_TYPE_APPLICATION)
+
+static gboolean
+gui_app_usb_source_cb(gpointer user_data)
+{
+ GuiApp *app = user_data;
+ GuiAppPrivate *priv = gui_app_get_instance_private(app);
+ struct timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 0;
+ int r = libusb_handle_events_timeout(priv->usb, &tv);
+ if (r)
+ g_error("unable to handle libusb events: %s", libusb_strerror(r));
+ g_signal_emit_by_name(app, "video-ready");
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+gui_app_init(GuiApp *app)
+{
+ GuiAppPrivate *priv = gui_app_get_instance_private(app);
+ priv->usb = NULL;
+ priv->usb_source = NULL;
+ options_add(G_APPLICATION(app));
+}
+
+static void
+gui_app_startup(GApplication *app)
+{
+ G_APPLICATION_CLASS(gui_app_parent_class)->startup(app);
+ GuiAppPrivate *priv = gui_app_get_instance_private(GUI_APP(app));
+ int r = libusb_init(&priv->usb);
+ if (r)
+ g_error("unable to initialize libusb: %s", libusb_strerror(r));
+ priv->usb_source = usb_source_new(priv->usb);
+ g_source_set_callback(priv->usb_source, gui_app_usb_source_cb, app,
+ NULL);
+ g_source_attach(priv->usb_source, NULL);
+}
+
+static void
+gui_app_activate(GApplication *app)
+{
+ GuiAppPrivate *priv = gui_app_get_instance_private(GUI_APP(app));
+ GError *error = NULL;
+ struct device *device = device_open(priv->usb, &error);
+ if (!device) {
+ utils_dialog_error(NULL, "unable to find device: %s", error->message);
+ g_error_free(error);
+ } else {
+ GuiAppWindow *window;
+ window = gui_app_window_new(GUI_APP(app));
+ gui_app_window_open(window, device);
+ gtk_window_present(GTK_WINDOW(window));
+ }
+}
+
+static void
+gui_app_shutdown(GApplication *app)
+{
+ GuiAppPrivate *priv = gui_app_get_instance_private(GUI_APP(app));
+ if (priv->usb_source) {
+ g_source_destroy(priv->usb_source);
+ g_source_unref(priv->usb_source);
+ }
+ if (priv->usb)
+ libusb_exit(priv->usb);
+ G_APPLICATION_CLASS(gui_app_parent_class)->shutdown(app);
+}
+
+static void
+gui_app_class_init(GuiAppClass *class)
+{
+ G_APPLICATION_CLASS(class)->startup = gui_app_startup;
+ G_APPLICATION_CLASS(class)->activate = gui_app_activate;
+ G_APPLICATION_CLASS(class)->shutdown = gui_app_shutdown;
+ G_APPLICATION_CLASS(class)->handle_local_options = options_handle;
+ g_signal_new("video-ready", GUI_APP_TYPE, G_SIGNAL_RUN_FIRST, 0, NULL,
+ NULL, NULL, G_TYPE_NONE, 0);
+}
+
+GuiApp *
+gui_app_new(void)
+{
+ return g_object_new(GUI_APP_TYPE,
+ "application-id", "org.eu.fr.ni.Camicro",
+ NULL);
+}
diff --git a/src/gui_app.h b/src/gui_app.h
new file mode 100644
index 0000000..8eeeff1
--- /dev/null
+++ b/src/gui_app.h
@@ -0,0 +1,33 @@
+#ifndef gui_app_h
+#define gui_app_h
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <gtk/gtk.h>
+
+#define GUI_APP_TYPE (gui_app_get_type())
+G_DECLARE_FINAL_TYPE(GuiApp, gui_app, GUI, APP, GtkApplication)
+
+GuiApp *
+gui_app_new(void);
+
+#endif /* gui_app_h */
diff --git a/src/gui_app_window.c b/src/gui_app_window.c
new file mode 100644
index 0000000..980daaa
--- /dev/null
+++ b/src/gui_app_window.c
@@ -0,0 +1,513 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <stdbool.h>
+#include <math.h>
+
+#include "gui_app_window.h"
+#include "utils.h"
+
+struct _GuiAppWindow {
+ GtkApplicationWindow parent;
+};
+
+typedef struct _GuiAppWindowPrivate GuiAppWindowPrivate;
+
+struct _GuiAppWindowPrivate {
+ GuiAppWindow *win;
+ GuiApp *app;
+ GtkDrawingArea *video;
+ GtkButton *start_stop_button;
+ GtkComboBoxText *resolution_combo_box;
+ GtkAdjustment *exposure_adj;
+ GtkAdjustment *gain_adj;
+ GtkCheckButton *wb_button;
+ GtkButton *wb_cal_button;
+ GtkCheckButton *rotate_button;
+ GtkButton *save_button;
+ GdkRectangle allocation;
+ cairo_matrix_t transform;
+ cairo_surface_t *surface;
+ struct image *image;
+ struct image *image_converted;
+ bool white_balance;
+ bool white_balance_calibrated;
+ struct image_white_balance_reference white_balance_reference;
+ bool rotate;
+ int width;
+ int height;
+ struct device *device;
+ bool started;
+ gulong video_ready_handler_id;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE(GuiAppWindow, gui_app_window,
+ GTK_TYPE_APPLICATION_WINDOW)
+
+/* When input or output size change, recompute transformation matrix. */
+static void
+video_size_update(GuiAppWindowPrivate *priv)
+{
+ int source_width = priv->width;
+ int source_height = priv->height;
+ int dest_width = priv->allocation.width;
+ int dest_height = priv->allocation.height;
+ if (source_width > 0 && dest_width > 0) {
+ double scale_x = (double) source_width / dest_width;
+ double scale_y = (double) source_height / dest_height;
+ double scale;
+ if (scale_x > scale_y)
+ scale = scale_x;
+ else
+ scale = scale_y;
+ if (scale < 1.0)
+ scale = 1.0;
+ double offset_x = -(dest_width - source_width / scale) / 2.0;
+ double offset_y = -(dest_height - source_height / scale) / 2.0;
+ if (priv->rotate) {
+ cairo_matrix_init_translate(&priv->transform,
+ source_width / 2, source_height / 2);
+ cairo_matrix_rotate(&priv->transform, M_PI);
+ cairo_matrix_translate(&priv->transform,
+ -source_width / 2, -source_height / 2);
+ } else {
+ cairo_matrix_init_identity(&priv->transform);
+ }
+ cairo_matrix_scale(&priv->transform, scale, scale);
+ cairo_matrix_translate(&priv->transform, offset_x, offset_y);
+ }
+}
+
+/* A new image could be available. */
+static void
+video_ready_cb(GuiApp *app, gpointer user_data)
+{
+ GuiAppWindowPrivate *priv = user_data;
+ if (priv->started) {
+ GError *error = NULL;
+ struct image *image = device_read(priv->device, &error);
+ if (!image) {
+ if (error) {
+ utils_dialog_error(GTK_WINDOW(priv->win),
+ "error reading image: %s",
+ error->message);
+ g_error_free(error);
+ gtk_widget_destroy(GTK_WIDGET(priv->win));
+ }
+ } else {
+ /* Release old data. */
+ if (priv->surface) {
+ cairo_surface_destroy(priv->surface);
+ priv->surface = NULL;
+ }
+ if (priv->image) {
+ image_unref(priv->image);
+ priv->image = NULL;
+ }
+ /* White balance. */
+ if (priv->white_balance && !priv->white_balance_calibrated) {
+ image_white_balance_calibrate(image,
+ &priv->white_balance_reference);
+ priv->white_balance_calibrated = true;
+ }
+ if (priv->white_balance)
+ image_white_balance(image, &priv->white_balance_reference);
+ /* Different format? Convert. */
+ if (image->format != IMAGE_FORMAT_XBGR32) {
+ if (priv->image_converted &&
+ (priv->image_converted->width != image->width
+ || priv->image_converted->height != image->height)) {
+ image_unref(priv->image_converted);
+ priv->image_converted = NULL;
+ }
+ if (!priv->image_converted) {
+ int stride =
+ cairo_format_stride_for_width(CAIRO_FORMAT_RGB24,
+ image->width);
+ priv->image_converted = image_new(image->width,
+ image->height, stride, IMAGE_FORMAT_XBGR32);
+ }
+ image_convert(priv->image_converted, image);
+ image_unref(image);
+ image = priv->image_converted;
+ } else {
+ if (priv->image_converted) {
+ image_unref(priv->image_converted);
+ priv->image_converted = NULL;
+ }
+ priv->image = image;
+ }
+ /* Make a surface. */
+ priv->surface = cairo_image_surface_create_for_data(
+ (unsigned char *) image->pixels, CAIRO_FORMAT_RGB24, image->width,
+ image->height, image->stride);
+ gtk_widget_queue_draw(GTK_WIDGET(priv->video));
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->save_button), TRUE);
+ }
+ }
+}
+
+/* Start video. */
+static void
+video_start(GuiAppWindowPrivate *priv)
+{
+ g_assert(!priv->started);
+ int stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24,
+ priv->width);
+ device_set_resolution(priv->device, priv->width, priv->height, stride);
+ video_size_update(priv);
+ GError *error = NULL;
+ if (!device_start(priv->device, &error)) {
+ utils_dialog_error(GTK_WINDOW(priv->win), "can not start: %s",
+ error->message);
+ g_error_free(error);
+ } else {
+ priv->video_ready_handler_id = g_signal_connect(priv->app, "video-ready",
+ G_CALLBACK(video_ready_cb), priv);
+ gtk_button_set_label(priv->start_stop_button, "Stop");
+ priv->started = true;
+ }
+}
+
+/* Stop video. */
+static void
+video_stop(GuiAppWindowPrivate *priv)
+{
+ g_assert(priv->started);
+ if (priv->surface) {
+ cairo_surface_destroy(priv->surface);
+ priv->surface = NULL;
+ }
+ if (priv->image) {
+ image_unref(priv->image);
+ priv->image = NULL;
+ }
+ if (priv->image_converted) {
+ image_unref(priv->image_converted);
+ priv->image_converted = NULL;
+ }
+ g_signal_handler_disconnect(priv->app, priv->video_ready_handler_id);
+ GError *error = NULL;
+ if (!device_stop(priv->device, &error)) {
+ g_warning("device_stop: %s", error->message);
+ g_error_free(error);
+ }
+ priv->started = false;
+ gtk_widget_queue_draw(GTK_WIDGET(priv->video));
+ gtk_button_set_label(priv->start_stop_button, "Start");
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->save_button), FALSE);
+}
+
+static gboolean
+video_draw_cb(GtkWidget *widget, cairo_t *cr, gpointer user_data)
+{
+ GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(widget));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+ cairo_paint(cr);
+ if (priv->surface) {
+ cairo_pattern_t *pattern =
+ cairo_pattern_create_for_surface(priv->surface);
+ cairo_pattern_set_matrix(pattern, &priv->transform);
+ cairo_set_source(cr, pattern);
+ cairo_paint(cr);
+ cairo_pattern_destroy(pattern);
+ }
+ return FALSE;
+}
+
+static void
+video_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation,
+ gpointer user_data)
+{
+ GuiAppWindow *win = GUI_APP_WINDOW(gtk_widget_get_toplevel(widget));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ priv->allocation = *allocation;
+ video_size_update(priv);
+}
+
+static void
+destroy_cb(GtkWidget *widget, gpointer user_data)
+{
+ GuiAppWindow *win = GUI_APP_WINDOW(widget);
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ if (priv->started)
+ video_stop(priv);
+ if (priv->device) {
+ GError *error = NULL;
+ if (!device_close(priv->device, &error)) {
+ g_warning("device_close: %s", error->message);
+ g_error_free(error);
+ }
+ priv->device = NULL;
+ }
+}
+
+static void
+start_stop_button_clicked_cb(GtkButton *button, gpointer user_data)
+{
+ GuiAppWindow *win =
+ GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ if (!priv->started)
+ video_start(priv);
+ else
+ video_stop(priv);
+}
+
+static void
+resolution_combo_box_changed_cb(GtkComboBox *combo, gpointer user_data)
+{
+ GuiAppWindow *win =
+ GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(combo)));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ int sel = gtk_combo_box_get_active(combo);
+ const struct device_info *info = device_get_info(priv->device);
+ g_assert(sel >= 0 && sel < info->resolutions);
+ priv->width = info->resolution[sel].width;
+ priv->height = info->resolution[sel].height;
+ if (priv->started) {
+ video_stop(priv);
+ video_start(priv);
+ }
+}
+
+static void
+exposure_adj_value_changed_cb(GtkAdjustment *adj, gpointer user_data)
+{
+ GuiAppWindow *win = GUI_APP_WINDOW(user_data);
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ double val = gtk_adjustment_get_value(adj);
+ device_set_exposure(priv->device, val);
+}
+
+static void
+gain_adj_value_changed_cb(GtkAdjustment *adj, gpointer user_data)
+{
+ GuiAppWindow *win = GUI_APP_WINDOW(user_data);
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ double val = gtk_adjustment_get_value(adj);
+ device_set_gain(priv->device, val);
+}
+
+static void
+wb_button_toggled_cb(GtkToggleButton *button, gpointer user_data)
+{
+ GuiAppWindow *win =
+ GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ priv->white_balance = gtk_toggle_button_get_active(button);
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->wb_cal_button),
+ priv->white_balance);
+}
+
+static void
+wb_cal_button_clicked_cb(GtkButton *button, gpointer user_data)
+{
+ GuiAppWindow *win =
+ GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ priv->white_balance_calibrated = false;
+}
+
+static void
+rotate_button_toggled_cb(GtkToggleButton *button, gpointer user_data)
+{
+ GuiAppWindow *win =
+ GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ priv->rotate = gtk_toggle_button_get_active(button);
+ video_size_update(priv);
+}
+
+static void
+save_button_clicked_cb(GtkButton *button, gpointer user_data)
+{
+ GuiAppWindow *win =
+ GUI_APP_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ if (priv->surface) {
+ GdkPixbuf *pixbuf = gdk_pixbuf_get_from_surface(priv->surface, 0, 0,
+ priv->width, priv->height);
+ if (priv->rotate && pixbuf) {
+ GdkPixbuf *rotated = gdk_pixbuf_rotate_simple(pixbuf, 180);
+ g_object_unref(pixbuf);
+ pixbuf = rotated;
+ }
+ if (pixbuf) {
+ GtkWidget *dialog = gtk_file_chooser_dialog_new("Save image",
+ GTK_WINDOW(win),
+ GTK_FILE_CHOOSER_ACTION_SAVE,
+ "_Cancel", GTK_RESPONSE_CANCEL,
+ "_Save", GTK_RESPONSE_ACCEPT,
+ NULL);
+ GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
+ gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
+ GtkFileFilter *png_filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(png_filter, "PNG image");
+ gtk_file_filter_add_mime_type(png_filter, "image/png");
+ gtk_file_filter_add_pattern(png_filter, "*.png");
+ gtk_file_chooser_add_filter(chooser, png_filter);
+ GtkFileFilter *jpg_filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(jpg_filter, "JPEG image");
+ gtk_file_filter_add_mime_type(jpg_filter, "image/jpeg");
+ gtk_file_filter_add_pattern(jpg_filter, "*.jpg");
+ gtk_file_filter_add_pattern(jpg_filter, "*.jpeg");
+ gtk_file_chooser_add_filter(chooser, jpg_filter);
+ gint res = gtk_dialog_run(GTK_DIALOG(dialog));
+ if (res == GTK_RESPONSE_ACCEPT) {
+ char *filename = gtk_file_chooser_get_filename(chooser);
+ if (filename) {
+ GtkFileFilter *filter =
+ gtk_file_chooser_get_filter(chooser);
+ const char *type = NULL;
+ if (filter == png_filter)
+ type = "png";
+ else if (filter == jpg_filter)
+ type = "jpeg";
+ if (type) {
+ GError *error = NULL;
+ if (!gdk_pixbuf_save(pixbuf, filename, type, &error,
+ NULL)) {
+ utils_dialog_error(GTK_WINDOW(win),
+ "can not save image: %s",
+ error->message);
+ g_error_free(error);
+ }
+ }
+ g_free(filename);
+ }
+ }
+ gtk_widget_destroy(dialog);
+ g_object_unref(pixbuf);
+ }
+ }
+}
+
+static void
+gui_app_window_init(GuiAppWindow *win)
+{
+ gtk_widget_init_template(GTK_WIDGET(win));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ priv->win = win;
+ priv->app = NULL;
+ priv->allocation.width = -1;
+ cairo_matrix_init_identity(&priv->transform);
+ priv->surface = NULL;
+ priv->image = NULL;
+ priv->image_converted = NULL;
+ priv->white_balance = false;
+ priv->white_balance_calibrated = false;
+ priv->rotate = true;
+ priv->width = -1;
+ priv->height = -1;
+ priv->device = NULL;
+ priv->started = false;
+ priv->video_ready_handler_id = 0;
+}
+
+static void
+gui_app_window_class_init(GuiAppWindowClass *class)
+{
+ gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS(class),
+ "/org/eu/fr/ni/Camicro/window.ui");
+ gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class),
+ GuiAppWindow, video);
+ gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class),
+ GuiAppWindow, start_stop_button);
+ gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class),
+ GuiAppWindow, resolution_combo_box);
+ gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class),
+ GuiAppWindow, exposure_adj);
+ gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class),
+ GuiAppWindow, gain_adj);
+ gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class),
+ GuiAppWindow, wb_button);
+ gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class),
+ GuiAppWindow, wb_cal_button);
+ gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class),
+ GuiAppWindow, rotate_button);
+ gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(class),
+ GuiAppWindow, save_button);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ destroy_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ video_draw_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ video_size_allocate_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ start_stop_button_clicked_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ resolution_combo_box_changed_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ exposure_adj_value_changed_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ gain_adj_value_changed_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ wb_button_toggled_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ wb_cal_button_clicked_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ rotate_button_toggled_cb);
+ gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class),
+ save_button_clicked_cb);
+}
+
+GuiAppWindow *
+gui_app_window_new(GuiApp *app)
+{
+ return g_object_new(GUI_APP_WINDOW_TYPE, "application", app, NULL);
+}
+
+void
+gui_app_window_open(GuiAppWindow *win, struct device *device)
+{
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ g_assert(!priv->device);
+ priv->app = GUI_APP(gtk_window_get_application(GTK_WINDOW(win)));
+ const struct device_info *info = device_get_info(device);
+ priv->width = info->resolution[0].width;
+ priv->height = info->resolution[0].height;
+ priv->device = device;
+ gtk_adjustment_set_lower(priv->exposure_adj, info->exposure_min_ms);
+ gtk_adjustment_set_upper(priv->exposure_adj, info->exposure_max_ms);
+ gtk_adjustment_set_value(priv->exposure_adj, info->exposure_default_ms);
+ gtk_adjustment_set_lower(priv->gain_adj, info->gain_min);
+ gtk_adjustment_set_upper(priv->gain_adj, info->gain_max);
+ gtk_adjustment_set_value(priv->gain_adj, info->gain_default);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->wb_button),
+ priv->white_balance);
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->wb_cal_button),
+ priv->white_balance);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->rotate_button),
+ priv->rotate);
+ gtk_combo_box_text_remove_all(priv->resolution_combo_box);
+ for (int i = 0; i < info->resolutions; i++) {
+ const struct device_info_resolution *ir = &info->resolution[i];
+ char buf[32];
+ int r = snprintf(buf, sizeof(buf), "%dx%d", ir->width, ir->height);
+ g_assert(r < sizeof(buf));
+ gtk_combo_box_text_append_text(priv->resolution_combo_box, buf);
+ }
+ gtk_combo_box_set_active(GTK_COMBO_BOX(priv->resolution_combo_box), 0);
+ video_start(priv);
+}
diff --git a/src/gui_app_window.h b/src/gui_app_window.h
new file mode 100644
index 0000000..2f886db
--- /dev/null
+++ b/src/gui_app_window.h
@@ -0,0 +1,40 @@
+#ifndef gui_app_window_h
+#define gui_app_window_h
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <gtk/gtk.h>
+
+#include "gui_app.h"
+#include "device.h"
+
+#define GUI_APP_WINDOW_TYPE (gui_app_window_get_type())
+G_DECLARE_FINAL_TYPE(GuiAppWindow, gui_app_window, GUI, APP_WINDOW,
+ GtkApplicationWindow)
+
+GuiAppWindow *
+gui_app_window_new(GuiApp *app);
+
+void
+gui_app_window_open(GuiAppWindow *win, struct device *device);
+
+#endif /* gui_app_window_h */
diff --git a/src/image.c b/src/image.c
new file mode 100644
index 0000000..ec29751
--- /dev/null
+++ b/src/image.c
@@ -0,0 +1,417 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <stdlib.h>
+#include <string.h>
+#include <png.h>
+
+#include "image.h"
+#include "utils.h"
+
+typedef uint8_t u8x16 __attribute__((vector_size(16)));
+typedef uint16_t u16x8 __attribute__((vector_size(16)));
+
+GQuark
+image_error_quark(void)
+{
+ return g_quark_from_static_string("image-error-quark");
+}
+
+static void
+image_free(struct image *image, void *user_data)
+{
+ free(image);
+}
+
+struct image *
+image_new(int width, int height, int stride, enum image_format format)
+{
+ size_t struct_size_aligned = (sizeof(struct image) + 15) / 16 * 16;
+ size_t image_size_aligned = ((height * stride) + 15) / 16 * 16;
+ uint8_t *m = aligned_alloc(16,
+ struct_size_aligned + image_size_aligned);
+ if (!m)
+ g_error("aligned_alloc: failed to allocate");
+ struct image *image = (struct image *) m;
+ image->width = width;
+ image->height = height;
+ image->stride = stride;
+ image->format = format;
+ image->pixels = m + struct_size_aligned;
+ image->refs = 1;
+ image->release = image_free;
+ image->release_user_data = NULL;
+ return image;
+}
+
+void
+image_ref(struct image *image)
+{
+ image->refs++;
+}
+
+void
+image_unref(struct image *image)
+{
+ image->refs--;
+ if (image->refs == 0 && image->release)
+ image->release(image, image->release_user_data);
+}
+
+static void
+image_histogram_sgrbg8(const struct image *image, uint32_t histogram[3][256])
+{
+ g_assert(image->width % 2 == 0);
+ g_assert(image->height % 2 == 0);
+ g_assert(image->stride == image->width);
+ memset(histogram, 0, 3 * 256 * sizeof(uint32_t));
+ const uint8_t *p = image->pixels;
+ for (int i = 0; i < image->height; i += 2) {
+ for (int j = 0; j < image->width; j += 2) {
+ uint8_t g = *p++;
+ histogram[1][g] += 2;
+ uint8_t r = *p++;
+ histogram[0][r] += 4;
+ }
+ for (int j = 0; j < image->width; j += 2) {
+ uint8_t b = *p++;
+ histogram[2][b] += 4;
+ uint8_t g = *p++;
+ histogram[1][g] += 2;
+ }
+ }
+}
+
+static void
+image_histogram_xrgb32(const struct image *image, uint32_t histogram[3][256])
+{
+ g_assert(image->stride == image->width * 4);
+ memset(histogram, 0, 3 * 256 * sizeof(uint32_t));
+ const uint8_t *p = image->pixels;
+ const uint8_t *pend = image->pixels + image->height * image->stride;
+ while (p != pend) {
+ uint8_t b = *p++;
+ uint8_t g = *p++;
+ uint8_t r = *p++;
+ p++;
+ histogram[0][r]++;
+ histogram[1][g]++;
+ histogram[2][b]++;
+ }
+}
+
+void
+image_histogram(const struct image *image, uint32_t histogram[3][256])
+{
+ switch (image->format) {
+ case IMAGE_FORMAT_SGRBG8:
+ image_histogram_sgrbg8(image, histogram);
+ break;
+ case IMAGE_FORMAT_XBGR32:
+ image_histogram_xrgb32(image, histogram);
+ break;
+ default:
+ g_assert_not_reached();
+ }
+}
+
+void
+image_white_balance_calibrate(const struct image *image,
+ struct image_white_balance_reference *reference)
+{
+ uint32_t histogram[3][256];
+ image_histogram(image, histogram);
+ const uint32_t top_ignore = image->width * image->height / 1000;
+ uint8_t top[3];
+ for (int i = 0; i < 3; i++) {
+ uint32_t acc = 0;
+ uint8_t j;
+ for (j = 255; acc < top_ignore && j > 0; j--) {
+ acc += histogram[i][j];
+ }
+ top[i] = j;
+ }
+ reference->r = top[0];
+ reference->g = top[1];
+ reference->b = top[2];
+}
+
+static void
+image_white_balance_factors(
+ const struct image_white_balance_reference *reference,
+ uint16_t *fr, uint16_t *fg, uint16_t *fb)
+{
+ uint8_t wr = reference->r;
+ uint8_t wg = reference->g;
+ uint8_t wb = reference->b;
+ if (wr > wg && wr > wb) {
+ *fr = 256;
+ *fg = 256 * wr / wg;
+ *fb = 256 * wr / wb;
+ } else if (wg > wr && wg > wb) {
+ *fr = 256 * wg / wr;
+ *fg = 256;
+ *fb = 256 * wg / wb;
+ } else {
+ *fr = 256 * wb / wr;
+ *fg = 256 * wb / wg;
+ *fb = 256;
+ }
+}
+
+static void
+image_white_balance_sgrbg8(struct image *image,
+ const struct image_white_balance_reference *reference)
+{
+ g_assert(image->width % 16 == 0);
+ g_assert(image->height % 2 == 0);
+ g_assert(image->stride == image->width);
+ uint16_t fr, fg, fb;
+ image_white_balance_factors(reference, &fr, &fg, &fb);
+ u8x16 *p = (u8x16 *) image->pixels;
+ const u8x16 zero = { 0 };
+ const u8x16 ex0 = { 0, 16, 1, 17, 2, 18, 3, 19,
+ 4, 20, 5, 21, 6, 22, 7, 23 };
+ const u8x16 ex1 = { 8, 24, 9, 25, 10, 26, 11, 27,
+ 12, 28, 13, 29, 14, 30, 15, 31 };
+ const u16x8 mulo = { fg, fr, fg, fr, fg, fr, fg, fr };
+ const u16x8 mule = { fb, fg, fb, fg, fb, fg, fb, fg };
+ const u8x16 imp = { 0, 2, 4, 6, 8, 10, 12, 14,
+ 16, 18, 20, 22, 24, 26, 28, 30 };
+ for (int i = 0; i < image->height; i += 2) {
+ for (int j = 0; j < image->width; j += 16) {
+ u8x16 pix4 = *p;
+ u16x8 pix2_0 = (u16x8) __builtin_shuffle(pix4, zero, ex0);
+ u16x8 pix2_0_ovf = __builtin_ia32_pmulhuw128(pix2_0, mulo)
+ != (u16x8) zero;
+ pix2_0 = ((pix2_0 * mulo) | pix2_0_ovf) / 256;
+ u16x8 pix2_1 = (u16x8) __builtin_shuffle(pix4, zero, ex1);
+ u16x8 pix2_1_ovf = __builtin_ia32_pmulhuw128(pix2_1, mulo)
+ != (u16x8) zero;
+ pix2_1 = ((pix2_1 * mulo) | pix2_1_ovf) / 256;
+ pix4 = __builtin_shuffle((u8x16) pix2_0, (u8x16) pix2_1, imp);
+ *p++ = pix4;
+ }
+ for (int j = 0; j < image->width; j += 16) {
+ u8x16 pix4 = *p;
+ u16x8 pix2_0 = (u16x8) __builtin_shuffle(pix4, zero, ex0);
+ u16x8 pix2_0_ovf = __builtin_ia32_pmulhuw128(pix2_0, mule)
+ != (u16x8) zero;
+ pix2_0 = ((pix2_0 * mule) | pix2_0_ovf) / 256;
+ u16x8 pix2_1 = (u16x8) __builtin_shuffle(pix4, zero, ex1);
+ u16x8 pix2_1_ovf = __builtin_ia32_pmulhuw128(pix2_1, mule)
+ != (u16x8) zero;
+ pix2_1 = ((pix2_1 * mule) | pix2_1_ovf) / 256;
+ pix4 = __builtin_shuffle((u8x16) pix2_0, (u8x16) pix2_1, imp);
+ *p++ = pix4;
+ }
+ }
+}
+
+static void
+image_white_balance_xbgr32(struct image *image,
+ const struct image_white_balance_reference *reference)
+{
+ g_assert(image->width % 4 == 0);
+ g_assert(image->stride == image->width * 4);
+ uint16_t fr, fg, fb;
+ image_white_balance_factors(reference, &fr, &fg, &fb);
+ u8x16 *p = (u8x16 *) image->pixels;
+ u8x16 *pend = (u8x16 *) (image->pixels + image->height * image->stride);
+ const u8x16 zero = { 0 };
+ const u8x16 ex0 = { 0, 16, 1, 17, 2, 18, 3, 19,
+ 4, 20, 5, 21, 6, 22, 7, 23 };
+ const u8x16 ex1 = { 8, 24, 9, 25, 10, 26, 11, 27,
+ 12, 28, 13, 29, 14, 30, 15, 31 };
+ const u16x8 mul = { fb, fg, fr, 255, fb, fg, fr, 255 };
+ const u8x16 imp = { 0, 2, 4, 6, 8, 10, 12, 14,
+ 16, 18, 20, 22, 24, 26, 28, 30 };
+ while (p != pend) {
+ u8x16 pix4 = *p;
+ u16x8 pix2_0 = (u16x8) __builtin_shuffle(pix4, zero, ex0);
+ u16x8 pix2_0_ovf = __builtin_ia32_pmulhuw128(pix2_0, mul)
+ != (u16x8) zero;
+ pix2_0 = ((pix2_0 * mul) | pix2_0_ovf) / 256;
+ u16x8 pix2_1 = (u16x8) __builtin_shuffle(pix4, zero, ex1);
+ u16x8 pix2_1_ovf = __builtin_ia32_pmulhuw128(pix2_1, mul)
+ != (u16x8) zero;
+ pix2_1 = ((pix2_1 * mul) | pix2_1_ovf) / 256;
+ pix4 = __builtin_shuffle((u8x16) pix2_0, (u8x16) pix2_1, imp);
+ *p++ = pix4;
+ }
+}
+
+void
+image_white_balance(struct image *image,
+ const struct image_white_balance_reference *reference)
+{
+ switch (image->format) {
+ case IMAGE_FORMAT_SGRBG8:
+ image_white_balance_sgrbg8(image, reference);
+ break;
+ case IMAGE_FORMAT_XBGR32:
+ image_white_balance_xbgr32(image, reference);
+ break;
+ default:
+ g_assert_not_reached();
+ }
+}
+
+static void
+image_convert_to_xbgr32_from_sgrbg8(struct image *dst,
+ const struct image *src)
+{
+ /*
+ * Compute missing value using an average of its neighbours.
+ * Input pattern:
+ * G R G R G R
+ * B G B G B G
+ * G R G R G R
+ * B G B G B G
+ */
+ g_assert(dst->width == src->width
+ && dst->height == src->height);
+ const int in_stride = src->stride;
+ const int out_stride = dst->stride;
+ int width = src->width;
+ int height = src->height;
+ /* Skip first line and column. */
+ const uint8_t *in = src->pixels + in_stride + 1;
+ uint8_t *out = dst->pixels + out_stride + 4;
+ /* Loop over lines. */
+ for (int i = 1; i < height - 1; i += 2) {
+ /* Even lines. */
+ const uint8_t *in_stop = in + (width - 2);
+ while (in != in_stop) {
+ *out++ = (in[-1] + in[+1] + 1) >> 1; /* B */
+ *out++ = in[0]; /* G */
+ *out++ = (in[-in_stride] + in[+in_stride] + 1) >> 1; /* R */
+ *out++ = 255; /* A */
+ in++;
+ *out++ = in[0]; /* B */
+ *out++ = (in[-in_stride] + in[+in_stride] /* G */
+ + in[-1] + in[+1] + 2) >> 2;
+ *out++ = (in[-in_stride - 1] + in[-in_stride + 1] /* R */
+ + in[+in_stride - 1] + in[+in_stride + 1] + 2) >> 2;
+ *out++ = 255; /* A */
+ in++;
+ }
+ /* Fill first and last pixels. */
+ out[-(width - 1) * 4 + 0] = out[-(width - 2) * 4 + 0];
+ out[-(width - 1) * 4 + 1] = out[-(width - 2) * 4 + 1];
+ out[-(width - 1) * 4 + 2] = out[-(width - 2) * 4 + 2];
+ out[-(width - 1) * 4 + 3] = out[-(width - 2) * 4 + 3];
+ out[0] = out[-4];
+ out[1] = out[-3];
+ out[2] = out[-2];
+ out[3] = out[-1];
+ out += out_stride - (width - 2) * 4;
+ in += in_stride - (width - 2);
+ /* Odd lines. */
+ in_stop = in + (width - 2);
+ while (in != in_stop) {
+ *out++ = (in[-in_stride - 1] + in[-in_stride + 1] /* B */
+ + in[+in_stride - 1] + in[+in_stride + 1] + 2) >> 2;
+ *out++ = (in[-in_stride] + in[+in_stride] /* G */
+ + in[-1] + in[+1] + 2) >> 2;
+ *out++ = in[0]; /* R */
+ *out++ = 255; /* A */
+ in++;
+ *out++ = (in[-in_stride] + in[+in_stride] + 1) >> 1; /* B */
+ *out++ = in[0]; /* G */
+ *out++ = (in[-1] + in[+1] + 1) >> 1; /* R */
+ *out++ = 255; /* A */
+ in++;
+ }
+ /* Fill first and last pixels. */
+ out[-(width - 1) * 4 + 0] = out[-(width - 2) * 4 + 0];
+ out[-(width - 1) * 4 + 1] = out[-(width - 2) * 4 + 1];
+ out[-(width - 1) * 4 + 2] = out[-(width - 2) * 4 + 2];
+ out[-(width - 1) * 4 + 3] = out[-(width - 2) * 4 + 3];
+ out[0] = out[-4];
+ out[1] = out[-3];
+ out[2] = out[-2];
+ out[3] = out[-1];
+ out += out_stride - (width - 2) * 4;
+ in += in_stride - (width - 2);
+ }
+ /* Last line. */
+ out -= 4;
+ memcpy(out, out - out_stride, width * 4);
+ /* First line. */
+ out -= (height - 1) * out_stride;
+ memcpy(out, out + out_stride, width * 4);
+}
+
+void
+image_convert(struct image *dst, const struct image *src)
+{
+ g_assert(dst->width == src->width
+ && dst->height == src->height);
+ if (dst->format == src->format) {
+ g_assert(dst->stride == src->stride);
+ memcpy(dst->pixels, src->pixels, src->stride * src->height);
+ } else {
+ switch (src->format) {
+ case IMAGE_FORMAT_SGRBG8:
+ switch (dst->format) {
+ case IMAGE_FORMAT_XBGR32:
+ image_convert_to_xbgr32_from_sgrbg8(dst, src);
+ break;
+ default:
+ g_assert_not_reached();
+ }
+ break;
+ default:
+ g_assert_not_reached();
+ }
+ }
+}
+
+bool
+image_save(const struct image *image, const char *name, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ png_image pimage;
+ memset(&pimage, 0, sizeof(pimage));
+ pimage.version = PNG_IMAGE_VERSION;
+ pimage.width = image->width;
+ pimage.height = image->height;
+ switch(image->format) {
+ case IMAGE_FORMAT_SGRBG8:
+ pimage.format = PNG_FORMAT_GRAY;
+ break;
+ case IMAGE_FORMAT_XBGR32:
+ pimage.format = PNG_FORMAT_BGRA;
+ break;
+ default:
+ g_assert_not_reached();
+ }
+ int r = png_image_write_to_file(&pimage, name, 0,
+ image->pixels, image->stride, NULL);
+ if (r == 0) {
+ g_set_error(error, IMAGE_ERROR, IMAGE_ERROR_SAVE,
+ "can not write image: %s", pimage.message);
+ return false;
+ }
+ return true;
+}
diff --git a/src/image.h b/src/image.h
new file mode 100644
index 0000000..0e36c02
--- /dev/null
+++ b/src/image.h
@@ -0,0 +1,121 @@
+#ifndef image_h
+#define image_h
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <stdint.h>
+#include <stdbool.h>
+#include <glib.h>
+
+/* Error domain for image related errors. */
+#define IMAGE_ERROR image_error_quark()
+
+/* Image format. */
+enum image_format
+{
+#define IMAGE_FORMAT_FOURCC(a, b, c, d) \
+ (((uint32_t)(a)) | ((uint32_t)(b) << 8) \
+ | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
+ /* Bayer image format:
+ * G R G R
+ * B G B G */
+ IMAGE_FORMAT_SGRBG8 = IMAGE_FORMAT_FOURCC('B', 'R', 'B', 'G'),
+ /* RGB 32 bits, B in LSB. */
+ IMAGE_FORMAT_XBGR32 = IMAGE_FORMAT_FOURCC('X', 'R', '2', '4'),
+};
+
+struct image;
+
+/* Image release callback. */
+typedef void (*image_release_f)(struct image *image, void *user_data);
+
+/* Image with attached information. */
+struct image {
+ /* Image width. */
+ int width;
+ /* Image height. */
+ int height;
+ /* Byte offset between a line and the next line. */
+ int stride;
+ /* Image format. */
+ enum image_format format;
+ /* Frame buffer. */
+ uint8_t *pixels;
+ /* Number of reference to this image. */
+ int refs;
+ /* Release callback. */
+ image_release_f release;
+ /* Release callback user data. */
+ void *release_user_data;
+};
+
+/* Reference for white balance. */
+struct image_white_balance_reference {
+ /* White (maximum of each component). */
+ uint8_t r, g, b;
+};
+
+/* Error codes. */
+enum ImageError {
+ /* Error writing an image to disk. */
+ IMAGE_ERROR_SAVE,
+};
+
+/* Return quark of error domain for image related errors. */
+GQuark
+image_error_quark(void);
+
+/* Allocate an image using system allocator. */
+struct image *
+image_new(int width, int height, int stride, enum image_format format);
+
+/* Add a reference to an image. */
+void
+image_ref(struct image *image);
+
+/* Release a reference to an image. */
+void
+image_unref(struct image *image);
+
+/* Compute histogram(s), should provide an uint32_t[3][256] array. */
+void
+image_histogram(const struct image *image, uint32_t histogram[3][256]);
+
+/* Calibrate for white balance. */
+void
+image_white_balance_calibrate(const struct image *image,
+ struct image_white_balance_reference *reference);
+
+/* Apply the given white balance. */
+void
+image_white_balance(struct image *image,
+ const struct image_white_balance_reference *reference);
+
+/* Copy an image to another one, converting format. */
+void
+image_convert(struct image *dst, const struct image *src);
+
+/* Save image to PNG. */
+bool
+image_save(const struct image *image, const char *name, GError **error);
+
+#endif /* image_h */
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..2abeeda
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,36 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <gtk/gtk.h>
+
+#include "gui_app.h"
+
+int
+main(int argc, char **argv)
+{
+ int status;
+ GuiApp *app;
+ app = gui_app_new();
+ status = g_application_run(G_APPLICATION(app), argc, argv);
+ g_object_unref(app);
+ return status;
+}
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..cf832e1
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,29 @@
+sources = [
+ 'device.c',
+ 'cli.c',
+ 'image.c',
+ 'options.c',
+ 'gui_app_window.c',
+ 'usb_source.c',
+ 'moticam.c',
+ 'main.c',
+ 'gui_app.c',
+ 'utils.c',
+]
+
+deps = [
+ libusb,
+ libpng,
+ gtk,
+]
+
+c_args = [
+ '-flax-vector-conversions',
+]
+
+executable('camicro',
+ sources + resources,
+ dependencies : deps,
+ c_args: c_args,
+ install : true,
+)
diff --git a/src/moticam.c b/src/moticam.c
new file mode 100644
index 0000000..232fb77
--- /dev/null
+++ b/src/moticam.c
@@ -0,0 +1,482 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * This files add support for direct connexion with USB Moticam 3+.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include "moticam.h"
+#include "utils.h"
+
+#define ID_VENDOR 0x232f
+#define ID_PRODUCT 0x0100
+#define ID_PRODUCT_USB3 0x0101
+
+struct moticam_device {
+ struct device device;
+ libusb_context *usb;
+ libusb_device_handle *handle;
+ double exposure_ms;
+ int width;
+ int height;
+ bool exposure_pending;
+ double gain;
+ bool gain_pending;
+ size_t rawbuffer_size;
+ struct image image[2];
+ struct image *current_image;
+ struct image *other_image;
+ struct libusb_transfer *usb_transfer;
+ bool usb_transfer_done;
+ int drop;
+};
+
+static char *
+moticam_usb_poll(struct libusb_device_descriptor *desc);
+
+static struct device *
+moticam_usb_open(libusb_context *usb, libusb_device *usb_device,
+ GError **error);
+
+static bool
+moticam_stop(struct device *device, GError **error);
+
+const struct device_driver moticam_device_driver = {
+ "Moticam USB cameras",
+ &moticam_usb_poll,
+ &moticam_usb_open,
+};
+
+static const struct device_info_resolution moticam_device_info_resolution[] = {
+ { 2048, 1536 },
+ { 1024, 768 },
+ { 512, 384 },
+};
+
+static const struct device_info moticam_device_info = {
+ .exposure_min_ms = 1,
+ .exposure_max_ms = 5000,
+ .exposure_default_ms = 100,
+ .exposure_step_ms = 1,
+ .gain_min = 0.33,
+ .gain_max = 42.66,
+ .gain_default = 1,
+ .gain_step = 1.0 / 24,
+ .resolutions = 3,
+ .resolution = moticam_device_info_resolution,
+};
+
+static bool
+moticam_control_vendor(struct moticam_device *mdev, uint16_t wValue,
+ const uint8_t *data, int data_cnt, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ int r = libusb_control_transfer(mdev->handle, LIBUSB_REQUEST_TYPE_VENDOR,
+ 240, wValue, 0, (uint8_t *) data, data_cnt, 0);
+ if (r < 0) {
+ g_set_error(error, DEVICE_ERROR, DEVICE_ERROR_USB,
+ "can not send vendor control: %s", libusb_strerror(r));
+ return false;
+ }
+ return true;
+}
+
+static bool
+moticam_control_vendor_w(struct moticam_device *mdev, uint16_t wValue,
+ const uint16_t data0, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ uint8_t data[] = { (uint8_t) (data0 >> 8), (uint8_t) data0 };
+ return moticam_control_vendor(mdev, wValue, data, sizeof(data), error);
+}
+
+static bool
+moticam_control_reset(struct moticam_device *mdev, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ if (!moticam_control_vendor_w(mdev, 0xba00, 0x0000, error))
+ return false;
+ if (!moticam_control_vendor_w(mdev, 0xba00, 0x0001, error))
+ return false;
+ return true;
+}
+
+static bool
+moticam_control_exposure(struct moticam_device *mdev, double exposure_ms,
+ GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ int exposure_w = exposure_ms * 12.82;
+ if (exposure_w < 0x000c)
+ exposure_w = 0x000c;
+ else if (exposure_w > 0xffff)
+ exposure_w = 0xffff;
+ return moticam_control_vendor_w(mdev, 0xba09, exposure_w, error);
+}
+
+static bool
+moticam_control_gain(struct moticam_device *mdev, double gain, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ double gmin, gmax;
+ int xmin, xmax;
+ bool up = false;
+ if (gain <= 1.34) {
+ gmin = 0.33;
+ gmax = 1.33;
+ xmin = 0x08;
+ xmax = 0x20;
+ } else if (gain <= 2.68) {
+ gmin = 1.42;
+ gmax = 2.67;
+ xmin = 0x51;
+ xmax = 0x60;
+ } else {
+ gmin = 3;
+ gmax = 42.67;
+ xmin = 0x1;
+ xmax = 0x78;
+ up = true;
+ }
+ int x = (gain - gmin) / (gmax - gmin) * (xmax - xmin) + xmin;
+ if (x < xmin)
+ x = xmin;
+ else if (x > xmax)
+ x = xmax;
+ if (up)
+ x = (x << 8) | 0x60;
+ if (!moticam_control_vendor_w(mdev, 0xba2d, x, error))
+ return false;
+ if (!moticam_control_vendor_w(mdev, 0xba2b, x, error))
+ return false;
+ if (!moticam_control_vendor_w(mdev, 0xba2e, x, error))
+ return false;
+ if (!moticam_control_vendor_w(mdev, 0xba2c, x, error))
+ return false;
+ return true;
+}
+
+static bool
+moticam_control_resolution(struct moticam_device *mdev, int width, int height,
+ GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ static const uint8_t control_init[] = {
+ 0x00, 0x14, 0x00, 0x20, 0x05, 0xff, 0x07, 0xff };
+ if (!moticam_control_vendor(mdev, 0xba01, control_init,
+ sizeof(control_init), error))
+ return false;
+ static const uint8_t control_512x384[] = { 0x00, 0x03, 0x00, 0x03 };
+ static const uint8_t control_1024x768[] = { 0x00, 0x11, 0x00, 0x11 };
+ static const uint8_t control_2048x1536[] = { 0x00, 0x00, 0x00, 0x00 };
+ bool ret;
+ switch (width)
+ {
+ case 512:
+ g_assert(height == 384);
+ ret = moticam_control_vendor(mdev, 0xba22, control_512x384,
+ sizeof(control_512x384), error);
+ break;
+ case 1024:
+ g_assert(height == 768);
+ ret = moticam_control_vendor(mdev, 0xba22, control_1024x768,
+ sizeof(control_1024x768), error);
+ break;
+ case 2048:
+ g_assert(height == 1536);
+ ret = moticam_control_vendor(mdev, 0xba22, control_2048x1536,
+ sizeof(control_2048x1536), error);
+ break;
+ default:
+ g_assert_not_reached();
+ }
+ return ret;
+}
+
+static const struct device_info *
+moticam_get_info(struct device *device)
+{
+ return &moticam_device_info;
+}
+
+static void
+moticam_set_exposure(struct device *device, double exposure_ms)
+{
+ struct moticam_device *mdev = (struct moticam_device *) device;
+ mdev->exposure_ms = exposure_ms;
+ mdev->exposure_pending = true;
+}
+
+static void
+moticam_set_gain(struct device *device, double gain)
+{
+ struct moticam_device *mdev = (struct moticam_device *) device;
+ mdev->gain = gain;
+ mdev->gain_pending = true;
+}
+
+static void
+moticam_set_resolution(struct device *device, int width, int height,
+ int stride)
+{
+ struct moticam_device *mdev = (struct moticam_device *) device;
+ g_assert(!mdev->current_image);
+ /* Ignore stride, store parameters. */
+ mdev->width = width;
+ mdev->height = height;
+}
+
+static void
+moticam_transfer_cb(struct libusb_transfer *transfer)
+{
+ struct moticam_device *mdev = transfer->user_data;
+ mdev->usb_transfer_done = true;
+}
+
+static bool
+moticam_start(struct device *device, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ struct moticam_device *mdev = (struct moticam_device *) device;
+ g_assert(!mdev->current_image);
+ /* Configure camera. */
+ if (!moticam_control_exposure(mdev, 30.0, error))
+ return false;
+ if (!moticam_control_gain(mdev, mdev->gain, error))
+ return false;
+ if (!moticam_control_resolution(mdev, mdev->width, mdev->height, error))
+ return false;
+ if (!moticam_control_exposure(mdev, mdev->exposure_ms, error))
+ return false;
+ mdev->exposure_pending = false;
+ mdev->gain_pending = false;
+ utils_delay_us(100000);
+ /* Prepare sizes and buffers, request an extra frame to read the zero
+ * length packet. */
+ int image_size = mdev->width * mdev->height;
+ int frame_size = 16384;
+ mdev->rawbuffer_size = (image_size + frame_size)
+ / frame_size * frame_size;
+ for (int i = 0; i < 2; i++) {
+ struct image *image = &mdev->image[i];
+ image->width = mdev->width;
+ image->height = mdev->height;
+ image->stride = mdev->width;
+ image->format = IMAGE_FORMAT_SGRBG8;
+ image->pixels = libusb_dev_mem_alloc(mdev->handle,
+ mdev->rawbuffer_size);
+ if (!image->pixels)
+ g_error("can not allocate raw buffer");
+ g_assert(image->refs == 0);
+ image->release = NULL;
+ image->release_user_data = NULL;
+ }
+ mdev->current_image = &mdev->image[0];
+ mdev->other_image = &mdev->image[1];
+ /* Drop first frame. */
+ mdev->drop = 1;
+ /* Prepare and submit transfer. */
+ mdev->usb_transfer = libusb_alloc_transfer(0);
+ libusb_fill_bulk_transfer(mdev->usb_transfer, mdev->handle, 0x83,
+ mdev->current_image->pixels, mdev->rawbuffer_size,
+ moticam_transfer_cb, mdev, 30000);
+ int r = libusb_submit_transfer(mdev->usb_transfer);
+ if (r) {
+ g_set_error(error, DEVICE_ERROR, DEVICE_ERROR_USB,
+ "can not submit transfer: %s", libusb_strerror(r));
+ /* Cancel start. */
+ moticam_stop(device, NULL);
+ return false;
+ }
+ return true;
+}
+
+static struct image *
+moticam_read(struct device *device, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, NULL);
+ struct moticam_device *mdev = (struct moticam_device *) device;
+ g_assert(mdev->current_image);
+ if (mdev->usb_transfer_done) {
+ if (mdev->usb_transfer->status == LIBUSB_TRANSFER_COMPLETED) {
+ const int image_size = mdev->width * mdev->height;
+ struct image *image = NULL;
+ bool error_set = false;
+ /* Image received? Return it, else drop. */
+ if (!mdev->drop
+ && mdev->usb_transfer->actual_length == image_size) {
+ image = mdev->current_image;
+ image->refs = 1;
+ mdev->current_image = mdev->other_image;
+ mdev->other_image = image;
+ }
+ if (mdev->drop)
+ mdev->drop--;
+ /* Pending updates? */
+ if (image && !error_set && mdev->exposure_pending) {
+ if (!moticam_control_exposure(mdev, mdev->exposure_ms, error))
+ error_set = true;
+ else
+ mdev->exposure_pending = false;
+ }
+ if (image && !error_set && mdev->gain_pending) {
+ if (!moticam_control_gain(mdev, mdev->gain, error))
+ error_set = true;
+ else
+ mdev->gain_pending = false;
+ }
+ /* Start a new transfer. */
+ if (!error_set) {
+ g_assert(mdev->current_image->refs == 0);
+ mdev->usb_transfer_done = false;
+ mdev->usb_transfer->buffer = mdev->current_image->pixels;
+ int r = libusb_submit_transfer(mdev->usb_transfer);
+ if (r) {
+ g_set_error(error, DEVICE_ERROR, DEVICE_ERROR_USB,
+ "can not submit transfer: %s", libusb_strerror(r));
+ error_set = true;
+ }
+ }
+ /* Return image or NULL. */
+ if (error_set && image) {
+ image_unref(image);
+ image = NULL;
+ }
+ return image;
+ } else {
+ g_set_error(error, DEVICE_ERROR, DEVICE_ERROR_USB,
+ "transfer stopped");
+ return NULL;
+ }
+ } else
+ return NULL;
+}
+
+static bool
+moticam_stop(struct device *device, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ struct moticam_device *mdev = (struct moticam_device *) device;
+ g_assert(mdev->current_image);
+ bool ret = true;
+ /* Cancel transfer if running. */
+ if (!mdev->usb_transfer_done) {
+ int r = libusb_cancel_transfer(mdev->usb_transfer);
+ if (r && r != LIBUSB_ERROR_NOT_FOUND) {
+ g_set_error(error, DEVICE_ERROR, DEVICE_ERROR_USB,
+ "can not cancel transfer: %s", libusb_strerror(r));
+ ret = false;
+ }
+ while (!mdev->usb_transfer_done) {
+ int r = libusb_handle_events(mdev->usb);
+ if (r) {
+ g_error("unable to handle libusb events: %s",
+ libusb_strerror(r));
+ }
+ }
+ }
+ /* Release. */
+ libusb_free_transfer(mdev->usb_transfer);
+ mdev->usb_transfer = NULL;
+ for (int i = 0; i < 2; i++) {
+ libusb_dev_mem_free(mdev->handle, mdev->image[i].pixels,
+ mdev->rawbuffer_size);
+ mdev->image[i].pixels = NULL;
+ }
+ mdev->current_image = NULL;
+ mdev->other_image = NULL;
+ /* Stop camera. */
+ for (int i = 0; ret && i < 3; i++)
+ ret = moticam_control_exposure(mdev, 0.0, error);
+ return ret;
+}
+
+static bool
+moticam_close(struct device *device, GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, false);
+ struct moticam_device *mdev = (struct moticam_device *) device;
+ bool ret = true;
+ if (mdev->current_image)
+ ret = moticam_stop(device, error);
+ libusb_close(mdev->handle);
+ free(device);
+ return ret;
+}
+
+static char *
+moticam_usb_poll(struct libusb_device_descriptor *desc)
+{
+ if (desc->idVendor == ID_VENDOR
+ && (desc->idProduct == ID_PRODUCT
+ || desc->idProduct == ID_PRODUCT_USB3))
+ return g_strdup("Moticam 3+");
+ else
+ return NULL;
+}
+
+static struct device *
+moticam_usb_open(libusb_context *usb, libusb_device *usb_device,
+ GError **error)
+{
+ g_return_val_if_fail(error == NULL || *error == NULL, NULL);
+ /* Create context. */
+ struct moticam_device *mdev = g_new(struct moticam_device, 1);
+ mdev->device.get_info = &moticam_get_info;
+ mdev->device.set_exposure = &moticam_set_exposure;
+ mdev->device.set_gain = &moticam_set_gain;
+ mdev->device.set_resolution = &moticam_set_resolution;
+ mdev->device.start = &moticam_start;
+ mdev->device.read = &moticam_read;
+ mdev->device.stop = &moticam_stop;
+ mdev->device.close = &moticam_close;
+ mdev->usb = usb;
+ mdev->handle = NULL;
+ mdev->width = 0;
+ mdev->height = 0;
+ mdev->exposure_ms = moticam_device_info.exposure_default_ms;
+ mdev->exposure_pending = false;
+ mdev->gain = moticam_device_info.gain_default;
+ mdev->gain_pending = false;
+ mdev->rawbuffer_size = 0;
+ for (int i = 0; i < 2; i++) {
+ mdev->image[i].refs = 0;
+ }
+ mdev->current_image = NULL;
+ mdev->other_image = NULL;
+ mdev->usb_transfer = NULL;
+ mdev->usb_transfer_done = false;
+ mdev->drop = 1;
+ /* Open USB device. */
+ int r = libusb_open(usb_device, &mdev->handle);
+ if (r) {
+ g_set_error(error, DEVICE_ERROR, DEVICE_ERROR_USB,
+ "can not open device: %s", libusb_strerror(r));
+ g_free(mdev);
+ return NULL;
+ }
+ /* Reset camera. */
+ if (!moticam_control_reset(mdev, error)) {
+ g_free(mdev);
+ return NULL;
+ }
+ /* Done. */
+ return &mdev->device;
+}
diff --git a/src/moticam.h b/src/moticam.h
new file mode 100644
index 0000000..b9eed99
--- /dev/null
+++ b/src/moticam.h
@@ -0,0 +1,31 @@
+#ifndef moticam_h
+#define moticam_h
+/* Camicro - Microscope camera viewer.
+ *
+ * This files add support for direct connexion with USB Moticam 3+.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include "device.h"
+
+extern const struct device_driver moticam_device_driver;
+
+#endif /* moticam_h */
diff --git a/src/options.c b/src/options.c
new file mode 100644
index 0000000..cbdaf3c
--- /dev/null
+++ b/src/options.c
@@ -0,0 +1,102 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#define _GNU_SOURCE
+#include <printf.h>
+
+#include "options.h"
+#include "cli.h"
+#include "utils.h"
+
+void
+options_add(GApplication *app)
+{
+ g_application_set_option_context_summary(app, "Microscope camera viewer");
+ g_application_set_option_context_description(app,
+ "Without options, run the GUI, else switch to batch mode to dump"
+ " image from the camera.");
+ g_application_add_main_option(app, "width", 'w', 0,
+ G_OPTION_ARG_INT, "image width", "VALUE");
+ g_application_add_main_option(app, "exposure", 'e', 0,
+ G_OPTION_ARG_DOUBLE, "exposure value", "MS");
+ g_application_add_main_option(app, "gain", 'g', 0,
+ G_OPTION_ARG_DOUBLE, "gain value", "VALUE");
+ g_application_add_main_option(app, "count", 'n', 0,
+ G_OPTION_ARG_INT, "number of image to take", "N");
+ g_application_add_main_option(app, "raw", 'r', 0,
+ G_OPTION_ARG_NONE, "do not convert image", NULL);
+ g_application_add_main_option(app, "output", 'o', 0,
+ G_OPTION_ARG_FILENAME,
+ "output file pattern (default: out%02d.png)", "PATTERN");
+}
+
+gint
+options_handle(GApplication *app, GVariantDict *options_dict)
+{
+ struct options options;
+ options.width = -1;
+ options.height = -1;
+ options.exposure_ms = -1.0;
+ options.gain = -1.0;
+ options.count = 1;
+ options.raw = false;
+ options.out = NULL;
+ bool option_set = false;
+ if (g_variant_dict_lookup(options_dict, "width", "i", &options.width)) {
+ option_set = true;
+ if (options.width <= 0)
+ utils_fatal("invalid width");
+ }
+ if (g_variant_dict_lookup(options_dict, "exposure", "d",
+ &options.exposure_ms)) {
+ option_set = true;
+ if (options.exposure_ms < 1.0)
+ utils_fatal("invalid exposure");
+ }
+ if (g_variant_dict_lookup(options_dict, "gain", "d", &options.gain)) {
+ option_set = true;
+ if (options.gain <= 0.0)
+ utils_fatal("invalid gain");
+ }
+ if (g_variant_dict_lookup(options_dict, "count", "i", &options.count)) {
+ option_set = true;
+ if (options.count <= 0)
+ utils_fatal("invalid count");
+ }
+ if (g_variant_dict_contains(options_dict, "raw")) {
+ option_set = true;
+ options.raw = true;
+ }
+ if (g_variant_dict_lookup(options_dict, "out", "^&ay", &options.out)) {
+ option_set = true;
+ int argtypes[1];
+ int formats = parse_printf_format(options.out, 1, argtypes);
+ if (formats != 1 || argtypes[0] != PA_INT)
+ utils_fatal("bad file pattern, use one %%d");
+ } else {
+ options.out = "out%02d.png";
+ }
+ if (option_set)
+ return cli_run(&options);
+ else
+ return -1;
+}
diff --git a/src/options.h b/src/options.h
new file mode 100644
index 0000000..7a08619
--- /dev/null
+++ b/src/options.h
@@ -0,0 +1,55 @@
+#ifndef options_h
+#define options_h
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <stdbool.h>
+#include <gtk/gtk.h>
+
+/* Runtime options. */
+struct options {
+ /* Image width, -1 for default. */
+ int width;
+ /* Image height, -1 for default. */
+ int height;
+ /* Exposure in ms, -1.0 for default. */
+ double exposure_ms;
+ /* Digital gain after image acquisition, -1.0 for default. */
+ double gain;
+ /* Number of frame to dump. */
+ int count;
+ /* Do not convert image. */
+ bool raw;
+ /* Output file name or pattern. This should include a printf like pattern
+ * (%d) used to name the files. */
+ const char *out;
+};
+
+/* Add options to application. */
+void
+options_add(GApplication *app);
+
+/* Handle options. */
+gint
+options_handle(GApplication *app, GVariantDict *options_dict);
+
+#endif /* options_h */
diff --git a/src/usb_source.c b/src/usb_source.c
new file mode 100644
index 0000000..761d103
--- /dev/null
+++ b/src/usb_source.c
@@ -0,0 +1,136 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <stdbool.h>
+
+#include "usb_source.h"
+
+struct fd_tag {
+ int fd;
+ gpointer tag;
+};
+
+struct usb_source {
+ GSource parent;
+ libusb_context *usb;
+ GArray *fd_tag;
+};
+
+static gboolean
+usb_source_prepare(GSource *source, int *timeout)
+{
+ return FALSE;
+}
+
+static gboolean
+usb_source_check(GSource *source)
+{
+ GIOCondition revents = 0;
+ struct usb_source *usb_source = (struct usb_source *)source;
+ for (guint i = 0; i < usb_source->fd_tag->len; i++) {
+ struct fd_tag e = g_array_index(usb_source->fd_tag, struct fd_tag, i);
+ revents |= g_source_query_unix_fd(source, e.tag);
+ }
+ return revents != 0;
+}
+
+static gboolean
+usb_source_dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
+{
+ if (!callback)
+ return G_SOURCE_CONTINUE;
+ else
+ return callback(user_data);
+}
+
+static void
+usb_source_finalize(GSource *source)
+{
+ struct usb_source *usb_source = (struct usb_source *)source;
+ libusb_set_pollfd_notifiers(usb_source->usb, NULL, NULL, NULL);
+ for (guint i = 0; i < usb_source->fd_tag->len; i++) {
+ struct fd_tag e = g_array_index(usb_source->fd_tag, struct fd_tag, i);
+ g_source_remove_unix_fd(source, e.tag);
+ }
+ g_array_unref(usb_source->fd_tag);
+}
+
+static void
+usb_source_pollfd_added(int fd, short events, void *user_data)
+{
+ struct usb_source *usb_source = user_data;
+ if (!g_source_is_destroyed(&usb_source->parent)) {
+ gpointer tag = g_source_add_unix_fd(&usb_source->parent, fd, events);
+ struct fd_tag fd_tag = { fd, tag };
+ g_array_append_val(usb_source->fd_tag, fd_tag);
+ }
+}
+
+static void
+usb_source_pollfd_removed(int fd, void *user_data)
+{
+ struct usb_source *usb_source = user_data;
+ if (!g_source_is_destroyed(&usb_source->parent)) {
+ bool found = false;
+ for (guint i = 0; !found && i < usb_source->fd_tag->len; i++) {
+ struct fd_tag e =
+ g_array_index(usb_source->fd_tag, struct fd_tag, i);
+ if (e.fd == fd) {
+ g_source_remove_unix_fd(&usb_source->parent, e.tag);
+ g_array_remove_index_fast(usb_source->fd_tag, i);
+ found = true;
+ }
+ }
+ g_assert(found);
+ }
+}
+
+GSource *
+usb_source_new(libusb_context *usb)
+{
+ /* Get file descriptors. */
+ const struct libusb_pollfd **pollfds = libusb_get_pollfds(usb);
+ if (!pollfds)
+ g_error("unable to get libusb file descriptors");
+ /* Create source. */
+ static GSourceFuncs funcs = {
+ .prepare = usb_source_prepare,
+ .check = usb_source_check,
+ .dispatch = usb_source_dispatch,
+ .finalize = usb_source_finalize,
+ };
+ GSource *source = g_source_new(&funcs, sizeof(struct usb_source));
+ struct usb_source *usb_source = (struct usb_source *)source;
+ g_source_set_name(source, "usb");
+ usb_source->usb = usb;
+ usb_source->fd_tag = g_array_new(FALSE, FALSE, sizeof(struct fd_tag));
+ g_assert(libusb_pollfds_handle_timeouts(usb));
+ /* Add existing file descriptors. */
+ for (const struct libusb_pollfd **pollfd = pollfds; *pollfd; pollfd++)
+ usb_source_pollfd_added((*pollfd)->fd, (*pollfd)->events, usb_source);
+ libusb_free_pollfds(pollfds);
+ /* Connect notifiers. */
+ libusb_set_pollfd_notifiers(usb, usb_source_pollfd_added,
+ usb_source_pollfd_removed, usb_source);
+ /* Done. */
+ return &usb_source->parent;
+}
diff --git a/src/usb_source.h b/src/usb_source.h
new file mode 100644
index 0000000..0b76ce8
--- /dev/null
+++ b/src/usb_source.h
@@ -0,0 +1,31 @@
+#ifndef usb_source_h
+#define usb_source_h
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <libusb.h>
+#include <gtk/gtk.h>
+
+GSource *
+usb_source_new(libusb_context *usb);
+
+#endif /* usb_source_h */
diff --git a/src/utils.c b/src/utils.c
new file mode 100644
index 0000000..ddd044f
--- /dev/null
+++ b/src/utils.c
@@ -0,0 +1,80 @@
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <time.h>
+
+#include "utils.h"
+
+void
+utils_fatal(const char *fmt, ...)
+{
+ va_list ap;
+ fprintf(stderr, "%s: ", program_invocation_short_name);
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+ exit(EXIT_FAILURE);
+}
+
+void
+utils_info(const char *fmt, ...)
+{
+ va_list ap;
+ fprintf(stderr, "%s: ", program_invocation_short_name);
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+}
+
+void
+utils_delay_us(int us)
+{
+ struct timespec delay, remaining;
+ delay.tv_sec = us / 1000000;
+ delay.tv_nsec = us % 1000000 * 1000;
+ while (nanosleep(&delay, &remaining) == -1) {
+ if (errno == EINTR)
+ delay = remaining;
+ else
+ g_error("can not sleep: %m");
+ }
+}
+
+void
+utils_dialog_error(GtkWindow *parent, const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ char *msg = g_strdup_vprintf(fmt, ap);
+ va_end(ap);
+ GtkWidget *dialog = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL,
+ GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", msg);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ g_free(msg);
+}
diff --git a/src/utils.h b/src/utils.h
new file mode 100644
index 0000000..ef495e5
--- /dev/null
+++ b/src/utils.h
@@ -0,0 +1,46 @@
+#ifndef utils_h
+#define utils_h
+/* Camicro - Microscope camera viewer.
+ *
+ * Copyright (C) 2019 Nicolas Schodet
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Contact :
+ * Web: http://ni.fr.eu.org/
+ * Email: <nico at ni.fr.eu.org>
+ */
+#include <gtk/gtk.h>
+
+/* Report a fatal error. */
+void
+utils_fatal(const char *fmt, ...)
+ __attribute__((format(printf, 1, 2), noreturn));
+
+/* Report an info. */
+void
+utils_info(const char *fmt, ...)
+ __attribute__((format(printf, 1, 2)));
+
+/* Sleep for a given number of microseconds. */
+void
+utils_delay_us(int us);
+
+/* Display an error message. */
+void
+utils_dialog_error(GtkWindow *parent, const char *fmt, ...)
+ __attribute__((format(printf, 2, 3)));
+
+#endif /* utils_h */