aboutsummaryrefslogtreecommitdiffstats
path: root/src/radio_impl_kingfisher.c
diff options
context:
space:
mode:
authorScott Murray <scott.murray@konsulko.com>2023-01-03 00:51:50 -0500
committerScott Murray <scott.murray@konsulko.com>2023-01-03 00:57:12 -0500
commitdd23c157bdba1b25bbb50cdb99a60aa597735f43 (patch)
tree65c6f192bfc2f685e50d059d1e4ffb87fe7414f6 /src/radio_impl_kingfisher.c
parente057ee1ea2af4ff1d8121a0857bd9fd63181fba2 (diff)
Repurpose into gRPC serviceneedlefish
Repurpose repository into a spiritual successor of the previous binding. The backend code is retained behind a new gRPC API defined in protos/radio.proto. The simpler synchronous gRPC API had been used for expediency, this may warrant revisiting to rework into an async or callback API based server instead. As well, authentication has been left until some consensus on an approach can be worked out. Bug-AGL: SPEC-4665 Signed-off-by: Scott Murray <scott.murray@konsulko.com> Change-Id: I28b122ce6e0ecfc7504aa08b90394cb1b9e22976
Diffstat (limited to 'src/radio_impl_kingfisher.c')
-rw-r--r--src/radio_impl_kingfisher.c522
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
+};