aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.clang-format120
-rw-r--r--CODE_OF_CONDUCT1
-rw-r--r--LICENCE20
-rw-r--r--Makefile8
-rw-r--r--deco.c279
-rw-r--r--deco.h68
-rw-r--r--opendeco.c81
-rw-r--r--output.c106
-rw-r--r--output.h19
-rw-r--r--schedule.c227
-rw-r--r--schedule.h42
11 files changed, 971 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..a273c2a
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,120 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# clang-format configuration file. Intended for clang-format >= 11.
+#
+# For more information, see:
+#
+# Documentation/process/clang-format.rst
+# https://clang.llvm.org/docs/ClangFormat.html
+# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
+#
+---
+AccessModifierOffset: -4
+AlignAfterOpenBracket: Align
+AlignArrayOfStructures: Left
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignEscapedNewlines: Left
+AlignOperands: true
+AlignTrailingComments: false
+AllowAllArgumentsOnNextLine: false
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: false
+BinPackArguments: true
+BinPackParameters: true
+BraceWrapping:
+ AfterClass: false
+ AfterControlStatement: false
+ AfterEnum: false
+ AfterFunction: true
+ AfterNamespace: true
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ AfterExternBlock: false
+ BeforeCatch: false
+ BeforeElse: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Custom
+BreakBeforeInheritanceComma: false
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeComma
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: false
+ColumnLimit: 119
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 8
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: false
+DerivePointerAlignment: false
+DisableFormat: false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: false
+IncludeBlocks: Preserve
+IncludeCategories:
+ - Regex: '.*'
+ Priority: 1
+IncludeIsMainRegex: '(Test)?$'
+IndentCaseLabels: false
+IndentGotoLabels: false
+IndentPPDirectives: None
+IndentWidth: 4
+IndentWrappedFunctionNames: false
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 8
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+
+# Taken from git's rules
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakString: 10
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 60
+PointerAlignment: Right
+ReflowComments: false
+SortIncludes: false
+SortUsingDeclarations: false
+SpaceAfterCStyleCast: true
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatementsExceptForEachMacros
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+Standard: Cpp03
+TabWidth: 8
+UseTab: Never
+Cpp11BracedListStyle: true
+...
diff --git a/CODE_OF_CONDUCT b/CODE_OF_CONDUCT
new file mode 100644
index 0000000..a94d3c0
--- /dev/null
+++ b/CODE_OF_CONDUCT
@@ -0,0 +1 @@
+Don't be a dickhead, or I'll block you from the repo until you stop.
diff --git a/LICENCE b/LICENCE
new file mode 100644
index 0000000..851f256
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,20 @@
+MIT No Attribution Licence
+
+Copyright 2022 timn
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+SPDX-License-Identifier: MIT-0
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6ebe7de
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,8 @@
+opendeco: opendeco.c deco.c deco.h schedule.c schedule.h output.c output.h
+ gcc -O3 -lm -Wall -Werror opendeco.c deco.c schedule.c output.c -o opendeco
+
+run: opendeco
+ ./opendeco
+
+clean:
+ rm opendeco
diff --git a/deco.c b/deco.c
new file mode 100644
index 0000000..66182d9
--- /dev/null
+++ b/deco.c
@@ -0,0 +1,279 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include <assert.h>
+#include <math.h>
+#include <stdbool.h>
+
+#include "deco.h"
+
+enum ALGO ALGO_VER = ZHL_16C;
+
+#define PO2_MAX (1.6)
+#define END_MAX (abs_depth(msw_to_bar(30)))
+#define RND(x) (round((x) *10000) / 10000)
+
+typedef struct zhl_n2_t {
+ double t;
+ double a[3];
+ double b;
+} zhl_n2_t;
+
+typedef struct zhl_he_t {
+ double t;
+ double a;
+ double b;
+} zhl_he_t;
+
+const zhl_n2_t ZHL16N[] = {
+ {.t = 5.0, .a = {1.1696, 1.1696, 1.1696}, .b = 0.5578},
+ {.t = 8.0, .a = {1.0000, 1.0000, 1.0000}, .b = 0.6514},
+ {.t = 12.5, .a = {0.8618, 0.8618, 0.8618}, .b = 0.7222},
+ {.t = 18.5, .a = {0.7562, 0.7562, 0.7562}, .b = 0.7825},
+ {.t = 27.0, .a = {0.6667, 0.6667, 0.6200}, .b = 0.8126},
+ {.t = 38.3, .a = {0.5933, 0.5600, 0.5043}, .b = 0.8434},
+ {.t = 54.3, .a = {0.5282, 0.4947, 0.4410}, .b = 0.8693},
+ {.t = 77.0, .a = {0.4701, 0.4500, 0.4000}, .b = 0.8910},
+ {.t = 109.0, .a = {0.4187, 0.4187, 0.3750}, .b = 0.9092},
+ {.t = 146.0, .a = {0.3798, 0.3798, 0.3500}, .b = 0.9222},
+ {.t = 187.0, .a = {0.3497, 0.3497, 0.3295}, .b = 0.9319},
+ {.t = 239.0, .a = {0.3223, 0.3223, 0.3065}, .b = 0.9403},
+ {.t = 305.0, .a = {0.2971, 0.2850, 0.2835}, .b = 0.9477},
+ {.t = 390.0, .a = {0.2737, 0.2737, 0.2610}, .b = 0.9544},
+ {.t = 498.0, .a = {0.2523, 0.2523, 0.2480}, .b = 0.9602},
+ {.t = 635.0, .a = {0.2327, 0.2327, 0.2327}, .b = 0.9653},
+};
+
+const zhl_he_t ZHL16He[] = {
+ {.t = 1.88, .a = 1.6189, .b = 0.4770},
+ {.t = 3.02, .a = 1.3830, .b = 0.5747},
+ {.t = 4.72, .a = 1.1919, .b = 0.6527},
+ {.t = 6.99, .a = 1.0458, .b = 0.7223},
+ {.t = 10.21, .a = 0.9220, .b = 0.7582},
+ {.t = 14.48, .a = 0.8205, .b = 0.7957},
+ {.t = 20.53, .a = 0.7305, .b = 0.8279},
+ {.t = 29.11, .a = 0.6502, .b = 0.8553},
+ {.t = 41.20, .a = 0.5950, .b = 0.8757},
+ {.t = 55.19, .a = 0.5545, .b = 0.8903},
+ {.t = 70.69, .a = 0.5333, .b = 0.8997},
+ {.t = 90.34, .a = 0.5189, .b = 0.9073},
+ {.t = 115.29, .a = 0.5181, .b = 0.9122},
+ {.t = 147.42, .a = 0.5176, .b = 0.9171},
+ {.t = 188.24, .a = 0.5172, .b = 0.9217},
+ {.t = 240.03, .a = 0.5119, .b = 0.9267},
+};
+
+double bar_to_msw(const double bar)
+{
+ return bar * 10;
+}
+
+double msw_to_bar(const double msw)
+{
+ return msw / 10;
+}
+
+double abs_depth(const double gd)
+{
+ return gd + SURFACE_PRESSURE;
+}
+
+double gauge_depth(const double ad)
+{
+ return ad - SURFACE_PRESSURE;
+}
+
+gas_t gas_new(const unsigned char o2, const unsigned char he, double mod)
+{
+ assert(o2 + he <= 100);
+
+ if (mod == MOD_AUTO) {
+ double mod_po2 = PO2_MAX / (o2 / 100.0);
+ double mod_end = END_MAX / (1 - he / 100.0);
+
+ mod = min(mod_po2, mod_end);
+ }
+
+ return (gas_t){.o2 = o2, .he = he, .n2 = 100 - o2 - he, .mod = mod};
+}
+
+int gas_equal(const gas_t *g1, const gas_t *g2)
+{
+ return g1->o2 == g2->o2 && g1->he == g2->he;
+}
+
+unsigned char gas_o2(const gas_t *gas)
+{
+ return gas->o2;
+}
+
+unsigned char gas_he(const gas_t *gas)
+{
+ return gas->he;
+}
+
+unsigned char gas_n2(const gas_t *gas)
+{
+ return gas->n2;
+}
+
+double gas_mod(const gas_t *gas)
+{
+ return gas->mod;
+}
+
+double add_segment_ascdec(decostate_t *ds, const double dstart, const double dend, const double time, const gas_t *gas)
+{
+ assert(time > 0);
+
+ const double rate = (dend - dstart) / time;
+
+ for (int i = 0; i < 16; i++) {
+ double pio = gas_he(gas) / 100.0 * (dstart - P_WV);
+ double po = ds->phe[i];
+ double r = gas_he(gas) / 100.0 * rate;
+ double k = log(2) / ZHL16He[i].t;
+ double t = time;
+
+ ds->phe[i] = pio + r * (t - 1 / k) - (pio - po - (r / k)) * exp(-k * t);
+ }
+
+ for (int i = 0; i < 16; i++) {
+ double pio = gas_n2(gas) / 100.0 * (dstart - P_WV);
+ double po = ds->pn2[i];
+ double r = gas_n2(gas) / 100.0 * rate;
+ double k = log(2) / ZHL16N[i].t;
+ double t = time;
+
+ ds->pn2[i] = pio + r * (t - 1 / k) - (pio - po - (r / k)) * exp(-k * t);
+ }
+
+ /* TODO add CNS */
+ /* TODO add OTU */
+
+ if (dend > ds->max_depth)
+ ds->max_depth = dend;
+
+ return time;
+}
+
+double add_segment_const(decostate_t *ds, const double depth, const double time, const gas_t *gas)
+{
+ assert(time > 0);
+
+ for (int i = 0; i < 16; i++) {
+ double pio = gas_he(gas) / 100.0 * (depth - P_WV);
+ double po = ds->phe[i];
+ double k = log(2) / ZHL16He[i].t;
+ double t = time;
+
+ ds->phe[i] = po + (pio - po) * (1 - exp(-k * t));
+ }
+
+ for (int i = 0; i < 16; i++) {
+ double pio = gas_n2(gas) / 100.0 * (depth - P_WV);
+ double po = ds->pn2[i];
+ double k = log(2) / ZHL16N[i].t;
+ double t = time;
+
+ ds->pn2[i] = po + (pio - po) * (1 - exp(-k * t));
+ }
+
+ /* TODO add CNS */
+ /* TODO add OTU */
+
+ if (depth > ds->max_depth)
+ ds->max_depth = depth;
+
+ return time;
+}
+
+double get_gf(const decostate_t *ds, const double depth)
+{
+ const unsigned char lo = ds->gflo;
+ const unsigned char hi = ds->gfhi;
+
+ if (ds->firststop == -1)
+ return lo;
+
+ if (depth < SURFACE_PRESSURE)
+ return hi;
+
+ if (depth > ds->firststop)
+ return lo;
+
+ /* interpolate lo and hi between first stop and surface */
+ double next_stop = depth - ds->ceil_multiple;
+ return hi - (hi - lo) * gauge_depth(next_stop) / gauge_depth(ds->firststop);
+}
+
+double round_ceiling(const decostate_t *ds, const double c)
+{
+ assert(ds->ceil_multiple != 0);
+
+ return abs_depth(ds->ceil_multiple * ceil(RND(gauge_depth(c) / ds->ceil_multiple)));
+}
+
+double ceiling(const decostate_t *ds, double gf)
+{
+ double c = 0;
+ gf /= 100;
+
+ for (int i = 0; i < 16; i++) {
+ /* n2 a and b values */
+ double an = ZHL16N[i].a[ALGO_VER];
+ double bn = ZHL16N[i].b;
+
+ /* he a and b values */
+ double ah = ZHL16He[i].a;
+ double bh = ZHL16He[i].b;
+
+ /* scale n2 and he values for a and b proportional to their pressure */
+ double pn2 = ds->pn2[i];
+ double phe = ds->phe[i];
+
+ double a = ((an * pn2) + (ah * phe)) / (pn2 + phe);
+ double b = ((bn * pn2) + (bh * phe)) / (pn2 + phe);
+
+ /* update ceiling */
+ c = max(c, ((pn2 + phe) - (a * gf)) / (gf / b + 1 - gf));
+ }
+
+ return round_ceiling(ds, c);
+}
+
+void init_tissues(decostate_t *ds)
+{
+ const double pn2 = 0.79 * (SURFACE_PRESSURE - P_WV);
+ const double phe = 0.00 * (SURFACE_PRESSURE - P_WV);
+
+ for (int i = 0; i < 16; i++)
+ ds->pn2[i] = pn2;
+
+ for (int i = 0; i < 16; i++)
+ ds->phe[i] = phe;
+}
+
+void init_decostate(decostate_t *ds, const unsigned char gflo, const unsigned char gfhi, const double ceil_multiple)
+{
+ init_tissues(ds);
+
+ ds->gflo = gflo;
+ ds->gfhi = gfhi;
+ ds->firststop = -1;
+ ds->ceil_multiple = ceil_multiple;
+}
+
+double ppO2(double depth, const gas_t *gas)
+{
+ return gas_o2(gas) / 100.0 * depth;
+}
+
+double end(double depth, const gas_t *gas)
+{
+ return (gas_o2(gas) + gas_n2(gas)) / 100.0 * depth;
+}
+
+double ead(double depth, const gas_t *gas)
+{
+ return depth * gas_n2(gas) / 79.0;
+}
diff --git a/deco.h b/deco.h
new file mode 100644
index 0000000..5860591
--- /dev/null
+++ b/deco.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#ifndef DECO_H
+#define DECO_H
+
+#include <stddef.h>
+
+#define max(X, Y) (((X) > (Y)) ? (X) : (Y))
+#define min(X, Y) (((X) < (Y)) ? (X) : (Y))
+#define len(X) (sizeof(X) / sizeof((X)[0]))
+
+#define P_WV_BUHL 0.0627 /* Buhlmann value, Rq = 1.0, least conservative */
+#define P_WV_NAVY 0.0567 /* US. Navy value, Rq = 0.9 */
+#define P_WV_SCHR 0.0493 /* Schreiner value, Rq = 0.8, most conservative */
+#define P_WV P_WV_BUHL
+
+#define SURFACE_PRESSURE 1.01325
+#define MOD_AUTO 0
+
+enum ALGO {
+ ZHL_16A = 0,
+ ZHL_16B = 1,
+ ZHL_16C = 2,
+};
+
+typedef struct decostate_t {
+ double pn2[16];
+ double phe[16];
+ unsigned char gflo;
+ unsigned char gfhi;
+ double firststop;
+ double max_depth;
+ double ceil_multiple;
+} decostate_t;
+
+typedef struct gas_t {
+ unsigned char o2;
+ unsigned char he;
+ unsigned char n2;
+ double mod;
+} gas_t;
+
+double bar_to_msw(const double bar);
+double msw_to_bar(const double msw);
+double abs_depth(const double gd);
+double gauge_depth(const double ad);
+
+gas_t gas_new(const unsigned char o2, const unsigned char he, double mod);
+int gas_equal(const gas_t *g1, const gas_t *g2);
+unsigned char gas_o2(const gas_t *gas);
+unsigned char gas_he(const gas_t *gas);
+unsigned char gas_n2(const gas_t *gas);
+double gas_mod(const gas_t *gas);
+
+double add_segment_ascdec(decostate_t *ds, const double dstart, const double dend, const double time,
+ const gas_t *gas);
+double add_segment_const(decostate_t *ds, const double depth, const double time, const gas_t *gas);
+double get_gf(const decostate_t *ds, const double depth);
+double ceiling(const decostate_t *ds, double gf);
+double round_ceiling(const decostate_t *ds, const double c);
+
+void init_decostate(decostate_t *ds, const unsigned char gflo, const unsigned char gfhi, const double ceil_multiple);
+
+double ppO2(double depth, const gas_t *gas);
+double end(double depth, const gas_t *gas);
+double ead(double depth, const gas_t *gas);
+
+#endif /* end of include guard: DECO_H */
diff --git a/opendeco.c b/opendeco.c
new file mode 100644
index 0000000..0e9880d
--- /dev/null
+++ b/opendeco.c
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include <locale.h>
+#include <math.h>
+#include <wchar.h>
+
+#include "deco.h"
+#include "schedule.h"
+#include "output.h"
+
+#define MOD_OXY (abs_depth(msw_to_bar(6)))
+
+void print_segment_callback(const waypoint_t wp, segtype_t type)
+{
+ static double last_depth;
+ static double runtime;
+
+ wchar_t sign;
+
+ runtime += wp.time;
+
+ if (wp.depth < last_depth)
+ sign = ASC;
+ else if (wp.depth > last_depth)
+ sign = DEC;
+ else
+ sign = LVL;
+
+ if (type != SEG_TRAVEL)
+ print_planline(sign, wp.depth, wp.time, runtime, wp.gas);
+
+ last_depth = wp.depth;
+}
+
+void empty_callback(const waypoint_t wp, segtype_t type)
+{
+}
+
+int main(int argc, const char *argv[])
+{
+ setlocale(LC_ALL, "en_US.utf8");
+
+ /* setup */
+ decostate_t ds;
+ init_decostate(&ds, 30, 75, msw_to_bar(3));
+
+ const gas_t ean32 = gas_new(32, 0, MOD_AUTO);
+
+ /* simulate dive */
+ waypoint_t waypoints[] = {
+ {.depth = abs_depth(msw_to_bar(30)), .time = 3.333, &ean32},
+ {.depth = abs_depth(msw_to_bar(30)), .time = 116.666, &ean32},
+ };
+
+ print_planhead();
+ simulate_dive(&ds, waypoints, len(waypoints), &print_segment_callback);
+
+ /* generate deco schedule */
+ double depth = waypoints[len(waypoints) - 1].depth;
+ const gas_t *gas = waypoints[len(waypoints) - 1].gas;
+
+ gas_t deco_gasses[] = {
+ /* gas_new(40, 0, MOD_AUTO), */
+ gas_new(50, 0, MOD_AUTO),
+ /* gas_new(100, 0, MOD_OXY), */
+ };
+
+ /* determine @+5 TTS */
+ decostate_t ds_ = ds;
+ add_segment_const(&ds_, depth, 5, gas);
+ decoinfo_t di_plus5 = calc_deco(&ds_, depth, gas, deco_gasses, len(deco_gasses), &empty_callback);
+
+ /* print actual deco schedule */
+ decoinfo_t di = calc_deco(&ds, depth, gas, deco_gasses, len(deco_gasses), &print_segment_callback);
+
+ /* output deco info and disclaimer */
+ wprintf(L"\nNDL: %i TTS: %i TTS @+5: %i\n", (int) floor(di.ndl), (int) ceil(di.tts), (int) ceil(di_plus5.tts));
+ print_planfoot(&ds);
+
+ return 0;
+}
diff --git a/output.c b/output.c
new file mode 100644
index 0000000..3d8a597
--- /dev/null
+++ b/output.c
@@ -0,0 +1,106 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include <math.h>
+#include <stdio.h>
+
+#include "output.h"
+
+extern enum ALGO ALGO_VER;
+
+void format_mm_ss(char *buf, const size_t buflen, const double time)
+{
+ double mm;
+ double ss = round(modf(time, &mm) * 60);
+
+ /* prevents 0.99999 minutes showing as 00:60 */
+ mm += ss / 60;
+ ss = (int) ss % 60;
+
+ snprintf(buf, buflen, "%3i:%02i", (int) mm, (int) ss);
+}
+
+void format_gas(char *buf, const size_t buflen, const gas_t *gas)
+{
+ if (gas_o2(gas) == 21 && gas_he(gas) == 0)
+ snprintf(buf, buflen, "Air");
+ else if (gas_o2(gas) == 100)
+ snprintf(buf, buflen, "Oxygen");
+ else if (gas_he(gas) == 0)
+ snprintf(buf, buflen, "Nitrox %i", gas_o2(gas));
+ else
+ snprintf(buf, buflen, "%i/%i", gas_o2(gas), gas_he(gas));
+}
+
+void print_planhead()
+{
+ wprintf(L"DIVE PLAN\n\n");
+ wprintf(L" %-1s %-5s %-8s %-7s %1s %-9s %-4s %-3s\n", "", "Depth", "Duration", "Runtime", "", "Gas", "pO2",
+ "EAD");
+}
+
+void print_planline(const wchar_t sign, const double depth, const double time, const double runtime, const gas_t *gas)
+{
+ static char gasbuf[11];
+ static char runbuf[8];
+ static char pO2buf[5];
+ static char eadbuf[4];
+ static char timbuf[16];
+
+ static gas_t last_gas;
+
+ const int depth_m = round(bar_to_msw(gauge_depth(depth)));
+ const int ead_m = round(bar_to_msw(max(0, gauge_depth(ead(depth, gas)))));
+
+ wchar_t swi = L' ';
+
+ snprintf(runbuf, len(runbuf), "(%i)", (int) ceil(runtime));
+ format_gas(gasbuf, len(gasbuf), gas);
+ format_mm_ss(timbuf, len(timbuf), time);
+
+ /* print gas swich symbol if gas changed */
+ if (!gas_equal(gas, &last_gas)) {
+ last_gas = *gas;
+ swi = SWI;
+ }
+
+ /* only print ead and pO2 on stops */
+ if (sign == LVL) {
+ snprintf(eadbuf, 4, "%3i", ead_m);
+ snprintf(pO2buf, 5, "%4.2f", ppO2(depth, gas));
+ } else {
+ snprintf(eadbuf, 4, "%3s", "-");
+ snprintf(pO2buf, 5, "%4s", "-");
+ }
+
+ wprintf(L" %lc %4im %8s %-7s %lc %-9s %s %s\n", sign, depth_m, timbuf, runbuf, swi, gasbuf, pO2buf, eadbuf);
+}
+
+void print_planfoot(const decostate_t *ds)
+{
+ char *model;
+ char *rq;
+
+ if (ALGO_VER == ZHL_16A)
+ model = "ZHL-16A";
+ else if (ALGO_VER == ZHL_16B)
+ model = "ZHL-16B";
+ else if (ALGO_VER == ZHL_16C)
+ model = "ZHL-16C";
+ else
+ model = "???";
+
+ if (P_WV == P_WV_BUHL)
+ rq = "1.0";
+ else if (P_WV == P_WV_NAVY)
+ rq = "0.9";
+ else if (P_WV == P_WV_SCHR)
+ rq = "0.8";
+ else
+ rq = "???";
+
+ wprintf(L"\nDeco model: Buhlmann %s\n", model);
+ wprintf(L"Conservatism: GF %i/%i, Rq = %s\n", ds->gflo, ds->gfhi, rq);
+ wprintf(L"Surface pressure: %4.3fbar\n\n", SURFACE_PRESSURE);
+
+ wprintf(L"WARNING: DIVE PLAN MAY BE INACCURATE AND MAY CONTAIN\nERRORS THAT COULD LEAD TO INJURY OR DEATH.\n");
+}
diff --git a/output.h b/output.h
new file mode 100644
index 0000000..0cedcc7
--- /dev/null
+++ b/output.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#ifndef OUTPUT_H
+#define OUTPUT_H
+
+#include <wchar.h>
+
+#include "deco.h"
+
+#define ASC 0x2197 /* Unicode North East Arrow */
+#define LVL 0x2192 /* Unicode Rightwards Arrow */
+#define DEC 0x2198 /* Unicode South East Arrow */
+#define SWI 0x1F5D8 /* Clockwise Right and Left Semicircle Arrows */
+
+void print_planhead();
+void print_planline(const wchar_t sign, const double depth, const double time, const double runtime, const gas_t *gas);
+void print_planfoot(const decostate_t *ds);
+
+#endif /* end of include guard: OUTPUT_H */
diff --git a/schedule.c b/schedule.c
new file mode 100644
index 0000000..5bfc1d5
--- /dev/null
+++ b/schedule.c
@@ -0,0 +1,227 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#include <assert.h>
+#include <math.h>
+
+#include "schedule.h"
+
+#define SAFETY_STOP_DEPTH (abs_depth(msw_to_bar(6)))
+#define SWITCH_INTERMEDIATE 1
+
+const gas_t *best_gas(const double depth, const gas_t *gasses, const int nof_gasses)
+{
+ const gas_t *best = NULL;
+ double mod_best = -1;
+
+ for (int i = 0; i < nof_gasses; i++) {
+ double mod = gas_mod(&gasses[i]);
+
+ if (depth <= mod && (mod_best == -1 || mod < mod_best)) {
+ best = &gasses[i];
+ mod_best = mod;
+ }
+ }
+
+ return best;
+}
+
+const gas_t *next_gas(const double depth, const gas_t *gasses, const int nof_gasses)
+{
+ const gas_t *next = NULL;
+ double mod_best = 0;
+
+ for (int i = 0; i < nof_gasses; i++) {
+ double mod = gas_mod(&gasses[i]);
+
+ if (depth > mod && mod > mod_best) {
+ next = &gasses[i];
+ mod_best = mod;
+ }
+ }
+
+ return next;
+}
+
+int direct_ascent(const decostate_t *ds, const double depth, const double time, const gas_t *gas)
+{
+ decostate_t ds_ = *ds;
+ assert(ds_.firststop == -1);
+
+ add_segment_ascdec(&ds_, depth, SURFACE_PRESSURE, time, gas);
+
+ return ceiling(&ds_, ds_.gfhi) <= SURFACE_PRESSURE;
+}
+
+void simulate_dive(decostate_t *ds, waypoint_t *waypoints, const int nof_waypoints, segment_callback_t seg_cb)
+{
+ double depth = SURFACE_PRESSURE;
+ double runtime = 0;
+
+ for (int i = 0; i < nof_waypoints; i++) {
+ double d = waypoints[i].depth;
+ double t = waypoints[i].time;
+ const gas_t *g = waypoints[i].gas;
+
+ if (d != depth) {
+ runtime += add_segment_ascdec(ds, depth, d, t, g);
+ depth = d;
+ } else {
+ runtime += add_segment_const(ds, d, t, g);
+ }
+
+ seg_cb((waypoint_t){.depth = d, .time = t, .gas = g}, SEG_DIVE);
+ }
+}
+
+double calc_ndl(decostate_t *ds, const double depth, const double ascrate, const gas_t *gas)
+{
+ decostate_t ds_ = *ds;
+ double ndl = 0;
+
+ while (ndl < 360) {
+ double tmp = add_segment_const(&ds_, depth, 1, gas);
+
+ if (!direct_ascent(&ds_, depth, gauge_depth(depth) / ascrate, gas))
+ break;
+
+ ndl += tmp;
+ }
+
+ return ndl;
+}
+
+void extend_to_ndl(decostate_t *ds, const double depth, const double ascrate, const gas_t *gas,
+ segment_callback_t seg_cb)
+{
+ double ndl = calc_ndl(ds, depth, ascrate, gas);
+
+ /* add segment to reach ndl */
+ if (ndl) {
+ add_segment_const(ds, depth, ndl, gas);
+ seg_cb((waypoint_t){.depth = depth, .time = ndl, .gas = gas}, SEG_NDL);
+ }
+
+ /* either ascend directly or make a safety stop */
+ if (depth < SAFETY_STOP_DEPTH || ds->max_depth < abs_depth(msw_to_bar(10))) {
+ /* surface */
+ add_segment_ascdec(ds, depth, SURFACE_PRESSURE, gauge_depth(depth) / ascrate, gas);
+ seg_cb((waypoint_t){.depth = SURFACE_PRESSURE, .time = gauge_depth(depth) / ascrate, .gas = gas}, SEG_SURFACE);
+ } else {
+ /* ascend to safety stop */
+ add_segment_ascdec(ds, depth, SAFETY_STOP_DEPTH, (depth - SAFETY_STOP_DEPTH) / ascrate, gas);
+ seg_cb((waypoint_t){.depth = SAFETY_STOP_DEPTH, .time = (depth - SAFETY_STOP_DEPTH) / ascrate, .gas = gas},
+ SEG_TRAVEL);
+
+ /* stop for 3 minutes */
+ add_segment_const(ds, SAFETY_STOP_DEPTH, 3, gas);
+ seg_cb((waypoint_t){.depth = SAFETY_STOP_DEPTH, .time = 3, .gas = gas}, SEG_SAFETY_STOP);
+
+ /* surface */
+ add_segment_ascdec(ds, SAFETY_STOP_DEPTH, SURFACE_PRESSURE, gauge_depth(SAFETY_STOP_DEPTH) / ascrate, gas);
+ seg_cb((waypoint_t){.depth = SURFACE_PRESSURE, .time = gauge_depth(SAFETY_STOP_DEPTH) / ascrate, .gas = gas},
+ SEG_SURFACE);
+ }
+}
+
+decoinfo_t calc_deco(decostate_t *ds, const double start_depth, const gas_t *start_gas, const gas_t *deco_gasses,
+ const int nof_gasses, segment_callback_t seg_cb)
+{
+ decoinfo_t ret = {.tts = 0, .ndl = 0};
+
+ /* setup start parameters */
+ double depth = start_depth;
+ const gas_t *gas = start_gas;
+
+ const double asc_per_min = msw_to_bar(9);
+
+ /* check if direct ascent is possible */
+ if (direct_ascent(ds, depth, gauge_depth(depth) / asc_per_min, gas)) {
+ ret.ndl = calc_ndl(ds, depth, asc_per_min, gas);
+ return ret;
+ }
+
+ /* determine first stop */
+ double current_gf = get_gf(ds, depth);
+
+ if (ds->firststop == -1)
+ ds->firststop = ceiling(ds, current_gf);
+
+ /* switch to best deco gas if there is one available */
+ const gas_t *best = best_gas(depth, deco_gasses, nof_gasses);
+
+ if (best)
+ gas = best;
+
+ /* alternate between ascending and stopping */
+ for (;;) {
+ /* ascend */
+ for (;;) {
+ double stopdep = ceiling(ds, current_gf);
+ const gas_t *next = next_gas(depth, deco_gasses, nof_gasses);
+
+ if (SWITCH_INTERMEDIATE && next) {
+ /* determine switch depth */
+ double switch_depth = round_ceiling(ds, next->mod) - ds->ceil_multiple;
+ assert(switch_depth <= next->mod);
+
+ if (switch_depth > stopdep) {
+ /* ascend to switch depth */
+ ret.tts += add_segment_ascdec(ds, depth, switch_depth, (depth - switch_depth) / asc_per_min, gas);
+ seg_cb(
+ (waypoint_t){.depth = switch_depth, .time = (depth - switch_depth) / asc_per_min, .gas = gas},
+ SEG_TRAVEL);
+
+ depth = switch_depth;
+ current_gf = get_gf(ds, depth);
+
+ /* switch gas */
+ gas = next;
+
+ add_segment_const(ds, switch_depth, 1, gas);
+ seg_cb((waypoint_t){.depth = depth, .time = 1, .gas = gas}, SEG_GAS_SWITCH);
+
+ continue;
+ }
+ }
+
+ ret.tts += add_segment_ascdec(ds, depth, stopdep, (depth - stopdep) / asc_per_min, gas);
+
+ if (stopdep <= SURFACE_PRESSURE)
+ seg_cb((waypoint_t){.depth = stopdep, .time = (depth - stopdep) / asc_per_min, .gas = gas},
+ SEG_SURFACE);
+ else
+ seg_cb((waypoint_t){.depth = stopdep, .time = (depth - stopdep) / asc_per_min, .gas = gas},
+ SEG_TRAVEL);
+
+ depth = stopdep;
+ current_gf = get_gf(ds, depth);
+
+ /* if the ceiling moved while we ascended, keep ascending */
+ if (depth > ceiling(ds, current_gf))
+ continue;
+
+ break;
+ }
+
+ /* terminate if we surfaced */
+ if (depth <= SURFACE_PRESSURE)
+ break;
+
+ /* switch to better gas if available */
+ const gas_t *best = best_gas(depth, deco_gasses, nof_gasses);
+
+ if (best)
+ gas = best;
+
+ /* stop */
+ double stoplen = 0;
+
+ while (ceiling(ds, current_gf) == depth)
+ stoplen += add_segment_const(ds, depth, 1, gas);
+
+ ret.tts += stoplen;
+ seg_cb((waypoint_t){.depth = depth, .time = stoplen, .gas = gas}, SEG_DECO_STOP);
+ }
+
+ return ret;
+}
diff --git a/schedule.h b/schedule.h
new file mode 100644
index 0000000..b1703d7
--- /dev/null
+++ b/schedule.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: MIT-0 */
+
+#ifndef SCHEDULE_H
+#define SCHEDULE_H
+
+#include "deco.h"
+
+typedef struct waypoint_t {
+ double depth;
+ double time;
+ const gas_t *gas;
+} waypoint_t;
+
+typedef struct decoinfo_t {
+ double ndl;
+ double tts;
+} decoinfo_t;
+
+typedef enum segtype_t {
+ SEG_DECO_STOP,
+ SEG_DIVE,
+ SEG_GAS_SWITCH,
+ SEG_NDL,
+ SEG_SAFETY_STOP,
+ SEG_SURFACE,
+ SEG_TRAVEL,
+} segtype_t;
+
+typedef void (*segment_callback_t)(const waypoint_t, const segtype_t);
+
+const gas_t *best_gas(const double depth, const gas_t *gasses, const int nof_gasses);
+const gas_t *next_gas(const double depth, const gas_t *gasses, const int nof_gasses);
+
+int direct_ascent(const decostate_t *ds, const double depth, const double time, const gas_t *gas);
+double calc_ndl(decostate_t *ds, const double depth, const double ascrate, const gas_t *gas);
+
+void simulate_dive(decostate_t *ds, waypoint_t *waypoints, const int nof_waypoints, segment_callback_t seg_cb);
+
+decoinfo_t calc_deco(decostate_t *ds, const double start_depth, const gas_t *start_gas, const gas_t *deco_gasses,
+ const int nof_gasses, segment_callback_t seg_cb);
+
+#endif /* end of include guard: SCHEDULE_H */