summaryrefslogtreecommitdiff
path: root/rds.c
diff options
context:
space:
mode:
Diffstat (limited to 'rds.c')
-rw-r--r--rds.c665
1 files changed, 665 insertions, 0 deletions
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';
+}