bottomtime 0.8.2__tar.gz → 0.8.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bottomtime-0.8.2 → bottomtime-0.8.3}/PKG-INFO +1 -1
- {bottomtime-0.8.2 → bottomtime-0.8.3}/pyproject.toml +1 -1
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/__init__.py +1 -1
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/deco.py +35 -4
- {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_deco.py +20 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/.gitignore +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/LICENSE +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/README.md +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/api.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/app.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/app_ui/index.html +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/cli.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/__init__.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/archive.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/garmin_fit.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/shearwater_db.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/shearwater_xml.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/match/__init__.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/match/correlate.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/match/matcher.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/plot.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/rebuild.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/store/__init__.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/store/db.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/store/schema.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/verify/__init__.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/verify/crosscheck.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_app.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_cli.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_compare.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_correlate.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_decoder_dependency.py +0 -0
- {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_shearwater_ingest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bottomtime
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.3
|
|
4
4
|
Summary: Lossless dive-log store: ingest, decode, and reconcile dives from Garmin (FIT) and Shearwater (Petrel Native Format) dive computers
|
|
5
5
|
Project-URL: Homepage, https://snehankekre.com/bottomtime/
|
|
6
6
|
Project-URL: Documentation, https://snehankekre.com/bottomtime/manual.html
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bottomtime"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.3"
|
|
8
8
|
description = "Lossless dive-log store: ingest, decode, and reconcile dives from Garmin (FIT) and Shearwater (Petrel Native Format) dive computers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
doesn't record.
|
|
3
3
|
|
|
4
4
|
bottomtime rebuilds the computer's Bühlmann ZHL-16C tissue loading from the
|
|
5
|
-
log's depth, gas, and (on a rebreather) loop ppO2
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
log's depth, gas, and (on a rebreather) loop ppO2. It starts from the tissue
|
|
6
|
+
state the computer itself wrote at the start of the log (PNF records 0x70 to
|
|
7
|
+
0x75), or, for a log without one, carries it over from the same computer's
|
|
8
|
+
dives in the 48 hours before. The GF99 that tissue state implies is compared
|
|
9
|
+
with the GF99 the computer logged, so every dive says how well the model
|
|
10
|
+
agreed with the computer.
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
from __future__ import annotations
|
|
12
14
|
|
|
13
15
|
import json
|
|
14
16
|
import sqlite3
|
|
17
|
+
import struct
|
|
15
18
|
from datetime import datetime, timedelta
|
|
16
19
|
from statistics import median
|
|
17
20
|
|
|
@@ -82,6 +85,26 @@ def _run(log: dict, state):
|
|
|
82
85
|
return (pn, ph), out
|
|
83
86
|
|
|
84
87
|
|
|
88
|
+
def first_snapshot(con: sqlite3.Connection, source_dive_id: int):
|
|
89
|
+
"""The tissue state a Shearwater wrote before its first sample, as
|
|
90
|
+
(16 nitrogen, 16 helium) pressures in bar, or None. A snapshot is six
|
|
91
|
+
records, 0x70 to 0x75, holding little-endian floats from byte 4: seven in
|
|
92
|
+
each of the first five records and six in the last, whose last four bytes
|
|
93
|
+
are a big-endian timestamp. The first 16 floats are nitrogen and the next
|
|
94
|
+
16 helium. The nine after that aren't known yet."""
|
|
95
|
+
rows = con.execute(
|
|
96
|
+
"SELECT type_key, payload FROM undecoded_payloads WHERE source_dive_id=? AND container='pnf_record'"
|
|
97
|
+
" AND type_key IN ('0x70','0x71','0x72','0x73','0x74','0x75') ORDER BY seq LIMIT 6",
|
|
98
|
+
(source_dive_id,),
|
|
99
|
+
).fetchall()
|
|
100
|
+
if [k for k, _ in rows] != ["0x70", "0x71", "0x72", "0x73", "0x74", "0x75"]:
|
|
101
|
+
return None
|
|
102
|
+
values = []
|
|
103
|
+
for k, payload in rows:
|
|
104
|
+
values += struct.unpack("<6f" if k == "0x75" else "<7f", payload[4:28 if k == "0x75" else 32])
|
|
105
|
+
return values[0:16], values[16:32]
|
|
106
|
+
|
|
107
|
+
|
|
85
108
|
def _log(con: sqlite3.Connection, source_dive_id: int) -> dict:
|
|
86
109
|
sd = con.execute(
|
|
87
110
|
"SELECT start_time_local, mode, header_json FROM source_dives WHERE id=?", (source_dive_id,)
|
|
@@ -109,6 +132,10 @@ def surface_gf(db_path, source_dive_id: int) -> dict | None:
|
|
|
109
132
|
log = _log(con, source_dive_id)
|
|
110
133
|
if log["header"].get("deco_model") not in (None, "gf"):
|
|
111
134
|
return None
|
|
135
|
+
own = first_snapshot(con, source_dive_id)
|
|
136
|
+
if own:
|
|
137
|
+
_, out = _run(log, own)
|
|
138
|
+
return _result(out, log)
|
|
112
139
|
device = con.execute("SELECT device FROM source_dives WHERE id=?", (source_dive_id,)).fetchone()[0]
|
|
113
140
|
earlier = con.execute(
|
|
114
141
|
"SELECT id FROM source_dives WHERE device=? AND start_time_local < ? AND start_time_local >= ?"
|
|
@@ -128,6 +155,10 @@ def surface_gf(db_path, source_dive_id: int) -> dict | None:
|
|
|
128
155
|
_, out = _run(log, state)
|
|
129
156
|
finally:
|
|
130
157
|
con.close()
|
|
158
|
+
return _result(out, log)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _result(out, log) -> dict:
|
|
131
162
|
gaps = [abs(m - g) for (m, _), g in zip(out, log["samples"]["gf99"]) if g is not None and g >= 5]
|
|
132
163
|
return {
|
|
133
164
|
"surface_gf": [round(max(sf, 0), 1) for _, sf in out],
|
|
@@ -24,3 +24,23 @@ def test_a_rebreather_at_a_high_setpoint_loads_less_than_air():
|
|
|
24
24
|
_, air = _run(_square(30, 25), _surface_state(1.013))
|
|
25
25
|
_, ccr = _run(_square(30, 25, mode="cc", ppo2=1.3), _surface_state(1.013))
|
|
26
26
|
assert ccr[150][1] < air[150][1]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_the_computers_own_tissue_snapshot_is_read_as_16_nitrogen_and_16_helium():
|
|
30
|
+
import sqlite3
|
|
31
|
+
import struct
|
|
32
|
+
|
|
33
|
+
from bottomtime.deco import first_snapshot
|
|
34
|
+
|
|
35
|
+
values = [0.75 + i / 100 for i in range(16)] + [0.001 * (i + 1) for i in range(16)] + [0.0] * 9
|
|
36
|
+
con = sqlite3.connect(":memory:")
|
|
37
|
+
con.execute("CREATE TABLE undecoded_payloads (source_dive_id, container, type_key, seq, payload)")
|
|
38
|
+
for k in range(6):
|
|
39
|
+
floats = values[k * 7:k * 7 + (6 if k == 5 else 7)]
|
|
40
|
+
payload = bytes([0x70 + k, 0, 0, 0]) + struct.pack("<%df" % len(floats), *floats)
|
|
41
|
+
payload += struct.pack(">I", 1783338232) if k == 5 else b""
|
|
42
|
+
con.execute("INSERT INTO undecoded_payloads VALUES (1, 'pnf_record', ?, ?, ?)", (f"0x{0x70 + k:x}", 10 + k, payload))
|
|
43
|
+
n2, he = first_snapshot(con, 1)
|
|
44
|
+
assert [round(v, 3) for v in n2] == [round(v, 3) for v in values[:16]]
|
|
45
|
+
assert [round(v, 4) for v in he] == [round(v, 4) for v in values[16:32]]
|
|
46
|
+
assert first_snapshot(con, 2) is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|