git: 94f24c0ee239 - main - fwcamctl: add control utility for fwcam(4) IIDC FireWire cameras

From: Adrian Chadd <adrian_at_FreeBSD.org>
Date: Wed, 08 Jul 2026 05:42:07 UTC
The branch main has been updated by adrian:

URL: https://cgit.FreeBSD.org/src/commit/?id=94f24c0ee23964f751fabc908a7564a388c9d1a1

commit 94f24c0ee23964f751fabc908a7564a388c9d1a1
Author:     Abdelkader Boudih <freebsd@seuros.com>
AuthorDate: 2026-07-08 05:31:46 +0000
Commit:     Adrian Chadd <adrian@FreeBSD.org>
CommitDate: 2026-07-08 05:32:44 +0000

    fwcamctl: add control utility for fwcam(4) IIDC FireWire cameras
    
    fwcamctl provides userland access to /dev/fwcam0.  Supported
    subcommands: info (camera state, format, mode, rate, features),
    snap (capture a frame as PPM), mode (set format/mode/rate), and
    feat (get/set camera feature registers).
    
    snap converts YUV422, YUV411, YUV444, RGB8, and Mono8 pixel
    formats to RGB24 PPM with no external dependencies.  A configurable
    frame skip (default 5) allows auto-exposure and auto-white-balance
    to settle before capture.
    
    (from adrian - yes, I've successfully captured images from an
    Apple isight camera on firewire with this tool and in-tree support.)
    
    Reviewed by:    adrian
    Differential Revision:  https://reviews.freebsd.org/D57914
---
 sys/dev/firewire/fwcam.c     |  21 --
 sys/dev/firewire/fwcam.h     |  64 ++++++
 usr.sbin/Makefile            |   1 +
 usr.sbin/fwcamctl/Makefile   |   9 +
 usr.sbin/fwcamctl/fwcamctl.8 | 156 +++++++++++++++
 usr.sbin/fwcamctl/fwcamctl.c | 450 +++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 680 insertions(+), 21 deletions(-)

diff --git a/sys/dev/firewire/fwcam.c b/sys/dev/firewire/fwcam.c
index 226295154e6e..27451316d613 100644
--- a/sys/dev/firewire/fwcam.c
+++ b/sys/dev/firewire/fwcam.c
@@ -149,27 +149,6 @@ fwcam_write_quadlet(struct fwcam_softc *sc, uint32_t offset, uint32_t val)
 	return (err);
 }
 
-#if 0
-static const char *
-fwcam_format_name(int format)
-{
-	static const char *names[] = {
-		"VGA (Format_0)",
-		"Super VGA 1 (Format_1)",
-		"Super VGA 2 (Format_2)",
-		"Reserved",
-		"Reserved",
-		"Reserved",
-		"Still Image (Format_6)",
-		"Partial Image (Format_7)",
-	};
-
-	if (format >= 0 && format <= 7)
-		return (names[format]);
-	return ("Unknown");
-}
-#endif
-
 static int
 fwcam_read_capabilities(struct fwcam_softc *sc)
 {
diff --git a/sys/dev/firewire/fwcam.h b/sys/dev/firewire/fwcam.h
index 841683bbc37d..79b0dd3b5ec6 100644
--- a/sys/dev/firewire/fwcam.h
+++ b/sys/dev/firewire/fwcam.h
@@ -207,6 +207,70 @@ struct fwcam_info {
 #define FWCAM_STATE_STREAMING	2
 #define FWCAM_STATE_DETACHING	3
 
+/* IIDC format/mode/rate/feature/state name tables for userland and driver */
+#define FWCAM_FMT_NMAX		8
+#define FWCAM_RATE_NMAX		8
+
+static const char * const fwcam_fmt_names[FWCAM_FMT_NMAX] = {
+	"VGA (Format_0)",
+	"Super VGA 1 (Format_1)",
+	"Super VGA 2 (Format_2)",
+	"Reserved",
+	"Reserved",
+	"Reserved",
+	"Still Image (Format_6)",
+	"Partial Image (Format_7)",
+};
+
+static const char * const fwcam_rate_names[FWCAM_RATE_NMAX] = {
+	"1.875 fps", "3.75 fps", "7.5 fps", "15 fps",
+	"30 fps",    "60 fps",   "120 fps", "240 fps",
+};
+
+static const char * const fwcam_feat_names[FWCAM_FEAT_MAX] = {
+	[FWCAM_FEAT_BRIGHTNESS]    = "brightness",
+	[FWCAM_FEAT_AUTO_EXPOSURE] = "auto_exposure",
+	[FWCAM_FEAT_SHARPNESS]     = "sharpness",
+	[FWCAM_FEAT_WHITE_BALANCE] = "white_balance",
+	[FWCAM_FEAT_HUE]           = "hue",
+	[FWCAM_FEAT_SATURATION]    = "saturation",
+	[FWCAM_FEAT_GAMMA]         = "gamma",
+	[FWCAM_FEAT_SHUTTER]       = "shutter",
+	[FWCAM_FEAT_GAIN]          = "gain",
+	[FWCAM_FEAT_IRIS]          = "iris",
+	[FWCAM_FEAT_FOCUS]         = "focus",
+	[FWCAM_FEAT_TEMPERATURE]   = "temperature",
+	[FWCAM_FEAT_TRIGGER]       = "trigger",
+	[FWCAM_FEAT_ZOOM]          = "zoom",
+	[FWCAM_FEAT_PAN]           = "pan",
+	[FWCAM_FEAT_TILT]          = "tilt",
+};
+
+static const char * const fwcam_state_names[] = {
+	[FWCAM_STATE_IDLE]      = "idle",
+	[FWCAM_STATE_PROBED]    = "probed",
+	[FWCAM_STATE_STREAMING] = "streaming",
+	[FWCAM_STATE_DETACHING] = "detaching",
+};
+
+/* Format_0 (VGA non-compressed) mode geometry */
+struct fwcam_fmt0_mode {
+	int		w, h;
+	const char	*pixfmt;
+};
+
+#define FWCAM_FMT0_NMODES	7
+
+static const struct fwcam_fmt0_mode fwcam_fmt0_modes[FWCAM_FMT0_NMODES] = {
+	{ 160, 120, "YUV444" },		/* mode 0 */
+	{ 320, 240, "YUV422" },		/* mode 1 */
+	{ 640, 480, "YUV411" },		/* mode 2 */
+	{ 640, 480, "YUV422" },		/* mode 3 */
+	{ 640, 480, "RGB8"   },		/* mode 4 */
+	{ 640, 480, "Mono8"  },		/* mode 5 */
+	{ 640, 480, "Mono16" },		/* mode 6 */
+};
+
 /*
  * Internal constants
  */
diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile
index 9e885d5d45c5..c55592c926a4 100644
--- a/usr.sbin/Makefile
+++ b/usr.sbin/Makefile
@@ -27,6 +27,7 @@ SUBDIR=	adduser \
 	extattrctl \
 	fifolog \
 	fstyp \
+	fwcamctl \
 	fwcontrol \
 	fwget \
 	getfmac \
diff --git a/usr.sbin/fwcamctl/Makefile b/usr.sbin/fwcamctl/Makefile
new file mode 100644
index 000000000000..127639c7e91f
--- /dev/null
+++ b/usr.sbin/fwcamctl/Makefile
@@ -0,0 +1,9 @@
+PROG=	fwcamctl
+SRCS=	fwcamctl.c
+MAN=	fwcamctl.8
+WARNS?=	3
+
+SDIR=	${SRCTOP}/sys
+CFLAGS+=	-I${SDIR}
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/fwcamctl/fwcamctl.8 b/usr.sbin/fwcamctl/fwcamctl.8
new file mode 100644
index 000000000000..389117bcc9d0
--- /dev/null
+++ b/usr.sbin/fwcamctl/fwcamctl.8
@@ -0,0 +1,156 @@
+.\" Copyright (c) 2026 Abdelkader Boudih <freebsd@seuros.com>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd May 27, 2026
+.Dt FWCAMCTL 8
+.Os
+.Sh NAME
+.Nm fwcamctl
+.Nd control utility for IIDC FireWire cameras
+.Sh SYNOPSIS
+.Nm
+.Op Fl f Ar device
+.Cm info
+.Nm
+.Op Fl f Ar device
+.Cm snap
+.Op Fl o Ar output.ppm
+.Op Fl s Ar skip
+.Nm
+.Op Fl f Ar device
+.Cm mode
+.Ar format mode rate
+.Nm
+.Op Fl f Ar device
+.Cm feat get
+.Ar id
+.Nm
+.Op Fl f Ar device
+.Cm feat set
+.Ar id value Op Ar value2
+.Sh DESCRIPTION
+The
+.Nm
+utility controls IIDC 1394-based digital cameras via the
+.Xr fwcam 4
+driver.
+.Pp
+The following options are available:
+.Bl -tag -width indent
+.It Fl f Ar device
+Specify the camera device.
+Defaults to
+.Pa /dev/fwcam0 .
+.El
+.Pp
+The subcommands are:
+.Bl -tag -width indent
+.It Cm info
+Display camera state, current format/mode/framerate, supported formats,
+basic function capabilities, and present features.
+.It Cm snap Op Fl o Ar output.ppm Op Fl s Ar skip
+Capture a single video frame and write it as a PPM image.
+.Pp
+.Bl -tag -width indent -compact
+.It Fl o Ar output.ppm
+Output file path.
+Use
+.Sq -
+for stdout.
+Defaults to
+.Pa snap.ppm .
+.It Fl s Ar skip
+Number of frames to skip before capturing, to allow auto-exposure
+and auto-white-balance to settle.
+Defaults to 5.
+.El
+.Pp
+Supported pixel formats: YUV444, YUV422, YUV411, RGB8, Mono8.
+The frame is converted to RGB24 and written as a binary PPM (P6).
+.It Cm mode Ar format mode rate
+Set the camera video format, mode, and frame rate.
+All arguments are integer indices per the IIDC specification.
+Only Format_0 (VGA) is currently supported by the
+.Xr fwcam 4
+frame assembly path.
+.Pp
+Format_0 modes:
+.Bl -column "Mode" "Resolution" "Pixel format" -compact
+.It Em Mode Ta Em Resolution Ta Em Pixel format
+.It 0 Ta 160x120 Ta YUV444
+.It 1 Ta 320x240 Ta YUV422
+.It 2 Ta 640x480 Ta YUV411
+.It 3 Ta 640x480 Ta YUV422
+.It 4 Ta 640x480 Ta RGB8
+.It 5 Ta 640x480 Ta Mono8
+.El
+.Pp
+Frame rates: 0=1.875 1=3.75 2=7.5 3=15 4=30 5=60 fps.
+.It Cm feat get Ar id
+Query the camera feature with the given numeric ID and display its
+inquiry (presence, on/off, auto, manual, range) and current value.
+.It Cm feat set Ar id value Op Ar value2
+Set the camera feature with the given numeric ID to
+.Ar value .
+For the
+.Sq white_balance
+feature (id 3),
+.Ar value
+is the U/B component and
+.Ar value2
+is the V/R component.
+.El
+.Pp
+Feature IDs:
+.Bl -column "ID" "Name" -compact
+.It Em ID Ta Em Name
+.It 0 Ta brightness
+.It 1 Ta auto_exposure
+.It 2 Ta sharpness
+.It 3 Ta white_balance
+.It 4 Ta hue
+.It 5 Ta saturation
+.It 6 Ta gamma
+.It 7 Ta shutter
+.It 8 Ta gain
+.It 9 Ta iris
+.It 10 Ta focus
+.It 11 Ta temperature
+.It 12 Ta trigger
+.It 13 Ta zoom
+.It 14 Ta pan
+.It 15 Ta tilt
+.El
+.Sh EXAMPLES
+Show camera information:
+.Bd -literal -offset indent
+fwcamctl info
+.Ed
+.Pp
+Capture a photo to
+.Pa photo.ppm ,
+skipping 10 frames for AE to settle:
+.Bd -literal -offset indent
+fwcamctl snap -o photo.ppm -s 10
+.Ed
+.Pp
+Switch to 320x240 YUV422 at 30 fps:
+.Bd -literal -offset indent
+fwcamctl mode 0 1 4
+.Ed
+.Pp
+Read the current gain value:
+.Bd -literal -offset indent
+fwcamctl feat get 8
+.Ed
+.Pp
+Set brightness to 200:
+.Bd -literal -offset indent
+fwcamctl feat set 0 200
+.Ed
+.Sh SEE ALSO
+.Xr fwcam 4 ,
+.Xr fwcontrol 8
+.Sh AUTHORS
+.An Abdelkader Boudih Aq Mt freebsd@seuros.com
diff --git a/usr.sbin/fwcamctl/fwcamctl.c b/usr.sbin/fwcamctl/fwcamctl.c
new file mode 100644
index 000000000000..af484c39dee6
--- /dev/null
+++ b/usr.sbin/fwcamctl/fwcamctl.c
@@ -0,0 +1,450 @@
+/*
+ * Copyright (c) 2026 Abdelkader Boudih <freebsd@seuros.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * fwcamctl - control utility for fwcam(4) IIDC FireWire cameras
+ */
+
+#include <sys/param.h>
+#include <sys/types.h>
+#include <sys/ioctl.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <dev/firewire/fwcam.h>
+
+static const char *default_dev = "/dev/fwcam0";
+
+static void
+usage(void)
+{
+	fprintf(stderr,
+	    "usage: fwcamctl [-f device] info\n"
+	    "       fwcamctl [-f device] snap [-o output.ppm] [-s skip]\n"
+	    "       fwcamctl [-f device] mode <format> <mode> <rate>\n"
+	    "       fwcamctl [-f device] feat get <id>\n"
+	    "       fwcamctl [-f device] feat set <id> <value> [value2]\n"
+	    "\n"
+	    "  Feature IDs: 0=brightness 1=auto_exposure 2=sharpness\n"
+	    "               3=white_balance 4=hue 5=saturation 6=gamma\n"
+	    "               7=shutter 8=gain 9=iris 10=focus 11=temperature\n"
+	    "               12=trigger 13=zoom 14=pan 15=tilt\n");
+	exit(1);
+}
+
+static inline int
+clamp_u8(int v)
+{
+	if (v < 0)   return (0);
+	if (v > 255) return (255);
+	return (v);
+}
+
+/*
+ * IIDC YUV 4:2:2 → RGB24
+ * IIDC spec layout: U0 Y0 V0 Y1  (UYVY, 2 pixels per 4 bytes)
+ */
+static void
+yuv422_to_rgb(const uint8_t *src, uint8_t *dst, int w, int h)
+{
+	int i, n = w * h / 2;
+
+	for (i = 0; i < n; i++) {
+		int cb = src[0] - 128;
+		int y0 = src[1];
+		int cr = src[2] - 128;
+		int y1 = src[3];
+		src += 4;
+
+		/* pixel 0 */
+		dst[0] = clamp_u8(y0 + (int)(1.402f * cr));
+		dst[1] = clamp_u8(y0 - (int)(0.34414f * cb) - (int)(0.71414f * cr));
+		dst[2] = clamp_u8(y0 + (int)(1.772f * cb));
+		/* pixel 1 */
+		dst[3] = clamp_u8(y1 + (int)(1.402f * cr));
+		dst[4] = clamp_u8(y1 - (int)(0.34414f * cb) - (int)(0.71414f * cr));
+		dst[5] = clamp_u8(y1 + (int)(1.772f * cb));
+		dst += 6;
+	}
+}
+
+/*
+ * IIDC YUV 4:1:1 → RGB24
+ * Layout: U0 Y0 Y1 V0 Y2 Y3  (4 pixels per 6 bytes)
+ */
+static void
+yuv411_to_rgb(const uint8_t *src, uint8_t *dst, int w, int h)
+{
+	int i, j, n = w * h / 4;
+
+	for (i = 0; i < n; i++) {
+		int cb = src[0] - 128;
+		int y[4];
+		y[0] = src[1];
+		y[1] = src[2];
+		int cr = src[3] - 128;
+		y[2] = src[4];
+		y[3] = src[5];
+		src += 6;
+
+		for (j = 0; j < 4; j++) {
+			*dst++ = clamp_u8(y[j] + (int)(1.402f * cr));
+			*dst++ = clamp_u8(y[j] - (int)(0.34414f * cb) - (int)(0.71414f * cr));
+			*dst++ = clamp_u8(y[j] + (int)(1.772f * cb));
+		}
+	}
+}
+
+/*
+ * IIDC YUV 4:4:4 → RGB24
+ * Layout: U Y V  (3 bytes/pixel)
+ */
+static void
+yuv444_to_rgb(const uint8_t *src, uint8_t *dst, int w, int h)
+{
+	int i, n = w * h;
+
+	for (i = 0; i < n; i++) {
+		int cb = src[0] - 128;
+		int y  = src[1];
+		int cr = src[2] - 128;
+		src += 3;
+
+		*dst++ = clamp_u8(y + (int)(1.402f * cr));
+		*dst++ = clamp_u8(y - (int)(0.34414f * cb) - (int)(0.71414f * cr));
+		*dst++ = clamp_u8(y + (int)(1.772f * cb));
+	}
+}
+
+static int
+write_ppm(const char *path, const uint8_t *rgb, int w, int h)
+{
+	FILE *f;
+
+	if (strcmp(path, "-") == 0) {
+		f = stdout;
+	} else {
+		f = fopen(path, "wb");
+		if (f == NULL) {
+			warn("fopen %s", path);
+			return (-1);
+		}
+	}
+
+	fprintf(f, "P6\n%d %d\n255\n", w, h);
+	if (fwrite(rgb, 1, (size_t)(w * h * 3), f) != (size_t)(w * h * 3)) {
+		warn("fwrite");
+		if (f != stdout)
+			fclose(f);
+		return (-1);
+	}
+
+	if (f != stdout)
+		fclose(f);
+	return (0);
+}
+
+static int
+do_snap(int fd, const char *outpath, int skip)
+{
+	struct fwcam_info info;
+	uint8_t *frame_buf, *rgb_buf;
+	uint32_t rgb_size;
+	ssize_t n;
+	int w, h, i;
+	const char *pixfmt;
+
+	if (ioctl(fd, FWCAM_GINFO, &info) < 0)
+		err(1, "FWCAM_GINFO");
+
+	if (info.frame_size == 0)
+		errx(1, "frame_size is 0 — camera not yet probed?");
+
+	if (info.cur_format != IIDC_FMT_VGA ||
+	    info.cur_mode >= nitems(fwcam_fmt0_modes))
+		errx(1, "unsupported format %d mode %d for snap "
+		    "(only Format_0 VGA supported)",
+		    info.cur_format, info.cur_mode);
+
+	w      = fwcam_fmt0_modes[info.cur_mode].w;
+	h      = fwcam_fmt0_modes[info.cur_mode].h;
+	pixfmt = fwcam_fmt0_modes[info.cur_mode].pixfmt;
+
+	if (strcmp(pixfmt, "Mono16") == 0)
+		errx(1, "Mono16 not supported for PPM output");
+
+	rgb_size  = (uint32_t)(w * h * 3);
+	frame_buf = malloc(info.frame_size);
+	rgb_buf   = malloc(rgb_size);
+	if (frame_buf == NULL || rgb_buf == NULL)
+		err(1, "malloc");
+
+	/* Read frames; skip the first N to let AE/AWB settle */
+	for (i = 0; i <= skip; i++) {
+		n = read(fd, frame_buf, info.frame_size);
+		if (n < 0)
+			err(1, "read");
+		if ((uint32_t)n < info.frame_size)
+			errx(1, "short read: got %zd of %u bytes",
+			    n, info.frame_size);
+		if (i < skip)
+			fprintf(stderr, "skipping frame %d/%d...\n", i + 1, skip);
+	}
+
+	/* Convert to RGB24 */
+	if (strcmp(pixfmt, "YUV422") == 0) {
+		yuv422_to_rgb(frame_buf, rgb_buf, w, h);
+	} else if (strcmp(pixfmt, "YUV411") == 0) {
+		yuv411_to_rgb(frame_buf, rgb_buf, w, h);
+	} else if (strcmp(pixfmt, "YUV444") == 0) {
+		yuv444_to_rgb(frame_buf, rgb_buf, w, h);
+	} else if (strcmp(pixfmt, "RGB8") == 0) {
+		memcpy(rgb_buf, frame_buf, rgb_size);
+	} else if (strcmp(pixfmt, "Mono8") == 0) {
+		for (i = 0; i < w * h; i++) {
+			rgb_buf[i * 3 + 0] = frame_buf[i];
+			rgb_buf[i * 3 + 1] = frame_buf[i];
+			rgb_buf[i * 3 + 2] = frame_buf[i];
+		}
+	} else {
+		errx(1, "no converter for pixel format %s", pixfmt);
+	}
+
+	if (write_ppm(outpath, rgb_buf, w, h) < 0) {
+		free(frame_buf);
+		free(rgb_buf);
+		return (1);
+	}
+
+	fprintf(stderr, "saved %s (%dx%d %s, %u bytes raw)\n",
+	    outpath, w, h, pixfmt, info.frame_size);
+
+	free(frame_buf);
+	free(rgb_buf);
+	return (0);
+}
+
+static void
+do_info(int fd)
+{
+	struct fwcam_info info;
+	int i;
+
+	if (ioctl(fd, FWCAM_GINFO, &info) < 0)
+		err(1, "FWCAM_GINFO");
+
+	printf("state:       %s\n",
+	    info.state < nitems(fwcam_state_names) ?
+	    fwcam_state_names[info.state] : "unknown");
+	printf("cur format:  %d (%s)\n", info.cur_format,
+	    info.cur_format < nitems(fwcam_fmt_names) ?
+	    fwcam_fmt_names[info.cur_format] : "?");
+
+	if (info.cur_format == IIDC_FMT_VGA &&
+	    info.cur_mode < nitems(fwcam_fmt0_modes)) {
+		printf("cur mode:    %d (%dx%d %s)\n", info.cur_mode,
+		    fwcam_fmt0_modes[info.cur_mode].w,
+		    fwcam_fmt0_modes[info.cur_mode].h,
+		    fwcam_fmt0_modes[info.cur_mode].pixfmt);
+	} else {
+		printf("cur mode:    %d\n", info.cur_mode);
+	}
+
+	printf("cur rate:    %d (%s)\n", info.cur_framerate,
+	    info.cur_framerate < nitems(fwcam_rate_names) ?
+	    fwcam_rate_names[info.cur_framerate] : "?");
+	printf("iso_channel: %u\n", info.iso_channel);
+	printf("frame_size:  %u bytes\n", info.frame_size);
+	printf("dropped:     %u frames\n", info.frame_dropped);
+
+	printf("formats:     0x%08x  (", info.formats);
+	for (i = 0; i < 8; i++) {
+		if (info.formats & (1 << (31 - i)))
+			printf("F%d ", i);
+	}
+	printf(")\n");
+
+	printf("basic_func:  0x%08x", info.basic_func);
+	if (info.basic_func & IIDC_CAM_POWER_CTRL) printf(" [power_ctrl]");
+	if (info.basic_func & IIDC_ONE_SHOT_INQ)   printf(" [one_shot]");
+	if (info.basic_func & IIDC_MULTI_SHOT_INQ) printf(" [multi_shot]");
+	printf("\n");
+
+	printf("features:    hi=0x%08x lo=0x%08x\n",
+	    info.features_hi, info.features_lo);
+	if (info.features_hi) {
+		printf("             ");
+		if (info.features_hi & IIDC_HAS_BRIGHTNESS)   printf("brightness ");
+		if (info.features_hi & IIDC_HAS_AUTO_EXPOSURE) printf("auto_exp ");
+		if (info.features_hi & IIDC_HAS_SHARPNESS)    printf("sharpness ");
+		if (info.features_hi & IIDC_HAS_WHITE_BALANCE) printf("white_bal ");
+		if (info.features_hi & IIDC_HAS_HUE)          printf("hue ");
+		if (info.features_hi & IIDC_HAS_SATURATION)   printf("saturation ");
+		if (info.features_hi & IIDC_HAS_GAMMA)        printf("gamma ");
+		if (info.features_hi & IIDC_HAS_SHUTTER)      printf("shutter ");
+		if (info.features_hi & IIDC_HAS_GAIN)         printf("gain ");
+		if (info.features_hi & IIDC_HAS_IRIS)         printf("iris ");
+		if (info.features_hi & IIDC_HAS_FOCUS)        printf("focus ");
+		if (info.features_hi & IIDC_HAS_TEMPERATURE)  printf("temperature ");
+		if (info.features_hi & IIDC_HAS_TRIGGER)      printf("trigger ");
+		printf("\n");
+	}
+}
+
+static void
+do_mode(int fd, int argc, char **argv)
+{
+	struct fwcam_mode mode;
+
+	if (argc < 3)
+		usage();
+
+	mode.format    = (uint8_t)atoi(argv[0]);
+	mode.mode      = (uint8_t)atoi(argv[1]);
+	mode.framerate = (uint8_t)atoi(argv[2]);
+	mode.frame_size = 0;
+	mode._pad = 0;
+
+	if (ioctl(fd, FWCAM_SMODE, &mode) < 0)
+		err(1, "FWCAM_SMODE");
+
+	printf("mode set: format=%d mode=%d framerate=%d frame_size=%u\n",
+	    mode.format, mode.mode, mode.framerate, mode.frame_size);
+}
+
+static void
+do_feat_get(int fd, int feat_id)
+{
+	struct fwcam_feature feat;
+
+	if (feat_id < 0 || feat_id >= FWCAM_FEAT_MAX)
+		errx(1, "invalid feature id %d (0-%d)", feat_id, FWCAM_FEAT_MAX - 1);
+
+	memset(&feat, 0, sizeof(feat));
+	feat.id = (uint32_t)feat_id;
+
+	if (ioctl(fd, FWCAM_GFEAT, &feat) < 0)
+		err(1, "FWCAM_GFEAT");
+
+	printf("feature %d (%s):\n", feat_id,
+	    fwcam_feat_names[feat_id] != NULL ? fwcam_feat_names[feat_id] : "?");
+	printf("  present: %s\n",
+	    (feat.flags & FWCAM_FEATF_PRESENT) ? "yes" : "no");
+	printf("  flags:   0x%x", feat.flags);
+	if (feat.flags & FWCAM_FEATF_ONOFF)  printf(" [on/off]");
+	if (feat.flags & FWCAM_FEATF_AUTO)   printf(" [auto]");
+	if (feat.flags & FWCAM_FEATF_MANUAL) printf(" [manual]");
+	printf("\n");
+	printf("  range:   %u - %u\n", feat.min, feat.max);
+
+	if (feat_id == FWCAM_FEAT_WHITE_BALANCE)
+		printf("  value:   U/B=%u V/R=%u\n", feat.value, feat.value2);
+	else
+		printf("  value:   %u\n", feat.value);
+}
+
+static void
+do_feat_set(int fd, int feat_id, uint32_t value, uint32_t value2)
+{
+	struct fwcam_feature feat;
+
+	if (feat_id < 0 || feat_id >= FWCAM_FEAT_MAX)
+		errx(1, "invalid feature id %d (0-%d)", feat_id, FWCAM_FEAT_MAX - 1);
+
+	memset(&feat, 0, sizeof(feat));
+	feat.id     = (uint32_t)feat_id;
+	feat.value  = value;
+	feat.value2 = value2;
+
+	if (ioctl(fd, FWCAM_SFEAT, &feat) < 0)
+		err(1, "FWCAM_SFEAT");
+
+	printf("feature %d (%s) set to %u\n", feat_id,
+	    fwcam_feat_names[feat_id] != NULL ? fwcam_feat_names[feat_id] : "?", value);
+}
+
+int
+main(int argc, char *argv[])
+{
+	const char *devpath = default_dev;
+	int fd, ch;
+
+	while ((ch = getopt(argc, argv, "f:")) != -1) {
+		switch (ch) {
+		case 'f':
+			devpath = optarg;
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (argc < 1)
+		usage();
+
+	fd = open(devpath, O_RDWR);
+	if (fd < 0)
+		err(1, "open %s", devpath);
+
+	if (strcmp(argv[0], "info") == 0) {
+		do_info(fd);
+
+	} else if (strcmp(argv[0], "snap") == 0) {
+		const char *out = "snap.ppm";
+		int skip = 5;
+
+		/* re-parse the snap subcommand's own flags */
+		optreset = 1;
+		optind = 1;
+		while ((ch = getopt(argc, argv, "o:s:")) != -1) {
+			switch (ch) {
+			case 'o':
+				out = optarg;
+				break;
+			case 's':
+				skip = atoi(optarg);
+				break;
+			default:
+				usage();
+			}
+		}
+		do_snap(fd, out, skip);
+
+	} else if (strcmp(argv[0], "mode") == 0) {
+		do_mode(fd, argc - 1, argv + 1);
+
+	} else if (strcmp(argv[0], "feat") == 0) {
+		if (argc < 3)
+			usage();
+
+		int feat_id = atoi(argv[2]);
+
+		if (strcmp(argv[1], "get") == 0) {
+			do_feat_get(fd, feat_id);
+		} else if (strcmp(argv[1], "set") == 0) {
+			if (argc < 4)
+				usage();
+			uint32_t val  = (uint32_t)atoi(argv[3]);
+			uint32_t val2 = (argc > 4) ? (uint32_t)atoi(argv[4]) : 0;
+			do_feat_set(fd, feat_id, val, val2);
+		} else {
+			usage();
+		}
+
+	} else {
+		warnx("unknown command: %s", argv[0]);
+		usage();
+	}
+
+	close(fd);
+	return (0);
+}