From 86ddda9d14500f8e476a3f77cba642a0c5067e8d Mon Sep 17 00:00:00 2001 From: Michael Keller Date: Sun, 1 Nov 2026 15:13:33 +1300 Subject: [PATCH 1/5] halcyon: Ignore disabled open-circuit gas slots Disabled Symbios gas slots were listed as available gases because ID_GAS_CONFIG parsing ignored the per-slot enabled byte at offset +5. Require that byte to be non-zero before registering non-diluent gas IDs 0-4, while retaining the existing non-zero O2/He requirement. Leave diluent IDs 5-7 on their existing path. Cover disabled and enabled slots, repeated configurations, diluents, compacted gas and tank indices, and short records with focused synthetic parser tests. Run make check and replay both confirmed native open-circuit BIN samples; each retains the starting gas and three diluents without the disabled slots. Signed-off-by: Michael Keller --- src/halcyon_symbios_parser.c | 4 +- test/Makefile.am | 5 +- test/test_halcyon_symbios_gases.c | 141 ++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 test/test_halcyon_symbios_gases.c diff --git a/src/halcyon_symbios_parser.c b/src/halcyon_symbios_parser.c index 320e8134..4b084573 100644 --- a/src/halcyon_symbios_parser.c +++ b/src/halcyon_symbios_parser.c @@ -73,6 +73,7 @@ #define NGASMIXES 10 #define NTANKS 10 +#define NGAS_SLOTS 5 /* Record IDs 0-4 (GAS1-GAS5) are gas slots; IDs >= 5 are diluents. */ #define TRANSMITTER_ID (1u << 16) @@ -631,7 +632,8 @@ halcyon_symbios_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callbac unsigned int id = data[offset + 2]; unsigned int o2 = data[offset + 3]; unsigned int he = data[offset + 4]; - if (o2 != 0 || he != 0) { + if ((o2 != 0 || he != 0) && + (id >= NGAS_SLOTS || data[offset + 5] != 0)) { unsigned int idx = 0; while (idx < ngasmixes) { if (id == gasmix[idx].id) diff --git a/test/Makefile.am b/test/Makefile.am index 6d07e758..42354868 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -3,13 +3,14 @@ AUTOMAKE_OPTIONS = subdir-objects AM_CPPFLAGS = -I$(top_builddir)/include -I$(top_srcdir)/include LDADD = $(top_builddir)/src/libdivecomputer.la -check_PROGRAMS = test_descriptor_product_name test_mares_iconhd_freedive +check_PROGRAMS = test_descriptor_product_name test_mares_iconhd_freedive test_halcyon_symbios_gases test_descriptor_product_name_SOURCES = test_descriptor_product_name.c test_mares_iconhd_freedive_SOURCES = test_mares_iconhd_freedive.c $(top_srcdir)/src/array.c test_mares_iconhd_freedive_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src +test_halcyon_symbios_gases_SOURCES = test_halcyon_symbios_gases.c -TESTS = test_descriptor_product_name test_mares_iconhd_freedive +TESTS = test_descriptor_product_name test_mares_iconhd_freedive test_halcyon_symbios_gases EXTRA_DIST = \ run-parse.sh \ diff --git a/test/test_halcyon_symbios_gases.c b/test/test_halcyon_symbios_gases.c new file mode 100644 index 00000000..bd41301e --- /dev/null +++ b/test/test_halcyon_symbios_gases.c @@ -0,0 +1,141 @@ +/* Synthetic Symbios records: no device logs or identifying data. */ +#include + +#include +#include +#include + +#define GAS(id, o2, he, enabled) 0x11, 8, id, o2, he, enabled, 0, 0 +#define TRANSMITTER(id, serial) 0x06, 12, id, 0, 0, 0, 0xD2, 0x04, serial, 0, 0, 0 + +typedef struct samples_t { + unsigned int gases[16]; + unsigned int ngases; + unsigned int pressures[16]; + unsigned int npressures; +} samples_t; + +static int failures = 0; + +static dc_descriptor_t * +handset_descriptor (void) +{ + dc_iterator_t *iterator = NULL; + dc_descriptor_t *descriptor = NULL; + dc_descriptor_t *item = NULL; + if (dc_descriptor_iterator (&iterator) != DC_STATUS_SUCCESS) + return NULL; + while (dc_iterator_next (iterator, &item) == DC_STATUS_SUCCESS) { + if (dc_descriptor_get_type (item) == DC_FAMILY_HALCYON_SYMBIOS && + dc_descriptor_get_model (item) == 7) { + descriptor = item; + break; + } + } + dc_iterator_free (iterator); + return descriptor; +} + +static void +check (const char *name, int condition) +{ + if (!condition) { + fprintf (stderr, "FAIL: %s\n", name); + failures++; + } +} + +static void +sample (dc_sample_type_t type, const dc_sample_value_t *value, void *userdata) +{ + samples_t *samples = userdata; + if (type == DC_SAMPLE_GASMIX && samples->ngases < 16) + samples->gases[samples->ngases++] = value->gasmix; + if (type == DC_SAMPLE_PRESSURE && samples->npressures < 16) + samples->pressures[samples->npressures++] = value->pressure.tank; +} + +static void +check_mix (dc_parser_t *parser, unsigned int index, unsigned int oxygen, unsigned int helium) +{ + dc_gasmix_t mix; + check ("mix field", dc_parser_get_field (parser, DC_FIELD_GASMIX, index, &mix) == DC_STATUS_SUCCESS); + check ("oxygen fraction", mix.oxygen == oxygen / 100.0); + check ("helium fraction", mix.helium == helium / 100.0); +} + +static void +check_profile (const unsigned char *data, size_t size, unsigned int expected_count, + const unsigned int *expected_samples, unsigned int nsamples) +{ + dc_parser_t *parser = NULL; + dc_descriptor_t *descriptor = handset_descriptor (); + samples_t samples = {{0}, 0, {0}, 0}; + unsigned int count = 0; + check ("handset descriptor", descriptor != NULL); + if (descriptor == NULL) + return; + check ("create parser", dc_parser_new2 (&parser, NULL, descriptor, data, size) == DC_STATUS_SUCCESS); + dc_descriptor_free (descriptor); + if (parser == NULL) + return; + check ("parse samples", dc_parser_samples_foreach (parser, sample, &samples) == DC_STATUS_SUCCESS); + check ("gas count field", dc_parser_get_field (parser, DC_FIELD_GASMIX_COUNT, 0, &count) == DC_STATUS_SUCCESS); + check ("gas count", count == expected_count); + check ("sample count", samples.ngases == nsamples); + for (unsigned int i = 0; i < nsamples && i < samples.ngases; ++i) + check ("sample gas index", samples.gases[i] == expected_samples[i]); + if (expected_count == 4) { + check_mix (parser, 0, 21, 0); + check_mix (parser, 1, 21, 0); + check_mix (parser, 2, 21, 35); + check_mix (parser, 3, 18, 45); + } else if (expected_count == 8) { + dc_tank_t tank; + check_mix (parser, 1, 32, 0); + check_mix (parser, 2, 30, 0); /* Enabled, never selected. */ + check_mix (parser, 3, 21, 0); + check_mix (parser, 6, 40, 0); + check ("tank count", dc_parser_get_field (parser, DC_FIELD_TANK_COUNT, 0, &count) == DC_STATUS_SUCCESS && count == 2); + check ("first tank", dc_parser_get_field (parser, DC_FIELD_TANK, 0, &tank) == DC_STATUS_SUCCESS && tank.gasmix == 1); + check ("second tank", dc_parser_get_field (parser, DC_FIELD_TANK, 1, &tank) == DC_STATUS_SUCCESS && tank.gasmix == 3); + check ("pressure tank indices", samples.npressures == 2 && samples.pressures[0] == 0 && samples.pressures[1] == 1); + } + check ("destroy parser", dc_parser_destroy (parser) == DC_STATUS_SUCCESS); +} + +int +main (void) +{ + /* Native-export pattern: disabled GAS2-5, enabled GAS1, unchanged diluents. */ + static const unsigned char native_pattern[] = { + GAS (0, 21, 0, 1), GAS (1, 50, 0, 0), GAS (2, 99, 0, 0), + GAS (3, 21, 35, 0), GAS (4, 18, 45, 0), GAS (5, 21, 0, 1), + GAS (6, 21, 35, 0), GAS (7, 18, 45, 0), GAS (0, 21, 0, 1), + GAS (1, 50, 0, 0), GAS (2, 0, 0, 1) + }; + static const unsigned int native_samples[] = {0}; + /* Non-boolean nonzero enable value; gas 2 remains visible without selection. */ + static const unsigned char linkage[] = { + GAS (0, 21, 0, 1), GAS (1, 50, 0, 0), GAS (2, 32, 0, 2), + GAS (4, 30, 0, 2), + GAS (5, 21, 0, 0), GAS (6, 21, 35, 1), GAS (7, 18, 45, 0), + GAS (8, 40, 0, 0), GAS (1, 50, 0, 0), GAS (0, 21, 0, 1), + TRANSMITTER (2, 1), TRANSMITTER (5, 2), + 0x02, 4, 36, 0 /* Gas-switch record retains its independent path. */ + }; + static const unsigned int linkage_samples[] = {0, 1, 3, 7}; + dc_parser_t *parser = NULL; + dc_descriptor_t *descriptor = handset_descriptor (); + static const unsigned char short_config[] = {0x11, 7, 0, 21, 0, 1, 0}; + + check_profile (native_pattern, sizeof (native_pattern), 4, native_samples, 1); + check_profile (linkage, sizeof (linkage), 8, linkage_samples, 4); + check ("create short parser", dc_parser_new2 (&parser, NULL, descriptor, short_config, sizeof (short_config)) == DC_STATUS_SUCCESS); + dc_descriptor_free (descriptor); + if (parser) { + check ("reject short gas config", dc_parser_samples_foreach (parser, NULL, NULL) == DC_STATUS_DATAFORMAT); + dc_parser_destroy (parser); + } + return failures != 0; +} From 4ac0bc9712c80034e3ec898ff1603242433becb2 Mon Sep 17 00:00:00 2001 From: Michael Keller Date: Sun, 1 Nov 2026 16:03:04 +1300 Subject: [PATCH 2/5] halcyon: Resolve disabled gas slots referenced by a transmitter A gas slot that is disabled in its ID_GAS_CONFIG record is no longer registered in the exposed gas list, but a subsequent ID_GAS_TRANSMITTER record can still reference that same id if the slot is actually in use. Previously this lookup failed with DC_STATUS_DATAFORMAT because only the exposed gas list was searched. Retain every configured gas slot, enabled or not, in a separate lookup table. When a transmitter record references an id that is not yet exposed, resolve it from that table and add it to the exposed list at that point, instead of rejecting the record. Add a synthetic test covering a disabled slot later referenced by a transmitter, confirming it resolves and becomes visible only once referenced. Signed-off-by: Michael Keller --- src/halcyon_symbios_parser.c | 50 +++++++++++++++++++++++++++++-- test/test_halcyon_symbios_gases.c | 25 ++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/halcyon_symbios_parser.c b/src/halcyon_symbios_parser.c index 4b084573..f625230a 100644 --- a/src/halcyon_symbios_parser.c +++ b/src/halcyon_symbios_parser.c @@ -339,6 +339,11 @@ halcyon_symbios_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callbac unsigned int ngasmixes = 0; unsigned int ntanks = 0; halcyon_symbios_gasmix_t gasmix[NGASMIXES] = {0}; + // Every configured gas slot (enabled or disabled), kept so a later + // ID_GAS_TRANSMITTER record can still resolve a disabled slot instead + // of failing, even though disabled slots are not exposed in gasmix[]. + halcyon_symbios_gasmix_t gasconfig[NGASMIXES] = {0}; + unsigned int ngasconfig = 0; halcyon_symbios_tank_t tank[NTANKS] = {0}; unsigned int gasmix_id_previous = UNDEFINED; unsigned int gasmix_idx = DC_GASMIX_UNKNOWN; @@ -476,8 +481,28 @@ halcyon_symbios_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callbac idx++; } if (idx >= ngasmixes) { - ERROR (abstract->context, "Invalid gas mix id (%u).", gas_id); - return DC_STATUS_DATAFORMAT; + // Not yet exposed, because the gas slot was disabled + // when its ID_GAS_CONFIG record was parsed. A + // transmitter still reporting it means it is in use + // after all, so resolve it from the retained + // configuration instead of failing outright. + unsigned int cfgidx = 0; + while (cfgidx < ngasconfig) { + if (gas_id == gasconfig[cfgidx].id) + break; + cfgidx++; + } + if (cfgidx >= ngasconfig) { + ERROR (abstract->context, "Invalid gas mix id (%u).", gas_id); + return DC_STATUS_DATAFORMAT; + } + if (ngasmixes >= NGASMIXES) { + ERROR (abstract->context, "Maximum number of gas mixes reached."); + return DC_STATUS_NOMEMORY; + } + idx = ngasmixes; + gasmix[idx] = gasconfig[cfgidx]; + ngasmixes++; } sample.gasmix = idx; if (callback) callback(DC_SAMPLE_GASMIX, &sample, userdata); @@ -632,6 +657,27 @@ halcyon_symbios_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callbac unsigned int id = data[offset + 2]; unsigned int o2 = data[offset + 3]; unsigned int he = data[offset + 4]; + if (o2 != 0 || he != 0) { + // Record every configured slot, including disabled ones, + // so a later ID_GAS_TRANSMITTER record can still resolve + // it instead of failing with DC_STATUS_DATAFORMAT. + unsigned int cfgidx = 0; + while (cfgidx < ngasconfig) { + if (id == gasconfig[cfgidx].id) + break; + cfgidx++; + } + if (cfgidx >= ngasconfig) { + if (ngasconfig >= NGASMIXES) { + ERROR (abstract->context, "Maximum number of gas mixes reached."); + return DC_STATUS_NOMEMORY; + } + gasconfig[ngasconfig].id = id; + gasconfig[ngasconfig].oxygen = o2; + gasconfig[ngasconfig].helium = he; + ngasconfig++; + } + } if ((o2 != 0 || he != 0) && (id >= NGAS_SLOTS || data[offset + 5] != 0)) { unsigned int idx = 0; diff --git a/test/test_halcyon_symbios_gases.c b/test/test_halcyon_symbios_gases.c index bd41301e..5a3a80a6 100644 --- a/test/test_halcyon_symbios_gases.c +++ b/test/test_halcyon_symbios_gases.c @@ -125,9 +125,20 @@ main (void) 0x02, 4, 36, 0 /* Gas-switch record retains its independent path. */ }; static const unsigned int linkage_samples[] = {0, 1, 3, 7}; + /* Gas1 is disabled in its config but later reported by a transmitter; + * it must resolve instead of being rejected, and only becomes part of + * the exposed gas list once referenced. */ + static const unsigned char deferred[] = { + GAS (0, 21, 0, 1), GAS (1, 32, 0, 0), + TRANSMITTER (1, 5) + }; dc_parser_t *parser = NULL; dc_descriptor_t *descriptor = handset_descriptor (); static const unsigned char short_config[] = {0x11, 7, 0, 21, 0, 1, 0}; + dc_parser_t *deferred_parser = NULL; + dc_descriptor_t *deferred_descriptor = handset_descriptor (); + unsigned int deferred_count = 0; + dc_tank_t deferred_tank; check_profile (native_pattern, sizeof (native_pattern), 4, native_samples, 1); check_profile (linkage, sizeof (linkage), 8, linkage_samples, 4); @@ -137,5 +148,19 @@ main (void) check ("reject short gas config", dc_parser_samples_foreach (parser, NULL, NULL) == DC_STATUS_DATAFORMAT); dc_parser_destroy (parser); } + + check ("handset descriptor (deferred)", deferred_descriptor != NULL); + if (deferred_descriptor) { + check ("create deferred parser", dc_parser_new2 (&deferred_parser, NULL, deferred_descriptor, deferred, sizeof (deferred)) == DC_STATUS_SUCCESS); + dc_descriptor_free (deferred_descriptor); + } + if (deferred_parser) { + check ("resolve deferred transmitter", dc_parser_samples_foreach (deferred_parser, NULL, NULL) == DC_STATUS_SUCCESS); + check ("deferred gas count field", dc_parser_get_field (deferred_parser, DC_FIELD_GASMIX_COUNT, 0, &deferred_count) == DC_STATUS_SUCCESS); + check ("deferred gas count", deferred_count == 2); + check ("deferred tank gasmix", dc_parser_get_field (deferred_parser, DC_FIELD_TANK, 0, &deferred_tank) == DC_STATUS_SUCCESS && deferred_tank.gasmix == 1); + dc_parser_destroy (deferred_parser); + } + return failures != 0; } From a55830aa4cfd67d3baa647d214813824bd9f122d Mon Sep 17 00:00:00 2001 From: Michael Keller Date: Sun, 1 Nov 2026 16:46:09 +1300 Subject: [PATCH 3/5] halcyon: Reject conflicting retained gas configurations The retained-configuration table added to resolve disabled gas slots referenced by a transmitter only recorded the first value seen for each id, silently keeping it even if a later ID_GAS_CONFIG record reported a different oxygen or helium percentage for the same id. This dropped the consistency check that the exposed gas list already applies, so a genuinely conflicting (and potentially corrupt) retained configuration could be exposed through a transmitter reference instead of being rejected. Validate repeated retained configurations the same way the exposed gas list does, and reject the record with DC_STATUS_DATAFORMAT on a mismatch. Add a synthetic test covering a disabled slot reconfigured with a different oxygen percentage before being referenced. Signed-off-by: Michael Keller --- src/halcyon_symbios_parser.c | 7 +++++++ test/test_halcyon_symbios_gases.c | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/halcyon_symbios_parser.c b/src/halcyon_symbios_parser.c index f625230a..d17812d0 100644 --- a/src/halcyon_symbios_parser.c +++ b/src/halcyon_symbios_parser.c @@ -676,6 +676,13 @@ halcyon_symbios_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callbac gasconfig[ngasconfig].oxygen = o2; gasconfig[ngasconfig].helium = he; ngasconfig++; + } else if (gasconfig[cfgidx].oxygen != o2 || + gasconfig[cfgidx].helium != he) { + ERROR (abstract->context, "Gas mix %u changed (%u/%u -> %u/%u).", + gasconfig[cfgidx].id, + gasconfig[cfgidx].oxygen, gasconfig[cfgidx].helium, + o2, he); + return DC_STATUS_DATAFORMAT; } } if ((o2 != 0 || he != 0) && diff --git a/test/test_halcyon_symbios_gases.c b/test/test_halcyon_symbios_gases.c index 5a3a80a6..918de66c 100644 --- a/test/test_halcyon_symbios_gases.c +++ b/test/test_halcyon_symbios_gases.c @@ -132,6 +132,12 @@ main (void) GAS (0, 21, 0, 1), GAS (1, 32, 0, 0), TRANSMITTER (1, 5) }; + /* Gas1 is disabled and reconfigured with a different oxygen percentage + * before ever being referenced; the conflicting retained configuration + * must still be rejected, instead of silently keeping the first one. */ + static const unsigned char deferred_conflict[] = { + GAS (0, 21, 0, 1), GAS (1, 32, 0, 0), GAS (1, 50, 0, 0) + }; dc_parser_t *parser = NULL; dc_descriptor_t *descriptor = handset_descriptor (); static const unsigned char short_config[] = {0x11, 7, 0, 21, 0, 1, 0}; @@ -139,6 +145,8 @@ main (void) dc_descriptor_t *deferred_descriptor = handset_descriptor (); unsigned int deferred_count = 0; dc_tank_t deferred_tank; + dc_parser_t *conflict_parser = NULL; + dc_descriptor_t *conflict_descriptor = handset_descriptor (); check_profile (native_pattern, sizeof (native_pattern), 4, native_samples, 1); check_profile (linkage, sizeof (linkage), 8, linkage_samples, 4); @@ -162,5 +170,15 @@ main (void) dc_parser_destroy (deferred_parser); } + check ("handset descriptor (conflict)", conflict_descriptor != NULL); + if (conflict_descriptor) { + check ("create conflict parser", dc_parser_new2 (&conflict_parser, NULL, conflict_descriptor, deferred_conflict, sizeof (deferred_conflict)) == DC_STATUS_SUCCESS); + dc_descriptor_free (conflict_descriptor); + } + if (conflict_parser) { + check ("reject conflicting retained configuration", dc_parser_samples_foreach (conflict_parser, NULL, NULL) == DC_STATUS_DATAFORMAT); + dc_parser_destroy (conflict_parser); + } + return failures != 0; } From d8bc9d81e9dca5434a387dcf091cacaa4e71899c Mon Sep 17 00:00:00 2001 From: Michael Keller Date: Sun, 1 Nov 2026 17:19:19 +1300 Subject: [PATCH 4/5] halcyon: Run transmitter gas resolution without a prior enabled gas The ID_GAS_TRANSMITTER resolution block was gated on have_gasmix, which was only set once an enabled (or diluent) gas had been registered in the exposed gas list. If every configured gas slot is disabled and none of them had been exposed yet, a transmitter referencing one of them skipped the whole block, leaving its tank's gas mix as DC_GASMIX_UNKNOWN instead of being resolved through the retained configuration added to handle this case. Gate on ngasconfig instead, since every valid gas configuration, enabled or not, is already recorded there. This removes the now-unused have_gasmix flag. Add a synthetic test where the only configured gas is disabled and is referenced by a transmitter before any gas is otherwise registered. Signed-off-by: Michael Keller --- src/halcyon_symbios_parser.c | 7 ++++--- test/test_halcyon_symbios_gases.c | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/halcyon_symbios_parser.c b/src/halcyon_symbios_parser.c index d17812d0..cb9788a3 100644 --- a/src/halcyon_symbios_parser.c +++ b/src/halcyon_symbios_parser.c @@ -354,7 +354,6 @@ halcyon_symbios_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callbac unsigned int have_time = 0; unsigned int have_depth = 0; - unsigned int have_gasmix = 0; unsigned int time = 0; unsigned int offset = 0; @@ -473,7 +472,10 @@ halcyon_symbios_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callbac unsigned int transmitter = array_uint16_le (data + offset + 8); dc_tank_usage_t usage = DC_TANK_USAGE_NONE; - if (have_gasmix && gasmix_id_previous != gas_id) { + // Every configured gas (enabled or not) is recorded in + // gasconfig[], so checking it covers both an already exposed + // gas and one only resolvable through this transmitter. + if (ngasconfig > 0 && gasmix_id_previous != gas_id) { unsigned int idx = 0; while (idx < ngasmixes) { if (gas_id == gasmix[idx].id) @@ -702,7 +704,6 @@ halcyon_symbios_parser_samples_foreach (dc_parser_t *abstract, dc_sample_callbac gasmix[ngasmixes].oxygen = o2; gasmix[ngasmixes].helium = he; ngasmixes++; - have_gasmix = 1; } else { if (gasmix[idx].oxygen != o2 || gasmix[idx].helium != he) { diff --git a/test/test_halcyon_symbios_gases.c b/test/test_halcyon_symbios_gases.c index 918de66c..5c584ef5 100644 --- a/test/test_halcyon_symbios_gases.c +++ b/test/test_halcyon_symbios_gases.c @@ -138,6 +138,12 @@ main (void) static const unsigned char deferred_conflict[] = { GAS (0, 21, 0, 1), GAS (1, 32, 0, 0), GAS (1, 50, 0, 0) }; + /* The only configured gas is disabled, so no enabled or diluent gas is + * ever registered; its transmitter must still resolve it instead of + * leaving the tank's gas mix unknown. */ + static const unsigned char disabled_only[] = { + GAS (0, 21, 0, 0), TRANSMITTER (0, 9) + }; dc_parser_t *parser = NULL; dc_descriptor_t *descriptor = handset_descriptor (); static const unsigned char short_config[] = {0x11, 7, 0, 21, 0, 1, 0}; @@ -147,6 +153,10 @@ main (void) dc_tank_t deferred_tank; dc_parser_t *conflict_parser = NULL; dc_descriptor_t *conflict_descriptor = handset_descriptor (); + dc_parser_t *disabled_only_parser = NULL; + dc_descriptor_t *disabled_only_descriptor = handset_descriptor (); + unsigned int disabled_only_count = 0; + dc_tank_t disabled_only_tank; check_profile (native_pattern, sizeof (native_pattern), 4, native_samples, 1); check_profile (linkage, sizeof (linkage), 8, linkage_samples, 4); @@ -180,5 +190,18 @@ main (void) dc_parser_destroy (conflict_parser); } + check ("handset descriptor (disabled only)", disabled_only_descriptor != NULL); + if (disabled_only_descriptor) { + check ("create disabled-only parser", dc_parser_new2 (&disabled_only_parser, NULL, disabled_only_descriptor, disabled_only, sizeof (disabled_only)) == DC_STATUS_SUCCESS); + dc_descriptor_free (disabled_only_descriptor); + } + if (disabled_only_parser) { + check ("resolve transmitter without a prior enabled gas", dc_parser_samples_foreach (disabled_only_parser, NULL, NULL) == DC_STATUS_SUCCESS); + check ("disabled-only gas count field", dc_parser_get_field (disabled_only_parser, DC_FIELD_GASMIX_COUNT, 0, &disabled_only_count) == DC_STATUS_SUCCESS); + check ("disabled-only gas count", disabled_only_count == 1); + check ("disabled-only tank gasmix", dc_parser_get_field (disabled_only_parser, DC_FIELD_TANK, 0, &disabled_only_tank) == DC_STATUS_SUCCESS && disabled_only_tank.gasmix == 0); + dc_parser_destroy (disabled_only_parser); + } + return failures != 0; } From 7b4f591387cc8837c065f2d0211f80f5ab3f9feb Mon Sep 17 00:00:00 2001 From: Michael Keller Date: Sun, 1 Nov 2026 16:03:24 +1300 Subject: [PATCH 5/5] test: Improve Symbios gas-config test clarity and robustness Address several review nits in test_halcyon_symbios_gases.c: - Replace the unexplained magic number 7 used to select the Symbios Handset descriptor with a named HANDSET_MODEL_SYMBIOS constant. - Compare oxygen and helium fractions with a small tolerance instead of ==, so the checks stay valid if the parser's conversion changes. - Document how the linkage fixture's expected gas count of 8 and its expected sample indices {0, 1, 3, 7} are derived, including which registered ids are enabled versus diluents that skip the enable check. Signed-off-by: Michael Keller --- test/test_halcyon_symbios_gases.c | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/test/test_halcyon_symbios_gases.c b/test/test_halcyon_symbios_gases.c index 5c584ef5..c7e2981f 100644 --- a/test/test_halcyon_symbios_gases.c +++ b/test/test_halcyon_symbios_gases.c @@ -1,4 +1,5 @@ /* Synthetic Symbios records: no device logs or identifying data. */ +#include #include #include @@ -8,6 +9,9 @@ #define GAS(id, o2, he, enabled) 0x11, 8, id, o2, he, enabled, 0, 0 #define TRANSMITTER(id, serial) 0x06, 12, id, 0, 0, 0, 0xD2, 0x04, serial, 0, 0, 0 +/* dc_descriptor_get_model() value for the "Symbios Handset", see descriptor.c. */ +#define HANDSET_MODEL_SYMBIOS 7 + typedef struct samples_t { unsigned int gases[16]; unsigned int ngases; @@ -27,7 +31,7 @@ handset_descriptor (void) return NULL; while (dc_iterator_next (iterator, &item) == DC_STATUS_SUCCESS) { if (dc_descriptor_get_type (item) == DC_FAMILY_HALCYON_SYMBIOS && - dc_descriptor_get_model (item) == 7) { + dc_descriptor_get_model (item) == HANDSET_MODEL_SYMBIOS) { descriptor = item; break; } @@ -60,8 +64,8 @@ check_mix (dc_parser_t *parser, unsigned int index, unsigned int oxygen, unsigne { dc_gasmix_t mix; check ("mix field", dc_parser_get_field (parser, DC_FIELD_GASMIX, index, &mix) == DC_STATUS_SUCCESS); - check ("oxygen fraction", mix.oxygen == oxygen / 100.0); - check ("helium fraction", mix.helium == helium / 100.0); + check ("oxygen fraction", fabs (mix.oxygen - oxygen / 100.0) < 1e-9); + check ("helium fraction", fabs (mix.helium - helium / 100.0) < 1e-9); } static void @@ -124,6 +128,22 @@ main (void) TRANSMITTER (2, 1), TRANSMITTER (5, 2), 0x02, 4, 36, 0 /* Gas-switch record retains its independent path. */ }; + /* + * Expected gas count of 8: 7 distinct ids registered from "linkage" + * (0, 2, 4, 5, 6, 7, 8, at indices 0-6) plus one more gas mix + * synthesized by the trailing ID_GAS_SWITCH record, which always uses + * its own untagged id and is appended as index 7. Ids 0, 2 and 4 are + * registered because they are enabled; ids 5, 7 and 8 are registered + * regardless of their enable byte because ids >= NGAS_SLOTS are + * diluents, which skip the enable check; id 6 is both enabled and a + * diluent. + * + * Expected sample indices {0, 1, 3, 7}: index 0 comes from the + * repeated GAS(0, 21, 0, 1) record re-confirming the already + * registered gas 0; index 1 and 3 come from TRANSMITTER(2, 1) and + * TRANSMITTER(5, 2) selecting gases 2 and 5; index 7 comes from the + * trailing gas-switch record. + */ static const unsigned int linkage_samples[] = {0, 1, 3, 7}; /* Gas1 is disabled in its config but later reported by a transmitter; * it must resolve instead of being rejected, and only becomes part of