Skip to content

Commit 980d590

Browse files
committed
runtime(io),ui: preserve RTL PPM apply semantics and C API constness
1 parent 5e9b1f2 commit 980d590

11 files changed

Lines changed: 242 additions & 28 deletions

File tree

include/dsd-neo/io/rtl_stream_c.h

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,39 @@ typedef struct RtlSdrContext RtlSdrContext;
2727

2828
/* Lifecycle */
2929
/**
30-
* @brief Create a new RTL-SDR stream context from options.
30+
* @brief Create a new RTL-SDR stream context from an immutable options snapshot.
31+
*
32+
* The supplied options are copied internally and never mutated. Use
33+
* rtl_stream_create_mirrored() when the caller needs live RTL PPM updates to
34+
* propagate back into its owning @ref dsd_opts.
35+
*
3136
* @param opts Decoder options snapshot used to configure the stream. Must not be NULL.
3237
* @param out_ctx [out] On success, receives an opaque context pointer.
3338
* @return 0 on success; otherwise <0 on error.
3439
*/
3540
int rtl_stream_create(const dsd_opts* opts, RtlSdrContext** out_ctx);
41+
/**
42+
* @brief Create a new RTL-SDR stream context mirrored to caller-owned options.
43+
*
44+
* The stream still owns an internal copy of @p opts, but live requested PPM
45+
* updates also write back into the caller-owned structure so restarts and
46+
* config snapshots retain the current correction.
47+
*
48+
* @param opts Mutable caller-owned decoder options to mirror. Must not be NULL.
49+
* @param out_ctx [out] On success, receives an opaque context pointer.
50+
* @return 0 on success; otherwise <0 on error.
51+
*/
52+
int rtl_stream_create_mirrored(dsd_opts* opts, RtlSdrContext** out_ctx);
3653
/**
3754
* @brief Start the stream threads and device I/O.
38-
* @param ctx Stream context created by rtl_stream_create().
55+
* @param ctx Stream context created by rtl_stream_create() or rtl_stream_create_mirrored().
3956
* @return 0 on success; otherwise <0 on error.
4057
*/
4158
int rtl_stream_start(RtlSdrContext* ctx);
4259
/**
4360
* @brief Stop the stream and cleanup resources associated with the run.
4461
* Safe to call multiple times; subsequent calls are no-ops.
45-
* @param ctx Stream context created by rtl_stream_create().
62+
* @param ctx Stream context created by rtl_stream_create() or rtl_stream_create_mirrored().
4663
* @return 0 on success; otherwise <0 on error.
4764
*/
4865
int rtl_stream_stop(RtlSdrContext* ctx);
@@ -52,7 +69,7 @@ int rtl_stream_stop(RtlSdrContext* ctx);
5269
* Mirrors rtl_stream_stop() but avoids toggling global shutdown state so the
5370
* UI can reconfigure and restart streaming without terminating the process.
5471
*
55-
* @param ctx Stream context created by rtl_stream_create().
72+
* @param ctx Stream context created by rtl_stream_create() or rtl_stream_create_mirrored().
5673
* @return 0 on success; otherwise <0 on error.
5774
*/
5875
int rtl_stream_soft_stop(RtlSdrContext* ctx);

include/dsd-neo/runtime/config.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ typedef struct dsdneoUserConfig {
636636
char rtl_freq[64];
637637
int rtl_gain;
638638
int rtl_ppm;
639+
int rtl_ppm_is_set; /* distinguish explicit 0 from omitted */
639640
int rtl_bw_khz;
640641
int rtl_sql;
641642
int rtl_volume;

src/engine/engine.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,7 +1518,7 @@ liveScanner(dsd_opts* opts, dsd_state* state) {
15181518
#ifdef USE_RADIO
15191519
if (opts->audio_in_type == AUDIO_IN_RTL) {
15201520
if (state->rtl_ctx == NULL) {
1521-
if (rtl_stream_create(opts, &state->rtl_ctx) < 0) {
1521+
if (rtl_stream_create_mirrored(opts, &state->rtl_ctx) < 0) {
15221522
LOG_ERROR("Failed to create radio stream.\n");
15231523
}
15241524
}
@@ -1875,7 +1875,7 @@ dsd_engine_run(dsd_opts* opts, dsd_state* state) {
18751875
#ifdef USE_RADIO
18761876
else if (opts->audio_in_type == AUDIO_IN_RTL) {
18771877
if (state->rtl_ctx == NULL) {
1878-
if (rtl_stream_create(opts, &state->rtl_ctx) < 0) {
1878+
if (rtl_stream_create_mirrored(opts, &state->rtl_ctx) < 0) {
18791879
LOG_ERROR("Failed to create radio stream.\n");
18801880
}
18811881
}

src/io/radio/rtl_stream_c.cpp

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,23 +71,16 @@ struct RtlSdrContext {
7171
RtlSdrOrchestrator* stream;
7272
};
7373

74-
/**
75-
* @brief Create a new RTL-SDR stream context from options.
76-
*
77-
* @param opts Decoder options snapshot used to configure the stream. Must not be NULL.
78-
* @param out_ctx [out] On success, receives an opaque context pointer.
79-
* @return 0 on success; otherwise <0 on error.
80-
*/
81-
extern "C" int
82-
rtl_stream_create(const dsd_opts* opts, RtlSdrContext** out_ctx) {
74+
static int
75+
rtl_stream_create_impl(const dsd_opts* opts, dsd_opts* mirrored_opts, RtlSdrContext** out_ctx) {
8376
if (!out_ctx || !opts) {
8477
return -1;
8578
}
8679
*out_ctx = (RtlSdrContext*)calloc(1, sizeof(RtlSdrContext));
8780
if (!*out_ctx) {
8881
return -1;
8982
}
90-
(*out_ctx)->stream = new (std::nothrow) RtlSdrOrchestrator(*opts, const_cast<dsd_opts*>(opts));
83+
(*out_ctx)->stream = new (std::nothrow) RtlSdrOrchestrator(*opts, mirrored_opts);
9184
if (!(*out_ctx)->stream) {
9285
free(*out_ctx);
9386
*out_ctx = NULL;
@@ -96,10 +89,34 @@ rtl_stream_create(const dsd_opts* opts, RtlSdrContext** out_ctx) {
9689
return 0;
9790
}
9891

92+
/**
93+
* @brief Create a new RTL-SDR stream context from an immutable options snapshot.
94+
*
95+
* @param opts Decoder options snapshot used to configure the stream. Must not be NULL.
96+
* @param out_ctx [out] On success, receives an opaque context pointer.
97+
* @return 0 on success; otherwise <0 on error.
98+
*/
99+
extern "C" int
100+
rtl_stream_create(const dsd_opts* opts, RtlSdrContext** out_ctx) {
101+
return rtl_stream_create_impl(opts, NULL, out_ctx);
102+
}
103+
104+
/**
105+
* @brief Create a new RTL-SDR stream context mirrored to caller-owned options.
106+
*
107+
* @param opts Mutable caller-owned decoder options to mirror. Must not be NULL.
108+
* @param out_ctx [out] On success, receives an opaque context pointer.
109+
* @return 0 on success; otherwise <0 on error.
110+
*/
111+
extern "C" int
112+
rtl_stream_create_mirrored(dsd_opts* opts, RtlSdrContext** out_ctx) {
113+
return rtl_stream_create_impl(opts, opts, out_ctx);
114+
}
115+
99116
/**
100117
* @brief Start the stream threads and device I/O.
101118
*
102-
* @param ctx Stream context created by rtl_stream_create().
119+
* @param ctx Stream context created by rtl_stream_create() or rtl_stream_create_mirrored().
103120
* @return 0 on success; otherwise <0 on error.
104121
*/
105122
extern "C" int
@@ -115,7 +132,7 @@ rtl_stream_start(RtlSdrContext* ctx) {
115132
*
116133
* Safe to call multiple times; subsequent calls are no-ops.
117134
*
118-
* @param ctx Stream context created by rtl_stream_create().
135+
* @param ctx Stream context created by rtl_stream_create() or rtl_stream_create_mirrored().
119136
* @return 0 on success; otherwise <0 on error.
120137
*/
121138
extern "C" int

src/runtime/config_user.cpp

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,17 @@ is_valid_rtl_bw_khz(int bw) {
218218
return (bw == 4 || bw == 6 || bw == 8 || bw == 12 || bw == 16 || bw == 24 || bw == 48);
219219
}
220220

221+
static int
222+
resolve_configured_rtl_ppm(const dsdneoUserConfig* cfg, const dsd_opts* opts) {
223+
if (!cfg) {
224+
return opts ? opts->rtlsdr_ppm_error : 0;
225+
}
226+
if (cfg->rtl_ppm_is_set) {
227+
return cfg->rtl_ppm;
228+
}
229+
return opts ? opts->rtlsdr_ppm_error : 0;
230+
}
231+
221232
static void
222233
apply_shared_radio_tuning_from_config(const dsdneoUserConfig* cfg, dsd_opts* opts) {
223234
if (!cfg || !opts) {
@@ -229,7 +240,7 @@ apply_shared_radio_tuning_from_config(const dsdneoUserConfig* cfg, dsd_opts* opt
229240
}
230241

231242
int gain = cfg->rtl_gain ? cfg->rtl_gain : opts->rtl_gain_value;
232-
int ppm = cfg->rtl_ppm;
243+
int ppm = resolve_configured_rtl_ppm(cfg, opts);
233244
int bw = cfg->rtl_bw_khz ? cfg->rtl_bw_khz : opts->rtl_dsp_bw_khz;
234245
int sql = cfg->rtl_sql;
235246
int vol = cfg->rtl_volume ? cfg->rtl_volume : opts->rtl_volume_multiplier;
@@ -257,6 +268,7 @@ snapshot_apply_live_rtl_values(const dsd_opts* opts, dsdneoUserConfig* cfg) {
257268

258269
cfg->rtl_gain = opts->rtl_gain_value;
259270
cfg->rtl_ppm = opts->rtlsdr_ppm_error;
271+
cfg->rtl_ppm_is_set = 1;
260272
cfg->rtl_bw_khz = opts->rtl_dsp_bw_khz;
261273
cfg->rtl_sql = (int)local_pwr_to_dB(opts->rtl_squelch_level);
262274
cfg->rtl_volume = opts->rtl_volume_multiplier;
@@ -286,6 +298,7 @@ snapshot_parse_rtl_device_spec(const char* audio_in_dev, dsdneoUserConfig* cfg)
286298
}
287299
if (n > 4) {
288300
cfg->rtl_ppm = atoi(tok[4]);
301+
cfg->rtl_ppm_is_set = 1;
289302
}
290303
if (n > 5) {
291304
cfg->rtl_bw_khz = atoi(tok[5]);
@@ -321,6 +334,7 @@ snapshot_parse_rtltcp_device_spec(const char* audio_in_dev, dsdneoUserConfig* cf
321334
}
322335
if (n > 5) {
323336
cfg->rtl_ppm = atoi(tok[5]);
337+
cfg->rtl_ppm_is_set = 1;
324338
}
325339
if (n > 6) {
326340
cfg->rtl_bw_khz = atoi(tok[6]);
@@ -529,7 +543,7 @@ dsd_user_config_render_ini(const dsdneoUserConfig* cfg, FILE* out) {
529543
if (cfg->rtl_gain) {
530544
fprintf(out, "rtl_gain = %d\n", cfg->rtl_gain);
531545
}
532-
if (cfg->rtl_ppm) {
546+
if (cfg->rtl_ppm_is_set) {
533547
fprintf(out, "rtl_ppm = %d\n", cfg->rtl_ppm);
534548
}
535549
if (cfg->rtl_bw_khz) {
@@ -553,7 +567,7 @@ dsd_user_config_render_ini(const dsdneoUserConfig* cfg, FILE* out) {
553567
if (cfg->rtl_gain) {
554568
fprintf(out, "rtl_gain = %d\n", cfg->rtl_gain);
555569
}
556-
if (cfg->rtl_ppm) {
570+
if (cfg->rtl_ppm_is_set) {
557571
fprintf(out, "rtl_ppm = %d\n", cfg->rtl_ppm);
558572
}
559573
if (cfg->rtl_bw_khz) {
@@ -574,7 +588,7 @@ dsd_user_config_render_ini(const dsdneoUserConfig* cfg, FILE* out) {
574588
if (cfg->rtl_gain) {
575589
fprintf(out, "rtl_gain = %d\n", cfg->rtl_gain);
576590
}
577-
if (cfg->rtl_ppm) {
591+
if (cfg->rtl_ppm_is_set) {
578592
fprintf(out, "rtl_ppm = %d\n", cfg->rtl_ppm);
579593
}
580594
if (cfg->rtl_bw_khz) {
@@ -732,7 +746,7 @@ dsd_apply_user_config_to_opts(const dsdneoUserConfig* cfg, dsd_opts* opts, dsd_s
732746
* defaults initOpts()/CLI already established (AGC and
733747
* multiplier 2) instead of forcing hardcoded values. */
734748
int gain = cfg->rtl_gain ? cfg->rtl_gain : opts->rtl_gain_value;
735-
int ppm = cfg->rtl_ppm;
749+
int ppm = resolve_configured_rtl_ppm(cfg, opts);
736750
int bw = cfg->rtl_bw_khz ? cfg->rtl_bw_khz : opts->rtl_dsp_bw_khz;
737751
int sql = cfg->rtl_sql;
738752
int vol = cfg->rtl_volume ? cfg->rtl_volume : opts->rtl_volume_multiplier;
@@ -744,7 +758,7 @@ dsd_apply_user_config_to_opts(const dsdneoUserConfig* cfg, dsd_opts* opts, dsd_s
744758
if (cfg->rtltcp_host[0]) {
745759
if (cfg->rtl_freq[0]) {
746760
int gain = cfg->rtl_gain ? cfg->rtl_gain : opts->rtl_gain_value;
747-
int ppm = cfg->rtl_ppm;
761+
int ppm = resolve_configured_rtl_ppm(cfg, opts);
748762
int bw = cfg->rtl_bw_khz ? cfg->rtl_bw_khz : opts->rtl_dsp_bw_khz;
749763
int sql = cfg->rtl_sql;
750764
int vol = cfg->rtl_volume ? cfg->rtl_volume : opts->rtl_volume_multiplier;

src/runtime/config_user_loader.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ apply_input_section_key(dsdneoUserConfig* cfg, const char* key_lc, const char* v
222222
cfg->rtl_gain = (int)parse_int_for_mode(val, 22, mode);
223223
} else if (strcmp(key_lc, "rtl_ppm") == 0) {
224224
cfg->rtl_ppm = (int)parse_int_for_mode(val, 0, mode);
225+
cfg->rtl_ppm_is_set = 1;
225226
} else if (strcmp(key_lc, "rtl_bw_khz") == 0) {
226227
cfg->rtl_bw_khz = (int)parse_int_for_mode(val, 12, mode);
227228
} else if (strcmp(key_lc, "rtl_sql") == 0) {

src/ui/terminal/menu_services.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ svc_rtl_restart(dsd_opts* opts, dsd_state* state) {
656656
/* If the radio pipeline is the active input, immediately recreate and start the stream
657657
so changes take effect as soon as the user confirms the setting. */
658658
if (opts->audio_in_type == AUDIO_IN_RTL) {
659-
if (rtl_stream_create(opts, &state->rtl_ctx) < 0) {
659+
if (rtl_stream_create_mirrored(opts, &state->rtl_ctx) < 0) {
660660
return -1;
661661
}
662662
if (rtl_stream_start(state->rtl_ctx) < 0) {

src/ui/terminal/ui_cmd_queue.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,13 +1086,15 @@ cfg_uses_rtl_runtime(const dsdneoUserConfig* cfg) {
10861086

10871087
static void
10881088
apply_cfg_live_rtl_ppm_request(dsd_opts* opts, const dsdneoUserConfig* cfg, int old_audio_in_type) {
1089-
if (!opts || old_audio_in_type != AUDIO_IN_RTL || opts->audio_in_type != AUDIO_IN_RTL
1090-
|| !cfg_uses_rtl_runtime(cfg)) {
1089+
if (!opts || old_audio_in_type != AUDIO_IN_RTL || opts->audio_in_type != AUDIO_IN_RTL || !cfg_uses_rtl_runtime(cfg)
1090+
|| !cfg->rtl_ppm_is_set) {
10911091
return;
10921092
}
10931093
/* Config apply must mint a fresh request generation even when the input
10941094
* device string is unchanged, otherwise same-value retries after a failed
1095-
* apply are mistaken for stale state and never reach the controller. */
1095+
* apply are mistaken for stale state and never reach the controller.
1096+
* Omitted rtl_ppm must preserve the live correction instead of clearing it
1097+
* back to the config struct's zero-initialized default. */
10961098
(void)rtl_stream_request_ppm(opts, cfg->rtl_ppm);
10971099
}
10981100

tests/io/test_io_rtl_stream_ppm_sync.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,70 @@ test_adjust_and_getter_use_active_snapshot_when_caller_is_stale(void) {
7777
return rc;
7878
}
7979

80+
static int
81+
test_c_api_create_mirrored_updates_live_caller_opts(void) {
82+
dsd_opts caller_opts;
83+
init_test_opts(&caller_opts);
84+
caller_opts.rtlsdr_ppm_error = 11;
85+
86+
RtlSdrContext* ctx = nullptr;
87+
88+
int rc = 0;
89+
rc |= expect_int_eq("c api mirrored create rc", rtl_stream_create_mirrored(&caller_opts, &ctx), 0);
90+
if (!ctx) {
91+
std::fprintf(stderr, "FAIL: c api mirrored create returned null ctx\n");
92+
return 1;
93+
}
94+
95+
dsd_opts request_opts;
96+
init_test_opts(&request_opts);
97+
request_opts.rtlsdr_ppm_error = 0;
98+
99+
rc |= expect_int_eq("request through live helper rc", rtl_stream_request_ppm(&request_opts, -12), 0);
100+
rc |= expect_int_eq("mirrored caller updated", caller_opts.rtlsdr_ppm_error, -12);
101+
rc |= expect_int_eq("request opts updated", request_opts.rtlsdr_ppm_error, -12);
102+
rc |= expect_int_eq("getter observes mirrored caller", rtl_stream_get_requested_ppm(&caller_opts), -12);
103+
104+
rc |= expect_int_eq("c api mirrored destroy rc", rtl_stream_destroy(ctx), 0);
105+
return rc;
106+
}
107+
108+
static int
109+
test_c_api_create_preserves_const_source_snapshot(void) {
110+
dsd_opts seed_opts;
111+
init_test_opts(&seed_opts);
112+
seed_opts.rtlsdr_ppm_error = 11;
113+
114+
const dsd_opts const_source_opts = seed_opts;
115+
RtlSdrContext* ctx = nullptr;
116+
117+
int rc = 0;
118+
rc |= expect_int_eq("c api create rc", rtl_stream_create(&const_source_opts, &ctx), 0);
119+
if (!ctx) {
120+
std::fprintf(stderr, "FAIL: c api create returned null ctx\n");
121+
return 1;
122+
}
123+
124+
dsd_opts request_opts;
125+
init_test_opts(&request_opts);
126+
request_opts.rtlsdr_ppm_error = 0;
127+
128+
rc |= expect_int_eq("request through live helper rc", rtl_stream_request_ppm(&request_opts, -12), 0);
129+
rc |= expect_int_eq("const source opts remain unchanged", const_source_opts.rtlsdr_ppm_error, 11);
130+
rc |= expect_int_eq("request opts updated", request_opts.rtlsdr_ppm_error, -12);
131+
rc |= expect_int_eq("getter still observes active snapshot", rtl_stream_get_requested_ppm(&const_source_opts), -12);
132+
133+
rc |= expect_int_eq("c api destroy rc", rtl_stream_destroy(ctx), 0);
134+
return rc;
135+
}
136+
80137
int
81138
main(void) {
82139
int rc = 0;
83140
rc |= test_caller_request_updates_active_snapshot();
84141
rc |= test_active_request_updates_caller_snapshot();
85142
rc |= test_adjust_and_getter_use_active_snapshot_when_caller_is_stale();
143+
rc |= test_c_api_create_mirrored_updates_live_caller_opts();
144+
rc |= test_c_api_create_preserves_const_source_snapshot();
86145
return rc ? 1 : 0;
87146
}

0 commit comments

Comments
 (0)