summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolas Schodet2019-11-12 15:54:32 +0100
committerNicolas Schodet2019-11-14 00:41:59 +0100
commitaea9101e6b3da183adcd99f5377c799d194efd11 (patch)
treefc0129281c7459071918817e72377326e2d5b094
parent2365eb0d2351308e9f2bf445dc59a84bedc03e7a (diff)
Replace SDL GUI with GTK+ GUI.
-rw-r--r--.gitignore1
-rw-r--r--Makefile15
-rw-r--r--device.c14
-rw-r--r--device.h22
-rw-r--r--gui.c37
-rw-r--r--gui.h (renamed from live.h)12
-rw-r--r--gui_app.c120
-rw-r--r--gui_app.gresource.xml6
-rw-r--r--gui_app.h33
-rw-r--r--gui_app_window.c330
-rw-r--r--gui_app_window.h40
-rw-r--r--live.c329
-rw-r--r--main.c49
-rw-r--r--moticam.c162
-rw-r--r--options.c20
-rw-r--r--usb_source.c138
-rw-r--r--usb_source.h31
-rw-r--r--window.ui157
18 files changed, 1093 insertions, 423 deletions
diff --git a/.gitignore b/.gitignore
index d4b91b7..6d2da66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+gui_resources.c
*.o
*.d
camicro
diff --git a/Makefile b/Makefile
index a7ff439..e80a611 100644
--- a/Makefile
+++ b/Makefile
@@ -1,23 +1,32 @@
-libs := libusb-1.0 libpng16 sdl2 SDL2_ttf
+libs := libusb-1.0 libpng16 gtk+-3.0
CFLAGS := -g -Wall $(shell pkg-config $(libs) --cflags)
CPPFLAGS := -MMD
LDLIBS := $(shell pkg-config $(libs) --libs)
+GLIB_COMPILE_RESOURCES = \
+ $(shell pkg-config --variable=glib_compile_resources gio-2.0)
SOURCES := \
device.c \
image.c \
- live.c \
+ gui.c \
+ gui_app.c \
+ gui_app_window.c \
+ gui_resources.c \
main.c \
moticam.c \
options.c \
+ usb_source.c \
utils.c
all: camicro
+gui_resources.c: gui_app.gresource.xml window.ui
+ $(GLIB_COMPILE_RESOURCES) $< --target=$@ --sourcedir=. --generate-source
+
camicro: $(SOURCES:%.c=%.o)
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
clean:
- rm -f camicro $(SOURCES:%.c=%.o)
+ rm -f camicro $(SOURCES:%.c=%.o) gui_resources.c
-include $(SOURCES:%.c=%.d)
diff --git a/device.c b/device.c
index 620f132..549cce8 100644
--- a/device.c
+++ b/device.c
@@ -67,7 +67,7 @@ device_open(libusb_context *usb)
struct device *device = NULL;
if (found_usb_device) {
free(found_device_name);
- device = found_device_driver->usb_open(found_usb_device);
+ device = found_device_driver->usb_open(usb, found_usb_device);
}
libusb_free_device_list(list, 1);
return device;
@@ -98,6 +98,12 @@ device_set_resolution(struct device *device, int width, int height,
device->set_resolution(device, width, height, stride);
}
+void
+device_start(struct device *device)
+{
+ device->start(device);
+}
+
const struct device_image *
device_read(struct device *device)
{
@@ -105,6 +111,12 @@ device_read(struct device *device)
}
void
+device_stop(struct device *device)
+{
+ device->stop(device);
+}
+
+void
device_close(struct device *device)
{
device->close(device);
diff --git a/device.h b/device.h
index 9e154d8..86e4b4f 100644
--- a/device.h
+++ b/device.h
@@ -37,7 +37,7 @@ typedef char *(*device_driver_usb_poll_f)(
/* USB open function. The device driver previously declared that it handles
* this device. */
typedef struct device *(*device_driver_usb_open_f)(
- libusb_device *usb_device);
+ libusb_context *usb, libusb_device *usb_device);
/* Information on a device driver. */
struct device_driver {
@@ -94,6 +94,10 @@ typedef void (*device_set_gain_f)(
typedef void (*device_set_resolution_f)(
struct device *device, int width, int height, int stride);
+/* Start streaming. */
+typedef void (*device_start_f)(
+ struct device *device);
+
/* Image with attached information. */
struct device_image {
/* Image width. */
@@ -110,6 +114,10 @@ struct device_image {
typedef const struct device_image *(*device_read_f)(
struct device *device);
+/* Stop streaming. */
+typedef void (*device_stop_f)(
+ struct device *device);
+
/* Close device. */
typedef void (*device_close_f)(
struct device *device);
@@ -124,8 +132,12 @@ struct device {
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;
};
@@ -151,10 +163,18 @@ void
device_set_resolution(struct device *device, int width, int height,
int stride);
+/* Start streaming. */
+void
+device_start(struct device *device);
+
/* Read an image. */
const struct device_image *
device_read(struct device *device);
+/* Stop streaming. */
+void
+device_stop(struct device *device);
+
/* Close device. */
void
device_close(struct device *device);
diff --git a/gui.c b/gui.c
new file mode 100644
index 0000000..bce344a
--- /dev/null
+++ b/gui.c
@@ -0,0 +1,37 @@
+/* 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.h"
+#include "gui_app.h"
+
+int
+gui_run(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/live.h b/gui.h
index b3b641c..b244717 100644
--- a/live.h
+++ b/gui.h
@@ -1,5 +1,5 @@
-#ifndef live_h
-#define live_h
+#ifndef gui_h
+#define gui_h
/* Camicro - Microscope camera viewer.
*
* Copyright (C) 2019 Nicolas Schodet
@@ -22,10 +22,8 @@
* Web: http://ni.fr.eu.org/
* Email: <nico at ni.fr.eu.org>
*/
-#include "device.h"
-#include "options.h"
-void
-live_run(struct device *device, struct options *options);
+int
+gui_run(int argc, char **argv);
-#endif /* live_h */
+#endif /* gui_h */
diff --git a/gui_app.c b/gui_app.c
new file mode 100644
index 0000000..d29ba2a
--- /dev/null
+++ b/gui_app.c
@@ -0,0 +1,120 @@
+/* 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 "gui_app_window.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;
+ libusb_handle_events_timeout(priv->usb, &tv);
+ 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;
+}
+
+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)
+ utils_fatal("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));
+ struct device *device = device_open(priv->usb);
+ if (!device)
+ utils_fatal("unable to find device");
+ 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_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/gui_app.gresource.xml b/gui_app.gresource.xml
new file mode 100644
index 0000000..f79e22f
--- /dev/null
+++ b/gui_app.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/eu/fr/ni/camicro">
+ <file preprocess="xml-stripblanks">window.ui</file>
+ </gresource>
+</gresources>
diff --git a/gui_app.h b/gui_app.h
new file mode 100644
index 0000000..8eeeff1
--- /dev/null
+++ b/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/gui_app_window.c b/gui_app_window.c
new file mode 100644
index 0000000..0acdaaa
--- /dev/null
+++ b/gui_app_window.c
@@ -0,0 +1,330 @@
+/* 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 <stdbool.h>
+#include <assert.h>
+#include <math.h>
+
+#include "device.h"
+
+#include "gui_app.h"
+#include "gui_app_window.h"
+
+struct _GuiAppWindow {
+ GtkApplicationWindow parent;
+};
+
+typedef struct _GuiAppWindowPrivate GuiAppWindowPrivate;
+
+struct _GuiAppWindowPrivate {
+ GuiApp *app;
+ GtkDrawingArea *video;
+ GtkButton *start_stop_button;
+ GtkComboBoxText *resolution_combo_box;
+ GtkAdjustment *exposure_adj;
+ GtkAdjustment *gain_adj;
+ GtkCheckButton *rotate_button;
+ GdkRectangle allocation;
+ cairo_matrix_t transform;
+ cairo_surface_t *surface;
+ 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 offset_x = 0.0;
+ double offset_y = 0.0;
+ if (scale_x > scale_y) {
+ scale_y = scale_x;
+ offset_y = -(dest_height - source_height / scale_y) / 2.0;
+ } else {
+ scale_x = scale_y;
+ offset_x = -(dest_width - source_width / scale_x) / 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_x, scale_y);
+ 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) {
+ const struct device_image *image = device_read(priv->device);
+ if (image) {
+ if (priv->surface) {
+ cairo_surface_destroy(priv->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));
+ }
+ }
+}
+
+/* Start video. */
+static void
+video_start(GuiAppWindowPrivate *priv)
+{
+ assert(!priv->started);
+ priv->started = true;
+ 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);
+ device_start(priv->device);
+ priv->video_ready_handler_id = g_signal_connect(priv->app, "video-ready",
+ G_CALLBACK(video_ready_cb), priv);
+}
+
+/* Stop video. */
+static void
+video_stop(GuiAppWindowPrivate *priv)
+{
+ assert(priv->started);
+ if (priv->surface) {
+ cairo_surface_destroy(priv->surface);
+ priv->surface = NULL;
+ }
+ g_signal_handler_disconnect(priv->app, priv->video_ready_handler_id);
+ device_stop(priv->device);
+ priv->started = false;
+ gtk_widget_queue_draw(GTK_WIDGET(priv->video));
+}
+
+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) {
+ device_close(priv->device);
+ 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) {
+ gtk_button_set_label(button, "Stop");
+ video_start(priv);
+ } else {
+ gtk_button_set_label(button, "Start");
+ 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);
+ 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
+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
+gui_app_window_init(GuiAppWindow *win)
+{
+ gtk_widget_init_template(GTK_WIDGET(win));
+ GuiAppWindowPrivate *priv = gui_app_window_get_instance_private(win);
+ priv->app = NULL;
+ priv->allocation.width = -1;
+ cairo_matrix_init_identity(&priv->transform);
+ priv->surface = NULL;
+ 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, rotate_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),
+ rotate_button_toggled_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);
+ 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->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);
+ 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);
+}
diff --git a/gui_app_window.h b/gui_app_window.h
new file mode 100644
index 0000000..2f886db
--- /dev/null
+++ b/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/live.c b/live.c
deleted file mode 100644
index 9bb52be..0000000
--- a/live.c
+++ /dev/null
@@ -1,329 +0,0 @@
-/* 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 <stdarg.h>
-#include <stdio.h>
-#include <string.h>
-
-#include <SDL.h>
-#include <SDL_ttf.h>
-
-#include "live.h"
-#include "utils.h"
-
-#define FONT "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
-#define PTSIZE 18
-
-#define HELP \
- "←,→ - change exposure\n" \
- "↑,↓ - change gain\n" \
- "z - reset exposure & gain\n" \
- "f - toggle full screen\n" \
- "q - exit\n" \
- "h - help screen"
-
-#define LOGICAL_WIDTH 1000
-
-struct live
-{
- struct device *device;
- struct options *options;
- SDL_Window *window;
- SDL_Renderer *renderer;
- SDL_Rect logical_rect;
- SDL_Texture *msg_texture;
- SDL_Rect msg_shaded_rect;
- SDL_Rect msg_rect;
- uint32_t msg_expire;
- TTF_Font *font;
- bool fullscreen;
- double exposure_ms;
- double gain;
-};
-
-static void
-live_init(struct live *live, struct device *device, struct options *options)
-{
- live->device = device;
- live->options = options;
- /* Initialise SDL. */
- if (SDL_Init(SDL_INIT_VIDEO))
- utils_fatal("unable to initialize SDL: %s", SDL_GetError());
- atexit(SDL_Quit);
- /* Compute window size. */
- int num_video_displays = SDL_GetNumVideoDisplays();
- int width_max = INT_MAX;
- int height_max = INT_MAX;
- for (int i = 0; i < num_video_displays; i++) {
- SDL_DisplayMode mode;
- if (SDL_GetCurrentDisplayMode(i, &mode) == 0) {
- if (mode.w < width_max)
- width_max = mode.w;
- if (mode.h < height_max)
- height_max = mode.h;
- }
- }
- width_max = width_max * 9 / 10;
- height_max = height_max * 9 / 10;
- int win_width = options->width;
- int win_height = options->height;
- if (win_width > width_max) {
- win_height = win_height * width_max / win_width;
- win_width = width_max;
- }
- if (win_height > height_max) {
- win_width = win_width * height_max / win_height;
- win_height = height_max;
- }
- /* Create window and renderer. */
- if (SDL_CreateWindowAndRenderer(win_width, win_height,
- SDL_WINDOW_RESIZABLE, &live->window, &live->renderer))
- utils_fatal("unable to create window: %s", SDL_GetError());
- SDL_SetWindowTitle(live->window, "Moticam");
- SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
- SDL_Rect logical_rect = { 0, 0, LOGICAL_WIDTH,
- LOGICAL_WIDTH * options->height / options->width };
- live->logical_rect = logical_rect;
- if (SDL_RenderSetLogicalSize(live->renderer, logical_rect.w,
- logical_rect.h))
- utils_fatal("can not set logical size: %s", SDL_GetError());
- /* Prepare message rendering. */
- if (TTF_Init())
- utils_fatal("can not initialize TTF: %s", SDL_GetError());
- live->msg_texture = NULL;
- live->font = TTF_OpenFont(FONT, PTSIZE);
- if (!live->font)
- utils_fatal("can not open font: %s", SDL_GetError());
- /* Disable screensaver. */
- SDL_DisableScreenSaver();
- /* Miscellaneous. */
- live->fullscreen = false;
- live->exposure_ms = options->exposure_ms;
- live->gain = options->gain;
-}
-
-static void
-live_uninit(struct live *live)
-{
- SDL_DestroyRenderer(live->renderer);
- SDL_DestroyWindow(live->window);
- TTF_Quit();
-}
-
-static void
-live_message_cancel(struct live *live)
-{
- /* Release previous message. */
- if (live->msg_texture)
- SDL_DestroyTexture(live->msg_texture);
- live->msg_texture = NULL;
-}
-
-static void
-live_message(struct live *live, int duration_ms, const char *fmt, ...)
- __attribute__((format(printf, 3, 4)));
-
-static void
-live_message(struct live *live, int duration_ms, const char *fmt, ...)
-{
- live_message_cancel(live);
- /* Format text. */
- va_list ap;
- va_start(ap, fmt);
- char *str = NULL;
- vasprintf(&str, fmt, ap);
- if (!str)
- utils_fatal("can not format message");
- va_end(ap);
- /* Compute text width. */
- int maxw = 0;
- char *line = str;
- char *end = strchr(line, '\n');
- int w;
- while (end)
- {
- *end = '\0';
- TTF_SizeUTF8(live->font, line, &w, NULL);
- if (w > maxw)
- maxw = w;
- *end = '\n';
- line = end + 1;
- end = strchr(line, '\n');
- }
- TTF_SizeUTF8(live->font, line, &w, NULL);
- if (w > maxw)
- maxw = w;
- /* Render new message. */
- SDL_Color color = { 255, 255, 255, 255 };
- SDL_Surface *surf = TTF_RenderUTF8_Blended_Wrapped(live->font, str, color,
- maxw);
- free(str);
- if (!surf)
- utils_fatal("can not render text: %s", SDL_GetError());
- /* Prepare rectangles and texture. */
- live->msg_shaded_rect = (SDL_Rect) { 0, 0, surf->w + 20, surf->h + 10 };
- live->msg_rect = (SDL_Rect) { 10, 5, surf->w, surf->h };
- live->msg_texture = SDL_CreateTextureFromSurface(live->renderer, surf);
- SDL_FreeSurface(surf);
- if (!live->msg_texture)
- utils_fatal("can not create text texture: %s", SDL_GetError());
- /* Update expiration. */
- if (duration_ms <= 0)
- live->msg_expire = 0;
- else
- live->msg_expire = SDL_GetTicks() + duration_ms;
-}
-
-static void
-live_render_message(struct live *live)
-{
- if (live->msg_texture) {
- if (SDL_TICKS_PASSED(SDL_GetTicks(), live->msg_expire)) {
- SDL_DestroyTexture(live->msg_texture);
- live->msg_texture = NULL;
- } else {
- SDL_SetRenderDrawBlendMode(live->renderer, SDL_BLENDMODE_BLEND);
- SDL_SetRenderDrawColor(live->renderer, 0, 0, 0, 128);
- SDL_RenderFillRect(live->renderer, &live->msg_shaded_rect);
- SDL_RenderCopy(live->renderer, live->msg_texture, NULL,
- &live->msg_rect);
- }
- }
-}
-
-static void
-live_change_exposure(struct live *live, int direction)
-{
- double now = live->exposure_ms + direction;
- double step = direction * (now < 100.0 ? 33.0
- : now < 1000.0 ? 100.0 : 500.0);
- double new_exposure_ms = live->exposure_ms + step;
- if (new_exposure_ms >= 33.0 && new_exposure_ms <= 5000.0) {
- live->exposure_ms = new_exposure_ms;
- device_set_exposure(live->device, new_exposure_ms);
- live_message(live, 5000, "exposure: %d ms", (int) new_exposure_ms);
- }
-}
-
-static void
-live_change_gain(struct live *live, int direction)
-{
- double now = live->gain + direction / 1000.0;
- double step = direction * (now < 1.0 ? 0.1 : 1.0);
- double new_gain = live->gain + step;
- if (new_gain >= 0.33 && new_gain <= 42.66) {
- live->gain = new_gain;
- device_set_gain(live->device, new_gain);
- live_message(live, 5000, "gain: %.2f", new_gain);
- }
-}
-
-static void
-live_reset(struct live *live)
-{
- if (live->exposure_ms != live->options->exposure_ms)
- device_set_exposure(live->device, live->options->exposure_ms);
- live->exposure_ms = live->options->exposure_ms;
- if (live->gain != live->options->gain)
- device_set_gain(live->device, live->options->gain);
- live->gain = live->options->gain;
- live_message(live, 5000, "reset exposure & gain");
-}
-
-static bool
-live_handle_event(struct live *live, SDL_Event *event)
-{
- if (event->type == SDL_QUIT)
- return true;
- if (event->type == SDL_KEYDOWN) {
- switch (event->key.keysym.sym) {
- case SDLK_q:
- case SDLK_ESCAPE:
- return true;
- case SDLK_f:
- live->fullscreen = !live->fullscreen;
- SDL_SetWindowFullscreen(live->window,
- live->fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
- break;
- case SDLK_QUESTION:
- case SDLK_h:
- live_message(live, 60000, HELP);
- break;
- case SDLK_LEFT:
- live_change_exposure(live, -1);
- break;
- case SDLK_RIGHT:
- live_change_exposure(live, +1);
- break;
- case SDLK_DOWN:
- live_change_gain(live, -1);
- break;
- case SDLK_UP:
- live_change_gain(live, +1);
- break;
- case SDLK_z:
- live_reset(live);
- break;
- default:
- live_message_cancel(live);
- break;
- }
- }
- return false;
-}
-
-void
-live_run(struct device *device, struct options *options)
-{
- struct live live;
- live_init(&live, device, options);
- live_message(&live, 5000, "Press h for help");
- SDL_Texture *texture = SDL_CreateTexture(live.renderer,
- SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING,
- options->width, options->height);
- if (!texture)
- utils_fatal("can not create texture: %s", SDL_GetError());
- bool exit = false;
- while (1) {
- SDL_Event event;
- while (SDL_PollEvent(&event)) {
- if (live_handle_event(&live, &event))
- exit = true;
- }
- if (exit)
- break;
- const struct device_image *image = device_read(device);
- if (image) {
- SDL_UpdateTexture(texture, NULL, image->pixels, image->stride);
- SDL_SetRenderDrawColor(live.renderer, 0, 0, 0, 0);
- SDL_RenderClear(live.renderer);
- SDL_RenderCopyEx(live.renderer, texture, NULL,
- &live.logical_rect, 180.0, NULL, SDL_FLIP_NONE);
- live_render_message(&live);
- SDL_RenderPresent(live.renderer);
- }
- }
- SDL_DestroyTexture(texture);
- live_uninit(&live);
-}
-
diff --git a/main.c b/main.c
index 7fd84ef..9ffd6f2 100644
--- a/main.c
+++ b/main.c
@@ -26,14 +26,16 @@
#include <string.h>
#include <png.h>
-#include "options.h"
#include "device.h"
-#include "live.h"
+#include "gui.h"
+#include "options.h"
#include "utils.h"
void
-run(struct device *device, int count, const char *out)
+cli_read_images(libusb_context *usb, struct device *device, int count,
+ const char *out)
{
+ device_start(device);
for (int i = 0; i < count;) {
const struct device_image *device_image = device_read(device);
if (device_image) {
@@ -53,15 +55,16 @@ run(struct device *device, int count, const char *out)
utils_fatal("can not write image: %s", image.message);
free(name);
i++;
+ } else {
+ libusb_handle_events(usb);
}
}
+ device_stop(device);
}
int
-main(int argc, char **argv)
+cli_run(struct options *options)
{
- struct options options;
- options_parse(argc, argv, &options);
libusb_context *usb;
int r = libusb_init(&usb);
if (r)
@@ -70,19 +73,15 @@ main(int argc, char **argv)
if (!device)
utils_fatal("unable to find device");
const struct device_info *info = device_get_info(device);
- if (options.exposure_ms > 0.0)
- device_set_exposure(device, options.exposure_ms);
- else
- options.exposure_ms = info->exposure_default_ms;
- if (options.gain > 0.0)
- device_set_gain(device, options.gain);
- else
- options.gain = info->gain_default;
+ 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)) {
+ if ((options->width == -1 || options->width == ir->width)
+ && (options->height == -1 || options->height == ir->height)) {
width = ir->width;
height = ir->height;
}
@@ -90,13 +89,19 @@ main(int argc, char **argv)
if (width == -1)
utils_fatal("no matching resolution");
device_set_resolution(device, width, height, 0);
- options.width = width;
- options.height = height;
- if (options.count)
- run(device, options.count, options.out);
- else
- live_run(device, &options);
+ cli_read_images(usb, device, options->count, options->out);
device_close(device);
libusb_exit(usb);
return EXIT_SUCCESS;
}
+
+int
+main(int argc, char **argv)
+{
+ struct options options;
+ options_parse(argc, argv, &options);
+ if (!options.count)
+ return gui_run(argc, argv);
+ else
+ return cli_run(&options);
+}
diff --git a/moticam.c b/moticam.c
index fd4c1e5..6e487ea 100644
--- a/moticam.c
+++ b/moticam.c
@@ -37,18 +37,23 @@
struct moticam_device {
struct device device;
+ libusb_context *usb;
libusb_device_handle *handle;
double exposure_ms;
+ bool exposure_pending;
double gain;
+ bool gain_pending;
size_t rawbuffer_size;
uint8_t *rawbuffer;
struct device_image image;
+ struct libusb_transfer *usb_transfer;
+ bool usb_transfer_done;
};
static char *
moticam_usb_poll(struct libusb_device_descriptor *desc);
static struct device *
-moticam_usb_open(libusb_device *usb_device);
+moticam_usb_open(libusb_context *usb, libusb_device *usb_device);
const struct device_driver moticam_device_driver = {
"Moticam USB cameras",
@@ -191,8 +196,7 @@ moticam_set_exposure(struct device *device, double exposure_ms)
{
struct moticam_device *mdev = (struct moticam_device *) device;
mdev->exposure_ms = exposure_ms;
- if (mdev->image.pixels)
- moticam_control_exposure(device, exposure_ms);
+ mdev->exposure_pending = true;
}
static void
@@ -200,8 +204,7 @@ moticam_set_gain(struct device *device, double gain)
{
struct moticam_device *mdev = (struct moticam_device *) device;
mdev->gain = gain;
- if (mdev->image.pixels)
- moticam_control_gain(device, gain);
+ mdev->gain_pending = true;
}
static void
@@ -209,12 +212,7 @@ moticam_set_resolution(struct device *device, int width, int height,
int stride)
{
struct moticam_device *mdev = (struct moticam_device *) device;
- /* Release previous configuration. */
- if (mdev->image.pixels) {
- free(mdev->rawbuffer);
- free(mdev->image.pixels);
- moticam_control_exposure(&mdev->device, 30.0);
- }
+ assert(!mdev->image.pixels);
/* Choose stride. */
if (!stride)
stride = width * sizeof(uint32_t);
@@ -223,37 +221,49 @@ moticam_set_resolution(struct device *device, int width, int height,
mdev->image.width = width;
mdev->image.height = height;
mdev->image.stride = stride;
- /* Prepare sizes and buffers, request an extra frame to read the zero
- * length packet. */
- int image_size = width * height;
- int frame_size = 16384;
- mdev->rawbuffer_size = (image_size + frame_size)
- / frame_size * frame_size;
- mdev->rawbuffer = utils_malloc(mdev->rawbuffer_size);
- mdev->image.pixels = utils_malloc(stride * height);
- /* Configure camera. */
- moticam_control_gain(&mdev->device, mdev->gain);
- moticam_control_resolution(&mdev->device, width, height);
- moticam_control_exposure(&mdev->device, mdev->exposure_ms);
- utils_delay_us(100000);
}
-static bool
-moticam_read_raw(struct device *device)
+static void
+moticam_transfer_cb(struct libusb_transfer *transfer)
+{
+ struct moticam_device *mdev = transfer->user_data;
+ mdev->usb_transfer_done = true;
+}
+
+static void
+moticam_start(struct device *device)
{
struct moticam_device *mdev = (struct moticam_device *) device;
+ assert(!mdev->image.pixels);
+ /* Configure camera. */
+ moticam_control_exposure(&mdev->device, 30.0);
+ moticam_control_gain(&mdev->device, mdev->gain);
+ moticam_control_resolution(&mdev->device, mdev->image.width,
+ mdev->image.height);
+ moticam_control_exposure(&mdev->device, mdev->exposure_ms);
+ 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->image.width * mdev->image.height;
- int transfered = 0;
- int r = libusb_bulk_transfer(mdev->handle, 0x83, mdev->rawbuffer,
- mdev->rawbuffer_size, &transfered, 0);
+ int frame_size = 16384;
+ mdev->rawbuffer_size = (image_size + frame_size)
+ / frame_size * frame_size;
+ mdev->rawbuffer = libusb_dev_mem_alloc(mdev->handle,
+ mdev->rawbuffer_size);
+ if (!mdev->rawbuffer)
+ utils_fatal("can not allocate raw buffer");
+ mdev->image.pixels = utils_malloc(
+ mdev->image.stride * mdev->image.height);
+ /* Prepare and submit transfer. */
+ mdev->usb_transfer = libusb_alloc_transfer(0);
+ libusb_fill_bulk_transfer(mdev->usb_transfer, mdev->handle, 0x83,
+ mdev->rawbuffer, mdev->rawbuffer_size, moticam_transfer_cb, mdev,
+ 30000);
+ int r = libusb_submit_transfer(mdev->usb_transfer);
if (r)
- utils_fatal("can not read data: %s", libusb_strerror(r));
- if (transfered != image_size) {
- utils_warning("bad image size (%d), drop", transfered);
- return false;
- } else {
- return true;
- }
+ utils_fatal("can not submit transfer: %s", libusb_strerror(r));
}
static const struct device_image *
@@ -261,27 +271,73 @@ moticam_read(struct device *device)
{
struct moticam_device *mdev = (struct moticam_device *) device;
assert(mdev->image.pixels);
- if (moticam_read_raw(device)) {
- image_bayer2argb(mdev->rawbuffer, mdev->image.pixels, mdev->image.width,
- mdev->image.height, mdev->image.stride);
- return &mdev->image;
- } else {
+ if (mdev->usb_transfer_done) {
+ if (mdev->usb_transfer->status == LIBUSB_TRANSFER_COMPLETED) {
+ const int image_size = mdev->image.width * mdev->image.height;
+ const struct device_image *image = NULL;
+ /* Image received? Convert, else drop. */
+ if (mdev->usb_transfer->actual_length == image_size) {
+ image_bayer2argb(mdev->rawbuffer, mdev->image.pixels,
+ mdev->image.width, mdev->image.height,
+ mdev->image.stride);
+ image = &mdev->image;
+ }
+ /* Pending updates? */
+ if (mdev->exposure_pending) {
+ moticam_control_exposure(device, mdev->exposure_ms);
+ mdev->exposure_pending = false;
+ }
+ if (mdev->gain_pending) {
+ moticam_control_gain(device, mdev->gain);
+ mdev->gain_pending = false;
+ }
+ /* Start a new transfer. */
+ mdev->usb_transfer_done = false;
+ int r = libusb_submit_transfer(mdev->usb_transfer);
+ if (r)
+ utils_fatal("can not submit transfer: %s",
+ libusb_strerror(r));
+ /* Return converted image or NULL. */
+ return image;
+ } else {
+ utils_fatal("transfer stopped: TODO");
+ return NULL;
+ }
+ } else
return NULL;
- }
}
static void
-moticam_close(struct device *device)
+moticam_stop(struct device *device)
{
struct moticam_device *mdev = (struct moticam_device *) device;
+ assert(mdev->image.pixels);
+ /* Cancel transfer if running. */
+ if (!mdev->usb_transfer_done) {
+ libusb_cancel_transfer(mdev->usb_transfer);
+ while (!mdev->usb_transfer_done)
+ libusb_handle_events(mdev->usb);
+ }
+ /* Release. */
+ libusb_free_transfer(mdev->usb_transfer);
+ mdev->usb_transfer = NULL;
+ libusb_dev_mem_free(mdev->handle, mdev->rawbuffer, mdev->rawbuffer_size);
+ mdev->rawbuffer = NULL;
+ free(mdev->image.pixels);
+ mdev->image.pixels = NULL;
+ /* Stop camera. */
moticam_control_exposure(device, 0.0);
moticam_control_exposure(device, 0.0);
moticam_control_exposure(device, 0.0);
+}
+
+static void
+moticam_close(struct device *device)
+{
+ struct moticam_device *mdev = (struct moticam_device *) device;
+ if (mdev->image.pixels)
+ moticam_stop(device);
libusb_close(mdev->handle);
- if (mdev->image.pixels) {
- free(mdev->rawbuffer);
- free(mdev->image.pixels);
- }
free(device);
}
@@ -297,7 +353,7 @@ moticam_usb_poll(struct libusb_device_descriptor *desc)
}
static struct device *
-moticam_usb_open(libusb_device *usb_device)
+moticam_usb_open(libusb_context *usb, libusb_device *usb_device)
{
/* Create context. */
struct moticam_device *mdev =
@@ -306,24 +362,30 @@ moticam_usb_open(libusb_device *usb_device)
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->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;
mdev->rawbuffer = NULL;
mdev->image.width = 0;
mdev->image.height = 0;
mdev->image.stride = 0;
mdev->image.pixels = NULL;
+ mdev->usb_transfer = NULL;
+ mdev->usb_transfer_done = false;
/* Open USB device. */
int r = libusb_open(usb_device, &mdev->handle);
if (r)
utils_fatal("can not open device: %s", libusb_strerror(r));
- /* Initialize camera. */
+ /* Reset camera. */
moticam_control_reset(&mdev->device);
- moticam_control_exposure(&mdev->device, 30.0);
/* Done. */
return &mdev->device;
}
diff --git a/options.c b/options.c
index 2bf1489..388402a 100644
--- a/options.c
+++ b/options.c
@@ -35,23 +35,21 @@ static void
usage(int status, const char *msg)
{
if (msg)
- fprintf(stderr, "%s: %s", program_invocation_short_name, msg);
+ fprintf(stderr, "%s: %s\n", program_invocation_short_name, msg);
fprintf(status == EXIT_SUCCESS ? stdout : stderr,
- "usage: %s [options] [FILE]\n"
+ "usage: %s [options]\n"
"\n"
"Microscope camera viewer.\n"
"\n"
- "positional arguments:\n"
- " FILE output file pattern (default:"
- " out%%02d.png)\n"
- "\n"
"optional arguments:\n"
" -h, --help show this help message and exit\n"
" -w, --width VALUE image width\n"
" -e, --exposure MS exposure value\n"
" -g, --gain VALUE gain value\n"
+ " -o, --output FILE output file pattern (default:"
+ " out%%02d.png)\n"
" -n, --count N number of image to take"
- " (default: live video)\n"
+ " (default: gui)\n"
, program_invocation_name);
exit(status);
}
@@ -72,11 +70,12 @@ options_parse(int argc, char **argv, struct options *options)
{ "width", required_argument, 0, 'w' },
{ "exposure", required_argument, 0, 'e' },
{ "gain", required_argument, 0, 'g' },
+ { "output", required_argument, 0, 'o' },
{ "count", required_argument, 0, 'n' },
{ NULL },
};
int option_index = 0;
- int c = getopt_long(argc, argv, "hw:e:g:n:", long_options,
+ int c = getopt_long(argc, argv, "hw:e:g:o:n:", long_options,
&option_index);
if (c == -1)
break;
@@ -102,6 +101,9 @@ options_parse(int argc, char **argv, struct options *options)
if (*tail != '\0' || errno || options->gain <= 0.0)
usage(EXIT_FAILURE, "bad gain value");
break;
+ case 'o':
+ options->out = optarg;
+ break;
case 'n':
errno = 0;
options->count = strtoul(optarg, &tail, 10);
@@ -116,8 +118,6 @@ options_parse(int argc, char **argv, struct options *options)
}
}
if (optind < argc)
- options->out = argv[optind++];
- if (optind < argc)
usage(EXIT_FAILURE, "too many arguments");
if (!options->out)
options->out = "out%02d.png";
diff --git a/usb_source.c b/usb_source.c
new file mode 100644
index 0000000..1a8174d
--- /dev/null
+++ b/usb_source.c
@@ -0,0 +1,138 @@
+/* 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 <assert.h>
+#include <stdbool.h>
+
+#include "usb_source.h"
+#include "utils.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;
+ }
+ }
+ assert(found);
+ }
+}
+
+GSource *
+usb_source_new(libusb_context *usb)
+{
+ /* Get file descriptors. */
+ const struct libusb_pollfd **pollfds = libusb_get_pollfds(usb);
+ if (!pollfds)
+ utils_fatal("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));
+ 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/usb_source.h b/usb_source.h
new file mode 100644
index 0000000..0b76ce8
--- /dev/null
+++ b/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/window.ui b/window.ui
new file mode 100644
index 0000000..fd44a1c
--- /dev/null
+++ b/window.ui
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkAdjustment" id="exposure_adj">
+ <property name="lower">1</property>
+ <property name="upper">1000</property>
+ <property name="value">100</property>
+ <property name="step_increment">25</property>
+ <property name="page_increment">100</property>
+ <signal name="value-changed" handler="exposure_adj_value_changed_cb" object="GuiAppWindow" swapped="no"/>
+ </object>
+ <object class="GtkAdjustment" id="gain_adj">
+ <property name="upper">10</property>
+ <property name="value">1</property>
+ <property name="step_increment">0.25</property>
+ <property name="page_increment">1</property>
+ <signal name="value-changed" handler="gain_adj_value_changed_cb" object="GuiAppWindow" swapped="no"/>
+ </object>
+ <template class="GuiAppWindow" parent="GtkApplicationWindow">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Camicro</property>
+ <signal name="destroy" handler="destroy_cb" swapped="no"/>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkDrawingArea" id="video">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <signal name="draw" handler="video_draw_cb" swapped="no"/>
+ <signal name="size-allocate" handler="video_size_allocate_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkButton" id="start_stop_button">
+ <property name="label">Start</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="clicked" handler="start_stop_button_clicked_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="resolution_combo_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <signal name="changed" handler="resolution_combo_box_changed_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Exposure (ms)</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="input_purpose">digits</property>
+ <property name="adjustment">exposure_adj</property>
+ <property name="update_policy">if-valid</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Gain</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="input_purpose">digits</property>
+ <property name="adjustment">gain_adj</property>
+ <property name="digits">2</property>
+ <property name="update_policy">if-valid</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="rotate_button">
+ <property name="label" translatable="yes">Rotate</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="rotate_button_toggled_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>