diff options
Diffstat (limited to 'src/radio_impl_rtlsdr.c')
-rw-r--r-- | src/radio_impl_rtlsdr.c | 496 |
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 +}; |