root/sys/dev/firewire/fwcam.c
/*
 * Copyright (c) 2026 Abdelkader Boudih <freebsd@seuros.com>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

/*
 * fwcam(4) - IIDC 1394-based Digital Camera driver
 *
 * Implements the IIDC v1.30 specification (TA Document 1999023) for
 * FireWire digital cameras.
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/module.h>
#include <sys/bus.h>
#include <sys/kernel.h>
#include <sys/conf.h>
#include <sys/malloc.h>
#include <sys/lock.h>
#include <sys/mutex.h>
#include <sys/sysctl.h>
#include <sys/taskqueue.h>
#include <sys/fcntl.h>
#include <sys/poll.h>
#include <sys/selinfo.h>
#include <sys/uio.h>
#include <sys/mbuf.h>

#include <dev/firewire/firewire.h>
#include <dev/firewire/firewirereg.h>
#include <dev/firewire/iec13213.h>
#include <dev/firewire/fwcam.h>
#include <dev/firewire/fw_helpers.h>

static MALLOC_DEFINE(M_FWCAM, "fwcam", "IIDC FireWire Camera");

static int debug = 0;
static int iso_channel = 0;
SYSCTL_DECL(_hw_firewire);
static SYSCTL_NODE(_hw_firewire, OID_AUTO, fwcam, CTLFLAG_RD | CTLFLAG_MPSAFE,
    0, "IIDC Camera");
SYSCTL_INT(_hw_firewire_fwcam, OID_AUTO, debug, CTLFLAG_RWTUN, &debug, 0,
    "fwcam debug level");
SYSCTL_INT(_hw_firewire_fwcam, OID_AUTO, iso_channel, CTLFLAG_RWTUN,
    &iso_channel, 0, "ISO channel for isochronous receive (default 0)");

#define FWCAM_DEBUG(lev, fmt, ...)                                      \
        do {                                                            \
                if (debug >= (lev))                                     \
                        printf("fwcam: " fmt, ## __VA_ARGS__);          \
        } while (0)

static int      fwcam_probe(device_t);
static int      fwcam_attach(device_t);
static int      fwcam_detach(device_t);
static void     fwcam_probe_task(void *, int);
static int      fwcam_iso_start(struct fwcam_softc *);
static void     fwcam_iso_stop(struct fwcam_softc *);
static void     fwcam_iso_input(struct fw_xferq *);

static d_open_t         fwcam_cdev_open;
static d_close_t        fwcam_cdev_close;
static d_read_t         fwcam_cdev_read;
static d_poll_t         fwcam_cdev_poll;
static d_ioctl_t        fwcam_cdev_ioctl;

static struct cdevsw fwcam_cdevsw = {
        .d_version =    D_VERSION,
        .d_flags =      D_TRACKCLOSE,
        .d_open =       fwcam_cdev_open,
        .d_close =      fwcam_cdev_close,
        .d_read =       fwcam_cdev_read,
        .d_poll =       fwcam_cdev_poll,
        .d_ioctl =      fwcam_cdev_ioctl,
        .d_name =       "fwcam",
};

/*
 * Search a CSR directory for the IIDC command base register (key 0x40).
 * The iSight places cmd_base inside a logical_unit_directory nested
 * within the IIDC unit directory, so we recurse into sub-directories
 * up to max_depth levels.
 */
static uint32_t
fwcam_search_dir_cmd_base(uint32_t *csrrom, uint32_t dir_qoff,
    uint32_t rom_quads, int max_depth)
{
        struct csrdirectory *dir;
        struct csrreg *reg;
        uint32_t dir_len, sub_qoff, val;
        int i;

        if (dir_qoff >= rom_quads || max_depth <= 0)
                return (0);
        dir = (struct csrdirectory *)&csrrom[dir_qoff];
        dir_len = dir->crc_len;
        if (dir_qoff + 1 + dir_len > rom_quads)
                return (0);
        if (!crom_crc_valid((uint32_t *)&dir->entry[0], dir_len, dir->crc)) {
                if (firewire_debug)
                        printf("fwcam: bad CRC in directory at 0x%x\n",
                            dir_qoff);
        }

        for (i = 0; i < (int)dir_len; i++) {
                if (dir_qoff + 1 + i >= rom_quads)
                        break;
                reg = &dir->entry[i];
                if (reg->key == IIDC_CROM_CMD_BASE && reg->val != 0)
                        return (reg->val);
                if ((reg->key & CSRTYPE_MASK) == CSRTYPE_D) {
                        sub_qoff = dir_qoff + 1 + i + reg->val;
                        val = fwcam_search_dir_cmd_base(csrrom, sub_qoff,
                            rom_quads, max_depth - 1);
                        if (val != 0)
                                return (val);
                }
        }
        return (0);
}

/*
 * Extract IIDC command base register from the unit directory.
 * Searches the unit directory and any sub-directories (the iSight
 * places cmd_base inside a logical_unit_directory).
 */
static uint32_t
fwcam_find_iidc_cmd_base(struct fw_unit *unit)
{

        return (fwcam_search_dir_cmd_base(unit->fwdev->csrrom,
            unit->dir_offset / 4, CSRROMSIZE / 4, 2));
}

static int
fwcam_read_quadlet(struct fwcam_softc *sc, uint32_t offset, uint32_t *val)
{
        uint16_t dst;
        uint8_t spd;
        int err;

        FWCAM_LOCK(sc);
        if (sc->fwdev == NULL) {
                FWCAM_UNLOCK(sc);
                return (ENXIO);
        }
        dst = FWLOCALBUS | sc->fwdev->dst;
        spd = min(sc->fwdev->speed, FWSPD_S400);
        FWCAM_UNLOCK(sc);

        err = fw_read_quadlet(sc->fd.fc, M_FWCAM, dst, spd,
            sc->cmd_hi, sc->cmd_lo + offset, val);
        if (err)
                FWCAM_DEBUG(1, "read_quadlet: offset=0x%x err=%d\n",
                    offset, err);
        return (err);
}

static int
fwcam_write_quadlet(struct fwcam_softc *sc, uint32_t offset, uint32_t val)
{
        uint16_t dst;
        uint8_t spd;
        int err;

        FWCAM_LOCK(sc);
        if (sc->fwdev == NULL) {
                FWCAM_UNLOCK(sc);
                return (ENXIO);
        }
        dst = FWLOCALBUS | sc->fwdev->dst;
        spd = min(sc->fwdev->speed, FWSPD_S400);
        FWCAM_UNLOCK(sc);

        err = fw_write_quadlet(sc->fd.fc, M_FWCAM, dst, spd,
            sc->cmd_hi, sc->cmd_lo + offset, val);
        if (err)
                FWCAM_DEBUG(1, "write_quadlet: offset=0x%x err=%d\n",
                    offset, err);
        return (err);
}

static int
fwcam_read_capabilities(struct fwcam_softc *sc)
{
        int err, f, m;

        err = fwcam_read_quadlet(sc, IIDC_V_FORMAT_INQ, &sc->formats);
        if (err) {
                device_printf(sc->fd.dev,
                    "failed to read V_FORMAT_INQ: %d\n", err);
                return (err);
        }

        err = fwcam_read_quadlet(sc, IIDC_BASIC_FUNC_INQ, &sc->basic_func);
        if (err) {
                device_printf(sc->fd.dev,
                    "failed to read BASIC_FUNC_INQ: %d\n", err);
                return (err);
        }

        /* Read mode and rate inquiry registers for each supported format */
        for (f = 0; f < 8; f++) {
                if (!(sc->formats & (1 << (31 - f)))) {
                        sc->modes[f] = 0;
                        continue;
                }
                err = fwcam_read_quadlet(sc, IIDC_V_MODE_INQ(f),
                    &sc->modes[f]);
                if (err) {
                        sc->modes[f] = 0;
                        continue;
                }
                for (m = 0; m < 8; m++) {
                        if (!(sc->modes[f] & (1 << (31 - m)))) {
                                sc->rates[f][m] = 0;
                                continue;
                        }
                        err = fwcam_read_quadlet(sc, IIDC_V_RATE_INQ(f, m),
                            &sc->rates[f][m]);
                        if (err)
                                sc->rates[f][m] = 0;
                }
        }

        err = fwcam_read_quadlet(sc, IIDC_FEATURE_HI_INQ, &sc->features_hi);
        if (err)
                sc->features_hi = 0;

        err = fwcam_read_quadlet(sc, IIDC_FEATURE_LO_INQ, &sc->features_lo);
        if (err)
                sc->features_lo = 0;

        return (0);
}

static int
fwcam_power_on(struct fwcam_softc *sc)
{
        uint32_t val;
        int err, retries;

        err = fwcam_read_quadlet(sc, IIDC_BASIC_FUNC_INQ, &val);
        if (err) {
                device_printf(sc->fd.dev,
                    "cannot read BASIC_FUNC_INQ: %d\n", err);
                return (err);
        }

        if ((val & IIDC_CAM_POWER_CTRL) == 0) {
                FWCAM_DEBUG(1, "no power control, assuming powered\n");
                return (0);
        }

        err = fwcam_read_quadlet(sc, IIDC_CAMERA_POWER, &val);
        if (err == 0 && (val & IIDC_POWER_ON)) {
                FWCAM_DEBUG(1, "camera already powered on\n");
                return (0);
        }

        err = fwcam_write_quadlet(sc, IIDC_CAMERA_POWER, IIDC_POWER_ON);
        if (err) {
                device_printf(sc->fd.dev,
                    "failed to write CAMERA_POWER: %d\n", err);
                return (err);
        }

        for (retries = 0; retries < 50; retries++) {
                pause("fwcampw", hz / 10);

                if (sc->state == FWCAM_STATE_DETACHING)
                        return (ENXIO);

                err = fwcam_read_quadlet(sc, IIDC_CAMERA_POWER, &val);
                if (err)
                        continue;       /* read may fail while powering up */

                if (val & IIDC_POWER_ON)
                        return (0);
        }

        device_printf(sc->fd.dev, "camera power-on timeout (5s)\n");
        return (ETIMEDOUT);
}

static void
fwcam_probe_task(void *arg, int pending __unused)
{
        struct fwcam_softc *sc = (struct fwcam_softc *)arg;
        int err;

        if (sc->state == FWCAM_STATE_DETACHING || sc->fwdev == NULL)
                return;

        err = fwcam_power_on(sc);
        if (err) {
                device_printf(sc->fd.dev,
                    "power-on failed (%d), trying to read anyway\n", err);
        }

        if (sc->state == FWCAM_STATE_DETACHING)
                return;

        if (fwcam_read_capabilities(sc) == 0) {
                uint32_t val;

                if (fwcam_read_quadlet(sc, IIDC_CUR_V_FORMAT, &val) == 0)
                        sc->cur_format = (val >> 29) & 0x7;
                if (fwcam_read_quadlet(sc, IIDC_CUR_V_MODE, &val) == 0)
                        sc->cur_mode = (val >> 29) & 0x7;
                if (fwcam_read_quadlet(sc, IIDC_CUR_V_FRM_RATE, &val) == 0)
                        sc->cur_framerate = (val >> 29) & 0x7;

                FWCAM_LOCK(sc);
                if (sc->state == FWCAM_STATE_DETACHING) {
                        FWCAM_UNLOCK(sc);
                        return;
                }
                sc->state = FWCAM_STATE_PROBED;
                wakeup(sc);
                FWCAM_UNLOCK(sc);

                if (sc->dma_ch >= 0 &&
                    sc->state != FWCAM_STATE_DETACHING)
                        fwcam_iso_start(sc);
        } else {
                FWCAM_LOCK(sc);
                if (sc->state == FWCAM_STATE_PROBING)
                        sc->state = FWCAM_STATE_IDLE;
                wakeup(sc);
                FWCAM_UNLOCK(sc);
        }
}

/*
 * Compute expected frame size for current format/mode.
 * Format_0 (VGA) modes:
 *   Mode 0: 160x120 YUV444  = 160*120*3 = 57600
 *   Mode 1: 320x240 YUV422  = 320*240*2 = 153600
 *   Mode 2: 640x480 YUV411  = 640*480*3/2 = 460800
 *   Mode 3: 640x480 YUV422  = 640*480*2 = 614400
 *   Mode 4: 640x480 RGB8    = 640*480*3 = 921600
 *   Mode 5: 640x480 Mono8   = 640*480 = 307200
 */
static uint32_t
fwcam_frame_size(struct fwcam_softc *sc)
{
        static const uint32_t fmt0_sizes[] = {
                160 * 120 * 3,          /* mode 0: YUV444 */
                320 * 240 * 2,          /* mode 1: YUV422 */
                640 * 480 * 3 / 2,      /* mode 2: YUV411 */
                640 * 480 * 2,          /* mode 3: YUV422 */
                640 * 480 * 3,          /* mode 4: RGB8 */
                640 * 480,              /* mode 5: Mono8 */
        };

        if (sc->cur_format == IIDC_FMT_VGA && sc->cur_mode < nitems(fmt0_sizes))
                return (fmt0_sizes[sc->cur_mode]);

        /* Default to largest VGA mode */
        return (FWCAM_MAX_FRAME_SIZE);
}

static int
fwcam_iso_start(struct fwcam_softc *sc)
{
        struct firewire_comm *fc = sc->fd.fc;
        struct fw_xferq *xferq;
        uint32_t val;
        int dma_ch, err;

        mtx_assert(&sc->mtx, MA_NOTOWNED);

        FWCAM_LOCK(sc);
        if (sc->dma_ch >= 0 || sc->state == FWCAM_STATE_STREAMING) {
                FWCAM_UNLOCK(sc);
                return (0);     /* already running or starting */
        }
        if (sc->state == FWCAM_STATE_DETACHING) {
                FWCAM_UNLOCK(sc);
                return (ENXIO);
        }
        FWCAM_UNLOCK(sc);

        dma_ch = fw_open_isodma(fc, 0);
        if (dma_ch < 0) {
                device_printf(sc->fd.dev, "no IR DMA channel available\n");
                return (EBUSY);
        }

        FWCAM_LOCK(sc);
        if (sc->dma_ch >= 0) {
                FWCAM_UNLOCK(sc);
                fc->ir[dma_ch]->flag &= ~FWXFERQ_OPEN;
                return (0);
        }
        FWCAM_UNLOCK(sc);

        xferq = fc->ir[dma_ch];
        xferq->flag |= FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_STREAM;

        sc->iso_channel = (uint8_t)(iso_channel & FWXFERQ_CHTAGMASK);
        xferq->flag &= ~FWXFERQ_CHTAGMASK;
        xferq->flag |= sc->iso_channel & FWXFERQ_CHTAGMASK;

        xferq->sc = (caddr_t)sc;
        xferq->hand = fwcam_iso_input;
        xferq->bnchunk = FWCAM_ISO_NCHUNK;
        xferq->bnpacket = 1;
        xferq->psize = FWCAM_ISO_PKTSIZE;
        xferq->queued = 0;
        xferq->buf = NULL;

        xferq->bulkxfer = malloc(sizeof(struct fw_bulkxfer) * xferq->bnchunk,
            M_FWCAM, M_WAITOK | M_ZERO);

        fw_iso_init_chunks(xferq);

        sc->frame_size = fwcam_frame_size(sc);
        sc->frame_buf = malloc(sc->frame_size, M_FWCAM, M_WAITOK | M_ZERO);
        sc->read_buf = malloc(sc->frame_size, M_FWCAM, M_WAITOK | M_ZERO);
        sc->frame_offset = 0;
        sc->frame_ready = 0;
        sc->frame_dropped = 0;

        /* IIDC spec s3.1: set video mode registers before ISO enable */
        err = fwcam_write_quadlet(sc, IIDC_CUR_V_FORMAT,
            (uint32_t)sc->cur_format << IIDC_CUR_V_SHIFT);
        if (err) {
                device_printf(sc->fd.dev,
                    "failed to set CUR_V_FORMAT: %d\n", err);
                goto fail;
        }
        err = fwcam_write_quadlet(sc, IIDC_CUR_V_MODE,
            (uint32_t)sc->cur_mode << IIDC_CUR_V_SHIFT);
        if (err) {
                device_printf(sc->fd.dev,
                    "failed to set CUR_V_MODE: %d\n", err);
                goto fail;
        }
        err = fwcam_write_quadlet(sc, IIDC_CUR_V_FRM_RATE,
            (uint32_t)sc->cur_framerate << IIDC_CUR_V_SHIFT);
        if (err) {
                device_printf(sc->fd.dev,
                    "failed to set CUR_V_FRM_RATE: %d\n", err);
                goto fail;
        }

        /* Check Vmode_Error_Status - camera rejects ISO_EN on error */
        err = fwcam_read_quadlet(sc, IIDC_VMODE_ERR_STATUS, &val);
        if (err == 0 && (val & IIDC_VMODE_ERROR)) {
                device_printf(sc->fd.dev,
                    "Vmode_Error_Status set: format=%d mode=%d rate=%d "
                    "speed=%d\n", sc->cur_format, sc->cur_mode,
                    sc->cur_framerate, sc->iso_speed);
                err = EINVAL;
                goto fail;
        }

        val = ((uint32_t)sc->iso_channel << IIDC_ISO_CH_SHIFT) |
            ((uint32_t)sc->iso_speed << IIDC_ISO_SPEED_SHIFT);
        err = fwcam_write_quadlet(sc, IIDC_ISO_CHANNEL, val);
        if (err) {
                device_printf(sc->fd.dev,
                    "failed to set ISO_CHANNEL: %d\n", err);
                goto fail;
        }

        err = fwcam_write_quadlet(sc, IIDC_ISO_EN, IIDC_ISO_EN_ON);
        if (err == EIO) {
                /*
                 * Cameras with a lens cover might power down the sensor
                 * when the cover is closed and reject streaming requests.
                 * Try to re-power and retry once before giving up.
                 */
                err = fwcam_power_on(sc);
                if (err == 0)
                        err = fwcam_write_quadlet(sc, IIDC_ISO_EN,
                            IIDC_ISO_EN_ON);
                if (err) {
                        device_printf(sc->fd.dev,
                            "ISO enable refused (lens cover closed?)\n");
                        goto fail;
                }
        } else if (err) {
                device_printf(sc->fd.dev,
                    "failed to enable ISO: %d\n", err);
                goto fail;
        }

        err = fc->irx_enable(fc, dma_ch);
        if (err) {
                device_printf(sc->fd.dev,
                    "failed to start IR DMA: %d\n", err);
                fwcam_write_quadlet(sc, IIDC_ISO_EN, 0);
                goto fail;
        }

        FWCAM_LOCK(sc);
        if (sc->state == FWCAM_STATE_DETACHING) {
                FWCAM_UNLOCK(sc);
                fc->irx_disable(fc, dma_ch);
                fwcam_write_quadlet(sc, IIDC_ISO_EN, 0);
                err = ENXIO;
                goto fail;
        }
        sc->dma_ch = dma_ch;
        sc->state = FWCAM_STATE_STREAMING;
        FWCAM_UNLOCK(sc);

        return (0);

fail:
        fw_iso_free_chunks(xferq, M_FWCAM);
        xferq->flag &= ~(FWXFERQ_MODEMASK | FWXFERQ_OPEN | FWXFERQ_STREAM |
            FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_CHTAGMASK);
        xferq->hand = NULL;

        free(sc->frame_buf, M_FWCAM);
        free(sc->read_buf, M_FWCAM);
        sc->frame_buf = NULL;
        sc->read_buf = NULL;
        sc->dma_ch = -1;

        return (err);
}

static void
fwcam_iso_stop(struct fwcam_softc *sc)
{
        struct firewire_comm *fc = sc->fd.fc;
        struct fw_xferq *xferq;
        int dma_ch;

        FWCAM_LOCK(sc);
        dma_ch = sc->dma_ch;
        if (dma_ch < 0) {
                FWCAM_UNLOCK(sc);
                return;
        }
        sc->dma_ch = -1;        /* claim ownership of teardown */
        FWCAM_UNLOCK(sc);

        xferq = fc->ir[dma_ch];

        if (xferq->flag & FWXFERQ_RUNNING)
                fc->irx_disable(fc, dma_ch);

        if (sc->fwdev != NULL)
                fwcam_write_quadlet(sc, IIDC_ISO_EN, 0);

        FWCAM_LOCK(sc);
        fw_iso_wait_inactive_locked(&sc->mtx, &sc->iso_active, "fwcamis");
        sc->frame_ready = 0;
        while (sc->read_in_progress)
                msleep(&sc->read_in_progress, &sc->mtx, PWAIT, "fwcamst", hz);
        FWCAM_UNLOCK(sc);

        xferq->flag &= ~(FWXFERQ_MODEMASK | FWXFERQ_OPEN | FWXFERQ_STREAM |
            FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_CHTAGMASK);
        xferq->hand = NULL;

        fw_iso_free_chunks(xferq, M_FWCAM);

        free(sc->frame_buf, M_FWCAM);
        free(sc->read_buf, M_FWCAM);
        sc->frame_buf = NULL;
        sc->read_buf = NULL;
}

static void
fwcam_iso_input(struct fw_xferq *xferq)
{
        struct fwcam_softc *sc = (struct fwcam_softc *)xferq->sc;
        struct fw_bulkxfer *sxfer;
        struct fw_pkt *fp;
        struct mbuf *m;
        uint8_t *payload;
        uint32_t plen;
        uint8_t *tmp;
        int dma_ch;

        FWCAM_LOCK(sc);
        dma_ch = sc->dma_ch;
        if (dma_ch < 0 || sc->frame_buf == NULL) {
                FWCAM_UNLOCK(sc);
                return;
        }
        sc->iso_active = 1;
        FWCAM_UNLOCK(sc);

        while ((sxfer = STAILQ_FIRST(&xferq->stvalid)) != NULL) {
                STAILQ_REMOVE_HEAD(&xferq->stvalid, link);

                m = fw_iso_dequeue(xferq, sxfer, sc->fd.fc);
                if (m == NULL)
                        continue;

                fp = mtod(m, struct fw_pkt *);
                plen = fp->mode.stream.len;
                if (plen == 0) {
                        m_freem(m);
                        continue;       /* empty packet (padding) */
                }

                if (fp->mode.stream.sy == 1) {
                        if (sc->frame_offset > 0) {
                                if (sc->frame_offset == sc->frame_size) {
                                        FWCAM_LOCK(sc);
                                        if (sc->read_in_progress) {
                                                sc->frame_dropped++;
                                        } else {
                                                if (sc->frame_ready)
                                                        sc->frame_dropped++;
                                                tmp = sc->read_buf;
                                                sc->read_buf = sc->frame_buf;
                                                sc->frame_buf = tmp;
                                                sc->frame_ready = 1;
                                                wakeup(sc);
                                                selwakeup(&sc->rsel);
                                        }
                                        FWCAM_UNLOCK(sc);
                                } else {
                                        sc->frame_dropped++;
                                }
                        }
                        sc->frame_offset = 0;
                }

                if (m->m_len < 4) {
                        m_freem(m);
                        continue;
                }
                payload = mtod(m, uint8_t *) + 4;
                if (plen > (uint32_t)(m->m_len - 4))
                        plen = m->m_len - 4;

                if (sc->frame_offset + plen <= sc->frame_size) {
                        memcpy(sc->frame_buf + sc->frame_offset, payload, plen);
                        sc->frame_offset += plen;
                } else {
                        sc->frame_dropped++;
                        sc->frame_offset = 0;
                }

                m_freem(m);
        }

        fw_iso_rearm_done(xferq, sc->fd.fc, &sc->mtx, &sc->iso_active,
            &sc->dma_ch, dma_ch);
}

static int
fwcam_cdev_open(struct cdev *dev, int oflags, int devtype, struct thread *td)
{
        struct fwcam_softc *sc = dev->si_drv1;
        int err;

        FWCAM_LOCK(sc);
        if (sc->state == FWCAM_STATE_DETACHING) {
                FWCAM_UNLOCK(sc);
                return (ENXIO);
        }

        if (sc->state == FWCAM_STATE_IDLE) {
                sc->state = FWCAM_STATE_PROBING;
                FWCAM_UNLOCK(sc);
                taskqueue_enqueue(taskqueue_thread, &sc->probe_task);
                FWCAM_LOCK(sc);
        }

        while (sc->state == FWCAM_STATE_PROBING) {
                err = msleep(sc, &sc->mtx, PCATCH, "fwcampr", 10 * hz);
                if (err) {
                        FWCAM_UNLOCK(sc);
                        return (err == EWOULDBLOCK ? ETIMEDOUT : err);
                }
        }

        if (sc->state != FWCAM_STATE_PROBED &&
            sc->state != FWCAM_STATE_STREAMING) {
                FWCAM_UNLOCK(sc);
                return (ENXIO);
        }

        sc->open_count++;
        FWCAM_UNLOCK(sc);
        return (0);
}

static int
fwcam_cdev_close(struct cdev *dev, int fflag, int devtype, struct thread *td)
{
        struct fwcam_softc *sc = dev->si_drv1;

        FWCAM_LOCK(sc);
        sc->open_count--;
        if (sc->open_count <= 0) {
                sc->open_count = 0;
                if (sc->state == FWCAM_STATE_STREAMING) {
                        FWCAM_UNLOCK(sc);
                        fwcam_iso_stop(sc);
                        FWCAM_LOCK(sc);
                        if (sc->state != FWCAM_STATE_DETACHING)
                                sc->state = FWCAM_STATE_PROBED;
                }
        }
        FWCAM_UNLOCK(sc);
        return (0);
}

static int
fwcam_cdev_read(struct cdev *dev, struct uio *uio, int ioflag)
{
        struct fwcam_softc *sc = dev->si_drv1;
        int err;

        FWCAM_LOCK(sc);
        if (sc->state == FWCAM_STATE_PROBED) {
                FWCAM_UNLOCK(sc);
                err = fwcam_iso_start(sc);
                if (err)
                        return (err);
                FWCAM_LOCK(sc);
        }
        while (!sc->frame_ready) {
                if (sc->state != FWCAM_STATE_STREAMING) {
                        FWCAM_UNLOCK(sc);
                        return (ENXIO);
                }
                if (ioflag & FNONBLOCK) {
                        FWCAM_UNLOCK(sc);
                        return (EAGAIN);
                }
                err = msleep(sc, &sc->mtx, PCATCH, "fwcamrd", 5 * hz);
                if (err) {
                        FWCAM_UNLOCK(sc);
                        return (err);
                }
        }

        sc->frame_ready = 0;
        if (sc->read_buf == NULL) {
                FWCAM_UNLOCK(sc);
                return (ENXIO);
        }
        sc->read_in_progress = 1;
        FWCAM_UNLOCK(sc);

        err = uiomove(sc->read_buf,
            MIN(uio->uio_resid, sc->frame_size), uio);

        FWCAM_LOCK(sc);
        sc->read_in_progress = 0;
        wakeup(&sc->read_in_progress);
        FWCAM_UNLOCK(sc);

        return (err);
}

static int
fwcam_cdev_poll(struct cdev *dev, int events, struct thread *td)
{
        struct fwcam_softc *sc = dev->si_drv1;
        int revents = 0;

        FWCAM_LOCK(sc);
        if (events & (POLLIN | POLLRDNORM)) {
                if (sc->frame_ready)
                        revents |= events & (POLLIN | POLLRDNORM);
                else
                        selrecord(td, &sc->rsel);
        }
        FWCAM_UNLOCK(sc);
        return (revents);
}

static const uint32_t fwcam_feat_inq[] = {
        [FWCAM_FEAT_BRIGHTNESS]   = IIDC_BRIGHTNESS_INQ,
        [FWCAM_FEAT_AUTO_EXPOSURE] = IIDC_AUTO_EXPOSURE_INQ,
        [FWCAM_FEAT_SHARPNESS]    = IIDC_SHARPNESS_INQ,
        [FWCAM_FEAT_WHITE_BALANCE] = IIDC_WHITE_BAL_INQ,
        [FWCAM_FEAT_HUE]         = IIDC_HUE_INQ,
        [FWCAM_FEAT_SATURATION]  = IIDC_SATURATION_INQ,
        [FWCAM_FEAT_GAMMA]       = IIDC_GAMMA_INQ,
        [FWCAM_FEAT_SHUTTER]     = IIDC_SHUTTER_INQ,
        [FWCAM_FEAT_GAIN]        = IIDC_GAIN_INQ,
        [FWCAM_FEAT_IRIS]        = IIDC_IRIS_INQ,
        [FWCAM_FEAT_FOCUS]       = IIDC_FOCUS_INQ,
        [FWCAM_FEAT_TEMPERATURE] = IIDC_TEMPERATURE_INQ,
        [FWCAM_FEAT_TRIGGER]     = IIDC_TRIGGER_INQ,
};

static const uint32_t fwcam_feat_ctrl[] = {
        [FWCAM_FEAT_BRIGHTNESS]   = IIDC_BRIGHTNESS,
        [FWCAM_FEAT_AUTO_EXPOSURE] = IIDC_AUTO_EXPOSURE,
        [FWCAM_FEAT_SHARPNESS]    = IIDC_SHARPNESS,
        [FWCAM_FEAT_WHITE_BALANCE] = IIDC_WHITE_BALANCE,
        [FWCAM_FEAT_HUE]         = IIDC_HUE,
        [FWCAM_FEAT_SATURATION]  = IIDC_SATURATION,
        [FWCAM_FEAT_GAMMA]       = IIDC_GAMMA,
        [FWCAM_FEAT_SHUTTER]     = IIDC_SHUTTER,
        [FWCAM_FEAT_GAIN]        = IIDC_GAIN,
        [FWCAM_FEAT_IRIS]        = IIDC_IRIS,
        [FWCAM_FEAT_FOCUS]       = IIDC_FOCUS,
        [FWCAM_FEAT_TEMPERATURE] = IIDC_TEMPERATURE,
        [FWCAM_FEAT_TRIGGER]     = IIDC_TRIGGER_MODE,
        [FWCAM_FEAT_ZOOM]        = IIDC_ZOOM,
        [FWCAM_FEAT_PAN]         = IIDC_PAN,
        [FWCAM_FEAT_TILT]        = IIDC_TILT,
};

static int
fwcam_get_feature(struct fwcam_softc *sc, struct fwcam_feature *feat)
{
        uint32_t inq, ctrl;
        int err;

        if (feat->id >= FWCAM_FEAT_MAX)
                return (EINVAL);

        feat->flags = 0;
        feat->min = 0;
        feat->max = 0;
        if (feat->id < nitems(fwcam_feat_inq) &&
            fwcam_feat_inq[feat->id] != 0) {
                err = fwcam_read_quadlet(sc, fwcam_feat_inq[feat->id], &inq);
                if (err)
                        return (err);

                if (inq & (1 << 31))
                        feat->flags |= FWCAM_FEATF_PRESENT;
                if (inq & (1 << 28))
                        feat->flags |= FWCAM_FEATF_ONOFF;
                if (inq & (1 << 27))
                        feat->flags |= FWCAM_FEATF_AUTO;
                if (inq & (1 << 26))
                        feat->flags |= FWCAM_FEATF_MANUAL;
                feat->min = (inq >> 12) & 0xfff;
                feat->max = inq & 0xfff;
        }

        if (feat->id >= nitems(fwcam_feat_ctrl) ||
            fwcam_feat_ctrl[feat->id] == 0)
                return (EINVAL);

        err = fwcam_read_quadlet(sc, fwcam_feat_ctrl[feat->id], &ctrl);
        if (err)
                return (err);

        feat->value = ctrl & 0xfff;
        /* White balance has U/B in bits [20:31] and V/R in bits [8:19] */
        if (feat->id == FWCAM_FEAT_WHITE_BALANCE)
                feat->value2 = (ctrl >> 12) & 0xfff;
        else
                feat->value2 = 0;

        return (0);
}

static int
fwcam_set_feature(struct fwcam_softc *sc, struct fwcam_feature *feat)
{
        uint32_t ctrl, val;
        int err;

        if (feat->id >= FWCAM_FEAT_MAX)
                return (EINVAL);
        if (feat->id >= nitems(fwcam_feat_ctrl) ||
            fwcam_feat_ctrl[feat->id] == 0)
                return (EINVAL);

        err = fwcam_read_quadlet(sc, fwcam_feat_ctrl[feat->id], &ctrl);
        if (err)
                return (err);

        if (feat->id == FWCAM_FEAT_WHITE_BALANCE) {
                /* White balance: value=V/R (low 12), value2=U/B (high 12) */
                val = (ctrl & 0xff000000) |
                    ((feat->value2 & 0xfff) << 12) |
                    (feat->value & 0xfff);
        } else {
                /* Preserve upper bits, set new value in lower 12 */
                val = (ctrl & 0xfffff000) | (feat->value & 0xfff);
        }

        return (fwcam_write_quadlet(sc, fwcam_feat_ctrl[feat->id], val));
}

static int
fwcam_cdev_ioctl(struct cdev *dev, u_long cmd, caddr_t data,
    int fflag, struct thread *td)
{
        struct fwcam_softc *sc = dev->si_drv1;
        struct fwcam_mode *mode;
        struct fwcam_feature *feat;
        struct fwcam_info *info;
        int err;

        if (sc->fwdev == NULL)
                return (ENXIO);

        switch (cmd) {
        case FWCAM_GMODE:
                mode = (struct fwcam_mode *)data;
                mode->format = sc->cur_format;
                mode->mode = sc->cur_mode;
                mode->framerate = sc->cur_framerate;
                mode->frame_size = sc->frame_size ?
                    sc->frame_size : fwcam_frame_size(sc);
                return (0);

        case FWCAM_SMODE:
            {
                int was_streaming = 0;

                mode = (struct fwcam_mode *)data;
                if (mode->format > 7 || mode->mode > 7 || mode->framerate > 7)
                        return (EINVAL);

                if (!(sc->formats & (1 << (31 - mode->format))))
                        return (EINVAL);
                if (!(sc->modes[mode->format] & (1 << (31 - mode->mode))))
                        return (EINVAL);
                if (!(sc->rates[mode->format][mode->mode] &
                    (1 << (31 - mode->framerate))))
                        return (EINVAL);

                FWCAM_LOCK(sc);
                if (sc->state == FWCAM_STATE_DETACHING) {
                        FWCAM_UNLOCK(sc);
                        return (ENXIO);
                }
                if (sc->state == FWCAM_STATE_STREAMING) {
                        was_streaming = 1;
                        FWCAM_UNLOCK(sc);
                        fwcam_iso_stop(sc);
                        FWCAM_LOCK(sc);
                        if (sc->state != FWCAM_STATE_DETACHING)
                                sc->state = FWCAM_STATE_PROBED;
                }
                FWCAM_UNLOCK(sc);

                err = fwcam_write_quadlet(sc, IIDC_CUR_V_FORMAT,
                    (uint32_t)mode->format << IIDC_CUR_V_SHIFT);
                if (err == 0)
                        err = fwcam_write_quadlet(sc, IIDC_CUR_V_MODE,
                            (uint32_t)mode->mode << IIDC_CUR_V_SHIFT);
                if (err == 0)
                        err = fwcam_write_quadlet(sc, IIDC_CUR_V_FRM_RATE,
                            (uint32_t)mode->framerate << IIDC_CUR_V_SHIFT);

                if (err == 0) {
                        sc->cur_format = mode->format;
                        sc->cur_mode = mode->mode;
                        sc->cur_framerate = mode->framerate;
                        mode->frame_size = fwcam_frame_size(sc);
                }

                if (was_streaming)
                        fwcam_iso_start(sc);
                return (err);
            }

        case FWCAM_GFEAT:
                feat = (struct fwcam_feature *)data;
                return (fwcam_get_feature(sc, feat));

        case FWCAM_SFEAT:
                feat = (struct fwcam_feature *)data;
                return (fwcam_set_feature(sc, feat));

        case FWCAM_GINFO:
                info = (struct fwcam_info *)data;
                info->formats = sc->formats;
                info->basic_func = sc->basic_func;
                info->features_hi = sc->features_hi;
                info->features_lo = sc->features_lo;
                info->cur_format = sc->cur_format;
                info->cur_mode = sc->cur_mode;
                info->cur_framerate = sc->cur_framerate;
                info->state = sc->state;
                info->frame_size = sc->frame_size ?
                    sc->frame_size : fwcam_frame_size(sc);
                info->frame_dropped = sc->frame_dropped;
                info->iso_channel = sc->iso_channel;
                info->_pad[0] = info->_pad[1] = info->_pad[2] = 0;
                return (0);

        default:
                return (ENOTTY);
        }
}


static int
fwcam_probe(device_t dev)
{
        struct fw_unit *unit;

        unit = fw_get_unit(dev);
        if (unit == NULL)
                return (ENXIO);

        if (unit->spec_id != CSRVAL_1394TA)
                return (ENXIO);
        if (unit->sw_version != CSR_PROTCAM130 &&
            unit->sw_version != CSR_PROTCAM120 &&
            unit->sw_version != CSR_PROTCAM104)
                return (ENXIO);

        device_set_desc(dev, "IIDC Digital Camera over FireWire");
        return (BUS_PROBE_DEFAULT);
}

static int
fwcam_attach(device_t dev)
{
        struct fwcam_softc *sc;
        struct fw_unit *unit;
        uint32_t cmd_base;

        unit = fw_get_unit(dev);
        if (unit == NULL || unit->fwdev == NULL)
                return (ENXIO);

        cmd_base = fwcam_find_iidc_cmd_base(unit);
        if (cmd_base == 0) {
                device_printf(dev, "no IIDC command base in unit directory\n");
                return (ENXIO);
        }

        sc = device_get_softc(dev);
        sc->fd.dev = dev;
        sc->fd.fc = fw_get_comm(dev);
        mtx_init(&sc->mtx, "fwcam", NULL, MTX_DEF);

        sc->fwdev = unit->fwdev;
        sc->cmd_hi = 0xffff;
        sc->cmd_lo = 0xf0000000 | (cmd_base << 2);
        sc->iso_speed = min(unit->fwdev->speed, FWSPD_S400);
        sc->state = FWCAM_STATE_IDLE;
        sc->dma_ch = -1;
        sc->iso_active = 0;
        sc->open_count = 0;
        knlist_init_mtx(&sc->rsel.si_note, &sc->mtx);
        TASK_INIT(&sc->probe_task, 0, fwcam_probe_task, sc);

        sc->cdev = make_dev(&fwcam_cdevsw, device_get_unit(dev),
            UID_ROOT, GID_OPERATOR, 0660, "fwcam%d", device_get_unit(dev));
        sc->cdev->si_drv1 = sc;

        return (0);
}

static int
fwcam_detach(device_t dev)
{
        struct fwcam_softc *sc;

        sc = device_get_softc(dev);

        FWCAM_LOCK(sc);
        sc->state = FWCAM_STATE_DETACHING;
        wakeup(sc);     /* wake any sleeping readers */
        FWCAM_UNLOCK(sc);

        taskqueue_drain(taskqueue_thread, &sc->probe_task);
        fwcam_iso_stop(sc);

        if (sc->cdev != NULL)
                destroy_dev(sc->cdev);

        seldrain(&sc->rsel);
        knlist_destroy(&sc->rsel.si_note);

        FWCAM_LOCK(sc);
        sc->fwdev = NULL;
        FWCAM_UNLOCK(sc);

        mtx_destroy(&sc->mtx);

        return (0);
}

static device_method_t fwcam_methods[] = {
        DEVMETHOD(device_probe,         fwcam_probe),
        DEVMETHOD(device_attach,        fwcam_attach),
        DEVMETHOD(device_detach,        fwcam_detach),

        DEVMETHOD_END
};

static driver_t fwcam_driver = {
        "fwcam",
        fwcam_methods,
        sizeof(struct fwcam_softc),
};

DRIVER_MODULE(fwcam, firewire, fwcam_driver, 0, 0);
MODULE_VERSION(fwcam, 1);
MODULE_DEPEND(fwcam, firewire, 1, 1, 1);