Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ _ResolvedCylinders _resolveCylinders(
return const _ResolvedCylinders([], {});
}
final tanks = <DownloadedTank>[];
final roles = _inferSensorlessRoles(gasMixes, parsed.diveMode);
for (var i = 0; i < gasMixes.length; i++) {
final g = gasMixes[i];
gasIndexToTankIndex[i] = g.index;
Expand All @@ -119,7 +120,7 @@ _ResolvedCylinders _resolveCylinders(
index: g.index,
o2Percent: g.o2Percent,
hePercent: g.hePercent,
role: _inferRole(null, g.o2Percent, g.hePercent),
role: roles[i],
),
);
}
Expand Down Expand Up @@ -205,6 +206,92 @@ String _inferRole(int? usage, double o2Percent, double hePercent) {
return TankRole.backGas.name;
}

/// Role for each of [gasMixes], in order, on a sensorless (tankless) dive.
///
/// A gas whose usage the computer reported directly on the gas mix itself
/// (`dc_gasmix_t.usage`, independent of any tank/transmitter record) is
/// authoritative device data, regardless of dive mode: oxygen maps to
/// [TankRole.oxygenSupply], diluent to [TankRole.diluent]. Sidemount maps to
/// [TankRole.backGas] rather than [TankRole.sidemountLeft]/[TankRole.sidemountRight]
/// -- the flag only says the gas is on a sidemount cylinder, not which side,
/// so it cannot pick between the two.
///
/// For a dive recognized as CCR, the gases left with no reported usage are
/// the open-circuit bailout candidates and are ranked against each other
/// instead of scored in isolation:
/// 1. Bottom gas: the lowest O2 percentage among them becomes
/// [TankRole.bailout]; a tie is broken by the higher helium percentage,
/// and a further tie gives Bailout to every still-tied gas. A gas that
/// only loses the helium tie-break gets no automatic Bailout role and
/// falls through to the next rule.
/// 2. Deco: every still-unassigned gas at or above the same 41% O2
/// threshold [_inferRole] uses for open circuit becomes [TankRole.deco].
/// 3. Stage: everything still unassigned becomes [TankRole.stage].
///
/// On any other recognized dive mode, a gas with no reported usage keeps
/// [_inferRole]'s original single-threshold heuristic, unaffected by this.
List<String> _inferSensorlessRoles(
List<pigeon.GasMix> gasMixes,
String? diveMode,
) {
final roles = List<String?>.filled(gasMixes.length, null);
final unranked = <int>[];
for (var i = 0; i < gasMixes.length; i++) {
final g = gasMixes[i];
switch (g.usage) {
case 1: // DC_USAGE_OXYGEN
roles[i] = TankRole.oxygenSupply.name;
case 2: // DC_USAGE_DILUENT
roles[i] = TankRole.diluent.name;
case 3: // DC_USAGE_SIDEMOUNT
roles[i] = TankRole.backGas.name;
default:
unranked.add(i);
}
}

if (diveMode != 'ccr') {
for (final i in unranked) {
final g = gasMixes[i];
roles[i] = _inferRole(null, g.o2Percent, g.hePercent);
}
return [for (final role in roles) role!];
}

if (unranked.isNotEmpty) {
final lowestO2 = unranked
.map((i) => gasMixes[i].o2Percent)
.reduce((a, b) => a < b ? a : b);
final atLowestO2 = unranked.where(
(i) => _nearlyEqualPercent(gasMixes[i].o2Percent, lowestO2),
);
final highestHeAtLowestO2 = atLowestO2
.map((i) => gasMixes[i].hePercent)
.reduce((a, b) => a > b ? a : b);
for (final i in atLowestO2) {
if (_nearlyEqualPercent(gasMixes[i].hePercent, highestHeAtLowestO2)) {
roles[i] = TankRole.bailout.name;
}
}
for (final i in unranked) {
if (roles[i] != null) continue;
roles[i] = gasMixes[i].o2Percent >= 41.0
? TankRole.deco.name
: TankRole.stage.name;
}
}

return [for (final role in roles) role!];
}

/// Whether two gas percentages are the same value within floating-point
/// noise. Each of the four platform converters independently computes
/// `fraction * 100.0` from the native `dc_gasmix_t`, so two mixes the diver
/// set to the same nominal percentage can differ by a few ULPs; an exact
/// `==` would then miss a real tie in [_inferSensorlessRoles]'s bailout
/// ranking.
bool _nearlyEqualPercent(double a, double b) => (a - b).abs() < 1e-6;

/// The gas-mix index (position in [gasMixes]) for [tank], preferring the gas
/// actually breathed on it. Returns null only when there are no gas mixes.
int? _resolveTankGasIndex(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1086,9 +1086,10 @@ Java_com_submersion_libdivecomputer_LibdcWrapper_nativeGetDiveGasmix(
if (index < 0 || static_cast<unsigned int>(index) >= dive->gasmix_count) return nullptr;

const libdc_gasmix_t *gm = &dive->gasmixes[index];
jdouble values[2] = { gm->oxygen, gm->helium };
jdoubleArray result = env->NewDoubleArray(2);
env->SetDoubleArrayRegion(result, 0, 2, values);
// Return [oxygen, helium, usage]. Positional: the Kotlin readers index it.
jdouble values[3] = { gm->oxygen, gm->helium, static_cast<jdouble>(gm->usage) };
jdoubleArray result = env->NewDoubleArray(3);
env->SetDoubleArrayRegion(result, 0, 3, values);
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,22 +255,31 @@ data class ProfileSample (
data class GasMix (
val index: Long,
val o2Percent: Double,
val hePercent: Double
val hePercent: Double,
/**
* Gas usage from libdivecomputer's `dc_usage_t` (1=oxygen, 2=diluent,
* 3=sidemount); null when the computer reported no usage (DC_USAGE_NONE).
* Set on the gas mix itself, so it is available even when the mix has no
* tank/transmitter record.
*/
val usage: Long? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): GasMix {
val index = pigeonVar_list[0] as Long
val o2Percent = pigeonVar_list[1] as Double
val hePercent = pigeonVar_list[2] as Double
return GasMix(index, o2Percent, hePercent)
val usage = pigeonVar_list[3] as Long?
return GasMix(index, o2Percent, hePercent, usage)
}
}
fun toList(): List<Any?> {
return listOf(
index,
o2Percent,
hePercent,
usage,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,8 @@ class DiveComputerHostApiImpl(
GasMix(
index = i.toLong(),
o2Percent = gm[0] * 100.0,
hePercent = gm[1] * 100.0
hePercent = gm[1] * 100.0,
usage = gm.getOrNull(2)?.toLong()?.takeIf { it != 0L }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ class SerialDownloadRunner(private val context: Context) {
GasMix(
index = i.toLong(),
o2Percent = gm[0] * 100.0,
hePercent = gm[1] * 100.0
hePercent = gm[1] * 100.0,
usage = gm.getOrNull(2)?.toLong()?.takeIf { it != 0L }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,8 @@ class DiveComputerHostApiImpl: DiveComputerHostApi {
gasMixes.append(GasMix(
index: Int64(i),
o2Percent: gm.oxygen * 100.0,
hePercent: gm.helium * 100.0
hePercent: gm.helium * 100.0,
usage: gm.usage == 0 ? nil : Int64(gm.usage)
))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,25 +301,33 @@ struct GasMix {
var index: Int64
var o2Percent: Double
var hePercent: Double
/// Gas usage from libdivecomputer's `dc_usage_t` (1=oxygen, 2=diluent,
/// 3=sidemount); null when the computer reported no usage (DC_USAGE_NONE).
/// Set on the gas mix itself, so it is available even when the mix has no
/// tank/transmitter record.
var usage: Int64? = nil


// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> GasMix? {
let index = pigeonVar_list[0] as! Int64
let o2Percent = pigeonVar_list[1] as! Double
let hePercent = pigeonVar_list[2] as! Double
let usage: Int64? = nilOrValue(pigeonVar_list[3])

return GasMix(
index: index,
o2Percent: o2Percent,
hePercent: hePercent
hePercent: hePercent,
usage: usage
)
}
func toList() -> [Any?] {
return [
index,
o2Percent,
hePercent,
usage,
]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ class GasMix {
required this.index,
required this.o2Percent,
required this.hePercent,
this.usage,
});

int index;
Expand All @@ -292,8 +293,14 @@ class GasMix {

double hePercent;

/// Gas usage from libdivecomputer's `dc_usage_t` (1=oxygen, 2=diluent,
/// 3=sidemount); null when the computer reported no usage (DC_USAGE_NONE).
/// Set on the gas mix itself, so it is available even when the mix has no
/// tank/transmitter record.
int? usage;

Object encode() {
return <Object?>[index, o2Percent, hePercent];
return <Object?>[index, o2Percent, hePercent, usage];
}

static GasMix decode(Object result) {
Expand All @@ -302,6 +309,7 @@ class GasMix {
index: result[0]! as int,
o2Percent: result[1]! as double,
hePercent: result[2]! as double,
usage: result[3] as int?,
);
}
}
Expand Down
27 changes: 25 additions & 2 deletions packages/libdivecomputer_plugin/linux/dive_computer_api.g.cc
Original file line number Diff line number Diff line change
Expand Up @@ -834,11 +834,14 @@ struct _LibdivecomputerPluginGasMix {
int64_t index;
double o2_percent;
double he_percent;
int64_t* usage;
};

G_DEFINE_TYPE(LibdivecomputerPluginGasMix, libdivecomputer_plugin_gas_mix, G_TYPE_OBJECT)

static void libdivecomputer_plugin_gas_mix_dispose(GObject* object) {
LibdivecomputerPluginGasMix* self = LIBDIVECOMPUTER_PLUGIN_GAS_MIX(object);
g_clear_pointer(&self->usage, g_free);
G_OBJECT_CLASS(libdivecomputer_plugin_gas_mix_parent_class)->dispose(object);
}

Expand All @@ -849,11 +852,18 @@ static void libdivecomputer_plugin_gas_mix_class_init(LibdivecomputerPluginGasMi
G_OBJECT_CLASS(klass)->dispose = libdivecomputer_plugin_gas_mix_dispose;
}

LibdivecomputerPluginGasMix* libdivecomputer_plugin_gas_mix_new(int64_t index, double o2_percent, double he_percent) {
LibdivecomputerPluginGasMix* libdivecomputer_plugin_gas_mix_new(int64_t index, double o2_percent, double he_percent, int64_t* usage) {
LibdivecomputerPluginGasMix* self = LIBDIVECOMPUTER_PLUGIN_GAS_MIX(g_object_new(libdivecomputer_plugin_gas_mix_get_type(), nullptr));
self->index = index;
self->o2_percent = o2_percent;
self->he_percent = he_percent;
if (usage != nullptr) {
self->usage = static_cast<int64_t*>(malloc(sizeof(int64_t)));
*self->usage = *usage;
}
else {
self->usage = nullptr;
}
return self;
}

Expand All @@ -872,11 +882,17 @@ double libdivecomputer_plugin_gas_mix_get_he_percent(LibdivecomputerPluginGasMix
return self->he_percent;
}

int64_t* libdivecomputer_plugin_gas_mix_get_usage(LibdivecomputerPluginGasMix* self) {
g_return_val_if_fail(LIBDIVECOMPUTER_PLUGIN_IS_GAS_MIX(self), nullptr);
return self->usage;
}

static FlValue* libdivecomputer_plugin_gas_mix_to_list(LibdivecomputerPluginGasMix* self) {
FlValue* values = fl_value_new_list();
fl_value_append_take(values, fl_value_new_int(self->index));
fl_value_append_take(values, fl_value_new_float(self->o2_percent));
fl_value_append_take(values, fl_value_new_float(self->he_percent));
fl_value_append_take(values, self->usage != nullptr ? fl_value_new_int(*self->usage) : fl_value_new_null());
return values;
}

Expand All @@ -887,7 +903,14 @@ static LibdivecomputerPluginGasMix* libdivecomputer_plugin_gas_mix_new_from_list
double o2_percent = fl_value_get_float(value1);
FlValue* value2 = fl_value_get_list_value(values, 2);
double he_percent = fl_value_get_float(value2);
return libdivecomputer_plugin_gas_mix_new(index, o2_percent, he_percent);
FlValue* value3 = fl_value_get_list_value(values, 3);
int64_t* usage = nullptr;
int64_t usage_value;
if (fl_value_get_type(value3) != FL_VALUE_TYPE_NULL) {
usage_value = fl_value_get_int(value3);
usage = &usage_value;
}
return libdivecomputer_plugin_gas_mix_new(index, o2_percent, he_percent, usage);
}

struct _LibdivecomputerPluginTankInfo {
Expand Down
16 changes: 15 additions & 1 deletion packages/libdivecomputer_plugin/linux/dive_computer_api.g.h
Original file line number Diff line number Diff line change
Expand Up @@ -524,12 +524,13 @@ G_DECLARE_FINAL_TYPE(LibdivecomputerPluginGasMix, libdivecomputer_plugin_gas_mix
* index: field in this object.
* o2_percent: field in this object.
* he_percent: field in this object.
* usage: field in this object.
*
* Creates a new #GasMix object.
*
* Returns: a new #LibdivecomputerPluginGasMix
*/
LibdivecomputerPluginGasMix* libdivecomputer_plugin_gas_mix_new(int64_t index, double o2_percent, double he_percent);
LibdivecomputerPluginGasMix* libdivecomputer_plugin_gas_mix_new(int64_t index, double o2_percent, double he_percent, int64_t* usage);

/**
* libdivecomputer_plugin_gas_mix_get_index
Expand Down Expand Up @@ -561,6 +562,19 @@ double libdivecomputer_plugin_gas_mix_get_o2_percent(LibdivecomputerPluginGasMix
*/
double libdivecomputer_plugin_gas_mix_get_he_percent(LibdivecomputerPluginGasMix* object);

/**
* libdivecomputer_plugin_gas_mix_get_usage
* @object: a #LibdivecomputerPluginGasMix.
*
* Gas usage from libdivecomputer's `dc_usage_t` (1=oxygen, 2=diluent,
* 3=sidemount); null when the computer reported no usage (DC_USAGE_NONE).
* Set on the gas mix itself, so it is available even when the mix has no
* tank/transmitter record.
*
* Returns: the field value.
*/
int64_t* libdivecomputer_plugin_gas_mix_get_usage(LibdivecomputerPluginGasMix* object);

/**
* LibdivecomputerPluginTankInfo:
*
Expand Down
6 changes: 5 additions & 1 deletion packages/libdivecomputer_plugin/linux/dive_converter.c
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,15 @@ LibdivecomputerPluginParsedDive* convert_parsed_dive(
// Convert gas mixes.
FlValue* gas_mixes = fl_value_new_list();
for (unsigned int i = 0; i < dive->gasmix_count; i++) {
int64_t gm_usage_val = (int64_t)dive->gasmixes[i].usage;
int64_t* gm_usage = (dive->gasmixes[i].usage == 0) ? NULL : &gm_usage_val;

LibdivecomputerPluginGasMix* mix =
libdivecomputer_plugin_gas_mix_new(
(int64_t)i,
dive->gasmixes[i].oxygen * 100.0,
dive->gasmixes[i].helium * 100.0);
dive->gasmixes[i].helium * 100.0,
gm_usage);
fl_value_append_take(
gas_mixes,
fl_value_new_custom_object(133, G_OBJECT(mix)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ static int extract_dive_fields(dc_parser_t *parser, libdc_parsed_dive_t *dive) {
if (dc_parser_get_field(parser, DC_FIELD_GASMIX, i, &gm) == DC_STATUS_SUCCESS) {
dive->gasmixes[i].oxygen = gm.oxygen;
dive->gasmixes[i].helium = gm.helium;
dive->gasmixes[i].usage = gm.usage;
}
}
dive->gasmix_count = gasmix_count;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ typedef struct {
typedef struct {
double oxygen; // fraction 0.0-1.0
double helium; // fraction 0.0-1.0
unsigned int usage; // dc_usage_t (0=none, 1=oxygen, 2=diluent, 3=sidemount)
} libdc_gasmix_t;

typedef struct {
Expand Down
Loading