summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJavier S. Pedro <maemo@javispedro.com>2011-12-31 17:50:06 +0100
committerJavier S. Pedro <maemo@javispedro.com>2011-12-31 17:50:06 +0100
commit352dad23c7847d234e11c1034e1354fbd9a8349a (patch)
tree6caab2315dcb20882a05453412788578acc119e5
downloadfmrxd-352dad23c7847d234e11c1034e1354fbd9a8349a.tar.gz
fmrxd-352dad23c7847d234e11c1034e1354fbd9a8349a.zip
initial import
-rw-r--r--Makefile44
-rw-r--r--bt.c119
-rw-r--r--capture.c259
-rw-r--r--debian/changelog5
-rw-r--r--debian/compat1
-rw-r--r--debian/control24
-rw-r--r--debian/copyright23
-rw-r--r--debian/docs0
-rw-r--r--debian/fmrxd.aegis15
-rw-r--r--debian/fmrxd.install3
-rwxr-xr-xdebian/rules13
-rw-r--r--fmrx-cat.c96
-rw-r--r--fmrx-ctl.c115
-rw-r--r--fmrxd.c58
-rw-r--r--fmrxd.h77
-rw-r--r--fmrxd.service3
-rw-r--r--radio.c97
-rw-r--r--rds.c665
-rw-r--r--server.c542
-rw-r--r--tuner.c208
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
diff --git a/bt.c b/bt.c
new file mode 100644
index 0000000..b0642df
--- /dev/null
+++ b/bt.c
@@ -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;
+}
diff --git a/fmrxd.c b/fmrxd.c
new file mode 100644
index 0000000..747b31a
--- /dev/null
+++ b/fmrxd.c
@@ -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;
+}
diff --git a/fmrxd.h b/fmrxd.h
new file mode 100644
index 0000000..e0e3b22
--- /dev/null
+++ b/fmrxd.h
@@ -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
diff --git a/radio.c b/radio.c
new file mode 100644
index 0000000..bc0d5ea
--- /dev/null
+++ b/radio.c
@@ -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);
+ }
+}
diff --git a/rds.c b/rds.c
new file mode 100644
index 0000000..ed116a2
--- /dev/null
+++ b/rds.c
@@ -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);
+}
diff --git a/tuner.c b/tuner.c
new file mode 100644
index 0000000..eb0cda8
--- /dev/null
+++ b/tuner.c
@@ -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;
+}