summaryrefslogtreecommitdiffstats
path: root/src/radio_impl_rtlsdr.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/radio_impl_rtlsdr.c')
-rw-r--r--src/radio_impl_rtlsdr.c496
1 files changed, 496 insertions, 0 deletions
diff --git a/src/radio_impl_rtlsdr.c b/src/radio_impl_rtlsdr.c
new file mode 100644
index 0000000..d978605
--- /dev/null
+++ b/src/radio_impl_rtlsdr.c
@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2017,2018,2020 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <glib.h>
+
+#include "radio_impl.h"
+
+#define HELPER_NAME "rtl_fm_helper"
+#define HELPER_MAX PATH_MAX + 64
+
+#define HELPER_CMD_MAXLEN 64
+#define HELPER_RSP_MAXLEN 128
+
+// Structure to describe FM band plans, all values in Hz.
+typedef struct {
+ char *name;
+ uint32_t min;
+ uint32_t max;
+ uint32_t step;
+} fm_band_plan_t;
+
+static fm_band_plan_t known_fm_band_plans[5] = {
+ { .name = "US", .min = 87900000, .max = 107900000, .step = 200000 },
+ { .name = "JP", .min = 76000000, .max = 95000000, .step = 100000 },
+ { .name = "EU", .min = 87500000, .max = 108000000, .step = 50000 },
+ { .name = "ITU-1", .min = 87500000, .max = 108000000, .step = 50000 },
+ { .name = "ITU-2", .min = 87900000, .max = 107900000, .step = 50000 }
+};
+
+static unsigned int bandplan;
+static char *helper_output;
+static pid_t helper_pid;
+static int helper_in;
+static int helper_out;
+static pthread_mutex_t helper_mutex = PTHREAD_MUTEX_INITIALIZER;
+static bool present;
+static bool initialized;
+static bool active;
+static bool scanning;
+static uint32_t current_frequency;
+static radio_freq_callback_t freq_callback;
+static void *freq_callback_data;
+
+static uint32_t rtlsdr_get_min_frequency(radio_band_t band);
+static void rtlsdr_set_frequency(uint32_t frequency);
+static void rtlsdr_scan_stop(void);
+
+/*
+ * Bi-directional popen implementation
+ * Based on one of the versions given in:
+ *
+ * https://stackoverflow.com/questions/12778672/killing-process-that-has-been-created-with-popen2
+ */
+static pid_t popen2(char *command, int *in_fd, int *out_fd)
+{
+ int pin[2], pout[2];
+ pid_t pid;
+
+ if(out_fd != NULL) {
+ if(pipe(pin) != 0)
+ return -1;
+ }
+ if(in_fd != NULL) {
+ if (pipe(pout) != 0) {
+ if(out_fd != NULL) {
+ close(pin[0]);
+ close(pin[1]);
+ }
+ return -1;
+ }
+ }
+
+ pid = fork();
+ if(pid < 0) {
+ if(out_fd != NULL) {
+ close(pin[0]);
+ close(pin[1]);
+ }
+ if(in_fd != NULL) {
+ close(pout[0]);
+ close(pout[1]);
+ }
+ return pid;
+ }
+ if(pid == 0) {
+ if(out_fd != NULL) {
+ close(pin[1]);
+ dup2(pin[0], 0);
+ }
+ if(in_fd != NULL) {
+ close(pout[0]);
+ dup2(pout[1], 1);
+ }
+ execlp(command, command, NULL);
+ perror("Error:");
+ exit(1);
+ }
+ if(in_fd != NULL) {
+ close(pout[1]);
+ *in_fd = pout[0];
+ }
+ if(out_fd != NULL) {
+ close(pin[0]);
+ *out_fd = pin[1];
+ }
+ return pid;
+}
+
+static int rtlsdr_probe(void)
+{
+ const char *bindir = "/usr/sbin";
+ char *helper_path;
+
+ // Run helper to detect adapter
+ helper_path = malloc(HELPER_MAX);
+ if(!helper_path)
+ return -ENOMEM;
+ if(snprintf(helper_path, HELPER_MAX, "%s/%s --detect", bindir, HELPER_NAME) == HELPER_MAX) {
+ fprintf(stderr, "Could not create command for \"%s --detect\"", HELPER_NAME);
+ return -EINVAL;
+ }
+ if(system(helper_path) != 0) {
+ free(helper_path);
+ return -1;
+ }
+
+ present = true;
+ return 0;
+}
+
+static int rtlsdr_start_helper(void)
+{
+ const char *bindir = "/usr/sbin";
+ char *helper_path;
+ static bool helper_started = false;
+
+ if(!present || initialized)
+ return -1;
+
+ if(helper_started)
+ return 0;
+
+ if(helper_output) {
+ // Indicate desired output to helper
+ printf("Setting RADIO_OUTPUT=%s", helper_output);
+ setenv("RADIO_OUTPUT", helper_output, 1);
+ }
+
+ // Run helper
+ helper_path = malloc(HELPER_MAX);
+ if(!helper_path)
+ return -ENOMEM;
+ if(snprintf(helper_path, PATH_MAX, "%s/%s", bindir, HELPER_NAME) == PATH_MAX) {
+ fprintf(stderr, "Could not create path to %s", HELPER_NAME);
+ return -EINVAL;
+ }
+ helper_pid = popen2(helper_path, &helper_out, &helper_in);
+ if(helper_pid < 0) {
+ fprintf(stderr, "Could not run %s!", HELPER_NAME);
+ return -1;
+ }
+ printf("%s started", HELPER_NAME);
+ helper_started = true;
+ free(helper_path);
+
+ return 0;
+}
+
+static int rtlsdr_init(void)
+{
+ GKeyFile *conf_file;
+ char *value_str;
+ int rc;
+
+ if(!present)
+ return -1;
+
+ if(initialized)
+ return 0;
+
+ // Load settings from configuration file if it exists
+ conf_file = g_key_file_new();
+ if(conf_file &&
+ g_key_file_load_from_dirs(conf_file,
+ "AGL.conf",
+ (const gchar**) g_get_system_config_dirs(),
+ NULL,
+ G_KEY_FILE_KEEP_COMMENTS,
+ NULL) == TRUE) {
+
+ // Set band plan if it is specified
+ value_str = g_key_file_get_string(conf_file,
+ "radio",
+ "fmbandplan",
+ NULL);
+ if(value_str) {
+ unsigned int i;
+ for(i = 0;
+ i < sizeof(known_fm_band_plans) / sizeof(fm_band_plan_t);
+ i++) {
+ if(!strcasecmp(value_str, known_fm_band_plans[i].name)) {
+ bandplan = i;
+ break;
+ }
+ }
+ }
+ }
+
+ // Start off with minimum bandplan frequency
+ current_frequency = rtlsdr_get_min_frequency(RADIO_BAND_FM);
+ rc = rtlsdr_start_helper();
+ if(rc != 0) {
+ return rc;
+ }
+
+ initialized = true;
+ rtlsdr_set_frequency(current_frequency);
+
+ return 0;
+}
+
+static void rtlsdr_set_output(const char *output)
+{
+ // Save output for later use
+ free(helper_output);
+ helper_output = output ? strdup(output) : NULL;
+}
+
+static uint32_t rtlsdr_get_frequency(void)
+{
+ return current_frequency;
+}
+
+static void rtlsdr_set_frequency(uint32_t frequency)
+{
+ char cmd[HELPER_CMD_MAXLEN];
+ char output[HELPER_RSP_MAXLEN];
+ bool found = false;
+ ssize_t rc;
+ uint32_t n;
+
+ if(!initialized)
+ return;
+
+ if(frequency < known_fm_band_plans[bandplan].min ||
+ frequency > known_fm_band_plans[bandplan].max)
+ return;
+
+ if(scanning)
+ rtlsdr_scan_stop();
+
+ current_frequency = frequency;
+ snprintf(cmd, sizeof(cmd), "F=%u\n", frequency);
+ pthread_mutex_lock(&helper_mutex);
+ rc = write(helper_in, cmd, strlen(cmd));
+ if(rc < 0) {
+ pthread_mutex_unlock(&helper_mutex);
+ return;
+ }
+ while(!found) {
+ rc = read(helper_out, output, sizeof(output)-1);
+ if(rc <= 0)
+ break;
+ output[rc] = '\0';
+ if(output[0] == 'F') {
+ if(sscanf(output, "F=%u\n", &n) == 1) {
+ if(freq_callback)
+ freq_callback(n, freq_callback_data);
+ found = true;
+ }
+ }
+ }
+ pthread_mutex_unlock(&helper_mutex);
+}
+
+static void rtlsdr_set_frequency_callback(radio_freq_callback_t callback,
+ void *data)
+{
+ freq_callback = callback;
+ freq_callback_data = data;
+}
+
+static radio_band_t rtlsdr_get_band(void)
+{
+ // We only support FM
+ return RADIO_BAND_FM;
+}
+
+static void rtlsdr_set_band(radio_band_t band)
+{
+ // We only support FM, so do nothing
+}
+
+static int rtlsdr_band_supported(radio_band_t band)
+{
+ if(band == RADIO_BAND_FM)
+ return 1;
+ return 0;
+}
+
+static uint32_t rtlsdr_get_min_frequency(radio_band_t band)
+{
+ return known_fm_band_plans[bandplan].min;
+}
+
+static uint32_t rtlsdr_get_max_frequency(radio_band_t band)
+{
+ return known_fm_band_plans[bandplan].max;
+}
+
+static uint32_t rtlsdr_get_frequency_step(radio_band_t band)
+{
+ uint32_t ret = 0;
+
+ switch (band) {
+ case RADIO_BAND_AM:
+ ret = 1000; // 1 kHz
+ break;
+ case RADIO_BAND_FM:
+ ret = known_fm_band_plans[bandplan].step;
+ break;
+ default:
+ break;
+ }
+ return ret;
+}
+
+static void rtlsdr_start(void)
+{
+ ssize_t rc;
+ char cmd[HELPER_CMD_MAXLEN];
+
+ if(!initialized)
+ return;
+
+ if(active)
+ return;
+
+ snprintf(cmd, sizeof(cmd), "START\n");
+ pthread_mutex_lock(&helper_mutex);
+ rc = write(helper_in, cmd, strlen(cmd));
+ pthread_mutex_unlock(&helper_mutex);
+ if (rc < 0) {
+ fprintf(stderr, "Failed to ask \"%s\" to start", HELPER_NAME);
+ return;
+ }
+ active = true;
+}
+
+static void rtlsdr_stop(void)
+{
+ ssize_t rc;
+ if(!initialized)
+ return;
+
+ if (!active)
+ return;
+
+ char cmd[HELPER_CMD_MAXLEN];
+
+ snprintf(cmd, sizeof(cmd), "STOP\n");
+ pthread_mutex_lock(&helper_mutex);
+ rc = write(helper_in, cmd, strlen(cmd));
+ pthread_mutex_unlock(&helper_mutex);
+ if (rc < 0) {
+ fprintf(stderr, "Failed to ask \"%s\" to stop", HELPER_NAME);
+ return;
+ }
+ active = false;
+}
+
+static void rtlsdr_scan_start(radio_scan_direction_t direction,
+ radio_scan_callback_t callback,
+ void *data)
+{
+ ssize_t rc;
+ char cmd[HELPER_CMD_MAXLEN];
+ char output[HELPER_RSP_MAXLEN];
+ bool found = false;
+ uint32_t n;
+
+ if(!active || scanning)
+ return;
+
+ scanning = true;
+ snprintf(cmd,
+ sizeof(cmd),
+ "S=%s\n", direction == RADIO_SCAN_FORWARD ? "UP" : "DOWN");
+ pthread_mutex_lock(&helper_mutex);
+ if(!scanning) {
+ pthread_mutex_unlock(&helper_mutex);
+ return;
+ }
+ rc = write(helper_in, cmd, strlen(cmd));
+ pthread_mutex_unlock(&helper_mutex);
+ if(rc < 0)
+ return;
+ while(!found) {
+ pthread_mutex_lock(&helper_mutex);
+ if(!scanning) {
+ pthread_mutex_unlock(&helper_mutex);
+ break;
+ }
+ rc = read(helper_out, output, sizeof(output)-1);
+ pthread_mutex_unlock(&helper_mutex);
+ if(rc <= 0)
+ break;
+
+ output[rc] = '\0';
+ if(output[0] == 'F') {
+ if(sscanf(output, "F=%u\n", &n) == 1) {
+ current_frequency = n;
+ if(freq_callback)
+ freq_callback(n, freq_callback_data);
+ }
+ }
+ if(output[0] == 'S') {
+ if(sscanf(output, "S=%u\n", &n) == 1) {
+ if(callback)
+ callback(n, data);
+ found = true;
+ scanning = false;
+ }
+ }
+ }
+}
+
+static void rtlsdr_scan_stop(void)
+{
+ char cmd[HELPER_CMD_MAXLEN];
+ ssize_t rc;
+
+ snprintf(cmd, sizeof(cmd), "S=STOP\n");
+ pthread_mutex_lock(&helper_mutex);
+ scanning = false;
+ rc = write(helper_in, cmd, strlen(cmd));
+ pthread_mutex_unlock(&helper_mutex);
+ if (rc < 0)
+ fprintf(stderr, "Failed to ask \"%s\" to stop scan", HELPER_NAME);
+}
+
+static radio_stereo_mode_t rtlsdr_get_stereo_mode(void)
+{
+ // We only support stereo
+ return RADIO_MODE_STEREO;
+}
+
+static void rtlsdr_set_stereo_mode(radio_stereo_mode_t mode)
+{
+ // We only support stereo, so do nothing
+}
+
+radio_impl_ops_t rtlsdr_impl_ops = {
+ .name = "RTL-SDR USB adapter",
+ .probe = rtlsdr_probe,
+ .init = rtlsdr_init,
+ .set_output = rtlsdr_set_output,
+ .get_frequency = rtlsdr_get_frequency,
+ .set_frequency = rtlsdr_set_frequency,
+ .set_frequency_callback = rtlsdr_set_frequency_callback,
+ .get_band = rtlsdr_get_band,
+ .set_band = rtlsdr_set_band,
+ .band_supported = rtlsdr_band_supported,
+ .get_min_frequency = rtlsdr_get_min_frequency,
+ .get_max_frequency = rtlsdr_get_max_frequency,
+ .get_frequency_step = rtlsdr_get_frequency_step,
+ .start = rtlsdr_start,
+ .stop = rtlsdr_stop,
+ .scan_start = rtlsdr_scan_start,
+ .scan_stop = rtlsdr_scan_stop,
+ .get_stereo_mode = rtlsdr_get_stereo_mode,
+ .set_stereo_mode = rtlsdr_set_stereo_mode
+};