diff options
Diffstat (limited to 'src/radio_impl_kingfisher.c')
-rw-r--r-- | src/radio_impl_kingfisher.c | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/src/radio_impl_kingfisher.c b/src/radio_impl_kingfisher.c new file mode 100644 index 0000000..a0b8449 --- /dev/null +++ b/src/radio_impl_kingfisher.c @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2017-2019 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 <glib.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <gst/gst.h> + +#include "radio_impl.h" + +#define SI_NODE "/sys/firmware/devicetree/base/si468x@0/compatible" +#define SI_CTL "/usr/bin/si_ctl" +#define SI_CTL_CMDLINE_MAXLEN 128 +#define SI_CTL_OUTPUT_MAXLEN 128 + +#define GST_PIPELINE_LEN 256 + +// Flag to enable using GST_STATE_READY instead of GST_STATE_PAUSED to trigger +// Wireplumber policy mechanism. Hopefully temporary. +#define WIREPLUMBER_WORKAROUND + +// 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 = 0; +static bool corking; +static bool present; +static bool initialized; +static uint32_t current_frequency; +static int scan_valid_snr_threshold = 128; +static int scan_valid_rssi_threshold = 128; +static bool scanning; + +// stream state +static GstElement *pipeline; +static bool running; + +static void (*freq_callback)(uint32_t, void*); +static void *freq_callback_data; + +static uint32_t kf_get_min_frequency(radio_band_t band); +static void kf_scan_stop(void); + +static gboolean handle_message(GstBus *bus, GstMessage *msg, __attribute__((unused)) void *ptr) +{ + GstState state; + + if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_REQUEST_STATE) { + gst_message_parse_request_state(msg, &state); + + if (state == GST_STATE_PAUSED) { + corking = true; + + // NOTE: Explicitly using PAUSED here, this case currently + // is separate from the general PAUSED/READY issue wrt + // Wireplumber policy. + gst_element_set_state(pipeline, GST_STATE_PAUSED); + } else if (state == GST_STATE_PLAYING) { + corking = false; + + gst_element_set_state(pipeline, GST_STATE_PLAYING); + } + } + + return TRUE; +} + +static void *gstreamer_loop_thread(void *ptr) +{ + g_main_loop_run(g_main_loop_new(NULL, FALSE)); + return NULL; +} + +static int kf_probe(void) +{ + struct stat statbuf; + + if(present) + return 0; + + // Check for Kingfisher SI468x devicetree node + if(stat(SI_NODE, &statbuf) != 0) + return -1; + + // Check for Cogent's si_ctl utility + if(stat(SI_CTL, &statbuf) != 0) + return -1; + + present = true; + return 0; +} + +static int kf_init(void) +{ + GKeyFile* conf_file; + bool conf_file_present = false; + char *value_str; + char cmd[SI_CTL_CMDLINE_MAXLEN]; + int rc; + char gst_pipeline_str[GST_PIPELINE_LEN]; + pthread_t thread_id; + + 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) { + conf_file_present = 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; + } + } + } + } + + if(conf_file_present) { + GError *error = NULL; + int n; + + // Allow over-riding scanning parameters just in case a demo + // setup needs to do so to work reliably. + n = g_key_file_get_integer(conf_file, + "radio", + "scan_valid_snr_threshold", + &error); + if(!error) { + printf("Scan valid SNR level set to %d", n); + scan_valid_snr_threshold = n; + } + + error = NULL; + n = g_key_file_get_integer(conf_file, + "radio", + "scan_valid_rssi_threshold", + &error); + if(!error) { + printf("Scan valid SNR level set to %d", n); + scan_valid_rssi_threshold = n; + } + + g_key_file_free(conf_file); + } + + printf("Using FM Bandplan: %s", known_fm_band_plans[bandplan].name); + current_frequency = kf_get_min_frequency(RADIO_BAND_FM); + snprintf(cmd, + sizeof(cmd), + "%s /dev/i2c-12 0x65 -b fm -p %s -t %d -u %d -c %d", + SI_CTL, + known_fm_band_plans[bandplan].name, + scan_valid_snr_threshold, + scan_valid_rssi_threshold, + current_frequency / 1000); + rc = system(cmd); + if(rc != 0) { + fprintf(stderr, "%s failed, rc = %d", SI_CTL, rc); + return -1; + } + + // Initialize GStreamer + gst_init(NULL, NULL); + + // Use PipeWire output + rc = snprintf(gst_pipeline_str, + GST_PIPELINE_LEN, + "pipewiresrc stream-properties=\"p,node.target=alsa:pcm:radio:0:capture\" ! " + "audio/x-raw,format=F32LE,channels=2 ! " + "pipewiresink stream-properties=\"p,media.role=Multimedia\""); + if(rc >= GST_PIPELINE_LEN) { + fprintf(stderr, "pipeline string too long"); + return -1; + } + pipeline = gst_parse_launch(gst_pipeline_str, NULL); + if(!pipeline) { + fprintf(stderr, "pipeline construction failed!"); + return -1; + } + + // Start pipeline in paused state +#ifdef WIREPLUMBER_WORKAROUND + gst_element_set_state(pipeline, GST_STATE_READY); +#else + gst_element_set_state(pipeline, GST_STATE_PAUSED); +#endif + + gst_bus_add_watch(gst_element_get_bus(pipeline), (GstBusFunc) handle_message, NULL); + + rc = pthread_create(&thread_id, NULL, gstreamer_loop_thread, NULL); + if(rc != 0) + return rc; + + initialized = true; + return 0; +} + +static void kf_set_output(const char *output) +{ +} + +static uint32_t kf_get_frequency(void) +{ + return current_frequency; +} + +static void kf_set_frequency(uint32_t frequency) +{ + char cmd[SI_CTL_CMDLINE_MAXLEN]; + int rc; + + if(!initialized) + return; + + if(scanning) + return; + + if(frequency < known_fm_band_plans[bandplan].min || + frequency > known_fm_band_plans[bandplan].max) + return; + + kf_scan_stop(); + snprintf(cmd, sizeof(cmd), "%s /dev/i2c-12 0x65 -c %d", SI_CTL, frequency / 1000); + rc = system(cmd); + if(rc == 0) + current_frequency = frequency; + + if(freq_callback) + freq_callback(current_frequency, freq_callback_data); +} + +static void kf_set_frequency_callback(radio_freq_callback_t callback, + void *data) +{ + freq_callback = callback; + freq_callback_data = data; +} + +static char * kf_get_rds_info(void) { + char cmd[SI_CTL_CMDLINE_MAXLEN]; + char line[SI_CTL_OUTPUT_MAXLEN]; + char * rds = NULL; + FILE *fp; + + if (scanning) + goto done; + + snprintf(cmd, sizeof(cmd), "%s /dev/i2c-12 0x65 -m", SI_CTL); + fp = popen(cmd, "r"); + if(fp == NULL) { + fprintf(stderr, "Could not run: %s!\n", cmd); + goto done; + } + /* Look for "Name:" in output */ + while (fgets(line, sizeof(line), fp) != NULL) { + + char* nS = strstr(line, "Name:"); + char * end; + if (!nS) + continue; + + end = nS+strlen("Name:"); + /* remove the trailing '\n' */ + end[strlen(end)-1] = '\0'; + + rds = strdup(end); + break; + } + + /* Make sure si_ctl has finished */ + pclose(fp); + +done: + return rds; +} + +static radio_band_t kf_get_band(void) +{ + return RADIO_BAND_FM; +} + +static void kf_set_band(radio_band_t band) +{ + // We only support FM, so do nothing +} + +static int kf_band_supported(radio_band_t band) +{ + if(band == RADIO_BAND_FM) + return 1; + return 0; +} + +static uint32_t kf_get_min_frequency(radio_band_t band) +{ + return known_fm_band_plans[bandplan].min; +} + +static uint32_t kf_get_max_frequency(radio_band_t band) +{ + return known_fm_band_plans[bandplan].max; +} + +static uint32_t kf_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 bool kf_get_corking_state(void) +{ + return corking; +} + +static void kf_start(void) +{ + if(!initialized) + return; + + if(!running || corking) { + // Start pipeline + gst_element_set_state(pipeline, GST_STATE_PLAYING); + running = true; + corking = false; + } +} + +static void kf_stop(void) +{ + GstEvent *event; + + if(initialized && running) { + // Stop pipeline + running = false; + +#ifdef WIREPLUMBER_WORKAROUND + // NOTE: Using NULL here instead of READY, as it seems to trigger + // some odd behavior in the pipeline; alsasrc does not seem to + // stop, and things get hung up on restart as there are a bunch + // of "old" samples that seemingly confuse pipewiresink. Going + // to NULL state seems to tear down things enough to avoid + // whatever happens. + gst_element_set_state(pipeline, GST_STATE_NULL); +#else + gst_element_set_state(pipeline, GST_STATE_PAUSED); +#endif + corking = false; + +#ifndef WIREPLUMBER_WORKAROUND + // Flush pipeline + // This seems required to avoid stutters on starts after a stop + event = gst_event_new_flush_start(); + gst_element_send_event(GST_ELEMENT(pipeline), event); + event = gst_event_new_flush_stop(TRUE); + gst_element_send_event(GST_ELEMENT(pipeline), event); +#endif + } +} + +static void kf_scan_start(radio_scan_direction_t direction, + radio_scan_callback_t callback, + void *data) +{ + int rc; + char cmd[SI_CTL_CMDLINE_MAXLEN]; + char line[SI_CTL_OUTPUT_MAXLEN]; + uint32_t new_frequency = 0; + FILE *fp; + + if(!initialized) + return; + + if(!running || scanning) + return; + + scanning = true; + snprintf(cmd, + SI_CTL_CMDLINE_MAXLEN, + "%s /dev/i2c-12 0x65 -l %s", + SI_CTL, direction == RADIO_SCAN_FORWARD ? "up" : "down"); + fp = popen(cmd, "r"); + if(fp == NULL) { + fprintf(stderr, "Could not run: %s!", cmd); + return; + } + // Look for "Frequency:" in output + while(fgets(line, SI_CTL_OUTPUT_MAXLEN, fp) != NULL) { + if(strncmp("Frequency:", line, 10) == 0) { + new_frequency = atoi(line + 10); + break; + } + } + + // Make sure si_ctl has finished + rc = pclose(fp); + if(rc != 0) { + // Make sure we reset to original frequency, the Si4689 seems + // to auto-mute sometimes on failed scans, this hopefully works + // around that. + new_frequency = 0; + } + + if(new_frequency) { + current_frequency = new_frequency * 1000; + + // Push up the new frequency + // This is more efficient than calling kf_set_frequency and calling + // out to si_ctl again. + if(freq_callback) + freq_callback(current_frequency, freq_callback_data); + } else { + // Assume no station found, go back to starting frequency + kf_set_frequency(current_frequency); + } + + // Push up scan state + if(callback) + callback(current_frequency, data); + + scanning = false; +} + +static void kf_scan_stop(void) +{ + // ATM, it's not straightforward to stop a scan since we're using the si_ctl utility... +} + +static radio_stereo_mode_t kf_get_stereo_mode(void) +{ + return RADIO_MODE_STEREO; +} + +static void kf_set_stereo_mode(radio_stereo_mode_t mode) +{ + // We only support stereo, so do nothing +} + +radio_impl_ops_t kf_impl_ops = { + .name = "Kingfisher Si4689", + .probe = kf_probe, + .init = kf_init, + .set_output = kf_set_output, + .get_frequency = kf_get_frequency, + .set_frequency = kf_set_frequency, + .set_frequency_callback = kf_set_frequency_callback, + .get_band = kf_get_band, + .set_band = kf_set_band, + .band_supported = kf_band_supported, + .get_min_frequency = kf_get_min_frequency, + .get_max_frequency = kf_get_max_frequency, + .get_frequency_step = kf_get_frequency_step, + .get_corking_state = kf_get_corking_state, + .start = kf_start, + .stop = kf_stop, + .scan_start = kf_scan_start, + .scan_stop = kf_scan_stop, + .get_stereo_mode = kf_get_stereo_mode, + .set_stereo_mode = kf_set_stereo_mode, + .get_rds_info = kf_get_rds_info +}; |