diff --git a/Makefile.am b/Makefile.am index 6e7c73e..774cca1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -5,7 +5,7 @@ jmpxrds_SOURCES = filters.c oscilator.c resampler.c rds_encoder.c \ jmpxrds_LDADD = $(LIBM) $(LIBRT) $(LIBSAMPLERATE) $(LIBFFTW3) $(LIBJACK) jmpxrds_CFLAGS = $(CFLAGS) -rds_tool_SOURCES = utils.c rds_config.c rds_tool.c +rds_tool_SOURCES = utils.c rds_config.c rds_dynpsrt.c rds_tool.c rds_tool_LDADD = $(LIBRT) rds_tool_CFLAGS = $(CFLAGS) @@ -17,6 +17,7 @@ if GUI bin_PROGRAMS += jmpxrds_gui jmpxrds_gui_SOURCES = utils.c rds_config.c +jmpxrds_gui_SOURCES += @srcdir@/gui/jmrg_file_chooser.c @srcdir@/rds_dynpsrt.c jmpxrds_gui_SOURCES += @srcdir@/gui/jmrg_radio_button.c @srcdir@/gui/jmrg_switch.c jmpxrds_gui_SOURCES += @srcdir@/gui/jmrg_level_bar.c @srcdir@/gui/jmrg_vscale.c jmpxrds_gui_SOURCES += @srcdir@/gui/jmrg_mpx_plotter.c @srcdir@/gui/jmrg_mpx_plotter_gl.c diff --git a/gui/jmpxrds_gui.h b/gui/jmpxrds_gui.h index 38aee85..d80d655 100644 --- a/gui/jmpxrds_gui.h +++ b/gui/jmpxrds_gui.h @@ -50,6 +50,13 @@ struct value_map { /* Autocomplete entry match index */ int acentry_match_idx; + + /* Dynamic PSN / RT infos */ + struct rds_dynps_state dps; + struct rds_dynrt_state drt; + + /* Switch for disabling Dynamic PSN / RT */ + GtkWidget *sw; }; @@ -61,6 +68,7 @@ GtkWidget* jmrg_level_bar_init(const char*, float*); GtkWidget* jmrg_radio_button_init(const char*, int *, int, GtkRadioButton*); /* Widgets on RDSEnc panel */ GtkWidget* jmrg_set_button_init(const char*, struct value_map*); +GtkWidget* jmrg_file_chooser_init(struct value_map*); GtkWidget* jmrg_checkbox_init(struct rds_encoder_state*, const char*, int, int, int); GtkWidget* jmrg_display_field_init(struct rds_encoder_state*, const char*, int); diff --git a/gui/jmrg_display_field.c b/gui/jmrg_display_field.c index acec74f..5551ace 100644 --- a/gui/jmrg_display_field.c +++ b/gui/jmrg_display_field.c @@ -31,12 +31,22 @@ jmrg_display_field_poll(gpointer data) gtk_label_set_text(GTK_LABEL(vmap->target), pi); return TRUE; case RDS_FIELD_PS: + if(!st->ps_set) + return TRUE; text = rds_get_ps(st); + if(gtk_widget_get_sensitive(vmap->target2) == FALSE) + gtk_widget_set_sensitive(vmap->target2, TRUE); break; case RDS_FIELD_RT: + if(!st->rt_set) + return TRUE; text = rds_get_rt(st); + if(gtk_widget_get_sensitive(vmap->target2) == FALSE) + gtk_widget_set_sensitive(vmap->target2, TRUE); break; case RDS_FIELD_PTYN: + if(!st->ptyn_set) + return TRUE; text = rds_get_ptyn(st); if(!text) { tmp = rds_get_pty(st); @@ -49,7 +59,9 @@ jmrg_display_field_poll(gpointer data) return FALSE; } - gtk_label_set_text(GTK_LABEL(vmap->target), text); + if(text) + gtk_label_set_text(GTK_LABEL(vmap->target), text); + return TRUE; } @@ -64,31 +76,32 @@ jmrg_display_field_init(struct rds_encoder_state *st, const char* label, int typ GtkWidget *container = NULL; GtkWidget *vbox = NULL; GtkWidget *display = NULL; - GtkWidget *hbox = NULL; + GtkWidget *input_hbox = NULL; GtkWidget *input = NULL; GtkWidget *flag_check = NULL; GtkWidget *set_button = NULL; + GtkWidget *file_chooser = NULL; GtkStyleContext *context = NULL; struct value_map *vmap = NULL; - int has_flag = 1; + int has_flag = 0; + int is_dynamic = 0; int max_len = 1; switch(type) { case RDS_FIELD_PI: max_len = 5; - has_flag = 0; break; case RDS_FIELD_PS: max_len = RDS_PS_LENGTH; - has_flag = 0; + is_dynamic = 1; break; case RDS_FIELD_RT: max_len = RDS_RT_LENGTH; has_flag = 1; + is_dynamic = 1; break; case RDS_FIELD_PTYN: max_len = RDS_PTYN_LENGTH; - has_flag = 0; break; default: return NULL; @@ -107,6 +120,7 @@ jmrg_display_field_init(struct rds_encoder_state *st, const char* label, int typ else gtk_frame_set_shadow_type(GTK_FRAME(container), GTK_SHADOW_NONE); + gtk_widget_set_valign(container, GTK_ALIGN_START); /* Use a box to have better control */ @@ -115,12 +129,11 @@ jmrg_display_field_init(struct rds_encoder_state *st, const char* label, int typ goto cleanup; gtk_container_add(GTK_CONTAINER(container), vbox); - /* Create the display as a label widget */ display = gtk_label_new(NULL); if(!display) goto cleanup; - gtk_box_pack_start(GTK_BOX(vbox), display, 1, 1, 6); + gtk_box_pack_start(GTK_BOX(vbox), display, 1, 0, 6); gtk_label_set_max_width_chars(GTK_LABEL(display), max_len); /* Register custom CSS */ @@ -130,15 +143,15 @@ jmrg_display_field_init(struct rds_encoder_state *st, const char* label, int typ /* Create the input field with its set button and * an optional checkbox (flag) for the RT field */ - hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - if(!hbox) + input_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + if(!input_hbox) goto cleanup; - gtk_box_pack_end(GTK_BOX(vbox), hbox, 1, 1, 6); + gtk_box_pack_start(GTK_BOX(vbox), input_hbox, 1, 1, 6); input = gtk_entry_new(); if(!input) goto cleanup; - gtk_box_pack_start(GTK_BOX(hbox), input, 1, 1, 6); + gtk_box_pack_start(GTK_BOX(input_hbox), input, 1, 1, 6); gtk_entry_set_max_width_chars(GTK_ENTRY(input), max_len); if(has_flag) { @@ -146,10 +159,9 @@ jmrg_display_field_init(struct rds_encoder_state *st, const char* label, int typ 0, 0, 1); if(!flag_check) goto cleanup; - gtk_box_pack_start(GTK_BOX(hbox), flag_check, 0, 0, 2); + gtk_box_pack_start(GTK_BOX(input_hbox), flag_check, 0, 0, 2); } - /* Initialize value_map */ vmap = malloc(sizeof(struct value_map)); if(!vmap) @@ -166,9 +178,20 @@ jmrg_display_field_init(struct rds_encoder_state *st, const char* label, int typ set_button = jmrg_set_button_init("Set", vmap); if(!set_button) goto cleanup; - gtk_box_pack_start(GTK_BOX(hbox), set_button, 0, 0, 6); + gtk_box_pack_start(GTK_BOX(input_hbox), set_button, 0, 0, 6); + /* If it's Dynamic PSN / RT, add the file chooser */ + if(is_dynamic) { + file_chooser = jmrg_file_chooser_init(vmap); + if(!file_chooser) + goto cleanup; + gtk_box_pack_start(GTK_BOX(vbox), file_chooser, 1, 1, 6); + gtk_widget_set_halign(file_chooser, GTK_ALIGN_START); + gtk_widget_set_sensitive(file_chooser, FALSE); + vmap->target2 = file_chooser; + } + /* Register polling function and signal handlers */ vmap->esid = g_timeout_add(200, jmrg_display_field_poll, vmap); @@ -179,10 +202,12 @@ jmrg_display_field_init(struct rds_encoder_state *st, const char* label, int typ cleanup: if(vmap) free(vmap); + if(set_button) + gtk_widget_destroy(set_button); if(input) gtk_widget_destroy(input); - if(hbox) - gtk_widget_destroy(hbox); + if(input_hbox) + gtk_widget_destroy(input_hbox); if(display) gtk_widget_destroy(display); if(vbox) diff --git a/gui/jmrg_file_chooser.c b/gui/jmrg_file_chooser.c new file mode 100644 index 0000000..3bc2123 --- /dev/null +++ b/gui/jmrg_file_chooser.c @@ -0,0 +1,155 @@ +#include /* For strtol() */ +#include "jmpxrds_gui.h" +#include /* For PATH_MAX */ +#include /* For strncmp() */ + +/*****************\ +* SIGNAL HANDLERS * +\*****************/ + +static void +jmrg_file_chooser_done(GtkWidget *button, gpointer data) +{ + struct value_map *vmap = (struct value_map*) data; + struct rds_encoder_state *st = vmap->st; + const char *filepath = NULL; + int ret = 0; + + filepath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(button)); + + switch(vmap->type) { + case RDS_FIELD_PS: + if((&vmap->dps)->active) + rds_dynps_destroy(&vmap->dps); + if(!filepath) + return; + ret = rds_dynps_init(&vmap->dps, vmap->st, filepath); + break; + case RDS_FIELD_RT: + if((&vmap->drt)->active) + rds_dynrt_destroy(&vmap->drt); + if(!filepath) + return; + ret = rds_dynrt_init(&vmap->drt, vmap->st, filepath); + break; + default: + return; + }; + + + if(gtk_switch_get_state(GTK_SWITCH(vmap->sw)) == FALSE) + gtk_switch_set_state(GTK_SWITCH(vmap->sw), TRUE); + + return; +} + +static gboolean +jmrg_file_chooser_swtoggle(GtkSwitch *widget, gboolean state, gpointer data) +{ + struct value_map *vmap = (struct value_map*) data; + GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(widget)); + GList *children = gtk_container_get_children(GTK_CONTAINER(parent)); + GtkWidget *file_chooser = g_list_nth_data(children, 1); + int active = 0; + + switch(vmap->type) { + case RDS_FIELD_PS: + active = (&vmap->dps)->active; + break; + case RDS_FIELD_RT: + active = (&vmap->drt)->active; + break; + default: + return FALSE; + }; + + if(state) { + if((&vmap->dps)->active) + return FALSE; + + if(vmap->type == RDS_FIELD_PS && (&vmap->dps)->filepath) + gtk_file_chooser_select_filename(GTK_FILE_CHOOSER(file_chooser), + (&vmap->dps)->filepath); + else if(vmap->type == RDS_FIELD_RT && (&vmap->drt)->filepath) + gtk_file_chooser_select_filename(GTK_FILE_CHOOSER(file_chooser), + (&vmap->drt)->filepath); + else { + gtk_switch_set_state(widget, FALSE); + return FALSE; + } + } else { + if(!(&vmap->dps)->active) + return FALSE; + gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(file_chooser)); + } + + g_signal_emit_by_name(G_OBJECT(file_chooser), "file-set"); + return FALSE; +} + +/*************\ +* ENTRY POINT * +\*************/ + +GtkWidget* +jmrg_file_chooser_init(struct value_map *vmap) +{ + GtkWidget *container = NULL; + GtkWidget *label = NULL; + GtkWidget *button = NULL; + GtkWidget *sw = NULL; + const char* label_text; + const char* chooser_title; + + if(!vmap) + goto cleanup; + + container = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + if(!container) + goto cleanup; + + switch(vmap->type) { + case RDS_FIELD_PS: + label_text = "Dynamic PSN from file"; + chooser_title = "Dynamic PSN data file"; + break; + case RDS_FIELD_RT: + label_text = "Dynamic RT from file"; + chooser_title = "Dynamic RT data file"; + break; + } + + label = gtk_label_new(label_text); + if(!label) + goto cleanup; + gtk_box_pack_start(GTK_BOX(container), label, 0, 0, 6); + + button = gtk_file_chooser_button_new(chooser_title, + GTK_FILE_CHOOSER_ACTION_OPEN); + if(!button) + goto cleanup; + gtk_box_pack_start(GTK_BOX(container), button, 0, 0, 6); + + sw = gtk_switch_new(); + if(!sw) + goto cleanup; + gtk_box_pack_end(GTK_BOX(container), sw, 0, 0, 8); + + vmap->sw = sw; + + g_signal_connect(sw, "state-set", G_CALLBACK(jmrg_file_chooser_swtoggle), + vmap); + + g_signal_connect(button, "file-set", + G_CALLBACK(jmrg_file_chooser_done), + vmap); + return container; + cleanup: + if(button) + gtk_widget_destroy(button); + if(label) + gtk_widget_destroy(label); + if(container) + gtk_widget_destroy(container); + return NULL; +} diff --git a/rds_config.c b/rds_config.c index 82bfdcb..e81c078 100644 --- a/rds_config.c +++ b/rds_config.c @@ -195,8 +195,8 @@ rds_set_ps(struct rds_encoder_state *st, const char *ps) return -1; pslen = strnlen(ps, RDS_PS_LENGTH); - if (pslen > RDS_PS_LENGTH) - return -1; + if (!pslen || pslen > RDS_PS_LENGTH) + return -2; memset(st->ps, 0, RDS_PS_LENGTH); diff --git a/rds_dynpsrt.c b/rds_dynpsrt.c new file mode 100644 index 0000000..41114ee --- /dev/null +++ b/rds_dynpsrt.c @@ -0,0 +1,395 @@ +/* + * JMPXRDS, an FM MPX signal generator with RDS support on + * top of Jack Audio Connection Kit - RDS Dynamic PSN / RT handling + * + * Copyright (C) 2015 Nick Kossifidis + * + * 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 3 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, see . + */ +#include "rds_encoder.h" +#include "utils.h" +#include /* For fopen(), FILE etc */ +#include /* For memset/strnlen/strncpy etc */ +#include /* For pthread support */ +#include /* For sleep() */ +#include /* For SIGTERM */ + + +/*************\ +* DYNAMIC PSN * +\*************/ + +/* + * Note: Dynamic PSN is highly discouraged and RDS forum has issued various + * statements against it. I've put it here since many stations use it and I + * got a request from radio Best 94.7 in Heraklion for it. This operation will + * switch the station's name every DYNPS_DELAY_SECS so that the PSN field on + * car / old radios shows like a scrolling text. The station's name (fixed_ps) + * will be shown each time the dynamic PSN text has been fully "scrolled". + * There are various dynamic PSN modes available out there, here I've implemented + * the "scroll by 8 characters" mode since it's the most reliable and takes fewer + * time to scroll the full text. + */ + +static void +rds_dynps_sanitize(struct rds_dynps_state *dps) +{ + /* TODO: Maybe clean up symbols, dots etc */ + dps->no_segments = (strnlen(dps->string, DYNPS_MAX_CHARS) + 7) / RDS_PS_LENGTH; + return; +} + +static char* +rds_dynps_get_next_str_segment(struct rds_dynps_state *dps) +{ + static char segment[RDS_PS_LENGTH + 1] = {0}; + + if(dps->no_segments == 0) + return dps->fixed_ps; + + if(dps->curr_segment + 1 > dps->no_segments) { + dps->curr_segment = 0; + return dps->fixed_ps; + } + + strncpy(segment, dps->string + (dps->curr_segment * RDS_PS_LENGTH), + RDS_PS_LENGTH); + dps->curr_segment++; + + return segment; +} + +static void +rds_dynps_sleep(struct rds_dynps_state *dps) +{ + sleep(DYNPS_DELAY_SECS); +} + +static void +rds_dynps_update(struct rds_dynps_state *dps, const char* segment) +{ + struct rds_encoder_state *st = dps->st; + int ret = 0; + ret = rds_set_ps(st, segment); + utils_dbg("[DYNPS] %s, status: %i\n",segment, ret); +} + +static void* +rds_dynps_consumer_thread(void *arg) +{ + struct rds_dynps_state *dps = (struct rds_dynps_state *) arg; + char* segment = NULL; + + while(dps->active) { + pthread_mutex_lock(&dps->dynps_proc_mutex); + segment = rds_dynps_get_next_str_segment(dps); + rds_dynps_update(dps, segment); + pthread_mutex_unlock(&dps->dynps_proc_mutex); + rds_dynps_sleep(dps); + } + + return arg; +} + +static void* +rds_dynps_filemon_thread(void *arg) +{ + struct rds_dynps_state *dps = (struct rds_dynps_state *) arg; + char *res = NULL; + int ret = 0; + FILE *file = NULL; + + while(dps->active) { + /* Blocking read until we get an event */ + if(dps->opened) + ret = read(dps->inotify_fd, dps->event_buf, EVENT_LEN); + if(ret < 0) { + utils_perr("[DYNPS] Failed to read inotify fd, read()"); + continue; + } + + file = fopen(dps->filepath, "r"); + if(!file) { + utils_perr("[DYNPS] Failed to open %s for reading PS, fopen()"); + continue; + } + + dps->opened = 1; + + pthread_mutex_lock(&dps->dynps_proc_mutex); + res = fgets(dps->string, DYNPS_MAX_CHARS - 1, file); + if(!res) { + utils_perr("[DYNPS] Failed to get string from file, fgets()"); + continue; + } + rds_dynps_sanitize(dps); + dps->curr_segment = 0; + pthread_mutex_unlock(&dps->dynps_proc_mutex); + fclose(file); + } + + return arg; +} + +void +rds_dynps_destroy(struct rds_dynps_state *dps) +{ + struct rds_encoder_state *st = dps->st; + dps->active = 0; + if(dps->inotify_fd && dps->watch_fd) + inotify_rm_watch(dps->inotify_fd, dps->watch_fd); + if(dps->inotify_fd) + close(dps->inotify_fd); + /* Give them some time to exit normaly */ + rds_dynps_sleep(dps); + /* Now kill them */ + if(dps->dynps_filemon_tid) + pthread_kill(dps->dynps_filemon_tid, SIGTERM); + if(dps->dynps_consumer_tid) + pthread_kill(dps->dynps_consumer_tid, SIGTERM); + if(dps->fixed_ps) + rds_set_ps(st, dps->fixed_ps); +} + +int +rds_dynps_init(struct rds_dynps_state *dps, struct rds_encoder_state *st, const char* filepath) +{ + int ret = 0; + + if(!st->ps_set) { + utils_wrn("[DYNPS] Fixed PS not set, dynamic PS request ignored\n"); + return -1; + } + + memset(dps, 0, sizeof(struct rds_dynps_state)); + + dps->st = st; + pthread_mutex_init(&dps->dynps_proc_mutex, NULL); + + strncpy(dps->fixed_ps, st->ps, RDS_PS_LENGTH); + + dps->inotify_fd = inotify_init(); + if(dps->inotify_fd < 0) { + utils_perr("[DYNPS] Unable to initialize inotify, inotify_init()"); + ret = -2; + goto cleanup; + } + + dps->filepath = filepath; + dps->watch_fd = inotify_add_watch(dps->inotify_fd, dps->filepath, IN_MODIFY); + if(dps->watch_fd < 0) { + utils_perr("[DYNPS] Unable to add inotify watch, inotify_add_watch()"); + ret = -3; + goto cleanup; + } + + dps->active = 1; + ret = pthread_create(&dps->dynps_filemon_tid, NULL, + rds_dynps_filemon_thread, (void*) dps); + if(ret != 0) { + utils_err("[DYNPS] Unable to create file monitor thread, pthred_create(): %d", ret); + ret = -4; + goto cleanup; + } + + ret = pthread_create(&dps->dynps_consumer_tid, NULL, + rds_dynps_consumer_thread, (void*) dps); + if(ret != 0) { + utils_err("[DYNPS] Unable to create file monitor thread, pthred_create(): %d", ret); + ret = -4; + goto cleanup; + } + + return ret; + + cleanup: + rds_dynps_destroy(dps); + return ret; +} + + +/*******************\ +* DYNAMIC RadioText * +\*******************/ + +static void +rds_dynrt_sanitize(struct rds_dynrt_state *drt) +{ + /* TODO: Maybe clean up symbols, dots etc */ + drt->no_segments = (strnlen(drt->string, DYNRT_MAX_CHARS) + 7) / RDS_RT_LENGTH; + return; +} + +static char* +rds_dynrt_get_next_str_segment(struct rds_dynrt_state *drt) +{ + static char segment[RDS_RT_LENGTH + 1] = {0}; + + if(drt->curr_segment + 1 > drt->no_segments) + drt->curr_segment = 0; + + strncpy(segment, drt->string + (drt->curr_segment * RDS_RT_LENGTH), + RDS_RT_LENGTH); + drt->curr_segment++; + + return segment; +} + +static void +rds_dynrt_sleep(struct rds_dynrt_state *drt) +{ + sleep(DYNRT_DELAY_SECS); +} + +static void +rds_dynrt_update(struct rds_dynrt_state *drt, const char* segment) +{ + struct rds_encoder_state *st = drt->st; + int ret = 0; + ret = rds_set_rt(st, segment, 1); + utils_dbg("[DYNRT] %s, status: %i\n",segment, ret); +} + +static void* +rds_dynrt_consumer_thread(void *arg) +{ + struct rds_dynrt_state *drt = (struct rds_dynrt_state *) arg; + char* segment = NULL; + + while(drt->active) { + pthread_mutex_lock(&drt->dynrt_proc_mutex); + segment = rds_dynrt_get_next_str_segment(drt); + rds_dynrt_update(drt, segment); + pthread_mutex_unlock(&drt->dynrt_proc_mutex); + rds_dynrt_sleep(drt); + } + + return arg; +} + +static void* +rds_dynrt_filemon_thread(void *arg) +{ + struct rds_dynrt_state *drt = (struct rds_dynrt_state *) arg; + char *res = NULL; + int ret = 0; + FILE *file = NULL; + + while(drt->active) { + /* Blocking read until we get an event */ + if(drt->opened) + ret = read(drt->inotify_fd, drt->event_buf, EVENT_LEN); + if(ret < 0) { + utils_perr("[DYNRT] Failed to read inotify fd, read()"); + continue; + } + + file = fopen(drt->filepath, "r"); + if(!file) { + utils_perr("[DYNRT] Failed to open %s for reading PS, fopen()"); + continue; + } + + drt->opened = 1; + + pthread_mutex_lock(&drt->dynrt_proc_mutex); + res = fgets(drt->string, DYNRT_MAX_CHARS - 1, file); + if(!res) { + utils_perr("[DYNRT] Failed to get string from file, fgets()"); + continue; + } + rds_dynrt_sanitize(drt); + drt->curr_segment = 0; + pthread_mutex_unlock(&drt->dynrt_proc_mutex); + fclose(file); + } + + return arg; +} + +void +rds_dynrt_destroy(struct rds_dynrt_state *drt) +{ + struct rds_encoder_state *st = drt->st; + drt->active = 0; + if(drt->inotify_fd && drt->watch_fd) + inotify_rm_watch(drt->inotify_fd, drt->watch_fd); + if(drt->inotify_fd) + close(drt->inotify_fd); + /* Give them some time to exit normaly */ + rds_dynrt_sleep(drt); + /* Now kill them */ + if(drt->dynrt_filemon_tid) + pthread_kill(drt->dynrt_filemon_tid, SIGTERM); + if(drt->dynrt_consumer_tid) + pthread_kill(drt->dynrt_consumer_tid, SIGTERM); + if(drt->fixed_rt) + rds_set_rt(st, drt->fixed_rt, 1); +} + +int +rds_dynrt_init(struct rds_dynrt_state *drt, struct rds_encoder_state *st, const char* filepath) +{ + int ret = 0; + + if(!st->rt_set) { + utils_wrn("[DYNRT] Fixed RT not set, dynamic RT request ignored\n"); + return -1; + } + + memset(drt, 0, sizeof(struct rds_dynrt_state)); + + drt->st = st; + pthread_mutex_init(&drt->dynrt_proc_mutex, NULL); + + strncpy(drt->fixed_rt, st->rt, RDS_RT_LENGTH); + + drt->inotify_fd = inotify_init(); + if(drt->inotify_fd < 0) { + utils_perr("[DYNRT] Unable to initialize inotify, inotify_init()"); + ret = -2; + goto cleanup; + } + + drt->filepath = filepath; + drt->watch_fd = inotify_add_watch(drt->inotify_fd, drt->filepath, IN_MODIFY); + if(drt->watch_fd < 0) { + utils_perr("[DYNRT] Unable to add inotify watch, inotify_add_watch()"); + ret = -3; + goto cleanup; + } + + drt->active = 1; + ret = pthread_create(&drt->dynrt_filemon_tid, NULL, + rds_dynrt_filemon_thread, (void*) drt); + if(ret != 0) { + utils_err("[DYNRT] Unable to create file monitor thread, pthred_create(): %d", ret); + ret = -4; + goto cleanup; + } + + ret = pthread_create(&drt->dynrt_consumer_tid, NULL, + rds_dynrt_consumer_thread, (void*) drt); + if(ret != 0) { + utils_err("[DYNRT] Unable to create file monitor thread, pthred_create(): %d", ret); + ret = -4; + goto cleanup; + } + + return ret; + + cleanup: + rds_dynrt_destroy(drt); + return ret; +} diff --git a/rds_encoder.c b/rds_encoder.c index 0d55f01..1b490f2 100644 --- a/rds_encoder.c +++ b/rds_encoder.c @@ -591,7 +591,7 @@ rds_get_next_group(struct rds_encoder *enc, struct rds_group *group) RDS_GROUP_VERSION_B); } /* Send a 1A group to update ECC / LIC on the receiver */ - else if (groups_per_sec_counter < 6 && (st->ecc_set || st->lic_set)) { + else if (groups_per_sec_counter < 5 && (st->ecc_set || st->lic_set)) { ret = rds_generate_group(enc, group, 1, RDS_GROUP_VERSION_A); } /* Send 2 10A groups for PTYN if available */ diff --git a/rds_encoder.h b/rds_encoder.h index b127f4e..00454f0 100644 --- a/rds_encoder.h +++ b/rds_encoder.h @@ -20,6 +20,8 @@ #include /* For typed ints */ #include /* For jack-related types */ #include /* For pthread mutex / conditional */ +#include /* For inotify_event */ +#include /* For NAME_MAX */ /* RDS encoding takes a data stream of specialy formated data, * does a differential encoding on it and passes it through a @@ -206,3 +208,73 @@ int rds_set_rt(struct rds_encoder_state *st, const char *rt, int flush); /* Abstract getter/setter for bit fields */ typedef uint8_t (*rds_bf_getter) (struct rds_encoder_state *); typedef int (*rds_bf_setter) (struct rds_encoder_state *, uint8_t); + +/* Dynamic PS */ + +#define EVENT_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1)) + +/* We send PSN once every second, 3 secs should be enough to make sure + * that most recievers got the update, even if they lost the packet + * twice */ +#define DYNPS_DELAY_SECS 3 + +/* This is 8 segments, it'll take 24secs to show the title which is a lot + * of time already. I've seen some programs that support 255 characters, that's + * insane, it'll take more than 10mins to show the whole text. If you change this + * make sure it's a multiple of 8 + 1 for the null terminator */ +#define DYNPS_MAX_CHARS 65 + +struct rds_dynps_state { + struct rds_encoder_state *st; + char string[DYNPS_MAX_CHARS]; + char fixed_ps[RDS_PS_LENGTH + 1]; + int curr_segment; + int no_segments; + pthread_t dynps_filemon_tid; + pthread_t dynps_consumer_tid; + pthread_mutex_t dynps_proc_mutex; + int active; + int opened; + int inotify_fd; + int watch_fd; + const char *filepath; + char event_buf[EVENT_LEN]; +}; + +int rds_dynps_init(struct rds_dynps_state *dps, struct rds_encoder_state *st, + const char* filepath); +void rds_dynps_destroy(struct rds_dynps_state *dps); + +/* Dynamic RT */ + +/* We send a minimum of 5 2A groups per second to update RT on the receivers + * and each group contains 4 characters so we need 16 groups to transmit the + * full 64 char message. That's almost 3 seconds to get one RT messge out. + * To have the same reduntancy as with the PSN, we want to at least send + * the message out 3 times, so it's 9 seconds, make it 10 to be sure */ +#define DYNRT_DELAY_SECS 10 + +/* That's 3 RT messages plus the null terminator, it'll take almost 30 + * seconds to send that in the worst case */ +#define DYNRT_MAX_CHARS 193 + +struct rds_dynrt_state { + struct rds_encoder_state *st; + char string[DYNRT_MAX_CHARS]; + char fixed_rt[RDS_RT_LENGTH + 1]; + int curr_segment; + int no_segments; + pthread_t dynrt_filemon_tid; + pthread_t dynrt_consumer_tid; + pthread_mutex_t dynrt_proc_mutex; + int active; + int opened; + int inotify_fd; + int watch_fd; + const char *filepath; + char event_buf[EVENT_LEN]; +}; + +int rds_dynrt_init(struct rds_dynrt_state *drt, struct rds_encoder_state *st, + const char* filepath); +void rds_dynrt_destroy(struct rds_dynrt_state *drt); diff --git a/rds_tool.c b/rds_tool.c index c9fcb19..e1c44aa 100644 --- a/rds_tool.c +++ b/rds_tool.c @@ -23,8 +23,10 @@ #include /* For snprintf */ #include /* For memset / strnlen / strncmp */ #include /* For getopt_long_only() */ +#include /* For signal handling / sig_atomic_t */ #define TEMP_BUF_LEN RDS_RT_LENGTH + 1 + void usage(char *name) { utils_ann("RDS Configuration tool for JMPXRDS\n"); @@ -43,7 +45,9 @@ void usage(char *name) "\t-tp \tSet Traffic Programme flag (TP)\n" "\t-ta \tSet Traffic Announcement flag (TA)\n" "\t-ms \tSet Music/Speech flag (MS)\n" - "\t-di \tSet Decoder Info (DI)\n"); + "\t-di \tSet Decoder Info (DI)\n" + "\t-dps \tUpdate PSN from file (Dynamic PSN)\n" + "\t-drt \tUpdate RT from file (Dynamic RT)\n"); } static const struct option opts[] = { @@ -58,10 +62,20 @@ static const struct option opts[] = { {"ta", required_argument,0, 9}, {"ms", required_argument,0, 10}, {"di", required_argument,0, 11}, + {"dps", required_argument,0, 12}, + {"drt", required_argument,0, 13}, {0, 0, 0, 0} }; -/* Yes it's ugly... */ +static volatile sig_atomic_t active; + +static void +signal_handler(int sig, siginfo_t * info, void *context) +{ + active = 0; + return; +} + int main(int argc, char *argv[]) { @@ -79,6 +93,10 @@ main(int argc, char *argv[]) char temp[TEMP_BUF_LEN] = { 0 }; struct shm_mapping *shmem = NULL; struct rds_encoder_state *st = NULL; + struct rds_dynps_state dps = {0}; + struct rds_dynrt_state drt = {0}; + struct sigaction sa = {0}; + int loop = 0; int opt = 0; int opt_idx = 0; @@ -248,6 +266,22 @@ main(int argc, char *argv[]) } else utils_info("MS set: \t0x%X\n", di); break; + case 12: /* Dynamic PSN */ + ret = rds_dynps_init(&dps, st, optarg); + if(ret < 0) { + utils_err("Failed to initialize Dynamic PSN mode !\n"); + goto cleanup; + } else + loop = 1; + break; + case 13: /* Dynamic RT */ + ret = rds_dynrt_init(&drt, st, optarg); + if(ret < 0) { + utils_err("Failed to initialize Dynamic RT mode !\n"); + goto cleanup; + } else + loop = 1; + break; default: usage(argv[0]); utils_shm_destroy(shmem, 0); @@ -260,6 +294,20 @@ main(int argc, char *argv[]) ret = -1; } + if(loop) { + /* Install a signal handler for graceful exit */ + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = signal_handler; + sigaction(SIGQUIT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + active = 1; + while(active); + rds_dynps_destroy(&dps); + rds_dynrt_destroy(&drt); + } + cleanup: utils_shm_destroy(shmem, 0); return ret; diff --git a/utils.c b/utils.c index 29edd9d..d2a637a 100644 --- a/utils.c +++ b/utils.c @@ -140,8 +140,8 @@ utils_shm_unlink_all() /****************\ * CONSOLE OUTPUT * - \****************/ + /* Some codes for prety output on the terminal */ #define NORMAL "\x1B[0m" #define BRIGHT "\x1B[1m" @@ -174,6 +174,18 @@ utils_info(const char* fmt,...) printf(NORMAL); } +void +utils_wrn(const char* fmt,...) +{ + va_list args; + + fprintf(stderr, YELLOW); + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + fprintf(stderr, NORMAL); +} + void utils_err(const char* fmt,...) { diff --git a/utils.h b/utils.h index 7ddbc43..a15ee37 100644 --- a/utils.h +++ b/utils.h @@ -55,6 +55,9 @@ utils_ann(const char* msg); void utils_info(const char* fmt,...); +void +utils_wrn(const char* fmt,...); + void utils_err(const char* fmt,...);