bottomtime 0.2.0__tar.gz → 0.3.0__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.2.0 → bottomtime-0.3.0}/.gitignore +2 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/PKG-INFO +1 -1
- {bottomtime-0.2.0 → bottomtime-0.3.0}/pyproject.toml +1 -1
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/__init__.py +1 -1
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/decode/pnf.py +65 -14
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/shearwater_db.py +6 -3
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/store/schema.py +8 -4
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/verify/crosscheck.py +5 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/tests/test_pnf.py +42 -2
- bottomtime-0.2.0/.claude/settings.local.json +0 -30
- {bottomtime-0.2.0 → bottomtime-0.3.0}/.github/workflows/test.yml +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/LICENSE +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/README.md +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/api.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/cli.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/decode/__init__.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/__init__.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/archive.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/garmin_fit.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/shearwater_xml.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/match/__init__.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/match/correlate.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/match/matcher.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/plot.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/store/__init__.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/store/db.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/verify/__init__.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/tests/test_cli.py +0 -0
- {bottomtime-0.2.0 → bottomtime-0.3.0}/tests/test_correlate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bottomtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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://github.com/snehankekre/bottomtime
|
|
6
6
|
Project-URL: Issues, https://github.com/snehankekre/bottomtime/issues
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bottomtime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -23,6 +23,18 @@ DECODER_VERSION history:
|
|
|
23
23
|
dive_details.EndGF99 and decays during surface offgassing; byte 24 sits
|
|
24
24
|
just below the 3 m-quantized stop depth and zeroes when deco clears;
|
|
25
25
|
byte 18 equals the XML export's batteryVoltage x 100
|
|
26
|
+
3 reconciled against Shearwater Cloud's own parser (DiveLogParser.dll,
|
|
27
|
+
class PetrelNativeLogV14Parser, read from the desktop app). Every sample
|
|
28
|
+
byte 0-31 is now named. Changes vs v2:
|
|
29
|
+
- bytes 24/25 are ceiling/gf99 only under gradient-factor deco models;
|
|
30
|
+
under DCIEM (deco model 3) they are a safe-ascent depth instead
|
|
31
|
+
(byte 24 integer metres, byte 25 hundredths), decoded to
|
|
32
|
+
safe_ascent_depth_m with ceiling_m/gf99 left None.
|
|
33
|
+
- battery_v is a 16-bit value: (byte 17 << 8 | byte 18) / 100, not byte 18
|
|
34
|
+
alone (byte 17 is zero only because a ~1.5 V cell never needs it; a
|
|
35
|
+
higher-voltage computer such as a Teric would overflow the old read).
|
|
36
|
+
- byte 11 is battery_pct (BATTERY_PERCENT_REMAINING).
|
|
37
|
+
- bytes 30-31 are sac (surface air consumption, u16 / 100; 0xFFFF = n/a).
|
|
26
38
|
"""
|
|
27
39
|
|
|
28
40
|
from __future__ import annotations
|
|
@@ -31,7 +43,7 @@ import gzip
|
|
|
31
43
|
import struct
|
|
32
44
|
from dataclasses import dataclass, field
|
|
33
45
|
|
|
34
|
-
DECODER_VERSION =
|
|
46
|
+
DECODER_VERSION = 3
|
|
35
47
|
|
|
36
48
|
RECORD_SIZE = 32
|
|
37
49
|
|
|
@@ -44,12 +56,17 @@ INFO_EVENT = 0x30
|
|
|
44
56
|
DIVE_SAMPLE_EXT = 0xE1
|
|
45
57
|
FINAL = 0xFF
|
|
46
58
|
|
|
47
|
-
#
|
|
59
|
+
# Sample byte 12 is a packed status/mode byte. Bit meanings per Shearwater's
|
|
60
|
+
# PetrelNativeLogV14Parser: 0 gas-switch-needed, 1 external ppO2, 2 setpoint
|
|
61
|
+
# type (high/low), 3 circuit-switch type, 4 circuit mode (set = OC on this
|
|
62
|
+
# family), 5 CCR mode, 6-7 solenoid-fired count.
|
|
48
63
|
FLAG_GASSWITCH = 0x01
|
|
49
64
|
FLAG_PPO2_EXTERNAL = 0x02
|
|
50
65
|
FLAG_SETPOINT_HIGH = 0x04
|
|
51
66
|
FLAG_SC = 0x08
|
|
52
67
|
FLAG_OC = 0x10
|
|
68
|
+
FLAG_CCR_MODE = 0x20
|
|
69
|
+
SOLENOID_COUNT_SHIFT = 6
|
|
53
70
|
|
|
54
71
|
DIVE_MODES = {
|
|
55
72
|
0: "cc",
|
|
@@ -82,10 +99,9 @@ COMPUTER_MODELS = {
|
|
|
82
99
|
12: "Tern",
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
UNMAPPED_SAMPLE_BYTES = (11, 17, 30, 31)
|
|
102
|
+
# As of v3 every sample byte 0-31 is named (reconciled against Shearwater's
|
|
103
|
+
# own PetrelNativeLogV14Parser), so no bytes are stashed raw in extra_json.
|
|
104
|
+
SAC_SENTINEL = 0xFFFF # bytes 30-31 when no air integration / no SAC
|
|
89
105
|
|
|
90
106
|
|
|
91
107
|
def _u16be(data: bytes, off: int) -> int:
|
|
@@ -127,6 +143,9 @@ class PnfSample:
|
|
|
127
143
|
cns_pct: float | None = None
|
|
128
144
|
gf99: float | None = None
|
|
129
145
|
ceiling_m: float | None = None
|
|
146
|
+
safe_ascent_depth_m: float | None = None # DCIEM only (bytes 24-25)
|
|
147
|
+
battery_pct: int | None = None
|
|
148
|
+
sac: float | None = None
|
|
130
149
|
sensor1_raw: int | None = None
|
|
131
150
|
sensor2_raw: int | None = None
|
|
132
151
|
sensor3_raw: int | None = None
|
|
@@ -135,6 +154,13 @@ class PnfSample:
|
|
|
135
154
|
sensor3_ppo2: float | None = None
|
|
136
155
|
battery_v: float | None = None
|
|
137
156
|
status_flags: int | None = None
|
|
157
|
+
gas_switch_needed: bool | None = None
|
|
158
|
+
ppo2_external: bool | None = None
|
|
159
|
+
setpoint_high: bool | None = None
|
|
160
|
+
circuit_switch: bool | None = None
|
|
161
|
+
circuit_mode: int | None = None
|
|
162
|
+
ccr_mode: int | None = None
|
|
163
|
+
solenoid_fired_count: int | None = None
|
|
138
164
|
tank0_psi: float | None = None
|
|
139
165
|
tank1_psi: float | None = None
|
|
140
166
|
gas_time_min: float | None = None
|
|
@@ -230,11 +256,15 @@ def decode(blob: bytes) -> PnfDive:
|
|
|
230
256
|
seq = 0
|
|
231
257
|
t = 0.0
|
|
232
258
|
|
|
259
|
+
is_dciem = header["deco_model"] == "dciem"
|
|
260
|
+
|
|
233
261
|
for rtype, off in raw_records:
|
|
234
262
|
rec = data[off : off + RECORD_SIZE]
|
|
235
263
|
if rtype in (DIVE_SAMPLE, AVELO_SAMPLE):
|
|
236
264
|
samples.append(
|
|
237
|
-
_parse_sample(
|
|
265
|
+
_parse_sample(
|
|
266
|
+
rec, t, imperial, calibrated, calibration, rtype, is_dciem
|
|
267
|
+
)
|
|
238
268
|
)
|
|
239
269
|
t += interval_ms / 1000.0
|
|
240
270
|
elif rtype == FREEDIVE_SAMPLE:
|
|
@@ -295,6 +325,7 @@ def _parse_sample(
|
|
|
295
325
|
calibrated: int,
|
|
296
326
|
calibration: list[float],
|
|
297
327
|
rtype: int,
|
|
328
|
+
is_dciem: bool,
|
|
298
329
|
) -> PnfSample:
|
|
299
330
|
ft = 0.3048
|
|
300
331
|
depth = _u16be(rec, 1) / 10.0
|
|
@@ -311,6 +342,7 @@ def _parse_sample(
|
|
|
311
342
|
|
|
312
343
|
status = rec[12] if rtype != AVELO_SAMPLE else 0
|
|
313
344
|
ccr = (status & FLAG_OC) == 0 if rtype != AVELO_SAMPLE else False
|
|
345
|
+
decoded_status = rtype != AVELO_SAMPLE
|
|
314
346
|
|
|
315
347
|
sample = PnfSample(
|
|
316
348
|
t_s=t,
|
|
@@ -328,11 +360,34 @@ def _parse_sample(
|
|
|
328
360
|
sensor2_raw=rec[15],
|
|
329
361
|
sensor3_raw=rec[16],
|
|
330
362
|
cns_pct=float(rec[23]),
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
battery_v=rec[18] / 100.0,
|
|
363
|
+
battery_pct=rec[11],
|
|
364
|
+
battery_v=((rec[17] << 8) | rec[18]) / 100.0,
|
|
334
365
|
)
|
|
335
366
|
|
|
367
|
+
if decoded_status:
|
|
368
|
+
sample.gas_switch_needed = bool(status & FLAG_GASSWITCH)
|
|
369
|
+
sample.ppo2_external = bool(status & FLAG_PPO2_EXTERNAL)
|
|
370
|
+
sample.setpoint_high = bool(status & FLAG_SETPOINT_HIGH)
|
|
371
|
+
sample.circuit_switch = bool(status & FLAG_SC)
|
|
372
|
+
sample.circuit_mode = (status >> 4) & 1
|
|
373
|
+
sample.ccr_mode = (status >> 5) & 1
|
|
374
|
+
sample.solenoid_fired_count = (status >> SOLENOID_COUNT_SHIFT) & 3
|
|
375
|
+
|
|
376
|
+
# Bytes 24-25 are ceiling + GF99 under gradient-factor deco models. Under
|
|
377
|
+
# DCIEM (deco model 3) the same two bytes are a safe-ascent depth: byte 24
|
|
378
|
+
# whole metres, byte 25 hundredths, subtracted.
|
|
379
|
+
if is_dciem:
|
|
380
|
+
sample.safe_ascent_depth_m = rec[24] - rec[25] / 100.0
|
|
381
|
+
else:
|
|
382
|
+
if rec[24] != 0xFF:
|
|
383
|
+
sample.ceiling_m = float(rec[24])
|
|
384
|
+
if rec[25] != 0xFF: # 0xFF = not available (still on-gassing)
|
|
385
|
+
sample.gf99 = float(rec[25])
|
|
386
|
+
|
|
387
|
+
sac_raw = _u16be(rec, 30)
|
|
388
|
+
if sac_raw != SAC_SENTINEL:
|
|
389
|
+
sample.sac = sac_raw / 100.0
|
|
390
|
+
|
|
336
391
|
if ccr:
|
|
337
392
|
sample.setpoint = rec[19] / 100.0
|
|
338
393
|
if not (status & FLAG_PPO2_EXTERNAL):
|
|
@@ -363,10 +418,6 @@ def _parse_sample(
|
|
|
363
418
|
if at_plus_five != 0xFFFF:
|
|
364
419
|
sample.extra["at_plus_five_min"] = at_plus_five
|
|
365
420
|
|
|
366
|
-
for b in UNMAPPED_SAMPLE_BYTES:
|
|
367
|
-
if rec[b]:
|
|
368
|
-
sample.extra[f"b{b}"] = rec[b]
|
|
369
|
-
|
|
370
421
|
return sample
|
|
371
422
|
|
|
372
423
|
|
|
@@ -139,19 +139,22 @@ def _ingest_log(
|
|
|
139
139
|
con.executemany(
|
|
140
140
|
"INSERT INTO shearwater_samples (source_dive_id, t_s, depth_m, temp_c,"
|
|
141
141
|
" stop_depth_m, stop_or_ndl_min, tts_min, in_deco, avg_ppo2, o2_pct, he_pct,"
|
|
142
|
-
" setpoint, cns_pct, gf99, ceiling_m,
|
|
142
|
+
" setpoint, cns_pct, gf99, ceiling_m, safe_ascent_depth_m, battery_pct, sac,"
|
|
143
|
+
" sensor1_raw, sensor2_raw, sensor3_raw,"
|
|
143
144
|
" sensor1_ppo2, sensor2_ppo2, sensor3_ppo2, battery_v, status_flags,"
|
|
144
|
-
" tank0_psi, tank1_psi, gas_time_min, extra_json)"
|
|
145
|
-
" VALUES (
|
|
145
|
+
" solenoid_fired_count, tank0_psi, tank1_psi, gas_time_min, extra_json)"
|
|
146
|
+
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
146
147
|
[
|
|
147
148
|
(
|
|
148
149
|
dive_id, s.t_s, s.depth_m, s.temp_c,
|
|
149
150
|
s.stop_depth_m, s.stop_or_ndl_min, s.tts_min, s.in_deco,
|
|
150
151
|
s.avg_ppo2, s.o2_pct, s.he_pct,
|
|
151
152
|
s.setpoint, s.cns_pct, s.gf99, s.ceiling_m,
|
|
153
|
+
s.safe_ascent_depth_m, s.battery_pct, s.sac,
|
|
152
154
|
s.sensor1_raw, s.sensor2_raw, s.sensor3_raw,
|
|
153
155
|
s.sensor1_ppo2, s.sensor2_ppo2, s.sensor3_ppo2,
|
|
154
156
|
s.battery_v, s.status_flags,
|
|
157
|
+
s.solenoid_fired_count,
|
|
155
158
|
s.tank0_psi, s.tank1_psi, s.gas_time_min,
|
|
156
159
|
json.dumps(s.extra, separators=(",", ":")) if s.extra else None,
|
|
157
160
|
)
|
|
@@ -11,7 +11,7 @@ Design principles:
|
|
|
11
11
|
dives is the canonical entity built from matches + singletons.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
SCHEMA_VERSION =
|
|
14
|
+
SCHEMA_VERSION = 2
|
|
15
15
|
|
|
16
16
|
DDL = """
|
|
17
17
|
CREATE TABLE IF NOT EXISTS meta (
|
|
@@ -89,8 +89,11 @@ CREATE TABLE IF NOT EXISTS shearwater_samples (
|
|
|
89
89
|
he_pct REAL,
|
|
90
90
|
setpoint REAL,
|
|
91
91
|
cns_pct REAL,
|
|
92
|
-
gf99 REAL, -- populated at decoder_version >= 2
|
|
93
|
-
ceiling_m REAL, -- populated at decoder_version >= 2
|
|
92
|
+
gf99 REAL, -- populated at decoder_version >= 2; NULL under DCIEM
|
|
93
|
+
ceiling_m REAL, -- populated at decoder_version >= 2; NULL under DCIEM
|
|
94
|
+
safe_ascent_depth_m REAL, -- DCIEM only (bytes 24-25); NULL otherwise, v3
|
|
95
|
+
battery_pct INTEGER, -- battery percent remaining (byte 11), v3
|
|
96
|
+
sac REAL, -- surface air consumption (bytes 30-31), v3
|
|
94
97
|
sensor1_raw INTEGER,
|
|
95
98
|
sensor2_raw INTEGER,
|
|
96
99
|
sensor3_raw INTEGER,
|
|
@@ -98,7 +101,8 @@ CREATE TABLE IF NOT EXISTS shearwater_samples (
|
|
|
98
101
|
sensor2_ppo2 REAL,
|
|
99
102
|
sensor3_ppo2 REAL,
|
|
100
103
|
battery_v REAL,
|
|
101
|
-
status_flags INTEGER,
|
|
104
|
+
status_flags INTEGER, -- raw sample byte 12 (packed mode/status bits)
|
|
105
|
+
solenoid_fired_count INTEGER, -- CCR O2 solenoid firings, byte 12 bits 6-7, v3
|
|
102
106
|
tank0_psi REAL,
|
|
103
107
|
tank1_psi REAL,
|
|
104
108
|
gas_time_min REAL,
|
|
@@ -30,6 +30,7 @@ TOL = {
|
|
|
30
30
|
"temp_c": 0.51,
|
|
31
31
|
"battery_v": 0.011,
|
|
32
32
|
"ndl_min": 0.001,
|
|
33
|
+
"sac": 0.011,
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
|
|
@@ -71,6 +72,10 @@ def check_xml(con: sqlite3.Connection, xml_dir: Path, failures: list) -> None:
|
|
|
71
72
|
("averagePPO2", "avg_ppo2", 1.0),
|
|
72
73
|
("waterTemp", "temp_c", 1.0),
|
|
73
74
|
("batteryVoltage", "battery_v", 1.0),
|
|
75
|
+
# SAC (bytes 30-31) only present with wireless air integration. The
|
|
76
|
+
# XML is produced by the same parser, so it should match exactly;
|
|
77
|
+
# inert on dives without AI (both sides None).
|
|
78
|
+
("sac", "sac", 1.0),
|
|
74
79
|
]
|
|
75
80
|
for i, (db_row, x) in enumerate(zip(samples, xs)):
|
|
76
81
|
for xml_field, col, scale in pairs:
|
|
@@ -25,8 +25,10 @@ def _sample(depth_dm: int, **kw) -> bytearray:
|
|
|
25
25
|
rec[8] = kw.get("o2", 21)
|
|
26
26
|
rec[9] = kw.get("he", 0)
|
|
27
27
|
rec[10] = kw.get("ndl_min", 99)
|
|
28
|
+
rec[11] = kw.get("battery_pct", 100)
|
|
28
29
|
rec[12] = kw.get("flags", pnf.FLAG_OC)
|
|
29
30
|
rec[14] = kw.get("temp", 27) & 0xFF
|
|
31
|
+
rec[17] = kw.get("battery_msb", 0)
|
|
30
32
|
rec[18] = kw.get("battery_raw", 152)
|
|
31
33
|
struct.pack_into(">H", rec, 20, 0xFFFF) # AI off
|
|
32
34
|
rec[22] = 0xFF # gas time not paired
|
|
@@ -35,10 +37,11 @@ def _sample(depth_dm: int, **kw) -> bytearray:
|
|
|
35
37
|
rec[25] = kw.get("gf99", 0xFF)
|
|
36
38
|
struct.pack_into(">H", rec, 26, kw.get("at_plus_five", 0))
|
|
37
39
|
struct.pack_into(">H", rec, 28, 0xFFFF) # AI off
|
|
40
|
+
struct.pack_into(">H", rec, 30, kw.get("sac_raw", 0xFFFF)) # SAC n/a
|
|
38
41
|
return rec
|
|
39
42
|
|
|
40
43
|
|
|
41
|
-
def build_blob(samples=None, interval_ms=10000, extra_records=None) -> bytes:
|
|
44
|
+
def build_blob(samples=None, interval_ms=10000, extra_records=None, deco_model=0) -> bytes:
|
|
42
45
|
records = []
|
|
43
46
|
|
|
44
47
|
opening0 = _record(0x10)
|
|
@@ -53,7 +56,7 @@ def build_blob(samples=None, interval_ms=10000, extra_records=None) -> bytes:
|
|
|
53
56
|
records.append(opening1)
|
|
54
57
|
|
|
55
58
|
opening2 = _record(0x12)
|
|
56
|
-
opening2[18] =
|
|
59
|
+
opening2[18] = deco_model # 0=GF, 1=VPM-B, 2=VPM-B/GFS, 3=DCIEM
|
|
57
60
|
records.append(opening2)
|
|
58
61
|
|
|
59
62
|
opening3 = _record(0x13)
|
|
@@ -123,6 +126,43 @@ def test_samples():
|
|
|
123
126
|
assert s1.depth_m == 52.8
|
|
124
127
|
assert (s1.stop_depth_m, s1.tts_min, s1.in_deco) == (9.0, 14.0, 1)
|
|
125
128
|
assert (s1.ceiling_m, s1.gf99, s1.cns_pct) == (9.0, 30.0, 4.0)
|
|
129
|
+
assert s0.battery_pct == 100
|
|
130
|
+
assert s0.sac is None # 0xFFFF sentinel
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_battery_voltage_is_16bit():
|
|
134
|
+
# A higher-voltage computer (e.g. Teric rechargeable ~4.2 V) overflows a
|
|
135
|
+
# single byte, so the decoder must read bytes 17-18 together.
|
|
136
|
+
dive = pnf.decode(build_blob(samples=[_sample(24, battery_msb=1, battery_raw=164)]))
|
|
137
|
+
assert dive.samples[0].battery_v == round((1 << 8 | 164) / 100.0, 2) # 4.20 V
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_sac_when_present():
|
|
141
|
+
dive = pnf.decode(build_blob(samples=[_sample(24, sac_raw=1234)]))
|
|
142
|
+
assert dive.samples[0].sac == 12.34
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_status_byte_bits_decoded():
|
|
146
|
+
flags = 0x01 | 0x20 | (2 << 6) # gas-switch-needed + CCR mode + solenoid count 2
|
|
147
|
+
dive = pnf.decode(build_blob(samples=[_sample(24, flags=flags)]))
|
|
148
|
+
s = dive.samples[0]
|
|
149
|
+
assert s.gas_switch_needed is True
|
|
150
|
+
assert s.ccr_mode == 1
|
|
151
|
+
assert s.solenoid_fired_count == 2
|
|
152
|
+
assert s.circuit_mode == 0
|
|
153
|
+
assert s.setpoint_high is False
|
|
154
|
+
assert s.status_flags == flags
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_dciem_uses_safe_ascent_depth():
|
|
158
|
+
# Under DCIEM (deco model 3), bytes 24-25 are a safe-ascent depth, not
|
|
159
|
+
# ceiling/GF99: byte 24 whole metres minus byte 25 hundredths.
|
|
160
|
+
blob = build_blob(samples=[_sample(300, ceiling=6, gf99=50)], deco_model=3)
|
|
161
|
+
dive = pnf.decode(blob)
|
|
162
|
+
assert dive.header["deco_model"] == "dciem"
|
|
163
|
+
s = dive.samples[0]
|
|
164
|
+
assert s.ceiling_m is None and s.gf99 is None
|
|
165
|
+
assert s.safe_ascent_depth_m == 6 - 0.50 # 5.5 m
|
|
126
166
|
|
|
127
167
|
|
|
128
168
|
def test_unknown_records_preserved():
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(sqlite3 \"file:data/dives.db?mode=ro\" \"SELECT source, count(*) FROM source_dives GROUP BY source;\" \"SELECT json_extract(header_json,'$.pnf.computer_model'), json_extract(header_json,'$.pnf.computer_firmware'), json_extract(header_json,'$.dive_details.Product'), count(*) FROM source_dives WHERE source='shearwater' GROUP BY 1,2,3;\")",
|
|
5
|
-
"Bash(sqlite3 \"file:data/dives.db?mode=ro\" \"SELECT json_extract(header_json,'$.file_id_mesgs[0].product_name'), json_extract(header_json,'$.file_id_mesgs[0].manufacturer'), json_extract(header_json,'$.file_id_mesgs[0].product'), count(*) FROM source_dives WHERE source='garmin' GROUP BY 1,2,3 LIMIT 10;\")",
|
|
6
|
-
"Bash(sqlite3 \"file:$(pwd)/data/dives.db?mode=ro&immutable=1\" \"SELECT source, count(*) FROM source_dives GROUP BY source;\")",
|
|
7
|
-
"Bash(DB=\"file:$(pwd)/data/dives.db?mode=ro&immutable=1\")",
|
|
8
|
-
"Bash(sqlite3 \"$DB\" \"SELECT json_extract(header_json,''$.pnf.computer_model''), json_extract(header_json,''$.pnf.computer_firmware''), json_extract(header_json,''$.dive_details.Product''), count(*) FROM source_dives WHERE source=''shearwater'' GROUP BY 1,2,3;\")",
|
|
9
|
-
"Bash(sqlite3 \"$DB\" \"SELECT json_extract(header_json,''$.file_id_mesgs[0].product_name''), json_extract(header_json,''$.device_info_mesgs[0].product_name''), json_extract(header_json,''$.device_info_mesgs[0].software_version''), count(*) FROM source_dives WHERE source=''garmin'' GROUP BY 1,2,3 LIMIT 10;\")",
|
|
10
|
-
"Bash(DB=\"file:$(pwd)/data/dives.db?mode=ro&immutable=1\")",
|
|
11
|
-
"Bash(sqlite3 \"$DB\" \"SELECT json_extract(header_json,''$.file_id_mesgs[0]'') FROM source_dives WHERE source=''garmin'' LIMIT 1;\")",
|
|
12
|
-
"Bash(sqlite3 \"$DB\" \"SELECT json_extract(header_json,''$.device_info_mesgs[0]'') FROM source_dives WHERE source=''garmin'' LIMIT 1;\")",
|
|
13
|
-
"Bash(sqlite3 \"$DB\" \"SELECT DISTINCT json_extract(header_json,''$.file_id_mesgs[0].garmin_product'') FROM source_dives WHERE source=''garmin'';\")",
|
|
14
|
-
"Bash(TMP=/var/folders/xx/8wmmss0x7lb3v3mrhc1rzzc00000gn/T/tmp.yC0jxrHEY0)",
|
|
15
|
-
"Bash(python -c \"import matplotlib; print(matplotlib.__version__)\")",
|
|
16
|
-
"Bash(python -m bottomtime.cli --db \"$TMP/dives.db\" plot 318 -o \"$TMP/d318.png\")",
|
|
17
|
-
"Bash(python -c \"\nfrom bottomtime.api import load_dive\nfrom bottomtime.plot import aligned_series\nd = load_dive(''$TMP/dives.db'', 318)\nfor s in aligned_series(d):\n print(s[''source''], s[''label''], ''x0=%.0f'' % s[''x''][0], ''xN=%.0f'' % s[''x''][-1])\n\")",
|
|
18
|
-
"Bash(python -m pytest tests/ -q 2 >& 1)",
|
|
19
|
-
"Bash(python -c \"import sys; print(sys.prefix != sys.base_prefix, sys.prefix)\")",
|
|
20
|
-
"Bash(rg -n \"—|–\" --no-heading /Users/snehan/Downloads/bottomtime/README.md /Users/snehan/Downloads/bottomtime/src /Users/snehan/Downloads/bottomtime/tests /Users/snehan/Downloads/bottomtime/pyproject.toml)",
|
|
21
|
-
"Bash(rg -ni \"leverage|seamless|robust|delve|deep dive|journey|landscape|ecosystem|empower|unlock|supercharge|elevate|streamline|game-chang|cutting-edge|state-of-the-art|best-in-class|thrilled|excited|passionate|humbled|honored|compelling|actionable|utilize|in order to|testament|underscores|boasts|not just|isn''t about|not about\" --no-heading /Users/snehan/Downloads/bottomtime/README.md /Users/snehan/Downloads/bottomtime/src /Users/snehan/Downloads/bottomtime/tests)",
|
|
22
|
-
"Bash(ls -R /Users/snehan/Downloads/snehankekre.github.io/.claude 2 > /dev/null)",
|
|
23
|
-
"Bash(ls /Users/snehan/.claude/projects/-Users-snehan-Downloads-snehankekre-github-io/memory/ 2>/dev/null)",
|
|
24
|
-
"Bash(rg -n \"—|–\" /Users/snehan/Downloads/bottomtime/README.md /Users/snehan/Downloads/bottomtime/src /Users/snehan/Downloads/bottomtime/tests /Users/snehan/Downloads/bottomtime/pyproject.toml)",
|
|
25
|
-
"Bash(echo \"clean: $?\")",
|
|
26
|
-
"Bash(python -m pytest tests/ -q)"
|
|
27
|
-
],
|
|
28
|
-
"deny": []
|
|
29
|
-
}
|
|
30
|
-
}
|
|
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
|