1+ import 'dart:typed_data' ;
2+
3+ import 'package:libdivecomputer_plugin/libdivecomputer_plugin.dart' as pigeon;
4+
15import 'package:submersion/features/universal_import/data/models/import_enums.dart' ;
26import 'package:submersion/features/universal_import/data/models/import_payload.dart' ;
7+ import 'package:submersion/features/universal_import/data/models/import_warning.dart' ;
38import 'package:submersion/features/universal_import/data/services/macdive_raw_types.dart' ;
49import 'package:submersion/features/universal_import/data/services/macdive_unit_converter.dart' ;
510import 'package:submersion/features/universal_import/data/services/macdive_value_mapper.dart' ;
611import 'package:submersion/features/universal_import/data/services/macdive_xml_models.dart'
712 show MacDiveUnitSystem;
813
14+ /// Signature for the [pigeon.DiveComputerHostApi.parseRawDiveData] call so
15+ /// tests can inject a fake without spawning the platform channel.
16+ typedef ParseRawDiveDataFn =
17+ Future <pigeon.ParsedDive > Function (
18+ String vendor,
19+ String product,
20+ int model,
21+ Uint8List data,
22+ );
23+
924/// Maps a [MacDiveRawLogbook] (raw SQLite rows read by [MacDiveDbReader] )
1025/// into a unified [ImportPayload] the rest of the import pipeline consumes
1126/// without knowing the source was SQLite. Key conventions mirror the M2
1227/// `MacDiveXmlParser` so the downstream `UddfEntityImporter` processes
1328/// both sources through the same code path.
1429///
15- /// Profile samples ( `ZSAMPLES` ) are NOT decoded in M3 - MacDive uses a
16- /// proprietary binary format (not bplist, not a common compression).
17- /// `profile: []` is emitted for every dive. Users who need profile
18- /// samples should use the UDDF import path (M1), which decodes MacDive's
19- /// UDDF profile correctly .
30+ /// Profile samples are decoded from the `ZRAWDATA` BLOB via
31+ /// [pigeon.DiveComputerHostApi.parseRawDiveData] (the same path used by
32+ /// [ShearwaterDiveMapper] for Shearwater Cloud imports). Dives without
33+ /// `ZRAWDATA` , with an unknown computer model, or where decoding fails emit
34+ /// ` profile: []` . Decode failures additionally emit an [ImportWarning] .
2035class MacDiveDiveMapper {
2136 const MacDiveDiveMapper ._();
2237
@@ -26,17 +41,27 @@ class MacDiveDiveMapper {
2641 /// String enum-ish values (waterType, entryType) go through
2742 /// [MacDiveValueMapper] so unrecognised inputs are dropped rather than
2843 /// mis-stored.
29- static ImportPayload toPayload (MacDiveRawLogbook logbook) {
44+ ///
45+ /// [parseRawDiveData] can be supplied by tests to skip the real FFI call.
46+ static Future <ImportPayload > toPayload (
47+ MacDiveRawLogbook logbook, {
48+ ParseRawDiveDataFn ? parseRawDiveData,
49+ }) async {
3050 final units = MacDiveUnitSystem .fromXml (logbook.unitsPreference);
3151 final converter = MacDiveUnitConverter (units);
52+ final parseFn = parseRawDiveData ?? _defaultParse;
53+ final warnings = < ImportWarning > [];
3254
3355 final siteMaps = _buildSiteMaps (logbook, converter);
3456 final buddyMaps = _buildBuddyMaps (logbook);
3557 final tagMaps = _buildTagMaps (logbook);
3658 final gearMaps = _buildGearMaps (logbook, converter);
37- final diveMaps = [
38- for (final d in logbook.dives) _buildDiveMap (d, logbook, converter),
39- ];
59+ final diveMaps = < Map <String , dynamic >> [];
60+ for (final d in logbook.dives) {
61+ diveMaps.add (
62+ await _buildDiveMap (d, logbook, converter, parseFn, warnings),
63+ );
64+ }
4065
4166 final entities = < ImportEntityType , List <Map <String , dynamic >>> {};
4267 if (diveMaps.isNotEmpty) entities[ImportEntityType .dives] = diveMaps;
@@ -47,7 +72,7 @@ class MacDiveDiveMapper {
4772
4873 return ImportPayload (
4974 entities: entities,
50- warnings: const [] ,
75+ warnings: warnings ,
5176 metadata: {
5277 'source' : 'macdive_sqlite' ,
5378 'diveCount' : logbook.dives.length,
@@ -56,6 +81,20 @@ class MacDiveDiveMapper {
5681 );
5782 }
5883
84+ // ---- default FFI implementation ----
85+
86+ static Future <pigeon.ParsedDive > _defaultParse (
87+ String vendor,
88+ String product,
89+ int model,
90+ Uint8List data,
91+ ) => pigeon.DiveComputerHostApi ().parseRawDiveData (
92+ vendor,
93+ product,
94+ model,
95+ data,
96+ );
97+
5998 // ---- site / buddy / tag / gear ----
6099
61100 static List <Map <String , dynamic >> _buildSiteMaps (
@@ -168,11 +207,13 @@ class MacDiveDiveMapper {
168207
169208 // ---- dive ----
170209
171- static Map <String , dynamic > _buildDiveMap (
210+ static Future < Map <String , dynamic > > _buildDiveMap (
172211 MacDiveRawDive d,
173212 MacDiveRawLogbook logbook,
174213 MacDiveUnitConverter c,
175- ) {
214+ ParseRawDiveDataFn parseFn,
215+ List <ImportWarning > warnings,
216+ ) async {
176217 final map = < String , dynamic > {};
177218
178219 if (d.uuid.isNotEmpty) map['sourceUuid' ] = d.uuid;
@@ -332,10 +373,99 @@ class MacDiveDiveMapper {
332373 map['tanks' ] = tanks;
333374 }
334375
335- // Profile: always empty in M3. ZSAMPLES is MacDive's proprietary
336- // binary sample format and is not decoded here.
337- map['profile' ] = const < Map <String , dynamic >> [];
376+ // Profile: decode ZRAWDATA via libdivecomputer_plugin when available.
377+ // Dives without ZRAWDATA or with an unmapped computer emit [] silently.
378+ // Decode failures emit [] and append an ImportWarning.
379+ map['profile' ] = await _decodeProfile (d, parseFn, warnings);
338380
339381 return map;
340382 }
383+
384+ // ---- profile decoding ----
385+
386+ static Future <List <Map <String , dynamic >>> _decodeProfile (
387+ MacDiveRawDive dive,
388+ ParseRawDiveDataFn parseFn,
389+ List <ImportWarning > warnings,
390+ ) async {
391+ final rawData = dive.rawDataBlob;
392+ final vendorProduct = _vendorProductFromZComputer (dive.computer);
393+ if (rawData == null || rawData.isEmpty || vendorProduct == null ) {
394+ return const [];
395+ }
396+ try {
397+ final parsed = await parseFn (
398+ vendorProduct.$1,
399+ vendorProduct.$2,
400+ 0 ,
401+ rawData,
402+ );
403+ return _projectSamples (parsed);
404+ } catch (e) {
405+ warnings.add (
406+ ImportWarning (
407+ severity: ImportWarningSeverity .warning,
408+ message: 'Profile decode failed for dive ${dive .uuid }: $e ' ,
409+ entityType: ImportEntityType .dives,
410+ ),
411+ );
412+ return const [];
413+ }
414+ }
415+
416+ /// Projects [pigeon.ParsedDive] samples into the canonical import map
417+ /// format. Mirrors [ShearwaterDiveMapper.mergeWithParsedDive] exactly for
418+ /// the sample projection block (same keys, same conditional emission).
419+ static List <Map <String , dynamic >> _projectSamples (pigeon.ParsedDive parsed) {
420+ return parsed.samples.map ((s) {
421+ final sampleMap = < String , dynamic > {
422+ 'timestamp' : s.timeSeconds,
423+ 'depth' : s.depthMeters,
424+ };
425+ if (s.temperatureCelsius != null ) {
426+ sampleMap['temperature' ] = s.temperatureCelsius;
427+ }
428+ if (s.pressureBar != null ) {
429+ sampleMap['allTankPressures' ] = < Map <String , dynamic >> [
430+ {'pressure' : s.pressureBar, 'tankIndex' : s.tankIndex ?? 0 },
431+ ];
432+ }
433+ if (s.setpoint != null ) sampleMap['setpoint' ] = s.setpoint;
434+ if (s.ppo2 != null ) sampleMap['ppO2' ] = s.ppo2;
435+ if (s.heartRate != null ) sampleMap['heartRate' ] = s.heartRate;
436+ if (s.cns != null ) sampleMap['cns' ] = s.cns;
437+ if (s.rbt != null ) sampleMap['rbt' ] = s.rbt;
438+ if (s.tts != null ) sampleMap['tts' ] = s.tts;
439+ if (s.decoType != null ) sampleMap['decoType' ] = s.decoType;
440+ if (s.decoDepth != null && s.decoType != null && s.decoType != 0 ) {
441+ sampleMap['ceiling' ] = s.decoDepth;
442+ }
443+ if (s.decoType == 0 && s.decoTime != null ) {
444+ sampleMap['ndl' ] = s.decoTime;
445+ }
446+ return sampleMap;
447+ }).toList ();
448+ }
449+
450+ /// Maps MacDive's ZCOMPUTER string to the (vendor, product) pair
451+ /// libdivecomputer expects. Returns null for computers the plugin
452+ /// does not support — caller emits `profile: []` without a warning
453+ /// (not a decode failure, just an unsupported model).
454+ static (String , String )? _vendorProductFromZComputer (String ? zComputer) {
455+ if (zComputer == null ) return null ;
456+ switch (zComputer) {
457+ case 'Shearwater Teric' :
458+ return ('Shearwater' , 'Teric' );
459+ case 'Shearwater Tern' :
460+ return ('Shearwater' , 'Tern' );
461+ case 'Shearwater Petrel' :
462+ return ('Shearwater' , 'Petrel' );
463+ case 'Shearwater Perdix' :
464+ return ('Shearwater' , 'Perdix' );
465+ case 'Shearwater Nerd' :
466+ return ('Shearwater' , 'Nerd' );
467+ default :
468+ return null ;
469+ }
470+ }
341471}
0 commit comments