diff options
| -rw-r--r-- | Makefile | 44 | ||||
| -rw-r--r-- | bt.c | 119 | ||||
| -rw-r--r-- | capture.c | 259 | ||||
| -rw-r--r-- | debian/changelog | 5 | ||||
| -rw-r--r-- | debian/compat | 1 | ||||
| -rw-r--r-- | debian/control | 24 | ||||
| -rw-r--r-- | debian/copyright | 23 | ||||
| -rw-r--r-- | debian/docs | 0 | ||||
| -rw-r--r-- | debian/fmrxd.aegis | 15 | ||||
| -rw-r--r-- | debian/fmrxd.install | 3 | ||||
| -rwxr-xr-x | debian/rules | 13 | ||||
| -rw-r--r-- | fmrx-cat.c | 96 | ||||
| -rw-r--r-- | fmrx-ctl.c | 115 | ||||
| -rw-r--r-- | fmrxd.c | 58 | ||||
| -rw-r--r-- | fmrxd.h | 77 | ||||
| -rw-r--r-- | fmrxd.service | 3 | ||||
| -rw-r--r-- | radio.c | 97 | ||||
| -rw-r--r-- | rds.c | 665 | ||||
| -rw-r--r-- | server.c | 542 | ||||
| -rw-r--r-- | tuner.c | 208 | 
20 files changed, 2367 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..07cc811 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +CFLAGS?=-Os -Wall -ggdb +LDFLAGS?=-Wl,--as-needed +PREFIX?=/usr +DESTDIR?= + +FMRXD_PKGS:=glib-2.0 dbus-glib-1 bluez alsa +FMRXD_CFLAGS:=$(shell pkg-config --cflags $(FMRXD_PKGS)) +FMRXD_LIBS:=$(shell pkg-config --libs $(FMRXD_PKGS)) + +FMRXUTILS_PKGS:=glib-2.0 dbus-glib-1 +FMRXUTILS_CFLAGS:=$(shell pkg-config --cflags $(FMRXUTILS_PKGS)) +FMRXUTILS_LIBS:=$(shell pkg-config --libs $(FMRXUTILS_PKGS)) + +all: fmrxd fmrx-cat fmrx-ctl + +fmrxd: fmrxd.o server.o radio.o bt.o capture.o tuner.o rds.o +	$(CC) $(FMRXD_LDFLAGS) $(LDFLAGS) -o $@ $+ $(LIBS) $(FMRXD_LIBS) + +fmrx-cat: fmrx-cat.o +	$(CC) $(FMRXUTILS_LDFLAGS) $(LDFLAGS) -o $@ $+ $(LIBS) $(FMRXUTILS_LIBS) + +fmrx-ctl: fmrx-ctl.o +	$(CC) $(FMRXUTILS_LDFLAGS) $(LDFLAGS) -o $@ $+ $(LIBS) $(FMRXUTILS_LIBS) + +%.o: %.c +	$(CC) $(FMRXD_CFLAGS) $(CFLAGS) -o $@ -c $< + +clean: +	rm -f *.o fmrxd fmrx-cat fmrx-ctl + +install: fmrxd fmrx-cat fmrx-ctl +	install -d $(DESTDIR)$(PREFIX)/sbin $(DESTDIR)$(PREFIX)/bin +	install -d $(DESTDIR)$(PREFIX)/share/dbus-1/system-services +	install -m 0755 fmrxd $(DESTDIR)$(PREFIX)/sbin +	install -m 0755 fmrx-cat fmrx-ctl $(DESTDIR)$(PREFIX)/bin +	install -m 0644 fmrxd.service \ +		$(DESTDIR)$(PREFIX)/share/dbus-1/system-services/com.javispedro.fmrxd.service + +uninstall: +	rm -f $(DESTDIR)$(PREFIX)/sbin/fmrxd +	rm -f $(DESTDIR)$(PREFIX)/bin/fmrx-cat $(DESTDIR)$(PREFIX)/bin/fmrx-ctl +	rm -f $(DESTDIR)$(PREFIX)/share/dbus-1/system-services/com.javispedro.fmrxd.service + +.PHONY: all clean install uninstall @@ -0,0 +1,119 @@ +/* + *  fmrxd - a daemon to enable and multiplex access to the N950/N9 radio tuner + *  Copyright (C) 2011 Javier S. Pedro <maemo@javispedro.com> + * + *  Mostly based on ohm-plugins-misc from MeeGo Multimedia + *  Copyright (C) 2010 Nokia Corporation + * + *  This program is free software; you can redistribute it and/or modify + *  it under the terms of the GNU General Public License as published by + *  the Free Software Foundation; either version 2 of the License, or + *  (at your option) any later version. + + *  This program is distributed in the hope that it will be useful, + *  but WITHOUT ANY WARRANTY; without even the implied warranty of + *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + *  GNU General Public License for more details. + + *  You should have received a copy of the GNU General Public License along + *  with this program; if not, write to the Free Software Foundation, Inc., + *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdbool.h> +#include <errno.h> +#include <unistd.h> +#include <sys/ioctl.h> +#include <glib.h> + +#include <bluetooth/bluetooth.h> +#include <bluetooth/hci.h> +#include <bluetooth/hci_lib.h> + +#include "fmrxd.h" + +static gboolean disable_bt_func(gpointer data) +{ +	int dd; + +	g_debug("Now disabling BT"); + +	dd = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI); +	if (dd == -1) { +		g_message("Failed to open HCI socket: %s", strerror(errno)); +		return FALSE; +	} + +	if (ioctl(dd, HCIDEVDOWN, BT_DEV_ID) < 0 && errno != EALREADY) { +		g_message("Failed to turn Bluetooth device off: %s", strerror(errno)); +	} + +	close(dd); + +	return FALSE; +} + +bool configure_bt_muxer(bool on) +{ +	struct sockaddr_hci a; +	int dd; +	bool was_up; + +	/* The magic numbers: */ +	const short ocf = 0x00; +	const short ogf = 0x3f; +	char cmd[4] = { 0xf3, 0x88, 0x01, on ? 0x02 : 0x01 }; + +	/* Open HCI socket. */ +	dd = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI); +	if (dd == -1) { +		g_warning("Failed to open HCI socket: %s", strerror(errno)); +		return false; +	} + +	/* Try to turn the specificied BT device on. */ +	if (ioctl(dd, HCIDEVUP, BT_DEV_ID) < 0) { +		if (errno == EALREADY) { +			/* The BT device was found to be already on; no need to turn it off later. */ +			was_up = true; +		} else { +			g_warning("Failed to turn Bluetooth device on: %s", strerror(errno)); +			goto failed; +		} +	} else { +		/* Turned on succesfully. */ +		g_debug("Turned on BT"); +		was_up = false; +	} + +	/* Bind HCI socket to this device. */ +	memset(&a, 0, sizeof(a)); +	a.hci_family = AF_BLUETOOTH; +	a.hci_dev = BT_DEV_ID; +	if (bind(dd, (struct sockaddr *) &a, sizeof(a)) < 0) { +		g_warning("Failed to bind HCI socket: %s", strerror(errno)); +		goto failed; +	} + +	/* Now send the magic command. */ +	if (hci_send_cmd(dd, ogf, ocf, sizeof(cmd), cmd) < 0) { +		g_warning("Failed to send magic HCI command: %s", strerror(errno)); +		goto failed; +	} + +	g_debug("Muxer config changed to 0x%x", cmd[3]); + +	close(dd); + +	if (!was_up) { +		/* Try to shut down bluetooth if we enabled it. */ +		/* Give ample time for everything to be setup though. */ +		g_timeout_add_seconds(1, disable_bt_func, NULL); +	} + +	return true; + +failed: +	close(dd); +	return false; +} diff --git a/capture.c b/capture.c new file mode 100644 index 0000000..f14afa2 --- /dev/null +++ b/capture.c @@ -0,0 +1,259 @@ +/* + *  fmrxd - a daemon to enable and multiplex access to the N950/N9 radio tuner + *  Copyright (C) 2011 Javier S. Pedro <maemo@javispedro.com> + * + *  This program is free software; you can redistribute it and/or modify + *  it under the terms of the GNU General Public License as published by + *  the Free Software Foundation; either version 2 of the License, or + *  (at your option) any later version. + + *  This program is distributed in the hope that it will be useful, + *  but WITHOUT ANY WARRANTY; without even the implied warranty of + *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + *  GNU General Public License for more details. + + *  You should have received a copy of the GNU General Public License along + *  with this program; if not, write to the Free Software Foundation, Inc., + *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include <stdbool.h> +#include <unistd.h> +#include <glib.h> +#include <alsa/asoundlib.h> + +#include "fmrxd.h" + +static snd_pcm_t *a_pcm = NULL; +static guint a_fd_watch = 0; + +/** Sets mixer switch <name> to <value> */ +static bool mixer_set_enum_value(snd_hctl_t *mixer, +	const char * name, const char * value) +{ +	int err; + +	snd_ctl_elem_id_t *id; +	snd_ctl_elem_id_alloca(&id); +	snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); +	snd_ctl_elem_id_set_name(id, name); + +	snd_hctl_elem_t *elem = snd_hctl_find_elem(mixer, id); +	if (!elem) { +		g_critical("Couldn't find mixer element '%s'", name); +		return false; +	} + +	snd_ctl_elem_info_t *info; +	snd_ctl_elem_info_alloca(&info); +	snd_ctl_elem_info_set_id(info, id); +	err = snd_hctl_elem_info(elem, info); +	if (err) { +		g_critical("Couldn't get mixer element '%s' information (%d)", name, +			err); +		return false; +	} + +	long value_idx = -1; +	int i, items = snd_ctl_elem_info_get_items(info); +	for (i = 0; i < items; i++) { +		snd_ctl_elem_info_set_item(info, i); +		err = snd_hctl_elem_info(elem, info); +		if (err) { +			g_critical("Couldn't get mixer element '%s' information (%d)", name, +				err); +			return false; +		} +		if (strcmp(snd_ctl_elem_info_get_item_name(info), value) == 0) { +			value_idx = i; +			break; +		} +	} +	if (value_idx < 0) { +		g_critical("Couldn't find mixer '%s' value '%s'", name, value); +		return false; +	} + +	snd_ctl_elem_value_t *control; +	snd_ctl_elem_value_alloca(&control); +	snd_ctl_elem_value_set_id(control, id); + +	items = snd_ctl_elem_info_get_count(info); +	for (i = 0; i < items; i++) { +		snd_ctl_elem_value_set_enumerated(control, i, value_idx); +	} + +	err = snd_hctl_elem_write(elem, control); +	if (err) { +		g_critical("Couldn't set mixer element '%s' (%d)", name, err); +		return false; +	} + +	return true; +} + +bool configure_mixer(bool on) +{ +	snd_hctl_t *mixer; +	int err; +	bool res; + +	err = snd_hctl_open(&mixer, ALSA_MIXER_NAME, 0); +	if (err < 0) { +		g_critical("Failed to open ALSA mixer '%s' (%d)", ALSA_MIXER_NAME, err); +		return false; +	} +	err = snd_hctl_load(mixer); +	if (err < 0) { +		g_critical("Failed to load ALSA hmixer elements (%d)", err); +		snd_hctl_close(mixer); +		return false; +	} + +	if (on) { +		res = mixer_set_enum_value(mixer, "Mode Switch", "Rx"); +		res &= mixer_set_enum_value(mixer, "Codec Mode", "FmRx"); +		res &= mixer_set_enum_value(mixer, "Audio Switch", "Digital"); +	} else { +		res = mixer_set_enum_value(mixer, "Codec Mode", "Bt"); +		res &= mixer_set_enum_value(mixer, "Mode Switch", "Off"); +	} + +	/* TODO: "Region Switch" for Japan freqs. */ + +	snd_hctl_close(mixer); + +	return res; +} + +static int alsa_recover(int err) +{ +	err	= snd_pcm_recover(a_pcm, err, 0); +	if (err < 0) { +		g_critical("ALSA stream failed to recover (%d)", err); +		return err; +	} + +	err = snd_pcm_start(a_pcm); +	if (err < 0) { +		g_critical("ALSA stream failed to restart (%d)", err); +		return err; +	} + +	return 0; +} + +static snd_pcm_sframes_t calculate_avail_frames() +{ +	snd_pcm_sframes_t frames_avail; +	int err; + +	while ((frames_avail = snd_pcm_avail_update(a_pcm)) < 0) { +		g_warning("ALSA stream avail failed (%d)", (int) frames_avail); +		err = alsa_recover(frames_avail); +		if (err < 0) { +			return err; +		} +	} + +	return frames_avail; +} + +static gboolean capture_callback(GIOChannel *source, GIOCondition condition, +	gpointer data) +{ +	snd_pcm_sframes_t frames_avail, frames_to_read, frames_read; +	size_t bytes_avail, bytes_buffer, bytes_to_read, bytes_read; +	void *buffer; +	int err; + +	/* Only read as much data as available; we do not want to block. */ +	while ((frames_avail = calculate_avail_frames()) > 0) { +		bytes_avail = snd_pcm_frames_to_bytes(a_pcm, frames_avail); + +		/* Request a large enough segment in the ring buffer. */ +		bytes_buffer = server_get_buffer(bytes_avail, &buffer); +		if (bytes_avail > bytes_buffer) { +			bytes_to_read = bytes_buffer; +			frames_to_read = snd_pcm_bytes_to_frames(a_pcm, bytes_to_read); +		} else { +			bytes_to_read = bytes_avail; +			frames_to_read = frames_avail; +		} + +		frames_read = snd_pcm_readi(a_pcm, buffer, frames_to_read); +		if (frames_read < 0) { +			g_warning("ALSA failed to read (%d)", (int) frames_read); +			err = alsa_recover(frames_read); +			if (err < 0) { +				return FALSE; +			} +			continue; // Retry +		} + +		bytes_read = snd_pcm_frames_to_bytes(a_pcm, frames_read); + +		server_commit_buffer(bytes_read); +	} + +	return TRUE; +} + +/** Equivalent to "arecord -Dhw:2,0 -f S16_LE -r 48000 -c 2" */ +bool configure_capture(bool on) +{ +	int err; + +	if (on) { +		err = snd_pcm_open(&a_pcm, ALSA_PCM_CAPTURE_NAME, +			SND_PCM_STREAM_CAPTURE,	SND_PCM_NONBLOCK); +		if (err < 0) { +			g_critical("Failed to open ALSA PCM device '%s' (%d)", ALSA_PCM_CAPTURE_NAME, err); +			return false; +		} + +		/* -f S16_LE -r 48000 -c 2; latency up to 0.3 seconds */ +		err = snd_pcm_set_params(a_pcm, SND_PCM_FORMAT_S16_LE, +			SND_PCM_ACCESS_RW_INTERLEAVED, 2, ALSA_CAPTURE_RATE, 0, 300000); +		if (err < 0) { +			g_critical("Failed to open ALSA PCM device '%s' (%d)", ALSA_PCM_CAPTURE_NAME, err); +			snd_pcm_close(a_pcm); +			return false; +		} + +		struct pollfd polls[1]; +		int count = snd_pcm_poll_descriptors(a_pcm, polls, 1); +		if (count != 1) { +			g_critical("Not able to get poll FD from ALSA"); +			snd_pcm_close(a_pcm); +			return false; +		} + +		GIOChannel *channel = g_io_channel_unix_new(polls[0].fd); +		a_fd_watch = g_io_add_watch(channel, polls[0].events, +			capture_callback, NULL); +		g_io_channel_unref(channel); + +		err = snd_pcm_start(a_pcm); +		if (err < 0) { +			g_critical("Couldn't start PCM device (%d)", err); +			snd_pcm_close(a_pcm); +			return false; +		} + +		g_debug("ALSA capture started"); + +		return true; +	} else { +		if (a_fd_watch) { +			g_source_remove(a_fd_watch); +			a_fd_watch = 0; +		} +		if (a_pcm) { +			snd_pcm_close(a_pcm); +			a_pcm = NULL; +		} + +		return true; +	} +} diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..f307b8a --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +fmrxd (0.1) unstable; urgency=low + +  * Initial Release. + + -- Javier S. Pedro <maemo@javispedro.com>  Sat, 03 Sep 2011 19:47:50 +0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..05a2e34 --- /dev/null +++ b/debian/control @@ -0,0 +1,24 @@ +Source: fmrxd +Section: sound +Priority: extra +Maintainer: Javier S. Pedro <maemo@javispedro.com> +Build-Depends: debhelper (>= 7), aegis-manifest-dev, libglib2.0-dev, + libasound2-dev, libdbus-glib-1-dev, libdbus-1-dev +Standards-Version: 3.8.4 +Homepage: https://gitorious.org/n950-fmrx/fmrxd + +Package: fmrxd +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: Daemon to enable and multiplex access to the N950/N9 radio tuner + fmrxd is a small daemon that enables the use of the FM Radio tuner available on + the Nokia N9 and N950 devices, providing a D-Bus API that can be used by + user programs that will not require any additional privileges. + +Package: fmrx-utils +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: Command line applets for the fmrxd FM radio daemon + This package contains the command line utilities fmrx-cat and fmrx-ctl that + can be used to fetch audio data from the radio device and control the + tuner device. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..1b606ec --- /dev/null +++ b/debian/copyright @@ -0,0 +1,23 @@ +This work was created and packaged for Debian by: + +    Javier S. Pedro <maemo@javispedro.com> on Sat, 03 Sep 2011 19:47:50 +0200 + +Copyright: + +    Javier S. Pedro <maemo@javispedro.com> + +License: + +    This program is free software: you can redistribute it and/or modify +    it under the terms of the GNU General Public License as published by +    the Free Software Foundation, either version 2 of the License, or +    (at your option) any later version. + +    This package is distributed in the hope that it will be useful, +    but WITHOUT ANY WARRANTY; without even the implied warranty of +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +    GNU General Public License for more details. + +    You should have received a copy of the GNU General Public License +    along with this program. If not, see <http://www.gnu.org/licenses/>. + diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/debian/docs diff --git a/debian/fmrxd.aegis b/debian/fmrxd.aegis new file mode 100644 index 0000000..18d2100 --- /dev/null +++ b/debian/fmrxd.aegis @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<aegis> +	<provide> +		<credential name="dbus-server-bind" /> +		<dbus name="com.javispedro.fmrxd" own="dbus-server-bind" bus="system"> +			<node name="/com/javispedro/fmrxd"> +			</node> +		</dbus> +	</provide> +	<request> +		<credential name="UID::root" /> +		<credential name="dbus-server-bind" /> +		<for path="/usr/sbin/fmrxd" /> +	</request> +</aegis> diff --git a/debian/fmrxd.install b/debian/fmrxd.install new file mode 100644 index 0000000..58728e8 --- /dev/null +++ b/debian/fmrxd.install @@ -0,0 +1,3 @@ +fmrxd usr/sbin +fmrx-ctl usr/bin +fmrx-cat usr/bin diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..917d9bf --- /dev/null +++ b/debian/rules @@ -0,0 +1,13 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +%: +	dh  $@ diff --git a/fmrx-cat.c b/fmrx-cat.c new file mode 100644 index 0000000..580203e --- /dev/null +++ b/fmrx-cat.c @@ -0,0 +1,96 @@ +#include <stdio.h> +#include <unistd.h> +#include <glib.h> +#include <dbus/dbus-glib.h> +#include <dbus/dbus-glib-lowlevel.h> + +static GMainLoop *main_loop; +static DBusConnection *bus; +static int fd; + +#define BUS_NAME      "com.javispedro.fmrxd" +#define BUS_PATH      "/com/javispedro/fmrxd" +#define BUS_INTERFACE BUS_NAME + +#define BUFFER_SIZE 4*4096 + +static gchar buffer[BUFFER_SIZE]; + +static gboolean monitor_callback(GIOChannel *source, GIOCondition condition, gpointer data) +{ +	GError *err = NULL; +	gsize read, written = 0; +	do { +		g_io_channel_read_chars(source, buffer, BUFFER_SIZE, &read, &err); +		if (err != NULL) { +			g_printerr("Unable to read from server: %s\n", err->message); +			g_error_free(err); +			g_main_loop_quit(main_loop); +		} +		if (read > 0) { +			written = fwrite(buffer, 1, read, stdout); +			if (written < 0) { +				g_printerr("Unable to write to stdout\n"); +				g_main_loop_quit(main_loop); +			} +		} +	} while (read > 0 && written > 0); + +	return TRUE; +} + +static void monitor() +{ +	GIOChannel *channel = g_io_channel_unix_new(fd); +	g_io_channel_set_encoding(channel, NULL, NULL); +	g_io_add_watch(channel, G_IO_IN, monitor_callback, NULL); +	g_io_channel_unref(channel); +} + +static void connect() +{ +	DBusError err; +	dbus_bool_t ret; +	DBusMessage *reply; +	DBusMessage *msg = dbus_message_new_method_call(BUS_NAME, BUS_PATH, +												  BUS_INTERFACE, "Connect"); +	g_assert(msg != NULL); + +	dbus_error_init(&err); + +	reply = dbus_connection_send_with_reply_and_block(bus, msg, -1, &err); +	g_assert(reply != NULL); +	g_assert(!dbus_error_is_set(&err)); +	dbus_message_unref(msg); + +	ret = dbus_message_get_args(reply, &err, DBUS_TYPE_UNIX_FD, &fd, DBUS_TYPE_INVALID); +	g_assert(ret == TRUE); +	g_assert(!dbus_error_is_set(&err)); +	g_assert(fd != -1); + +	dbus_message_unref(reply); +} + +static void dbus_init() +{ +	DBusError err;	dbus_error_init(&err); +	bus = dbus_bus_get(DBUS_BUS_SESSION, &err); +	g_assert(bus != NULL); +	g_assert(!dbus_error_is_set(&err)); +	dbus_connection_setup_with_g_main(bus, g_main_loop_get_context(main_loop)); +} + +int main(int argc, char **argv) +{ +	main_loop = g_main_loop_new(NULL, FALSE); + +	dbus_init(); +	connect(); +	monitor(); + +	g_main_loop_run(main_loop); + +	close(fd); + +	return 0; +} diff --git a/fmrx-ctl.c b/fmrx-ctl.c new file mode 100644 index 0000000..408dadd --- /dev/null +++ b/fmrx-ctl.c @@ -0,0 +1,115 @@ +#include <stdio.h> +#include <unistd.h> +#include <glib.h> +#include <dbus/dbus-glib.h> +#include <dbus/dbus-glib-lowlevel.h> + +static GMainLoop *main_loop; +static DBusConnection *bus; + +static gdouble cmd_tune = 0.0; +static gboolean cmd_next = FALSE; +static gboolean cmd_prev = FALSE; + +static GOptionEntry entries[] = { +	{ "tune", 't', 0, G_OPTION_ARG_DOUBLE, &cmd_tune, "Tune to a specific frequency", "MHz" }, +	{ "next", 'n', 0, G_OPTION_ARG_NONE, &cmd_next, "Forward scan for a signal", NULL }, +	{ "prev", 'p', 0, G_OPTION_ARG_NONE, &cmd_prev, "Backward scan for a signal", NULL }, +	{ NULL } +}; + +#define BUS_NAME      "com.javispedro.fmrxd" +#define BUS_PATH      "/com/javispedro/fmrxd" +#define BUS_INTERFACE BUS_NAME + +static void tune(double f) +{ +	DBusError err; +	DBusMessage *reply; +	DBusMessage *msg = dbus_message_new_method_call(BUS_NAME, BUS_PATH, +												  BUS_INTERFACE, "Tune"); +	g_assert(msg != NULL); + +	dbus_error_init(&err); + +	dbus_message_append_args(msg, DBUS_TYPE_DOUBLE, &f, DBUS_TYPE_INVALID); + +	reply = dbus_connection_send_with_reply_and_block(bus, msg, -1, &err); +	g_assert(reply != NULL); +	g_assert(!dbus_error_is_set(&err)); +	dbus_message_unref(msg); + +	dbus_message_unref(reply); +} + +static void next() +{ +	DBusError err; +	DBusMessage *reply; +	DBusMessage *msg = dbus_message_new_method_call(BUS_NAME, BUS_PATH, +												  BUS_INTERFACE, "SearchForward"); +	g_assert(msg != NULL); + +	dbus_error_init(&err); + +	reply = dbus_connection_send_with_reply_and_block(bus, msg, -1, &err); +	g_assert(reply != NULL); +	g_assert(!dbus_error_is_set(&err)); +	dbus_message_unref(msg); + +	dbus_message_unref(reply); +} + +static void prev() +{ +	DBusError err; +	DBusMessage *reply; +	DBusMessage *msg = dbus_message_new_method_call(BUS_NAME, BUS_PATH, +												  BUS_INTERFACE, "SearchBackward"); +	g_assert(msg != NULL); + +	dbus_error_init(&err); + +	reply = dbus_connection_send_with_reply_and_block(bus, msg, -1, &err); +	g_assert(reply != NULL); +	g_assert(!dbus_error_is_set(&err)); +	dbus_message_unref(msg); + +	dbus_message_unref(reply); +} + +static void dbus_init() +{ +	DBusError err;	dbus_error_init(&err); +	bus = dbus_bus_get(DBUS_BUS_SESSION, &err); +	g_assert(bus != NULL); +	g_assert(!dbus_error_is_set(&err)); +	dbus_connection_setup_with_g_main(bus, g_main_loop_get_context(main_loop)); +} + +int main(int argc, char **argv) +{ +	GError *error = NULL; +	GOptionContext *context = g_option_context_new("- control the fmrxd daemon"); +	main_loop = g_main_loop_new(NULL, FALSE); + +	g_option_context_add_main_entries(context, entries, NULL); +	if (!g_option_context_parse(context, &argc, &argv, &error)) { +		g_printerr("Option parsing failed: %s\n", error->message); +		return 1; +	} + +	dbus_init(); + +	if (cmd_tune > 0.0) { +		tune(cmd_tune); +	} +	if (cmd_prev) { +		prev(); +	} +	if (cmd_next) { +		next(); +	} + +	return 0; +} @@ -0,0 +1,58 @@ +/* + *  fmrxd - a daemon to enable and multiplex access to the N950/N9 radio tuner + *  Copyright (C) 2011 Javier S. Pedro <maemo@javispedro.com> + * + *  This program is free software; you can redistribute it and/or modify + *  it under the terms of the GNU General Public License as published by + *  the Free Software Foundation; either version 2 of the License, or + *  (at your option) any later version. + + *  This program is distributed in the hope that it will be useful, + *  but WITHOUT ANY WARRANTY; without even the implied warranty of + *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + *  GNU General Public License for more details. + + *  You should have received a copy of the GNU General Public License along + *  with this program; if not, write to the Free Software Foundation, Inc., + *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include <stdlib.h> +#include <signal.h> +#include <glib.h> + +#include "fmrxd.h" + +GMainLoop *main_loop; + +static gboolean quit_func(gpointer user_data) +{ +	g_main_loop_quit(main_loop); +	return FALSE; +} + +static void signal_handler(int signal) +{ +	// Signal handler; try not to do much stuff here. +	radio_queue_stop(); +	g_idle_add(quit_func, NULL); +} + +int main(int argc, char **argv) +{ +	main_loop = g_main_loop_new(NULL, FALSE); + +	/* Trap quit signals to die gracefully. */ +	signal(SIGTERM, signal_handler); +	signal(SIGINT, signal_handler); +	signal(SIGHUP, signal_handler); +	signal(SIGPIPE, SIG_IGN); + +	/* Start the D-Bus server. */ +	if (server_start() < 0) { +		return 1; +	} + +	g_main_loop_run(main_loop); +	return 0; +} @@ -0,0 +1,77 @@ +#ifndef FMRXD_H +#define FMRXD_H + +#include <stddef.h> +#include <stdint.h> +#include <stdbool.h> +#include <glib.h> + +#define BUS_NAME      "com.javispedro.fmrxd" +#define BUS_PATH      "/com/javispedro/fmrxd" +#define BUS_INTERFACE BUS_NAME + +#define SERVER_ON_DEMAND    1 +#define SERVER_LINGER_TIME	6 + +#define RADIO_LINGER_TIME   2 + +#define BT_DEV_ID 0 + +#define ALSA_MIXER_NAME "hw:2" +#define ALSA_PCM_CAPTURE_NAME "hw:2,0" +#define ALSA_CAPTURE_RATE 48000 + +#define RING_BUFFER_SIZE (ALSA_CAPTURE_RATE) + +#define TUNER_DEVICE "/dev/radio0" +#define TUNER_DEVICE_ID 0 + +#define CRAPPY_WL1273_RDS 1 + +/* fmrxd.c */ +extern GMainLoop* main_loop; + +/* server.c */ +int server_start(); +void server_stop(); +void server_queue_stop(); +int server_new_client(); +size_t server_get_buffer(size_t size, void **buffer); +void server_commit_buffer(size_t size); +void server_notify_tuned(double mhz); +void server_notify_stopped(); +void server_notify_pi(uint16_t pi); +void server_notify_ps(const char *ps); +void server_notify_rt(const char *rt); + +/* radio.c */ +bool radio_active(); +void radio_start(); +void radio_stop(); +void radio_queue_start(); +void radio_queue_stop(); + +/* bt.c */ +bool configure_bt_muxer(bool on); + +/* capture.c */ +bool configure_mixer(bool on); +bool configure_capture(bool on); + +/* tuner.c */ +bool configure_tuner(bool on); +extern int tuner_fd; +bool tuner_set_frequency(double mhz); +bool tuner_search(bool forward); + +/* rds.c */ +bool configure_rds(bool on); +void rds_reset(); + +unsigned short rds_get_pi(); +unsigned char rds_get_pty(); +const char * rds_get_pty_text(); +gchar * rds_get_ps(); +gchar * rds_get_rt(); + +#endif // FMRXD_H diff --git a/fmrxd.service b/fmrxd.service new file mode 100644 index 0000000..45c9afb --- /dev/null +++ b/fmrxd.service @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=com.javispedro.fmrxd +Exec=/usr/sbin/fmrxd @@ -0,0 +1,97 @@ +/* + *  fmrxd - a daemon to enable and multiplex access to the N950/N9 radio tuner + *  Copyright (C) 2011 Javier S. Pedro <maemo@javispedro.com> + * + *  This program is free software; you can redistribute it and/or modify + *  it under the terms of the GNU General Public License as published by + *  the Free Software Foundation; either version 2 of the License, or + *  (at your option) any later version. + + *  This program is distributed in the hope that it will be useful, + *  but WITHOUT ANY WARRANTY; without even the implied warranty of + *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + *  GNU General Public License for more details. + + *  You should have received a copy of the GNU General Public License along + *  with this program; if not, write to the Free Software Foundation, Inc., + *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include <glib.h> +#include "fmrxd.h" + +static guint start_source_id = 0; +static guint stop_source_id = 0; +static bool active = false; + +bool radio_active() +{ +	return active; +} + +void radio_start() +{ +	g_return_if_fail(!active); +	g_debug("Starting radio"); + +	configure_bt_muxer(true); +	configure_mixer(true); +	configure_tuner(true); +	configure_capture(true); +	configure_rds(true); + +	active = true; +} + +void radio_stop() +{ +	g_return_if_fail(active); +	g_debug("Stopping radio"); + +	configure_rds(false); +	configure_capture(false); +	configure_tuner(false); +	configure_mixer(false); +	configure_bt_muxer(false); + +	server_notify_stopped(); + +	active = false; +} + +static gboolean radio_start_func(gpointer data) +{ +	radio_start(); +	start_source_id = 0; +	return FALSE; +} + +static gboolean radio_stop_func(gpointer data) +{ +	radio_stop(); +	stop_source_id = 0; +	return FALSE; +} + +void radio_queue_start() +{ +	if (stop_source_id) { +		/* If there was a queued stop pending, remove it. */ +		g_source_remove(stop_source_id); +	} +	if (!start_source_id) { +		start_source_id = g_idle_add(radio_start_func, NULL); +	} +} + +void radio_queue_stop() +{ +	if (start_source_id) { +		/* If there is a queued start, remove it. */ +		g_source_remove(start_source_id); +	} +	if (!stop_source_id) { +		stop_source_id = g_timeout_add_seconds(RADIO_LINGER_TIME, +			radio_stop_func, NULL); +	} +} @@ -0,0 +1,665 @@ +/* + *  fmrxd - a daemon to enable and multiplex access to the N950/N9 radio tuner + *  Copyright (C) 2011 Javier S. Pedro <maemo@javispedro.com> + * + *  This program is free software; you can redistribute it and/or modify + *  it under the terms of the GNU General Public License as published by + *  the Free Software Foundation; either version 2 of the License, or + *  (at your option) any later version. + + *  This program is distributed in the hope that it will be useful, + *  but WITHOUT ANY WARRANTY; without even the implied warranty of + *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + *  GNU General Public License for more details. + + *  You should have received a copy of the GNU General Public License along + *  with this program; if not, write to the Free Software Foundation, Inc., + *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include <stdint.h> +#include <string.h> +#include <unistd.h> +#include <glib.h> +#include <linux/videodev2.h> + +#include "fmrxd.h" + +#if CRAPPY_WL1273_RDS +#define WL1273_RDS_BLOCK_A              0 +#define WL1273_RDS_BLOCK_B              1 +#define WL1273_RDS_BLOCK_C              2 +#define WL1273_RDS_BLOCK_C_ALT          3 +#define WL1273_RDS_BLOCK_D              4 +#define WL1273_RDS_BLOCK_E              5 +#define WL1273_RDS_BLOCK_MASK           0x7 +#define WL1273_RDS_CORRECTABLE_ERROR    (1 << 3) +#define WL1273_RDS_UNCORRECTABLE_ERROR  (1 << 4) +#endif + +#define RDS_PS_SIZE     8 + +#define RDS_RT_2A_SIZE  64 +#define RDS_RT_2B_SIZE  32 +#define RDS_RT_MAX_SIZE RDS_RT_2A_SIZE + +enum { +	RDS_TYPE_0A = 0, +	RDS_TYPE_0B = 1, +	RDS_TYPE_1A = 2, +	RDS_TYPE_1B = 3, +	RDS_TYPE_2A = 4, +	RDS_TYPE_2B = 5 +}; + +#define RDS_TYPE_IS_B(type) (type & 1) + +static guint rds_watch = 0; + +static uint16_t rds_pi; +static bool rds_pi_notified; + +static bool rds_tp; +static uint8_t rds_pty; + +static char rds_ps[RDS_PS_SIZE+1]; +static uint8_t rds_ps_segs; +static bool rds_ps_notified; + +static char rds_rt[RDS_RT_MAX_SIZE+1]; +static uint32_t rds_rt_segs; +static int8_t rds_rt_ab; +static bool rds_rt_notified; + +static int8_t last_type; +static int8_t last_spare; + +static void rds_decode(char *dst, const char *src); + +static void rds_pi_reset() +{ +	rds_pi = 0; +	rds_pi_notified = false; +} + +static void rds_pi_maybe_notify() +{ +	if (!rds_pi_notified) { +		server_notify_pi(rds_pi); +		rds_pi_notified = true; +	} +} + +static void rds_ps_reset() +{ +	memset(rds_ps, '\0', sizeof(rds_ps)); +	rds_ps_segs = 0; +	rds_ps_notified = false; +} + +static void rds_ps_add(int seg, uint8_t msb, uint8_t lsb) +{ +	g_return_if_fail(seg >= 0 && seg < 4); +	rds_ps_segs |= 1 << seg; +	rds_ps[2*seg+0] = msb; +	rds_ps[2*seg+1] = lsb; +} + +static bool rds_ps_complete() +{ +	return (rds_ps_segs & 0xf) == 0xf; +} + + +static void rds_ps_maybe_notify() +{ +	if (!rds_ps_notified && rds_ps_complete()) { +		// Each RDS char can be up to two UTF-8 bytes. +		// 6 of extra safety margin (max size of UTF-8 char). +		char decoded_ps[(RDS_PS_SIZE * 2) + 6]; +		rds_decode(decoded_ps, rds_ps); +		server_notify_ps(decoded_ps); +		rds_ps_notified = true; +	} +} + +static void rds_rt_reset(int new_ab) +{ +	memset(rds_rt, '\0', sizeof(rds_rt)); +	rds_rt_segs = 0; +	rds_rt_ab = new_ab; +	rds_rt_notified = false; +} + +static void rds_rt_add(bool is_type_2b, bool is_block_d, int ab, int seg, +					   uint8_t msb, uint8_t lsb) +{ +	if (ab != rds_rt_ab) { +		rds_rt_reset(ab); +	} +	if (is_type_2b) { +		g_warn_if_fail(is_block_d); +		rds_rt[RDS_RT_2B_SIZE] = '\r'; // Type2B RadioText is only 32bytes +		rds_rt_segs |= 0xffff0000u; // Mark the upper segments as valid. +	} else { +		seg = 2*seg + (is_block_d ? 1 : 0); +	} + +	g_return_if_fail(seg >= 0 && seg < (RDS_RT_MAX_SIZE/2)); + +	rds_rt_segs |= 1 << seg; +	rds_rt[2*seg+0] = msb; +	rds_rt[2*seg+1] = lsb; +} + +static bool rds_rt_complete() +{ +	char *cr = strchr(rds_rt, '\r'); +	if (cr) { +		/* Only complete if all segments received up to the one with \r. */ +		int index_of_cr = cr - rds_rt; +		int seg_of_cr = index_of_cr / 2; +		uint32_t mask = (1 << (seg_of_cr + 1)) - 1; +		return (rds_rt_segs & mask) == mask; +	} else { +		/* Otherwise, only complete if all segments received. */ +		return rds_rt_segs == 0xffffffffu; +	} +} + +static void rds_rt_maybe_notify() +{ +	if (!rds_rt_notified && rds_rt_complete()) { +		// Each RDS char can be up to two UTF-8 bytes. +		char decoded_rt[(RDS_RT_MAX_SIZE * 2) + 6]; +		rds_decode(decoded_rt, rds_rt); +		server_notify_rt(decoded_rt); +		rds_rt_notified = true; +	} +} +static gboolean rds_callback(GIOChannel *source, GIOCondition condition, +	gpointer data) +{ +	struct v4l2_rds_data rds; +	int res = read(tuner_fd, &rds, sizeof(rds)); +	if (res < sizeof(rds)) { +		g_warning("Failed to read RDS information"); +		return TRUE; // Not fatal +	} + +	int block; +	bool error; + +#if CRAPPY_WL1273_RDS +	block = rds.block & WL1273_RDS_BLOCK_MASK; +	// The following is, in a "more" readable way, swapping 3 and 4. +	switch (block) { +		case WL1273_RDS_BLOCK_C_ALT: +			block = V4L2_RDS_BLOCK_C_ALT; +			break; +		case WL1273_RDS_BLOCK_D: +			block = V4L2_RDS_BLOCK_D; +			break; +	} +	error = rds.block & WL1273_RDS_UNCORRECTABLE_ERROR; +#else +	block = rds.block & V4L2_RDS_BLOCK_MSK; +	error = rds.block & V4L2_RDS_BLOCK_ERROR; +#endif + +	if (error) { +		g_debug("Incorrectable error in RDS block"); +		return TRUE; // Skip block +	} + +	switch (block) { +	case V4L2_RDS_BLOCK_A: +		rds_pi = (rds.msb << 8) | rds.lsb; +		rds_pi_maybe_notify(); +		break; +	case V4L2_RDS_BLOCK_B: +		last_type = (rds.msb >> 3) & 0x1f; +		last_spare = rds.lsb & 0x1f; + +		rds_tp = (rds.msb >> 2) & 1; +		rds_pty = ((rds.msb << 3) & 0x18) | ((rds.lsb >> 5) & 0x7); +		break; +	case V4L2_RDS_BLOCK_C: +		if (RDS_TYPE_IS_B(last_type)) return TRUE; +		// Desynchronization? Skip block + +		switch (last_type) { +		case RDS_TYPE_0A: +			// Alternative frequency +			// TODO +			break; +		case RDS_TYPE_2A: +			// RadioText +			rds_rt_add(false, false, +					   (last_spare & 0x10) >> 4, last_spare & 0x0f, +					   rds.msb, rds.lsb); +			rds_rt_maybe_notify(); +			break; +		} + +		break; +	case V4L2_RDS_BLOCK_C_ALT: +		if (!RDS_TYPE_IS_B(last_type)) return TRUE; + +		switch (last_type) { +		case RDS_TYPE_0B: +			// PI +			// TODO: Compare with one received in A? +			break; +		} +		break; +	case V4L2_RDS_BLOCK_D: +		switch (last_type) { +		case RDS_TYPE_0A: +		case RDS_TYPE_0B: +			// Program Service Name +			rds_ps_add(last_spare & 0x03, rds.msb, rds.lsb); +			rds_ps_maybe_notify(); +			break; +		case RDS_TYPE_2A: +		case RDS_TYPE_2B: +			// RadioText +			rds_rt_add(RDS_TYPE_IS_B(last_type), true, +					   (last_spare & 0x10) >> 4, last_spare & 0x0f, +					   rds.msb, rds.lsb); +			rds_rt_maybe_notify(); +			break; +		} +		break; +	default: +		break; +	} + +	return TRUE; +} + +bool configure_rds(bool on) +{ +	rds_reset(); + +	if (on) { +		g_return_val_if_fail(tuner_fd != -1, false); +		GIOChannel *channel = g_io_channel_unix_new(tuner_fd); +		rds_watch = g_io_add_watch(channel, G_IO_IN, rds_callback, NULL); +		g_io_channel_unref(channel); +	} else { +		if (rds_watch) { +			g_source_remove(rds_watch); +			rds_watch = 0; +		} +	} + +	return true; +} + +void rds_reset() +{ +	last_type = -1; +	last_spare = -1; +	rds_pi_reset(); +	rds_tp = false; +	rds_pty = 0; +	rds_ps_reset(); +	rds_rt_reset(-1); +} + +unsigned short rds_get_pi() +{ +	return rds_pi; +} + +unsigned char rds_get_pty() +{ +	return rds_pty; +} + +const char * rds_get_pty_text() +{ +	switch (rds_pty) { +	case 0: +	default: +		return "None"; +	case 1: +		return "News"; +	case 2: +		return "Current affairs"; +	case 3: +		return "Information"; +	case 4: +		return "Sport"; +	case 5: +		return "Education"; +	case 6: +		return "Drama"; +	case 7: +		return "Culture"; +	case 8: +		return "Science"; +	case 9: +		return "Varied"; +	case 10: +		return "Pop music"; +	case 11: +		return "Rock music"; +	case 12: +		return "Easy listening music"; +	case 13: +		return "Light classical"; +	case 14: +		return "Serious classical"; +	case 15: +		return "Other music"; +	case 16: +		return "Weather"; +	case 17: +		return "Finance"; +	case 18: +		return "Children's programmes"; +	case 19: +		return "Social affairs"; +	case 20: +		return "Religion"; +	case 21: +		return "Phone In"; +	case 22: +		return "Travel"; +	case 23: +		return "Leisure"; +	case 24: +		return "Jazz music"; +	case 25: +		return "Country music"; +	case 26: +		return "National music"; +	case 27: +		return "Oldies music"; +	case 28: +		return "Folk music"; +	case 29: +		return "Documentary"; +	case 30: +		return "Alarm test"; +	case 31: +		return "Alarm"; +	} +} + +/* This table comes from pyFMRadio source code. */ +static const gunichar rds_charset_table[256] = { +	0,	/* 0 */ +	0,	/* 1 */ +	0,	/* 2 */ +	0,	/* 3 */ +	0,	/* 4 */ +	0,	/* 5 */ +	0,	/* 6 */ +	0,	/* 7 */ +	0,	/* 8 */ +	0,	/* 9 */ +	10,	/* 10 */ +	0,	/* 11 */ +	0,	/* 12 */ +	0,	/* 13 */ +	0,	/* 14 */ +	0,	/* 15 */ +	0,	/* 16 */ +	0,	/* 17 */ +	0,	/* 18 */ +	0,	/* 19 */ +	0,	/* 20 */ +	0,	/* 21 */ +	0,	/* 22 */ +	0,	/* 23 */ +	0,	/* 24 */ +	0,	/* 25 */ +	0,	/* 26 */ +	0,	/* 27 */ +	0,	/* 28 */ +	0,	/* 29 */ +	0,	/* 30 */ +	0,	/* 31 */ +	32,	/* 32 */ +	33,	/* 33 */ +	34,	/* 34 */ +	35,	/* 35 */ +	164,	/* 36 */ +	37,	/* 37 */ +	38,	/* 38 */ +	39,	/* 39 */ +	40,	/* 40 */ +	41,	/* 41 */ +	42,	/* 42 */ +	43,	/* 43 */ +	44,	/* 44 */ +	45,	/* 45 */ +	46,	/* 46 */ +	47,	/* 47 */ +	48,	/* 48 */ +	49,	/* 49 */ +	50,	/* 50 */ +	51,	/* 51 */ +	52,	/* 52 */ +	53,	/* 53 */ +	54,	/* 54 */ +	55,	/* 55 */ +	56,	/* 56 */ +	57,	/* 57 */ +	58,	/* 58 */ +	59,	/* 59 */ +	60,	/* 60 */ +	61,	/* 61 */ +	62,	/* 62 */ +	63,	/* 63 */ +	64,	/* 64 */ +	65,	/* 65 */ +	66,	/* 66 */ +	67,	/* 67 */ +	68,	/* 68 */ +	69,	/* 69 */ +	70,	/* 70 */ +	71,	/* 71 */ +	72,	/* 72 */ +	73,	/* 73 */ +	74,	/* 74 */ +	75,	/* 75 */ +	76,	/* 76 */ +	77,	/* 77 */ +	78,	/* 78 */ +	79,	/* 79 */ +	80,	/* 80 */ +	81,	/* 81 */ +	82,	/* 82 */ +	83,	/* 83 */ +	84,	/* 84 */ +	85,	/* 85 */ +	86,	/* 86 */ +	87,	/* 87 */ +	88,	/* 88 */ +	89,	/* 89 */ +	90,	/* 90 */ +	91,	/* 91 */ +	92,	/* 92 */ +	93,	/* 93 */ +	32,	/* 94 */ +	32,	/* 95 */ +	32,	/* 96 */ +	97,	/* 97 */ +	98,	/* 98 */ +	99,	/* 99 */ +	100,	/* 100 */ +	101,	/* 101 */ +	102,	/* 102 */ +	103,	/* 103 */ +	104,	/* 104 */ +	105,	/* 105 */ +	106,	/* 106 */ +	107,	/* 107 */ +	108,	/* 108 */ +	109,	/* 109 */ +	110,	/* 110 */ +	111,	/* 111 */ +	112,	/* 112 */ +	113,	/* 113 */ +	114,	/* 114 */ +	115,	/* 115 */ +	116,	/* 116 */ +	117,	/* 117 */ +	118,	/* 118 */ +	119,	/* 119 */ +	120,	/* 120 */ +	121,	/* 121 */ +	122,	/* 122 */ +	123,	/* 123 */ +	124,	/* 124 */ +	125,	/* 125 */ +	32,	/* 126 */ +	32,	/* 127 */ +	225,	/* 128 */ +	224,	/* 129 */ +	233,	/* 130 */ +	232,	/* 131 */ +	237,	/* 132 */ +	236,	/* 133 */ +	243,	/* 134 */ +	242,	/* 135 */ +	250,	/* 136 */ +	249,	/* 137 */ +	209,	/* 138 */ +	199,	/* 139 */ +	350,	/* 140 */ +	223,	/* 141 */ +	161,	/* 142 */ +	306,	/* 143 */ +	226,	/* 144 */ +	228,	/* 145 */ +	234,	/* 146 */ +	235,	/* 147 */ +	238,	/* 148 */ +	239,	/* 149 */ +	244,	/* 150 */ +	246,	/* 151 */ +	251,	/* 152 */ +	252,	/* 153 */ +	241,	/* 154 */ +	231,	/* 155 */ +	351,	/* 156 */ +	287,	/* 157 */ +	63,	/* 158 */ +	307,	/* 159 */ +	170,	/* 160 */ +	945,	/* 161 */ +	169,	/* 162 */ +	8240,	/* 163 */ +	486,	/* 164 */ +	277,	/* 165 */ +	328,	/* 166 */ +	337,	/* 167 */ +	960,	/* 168 */ +	63,	/* 169 */ +	163,	/* 170 */ +	36,	/* 171 */ +	8592,	/* 172 */ +	8593,	/* 173 */ +	8594,	/* 174 */ +	8595,	/* 175 */ +	186,	/* 176 */ +	185,	/* 177 */ +	178,	/* 178 */ +	179,	/* 179 */ +	177,	/* 180 */ +	304,	/* 181 */ +	324,	/* 182 */ +	369,	/* 183 */ +	956,	/* 184 */ +	191,	/* 185 */ +	247,	/* 186 */ +	176,	/* 187 */ +	188,	/* 188 */ +	189,	/* 189 */ +	190,	/* 190 */ +	167,	/* 191 */ +	193,	/* 192 */ +	192,	/* 193 */ +	201,	/* 194 */ +	200,	/* 195 */ +	205,	/* 196 */ +	204,	/* 197 */ +	211,	/* 198 */ +	210,	/* 199 */ +	218,	/* 200 */ +	217,	/* 201 */ +	344,	/* 202 */ +	268,	/* 203 */ +	352,	/* 204 */ +	381,	/* 205 */ +	272,	/* 206 */ +	317,	/* 207 */ +	194,	/* 208 */ +	196,	/* 209 */ +	202,	/* 210 */ +	203,	/* 211 */ +	206,	/* 212 */ +	207,	/* 213 */ +	212,	/* 214 */ +	214,	/* 215 */ +	219,	/* 216 */ +	220,	/* 217 */ +	345,	/* 218 */ +	269,	/* 219 */ +	353,	/* 220 */ +	382,	/* 221 */ +	271,	/* 222 */ +	318,	/* 223 */ +	195,	/* 224 */ +	197,	/* 225 */ +	198,	/* 226 */ +	338,	/* 227 */ +	375,	/* 228 */ +	221,	/* 229 */ +	213,	/* 230 */ +	216,	/* 231 */ +	254,	/* 232 */ +	330,	/* 233 */ +	340,	/* 234 */ +	262,	/* 235 */ +	346,	/* 236 */ +	377,	/* 237 */ +	63,	/* 238 */ +	240,	/* 239 */ +	227,	/* 240 */ +	229,	/* 241 */ +	230,	/* 242 */ +	339,	/* 243 */ +	373,	/* 244 */ +	253,	/* 245 */ +	245,	/* 246 */ +	248,	/* 247 */ +	254,	/* 248 */ +	331,	/* 249 */ +	341,	/* 250 */ +	263,	/* 251 */ +	347,	/* 252 */ +	378,	/* 253 */ +	63,	/* 254 */ +	32,	/* 255 */ +}; + +static void rds_decode(char *dst, const char *src) +{ +	guchar *s = (guchar*) src; +	gchar *d = (gchar*) dst; +	while (*s) { +		gunichar unichar = rds_charset_table[*s]; +		gint bytes = g_unichar_to_utf8(unichar, d); +		g_warn_if_fail(bytes <= 2); +		if (unichar == '\0') return; +		s++; +		d += bytes; +	} +	*d = '\0'; +} diff --git a/server.c b/server.c new file mode 100644 index 0000000..442b63b --- /dev/null +++ b/server.c @@ -0,0 +1,542 @@ +/* + *  fmrxd - a daemon to enable and multiplex access to the N950/N9 radio tuner + *  Copyright (C) 2011 Javier S. Pedro <maemo@javispedro.com> + * + *  This program is free software; you can redistribute it and/or modify + *  it under the terms of the GNU General Public License as published by + *  the Free Software Foundation; either version 2 of the License, or + *  (at your option) any later version. + + *  This program is distributed in the hope that it will be useful, + *  but WITHOUT ANY WARRANTY; without even the implied warranty of + *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + *  GNU General Public License for more details. + + *  You should have received a copy of the GNU General Public License along + *  with this program; if not, write to the Free Software Foundation, Inc., + *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <dbus/dbus.h> +#include <dbus/dbus-glib-lowlevel.h> + +#include "fmrxd.h" + +typedef struct Client { +	int fd; +	GIOChannel *channel; +	guint out_watch; +	guint err_watch; +	unsigned int read_p; +} Client; + +static DBusConnection *bus; +static GList *clients; +static guint num_clients; + +#if SERVER_ON_DEMAND +static guint server_stop_source; +#endif + +struct { +	char data[RING_BUFFER_SIZE]; +	unsigned int write_p; +} rbuffer; + +static const char * introspect_data = { +	"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n" +	"\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n" +	"<node>\n" +	"  <interface name=\"org.freedesktop.DBus.Introspectable\">\n" +	"   <method name=\"Introspect\">\n" +	"    <arg type=\"s\" name=\"data\" direction=\"out\" />\n" +	"   </method>\n" +	"  </interface>\n" +	"  <interface name=\"" BUS_INTERFACE "\">\n" +	"   <method name=\"Connect\">\n" +	"     <arg type=\"h\" name=\"pipe\" direction=\"out\" />\n" +	"   </method>\n" +	"   <method name=\"Tune\">\n" +	"     <arg type=\"d\" name=\"frequency\" direction=\"in\" />\n" +	"   </method>\n" +	"   <method name=\"SearchForward\">\n" +	"   </method>\n" +	"   <method name=\"SearchBackward\">\n" +	"   </method>\n" +	"   <signal name=\"Tuned\">\n" +	"     <arg type=\"d\" name=\"frequency\" />\n" +	"   </signal>\n" +	"   <signal name=\"Stopped\">\n" +	"   </signal>\n" +	"   <signal name=\"PiReceived\">\n" +	"     <arg type=\"q\" name=\"pi\" />\n" +	"   </signal>\n" +	"   <signal name=\"PsReceived\">\n" +	"     <arg type=\"s\" name=\"ps\" />\n" +	"   </signal>\n" +	"   <signal name=\"RtReceived\">\n" +	"     <arg type=\"s\" name=\"rt\" />\n" +	"   </signal>\n" +	"  </interface>\n" +	"</node>\n" +}; + +static void server_kill_client(Client *c); + +static gboolean client_out_callback(GIOChannel *source, GIOCondition condition, +	gpointer data); +static gboolean client_err_callback(GIOChannel *source, GIOCondition condition, +	gpointer data); + +static DBusHandlerResult handle_introspect(DBusConnection *conn, DBusMessage *m) +{ +	DBusMessage *reply = dbus_message_new_method_return(m); +	dbus_message_append_args(reply, DBUS_TYPE_STRING, &introspect_data, DBUS_TYPE_INVALID); +	dbus_connection_send(conn, reply, NULL); +	dbus_message_unref(reply); + +	return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult handle_connect(DBusConnection *conn, DBusMessage *m) +{ +	int fd = server_new_client(); + +	if (fd == -1) { +		g_warning("Error while spawning a new pipe"); +		return DBUS_HANDLER_RESULT_NEED_MEMORY; +	} + +	DBusMessage *reply = dbus_message_new_method_return(m); +	dbus_message_append_args(reply, DBUS_TYPE_UNIX_FD, &fd, DBUS_TYPE_INVALID); +	dbus_connection_send(conn, reply, NULL); +	dbus_message_unref(reply); + +	close(fd); + +	return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult handle_tune(DBusConnection *conn, DBusMessage *m) +{ +	double freq; +	DBusError err; +	DBusMessage *reply; +	dbus_bool_t ret; + +	dbus_error_init(&err); +	ret = dbus_message_get_args(m, &err, DBUS_TYPE_DOUBLE, &freq, DBUS_TYPE_INVALID); +	if (!ret) { +		reply = dbus_message_new_error(m, err.name, err.message); +		dbus_connection_send(conn, reply, NULL); +		dbus_message_unref(reply); +		return DBUS_HANDLER_RESULT_HANDLED; +	} + +	if (!tuner_set_frequency(freq)) { +		reply = dbus_message_new_error(m, BUS_INTERFACE ".TunerError", +			"Failed to tune to the specified frequency"); +	} else { +		reply = dbus_message_new_method_return(m); +	} + +	dbus_connection_send(conn, reply, NULL); +	dbus_message_unref(reply); +	return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult handle_search(DBusConnection *conn, DBusMessage *m, bool forward) +{ +	DBusMessage *reply; + +	if (!tuner_search(forward)) { +		reply = dbus_message_new_error(m, BUS_INTERFACE ".TunerError", +			"Failed to start a search"); +	} else { +		reply = dbus_message_new_method_return(m); +	} + +	dbus_connection_send(conn, reply, NULL); +	dbus_message_unref(reply); +	return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult on_message(DBusConnection *connection, DBusMessage *message, void *user_data) +{ +	if (dbus_message_is_method_call(message, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) { +		return handle_introspect(connection, message); +	} else if (dbus_message_is_method_call(message, BUS_INTERFACE, "Connect")) { +		return handle_connect(connection, message); +	} else if (dbus_message_is_method_call(message, BUS_INTERFACE, "Tune")) { +		return handle_tune(connection, message); +	} else if (dbus_message_is_method_call(message, BUS_INTERFACE, "SearchForward")) { +		return handle_search(connection, message, true); +	} else if (dbus_message_is_method_call(message, BUS_INTERFACE, "SearchBackward")) { +		return handle_search(connection, message, false); +	} + +	return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static DBusObjectPathVTable bus_vtable = { +	.message_function = on_message +}; + +static inline char * client_data_p(Client *c) +{ +	return &rbuffer.data[c->read_p]; +} + +// This does not return all available data, but only +// max contiguous segment. This is by design. +static inline size_t client_data_avail(Client *c) +{ +	if (rbuffer.write_p >= c->read_p) { +		return rbuffer.write_p - c->read_p; +	} else { +		return RING_BUFFER_SIZE - c->read_p; +	} +} + +static inline void client_data_forget(Client *c, size_t bytes) +{ +	c->read_p = (c->read_p + bytes) % RING_BUFFER_SIZE; +} + +static inline void client_watch(Client *c) +{ +	g_return_if_fail(c->out_watch == 0); +	c->out_watch = g_io_add_watch(c->channel, G_IO_OUT, client_out_callback, c); +} + +static inline void client_unwatch(Client *c) +{ +	g_return_if_fail(c->out_watch != 0); +	g_source_remove(c->out_watch); +	c->out_watch = 0; +} + +static inline bool client_watched(const Client *c) +{ +	return c->out_watch != 0; +} + +static bool client_try_send(Client *c) +{ +	size_t avail = client_data_avail(c); +	char *p = client_data_p(c); +	ssize_t written; + +	written = write(c->fd, p, avail); + +	if (written == -1) { +		switch (errno) { +		case EAGAIN: +			// Client turned out to be busy +			//  just assume nothing was written. +			written = 0; +			break; +		case EPIPE: +			// Broken pipe; kill client. +			g_message("Client pipe broken, closing client"); +			server_kill_client(c); +			return FALSE; +		default: +			g_warning("Failed to send buffer to client in %d: %s", +				c->fd, strerror(errno)); +			server_kill_client(c); +			return FALSE; +		} +	} else if (written > 0) { +		client_data_forget(c, written); +	} + +	// Determine if we need to start/stop watching the client +	if (client_watched(c) && client_data_avail(c) == 0) { +		// No more data to be sent. +		client_unwatch(c); +	} else if (!client_watched(c) && client_data_avail(c) > 0) { +		// Client is not watched but there is still data to be sent +		client_watch(c); +	} + +	return TRUE; +} + +static gboolean client_out_callback(GIOChannel *source, GIOCondition condition, +	gpointer data) +{ +	Client *c = (Client*) data; + +	if (condition & G_IO_OUT) { +		return client_try_send(c); +	} + +	return TRUE; +} + +static gboolean client_err_callback(GIOChannel *source, GIOCondition condition, +	gpointer data) +{ +	Client *c = (Client*) data; + +	if (condition & G_IO_HUP) { +		g_message("Client pipe closed, closing client"); +		server_kill_client(c); +		return FALSE; +	} +	if (condition & G_IO_ERR) { +		g_message("Client pipe broken, closing client"); +		server_kill_client(c); +		return FALSE; +	} + +	return TRUE; +} + +int server_start() +{ +	DBusError err; +	int ret; +	dbus_error_init(&err); + +	clients = NULL; +	num_clients = 0; + +	rbuffer.write_p = 0; + +	bus = dbus_bus_get(DBUS_BUS_SESSION, &err); +	if (dbus_error_is_set(&err)) { +		g_critical("Cannot connect to the system message bus: %s", err.message); +		return -1; +	} +	g_assert(bus != NULL); + +	ret = dbus_bus_request_name(bus, BUS_NAME, DBUS_NAME_FLAG_REPLACE_EXISTING, &err); +	if (dbus_error_is_set(&err)) { +		g_critical("Cannot claim ownership of my bus name: %s", err.message); +		return -1; +	} +	g_assert(ret == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER); + +	ret = dbus_connection_register_object_path(bus, BUS_PATH, &bus_vtable, NULL); +	if (!ret) { +		g_critical("Cannot register fmrxd object in bus"); +		return -1; +	} + +	dbus_connection_setup_with_g_main(bus, g_main_loop_get_context(main_loop)); + +#if SERVER_ON_DEMAND +	// Set up the timer for inactivity, in case we do not get any clients. +	server_queue_stop(); +#endif + +	return 0; +} + +void server_stop() +{ +	g_warn_if_fail(num_clients == 0); +	dbus_connection_unregister_object_path(bus, BUS_PATH); +	dbus_bus_release_name(bus, BUS_NAME, NULL); +} + +#if SERVER_ON_DEMAND +static gboolean server_stop_func(gpointer data) +{ +	g_message("Stopping service because of inactivity"); +	server_stop(); +	g_main_loop_quit(main_loop); +	return FALSE; +} + +void server_queue_stop() +{ +	if (!server_stop_source) { +		server_stop_source = g_timeout_add_seconds(SERVER_LINGER_TIME, +			server_stop_func, NULL); +	} +} +#endif + +int server_new_client() +{ +	int fds[2]; +	int ret = pipe(fds); +	if (ret < 0) { +		g_warning("Could not create a pipe"); +		return -1; +	} + +	Client* c = g_slice_new(Client); +	c->fd = fds[1]; +	c->channel = g_io_channel_unix_new(c->fd); +	c->out_watch = 0; +	c->err_watch = g_io_add_watch(c->channel, G_IO_ERR | G_IO_HUP, +								  client_err_callback, c); +	c->read_p = rbuffer.write_p; + +	// Configure some flags of the pipe using GLib +	GIOFlags flags = g_io_channel_get_flags(c->channel); +	if (g_io_channel_set_flags(c->channel, flags | G_IO_FLAG_NONBLOCK, NULL) +			!= G_IO_STATUS_NORMAL) { +		g_warning("Failed to set non-blocking flag for pipe fd"); +	} +	g_io_channel_set_encoding(c->channel, NULL, NULL); +	g_io_channel_set_buffered(c->channel, FALSE); + +	g_debug("New pipe, write fd = %d", fds[1]); + +	// Add client to list +	clients = g_list_append(clients, c); + +	if (num_clients == 0) { +		// First client, start tuner. +		radio_queue_start(); +#if SERVER_ON_DEMAND +		if (server_stop_source) { +			g_source_remove(server_stop_source); +			server_stop_source = 0; +		} +#endif +	} +	num_clients++; + +	// Return the other end of the pipe +	return fds[0]; +} + +static void server_kill_client(Client *c) +{ +	g_source_remove(c->err_watch); +	if (c->out_watch) g_source_remove(c->out_watch); + +	clients = g_list_remove(clients, c); + +	g_io_channel_shutdown(c->channel, TRUE, NULL); +	g_io_channel_unref(c->channel); + +	g_slice_free(Client, c); + +	if (num_clients == 1) { +		// Last client leaving, stop tuner. +		radio_queue_stop(); +#if SERVER_ON_DEMAND +		server_queue_stop(); +#endif +	} +	num_clients--; +} + +size_t server_get_buffer(size_t size, void **buffer) +{ +	g_return_val_if_fail(buffer != NULL, 0); +	g_return_val_if_fail(size > 0, 0); + +	*buffer = &rbuffer.data[rbuffer.write_p]; + +	size_t avail_size = RING_BUFFER_SIZE - rbuffer.write_p; +	if (size > avail_size) { +		return avail_size; +	} else { +		return size; +	} +} + +static void overflow_check_func(gpointer data, gpointer user_data) +{ +	Client * c = (Client*) data; +	unsigned int new_write_p = *(unsigned int*)user_data; +	unsigned int old_write_p = rbuffer.write_p; +	unsigned int data_lost = 0; + +	if (new_write_p < rbuffer.write_p) { +		// write_p wraparound: Overflow happens if the read_p was +		//  positioned right after old write_p. +		g_warn_if_fail(new_write_p == 0); // Valid simplification here +		if (c->read_p > old_write_p) { +			data_lost = c->read_p - old_write_p; +			g_warn_if_fail(data_lost < RING_BUFFER_SIZE); +		} +	} else { +		if (c->read_p > old_write_p && c->read_p <= new_write_p) { +			data_lost = new_write_p - c->read_p; +			g_warn_if_fail(data_lost < RING_BUFFER_SIZE); +		} +	} + +	if (data_lost > 0) { +		g_message("Overflow detected (%u bytes), glitch!", data_lost); +		c->read_p = (new_write_p) % RING_BUFFER_SIZE; +	} +} + +static void commit_buffer_func(gpointer data, gpointer user_data) +{ +	Client * c = (Client*) data; + +	client_try_send(c); +} + +void server_commit_buffer(size_t size) +{ +	g_return_if_fail(size > 0); +	g_warn_if_fail(size < RING_BUFFER_SIZE); + +	unsigned int new_write_p = (rbuffer.write_p + size) % RING_BUFFER_SIZE; + +	g_list_foreach(clients, overflow_check_func, &new_write_p); + +	rbuffer.write_p = new_write_p; + +	g_list_foreach(clients, commit_buffer_func, NULL); +} + +void server_notify_tuned(double mhz) +{ +	DBusMessage *m = dbus_message_new_signal(BUS_PATH, BUS_INTERFACE, "Tuned"); +	g_return_if_fail(m != NULL); +	dbus_message_append_args(m, DBUS_TYPE_DOUBLE, &mhz, DBUS_TYPE_INVALID); +	dbus_connection_send(bus, m, NULL); +	dbus_message_unref(m); +} + +void server_notify_stopped() +{ +	DBusMessage *m = dbus_message_new_signal(BUS_PATH, BUS_INTERFACE, "Stopped"); +	g_return_if_fail(m != NULL); +	dbus_connection_send(bus, m, NULL); +	dbus_message_unref(m); +} + +void server_notify_pi(uint16_t pi) +{ +	DBusMessage *m = dbus_message_new_signal(BUS_PATH, BUS_INTERFACE, "PiChanged"); +	g_return_if_fail(m != NULL); +	dbus_message_append_args(m, DBUS_TYPE_UINT16, &pi, DBUS_TYPE_INVALID); +	dbus_connection_send(bus, m, NULL); +	dbus_message_unref(m); +} + +void server_notify_ps(const char *ps) +{ +	DBusMessage *m = dbus_message_new_signal(BUS_PATH, BUS_INTERFACE, "PsChanged"); +	g_return_if_fail(m != NULL); +	dbus_message_append_args(m, DBUS_TYPE_STRING, &ps, DBUS_TYPE_INVALID); +	dbus_connection_send(bus, m, NULL); +	dbus_message_unref(m); +} + +void server_notify_rt(const char *rt) +{ +	DBusMessage *m = dbus_message_new_signal(BUS_PATH, BUS_INTERFACE, "RtChanged"); +	g_return_if_fail(m != NULL); +	dbus_message_append_args(m, DBUS_TYPE_STRING, &rt, DBUS_TYPE_INVALID); +	dbus_connection_send(bus, m, NULL); +	dbus_message_unref(m); +} @@ -0,0 +1,208 @@ +/* + *  fmrxd - a daemon to enable and multiplex access to the N950/N9 radio tuner + *  Copyright (C) 2011 Javier S. Pedro <maemo@javispedro.com> + * + *  This program is free software; you can redistribute it and/or modify + *  it under the terms of the GNU General Public License as published by + *  the Free Software Foundation; either version 2 of the License, or + *  (at your option) any later version. + + *  This program is distributed in the hope that it will be useful, + *  but WITHOUT ANY WARRANTY; without even the implied warranty of + *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + *  GNU General Public License for more details. + + *  You should have received a copy of the GNU General Public License along + *  with this program; if not, write to the Free Software Foundation, Inc., + *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include <stdbool.h> +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/ioctl.h> +#include <glib.h> +#include <linux/videodev2.h> + +#include "fmrxd.h" + +int tuner_fd = -1; + +static bool tuner_precise; +static unsigned long tuner_min, tuner_max; + +static unsigned long tuner_freq = 0; + +static inline unsigned long v4l_to_hz(unsigned f) +{ +	if (tuner_precise) { +		/* f * 62.5 */ +		return (f * 125UL) / 2; +	} else { +		return f * 62500UL; +	} +} + +static inline unsigned hz_to_v4l(unsigned long f) +{ +	if (tuner_precise) { +		/* f / 62.5 */ +		return (f * 2UL) / 125; +	} else { +		return f / 62500; +	} +} + +static inline unsigned long mhz_to_hz(double f) +{ +	return f * 1000000.0; +} + +static inline double hz_to_mhz(unsigned long f) +{ +	return f / 1000000.0; +} + +static bool tuner_tune(unsigned long hz) +{ +	struct v4l2_frequency t_freq = { +		.tuner = TUNER_DEVICE_ID, +		.type = V4L2_TUNER_RADIO, +		.frequency = hz_to_v4l(hz) +	}; + +	g_return_val_if_fail(tuner_fd != -1, false); + +	if (ioctl(tuner_fd, VIDIOC_S_FREQUENCY, &t_freq) < 0) { +		g_warning("Failed to tune: %s", strerror(errno)); +		return false; +	} + +	return true; +} + +static unsigned long tuner_get_tuned_freq() +{ +	struct v4l2_frequency t_freq = { +		.tuner = TUNER_DEVICE_ID +	}; + +	g_return_val_if_fail(tuner_fd != -1, 0); + +	if (ioctl(tuner_fd, VIDIOC_G_FREQUENCY, &t_freq) < 0) { +		g_warning("Failed to get freq: %s", strerror(errno)); +		return 0; +	} + +	return v4l_to_hz(t_freq.frequency); +} + +static uint16_t tuner_get_signal() +{ +	struct v4l2_tuner t_tuner = { +		.index = TUNER_DEVICE_ID +	}; + +	g_return_val_if_fail(tuner_fd != -1, 0); + +	if (ioctl(tuner_fd, VIDIOC_G_TUNER, &t_tuner) < 0) { +		g_warning("Failed to get signal level: %s", strerror(errno)); +		return 0; +	} + +	return t_tuner.signal; +} + +bool configure_tuner(bool on) +{ +	if (on) { +		struct v4l2_tuner tuner = { 0 }; +		int res; + +		tuner_fd = open(TUNER_DEVICE, O_RDONLY); +		if (tuner_fd == -1) { +			g_critical("Couldn't open V4L2 tuner"); +			return false; +		} + +		tuner.index = 0; +		res = ioctl(tuner_fd, VIDIOC_G_TUNER, &tuner); +		if (res < 0) { +			g_critical("Couldn't get V4L2 tuner information"); +			return false; +		} + +		if (tuner.type != V4L2_TUNER_RADIO) { +			g_critical("Not a radio tuner\n"); +			return false; +		} + +		tuner_precise = (tuner.capability & V4L2_TUNER_CAP_LOW) ? +								TRUE : FALSE; +		tuner_min = v4l_to_hz(tuner.rangelow); +		tuner_max = v4l_to_hz(tuner.rangelow); + +		if (tuner_freq >= tuner_min && tuner_freq <= tuner_max && +				tuner_tune(tuner_freq)) { +			// All is well, we are on air! +		} else { +			// Use whatever frequency the tuner is currently using +			tuner_freq = tuner_get_tuned_freq(); +		} + +		server_notify_tuned(hz_to_mhz(tuner_freq)); + +		return true; +	} else { +		if (tuner_fd != -1) { +			close(tuner_fd); +			tuner_fd = -1; +		} +		return true; +	} +} + +bool tuner_set_frequency(double mhz) +{ +	unsigned long hz = mhz_to_hz(mhz); + +	if (tuner_tune(hz)) { +		g_message("Tuned to %.1f Mhz", mhz); +		server_notify_tuned(mhz); +		tuner_freq = hz; +		rds_reset(); +		return true; +	} + +	return false; +} + +bool tuner_search(bool forward) +{ +	struct v4l2_hw_freq_seek t_freq_seek = { +		.tuner = TUNER_DEVICE_ID, +		.type = V4L2_TUNER_RADIO, +		.seek_upward = forward +	}; + +	g_return_val_if_fail(tuner_fd != -1, false); + +	if (ioctl(tuner_fd, VIDIOC_S_HW_FREQ_SEEK, &t_freq_seek) < 0) { +		g_warning("Failed to start seek: %s", strerror(errno)); +		return false; +	} + +	// Search complete, get new frequency +	unsigned long hz = tuner_get_tuned_freq(); +	if (!hz) { +		return false; +	} + +	// Got new frequency, fire signals. +	tuner_freq = hz; +	server_notify_tuned(hz_to_mhz(hz)); +	rds_reset(); +	return true; +}  | 
