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.
Files changed (33) hide show
  1. {bottomtime-0.8.2 → bottomtime-0.8.3}/PKG-INFO +1 -1
  2. {bottomtime-0.8.2 → bottomtime-0.8.3}/pyproject.toml +1 -1
  3. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/__init__.py +1 -1
  4. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/deco.py +35 -4
  5. {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_deco.py +20 -0
  6. {bottomtime-0.8.2 → bottomtime-0.8.3}/.gitignore +0 -0
  7. {bottomtime-0.8.2 → bottomtime-0.8.3}/LICENSE +0 -0
  8. {bottomtime-0.8.2 → bottomtime-0.8.3}/README.md +0 -0
  9. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/api.py +0 -0
  10. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/app.py +0 -0
  11. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/app_ui/index.html +0 -0
  12. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/cli.py +0 -0
  13. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/__init__.py +0 -0
  14. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/archive.py +0 -0
  15. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/garmin_fit.py +0 -0
  16. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/shearwater_db.py +0 -0
  17. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/ingest/shearwater_xml.py +0 -0
  18. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/match/__init__.py +0 -0
  19. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/match/correlate.py +0 -0
  20. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/match/matcher.py +0 -0
  21. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/plot.py +0 -0
  22. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/rebuild.py +0 -0
  23. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/store/__init__.py +0 -0
  24. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/store/db.py +0 -0
  25. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/store/schema.py +0 -0
  26. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/verify/__init__.py +0 -0
  27. {bottomtime-0.8.2 → bottomtime-0.8.3}/src/bottomtime/verify/crosscheck.py +0 -0
  28. {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_app.py +0 -0
  29. {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_cli.py +0 -0
  30. {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_compare.py +0 -0
  31. {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_correlate.py +0 -0
  32. {bottomtime-0.8.2 → bottomtime-0.8.3}/tests/test_decoder_dependency.py +0 -0
  33. {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.2
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.2"
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"
@@ -1,6 +1,6 @@
1
1
  """bottomtime: lossless dive-log store for multi-computer divers."""
2
2
 
3
- __version__ = "0.8.2"
3
+ __version__ = "0.8.3"
4
4
 
5
5
  from .api import compare_logs, list_dives, load_dive
6
6
 
@@ -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, carrying it over from the
6
- same computer's dives in the 48 hours before. The GF99 that tissue state
7
- implies is compared with the GF99 the computer logged, so every dive says how
8
- well the model agreed with the computer before its surface GF is trusted.
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