diff --git a/configure.ac b/configure.ac
index 2616f92..41ab97b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -24,6 +24,7 @@ AC_CONFIG_FILES([Makefile
 	utils/decode_tm6000/Makefile
 	utils/dvb/Makefile
 	utils/keytable/Makefile
+	utils/ir-ctl/Makefile
 	utils/cx18-ctl/Makefile
 	utils/ivtv-ctl/Makefile
 	utils/media-ctl/Makefile
@@ -59,6 +60,7 @@ AC_CONFIG_FILES([Makefile
 	utils/v4l2-compliance/v4l2-compliance.1
 	utils/v4l2-ctl/v4l2-ctl.1
 	utils/keytable/ir-keytable.1
+	utils/ir-ctl/ir-ctl.1
 	utils/dvb/dvb-fe-tool.1
 	utils/dvb/dvbv5-scan.1
 	utils/dvb/dvb-format-convert.1
diff --git a/include/media/lirc.h b/include/media/lirc.h
new file mode 100644
index 0000000..4b3ab29
--- /dev/null
+++ b/include/media/lirc.h
@@ -0,0 +1,168 @@
+/*
+ * lirc.h - linux infrared remote control header file
+ * last modified 2010/07/13 by Jarod Wilson
+ */
+
+#ifndef _LINUX_LIRC_H
+#define _LINUX_LIRC_H
+
+#include <linux/types.h>
+#include <linux/ioctl.h>
+
+#define PULSE_BIT       0x01000000
+#define PULSE_MASK      0x00FFFFFF
+
+#define LIRC_MODE2_SPACE     0x00000000
+#define LIRC_MODE2_PULSE     0x01000000
+#define LIRC_MODE2_FREQUENCY 0x02000000
+#define LIRC_MODE2_TIMEOUT   0x03000000
+
+#define LIRC_VALUE_MASK      0x00FFFFFF
+#define LIRC_MODE2_MASK      0xFF000000
+
+#define LIRC_SPACE(val) (((val)&LIRC_VALUE_MASK) | LIRC_MODE2_SPACE)
+#define LIRC_PULSE(val) (((val)&LIRC_VALUE_MASK) | LIRC_MODE2_PULSE)
+#define LIRC_FREQUENCY(val) (((val)&LIRC_VALUE_MASK) | LIRC_MODE2_FREQUENCY)
+#define LIRC_TIMEOUT(val) (((val)&LIRC_VALUE_MASK) | LIRC_MODE2_TIMEOUT)
+
+#define LIRC_VALUE(val) ((val)&LIRC_VALUE_MASK)
+#define LIRC_MODE2(val) ((val)&LIRC_MODE2_MASK)
+
+#define LIRC_IS_SPACE(val) (LIRC_MODE2(val) == LIRC_MODE2_SPACE)
+#define LIRC_IS_PULSE(val) (LIRC_MODE2(val) == LIRC_MODE2_PULSE)
+#define LIRC_IS_FREQUENCY(val) (LIRC_MODE2(val) == LIRC_MODE2_FREQUENCY)
+#define LIRC_IS_TIMEOUT(val) (LIRC_MODE2(val) == LIRC_MODE2_TIMEOUT)
+
+/* used heavily by lirc userspace */
+#define lirc_t int
+
+/*** lirc compatible hardware features ***/
+
+#define LIRC_MODE2SEND(x) (x)
+#define LIRC_SEND2MODE(x) (x)
+#define LIRC_MODE2REC(x) ((x) << 16)
+#define LIRC_REC2MODE(x) ((x) >> 16)
+
+#define LIRC_MODE_RAW                  0x00000001
+#define LIRC_MODE_PULSE                0x00000002
+#define LIRC_MODE_MODE2                0x00000004
+#define LIRC_MODE_LIRCCODE             0x00000010
+
+
+#define LIRC_CAN_SEND_RAW              LIRC_MODE2SEND(LIRC_MODE_RAW)
+#define LIRC_CAN_SEND_PULSE            LIRC_MODE2SEND(LIRC_MODE_PULSE)
+#define LIRC_CAN_SEND_MODE2            LIRC_MODE2SEND(LIRC_MODE_MODE2)
+#define LIRC_CAN_SEND_LIRCCODE         LIRC_MODE2SEND(LIRC_MODE_LIRCCODE)
+
+#define LIRC_CAN_SEND_MASK             0x0000003f
+
+#define LIRC_CAN_SET_SEND_CARRIER      0x00000100
+#define LIRC_CAN_SET_SEND_DUTY_CYCLE   0x00000200
+#define LIRC_CAN_SET_TRANSMITTER_MASK  0x00000400
+
+#define LIRC_CAN_REC_RAW               LIRC_MODE2REC(LIRC_MODE_RAW)
+#define LIRC_CAN_REC_PULSE             LIRC_MODE2REC(LIRC_MODE_PULSE)
+#define LIRC_CAN_REC_MODE2             LIRC_MODE2REC(LIRC_MODE_MODE2)
+#define LIRC_CAN_REC_LIRCCODE          LIRC_MODE2REC(LIRC_MODE_LIRCCODE)
+
+#define LIRC_CAN_REC_MASK              LIRC_MODE2REC(LIRC_CAN_SEND_MASK)
+
+#define LIRC_CAN_SET_REC_CARRIER       (LIRC_CAN_SET_SEND_CARRIER << 16)
+#define LIRC_CAN_SET_REC_DUTY_CYCLE    (LIRC_CAN_SET_SEND_DUTY_CYCLE << 16)
+
+#define LIRC_CAN_SET_REC_DUTY_CYCLE_RANGE 0x40000000
+#define LIRC_CAN_SET_REC_CARRIER_RANGE    0x80000000
+#define LIRC_CAN_GET_REC_RESOLUTION       0x20000000
+#define LIRC_CAN_SET_REC_TIMEOUT          0x10000000
+#define LIRC_CAN_SET_REC_FILTER           0x08000000
+
+#define LIRC_CAN_MEASURE_CARRIER          0x02000000
+#define LIRC_CAN_USE_WIDEBAND_RECEIVER    0x04000000
+
+#define LIRC_CAN_SEND(x) ((x)&LIRC_CAN_SEND_MASK)
+#define LIRC_CAN_REC(x) ((x)&LIRC_CAN_REC_MASK)
+
+#define LIRC_CAN_NOTIFY_DECODE            0x01000000
+
+/*** IOCTL commands for lirc driver ***/
+
+#define LIRC_GET_FEATURES              _IOR('i', 0x00000000, __u32)
+
+#define LIRC_GET_SEND_MODE             _IOR('i', 0x00000001, __u32)
+#define LIRC_GET_REC_MODE              _IOR('i', 0x00000002, __u32)
+#define LIRC_GET_SEND_CARRIER          _IOR('i', 0x00000003, __u32)
+#define LIRC_GET_REC_CARRIER           _IOR('i', 0x00000004, __u32)
+#define LIRC_GET_SEND_DUTY_CYCLE       _IOR('i', 0x00000005, __u32)
+#define LIRC_GET_REC_DUTY_CYCLE        _IOR('i', 0x00000006, __u32)
+#define LIRC_GET_REC_RESOLUTION        _IOR('i', 0x00000007, __u32)
+
+#define LIRC_GET_MIN_TIMEOUT           _IOR('i', 0x00000008, __u32)
+#define LIRC_GET_MAX_TIMEOUT           _IOR('i', 0x00000009, __u32)
+
+#define LIRC_GET_MIN_FILTER_PULSE      _IOR('i', 0x0000000a, __u32)
+#define LIRC_GET_MAX_FILTER_PULSE      _IOR('i', 0x0000000b, __u32)
+#define LIRC_GET_MIN_FILTER_SPACE      _IOR('i', 0x0000000c, __u32)
+#define LIRC_GET_MAX_FILTER_SPACE      _IOR('i', 0x0000000d, __u32)
+
+/* code length in bits, currently only for LIRC_MODE_LIRCCODE */
+#define LIRC_GET_LENGTH                _IOR('i', 0x0000000f, __u32)
+
+#define LIRC_SET_SEND_MODE             _IOW('i', 0x00000011, __u32)
+#define LIRC_SET_REC_MODE              _IOW('i', 0x00000012, __u32)
+/* Note: these can reset the according pulse_width */
+#define LIRC_SET_SEND_CARRIER          _IOW('i', 0x00000013, __u32)
+#define LIRC_SET_REC_CARRIER           _IOW('i', 0x00000014, __u32)
+#define LIRC_SET_SEND_DUTY_CYCLE       _IOW('i', 0x00000015, __u32)
+#define LIRC_SET_REC_DUTY_CYCLE        _IOW('i', 0x00000016, __u32)
+#define LIRC_SET_TRANSMITTER_MASK      _IOW('i', 0x00000017, __u32)
+
+/*
+ * when a timeout != 0 is set the driver will send a
+ * LIRC_MODE2_TIMEOUT data packet, otherwise LIRC_MODE2_TIMEOUT is
+ * never sent, timeout is disabled by default
+ */
+#define LIRC_SET_REC_TIMEOUT           _IOW('i', 0x00000018, __u32)
+
+/* 1 enables, 0 disables timeout reports in MODE2 */
+#define LIRC_SET_REC_TIMEOUT_REPORTS   _IOW('i', 0x00000019, __u32)
+
+/*
+ * pulses shorter than this are filtered out by hardware (software
+ * emulation in lirc_dev?)
+ */
+#define LIRC_SET_REC_FILTER_PULSE      _IOW('i', 0x0000001a, __u32)
+/*
+ * spaces shorter than this are filtered out by hardware (software
+ * emulation in lirc_dev?)
+ */
+#define LIRC_SET_REC_FILTER_SPACE      _IOW('i', 0x0000001b, __u32)
+/*
+ * if filter cannot be set independently for pulse/space, this should
+ * be used
+ */
+#define LIRC_SET_REC_FILTER            _IOW('i', 0x0000001c, __u32)
+
+/*
+ * if enabled from the next key press on the driver will send
+ * LIRC_MODE2_FREQUENCY packets
+ */
+#define LIRC_SET_MEASURE_CARRIER_MODE	_IOW('i', 0x0000001d, __u32)
+
+/*
+ * to set a range use
+ * LIRC_SET_REC_DUTY_CYCLE_RANGE/LIRC_SET_REC_CARRIER_RANGE with the
+ * lower bound first and later
+ * LIRC_SET_REC_DUTY_CYCLE/LIRC_SET_REC_CARRIER with the upper bound
+ */
+
+#define LIRC_SET_REC_DUTY_CYCLE_RANGE  _IOW('i', 0x0000001e, __u32)
+#define LIRC_SET_REC_CARRIER_RANGE     _IOW('i', 0x0000001f, __u32)
+
+#define LIRC_NOTIFY_DECODE             _IO('i', 0x00000020)
+
+#define LIRC_SETUP_START               _IO('i', 0x00000021)
+#define LIRC_SETUP_END                 _IO('i', 0x00000022)
+
+#define LIRC_SET_WIDEBAND_RECEIVER     _IOW('i', 0x00000023, __u32)
+
+#endif
diff --git a/utils/Makefile.am b/utils/Makefile.am
index 2cb56f0..a19617a 100644
--- a/utils/Makefile.am
+++ b/utils/Makefile.am
@@ -4,6 +4,7 @@ SUBDIRS = \
 	libmedia_dev \
 	decode_tm6000 \
 	ivtv-ctl \
+	ir-ctl \
 	cx18-ctl \
 	keytable \
 	media-ctl \
diff --git a/utils/ir-ctl/.gitignore b/utils/ir-ctl/.gitignore
new file mode 100644
index 0000000..3220d69
--- /dev/null
+++ b/utils/ir-ctl/.gitignore
@@ -0,0 +1,2 @@
+ir-ctl
+ir-ctl.1
diff --git a/utils/ir-ctl/Makefile.am b/utils/ir-ctl/Makefile.am
new file mode 100644
index 0000000..9a1bfed
--- /dev/null
+++ b/utils/ir-ctl/Makefile.am
@@ -0,0 +1,6 @@
+bin_PROGRAMS = ir-ctl
+man_MANS = ir-ctl.1
+
+ir_ctl_SOURCES = ir-ctl.c
+ir_ctl_LDADD = @LIBINTL@
+ir_ctl_LDFLAGS = $(ARGP_LIBS)
diff --git a/utils/ir-ctl/ir-ctl.1.in b/utils/ir-ctl/ir-ctl.1.in
new file mode 100644
index 0000000..4bdf47e
--- /dev/null
+++ b/utils/ir-ctl/ir-ctl.1.in
@@ -0,0 +1,192 @@
+.TH "IR\-CTL" "1" "Tue Jul 5 2016" "v4l-utils @PACKAGE_VERSION@" "User Commands"
+.SH NAME
+ir\-ctl \- a swiss\-knife tool to handle raw IR and to set lirc options
+.SH SYNOPSIS
+.B ir\-ctl
+[\fIOPTION\fR]...
+.br
+.B ir\-ctl
+[\fIOPTION\fR]... \fI\-\-features\fR
+.br
+.B ir\-ctl
+[\fIOPTION\fR]... \fI\-\-send\fR [\fIpulse and space file to send\fR]
+.br
+.B ir\-ctl
+[\fIOPTION\fR]... \fI\-\-record\fR [\fIsave to file\fR]
+.SH DESCRIPTION
+ir\-ctl is a tool that allows one to list the features of a lirc device,
+set its options, record raw IR and send raw IR.
+.PP
+Note: You need to have read or write permissions on the /dev/lirc device
+for options to work.
+.SH OPTIONS
+.TP
+\fB\-d\fR, \fB\-\-device\fR=\fIDEV\fR
+lirc device to control, /dev/lirc0 by default
+.TP
+\fB\-f\fR, \fB\-\-features\fR
+List the features of the lirc device.
+.TP
+\fB\-r\fR, \fB\-\-record\fR=[\fIFILE\fR]
+Record IR and print to standard output if no file is specified, else
+save to the filename.
+.TP
+\fB\-s\fR, \fB\-\-send\fR=\fIFILE\fR
+Send IR in text file. It must be in the format described below. If this
+option is specified multiple times, send all files in order with 125ms delay
+between them.
+.TP
+\fB\-1\fR, \fB\-\-oneshot\fR
+When recording, stop recording after the first message, i.e. after a space or
+timeout of more than 19ms is received.
+.TP
+\fB\-w\fR, \fB\-\-wideband\fR
+Use the wideband receiver if available on the hardware. This is also
+known as learning mode. The measurements should be more precise and any
+carrier frequency should be accepted.
+.TP
+\fB\-n\fR, \fB\-\-no-wideband\fR
+Switches back to the normal, narrowband receiver if the wideband receiver
+was enabled.
+.TP
+\fB\-R\fR, \fB\-\-carrier-range\fR=\fIRANGE\fR
+Set the accepted carrier range for the narrowband receiver. It should be
+specified in the form \fI30000-50000\fR.
+.TP
+\fB\-m\fR, \fB\-\-measure\-carrier\fR
+If the hardware supports it, report what the carrier frequency is on
+recording. You will get the keyword \fIcarrier\fR followed by the frequency.
+This might use the wideband receiver although this is hardware specific.
+.TP
+\fB\-M\fR, \fB\-\-no\-measure\-carrier\fR
+Disable reporting of the carrier frequency. This should make it possible
+to use the narrowband receiver. This is the default.
+.TP
+\fB\-p\fR, \fB\-\-timeout\-reports\fR
+When the IR receiver times out due to inactivity, a timeout message is
+reported. When recording you will get the keyword \fItimeout\fR followed by
+the length of time that no IR was detected for.
+.TP
+\fB\-P\fR, \fB\-\-no\-timeout\-reports\fR
+When the IR receiver times out due to inactivity, do not report this.
+This is the default.
+.TP
+\fB\-t\fR, \fB\-\-timeout\fR=\fITIMEOUT\fR
+Set the length of a space at which the recorder goes idle, specified in
+microseconds.
+.TP
+\fB\-c\fR, \fB\-\-carrier\fR=\fICARRIER\fR
+Sets the send carrier frequency.
+.TP
+\fB\-D\fR, \fB\-\-duty\-cycle\fR=\fIDUTY\fR
+Set the duty cycle for sending in percent if the hardware support it.
+.TP
+\fB\-e\fR, \fB\-\-emitters\fR=\fIEMITTERS\fR
+Comma separated list of emitters to use for sending. The first emitter is
+number 1. Some devices only support enabling one emitter (the winbond-cir
+driver).
+.TP
+\fB\-?\fR, \fB\-\-help\fR
+Prints the help message
+.TP
+\fB\-\-usage\fR
+Give a short usage message
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+print the v4l2\-utils version
+.PP
+.SS Format of pulse and space file
+When sending IR, the format of the file should be as follows. A comment
+start with #. The carrier frequency can be specified as:
+.PP
+	carrier 38000
+.PP
+The file should contain alternating lines with pulse and space, followed
+by length in microseconds. The following is a rc-5 encoded message:
+.PP
+	carrier 36000
+.br
+	pulse 920
+.br
+	space 110
+.br
+	pulse 270
+.br
+	space 380
+.br
+	pulse 1800
+.br
+	space 1560
+.br
+	pulse 1730
+.br
+	space 1630
+.br
+	pulse 1730
+.br
+	space 1640
+.br
+	pulse 850
+.br
+	space 830
+.br
+	pulse 1690
+.br
+	space 820
+.br
+	pulse 860
+.br
+	space 1660
+.br
+	pulse 1690
+.br
+	space 830
+.br
+	pulse 850
+.SS Wideband and narrowband receiver
+Most IR receivers have a narrowband and wideband receiver. The narrowband
+receiver can receive over longer distances (usually around 10 metres without
+interference) and is limited to certain carrier frequencies.
+.PP
+The wideband receiver is for higher precision measurements and when the
+carrier frequency is unknown; however it only works over very short
+distances (about 5 centimetres). This is also known as \fBlearning mode\fR.
+.PP
+For most drivers, enabling \fBcarrier reports\fR using \fB\-m\fR also enables
+the wideband receiver.
+.SS Global state
+All the options which can be set for lirc devices are maintained until
+the device is powered down or a new option is set.
+.SH EXIT STATUS
+On success, it returns 0. Otherwise, it will return the error code.
+.SH EXAMPLES
+To list all capabilities of /dev/lirc2:
+.br
+	\fBir\-ctl \-f \-d /dev/lirc2\fR
+.PP
+To show the IR of the first button press on a remote in learning mode:
+.br
+	\fBir\-ctl \-r \-m \-w\fR
+.PP
+Note that \fBir\-ctl \-rmw\fR would record to a file called \fBmw\fR.
+.PP
+To restore the normal (longer distance) receiver:
+.br
+	\fBir\-ctl \-n \-M\fR
+.PP
+To send the pulse and space file \fBplay\fR on emitter 3:
+.br
+	\fBir\-ctl \-e 3 \-\-send=play\fR
+.PP
+To restore the IR receiver on /dev/lirc2 to the default state:
+.br
+	\fBir\-ctl \-PMn \-\-timeout 125000 \-\-device=/dev/lirc2\fR
+.SH BUGS
+Report bugs to \fBLinux Media Mailing List <linux-media@vger.kernel.org>\fR
+.SH COPYRIGHT
+Copyright (c) 2016 by Sean Young.
+.PP
+License GPLv2: GNU GPL version 2 <http://gnu.org/licenses/gpl.html>.
+.br
+This is free software: you are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law.
diff --git a/utils/ir-ctl/ir-ctl.c b/utils/ir-ctl/ir-ctl.c
new file mode 100644
index 0000000..7f2afc0
--- /dev/null
+++ b/utils/ir-ctl/ir-ctl.c
@@ -0,0 +1,759 @@
+/*
+ *  ir-ctl.c - Program to send and record IR using lirc interface
+ *
+ *  Copyright (C) 2016 Sean Young <sean@mess.org>
+ *
+ *  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, version 2 of the License.
+ *
+ *  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.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#include <fcntl.h>
+#include <argp.h>
+#include <sysexits.h>
+
+#include <config.h>
+
+#include <media/lirc.h>
+
+#ifdef ENABLE_NLS
+# define _(string) gettext(string)
+# include "gettext.h"
+# include <locale.h>
+# include <langinfo.h>
+# include <iconv.h>
+#else
+# define _(string) string
+#endif
+
+# define N_(string) string
+
+
+/* See drivers/media/rc/ir-lirc-codec.c line 23 */
+#define LIRCBUF_SIZE	512
+#define IR_DEFAULT_TIMEOUT 125000
+
+const char *argp_program_version = "IR raw version " V4L_UTILS_VERSION;
+const char *argp_program_bug_address = "Sean Young <sean@mess.org>";
+
+/*
+ * Since this program drives the lirc interface, use the same terminology
+ */
+struct file {
+	struct file *next;
+	const char *fname;
+	unsigned carrier;
+	unsigned len;
+	unsigned buf[LIRCBUF_SIZE];
+};
+
+struct arguments {
+	char *device;
+	bool features;
+	bool record;
+	struct file *send;
+	bool oneshot;
+	char *savetofile;
+	int wideband;
+	unsigned carrier_low, carrier_high;
+	unsigned timeout;
+	int carrier_reports;
+	int timeout_reports;
+	unsigned carrier;
+	unsigned duty;
+	unsigned emitters;
+	bool work_to_do;
+};
+
+static const struct argp_option options[] = {
+	{ "device",	'd',	N_("DEV"),	0,	N_("lirc device to use") },
+	{ "features",	'f',	0,		0,	N_("list lirc device features") },
+	{ "record",	'r',	N_("FILE"),	OPTION_ARG_OPTIONAL,	N_("record IR to stdout or file") },
+	{ "send",	's',	N_("FILE"),	0,	N_("send IR pulse and space file") },
+		{ .doc = N_("Recording options:") },
+	{ "one-shot",	'1',	0,		0,	N_("end recording after first message") },
+	{ "wideband",	'w',	0,		0,	N_("use wideband receiver aka learning mode") },
+	{ "no-wideband",'n',	0,		0,	N_("use normal narrowband receiver, disable learning mode") },
+	{ "carrier-range", 'R', N_("RANGE"),	0,	N_("set receiver carrier range") },
+	{ "measure-carrier", 'm', 0,		0,	N_("report carrier frequency") },
+	{ "no-measure-carrier", 'M', 0,		0,	N_("disable reporting carrier frequency") },
+	{ "timeout-reports", 'p', 0,		0,	N_("report when a timeout occurs") },
+	{ "no-timeout-reports", 'P', 0,		0,	N_("disable reporting when a timeout occurs") },
+	{ "timeout",	't',	N_("TIMEOUT"),	0,	N_("set recording timeout") },
+		{ .doc = "Sending options:" },
+	{ "carrier",	'c',	N_("CARRIER"),	0,	N_("set send carrier") },
+	{ "duty-cycle",	'D',	N_("DUTY"),	0,	N_("set duty cycle") },
+	{ "emitters",	'e',	N_("EMITTERS"),	0,	N_("set send emitters") },
+	{ }
+};
+
+static const char args_doc[] = N_(
+	"--features\n"
+	"--record [save to file]\n"
+	"--send [file to send]\n"
+	"[to set lirc option]");
+
+static const char doc[] = N_(
+	"\nRecord IR, send IR and list features of lirc device\n"
+	"You will need permission on /dev/lirc for the program to work\n"
+	"\nOn the options below, the arguments are:\n"
+	"  DEV	    - the /dev/lirc* device to use\n"
+	"  FILE     - a text file containing pulses and spaces\n"
+	"  CARRIER  - the carrier frequency to use for sending\n"
+	"  DUTY     - the duty cycle to use for sending\n"
+	"  EMITTERS - comma separated list of emitters to use for sending, e.g. 1,2\n"
+	"  RANGE    - set range of accepted carrier frequencies, e.g. 20000-40000\n"
+	"  TIMEOUT  - set length of space before recording stops in µs (microseonds)\n"
+	"\nNote that most lirc setting have global state, i.e. the device will remain\n"
+	"in this state until set otherwise.");
+
+static int strtoint(const char *p, const char *unit)
+{
+	char *end;
+	long arg = strtol(p, &end, 10);
+	if (end == NULL || (end[0] != 0 && strcasecmp(end, unit) != 0))
+		return 0;
+
+	if (arg <= 0 || arg >= 0xffffff)
+		return 0;
+
+	return arg;
+}
+
+static unsigned parse_emitters(char *p)
+{
+	unsigned emit = 0;
+	const char *sep = " ,;:";
+	char *saveptr, *q;
+
+	q = strtok_r(p, sep, &saveptr);
+	while (q) {
+		if (*q) {
+			char *endptr;
+			long e = strtol(q, &endptr, 10);
+			if ((endptr && *endptr) || e <= 0 || e > 32)
+				return 0;
+
+			emit |= 1 << (e - 1);
+		}
+		q = strtok_r(NULL, sep, &saveptr);
+	}
+
+	return emit;
+}
+
+static struct file *read_file(const char *fname)
+{
+	bool expect_pulse = true;
+	int lineno = 0, lastspace = 0;
+	char line[1024];
+	int len = 0;
+	const char *whitespace = " \n\r\t";
+	struct file *f;
+
+	FILE *input = fopen(fname, "r");
+
+	if (!input) {
+		fprintf(stderr, _("%s: could not open: %m\n"), fname);
+		return NULL;
+	}
+
+	f = malloc(sizeof(*f));
+	if (f == NULL) {
+		fprintf(stderr, _("Failed to allocate memory\n"));
+		return NULL;
+	}
+	f->carrier = 0;
+	f->fname = fname;
+
+	while (fgets(line, sizeof(line), input)) {
+		char *p, *saveptr;
+		lineno++;
+		char *keyword = strtok_r(line, whitespace, &saveptr);
+
+		if (keyword == NULL || *keyword == 0 || *keyword == '#' ||
+				(keyword[0] == '/' && keyword[1] == '/'))
+			continue;
+
+		p = strtok_r(NULL, whitespace, &saveptr);
+		if (p == NULL) {
+			fprintf(stderr, _("warning: %s:%d: missing argument\n"), fname, lineno);
+			continue;
+		}
+
+		int arg = strtoint(p, "");
+		if (arg == 0) {
+			fprintf(stderr, _("warning: %s:%d: invalid argument '%s'\n"), fname, lineno, p);
+			continue;
+		}
+
+		p = strtok_r(NULL, whitespace, &saveptr);
+		if (p && p[0] != '#' && !(p[0] == '/' && p[1] == '/')) {
+			fprintf(stderr, _("warning: %s:%d: '%s' unexpected\n"), fname, lineno, p);
+			continue;
+		}
+
+		if (strcmp(keyword, "space") == 0) {
+			if (expect_pulse) {
+				if (len == 0) {
+					fprintf(stderr, _("warning: %s:%d: leading space ignored\n"),
+						fname, lineno);
+				} else {
+					f->buf[len] += arg;
+				}
+			} else {
+				f->buf[len++] = arg;
+			}
+			lastspace = lineno;
+			expect_pulse = true;
+		} else if (strcmp(keyword, "pulse") == 0) {
+			if (!expect_pulse)
+				f->buf[len] += arg;
+			else
+				f->buf[len++] = arg;
+			expect_pulse = false;
+		} else if (strcmp(keyword, "carrier") == 0) {
+			if (f->carrier) {
+				fprintf(stderr, _("warning: %s:%d: carrier already specified\n"), fname, lineno);
+			} else {
+				f->carrier = arg;
+			}
+		} else {
+			fprintf(stderr, _("warning: %s:%d: unknown keyword '%s' ignored\n"), fname, lineno, keyword);
+			continue;
+		}
+
+		if (len >= LIRCBUF_SIZE) {
+			fprintf(stderr, _("warning: %s:%d: IR cannot exceed %u edges\n"), fname, lineno, LIRCBUF_SIZE);
+			break;
+		}
+	}
+
+	fclose(input);
+
+	if (len == 0) {
+		fprintf(stderr, _("%s: no pulses or spaces found\n"), fname);
+		free(f);
+		return NULL;
+	}
+
+	if ((len % 2) == 0) {
+		fprintf(stderr, _("warning: %s:%d: trailing space ignored\n"),
+							fname, lastspace);
+		len--;
+	}
+
+	f->len = len;
+
+	return f;
+}
+
+
+static error_t parse_opt(int k, char *arg, struct argp_state *state)
+{
+	struct arguments *arguments = state->input;
+	struct file *s;
+
+	switch (k) {
+	case 'f':
+		if (arguments->record || arguments->send)
+			argp_error(state, _("features can not be combined with record or send option"));
+		arguments->features = true;
+		break;
+	// recording
+	case 'r':
+		if (arguments->features || arguments->send)
+			argp_error(state, _("record can not be combined with features or send option"));
+
+		arguments->record = true;
+		if (arg) {
+			if (arguments->savetofile)
+				argp_error(state, _("record filename already set"));
+
+			arguments->savetofile = arg;
+		}
+		break;
+	case '1':
+		arguments->oneshot = true;
+		break;
+	case 'm':
+		if (arguments->carrier_reports == 2)
+			argp_error(state, _("cannot enable and disable carrier reports"));
+
+		arguments->carrier_reports = 1;
+		break;
+	case 'M':
+		if (arguments->carrier_reports == 1)
+			argp_error(state, _("cannot enable and disable carrier reports"));
+
+		arguments->carrier_reports = 2;
+		break;
+	case 'p':
+		if (arguments->timeout_reports == 2)
+			argp_error(state, _("cannot enable and disable timeout reports"));
+
+		arguments->timeout_reports = 1;
+		break;
+	case 'P':
+		if (arguments->timeout_reports == 1)
+			argp_error(state, _("cannot enable and disable timeout reports"));
+
+		arguments->timeout_reports = 2;
+		break;
+	case 'n':
+		if (arguments->wideband)
+			argp_error(state, _("cannot use narrowband and wideband receiver at once"));
+
+		arguments->wideband = 2;
+		break;
+	case 'w':
+		if (arguments->wideband)
+			argp_error(state, _("cannot use narrowband and wideband receiver at once"));
+
+		arguments->wideband = 1;
+		break;
+	case 'R': {
+		long low, high;
+		char *end;
+
+		low = strtol(arg, &end, 10);
+		if (end == NULL || end[0] != '-')
+			argp_error(state, _("cannot parse carrier range `%s'"), arg);
+		high = strtol(end + 1, &end, 10);
+		if (end[0] != 0 || low <= 0 || low >= high || high > 1000000)
+			argp_error(state, _("cannot parse carrier range `%s'"), arg);
+
+		arguments->carrier_low = low;
+		arguments->carrier_high = high;
+		break;
+	}
+	case 't':
+		arguments->timeout = strtoint(arg, "µs");
+		if (arguments->timeout == 0)
+			argp_error(state, _("cannot parse timeout `%s'"), arg);
+		break;
+
+	// sending
+	case 'd':
+		arguments->device = arg;
+		break;
+	case 'c':
+		arguments->carrier = strtoint(arg, "Hz");
+		if (arguments->carrier == 0)
+			argp_error(state, _("cannot parse carrier `%s'"), arg);
+		break;
+	case 'e':
+		arguments->emitters = parse_emitters(arg);
+		if (arguments->emitters == 0)
+			argp_error(state, _("cannot parse emitters `%s'"), arg);
+		break;
+	case 'D':
+		arguments->duty = strtoint(arg, "%");
+		if (arguments->duty == 0 || arguments->duty >= 100)
+			argp_error(state, _("invalid duty cycle `%s'"), arg);
+		break;
+	case 's':
+		if (arguments->record || arguments->features)
+			argp_error(state, _("send can not be combined with record or features option"));
+		s = read_file(arg);
+		if (s == NULL)
+			exit(EX_DATAERR);
+
+		s->next = NULL;
+		if (arguments->send == NULL)
+			arguments->send = s;
+		else {
+			struct file *p = arguments->send;
+			while (p->next) p = p->next;
+			p->next = s;
+		}
+		break;
+	case ARGP_KEY_END:
+		if (!arguments->work_to_do)
+			argp_usage(state);
+
+		break;
+	default:
+		return ARGP_ERR_UNKNOWN;
+	}
+
+	if (k != '1' && k != 'd')
+		arguments->work_to_do = true;
+
+	return 0;
+}
+
+static const struct argp argp = {
+	.options = options,
+	.parser = parse_opt,
+	.args_doc = args_doc,
+	.doc = doc
+};
+
+static int open_lirc(const char *fname, unsigned *features)
+{
+	int fd;
+
+	fd = TEMP_FAILURE_RETRY(open(fname, O_RDWR | O_CLOEXEC));
+	if (fd == -1) {
+		fprintf(stderr, _("%s: cannot open: %m\n"), fname);
+		return -1;
+	}
+
+	struct stat st;
+	int rc = TEMP_FAILURE_RETRY(fstat(fd, &st));
+	if (rc) {
+		fprintf(stderr, _("%s: cannot stat: %m\n"), fname);
+		close(fd);
+		return -1;
+	}
+
+	if ((st.st_mode & S_IFMT) != S_IFCHR) {
+		fprintf(stderr, _("%s: not character device\n"), fname);
+		close(fd);
+		return -1;
+	}
+
+	rc = ioctl(fd, LIRC_GET_FEATURES, features);
+	if (rc) {
+		fprintf(stderr, _("%s: failed to get lirc features: %m\n"), fname);
+		close(fd);
+		return -1;
+	}
+
+	return fd;
+}
+
+static void lirc_set_send_carrier(int fd, const char *devname, unsigned features, unsigned carrier)
+{
+	if (features & LIRC_CAN_SET_SEND_CARRIER) {
+		int rc = ioctl(fd, LIRC_SET_SEND_CARRIER, &carrier);
+		if (rc < 0)
+			fprintf(stderr, _("warning: %s: failed to set carrier: %m\n"), devname);
+		if (rc != 0)
+			fprintf(stderr, _("warning: %s: set send carrier returned %d, should return 0\n"), devname, rc);
+	} else
+		fprintf(stderr, _("warning: %s: does not support setting send carrier\n"), devname);
+}
+
+static int lirc_options(struct arguments *args, int fd, unsigned features)
+{
+	const char *dev = args->device;
+	int rc;
+
+	if (args->timeout) {
+		if (features & LIRC_CAN_SET_REC_TIMEOUT) {
+			rc = ioctl(fd, LIRC_SET_REC_TIMEOUT, &args->timeout);
+			if (rc)
+				fprintf(stderr, _("%s: failed to set recording timeout\n"), dev);
+		} else
+			fprintf(stderr, _("%s: device does not support setting timeout\n"), dev);
+	}
+
+	if (args->wideband) {
+		unsigned on = args->wideband == 1;
+		if (features & LIRC_CAN_USE_WIDEBAND_RECEIVER) {
+			rc = ioctl(fd, LIRC_SET_WIDEBAND_RECEIVER, &on);
+			if (rc)
+				fprintf(stderr, _("%s: failed to set wideband receiver %s\n"), dev, on ? _("on") : _("off"));
+		} else
+			fprintf(stderr, _("%s: device does not have wideband receiver\n"), dev);
+	}
+
+	if (args->carrier_reports) {
+		unsigned on = args->carrier_reports == 1;
+		if (features & LIRC_CAN_MEASURE_CARRIER) {
+			rc = ioctl(fd, LIRC_SET_MEASURE_CARRIER_MODE, &on);
+			if (rc)
+				fprintf(stderr, _("%s: failed to set carrier reports %s\n"), dev, on ? _("on") : _("off"));
+		} else
+			fprintf(stderr, _("%s: device cannot measure carrier\n"), dev);
+	}
+
+	if (args->timeout_reports) {
+		unsigned on = args->timeout_reports == 1;
+		rc = ioctl(fd, LIRC_SET_REC_TIMEOUT_REPORTS, &on);
+		if (rc)
+			fprintf(stderr, _("%s: failed to set timeout reports %s: %m\n"), dev, on ? _("on") : _("off"));
+	}
+
+	if (args->carrier_low) {
+		if (features & LIRC_CAN_SET_REC_CARRIER_RANGE) {
+			rc = ioctl(fd, LIRC_SET_REC_CARRIER_RANGE, &args->carrier_low);
+			if (rc)
+				fprintf(stderr, _("%s: failed to set low carrier range: %m\n"), dev);
+			rc = ioctl(fd, LIRC_SET_REC_CARRIER, &args->carrier_high);
+			if (rc)
+				fprintf(stderr, _("%s: failed to set high carrier range: %m\n"), dev);
+		} else
+			fprintf(stderr, _("%s: device does not support setting receiver carrier range\n"), dev);
+	}
+
+	if (args->carrier)
+		lirc_set_send_carrier(fd, dev, features, args->carrier);
+
+	if (args->duty) {
+		if (features & LIRC_CAN_SET_SEND_DUTY_CYCLE) {
+			rc = ioctl(fd, LIRC_SET_SEND_DUTY_CYCLE, &args->duty);
+			if (rc)
+				fprintf(stderr, _("warning: %s: failed to set duty cycle: %m\n"), dev);
+		} else
+			fprintf(stderr, _("warning: %s: does not support setting send duty cycle\n"), dev);
+	}
+
+	if (args->emitters) {
+		if (features & LIRC_CAN_SET_TRANSMITTER_MASK) {
+			rc = ioctl(fd, LIRC_SET_TRANSMITTER_MASK, &args->emitters);
+			if (rc)
+				fprintf(stderr, _("warning: %s: failed to set send transmitters: %m\n"), dev);
+		} else
+			fprintf(stderr, _("warning: %s: does not support setting send transmitters\n"), dev);
+	}
+
+
+	return 0;
+}
+
+static void lirc_features(struct arguments *args, int fd, unsigned features)
+{
+	const char *dev = args->device;
+	unsigned resolution = 0;
+	int rc;
+
+	if (features & LIRC_CAN_GET_REC_RESOLUTION) {
+		rc = ioctl(fd, LIRC_GET_REC_RESOLUTION, &resolution);
+		if (rc == 0 && resolution == 0)
+			fprintf(stderr, _("warning: %s: device returned resolution of 0\n"), dev);
+		else if (rc)
+			fprintf(stderr, _("warning: %s: unexpected error while retrieving resolution: %m\n"), dev);
+	}
+
+	printf(_("Receive features %s:\n"), dev);
+	if (features & LIRC_CAN_REC_MODE2) {
+		printf(_(" - Device can receive raw IR\n"));
+		if (resolution)
+			printf(_(" - Resolution %u nanoseconds\n"), resolution);
+		if (features & LIRC_CAN_SET_REC_CARRIER)
+			printf(_(" - Set receive carrier\n"));
+		if (features & LIRC_CAN_USE_WIDEBAND_RECEIVER)
+			printf(_(" - Use wideband receiver\n"));
+		if (features & LIRC_CAN_MEASURE_CARRIER)
+			printf(_(" - Can measure carrier\n"));
+		if (features & LIRC_CAN_SET_REC_TIMEOUT) {
+			unsigned min_timeout, max_timeout;
+			int rc = ioctl(fd, LIRC_GET_MIN_TIMEOUT, &min_timeout);
+			if (rc) {
+				fprintf(stderr, _("warning: %s: device supports setting recording timeout but LIRC_GET_MIN_TIMEOUT returns: %m\n"), dev);
+				min_timeout = 0;
+			} else if (min_timeout == 0)
+				fprintf(stderr, _("warning: %s: device supports setting recording timeout but min timeout is 0\n"), dev);
+			rc = ioctl(fd, LIRC_GET_MAX_TIMEOUT, &max_timeout);
+			if (rc) {
+				fprintf(stderr, _("warning: %s: device supports setting recording timeout but LIRC_GET_MAX_TIMEOUT returns: %m\n"), dev);
+				max_timeout = 0;
+			} else if (max_timeout == 0) {
+				fprintf(stderr, _("warning: %s: device supports setting recording timeout but max timeout is 0\n"), dev);
+			}
+
+			if (min_timeout || max_timeout)
+				printf(_(" - Can set recording timeout min:%uµs max:%uµs\n"), min_timeout, max_timeout);
+		}
+	} else {
+		printf(_(" - Device cannot receive\n"));
+	}
+
+	printf(_("Send features %s:\n"), dev);
+	if (features & LIRC_CAN_SEND_PULSE) {
+		printf(_(" - Device can send raw IR\n"));
+		if (features & LIRC_CAN_SET_SEND_CARRIER)
+			printf(_(" - Set carrier\n"));
+		if (features & LIRC_CAN_SET_SEND_DUTY_CYCLE)
+			printf(_(" - Set duty cycle\n"));
+		if (features & LIRC_CAN_SET_TRANSMITTER_MASK) {
+			unsigned mask = ~0;
+			rc = ioctl(fd, LIRC_SET_TRANSMITTER_MASK, &mask);
+			if (rc == 0)
+				fprintf(stderr, _("warning: %s: device supports setting transmitter mask but returns 0 as number of transmitters\n"), dev);
+			else if (rc < 0)
+				fprintf(stderr, _("warning: %s: device supports setting transmitter mask but returns: %m\n"), dev);
+			else
+				printf(_(" - Set transmitter (%d available)\n"), rc);
+		}
+	} else {
+		printf(_(" - Device cannot send\n"));
+	}
+}
+
+static int lirc_send(struct arguments *args, int fd, unsigned features, struct file *f)
+{
+	const char *dev = args->device;
+
+	if (!(features & LIRC_CAN_SEND_PULSE)) {
+		fprintf(stderr, _("%s: device cannot send\n"), dev);
+		return EX_UNAVAILABLE;
+	}
+
+	if (args->carrier && f->carrier)
+		fprintf(stderr, _("warning: %s: carrier specified but overwritten on command line\n"), f->fname);
+	else if (f->carrier && args->carrier == 0)
+		lirc_set_send_carrier(fd, dev, features, f->carrier);
+
+	size_t size = f->len * sizeof(unsigned);
+	ssize_t ret = TEMP_FAILURE_RETRY(write(fd, f->buf, size));
+	if (ret < 0) {
+		fprintf(stderr, _("%s: failed to send: %m\n"), dev);
+		return EX_IOERR;
+	}
+
+	if (size < ret) {
+		fprintf(stderr, _("warning: %s: sent %zd out %zd edges\n"),
+				dev,
+				ret / sizeof(unsigned),
+				size / sizeof(unsigned));
+		return EX_IOERR;
+	}
+
+	return 0;
+}
+
+int lirc_record(struct arguments *args, int fd, unsigned features)
+{
+	char *dev = args->device;
+	FILE *out = stdout;
+	int rc = EX_IOERR;
+
+	if (args->savetofile) {
+		out = fopen(args->savetofile, "w");
+		if (!out) {
+			fprintf(stderr, _("%s: failed to open for writing: %m\n"), args->savetofile);
+			return EX_CANTCREAT;
+		}
+	}
+	unsigned buf[LIRCBUF_SIZE];
+
+	bool keep_reading = true;
+	bool leading_space = true;
+
+	while (keep_reading) {
+		ssize_t ret = TEMP_FAILURE_RETRY(read(fd, buf, sizeof(buf)));
+		if (ret < 0) {
+			fprintf(stderr, _("%s: failed read: %m\n"), dev);
+			goto err;
+		}
+
+		if (ret == 0 || ret % sizeof(unsigned)) {
+			fprintf(stderr, _("%s: read returned %zd bytes\n"),
+								dev, ret);
+			goto err;
+		}
+
+		for (int i=0; i<ret / sizeof(unsigned); i++) {
+			unsigned val = buf[i] & LIRC_VALUE_MASK;
+			unsigned msg = buf[i] & LIRC_MODE2_MASK;
+
+			// FIXME: the kernel often send us a space after
+			// the IR receiver comes out of idle mode. This
+			// is meaningless, maybe fix the kernel?
+			if (leading_space && msg == LIRC_MODE2_SPACE)
+				continue;
+			else
+				leading_space = false;
+
+			if (args->oneshot &&
+				(msg == LIRC_MODE2_TIMEOUT ||
+				(msg == LIRC_MODE2_SPACE && val > 19000))) {
+				keep_reading = false;
+				break;
+			}
+
+			switch (msg) {
+			case LIRC_MODE2_TIMEOUT:
+				fprintf(out, "timeout %u\n", val);
+				leading_space = true;
+				break;
+			case LIRC_MODE2_PULSE:
+				fprintf(out, "pulse %u\n", val);
+				break;
+			case LIRC_MODE2_SPACE:
+				fprintf(out, "space %u\n", val);
+				break;
+			case LIRC_MODE2_FREQUENCY:
+				fprintf(out, "carrier %u\n", val);
+				break;
+			}
+
+			fflush(out);
+		}
+	}
+
+	rc = 0;
+err:
+	if (args->savetofile)
+		fclose(out);
+
+	return rc;
+}
+
+int main(int argc, char *argv[])
+{
+	struct arguments args = {};
+
+	argp_parse(&argp, argc, argv, 0, 0, &args);
+
+	if (args.device == NULL)
+		args.device = "/dev/lirc0";
+
+	int rc, fd;
+	unsigned features;
+
+	fd = open_lirc(args.device, &features);
+	if (fd < 0)
+		exit(EX_NOINPUT);
+
+	rc = lirc_options(&args, fd, features);
+	if (rc)
+		exit(EX_IOERR);
+
+	struct file *s = args.send;
+	while (s) {
+		struct file *next = s->next;
+		if (s != args.send)
+			usleep(IR_DEFAULT_TIMEOUT);
+
+		rc = lirc_send(&args, fd, features, s);
+		if (rc) {
+			close(fd);
+			exit(rc);
+		}
+
+		free(s);
+		s = next;
+	}
+
+	if (args.record) {
+		rc = lirc_record(&args, fd, features);
+		if (rc) {
+			close(fd);
+			exit(rc);
+		}
+	}
+
+	if (args.features)
+		lirc_features(&args, fd, features);
+
+	close(fd);
+
+	return 0;
+}
diff --git a/v4l-utils.spec.in b/v4l-utils.spec.in
index dd8959b..bdbb27b 100644
--- a/v4l-utils.spec.in
+++ b/v4l-utils.spec.in
@@ -17,8 +17,8 @@ Requires:       libv4l = %{version}-%{release}
 
 %description
 v4l-utils is a collection of various video4linux (V4L) and DVB utilities. The
-main v4l-utils package contains cx18-ctl, ir-keytable, ivtv-ctl, v4l2-ctl and
-v4l2-sysfs-path.
+main v4l-utils package contains cx18-ctl, ir-keytable, ir-ctl, ivtv-ctl,
+v4l2-ctl and v4l2-sysfs-path.
 
 
 %package        devel-tools
@@ -129,10 +129,12 @@ gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :
 %config(noreplace) %{_sysconfdir}/udev/rules.d/70-infrared.rules
 %{_bindir}/cx18-ctl
 %{_bindir}/ir-keytable
+%{_bindir}/ir-ctl
 %{_bindir}/ivtv-ctl
 %{_bindir}/v4l2-ctl
 %{_bindir}/v4l2-sysfs-path
 %{_mandir}/man1/ir-keytable.1*
+%{_mandir}/man1/ir-ctl.1*
 
 %files devel-tools
 %defattr(-,root,root,-)
