From 352dad23c7847d234e11c1034e1354fbd9a8349a Mon Sep 17 00:00:00 2001 From: "Javier S. Pedro" Date: Sat, 31 Dec 2011 17:50:06 +0100 Subject: initial import --- Makefile | 44 ++++ bt.c | 119 +++++++++ capture.c | 259 ++++++++++++++++++++ debian/changelog | 5 + debian/compat | 1 + debian/control | 24 ++ debian/copyright | 23 ++ debian/docs | 0 debian/fmrxd.aegis | 15 ++ debian/fmrxd.install | 3 + debian/rules | 13 + fmrx-cat.c | 96 ++++++++ fmrx-ctl.c | 115 +++++++++ fmrxd.c | 58 +++++ fmrxd.h | 77 ++++++ fmrxd.service | 3 + radio.c | 97 ++++++++ rds.c | 665 +++++++++++++++++++++++++++++++++++++++++++++++++++ server.c | 542 +++++++++++++++++++++++++++++++++++++++++ tuner.c | 208 ++++++++++++++++ 20 files changed, 2367 insertions(+) create mode 100644 Makefile create mode 100644 bt.c create mode 100644 capture.c create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/docs create mode 100644 debian/fmrxd.aegis create mode 100644 debian/fmrxd.install create mode 100755 debian/rules create mode 100644 fmrx-cat.c create mode 100644 fmrx-ctl.c create mode 100644 fmrxd.c create mode 100644 fmrxd.h create mode 100644 fmrxd.service create mode 100644 radio.c create mode 100644 rds.c create mode 100644 server.c create mode 100644 tuner.c 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 + * + * 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 +#include +#include +#include +#include + +#include +#include +#include + +#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 + * + * 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 +#include +#include +#include + +#include "fmrxd.h" + +static snd_pcm_t *a_pcm = NULL; +static guint a_fd_watch = 0; + +/** Sets mixer switch to */ +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 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 +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 on Sat, 03 Sep 2011 19:47:50 +0200 + +Copyright: + + Javier S. Pedro + +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 . + diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..e69de29 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 @@ + + + + + + + + + + + + + + + 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 +#include +#include +#include +#include + +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 +#include +#include +#include +#include + +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 + * + * 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 +#include +#include + +#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 +#include +#include +#include + +#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 + * + * 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 +#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 + * + * 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 +#include +#include +#include +#include + +#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 + * + * 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 +#include +#include +#include +#include +#include + +#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 = { + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\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 + * + * 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 +#include +#include +#include +#include +#include +#include +#include + +#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; +} -- cgit v1.2.3