MRTKLIB 0.7.14
Modernized GNSS Positioning Library
Loading...
Searching...
No Matches
mrtk_toml.c
Go to the documentation of this file.
1/*------------------------------------------------------------------------------
2 * mrtk_toml.c : TOML configuration file loader for MRTKLIB
3 *
4 * Copyright (C) 2026 H.SHIONO (MRTKLIB Project)
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 *
8 * Maps TOML semantic section.key paths → legacy sysopts[] option names,
9 * then uses the existing str2opt() infrastructure to set values.
10 *----------------------------------------------------------------------------*/
11#include "mrtklib/mrtk_toml.h"
12
13#include <stdio.h>
14#include <stdlib.h>
15#include <string.h>
16#include <sys/stat.h>
17
18#include "mrtklib/mrtk_const.h"
19#include "mrtklib/mrtk_trace.h"
20#include "tomlc99/toml.h"
21
22/* ── TOML path → legacy option name mapping ────────────────────────────────── */
23
24typedef struct {
25 const char* toml_section; /* e.g. "positioning" */
26 const char* toml_key; /* e.g. "mode" */
27 const char* legacy_name; /* e.g. "pos1-posmode" */
29
30static const toml_map_t toml_mapping[] = {
31 /* ── positioning ───────────────────────────────────────────────────────── */
32 {"positioning", "mode", "pos1-posmode"},
33 {"positioning", "correction", "pos1-correction"},
34 {"positioning", "frequency", "pos1-frequency"},
35 {"positioning", "solution_type", "pos1-soltype"},
36 {"positioning", "elevation_mask", "pos1-elmask"},
37 {"positioning", "dynamics", "pos1-dynamics"},
38 {"positioning", "satellite_ephemeris", "pos1-sateph"},
39 {"positioning", "constellations", "pos1-navsys"},
40 {"positioning", "excluded_sats", "pos1-exclsats"},
41 {"positioning", "bds2_sats", "pos2-bds2prn"},
42 {"positioning", "signals", "pos1-signals"},
43 {"positioning", "robust", "pos1-robust"},
44 {"positioning", "robust_k0", "pos1-robustk0"},
45 {"positioning", "robust_k1", "pos1-robustk1"},
46 {"positioning", "tdcp", "pos1-tdcp"},
47 {"positioning", "tdcp_jump", "pos1-tdcpjump"},
48
49 /* ── positioning.clas ──────────────────────────────────────────────────── */
50 {"positioning.clas", "grid_selection_radius", "pos1-gridsel"},
51 {"positioning.clas", "receiver_type", "pos1-rectype"},
52 {"positioning.clas", "enhanced_spp_seed", "pos1-seedenh"},
53
54 /* ── positioning.snr_mask ──────────────────────────────────────────────── */
55 {"positioning.snr_mask", "rover_enabled", "pos1-snrmask_r"},
56 {"positioning.snr_mask", "base_enabled", "pos1-snrmask_b"},
57 {"positioning.snr_mask", "L1", "pos1-snrmask_L1"},
58 {"positioning.snr_mask", "L2", "pos1-snrmask_L2"},
59 {"positioning.snr_mask", "L5", "pos1-snrmask_L5"},
60
61 /* ── positioning.corrections ───────────────────────────────────────────── */
62 {"positioning.corrections", "satellite_antenna", "pos1-posopt1"},
63 {"positioning.corrections", "receiver_antenna", "pos1-posopt2"},
64 {"positioning.corrections", "phase_windup", "pos1-posopt3"},
65 {"positioning.corrections", "exclude_eclipse", "pos1-posopt4"},
66 {"positioning.corrections", "raim_fde", "pos1-posopt5"},
67 {"positioning.corrections", "iono_compensation", "pos1-posopt6"},
68 {"positioning.corrections", "partial_ar", "pos1-posopt7"},
69 {"positioning.corrections", "shapiro_delay", "pos1-posopt8"},
70 {"positioning.corrections", "exclude_qzs_ref", "pos1-posopt9"},
71 {"positioning.corrections", "no_phase_bias_adj", "pos1-posopt10"},
72 {"positioning.corrections", "gps_frequency", "pos1-posopt11"},
73 {"positioning.corrections", "snr_fixed", "pos1-posopt12"},
74 {"positioning.corrections", "qzs_frequency", "pos1-posopt13"},
75 {"positioning.corrections", "tidal_correction", "pos1-tidecorr"},
76
77 /* ── positioning.atmosphere ─────────────────────────────────────────────── */
78 {"positioning.atmosphere", "ionosphere", "pos1-ionoopt"},
79 {"positioning.atmosphere", "troposphere", "pos1-tropopt"},
80
81 /* ── ambiguity_resolution ──────────────────────────────────────────────── */
82 {"ambiguity_resolution", "mode", "pos2-armode"},
83 {"ambiguity_resolution", "gps_ar", "pos2-gpsarmode"},
84 {"ambiguity_resolution", "glonass_ar", "pos2-gloarmode"},
85 {"ambiguity_resolution", "bds_ar", "pos2-bdsarmode"},
86 {"ambiguity_resolution", "qzs_ar", "pos2-qzsarmode"},
87 {"ambiguity_resolution", "bds3_igso_sats", "pos2-bds3igso"},
88 {"ambiguity_resolution", "systems", "pos2-arsys"},
89
90 /* ── ambiguity_resolution.thresholds ───────────────────────────────────── */
91 {"ambiguity_resolution.thresholds", "ratio", "pos2-arthres"},
92 {"ambiguity_resolution.thresholds", "ratio1", "pos2-arthres1"},
93 {"ambiguity_resolution.thresholds", "ratio5", "pos2-arthres5"},
94 {"ambiguity_resolution.thresholds", "ratio6", "pos2-arthres6"},
95 {"ambiguity_resolution.thresholds", "alpha", "pos2-aralpha"},
96 {"ambiguity_resolution.thresholds", "elevation_mask", "pos2-arelmask"},
97 {"ambiguity_resolution.thresholds", "hold_elevation", "pos2-elmaskhold"},
98 {"ambiguity_resolution.thresholds", "max_pdop_ar", "pos2-maxpdopar"},
99 {"ambiguity_resolution.thresholds", "max_pdop_hold", "pos2-maxpdophold"},
100 {"ambiguity_resolution.thresholds", "reference_dop", "pos2-refdop"},
101
102 /* ── ambiguity_resolution.counters ─────────────────────────────────────── */
103 {"ambiguity_resolution.counters", "lock_count", "pos2-arlockcnt"},
104 {"ambiguity_resolution.counters", "min_fix", "pos2-arminfix"},
105 {"ambiguity_resolution.counters", "max_iterations", "pos2-armaxiter"},
106 {"ambiguity_resolution.counters", "out_count", "pos2-aroutcnt"},
107
108 /* ── ambiguity_resolution.partial_ar ───────────────────────────────────── */
109 {"ambiguity_resolution.partial_ar", "min_ambiguities", "pos2-arminamb"},
110 {"ambiguity_resolution.partial_ar", "max_excluded_sats", "pos2-armaxdelsat"},
111 {"ambiguity_resolution.partial_ar", "min_fix_sats", "pos2-arminfixsats"},
112 {"ambiguity_resolution.partial_ar", "min_drop_sats", "pos2-armindropsats"},
113 {"ambiguity_resolution.partial_ar", "min_hold_sats", "pos2-arminholdsats"},
114 {"ambiguity_resolution.partial_ar", "ar_filter", "pos2-arfilter"},
115
116 /* ── ambiguity_resolution.hold ─────────────────────────────────────────── */
117 {"ambiguity_resolution.hold", "variance", "pos2-varholdamb"},
118
119 /* ── rejection ─────────────────────────────────────────────────────────── */
120 {"rejection", "innovation", "pos2-rejionno"},
121 {"rejection", "l1_l2_residual", "pos2-rejionno1"},
122 {"rejection", "dispersive", "pos2-rejionno2"},
123 {"rejection", "non_dispersive", "pos2-rejionno3"},
124 {"rejection", "hold_chi_square", "pos2-rejionno4"},
125 {"rejection", "fix_chi_square", "pos2-rejionno5"},
126 {"rejection", "spp_residual", "pos2-rejethres"},
127 {"rejection", "spp_min_sats", "pos2-rejeminsat"},
128 {"rejection", "gdop", "pos2-rejgdop"},
129 {"rejection", "pseudorange_diff", "pos2-rejdiffpse"},
130 {"rejection", "position_error_count", "pos2-poserrcnt"},
131
132 /* ── slip_detection ────────────────────────────────────────────────────── */
133 {"slip_detection", "threshold", "pos2-slipthres"},
134 {"slip_detection", "doppler", "pos2-thresdop"},
135
136 /* ── kalman_filter ─────────────────────────────────────────────────────── */
137 {"kalman_filter", "iterations", "pos2-niter"},
138
139 /* ── kalman_filter.measurement_error ───────────────────────────────────── */
140 {"kalman_filter.measurement_error", "code_phase_ratio_L1", "stats-eratio1"},
141 {"kalman_filter.measurement_error", "code_phase_ratio_L2", "stats-eratio2"},
142 {"kalman_filter.measurement_error", "code_phase_ratio_L5", "stats-eratio3"},
143 {"kalman_filter.measurement_error", "phase", "stats-errphase"},
144 {"kalman_filter.measurement_error", "phase_elevation", "stats-errphaseel"},
145 {"kalman_filter.measurement_error", "phase_baseline", "stats-errphasebl"},
146 {"kalman_filter.measurement_error", "doppler", "stats-errdoppler"},
147 {"kalman_filter.measurement_error", "snr_max", "stats-snrmax"},
148 {"kalman_filter.measurement_error", "snr_error", "stats-errsnr"},
149 {"kalman_filter.measurement_error", "ionosphere", "stats-erriono"},
150 {"kalman_filter.measurement_error", "troposphere", "stats-errtrop"},
151 {"kalman_filter.measurement_error", "ura_ratio", "stats-uraratio"},
152
153 /* ── kalman_filter.initial_std ─────────────────────────────────────────── */
154 {"kalman_filter.initial_std", "bias", "stats-stdbias"},
155 {"kalman_filter.initial_std", "ionosphere", "stats-stdiono"},
156 {"kalman_filter.initial_std", "troposphere", "stats-stdtrop"},
157
158 /* ── kalman_filter.process_noise ───────────────────────────────────────── */
159 {"kalman_filter.process_noise", "bias", "stats-prnbias"},
160 {"kalman_filter.process_noise", "ionosphere", "stats-prniono"},
161 {"kalman_filter.process_noise", "iono_max", "stats-prnionomax"},
162 {"kalman_filter.process_noise", "troposphere", "stats-prntrop"},
163 {"kalman_filter.process_noise", "accel_h", "stats-prnaccelh"},
164 {"kalman_filter.process_noise", "accel_v", "stats-prnaccelv"},
165 {"kalman_filter.process_noise", "position_h", "stats-prnposith"},
166 {"kalman_filter.process_noise", "position_v", "stats-prnpositv"},
167 {"kalman_filter.process_noise", "position", "stats-prnpos"},
168 {"kalman_filter.process_noise", "ifb", "stats-prnifb"},
169 {"kalman_filter.process_noise", "iono_time_const", "stats-tconstiono"},
170 {"kalman_filter.process_noise", "clock_stability", "stats-clkstab"},
171
172 /* ── positioning.clas.adaptive_filter ──────────────────────────────────── */
173 {"positioning.clas.adaptive_filter", "enabled", "pos2-prnadpt"},
174 {"positioning.clas.adaptive_filter", "iono_forgetting", "pos2-forgetion"},
175 {"positioning.clas.adaptive_filter", "iono_gain", "pos2-afgainion"},
176 {"positioning.clas.adaptive_filter", "pva_forgetting", "pos2-forgetpva"},
177 {"positioning.clas.adaptive_filter", "pva_gain", "pos2-afgainpva"},
178
179 /* ── signals ───────────────────────────────────────────────────────────── */
180 {"signals", "gps", "pos2-siggps"},
181 {"signals", "qzs", "pos2-sigqzs"},
182 {"signals", "galileo", "pos2-siggal"},
183 {"signals", "bds2", "pos2-sigbds2"},
184 {"signals", "bds3", "pos2-sigbds3"},
185
186 /* ── positioning engine options ───────────────────────────────────────── */
187 {"positioning.spp", "ignore_chi_error", "pos2-ign_chierr"},
188 {"positioning.madoca", "iono_correction", "pos2-ionocorr"},
189 {"positioning.ppp", "satellite_clock_bias", "pos2-pppsatcb"},
190 {"positioning.ppp", "satellite_phase_bias", "pos2-pppsatpb"},
191 {"positioning.ppp", "drop_uncorrected_code", "pos2-uncorrbias"},
192 {"positioning.ppp", "clock_jump", "pos2-clkjump"},
193 {"positioning.ppp", "max_bias_dt", "pos2-maxbiasdt"},
194 {"positioning.ppp", "options", "misc-pppopt"},
195 {"positioning.clas.ambiguities", "phase_shift", "pos2-phasshft"},
196 {"positioning.clas.ambiguities", "isb", "pos2-isb"},
197 {"positioning.clas.ambiguities", "reference_type", "pos2-rectype"},
198 {"positioning.clas.resilience", "max_obs_loss", "misc-maxobsloss"},
199 {"positioning.clas.resilience", "float_count", "misc-floatcnt"},
200 {"positioning.clas.resilience", "l6_merge", "misc-l6mrg"},
201 {"positioning.clas.resilience", "reset_interval", "misc-regularly"},
202 {"positioning.relative", "max_age", "pos2-maxage"},
203 {"positioning.relative", "baseline_length", "pos2-baselen"},
204 {"positioning.relative", "baseline_sigma", "pos2-basesig"},
205 {"positioning.relative", "time_interpolation", "misc-timeinterp"},
206
207 /* ── input decoding ───────────────────────────────────────────────────── */
208 {"input.rinex", "option_1", "misc-rnxopt1"},
209 {"input.rinex", "option_2", "misc-rnxopt2"},
210 {"input.rtcm", "options", "misc-rtcmopt"},
211 {"input.sbas", "satellite", "misc-sbasatsel"},
212
213 /* ── antenna.rover ─────────────────────────────────────────────────────── */
214 {"antenna.rover", "position_type", "ant1-postype"},
215 {"antenna.rover", "position_1", "ant1-pos1"},
216 {"antenna.rover", "position_2", "ant1-pos2"},
217 {"antenna.rover", "position_3", "ant1-pos3"},
218 {"antenna.rover", "type", "ant1-anttype"},
219 {"antenna.rover", "delta_e", "ant1-antdele"},
220 {"antenna.rover", "delta_n", "ant1-antdeln"},
221 {"antenna.rover", "delta_u", "ant1-antdelu"},
222
223 /* ── antenna.base ──────────────────────────────────────────────────────── */
224 {"antenna.base", "position_type", "ant2-postype"},
225 {"antenna.base", "position_1", "ant2-pos1"},
226 {"antenna.base", "position_2", "ant2-pos2"},
227 {"antenna.base", "position_3", "ant2-pos3"},
228 {"antenna.base", "type", "ant2-anttype"},
229 {"antenna.base", "delta_e", "ant2-antdele"},
230 {"antenna.base", "delta_n", "ant2-antdeln"},
231 {"antenna.base", "delta_u", "ant2-antdelu"},
232 {"antenna.base", "max_average_epochs", "ant2-maxaveep"},
233 {"antenna.base", "init_reset", "ant2-initrst"},
234
235 /* ── output ────────────────────────────────────────────────────────────── */
236 {"output", "format", "out-solformat"},
237 {"output", "header", "out-outhead"},
238 {"output", "options", "out-outopt"},
239 {"output", "velocity", "out-outvel"},
240 {"output", "time_system", "out-timesys"},
241 {"output", "time_format", "out-timeform"},
242 {"output", "time_decimals", "out-timendec"},
243 {"output", "coordinate_format", "out-degform"},
244 {"output", "field_separator", "out-fieldsep"},
245 {"output", "single_output", "out-outsingle"},
246 {"output", "max_solution_std", "out-maxsolstd"},
247 {"output", "height_type", "out-height"},
248 {"output", "geoid_model", "out-geoid"},
249 {"output", "static_solution", "out-solstatic"},
250 {"output", "nmea_interval_1", "out-nmeaintv1"},
251 {"output", "nmea_interval_2", "out-nmeaintv2"},
252 {"output", "solution_status", "out-outstat"},
253
254 /* ── files ─────────────────────────────────────────────────────────────── */
255 {"files", "satellite_atx", "file-satantfile"},
256 {"files", "receiver_atx", "file-rcvantfile"},
257 {"files", "station_pos", "file-staposfile"},
258 {"files", "geoid", "file-geoidfile"},
259 {"files", "ionosphere", "file-ionofile"},
260 {"files", "dcb", "file-dcbfile"},
261 {"files", "eop", "file-eopfile"},
262 {"files", "ocean_loading", "file-blqfile"},
263 {"files", "temp_dir", "file-tempdir"},
264 {"files", "trace", "file-tracefile"},
265 {"files", "fcb", "file-fcbfile"},
266 {"files", "bias_sinex", "file-biafile"},
267 {"files", "cssr_grid", "file-cssrgridfile"},
268 {"files", "isb_table", "file-isbfile"},
269 {"files", "phase_cycle", "file-phacycfile"},
270
271 /* ── server (rtkrcv runtime only) ──────────────────────────────────────── */
272 {"server", "cycle_ms", "misc-svrcycle"},
273 {"server", "timeout_ms", "misc-timeout"},
274 {"server", "reconnect_ms", "misc-reconnect"},
275 {"server", "nmea_cycle_ms", "misc-nmeacycle"},
276 {"server", "buffer_size", "misc-buffsize"},
277 {"server", "nav_msg_select", "misc-navmsgsel"},
278 {"server", "proxy", "misc-proxyaddr"},
279 {"server", "swap_margin", "misc-fswapmargin"},
280 {"server", "start_cmd", "misc-startcmd"},
281 {"server", "stop_cmd", "misc-stopcmd"},
282
283 /* ── streams (rtkrcv rcvopts) ──────────────────────────────────────────── */
284 {"streams.input.rover", "type", "inpstr1-type"},
285 {"streams.input.rover", "path", "inpstr1-path"},
286 {"streams.input.rover", "format", "inpstr1-format"},
287 {"streams.input.base", "type", "inpstr2-type"},
288 {"streams.input.base", "path", "inpstr2-path"},
289 {"streams.input.base", "format", "inpstr2-format"},
290 {"streams.input.base", "nmeareq", "inpstr2-nmeareq"},
291 {"streams.input.base", "nmealat", "inpstr2-nmealat"},
292 {"streams.input.base", "nmealon", "inpstr2-nmealon"},
293 {"streams.input.base", "nmeahgt", "inpstr2-nmeahgt"},
294 {"streams.input.correction", "type", "inpstr3-type"},
295 {"streams.input.correction", "path", "inpstr3-path"},
296 {"streams.input.correction", "format", "inpstr3-format"},
297 {"streams.output.stream1", "type", "outstr1-type"},
298 {"streams.output.stream1", "path", "outstr1-path"},
299 {"streams.output.stream1", "format", "outstr1-format"},
300 {"streams.output.stream2", "type", "outstr2-type"},
301 {"streams.output.stream2", "path", "outstr2-path"},
302 {"streams.output.stream2", "format", "outstr2-format"},
303 {"streams.log.stream1", "type", "logstr1-type"},
304 {"streams.log.stream1", "path", "logstr1-path"},
305 {"streams.log.stream2", "type", "logstr2-type"},
306 {"streams.log.stream2", "path", "logstr2-path"},
307 {"streams.log.stream3", "type", "logstr3-type"},
308 {"streams.log.stream3", "path", "logstr3-path"},
309
310 /* ── console (rtkrcv rcvopts) ──────────────────────────────────────────── */
311 {"console", "passwd", "console-passwd"},
312 {"console", "timetype", "console-timetype"},
313 {"console", "soltype", "console-soltype"},
314 {"console", "solflag", "console-solflag"},
315
316 /* ── files (rtkrcv rcvopts) ────────────────────────────────────────────── */
317 {"files", "cmd_file_1", "file-cmdfile1"},
318 {"files", "cmd_file_2", "file-cmdfile2"},
319 {"files", "cmd_file_3", "file-cmdfile3"},
320
321 {NULL, NULL, NULL} /* terminator */
322};
323
324/* ── Deprecated section.key aliases ────────────────────────────────────────── */
325
326/* Old TOML paths retired by the section-taxonomy refactor (#266) and later
327 * renames. Each is still accepted for backward compatibility: loadopts_toml()
328 * applies the value and emits a one-line deprecation warning pointing at the
329 * new path. The new location wins when both are present. saveopts_toml() does
330 * not consult this table, so a re-saved file always uses the new layout.
331 * Add an entry here whenever a section.key is renamed (see #286). */
332typedef struct {
333 const char* old_section;
334 const char* old_key;
335 const char* new_section;
336 const char* new_key;
337 const char* legacy_name;
339
340static const toml_alias_t toml_alias[] = {
341 /* [receiver] dismantled (#266) */
342 {"receiver", "phase_shift", "positioning.clas.ambiguities", "phase_shift", "pos2-phasshft"},
343 {"receiver", "isb", "positioning.clas.ambiguities", "isb", "pos2-isb"},
344 {"receiver", "reference_type", "positioning.clas.ambiguities", "reference_type", "pos2-rectype"},
345 {"receiver", "iono_correction", "positioning.madoca", "iono_correction", "pos2-ionocorr"},
346 {"receiver", "ignore_chi_error", "positioning.spp", "ignore_chi_error", "pos2-ign_chierr"},
347 {"receiver", "ppp_sat_clock_bias", "positioning.ppp", "satellite_clock_bias", "pos2-pppsatcb"},
348 {"receiver", "ppp_sat_phase_bias", "positioning.ppp", "satellite_phase_bias", "pos2-pppsatpb"},
349 {"receiver", "max_bias_dt", "positioning.ppp", "max_bias_dt", "pos2-maxbiasdt"},
350 {"receiver", "uncorr_bias", "positioning.ppp", "drop_uncorrected_code", "pos2-uncorrbias"},
351 {"receiver", "max_age", "positioning.relative", "max_age", "pos2-maxage"},
352 {"receiver", "baseline_length", "positioning.relative", "baseline_length", "pos2-baselen"},
353 {"receiver", "baseline_sigma", "positioning.relative", "baseline_sigma", "pos2-basesig"},
354 /* keys moved out of [server] (#266) */
355 {"server", "max_obs_loss", "positioning.clas.resilience", "max_obs_loss", "misc-maxobsloss"},
356 {"server", "float_count", "positioning.clas.resilience", "float_count", "misc-floatcnt"},
357 {"server", "l6_margin", "positioning.clas.resilience", "l6_merge", "misc-l6mrg"},
358 {"server", "regularly", "positioning.clas.resilience", "reset_interval", "misc-regularly"},
359 {"server", "ppp_option", "positioning.ppp", "options", "misc-pppopt"},
360 {"server", "time_interpolation", "positioning.relative", "time_interpolation", "misc-timeinterp"},
361 {"server", "rinex_option_1", "input.rinex", "option_1", "misc-rnxopt1"},
362 {"server", "rinex_option_2", "input.rinex", "option_2", "misc-rnxopt2"},
363 {"server", "rtcm_option", "input.rtcm", "options", "misc-rtcmopt"},
364 {"server", "sbas_satellite", "input.sbas", "satellite", "misc-sbasatsel"},
365 /* [adaptive_filter] nested under CLAS (#287) */
366 {"adaptive_filter", "enabled", "positioning.clas.adaptive_filter", "enabled", "pos2-prnadpt"},
367 {"adaptive_filter", "iono_forgetting", "positioning.clas.adaptive_filter", "iono_forgetting", "pos2-forgetion"},
368 {"adaptive_filter", "iono_gain", "positioning.clas.adaptive_filter", "iono_gain", "pos2-afgainion"},
369 {"adaptive_filter", "pva_forgetting", "positioning.clas.adaptive_filter", "pva_forgetting", "pos2-forgetpva"},
370 {"adaptive_filter", "pva_gain", "positioning.clas.adaptive_filter", "pva_gain", "pos2-afgainpva"},
371 {NULL, NULL, NULL, NULL, NULL} /* terminator */
372};
373
374/* Sections owned by a specific app (not the positioning option table); their
375 * keys must not be flagged as unknown. */
376static const char* const toml_app_sections[] = {"cssr2rtcm3", "signal_remap", NULL};
377
378/* ── Helper: navigate nested TOML tables ───────────────────────────────────── */
379
380/**
381 * @brief Navigate to a nested TOML table by dotted path.
382 * @param[in] root Root TOML table.
383 * @param[in] path Dotted path (e.g. "positioning.atmosphere").
384 * @return Pointer to the nested table, or NULL if not found.
385 */
386static toml_table_t* navigate_table(toml_table_t* root, const char* path) {
387 char buf[256];
388 char *p, *tok;
389 toml_table_t* tbl = root;
390
391 if (!path || !*path) {
392 return root;
393 }
394
395 strncpy(buf, path, sizeof(buf) - 1);
396 buf[sizeof(buf) - 1] = '\0';
397
398 for (tok = buf; (p = strchr(tok, '.')) != NULL; tok = p + 1) {
399 *p = '\0';
400 tbl = toml_table_in(tbl, tok);
401 if (!tbl) {
402 return NULL;
403 }
404 }
405 /* last segment */
406 if (*tok) {
407 tbl = toml_table_in(tbl, tok);
408 }
409 return tbl;
410}
411
412/* ── Helper: unknown-key detection ─────────────────────────────────────────── */
413
414/**
415 * @brief Report whether a section.key pair is recognized by the loader.
416 * @param[in] section Dotted TOML section path.
417 * @param[in] key Leaf key name.
418 * @return 1 if the key is in the mapping, an alias, or an app-owned section.
419 */
420static int is_known_key(const char* section, const char* key) {
421 const toml_map_t* m;
422 const toml_alias_t* a;
423 const char* const* s;
424
425 for (s = toml_app_sections; *s; s++) {
426 if (!strcmp(section, *s)) {
427 return 1;
428 }
429 }
430 /* positioning.systems is a string-list key handled outside toml_mapping */
431 if (!strcmp(section, "positioning") && !strcmp(key, "systems")) {
432 return 1;
433 }
434 for (m = toml_mapping; m->toml_section; m++) {
435 if (!strcmp(m->toml_section, section) && !strcmp(m->toml_key, key)) {
436 return 1;
437 }
438 }
439 for (a = toml_alias; a->old_section; a++) {
440 if (!strcmp(a->old_section, section) && !strcmp(a->old_key, key)) {
441 return 1;
442 }
443 }
444 return 0;
445}
446
447/**
448 * @brief Recursively walk a parsed TOML table and warn on unrecognized keys.
449 * Sub-tables extend the dotted section path; scalar/array leaves are
450 * checked against is_known_key(). Catches values silently dropped
451 * because their section.key is not in the mapping (typo or stale name).
452 * @param[in] tab TOML table to walk.
453 * @param[in] path Dotted section path accumulated so far ("" at the root).
454 */
455static void check_unknown_keys(toml_table_t* tab, const char* path) {
456 int i, n = toml_table_nkval(tab) + toml_table_narr(tab) + toml_table_ntab(tab);
457 char subpath[256];
458
459 for (i = 0; i < n; i++) {
460 const char* key = toml_key_in(tab, i);
461 toml_table_t* sub;
462 if (!key) {
463 continue;
464 }
465 sub = toml_table_in(tab, key);
466 if (sub) {
467 if (*path) {
468 snprintf(subpath, sizeof(subpath), "%s.%s", path, key);
469 } else {
470 snprintf(subpath, sizeof(subpath), "%s", key);
471 }
472 check_unknown_keys(sub, subpath);
473 } else if (!is_known_key(path, key)) {
474 fprintf(stderr, "TOML: unknown key [%s].%s (ignored)\n", *path ? path : "(root)", key);
475 }
476 }
477}
478
479/* ── Helper: read a TOML value as string for str2opt() ─────────────────────── */
480
481/**
482 * @brief Read a TOML key value and convert to string representation
483 * compatible with str2opt().
484 * @param[in] tbl TOML table containing the key.
485 * @param[in] key Key name within the table.
486 * @param[out] buf Buffer to receive the string value.
487 * @param[in] bufsz Size of buf.
488 * @return 1 if key found and converted, 0 if not found.
489 */
490static int toml_val_to_str(toml_table_t* tbl, const char* key, char* buf, int bufsz) {
491 toml_datum_t d;
492 toml_array_t* arr;
493 int i, n, len;
494 char* p;
495
496 /* Try string first */
497 d = toml_string_in(tbl, key);
498 if (d.ok) {
499 strncpy(buf, d.u.s, bufsz - 1);
500 buf[bufsz - 1] = '\0';
501 free(d.u.s);
502 return 1;
503 }
504
505 /* Try boolean */
506 d = toml_bool_in(tbl, key);
507 if (d.ok) {
508 strncpy(buf, d.u.b ? "on" : "off", bufsz - 1);
509 buf[bufsz - 1] = '\0';
510 return 1;
511 }
512
513 /* Try integer */
514 d = toml_int_in(tbl, key);
515 if (d.ok) {
516 snprintf(buf, bufsz, "%lld", (long long)d.u.i);
517 return 1;
518 }
519
520 /* Try double */
521 d = toml_double_in(tbl, key);
522 if (d.ok) {
523 snprintf(buf, bufsz, "%.15g", d.u.d);
524 return 1;
525 }
526
527 /* Try array (comma-separated: numbers for SNR masks, strings for signals) */
528 arr = toml_array_in(tbl, key);
529 if (arr) {
530 int rem;
531 n = toml_array_nelem(arr);
532 p = buf;
533 for (i = 0; i < n; i++) {
534 rem = (int)(bufsz - (p - buf));
535 if (rem < 2) {
536 break;
537 }
538 /* try string element first */
539 d = toml_string_at(arr, i);
540 if (d.ok) {
541 if (i > 0) {
542 *p++ = ',';
543 rem--;
544 }
545 len = snprintf(p, rem, "%s", d.u.s);
546 p += (len < rem) ? len : rem - 1;
547 free(d.u.s);
548 continue;
549 }
550 d = toml_double_at(arr, i);
551 if (d.ok) {
552 if (i > 0) {
553 *p++ = ',';
554 rem--;
555 }
556 len = snprintf(p, rem, "%.0f", d.u.d);
557 p += (len < rem) ? len : rem - 1;
558 } else {
559 d = toml_int_at(arr, i);
560 if (d.ok) {
561 if (i > 0) {
562 *p++ = ',';
563 rem--;
564 }
565 len = snprintf(p, rem, "%lld", (long long)d.u.i);
566 p += (len < rem) ? len : rem - 1;
567 }
568 }
569 }
570 *p = '\0';
571 return 1;
572 }
573
574 return 0;
575}
576
577/* ── constellation name → bitmask conversion ──────────────────────────────── */
578
579static int navsys_stricmp(const char* a, const char* b) {
580 for (; *a && *b; a++, b++) {
581 int ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
582 int cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
583 if (ca != cb) return ca - cb;
584 }
585 return (unsigned char)*a - (unsigned char)*b;
586}
587
588static int navsys_str2mask(const char* name) {
589 static const struct {
590 const char* str;
591 int mask;
592 } tbl[] = {
593 {"GPS", SYS_GPS}, {"G", SYS_GPS}, {"SBAS", SYS_SBS}, {"S", SYS_SBS}, {"GLONASS", SYS_GLO},
594 {"GLO", SYS_GLO}, {"R", SYS_GLO}, {"Galileo", SYS_GAL}, {"GAL", SYS_GAL}, {"E", SYS_GAL},
595 {"QZSS", SYS_QZS}, {"QZS", SYS_QZS}, {"J", SYS_QZS}, {"BeiDou", SYS_CMP}, {"BDS", SYS_CMP},
596 {"CMP", SYS_CMP}, {"C", SYS_CMP}, {"NavIC", SYS_IRN}, {"IRNSS", SYS_IRN}, {"IRN", SYS_IRN},
597 {"I", SYS_IRN},
598 };
599 int i;
600 for (i = 0; i < (int)(sizeof(tbl) / sizeof(tbl[0])); i++) {
601 if (!navsys_stricmp(name, tbl[i].str)) return tbl[i].mask;
602 }
603 return 0;
604}
605
606/* ── loadopts_toml ─────────────────────────────────────────────────────────── */
607
608extern int loadopts_toml(const char* file, opt_t* opts) {
609 FILE* fp;
610 toml_table_t* root;
611 char errbuf[256];
612 const toml_map_t* m;
613 const toml_alias_t* a;
614 toml_table_t* tbl;
615 opt_t* opt;
616 char valbuf[2048];
617 int count = 0;
618
619 trace(NULL, 3, "loadopts_toml: file=%s\n", file);
620
621 if (!(fp = fopen(file, "r"))) {
622 trace(NULL, 1, "loadopts_toml: file open error (%s)\n", file);
623 return 0;
624 }
625
626 root = toml_parse_file(fp, errbuf, sizeof(errbuf));
627 fclose(fp);
628
629 if (!root) {
630 trace(NULL, 1, "loadopts_toml: parse error (%s): %s\n", file, errbuf);
631 fprintf(stderr, "TOML parse error in %s: %s\n", file, errbuf);
632 return 0;
633 }
634
635 /* Walk the mapping table */
636 for (m = toml_mapping; m->toml_section; m++) {
637 /* Navigate to the TOML section */
638 tbl = navigate_table(root, m->toml_section);
639 if (!tbl) {
640 continue;
641 }
642
643 /* Read the value */
644 if (!toml_val_to_str(tbl, m->toml_key, valbuf, sizeof(valbuf))) {
645 continue;
646 }
647
648 /* Find the legacy option and set it */
649 opt = searchopt(m->legacy_name, opts);
650 if (!opt) {
651 trace(NULL, 2, "loadopts_toml: unknown legacy key %s\n", m->legacy_name);
652 continue;
653 }
654
655 if (!str2opt(opt, valbuf)) {
656 fprintf(stderr, "TOML: invalid value for %s.%s = %s (legacy: %s)\n", m->toml_section, m->toml_key, valbuf,
657 m->legacy_name);
658 continue;
659 }
660 count++;
661 }
662
663 /* Accept retired section.key paths for backward compatibility, warning the
664 * user to migrate. The new location (already read above) takes precedence.
665 * Mirrors the main mapping loop's error handling: stay silent when this
666 * opts table does not own the legacy option (e.g. the same file loaded
667 * into rcvopts), and report an invalid value on a str2opt() failure. */
668 for (a = toml_alias; a->old_section; a++) {
669 toml_table_t* ntbl;
670 tbl = navigate_table(root, a->old_section);
671 if (!tbl || !toml_val_to_str(tbl, a->old_key, valbuf, sizeof(valbuf))) {
672 continue; /* old key not present */
673 }
674 opt = searchopt(a->legacy_name, opts);
675 if (!opt) {
676 continue; /* legacy option not in this opts table */
677 }
678 ntbl = navigate_table(root, a->new_section);
679 if (ntbl && toml_key_exists(ntbl, a->new_key)) {
680 fprintf(stderr, "TOML: [%s].%s is deprecated and ignored; [%s].%s takes precedence\n", a->old_section,
681 a->old_key, a->new_section, a->new_key);
682 continue;
683 }
684 fprintf(stderr, "TOML: [%s].%s is deprecated; move it to [%s].%s\n", a->old_section, a->old_key, a->new_section,
685 a->new_key);
686 if (!str2opt(opt, valbuf)) {
687 fprintf(stderr, "TOML: invalid value for %s.%s = %s (legacy: %s)\n", a->old_section, a->old_key, valbuf,
688 a->legacy_name);
689 continue;
690 }
691 count++;
692 }
693
694 /* Handle positioning.systems string list (overrides constellations) */
695 {
696 toml_table_t* pos_tbl = navigate_table(root, "positioning");
697 if (pos_tbl) {
698 toml_array_t* arr = toml_array_in(pos_tbl, "systems");
699 if (arr) {
700 int mask = 0, nelem = toml_array_nelem(arr);
701 int k;
702 for (k = 0; k < nelem; k++) {
703 toml_datum_t d = toml_string_at(arr, k);
704 if (d.ok) {
705 int sys = navsys_str2mask(d.u.s);
706 if (sys) {
707 mask |= sys;
708 } else {
709 fprintf(stderr, "TOML: unknown constellation '%s' in positioning.systems\n", d.u.s);
710 }
711 free(d.u.s);
712 }
713 }
714 {
715 opt_t* opt = searchopt("pos1-navsys", opts);
716 if (opt) {
717 *(int*)opt->var = mask;
718 }
719 }
720 }
721 }
722 }
723
724 /* Handle positioning.excluded_sats as string list (joins to space-separated) */
725 {
726 toml_table_t* pos_tbl = navigate_table(root, "positioning");
727 if (pos_tbl) {
728 toml_array_t* arr = toml_array_in(pos_tbl, "excluded_sats");
729 if (arr) {
730 opt_t* opt = searchopt("pos1-exclsats", opts);
731 if (opt) {
732 char* buf = (char*)opt->var;
733 int nelem = toml_array_nelem(arr), k, pos = 0;
734 buf[0] = '\0';
735 for (k = 0; k < nelem; k++) {
736 toml_datum_t d = toml_string_at(arr, k);
737 if (d.ok) {
738 if (pos > 0 && pos < 1023) {
739 buf[pos++] = ' ';
740 }
741 int len = (int)strlen(d.u.s);
742 if (pos + len < 1024) {
743 memcpy(buf + pos, d.u.s, len);
744 pos += len;
745 }
746 free(d.u.s);
747 }
748 }
749 buf[pos] = '\0';
750 }
751 }
752 }
753 }
754
755 /* Warn on any key the loader does not recognize (typo, or a stale name with
756 * no alias) so it does not silently fall back to a default. rtkrcv loads
757 * the same file twice per logical load (once for rcvopts, once for
758 * sysopts), which would print every warning twice; sweep once per file
759 * fingerprint (path + mtime + size), so an edited file is re-checked on
760 * reload but a back-to-back second pass is not. When the file cannot be
761 * fingerprinted, fail open: sweeping twice beats losing the warnings. */
762 {
763 static char swept_path[MAXSTRPATH];
764 static time_t swept_mtime;
765 static off_t swept_size;
766 struct stat st;
767 if (stat(file, &st) != 0) {
768 check_unknown_keys(root, "");
769 } else if (strcmp(file, swept_path) != 0 || st.st_mtime != swept_mtime || st.st_size != swept_size) {
770 check_unknown_keys(root, "");
771 snprintf(swept_path, sizeof(swept_path), "%s", file);
772 swept_mtime = st.st_mtime;
773 swept_size = st.st_size;
774 }
775 }
776
777 toml_free(root);
778
779 trace(NULL, 3, "loadopts_toml: loaded %d options from %s\n", count, file);
780 return 1;
781}
782
783/* ── saveopts_toml ─────────────────────────────────────────────────────────── */
784
785extern int saveopts_toml(const char* file, const char* comment, const opt_t* opts) {
786 FILE* fp;
787 const toml_map_t* m;
788 opt_t* opt;
789 char valbuf[2048];
790 const char* prev_section = "";
791
792 trace(NULL, 3, "saveopts_toml: file=%s\n", file);
793
794 if (!(fp = fopen(file, "w"))) {
795 trace(NULL, 1, "saveopts_toml: file open error (%s)\n", file);
796 return 0;
797 }
798
799 fprintf(fp, "# MRTKLIB Configuration (TOML v1.0.0)\n");
800 if (comment) {
801 fprintf(fp, "# %s\n", comment);
802 }
803 fprintf(fp, "\n");
804
805 for (m = toml_mapping; m->toml_section; m++) {
806 opt = searchopt(m->legacy_name, (opt_t*)opts);
807 if (!opt) {
808 continue;
809 }
810
811 /* Section header */
812 if (strcmp(m->toml_section, prev_section) != 0) {
813 if (*prev_section) {
814 fprintf(fp, "\n");
815 }
816 fprintf(fp, "[%s]\n", m->toml_section);
817 prev_section = m->toml_section;
818 }
819
820 /* Value */
821 opt2str(opt, valbuf);
822
823 /* Format based on type */
824 switch (opt->format) {
825 case 0: /* int */
826 fprintf(fp, "%-24s = %s\n", m->toml_key, valbuf);
827 break;
828 case 1: /* double */
829 fprintf(fp, "%-24s = %s\n", m->toml_key, valbuf);
830 break;
831 case 2: /* string */
832 fprintf(fp, "%-24s = \"%s\"\n", m->toml_key, valbuf);
833 break;
834 case 3: /* enum */
835 fprintf(fp, "%-24s = \"%s\"\n", m->toml_key, valbuf);
836 break;
837 }
838 }
839
840 fclose(fp);
841 return 1;
842}
MRTKLIB Constants — Physical, navigation, and system constants.
#define SYS_GPS
Definition mrtk_eph.c:65
#define SYS_SBS
Definition mrtk_eph.c:66
#define SYS_QZS
Definition mrtk_eph.c:69
#define SYS_IRN
Definition mrtk_eph.c:71
#define SYS_CMP
Definition mrtk_eph.c:70
#define SYS_GAL
Definition mrtk_eph.c:68
#define SYS_GLO
Definition mrtk_eph.c:67
#define MAXSTRPATH
int opt2str(const opt_t *opt, char *str)
Convert option value to string.
opt_t * searchopt(const char *name, const opt_t *opts)
Search option record.
int str2opt(opt_t *opt, const char *str)
Convert string to option value.
static int navsys_stricmp(const char *a, const char *b)
Definition mrtk_toml.c:579
static const char *const toml_app_sections[]
Definition mrtk_toml.c:376
static const toml_alias_t toml_alias[]
Definition mrtk_toml.c:340
static const toml_map_t toml_mapping[]
Definition mrtk_toml.c:30
static int toml_val_to_str(toml_table_t *tbl, const char *key, char *buf, int bufsz)
Read a TOML key value and convert to string representation compatible with str2opt().
Definition mrtk_toml.c:490
static void check_unknown_keys(toml_table_t *tab, const char *path)
Recursively walk a parsed TOML table and warn on unrecognized keys. Sub-tables extend the dotted sect...
Definition mrtk_toml.c:455
static toml_table_t * navigate_table(toml_table_t *root, const char *path)
Navigate to a nested TOML table by dotted path.
Definition mrtk_toml.c:386
static int is_known_key(const char *section, const char *key)
Report whether a section.key pair is recognized by the loader.
Definition mrtk_toml.c:420
int loadopts_toml(const char *file, opt_t *opts)
Load options from a TOML configuration file.
Definition mrtk_toml.c:608
int saveopts_toml(const char *file, const char *comment, const opt_t *opts)
Save options to a TOML configuration file.
Definition mrtk_toml.c:785
static int navsys_str2mask(const char *name)
Definition mrtk_toml.c:588
void trace(mrtk_ctx_t *ctx, int level, const char *format,...)
Output trace message.
Definition mrtk_trace.c:78
MRTKLIB Trace Module — debug trace and logging functions.
Option type for configuration file parsing.
int format
void * var
const char * new_key
Definition mrtk_toml.c:336
const char * new_section
Definition mrtk_toml.c:335
const char * old_section
Definition mrtk_toml.c:333
const char * legacy_name
Definition mrtk_toml.c:337
const char * old_key
Definition mrtk_toml.c:334
union toml_datum_t::@034014057205341050151107111126247137201246022032 u
int b
Definition toml.h:87
double d
Definition toml.h:89
char * s
Definition toml.h:86
int ok
Definition toml.h:83
int64_t i
Definition toml.h:88
const char * toml_key
Definition mrtk_toml.c:26
const char * toml_section
Definition mrtk_toml.c:25
const char * legacy_name
Definition mrtk_toml.c:27
int toml_table_ntab(const toml_table_t *tab)
Definition toml.c:1950
toml_datum_t toml_string_in(const toml_table_t *arr, const char *key)
Definition toml.c:2323
toml_datum_t toml_int_at(const toml_array_t *arr, int idx)
Definition toml.c:2279
toml_array_t * toml_array_in(const toml_table_t *tab, const char *key)
Definition toml.c:1906
toml_datum_t toml_double_at(const toml_array_t *arr, int idx)
Definition toml.c:2286
int toml_table_nkval(const toml_table_t *tab)
Definition toml.c:1946
int toml_array_nelem(const toml_array_t *arr)
Definition toml.c:1940
const char * toml_key_in(const toml_table_t *tab, int keyidx)
Definition toml.c:1865
toml_datum_t toml_double_in(const toml_table_t *arr, const char *key)
Definition toml.c:2347
toml_datum_t toml_int_in(const toml_table_t *arr, const char *key)
Definition toml.c:2340
int toml_key_exists(const toml_table_t *tab, const char *key)
Definition toml.c:1880
toml_table_t * toml_table_in(const toml_table_t *tab, const char *key)
Definition toml.c:1915
toml_datum_t toml_bool_in(const toml_table_t *arr, const char *key)
Definition toml.c:2333
int toml_table_narr(const toml_table_t *tab)
Definition toml.c:1948
void toml_free(toml_table_t *tab)
Definition toml.c:1583
toml_table_t * toml_parse_file(FILE *fp, char *errbuf, int errbufsz)
Definition toml.c:1480
toml_datum_t toml_string_at(const toml_array_t *arr, int idx)
Definition toml.c:2265
#define free(x)
Definition toml.c:53