From c42322e69c6443861318bc4dbbd6954e6c4545d0 Mon Sep 17 00:00:00 2001 From: Speleobrad Date: Mon, 29 Sep 2025 17:12:35 +0100 Subject: [PATCH 1/7] Add DMP file format v6 support with Lidar data processing This commit implements comprehensive support for the new v6 DMP file format used by dry caving devices with Lidar capabilities. The changes include enhanced data models, processing services, and coordinate calculations while maintaining full backward compatibility with v5 format files. All numeric values have been converted from raw integer format to properly scaled doubles for improved accuracy. --- doc/MNemo DMP File Description.md | 240 ++++++++++++--- lib/excelexport.dart | 10 +- lib/mapsurvey.dart | 20 +- lib/models/lidar_data.dart | 31 ++ lib/models/models.dart | 3 +- lib/models/shot.dart | 89 ++++-- lib/services/data_processing_service.dart | 290 ++++++++++++++++-- lib/services/lidar_coordinate_calculator.dart | 92 ++++++ lib/services/survey_quality_service.dart | 10 +- lib/shotexport.dart | 8 +- 10 files changed, 662 insertions(+), 131 deletions(-) create mode 100644 lib/models/lidar_data.dart create mode 100644 lib/services/lidar_coordinate_calculator.dart diff --git a/doc/MNemo DMP File Description.md b/doc/MNemo DMP File Description.md index 70e7501..605f303 100644 --- a/doc/MNemo DMP File Description.md +++ b/doc/MNemo DMP File Description.md @@ -1,108 +1,252 @@ -# Mnemo v2 DMP File Format (File Version 5): # +# Mnemo v2 DMP File Format # -**[No File Header]** +## File Version 5 (Standard Format) +**[No File Header]** **[Per Section Header]** _(13 Bytes)_ Fileversion; ( 5 for firmware >2.6.0) - + 68; - + 89; - + 101; - + Year; - + Month; - + Day ; - + Hour ; - + Minute ; - + NameChar1; - + NameChar2; - + NameChar3; - + Direction ; ( 0=IN,1=OUT) **[Per Shot]** _(35 Bytes)_ 57; - + + 67; + + 77; + + Typeshot ; (0=CSA,1=CSB,2=STD,3=EOL) + + HeadingIN LSB; + + HeadingIN MSB; + + HeadingOUT LSB; + + HeadingOUT MSB; + + Length LSB; + + Length MSB; + + DepthIN LSB; + + DepthIN MSB; + + DepthOUT LSB; + + DepthOUT MSB; + + PitchIN LSB; + + PitchIN MSB; + + PitchOUT LSB; + + PitchOUT MSB; + + Left LSB; + + Left MSB; + + Right LSB; + + Right MSB; + + Up LSB; + + Up MSB; + + Down LSB; + + Down MSB; + + Temperature LSB; + + Temperature MSB; + + Hour; + + Minute; + + Second; + + MarkerIdx; + + 95; + + 25; + + 35; + +**[Section Termination]** _(35 Bytes)_ + + 57;67;77;3; [28 times 0;]95;25;35; + +--- + +## File Version 6 (Dry Caving Device with Lidar Support) + +**IMPORTANT**: File Version 6 is identical to Version 5 for all common fields, with only optional Lidar data added. + +**[No File Header]** + +**[Per Section Header]** _(13 Bytes - Same as V5)_ + + Fileversion; ( 6 for dry caving devices with Lidar support) + + 68; + + 89; + + 101; + + Year; + + Month; + + Day ; + + Hour ; + + Minute ; + + NameChar1; + + NameChar2; + + NameChar3; + + Direction ; ( 0=IN,1=OUT) + +**[Per Shot]** _(35 Bytes - Same as V5 + Optional Lidar Data)_ + + 57; + 67; - + 77; - + Typeshot ; (0=CSA,1=CSB,2=STD,3=EOL) - + HeadingIN LSB; - + HeadingIN MSB; - + HeadingOUT LSB; - + HeadingOUT MSB; - + Length LSB; - + Length MSB; - + DepthIN LSB; - + DepthIN MSB; - + DepthOUT LSB; - + DepthOUT MSB; - + PitchIN LSB; - + PitchIN MSB; - + PitchOUT LSB; - + PitchOUT MSB; - + Left LSB; - + Left MSB; - + Right LSB; - + Right MSB; - + Up LSB; - + Up MSB; - + Down LSB; - + Down MSB; - + Temperature LSB; - + Temperature MSB; - + Hour; - + Minute; - + Second; - + MarkerIdx; - + 95; - + 25; - + 35; +**[Optional Lidar Data]** _(Variable Length)_ + + 32; # VOLSTART_VALA + + 33; # VOLSTART_VALB + + 34; # VOLSTART_VALC + + DataLength LSB; + DataLength MSB; + + # Repeat for DataLength/6 triplet entries: + YAW LSB; # (uint16_t) 1/100th of degree to magnetic north (0 = north) + YAW MSB; + + PITCH LSB; # (int16_t) 1/100th of degree + PITCH MSB; + + DISTANCE LSB; # (uint16_t) cm + DISTANCE MSB; + **[Section Termination]** _(35 Bytes)_ 57;67;77;3; [28 times 0;]95;25;35; + +## Format Notes + +- **File Version 5**: Standard format for wet caving devices +- **File Version 6**: Enhanced format for dry caving devices with optional Lidar data +- **Byte Order**: Both V5 and V6 use little-endian (LSB first) for all 16-bit values +- **Compatibility**: V6 is fully backward compatible with V5 for all common fields +- **Lidar Data**: Only present in V6 format when device captures 3D point cloud data. +- **Magic Bytes**: Used for data validation and format detection +- **Data Length**: Specifies the total bytes of Lidar data (must be divisible by 6) diff --git a/lib/excelexport.dart b/lib/excelexport.dart index 52cb5b4..4eabf66 100644 --- a/lib/excelexport.dart +++ b/lib/excelexport.dart @@ -115,19 +115,19 @@ void writeRowOnSheet(Section section, Shot data, Sheet sheet, int rowNumber) { cell.cellStyle = cellStyleWithNumberFormatForNumber; cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.headingIn / 10.0); + cell.value = DoubleCellValue(data.headingIn); cell.cellStyle = cellStyleWithNumberFormatForNumber; cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.headingOut / 10.0); + cell.value = DoubleCellValue(data.headingOut); cell.cellStyle = cellStyleWithNumberFormatForNumber; cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.pitchIn / 10.0); + cell.value = DoubleCellValue(data.pitchIn); cell.cellStyle = cellStyleWithNumberFormatForNumber; cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.pitchOut / 10.0); + cell.value = DoubleCellValue(data.pitchOut); cell.cellStyle = cellStyleWithNumberFormatForNumber; cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); @@ -147,7 +147,7 @@ void writeRowOnSheet(Section section, Shot data, Sheet sheet, int rowNumber) { cell.cellStyle = cellStyleWithNumberFormatForNumber; cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = DoubleCellValue(data.temperature / 10.0); + cell.value = DoubleCellValue(data.temperature); cell.cellStyle = cellStyleWithNumberFormatForNumber; cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); diff --git a/lib/mapsurvey.dart b/lib/mapsurvey.dart index 42cb859..dd8dcfc 100644 --- a/lib/mapsurvey.dart +++ b/lib/mapsurvey.dart @@ -35,22 +35,26 @@ class MapSurvey { isProblematicShot.add(shot.hasProblematicLength()); // Calculate horizontal distance using corrected length - final depthChange = (shot.depthOut - shot.depthIn).abs(); + final depthChange = shot.depthOut - shot.depthIn; // Preserve sign for direction + final absDepthChange = depthChange.abs(); double factecr; - - if (lengthToUse <= depthChange) { + + if (lengthToUse <= absDepthChange) { // For vertical or near-vertical shots, use minimal horizontal distance factecr = 0.1; } else { - factecr = sqrt(pow(lengthToUse, 2) - pow(depthChange, 2)); + factecr = sqrt(pow(lengthToUse, 2) - pow(absDepthChange, 2)); } - + + // Get the best vertical displacement (depth sensor or calculated from angles) + final verticalDisplacement = shot.getBestVerticalDisplacement(); + points.add(Point3d( points[i].x + - factecr * sin(-shot.headingOut / 3600.0 * 2.0 * pi), + factecr * sin(-shot.headingOut * pi / 180.0), points[i].y + - factecr * cos(shot.headingOut / 3600.0 * 2.0 * pi), - max(shot.depthOut, i < section.shots.length-1 ? section.shots[i+1].depthIn : shot.depthOut))); + factecr * cos(shot.headingOut * pi / 180.0), + points[i].z + verticalDisplacement)); // Use calculated vertical displacement } } diff --git a/lib/models/lidar_data.dart b/lib/models/lidar_data.dart new file mode 100644 index 0000000..bcc9cec --- /dev/null +++ b/lib/models/lidar_data.dart @@ -0,0 +1,31 @@ +/// Represents a single Lidar measurement point +class LidarPoint { + final double yaw; // degrees (processed measurement) + final double pitch; // degrees (processed measurement) + final double distance; // m (processed measurement) + + const LidarPoint({ + required this.yaw, + required this.pitch, + required this.distance, + }); + + /// Get yaw in degrees (for compatibility) + double get yawDegrees => yaw; + + /// Get pitch in degrees (for compatibility) + double get pitchDegrees => pitch; +} + +/// Represents Lidar data collection for a shot +class LidarData { + final List points; + + const LidarData({required this.points}); + + /// Check if this contains valid Lidar data + bool get hasData => points.isNotEmpty; + + /// Get number of Lidar points + int get pointCount => points.length; +} \ No newline at end of file diff --git a/lib/models/models.dart b/lib/models/models.dart index f7b2b11..f2dc2d0 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -3,4 +3,5 @@ export 'enums.dart'; export 'shot.dart'; export 'section.dart'; export 'section_list.dart'; -export 'survey_quality.dart'; \ No newline at end of file +export 'survey_quality.dart'; +export 'lidar_data.dart'; \ No newline at end of file diff --git a/lib/models/shot.dart b/lib/models/shot.dart index ded3353..83da683 100644 --- a/lib/models/shot.dart +++ b/lib/models/shot.dart @@ -1,5 +1,6 @@ import 'dart:math'; import 'enums.dart'; +import 'lidar_data.dart'; /// Represents a single shot measurement in a cave survey class Shot { @@ -9,11 +10,11 @@ class Shot { double depthIn; double depthOut; - // Compass readings (in degrees * 10) - int headingIn; - int headingOut; - int pitchIn; - int pitchOut; + // Compass readings (in degrees) + double headingIn; + double headingOut; + double pitchIn; + double pitchOut; // LRUD measurements (Left, Right, Up, Down) double left; @@ -22,31 +23,35 @@ class Shot { double down; // Additional metadata - int temperature; + double temperature; int hr; // Hour - int min; // Minute + int min; // Minute int sec; // Second int markerIndex; + // Optional Lidar data (V6 format and later) + LidarData? lidarData; + /// Default constructor Shot({ this.typeShot = TypeShot.std, this.length = 0.0, - this.headingIn = 0, - this.headingOut = 0, - this.pitchIn = 0, - this.pitchOut = 0, + this.headingIn = 0.0, + this.headingOut = 0.0, + this.pitchIn = 0.0, + this.pitchOut = 0.0, this.depthIn = 0.0, this.depthOut = 0.0, this.left = 0.0, this.right = 0.0, this.up = 0.0, this.down = 0.0, - this.temperature = 0, + this.temperature = 0.0, this.hr = 0, this.min = 0, this.sec = 0, this.markerIndex = 0, + this.lidarData, }); /// Creates a zero-valued shot @@ -65,17 +70,17 @@ class Shot { double getDepthOut() => depthOut; void setDepthOut(double newDepthOut) => depthOut = newDepthOut; - int getHeadingIn() => headingIn; - void setHeadingIn(int newHeadingIn) => headingIn = newHeadingIn; - - int getHeadingOut() => headingOut; - void setHeadingOut(int newHeadingOut) => headingOut = newHeadingOut; - - int getPitchIn() => pitchIn; - void setPitchIn(int newPitchIn) => pitchIn = newPitchIn; - - int getPitchOut() => pitchOut; - void setPitchOut(int newPitchOut) => pitchOut = newPitchOut; + double getHeadingIn() => headingIn; + void setHeadingIn(double newHeadingIn) => headingIn = newHeadingIn; + + double getHeadingOut() => headingOut; + void setHeadingOut(double newHeadingOut) => headingOut = newHeadingOut; + + double getPitchIn() => pitchIn; + void setPitchIn(double newPitchIn) => pitchIn = newPitchIn; + + double getPitchOut() => pitchOut; + void setPitchOut(double newPitchOut) => pitchOut = newPitchOut; double getLeft() => left; void setLeft(double newLeft) => left = newLeft; @@ -89,8 +94,8 @@ class Shot { double getDown() => down; void setDown(double newDown) => down = newDown; - int getTemperature() => temperature; - void setTemperature(int newTemperature) => temperature = newTemperature; + double getTemperature() => temperature; + void setTemperature(double newTemperature) => temperature = newTemperature; int getHr() => hr; void setHr(int newHr) => hr = newHr; @@ -118,8 +123,8 @@ class Shot { if (!hasProblematicLength()) return length; final depthChange = getDepthChange(); - // Use average inclination from pitchIn and pitchOut (in degrees/10) - final avgPitch = (pitchIn + pitchOut) / 2.0 / 10.0; // Convert to degrees + // Use average inclination from pitchIn and pitchOut (already in degrees) + final avgPitch = (pitchIn + pitchOut) / 2.0; final radians = avgPitch * pi / 180.0; // Convert to radians // If inclination is near 90 degrees (vertical), use depth change as length @@ -133,4 +138,34 @@ class Shot { /// Check if this shot uses calculated length (for display purposes) bool usesCalculatedLength() => hasProblematicLength(); + + /// Calculate true vertical displacement using pitch angles + /// This is used when depth sensor is unreliable (e.g., above water scenarios) + double getCalculatedVerticalDisplacement() { + final lengthToUse = hasProblematicLength() ? getCalculatedLength() : length; + final avgPitch = (pitchIn + pitchOut) / 2.0; + final pitchRadians = avgPitch * pi / 180.0; + + return lengthToUse * sin(pitchRadians); + } + + /// Get the best available vertical displacement + /// Uses depth sensor data when reliable, otherwise calculates from angles + double getBestVerticalDisplacement() { + final depthChange = depthOut - depthIn; // Preserve sign + final absDepthChange = depthChange.abs(); + final lengthToUse = hasProblematicLength() ? getCalculatedLength() : length; + + // If depth sensor reading is very small but we have significant length and pitch, + // prefer calculated displacement from angles + if (absDepthChange < 0.1 && lengthToUse > 0.5) { + return getCalculatedVerticalDisplacement(); + } else { + // Use depth sensor reading + return depthChange; + } + } + + /// Check if this shot has Lidar data + bool hasLidarData() => lidarData?.hasData ?? false; } \ No newline at end of file diff --git a/lib/services/data_processing_service.dart b/lib/services/data_processing_service.dart index c55871e..1c0d5a2 100644 --- a/lib/services/data_processing_service.dart +++ b/lib/services/data_processing_service.dart @@ -17,42 +17,47 @@ class DataProcessingService { static const int _shotEndValueB = 25; static const int _shotEndValueC = 35; + // V6 format Lidar data constants + static const int _lidarStartValueA = 32; + static const int _lidarStartValueB = 33; + static const int _lidarStartValueC = 34; + /// Process raw binary transfer buffer into survey sections Future processTransferBuffer( - List transferBuffer, + List transferBuffer, UnitType unitType ) async { try { final sections =
[]; int cursor = 0; bool brokenSegmentDetected = false; - + final conversionFactor = unitType == UnitType.metric ? 1.0 : 3.28084; while (cursor < transferBuffer.length - 2) { final sectionResult = await _processSection( - transferBuffer, - cursor, + transferBuffer, + cursor, conversionFactor ); - + if (sectionResult.section != null) { sections.add(sectionResult.section!); } - + cursor = sectionResult.newCursor; - + if (sectionResult.brokenSegment) { brokenSegmentDetected = true; } - + if (sectionResult.shouldStop) { break; } } return DataProcessingResult.success( - sections, + sections, brokenSegmentDetected: brokenSegmentDetected ); } catch (e) { @@ -79,7 +84,7 @@ class DataProcessingService { // Find file version int fileVersion = 0; - while (fileVersion != 2 && fileVersion != 3 && fileVersion != 4 && fileVersion != 5) { + while (fileVersion != 2 && fileVersion != 3 && fileVersion != 4 && fileVersion != 5 && fileVersion != 6) { if (cursor >= transferBuffer.length) { return _SectionProcessingResult(null, cursor, false, true); } @@ -194,9 +199,6 @@ class DataProcessingService { int cursor = startCursor; final shot = Shot.zero(); - if (kDebugMode) { - debugPrint("DataProcessingService: Processing shot at cursor $cursor"); - } // Validate shot start magic bytes for version 5+ if (fileVersion >= 5) { @@ -237,31 +239,72 @@ class DataProcessingService { shot.typeShot = TypeShot.values[typeShot]; + // This is handling broken sample file I got. Might not be needed in final version. + if (kDebugMode && shot.typeShot == TypeShot.eoc) { + debugPrint("DataProcessingService: EOC shot detected, cursor at $cursor"); + if (cursor + 20 < transferBuffer.length) { + debugPrint("DataProcessingService: Next 20 bytes after type: ${transferBuffer.sublist(cursor, cursor + 20)}"); + } + } + + // For EOC shots, only skip the 9 zero bytes and don't read shot fields + if (shot.typeShot == TypeShot.eoc) { + cursor += 9; // Skip the 9 zero bytes in EOC data + if (kDebugMode) { + debugPrint("DataProcessingService: EOC shot - skipped 9 zero bytes, cursor at $cursor"); + } + + // Some V6 files incorrectly have Lidar data after EOC shots - skip it + if (fileVersion >= 6) { + if (cursor + 2 < transferBuffer.length && + transferBuffer[cursor] == _lidarStartValueA && + transferBuffer[cursor + 1] == _lidarStartValueB && + transferBuffer[cursor + 2] == _lidarStartValueC) { + if (kDebugMode) { + debugPrint("DataProcessingService: Skipping invalid Lidar data after EOC shot at position $cursor"); + } + cursor += 3; // Skip Lidar magic bytes + + if (cursor + 1 < transferBuffer.length) { + final lidarLength = _readIntFromBuffer(transferBuffer, cursor); + cursor += 2; + cursor += lidarLength; // Skip Lidar data + + if (kDebugMode) { + debugPrint("DataProcessingService: Skipped $lidarLength bytes of Lidar data, cursor now at $cursor"); + } + } + } + } + + return _ShotProcessingResult(shot, cursor, false); + } + // Read shot data (need at least 16 bytes for basic shot data) if (cursor + 15 >= transferBuffer.length) { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - shot.headingIn = _readIntFromBuffer(transferBuffer, cursor); + shot.headingIn = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; - - shot.headingOut = _readIntFromBuffer(transferBuffer, cursor); + + shot.headingOut = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; - + shot.length = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; - + shot.depthIn = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; - + shot.depthOut = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; - - shot.pitchIn = _readIntFromBuffer(transferBuffer, cursor); + + shot.pitchIn = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; - - shot.pitchOut = _readIntFromBuffer(transferBuffer, cursor); + + shot.pitchOut = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; if (kDebugMode) { @@ -272,13 +315,14 @@ class DataProcessingService { "pitch=${shot.pitchIn}/${shot.pitchOut}"); } + // Read LRUD data for version 4+ if (fileVersion >= 4) { if (cursor + 7 >= transferBuffer.length) { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - + shot.left = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; shot.right = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; @@ -287,7 +331,7 @@ class DataProcessingService { cursor += 2; shot.down = _readIntFromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; cursor += 2; - + if (kDebugMode) { debugPrint("DataProcessingService: LRUD: ${shot.left} ${shot.right} ${shot.up} ${shot.down}"); } @@ -299,13 +343,14 @@ class DataProcessingService { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - - shot.temperature = _readIntFromBuffer(transferBuffer, cursor); + + shot.temperature = _readIntFromBuffer(transferBuffer, cursor) / 10.0; cursor += 2; + shot.hr = _readByteFromBuffer(transferBuffer, cursor++); shot.min = _readByteFromBuffer(transferBuffer, cursor++); shot.sec = _readByteFromBuffer(transferBuffer, cursor++); - + if (kDebugMode) { debugPrint("DataProcessingService: Temperature=${shot.temperature}, " "Time=${shot.hr}:${shot.min}:${shot.sec}"); @@ -317,7 +362,7 @@ class DataProcessingService { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - + shot.markerIndex = _readByteFromBuffer(transferBuffer, cursor++); // Validate shot end magic bytes for version 5+ @@ -326,21 +371,32 @@ class DataProcessingService { shot.typeShot = TypeShot.eoc; return _ShotProcessingResult(shot, cursor, true); } - + final checkByteA = _readByteFromBuffer(transferBuffer, cursor++); final checkByteB = _readByteFromBuffer(transferBuffer, cursor++); final checkByteC = _readByteFromBuffer(transferBuffer, cursor++); - + if (checkByteA != _shotEndValueA || checkByteB != _shotEndValueB || checkByteC != _shotEndValueC) { if (kDebugMode) { debugPrint("DataProcessingService: Invalid shot end magic bytes"); - } + } return _ShotProcessingResult(shot, cursor - 3, true); } } + // Process optional Lidar data for V6 format (but not for EOC shots) + if (fileVersion >= 6 && shot.typeShot != TypeShot.eoc) { + final lidarResult = await _processLidarData(transferBuffer, cursor, conversionFactor); + shot.lidarData = lidarResult.lidarData; + cursor = lidarResult.newCursor; + + if (lidarResult.brokenSegment) { + return _ShotProcessingResult(shot, cursor, true); + } + } + return _ShotProcessingResult(shot, cursor, false); } @@ -352,11 +408,170 @@ class DataProcessingService { /// Read a 16-bit integer from the buffer (little endian) int _readIntFromBuffer(List buffer, int address) { if (address + 1 >= buffer.length) return 0; - + final bytes = Uint8List.fromList([buffer[address], buffer[address + 1]]); final byteData = ByteData.sublistView(bytes); return byteData.getInt16(0); } + + int _readUInt16FromBuffer(List buffer, int address) { + if (address + 1 >= buffer.length) return 0; + + // notice the order of the bytes is reversed for uint16 + final bytes = Uint8List.fromList([buffer[address + 1 ], buffer[address]]); + final byteData = ByteData.sublistView(bytes); + return byteData.getUint16(0); + } + + + /// Process optional Lidar data from V6 format + Future<_LidarProcessingResult> _processLidarData( + List transferBuffer, + int startCursor, + double conversionFactor, + ) async { + int cursor = startCursor; + + // Check if there's enough space for Lidar header (3 magic bytes + 2 length bytes) + if (cursor + 4 >= transferBuffer.length) { + if (kDebugMode) { + debugPrint("DataProcessingService: No space for Lidar header, skipping"); + } + return _LidarProcessingResult(null, cursor, false); + } + + // Check for Lidar start magic bytes + final checkByteA = _readByteFromBuffer(transferBuffer, cursor); + final checkByteB = _readByteFromBuffer(transferBuffer, cursor + 1); + final checkByteC = _readByteFromBuffer(transferBuffer, cursor + 2); + + if (checkByteA != _lidarStartValueA || + checkByteB != _lidarStartValueB || + checkByteC != _lidarStartValueC) { + if (kDebugMode) { + debugPrint("DataProcessingService: No Lidar magic bytes found, skipping"); + } + return _LidarProcessingResult(null, cursor, false); + } + + cursor += 3; // Skip magic bytes + + // Read data length + final dataLength = _readIntFromBuffer(transferBuffer, cursor); + cursor += 2; + + // Validate data length and buffer space + if (dataLength == 0 || cursor + dataLength > transferBuffer.length) { + if (kDebugMode) { + debugPrint("DataProcessingService: Invalid Lidar data length or insufficient buffer"); + } + return _LidarProcessingResult(null, cursor, true); + } + + // Each Lidar point is 6 bytes (2 bytes each for YAW, PITCH, DISTANCE) + if (dataLength % 6 != 0) { + if (kDebugMode) { + debugPrint("DataProcessingService: Invalid Lidar data length (not divisible by 6)"); + } + return _LidarProcessingResult(null, cursor + dataLength, true); + } + + final pointCount = dataLength ~/ 6; + final lidarPoints = []; + + // Track statistics for debug output + double minYaw = 0, maxYaw = 0; + double minPitch = 0, maxPitch = 0; + double minDistance = 0, maxDistance = 0; + bool firstPoint = true; + + // Read each Lidar point + for (int i = 0; i < pointCount; i++) { + + final yaw = _readUInt16FromBuffer(transferBuffer, cursor) / 100.0; + cursor += 2; + + final pitch = _readIntFromBuffer(transferBuffer, cursor) / 100.0; + cursor += 2; + + final distance = _readUInt16FromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; + cursor += 2; + + // Update statistics + if (kDebugMode) { + + if (firstPoint) { + minYaw = maxYaw = yaw; + minPitch = maxPitch = pitch; + minDistance = maxDistance = distance; + firstPoint = false; + } else { + if (yaw < minYaw) minYaw = yaw; + if (yaw > maxYaw) maxYaw = yaw; + if (pitch < minPitch) minPitch = pitch; + if (pitch > maxPitch) maxPitch = pitch; + if (distance < minDistance) minDistance = distance; + if (distance > maxDistance) maxDistance = distance; + } + } + + lidarPoints.add(LidarPoint( + yaw: yaw, + pitch: pitch, + distance: distance, + )); + } + + final lidarData = LidarData(points: lidarPoints); + + if (kDebugMode && lidarPoints.isNotEmpty) { + // Normalize angle ranges for better representation + final (normalizedMinYaw, normalizedMaxYaw) = _normalizeAngleRange(minYaw, maxYaw); + final (normalizedMinPitch, normalizedMaxPitch) = _normalizeAngleRange(minPitch, maxPitch); + + debugPrint("DataProcessingService: Lidar shot statistics - ${lidarPoints.length} points: " + "Yaw(${normalizedMinYaw.toStringAsFixed(1)}°-${normalizedMaxYaw.toStringAsFixed(1)}°), " + "Pitch(${normalizedMinPitch.toStringAsFixed(1)}°-${normalizedMaxPitch.toStringAsFixed(1)}°), " + "Distance(${minDistance}m-${maxDistance}m)"); + } + + return _LidarProcessingResult(lidarData, cursor, false); + } + + /// Normalize angle range to show the most logical representation + /// For example: -5° to +5° instead of 355° to 5° + /// Returns a tuple of (minAngle, maxAngle) in degrees, always with min <= max + (double, double) _normalizeAngleRange(double minAngleDegrees, double maxAngleDegrees) { + // Normalize angles to 0-360 range first + double normalizedMin = minAngleDegrees % 360.0; + double normalizedMax = maxAngleDegrees % 360.0; + + // Handle negative angles + if (normalizedMin < 0) normalizedMin += 360.0; + if (normalizedMax < 0) normalizedMax += 360.0; + + // Handle the case where the range crosses 0°/360° boundary + if (normalizedMax - normalizedMin > 180.0) { + // Range crosses the 0°/360° boundary + // Try both representations and choose the one with smaller absolute values + final double range1 = normalizedMax - normalizedMin; + final double range2 = (normalizedMax - 360.0) - normalizedMin; + + // If the second representation gives a smaller range, use it + if (range2.abs() < range1) { + final double adjustedMax = normalizedMax - 360.0; + // Ensure min <= max for the adjusted values + return adjustedMax <= normalizedMin ? (adjustedMax, normalizedMin) : (normalizedMin, adjustedMax); + } else { + // Ensure min <= max + return normalizedMin <= normalizedMax ? (normalizedMin, normalizedMax) : (normalizedMax, normalizedMin); + } + } else { + // Range doesn't cross the boundary, ensure min <= max + return normalizedMin <= normalizedMax ? (normalizedMin, normalizedMax) : (normalizedMax, normalizedMin); + } + } + } /// Result of data processing operation @@ -402,4 +617,13 @@ class _ShotProcessingResult { final bool brokenSegment; _ShotProcessingResult(this.shot, this.newCursor, this.brokenSegment); +} + +/// Internal result for Lidar data processing +class _LidarProcessingResult { + final LidarData? lidarData; + final int newCursor; + final bool brokenSegment; + + _LidarProcessingResult(this.lidarData, this.newCursor, this.brokenSegment); } \ No newline at end of file diff --git a/lib/services/lidar_coordinate_calculator.dart b/lib/services/lidar_coordinate_calculator.dart new file mode 100644 index 0000000..06fd6fe --- /dev/null +++ b/lib/services/lidar_coordinate_calculator.dart @@ -0,0 +1,92 @@ +import 'dart:math'; +import '../mapsurvey.dart'; +import '../models/models.dart'; + +/// Utility class for calculating Lidar global coordinates on-demand +class LidarCoordinateCalculator { + /// Calculate global coordinates for a Lidar point given shot position + static Point3d calculateGlobalCoordinates( + LidarPoint point, + double shotX, + double shotY, + double shotZ, + ) { + // Convert angles to radians + final yawRadians = point.yaw * pi / 180.0; + final pitchRadians = point.pitch * pi / 180.0; + final distanceMeters = point.distance; + + // Calculate global coordinates relative to shot position + // X: East-West (positive = East) - sin(yaw) where yaw=0 is north + // Y: North-South (positive = North) - cos(yaw) where yaw=0 is north + // Z: Vertical (positive = up) + final globalX = shotX + distanceMeters * cos(pitchRadians) * sin(yawRadians); + final globalY = shotY + distanceMeters * cos(pitchRadians) * cos(yawRadians); + final globalZ = shotZ + distanceMeters * sin(pitchRadians); + + return Point3d(globalX, globalY, globalZ); + } + + /// Calculate shot positions for a section (replicates MapSurvey.buildMap logic) + static List calculateShotPositions(Section section) { + final points = []; + final start = Point3d(0, 0, section.shots.first.depthIn); + points.add(start); + + for (int i = 0; i < section.shots.length; i++) { + final shot = section.shots[i]; + + // Use calculated length if shot is problematic, otherwise use original length + final lengthToUse = shot.hasProblematicLength() ? shot.getCalculatedLength() : shot.length; + + // Calculate horizontal distance using corrected length + final depthChange = shot.depthOut - shot.depthIn; // Preserve sign for direction + final absDepthChange = depthChange.abs(); + double horizontalDistance; + + if (lengthToUse <= absDepthChange) { + // For vertical or near-vertical shots, use minimal horizontal distance + horizontalDistance = 0.1; + } else { + horizontalDistance = sqrt(pow(lengthToUse, 2) - pow(absDepthChange, 2)); + } + + // Get the best vertical displacement (depth sensor or calculated from angles) + final verticalDisplacement = shot.getBestVerticalDisplacement(); + + points.add(Point3d( + points[i].x + horizontalDistance * sin(-shot.headingOut * pi / 180.0), + points[i].y + horizontalDistance * cos(shot.headingOut * pi / 180.0), + points[i].z + verticalDisplacement // Use calculated vertical displacement + )); + } + + return points; + } + + /// Get all Lidar points with global coordinates for a section + static List getAllLidarGlobalPoints(Section section) { + final shotPositions = calculateShotPositions(section); + final allLidarPoints = []; + + for (int shotIndex = 0; shotIndex < section.shots.length - 1; shotIndex++) { + final shot = section.shots[shotIndex]; + if (!shot.hasLidarData() || shotIndex >= shotPositions.length) continue; + + final lidarData = shot.lidarData!; + final shotPosition = shotPositions[shotIndex]; + + for (final lidarPoint in lidarData.points) { + final globalPoint = calculateGlobalCoordinates( + lidarPoint, + shotPosition.x, + shotPosition.y, + shotPosition.z, + ); + allLidarPoints.add(globalPoint); + } + } + + return allLidarPoints; + } +} \ No newline at end of file diff --git a/lib/services/survey_quality_service.dart b/lib/services/survey_quality_service.dart index b8f4c1a..0cc4362 100644 --- a/lib/services/survey_quality_service.dart +++ b/lib/services/survey_quality_service.dart @@ -51,12 +51,12 @@ class SurveyQualityService { for (int i = 0; i < section.shots.length - 1; i++) { // Exclude EOC shot final shot = section.shots[i]; - // Check heading consistency (angles are in degrees * 10) - final headingDiff = ((shot.headingOut - shot.headingIn).abs() / 10.0) % 360; + // Check heading consistency (angles are already in degrees) + final headingDiff = (shot.headingOut - shot.headingIn).abs() % 360; final normalizedHeadingDiff = headingDiff > 180 ? 360 - headingDiff : headingDiff; // Check pitch consistency - final pitchDiff = (shot.pitchOut - shot.pitchIn).abs() / 10.0; + final pitchDiff = (shot.pitchOut - shot.pitchIn).abs(); // Track maximum discrepancies maxHeadingDiff = max(maxHeadingDiff, normalizedHeadingDiff); @@ -158,8 +158,8 @@ class SurveyQualityService { // For valid shots, calculate length from inclination and depth change final depthChange = shot.getDepthChange(); if (depthChange > 0 && shot.length > 0) { - // Use average inclination from pitchIn and pitchOut (in degrees/10) - final avgPitch = (shot.pitchIn + shot.pitchOut) / 2.0 / 10.0; // Convert to degrees + // Use average inclination from pitchIn and pitchOut (already in degrees) + final avgPitch = (shot.pitchIn + shot.pitchOut) / 2.0; final radians = avgPitch * pi / 180.0; // Convert to radians double calculatedLength; diff --git a/lib/shotexport.dart b/lib/shotexport.dart index 0b64584..7f28d82 100644 --- a/lib/shotexport.dart +++ b/lib/shotexport.dart @@ -116,14 +116,14 @@ mixin ShotExport { } final double length = shot.getCalculatedLength(); - final double azimuthIn = (shot.getHeadingIn().toDouble()) / 10.0; - final double azimuthOut = (shot.getHeadingOut().toDouble()) / 10.0; + final double azimuthIn = shot.getHeadingIn(); + final double azimuthOut = shot.getHeadingOut(); final double azimuthMean = getAzimuthMean(azimuthIn, azimuthOut); final double azimuthDelta = getAzimuthDelta(azimuthIn, azimuthOut); final List azimuthComments = getAzimuthComment(azimuthMean, azimuthDelta, azimuthIn, azimuthOut); - final double pitchIn = (shot.getPitchIn().toDouble()) / 10.0; - final double pitchOut = (shot.getPitchOut().toDouble()) / 10.0; + final double pitchIn = shot.getPitchIn(); + final double pitchOut = shot.getPitchOut(); final double depthIn = shot.getDepthIn(); final double depthOut = shot.getDepthOut(); From 60d2450afc1ea0ee79ac710e5122cdf4de84ac2b Mon Sep 17 00:00:00 2001 From: Speleobrad Date: Mon, 29 Sep 2025 23:30:12 +0100 Subject: [PATCH 2/7] lidar is measured at arriving point, not starting. --- lib/services/lidar_coordinate_calculator.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/services/lidar_coordinate_calculator.dart b/lib/services/lidar_coordinate_calculator.dart index 06fd6fe..860e418 100644 --- a/lib/services/lidar_coordinate_calculator.dart +++ b/lib/services/lidar_coordinate_calculator.dart @@ -71,10 +71,11 @@ class LidarCoordinateCalculator { for (int shotIndex = 0; shotIndex < section.shots.length - 1; shotIndex++) { final shot = section.shots[shotIndex]; - if (!shot.hasLidarData() || shotIndex >= shotPositions.length) continue; + if (!shot.hasLidarData() || shotIndex + 1 >= shotPositions.length) continue; final lidarData = shot.lidarData!; - final shotPosition = shotPositions[shotIndex]; + // Lidar measurements are taken at the arriving point (end) of the shot + final shotPosition = shotPositions[shotIndex + 1]; for (final lidarPoint in lidarData.points) { final globalPoint = calculateGlobalCoordinates( From 841d1a33e2ae63fd58cca0ebd8b3332ced175d94 Mon Sep 17 00:00:00 2001 From: Speleobrad Date: Tue, 30 Sep 2025 23:52:07 +0100 Subject: [PATCH 3/7] excel export --- lib/excelexport.dart | 135 +++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 55 deletions(-) diff --git a/lib/excelexport.dart b/lib/excelexport.dart index 4eabf66..b49704f 100644 --- a/lib/excelexport.dart +++ b/lib/excelexport.dart @@ -4,24 +4,10 @@ import 'package:intl/intl.dart'; import 'package:excel/excel.dart'; import 'models/models.dart'; + void writeHeaderOnSheet(Sheet sheet, int rowNumber) { var ls = [ - "A", - "B", - "C", - "D", - "E", - "F", - "G", - "H", - "I", - "J", - "K", - "L", - "M", - "N", - "O", - "P" + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S" ]; int index = 0; @@ -70,87 +56,85 @@ void writeHeaderOnSheet(Sheet sheet, int rowNumber) { cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); cell.value = TextCellValue("Marker"); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell.value = TextCellValue("Lidar Yaw"); + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell.value = TextCellValue("Lidar Pitch"); + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell.value = TextCellValue("Lidar Distance"); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); cell.value = TextCellValue("Comments"); } -void writeRowOnSheet(Section section, Shot data, Sheet sheet, int rowNumber) { + +int writeRowOnSheet(Section section, Shot data, Sheet sheet, int startRowNumber) { var ls = [ - "A", - "B", - "C", - "D", - "E", - "F", - "G", - "H", - "I", - "J", - "K", - "L", - "M", - "N", - "O", - "P" + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S" ]; - int index = 0; final cellStyleWithNumberFormatForNumber = CellStyle( numberFormat: NumFormat.standard_0, ); - var cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + int currentRow = startRowNumber; + + // Write the main shot data row + int index = 0; + var cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = TextCellValue(data.typeShot.name); - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.getCalculatedLength()); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.depthIn); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.depthOut); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.headingIn); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.headingOut); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.pitchIn); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.pitchOut); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.left); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.right); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.up); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.down); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = DoubleCellValue(data.temperature); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = TextCellValue(DateTime( section.dateSurvey.year, section.dateSurvey.month, @@ -160,16 +144,50 @@ void writeRowOnSheet(Section section, Shot data, Sheet sheet, int rowNumber) { data.sec) .toIso8601String()); - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); cell.value = IntCellValue(data.markerIndex); cell.cellStyle = cellStyleWithNumberFormatForNumber; - cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$rowNumber")); - cell.value = TextCellValue(data.usesCalculatedLength() - ? "Length calculated from depth change and inclination (original measurement was insufficient)" + // Leave Lidar columns empty for main shot row + index += 3; // Skip Lidar Yaw, Pitch, Distance columns + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = TextCellValue(data.usesCalculatedLength() + ? "Length calculated from depth change and inclination (original measurement was insufficient)" : ""); + + currentRow++; + + // Write Lidar data rows if available + if (data.hasLidarData() && data.lidarData != null) { + for (final lidarPoint in data.lidarData!.points) { + // Clear all columns for Lidar-only rows + index = 0; + + // Skip all main shot data columns (15 columns) + index += 15; + + // Write Lidar data + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(lidarPoint.yaw); + cell.cellStyle = cellStyleWithNumberFormatForNumber; + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(lidarPoint.pitch); + cell.cellStyle = cellStyleWithNumberFormatForNumber; + + cell = sheet.cell(CellIndex.indexByString("${ls[index++]}$currentRow")); + cell.value = DoubleCellValue(lidarPoint.distance); + cell.cellStyle = cellStyleWithNumberFormatForNumber; + + currentRow++; + } + } + + return currentRow; } + void writeTitleOnSheet(Sheet sheet, Section s, UnitType unitType) { var cell = sheet.cell(CellIndex.indexByString("A1")); cell.value = TextCellValue(s.getName()); @@ -212,10 +230,17 @@ void exportAsExcel(SectionList sectionList, File file, UnitType unitType) { int rownum = 5; + // Check if this section has Lidar data and add a note if so + bool hasLidarData = element.getShots().any((shot) => shot.hasLidarData()); + if (hasLidarData) { + var cell = sheet.cell(CellIndex.indexByString("A4")); + cell.value = TextCellValue("Survey includes Lidar data"); + } + writeHeaderOnSheet(sheet, rownum++); - element.getShots().forEach((data) { - writeRowOnSheet(element, data, sheet, rownum++); - }); + for (final data in element.getShots()) { + rownum = writeRowOnSheet(element, data, sheet, rownum); + } } excel.delete("Sheet1"); From aa3ac21b4ae590a9809c26ef655aefffd4ddedcd Mon Sep 17 00:00:00 2001 From: Speleobrad Date: Wed, 1 Oct 2025 00:51:19 +0100 Subject: [PATCH 4/7] therion and survex export --- lib/mapsurvey.dart | 5 +- lib/shotexport.dart | 326 +++++++++++++++++++++++++++++++++++++----- lib/survexporter.dart | 67 ++++----- lib/thexporter.dart | 68 ++++----- 4 files changed, 344 insertions(+), 122 deletions(-) diff --git a/lib/mapsurvey.dart b/lib/mapsurvey.dart index dd8dcfc..266c3bc 100644 --- a/lib/mapsurvey.dart +++ b/lib/mapsurvey.dart @@ -48,12 +48,13 @@ class MapSurvey { // Get the best vertical displacement (depth sensor or calculated from angles) final verticalDisplacement = shot.getBestVerticalDisplacement(); + final adjustedHeading = (90-(shot.headingIn + shot.headingOut)/2); points.add(Point3d( points[i].x + - factecr * sin(-shot.headingOut * pi / 180.0), + factecr * cos(adjustedHeading * pi / 180.0), points[i].y + - factecr * cos(shot.headingOut * pi / 180.0), + factecr * sin(adjustedHeading * pi / 180.0), points[i].z + verticalDisplacement)); // Use calculated vertical displacement } } diff --git a/lib/shotexport.dart b/lib/shotexport.dart index 7f28d82..5ba2034 100644 --- a/lib/shotexport.dart +++ b/lib/shotexport.dart @@ -17,6 +17,246 @@ mixin ShotExport { String get extension; + /// Check if a section contains Lidar data (indicating dry cave survey) + bool hasDryCaveSurvey(Section section) { + return section.getShots().any((shot) => shot.hasLidarData()); + } + + /// Generate instrument configuration for survey type + void writeInstrumentConfig( + StringBuffer contents, + bool isDryCave, + String prefix, // '' for Therion, '*' for Survex + ) { + if (isDryCave) { + contents.write(newLine('${prefix}instrument compass "Jedeye"')); + contents.write(newLine('${prefix}instrument clino "Jedeye"')); + contents.write(newLine('${prefix}instrument tape "Jedeye"')); + } else { + contents.write(newLine('${prefix}instrument compass "MNemo V2"')); + contents.write(newLine('${prefix}instrument depth "MNemo V2"')); + contents.write(newLine('${prefix}instrument tape "MNemo V2"')); + } + contents.write('\n'); + + contents.write(newLine('${prefix}sd compass 1.5 degrees')); + contents.write(newLine('${prefix}sd tape 0.086 metres')); + if (!isDryCave) { + contents.write(newLine('${prefix}sd depth 0.1 metres')); + } + contents.write('\n'); + + if (!isDryCave) { + contents.write(newLine('${prefix}calibrate depth 0 -1')); + contents.write('\n'); + } + } + + /// Generate units configuration for survey type + void writeUnitsConfig( + StringBuffer contents, + bool isDryCave, + UnitType unitType, + String prefix, // '' for Therion, '*' for Survex + ) { + if (isDryCave) { + // Dry cave: no depth measurements + if (unitType == UnitType.metric) { + contents.write(newLine('${prefix}units tape metres')); + } else { + contents.write(newLine('${prefix}units tape feet')); + } + contents.write(newLine('${prefix}units clino deg')); + } else { + // Underwater survey: include depth + if (unitType == UnitType.metric) { + contents.write(newLine('${prefix}units tape depth metres')); + } else { + contents.write(newLine('${prefix}units tape depth feet')); + } + } + contents.write('\n'); + } + + /// Generate data format configuration for survey type + void writeDataConfig( + StringBuffer contents, + bool isDryCave, + String prefix, // '' for Therion, '*' for Survex + String commentPrefix, // '#' for Therion, ';' for Survex + ) { + if (isDryCave) { + // Dry cave data format + final dryDataFormat = [ + '${prefix}data', + 'normal', + 'from', + 'to', + 'tape', + 'compass', + 'backcompass', + 'clino', + 'backclino', + 'ignoreall' + ].join(' '); + + final dryHeaderFormat = [ + '$commentPrefix From', + 'To', + 'Length', + 'AzIn', + '180-AzOut', + 'PitchIn', + 'PitchOut', + 'AzMean', + 'AzOut', + 'AzDelta' + ].join('\t'); + + contents.write(newLine(dryDataFormat)); + contents.write(newLine(dryHeaderFormat)); + } else { + // Underwater data format + final underwaterDataFormat = [ + '${prefix}data', + 'diving', + 'from', + 'to', + 'tape', + 'compass', + 'backcompass', + 'fromdepth', + 'todepth', + 'ignoreall' + ].join(' '); + + final underwaterHeaderFormat = [ + '$commentPrefix From', + 'To', + 'Length', + 'AzIn', + '180-AzOut', + 'DepIn', + 'DepOut', + 'AzMean', + 'AzOut', + 'AzDelta', + 'PitchIn', + 'PitchOut' + ].join('\t'); + + contents.write(newLine(underwaterDataFormat)); + contents.write(newLine(underwaterHeaderFormat)); + } + contents.write('\n'); + } + + /// Format shot data line for export + String formatShotDataLine( + ExportShot exportShot, + bool isDryCave, + int paddingWidth, + ) { + final String fromStation = exportShot.from.padLeft(paddingWidth, '0'); + final String toStation = exportShot.to.padLeft(paddingWidth, '0'); + + if (isDryCave) { + return [ + fromStation, + toStation, + exportShot.length.toStringAsFixed(2), + exportShot.azimuthIn.toStringAsFixed(1), + exportShot.azimuthOut180.toStringAsFixed(1), + exportShot.pitchIn.toStringAsFixed(1), + exportShot.pitchOut.toStringAsFixed(1), + exportShot.azimuthMean.toStringAsFixed(1), + exportShot.azimuthOut.toStringAsFixed(1), + exportShot.azimuthDelta.toStringAsFixed(1), + ].join('\t'); + } else { + return [ + fromStation, + toStation, + exportShot.length.toStringAsFixed(2), + exportShot.azimuthIn.toStringAsFixed(1), + exportShot.azimuthOut180.toStringAsFixed(1), + exportShot.depthIn.toStringAsFixed(2), + exportShot.depthOut.toStringAsFixed(2), + exportShot.azimuthMean.toStringAsFixed(1), + exportShot.azimuthOut.toStringAsFixed(1), + exportShot.azimuthDelta.toStringAsFixed(1), + exportShot.pitchIn.toStringAsFixed(1), + exportShot.pitchOut.toStringAsFixed(1), + ].join('\t'); + } + } + + /// Process LRUD and Lidar data for an export shot + /// Returns a map with 'lrud' and 'lidar' StringBuffer contents + Map processLRUDAndLidarData( + ExportShot exportShot, + bool isDryCave, + int paddingWidth, + String commentPrefix, + String Function(String) stationNameFormatter, + ) { + final Map result = { + 'lrud': StringBuffer(), + 'lidar': StringBuffer(), + }; + + if (exportShot.lrudShots.isEmpty) return result; + + final String stationName = stationNameFormatter(exportShot.to); + + // Separate regular LRUD from Lidar data + final List regularLrudShots = exportShot.lrudShots + .where((shot) => shot.direction != LRUDDirection.lidar) + .toList(); + final List lidarShots = exportShot.lrudShots + .where((shot) => shot.direction == LRUDDirection.lidar) + .toList(); + + // Process regular LRUD measurements + if (regularLrudShots.isNotEmpty) { + result['lrud']!.write(newLine('$commentPrefix LRUD for station $stationName')); + for (LRUDShot lrudShot in regularLrudShots) { + result['lrud']!.write(newLine( + '$commentPrefix ${enumToStringWithoutClassName(lrudShot.direction.toString())}')); + + final lrudDataLine = [ + stationName, + '-', + lrudShot.length.toStringAsFixed(2), + lrudShot.azimuth.toStringAsFixed(1), + lrudShot.clino.toStringAsFixed(1) + ].join('\t'); + + result['lrud']!.write(newLine(lrudDataLine)); + } + result['lrud']!.write('\n'); + } + + // Process Lidar measurements + if (lidarShots.isNotEmpty && isDryCave) { + result['lidar']!.write(newLine('$commentPrefix Lidar measurements from station $stationName')); + for (LRUDShot lidarShot in lidarShots) { + final lidarDataLine = [ + stationName, + '-', + lidarShot.length.toStringAsFixed(2), + lidarShot.azimuth.toStringAsFixed(1), + lidarShot.clino.toStringAsFixed(1) + ].join('\t'); + + result['lidar']!.write(newLine(lidarDataLine)); + } + result['lidar']!.write('\n'); + } + + return result; + } + double getAzimuthMean(double az1, double az2) { // Convert degrees to radians final double az1Rad = deg2rad(az1); @@ -139,12 +379,13 @@ mixin ShotExport { depthOut: depthOut, azimuthMean: azimuthMean, azimuthDelta: azimuthDelta, - lurdLeft: shot.getLeft(), - lurdRight: shot.getRight(), - lurdUp: shot.getUp(), - lurdDown: shot.getDown(), + lrudLeft: shot.getLeft(), + lrudRight: shot.getRight(), + lrudUp: shot.getUp(), + lrudDown: shot.getDown(), azimuthComments: azimuthComments, - isCalculatedLength: shot.usesCalculatedLength()); + isCalculatedLength: shot.usesCalculatedLength(), + lidarData: shot.lidarData); svxShots.add(svxShot); @@ -189,7 +430,7 @@ class ExportShot { double depthOut; double azimuthMean; double azimuthDelta; - late List lurdShots; + late List lrudShots; List azimuthComments; bool isCalculatedLength; @@ -205,55 +446,70 @@ class ExportShot { required this.depthOut, required this.azimuthMean, required this.azimuthDelta, - required double lurdLeft, - required double lurdRight, - required double lurdUp, - required double lurdDown, + required double lrudLeft, + required double lrudRight, + required double lrudUp, + required double lrudDown, required this.azimuthComments, required this.isCalculatedLength, + LidarData? lidarData, }) { azimuthOut180 = (azimuthOut + 180) % 360; - lurdShots = []; - if (!almostEqual(0.0, lurdLeft)) { - lurdShots.add( - LURDShot( - direction: LURDDirection.left, - length: lurdLeft, + lrudShots = []; + if (!almostEqual(0.0, lrudLeft)) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.left, + length: lrudLeft, azimuth: _addAngles(azimuthMean, -90.0), clino: 0.0, ), ); } - if (!almostEqual(0.0, lurdRight)) { - lurdShots.add( - LURDShot( - direction: LURDDirection.right, - length: lurdRight, + if (!almostEqual(0.0, lrudRight)) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.right, + length: lrudRight, azimuth: _addAngles(azimuthMean, 90.0), clino: 0.0, ), ); } - if (!almostEqual(0.0, lurdUp)) { - lurdShots.add( - LURDShot( - direction: LURDDirection.up, - length: lurdUp, + if (!almostEqual(0.0, lrudUp)) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.up, + length: lrudUp, azimuth: 0.0, clino: 90.0, ), ); } - if (!almostEqual(0.0, lurdDown)) { - lurdShots.add( - LURDShot( - direction: LURDDirection.down, - length: lurdDown, + if (!almostEqual(0.0, lrudDown)) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.down, + length: lrudDown, azimuth: 0.0, clino: -90.0, ), ); } + + // Process Lidar data as LRUD shots + if (lidarData != null && lidarData.hasData) { + for (final point in lidarData.points) { + lrudShots.add( + LRUDShot( + direction: LRUDDirection.lidar, + length: point.distance, + azimuth: point.yaw, + clino: point.pitch, + ), + ); + } + } } double _addAngles(double angle, double delta) { @@ -279,13 +535,13 @@ String enumToStringWithoutClassName(dynamic enumValue) { return enumValue.toString().split('.').last; } -class LURDShot { - LURDDirection direction; +class LRUDShot { + LRUDDirection direction; double length; double azimuth; double clino; - LURDShot({ + LRUDShot({ required this.direction, required this.length, required this.azimuth, @@ -293,4 +549,4 @@ class LURDShot { }); } -enum LURDDirection { left, right, up, down } +enum LRUDDirection { left, right, up, down, lidar } diff --git a/lib/survexporter.dart b/lib/survexporter.dart index 8480c69..5a13d2b 100644 --- a/lib/survexporter.dart +++ b/lib/survexporter.dart @@ -22,7 +22,7 @@ class SurvexExporter with ShotExport { Future getContents(Section section, ExportShots exportShots, String surveyName, UnitType unitType) async { StringBuffer contents = StringBuffer(await headerComments()); - StringBuffer lurdContents = StringBuffer(); + StringBuffer lrudContents = StringBuffer(); contents.write(newLine('*begin $surveyName')); @@ -51,33 +51,15 @@ class SurvexExporter with ShotExport { contents.write(newLine('*require 1.2.21')); contents.write('\n'); - contents.write(newLine('*instrument compass "MNemo V2"')); - contents.write(newLine('*instrument depth "MNemo V2"')); - contents.write(newLine('*instrument tape "MNemo V2"')); - contents.write('\n'); - - contents.write(newLine('*sd compass 1.5 degrees')); - contents.write(newLine('*sd depth 0.1 metres')); - contents.write(newLine('*sd tape 0.086 metres')); - contents.write('\n'); + final bool isDryCave = hasDryCaveSurvey(section); - contents.write(newLine('*calibrate depth 0 -1')); - contents.write('\n'); + // Write configuration using shared methods + writeInstrumentConfig(contents, isDryCave, '*'); + writeUnitsConfig(contents, isDryCave, unitType, '*'); + writeDataConfig(contents, isDryCave, '*', ';'); - // Unit handling - if (unitType == UnitType.metric) { - contents.write(newLine('*units tape depth metres')); - } else { - contents.write(newLine('*units tape depth feet')); - } - contents.write('\n'); - - // Main topo data - contents.write(newLine( - '*data diving from to tape compass backcompass fromdepth todepth ignoreall')); - contents.write(newLine( - '; From\tTo\tLength\tAzIn\t180-AzOut\tDepIn\tDepOut\tAzIn\tAzOut\tAzDelta\tPitchIn\tPitchOut')); - contents.write('\n'); + // Calculate padding width for station names based on total number of stations + final int paddingWidth = (exportShots.shots.length + 1).toString().length; bool firstLine = true; for (ExportShot exportShot in exportShots.shots) { @@ -89,36 +71,35 @@ class SurvexExporter with ShotExport { for (var comment in exportShot.azimuthComments) { contents.write(newLine('; $comment')); } - + if (exportShot.isCalculatedLength) { contents.write(newLine('; Length calculated from depth change and inclination (original measurement was insufficient)')); } } // Formatting the measurement line - contents.write(newLine( - '${exportShot.from}\t${exportShot.to}\t${exportShot.length.toStringAsFixed(2)}\t${exportShot.azimuthIn.toStringAsFixed(1)}\t${exportShot.azimuthOut180.toStringAsFixed(1)}\t${exportShot.depthIn.toStringAsFixed(2)}\t${exportShot.depthOut.toStringAsFixed(2)}\t${exportShot.azimuthMean.toStringAsFixed(1)}\t${exportShot.azimuthOut.toStringAsFixed(1)}\t${exportShot.azimuthDelta.toStringAsFixed(1)}\t${exportShot.pitchIn.toStringAsFixed(1)}\t${exportShot.pitchOut.toStringAsFixed(1)}')); - - if (exportShot.lurdShots.isNotEmpty) { - lurdContents.write(newLine('; LURD for station ${exportShot.to}')); - for (LURDShot lurdShot in exportShot.lurdShots) { - lurdContents.write(newLine( - '; ${enumToStringWithoutClassName(lurdShot.direction.toString())}')); - lurdContents.write(newLine( - '${exportShot.to}\t-\t${lurdShot.length.toStringAsFixed(2)}\t${lurdShot.azimuth.toStringAsFixed(1)}\t${lurdShot.clino.toStringAsFixed(1)}')); - } - lurdContents.write('\n'); - } + contents.write(newLine(formatShotDataLine(exportShot, isDryCave, paddingWidth))); + + final Map lrudData = processLRUDAndLidarData( + exportShot, + isDryCave, + paddingWidth, + ';', + (stationTo) => stationTo.padLeft(paddingWidth, '0'), + ); + + lrudContents.write(lrudData['lrud']!.toString()); + lrudContents.write(lrudData['lidar']!.toString()); firstLine = false; } contents.write('\n'); - if (lurdContents.isNotEmpty) { - contents.write(newLine('; LURD measurements')); + if (lrudContents.isNotEmpty) { + contents.write(newLine('; LRUD measurements')); contents.write(newLine('*data normal from to tape compass clino')); contents.write('\n'); - contents.write(lurdContents.toString()); + contents.write(lrudContents.toString()); } // Finalizing the survey contents diff --git a/lib/thexporter.dart b/lib/thexporter.dart index 7f2e4df..cc97227 100644 --- a/lib/thexporter.dart +++ b/lib/thexporter.dart @@ -5,11 +5,12 @@ class THExporter with ShotExport { @override String get extension => '.th'; + @override Future getContents(Section section, ExportShots exportShots, String surveyName, UnitType unitType) async { StringBuffer contents = StringBuffer(newLine('encoding UTF-8')); - StringBuffer lurdContents = StringBuffer(); + StringBuffer lrudContents = StringBuffer(); contents.write('\n'); @@ -33,35 +34,18 @@ class THExporter with ShotExport { contents.write(newLine('#explo-team ""')); contents.write('\n'); - contents.write(newLine('instrument compass "MNemo V2"')); - contents.write(newLine('instrument depth "MNemo V2"')); - contents.write(newLine('instrument tape "MNemo V2"')); - contents.write('\n'); + final bool isDryCave = hasDryCaveSurvey(section); - contents.write(newLine('sd compass 1.5 degrees')); - contents.write(newLine('sd depth 0.1 metres')); - contents.write(newLine('sd tape 0.086 metres')); - contents.write('\n'); + // Write configuration using shared methods + writeInstrumentConfig(contents, isDryCave, ''); + writeUnitsConfig(contents, isDryCave, unitType, ''); + writeDataConfig(contents, isDryCave, '', '#'); - contents.write(newLine('calibrate depth 0 -1')); - contents.write('\n'); + increasePrefix(); - // Unit handling - if (unitType == UnitType.metric) { - contents.write(newLine('units tape depth metres')); - } else { - contents.write(newLine('units tape depth feet')); - } - contents.write('\n'); + // Calculate padding width for station names based on total number of stations + final int paddingWidth = isDryCave ? (exportShots.shots.length + 1).toString().length : 0; - // Main topo data - contents.write(newLine( - 'data diving from to tape compass backcompass fromdepth todepth ignoreall')); - contents.write(newLine( - '# From\tTo\tLength\tAzIn\t180-AzOut\tDepIn\tDepOut\tAzMean\tAzOut\tAzDelta\tPitchIn\tPitchOut')); - contents.write('\n'); - - increasePrefix(); bool firstLine = true; for (ExportShot exportShot in exportShots.shots) { if (exportShot.azimuthComments.isNotEmpty || exportShot.isCalculatedLength) { @@ -79,31 +63,31 @@ class THExporter with ShotExport { } // Formatting the measurement line - contents.write(newLine( - '${exportShot.from}\t${exportShot.to}\t${exportShot.length.toStringAsFixed(2)}\t${exportShot.azimuthIn.toStringAsFixed(1)}\t${exportShot.azimuthOut180.toStringAsFixed(1)}\t${exportShot.depthIn.toStringAsFixed(2)}\t${exportShot.depthOut.toStringAsFixed(2)}\t${exportShot.azimuthMean.toStringAsFixed(1)}\t${exportShot.azimuthOut.toStringAsFixed(1)}\t${exportShot.azimuthDelta.toStringAsFixed(1)}\t${exportShot.pitchIn.toStringAsFixed(1)}\t${exportShot.pitchOut.toStringAsFixed(1)}')); - - if (exportShot.lurdShots.isNotEmpty) { - lurdContents.write(newLine('# LURD for station ${exportShot.to}')); - for (LURDShot lurdShot in exportShot.lurdShots) { - lurdContents.write(newLine( - '# ${enumToStringWithoutClassName(lurdShot.direction.toString())}')); - lurdContents.write(newLine( - '${exportShot.to}\t-\t${lurdShot.length.toStringAsFixed(2)}\t${lurdShot.azimuth.toStringAsFixed(1)}\t${lurdShot.clino.toStringAsFixed(1)}')); - } - lurdContents.write('\n'); - } + contents.write(newLine(formatShotDataLine(exportShot, isDryCave, paddingWidth))); + + final Map lrudData = processLRUDAndLidarData( + exportShot, + isDryCave, + paddingWidth, + '#', + (stationTo) => stationTo.padLeft(paddingWidth, '0'), + ); + + lrudContents.write(lrudData['lrud']!.toString()); + lrudContents.write(lrudData['lidar']!.toString()); firstLine = false; } contents.write('\n'); - if (lurdContents.isNotEmpty) { - contents.write(newLine('# LURD measurements')); + if (lrudContents.isNotEmpty) { + contents.write(newLine('# LRUD measurements')); contents.write(newLine('data normal from to tape compass clino')); contents.write('\n'); - contents.write(lurdContents.toString()); + contents.write(lrudContents.toString()); } + decreasePrefix(); decreasePrefix(); From 4ae5d3e54fbf8c56281c503a0a0cfecec6fd2dc1 Mon Sep 17 00:00:00 2001 From: Speleobrad Date: Wed, 1 Oct 2025 01:53:11 +0100 Subject: [PATCH 5/7] use MSB;LSB int for pitch. simplify angle normalization. --- lib/services/data_processing_service.dart | 54 ++++++++++++----------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/lib/services/data_processing_service.dart b/lib/services/data_processing_service.dart index 1c0d5a2..f368057 100644 --- a/lib/services/data_processing_service.dart +++ b/lib/services/data_processing_service.dart @@ -414,6 +414,15 @@ class DataProcessingService { return byteData.getInt16(0); } + /// Read a 16-bit integer from the buffer (inverted endian) + int _readInvIntFromBuffer(List buffer, int address) { + if (address + 1 >= buffer.length) return 0; + + final bytes = Uint8List.fromList([buffer[address + 1], buffer[address]]); + final byteData = ByteData.sublistView(bytes); + return byteData.getInt16(0); + } + int _readUInt16FromBuffer(List buffer, int address) { if (address + 1 >= buffer.length) return 0; @@ -491,7 +500,7 @@ class DataProcessingService { final yaw = _readUInt16FromBuffer(transferBuffer, cursor) / 100.0; cursor += 2; - final pitch = _readIntFromBuffer(transferBuffer, cursor) / 100.0; + final pitch = _readInvIntFromBuffer(transferBuffer, cursor) / 100.0; cursor += 2; final distance = _readUInt16FromBuffer(transferBuffer, cursor) * conversionFactor / 100.0; @@ -530,8 +539,8 @@ class DataProcessingService { final (normalizedMinPitch, normalizedMaxPitch) = _normalizeAngleRange(minPitch, maxPitch); debugPrint("DataProcessingService: Lidar shot statistics - ${lidarPoints.length} points: " - "Yaw(${normalizedMinYaw.toStringAsFixed(1)}°-${normalizedMaxYaw.toStringAsFixed(1)}°), " - "Pitch(${normalizedMinPitch.toStringAsFixed(1)}°-${normalizedMaxPitch.toStringAsFixed(1)}°), " + "Yaw(${normalizedMinYaw.toStringAsFixed(1)}° - ${normalizedMaxYaw.toStringAsFixed(1)}°), " + "Pitch(${normalizedMinPitch.toStringAsFixed(1)}° - ${normalizedMaxPitch.toStringAsFixed(1)}°), " "Distance(${minDistance}m-${maxDistance}m)"); } @@ -542,33 +551,28 @@ class DataProcessingService { /// For example: -5° to +5° instead of 355° to 5° /// Returns a tuple of (minAngle, maxAngle) in degrees, always with min <= max (double, double) _normalizeAngleRange(double minAngleDegrees, double maxAngleDegrees) { - // Normalize angles to 0-360 range first + // Normalize angles to 0-360 range double normalizedMin = minAngleDegrees % 360.0; double normalizedMax = maxAngleDegrees % 360.0; - - // Handle negative angles + if (normalizedMin < 0) normalizedMin += 360.0; if (normalizedMax < 0) normalizedMax += 360.0; - - // Handle the case where the range crosses 0°/360° boundary - if (normalizedMax - normalizedMin > 180.0) { - // Range crosses the 0°/360° boundary - // Try both representations and choose the one with smaller absolute values - final double range1 = normalizedMax - normalizedMin; - final double range2 = (normalizedMax - 360.0) - normalizedMin; - - // If the second representation gives a smaller range, use it - if (range2.abs() < range1) { - final double adjustedMax = normalizedMax - 360.0; - // Ensure min <= max for the adjusted values - return adjustedMax <= normalizedMin ? (adjustedMax, normalizedMin) : (normalizedMin, adjustedMax); - } else { - // Ensure min <= max - return normalizedMin <= normalizedMax ? (normalizedMin, normalizedMax) : (normalizedMax, normalizedMin); - } + + // Calculate direct span (clockwise from min to max) + final double directSpan = normalizedMax >= normalizedMin ? + normalizedMax - normalizedMin : + normalizedMin - normalizedMax + 360.0; + + // If direct span is <= 180°, use it; otherwise use boundary-crossing representation + if (directSpan <= 180.0) { + return normalizedMin <= normalizedMax ? + (normalizedMin, normalizedMax) : + (normalizedMax, normalizedMin); } else { - // Range doesn't cross the boundary, ensure min <= max - return normalizedMin <= normalizedMax ? (normalizedMin, normalizedMax) : (normalizedMax, normalizedMin); + // Use boundary-crossing representation (shorter path across 0°) + return normalizedMax >= normalizedMin ? + (normalizedMin, normalizedMax - 360.0) : + (normalizedMin - 360.0, normalizedMax); } } From 6fd01985514b488973bbcd97d81a1ff0dea76820 Mon Sep 17 00:00:00 2001 From: Speleobrad Date: Wed, 1 Oct 2025 10:09:54 +0100 Subject: [PATCH 6/7] bump kotlin. --- android/build.gradle | 2 +- android/settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index cff2187..a0c36a3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '2.0.21' + ext.kotlin_version = '2.1.0' repositories { google() mavenCentral() diff --git a/android/settings.gradle b/android/settings.gradle index 7a27933..0b96fb4 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -23,7 +23,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.3.2' apply false + id "com.android.application" version '8.6.0' apply false } include ":app" From 2764e2857253f8faef5dbce45acba89d821e4321 Mon Sep 17 00:00:00 2001 From: Speleobrad Date: Wed, 1 Oct 2025 12:06:36 +0100 Subject: [PATCH 7/7] android fix: gradle and fixed package_info_plus version --- android/settings.gradle | 1 + pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/android/settings.gradle b/android/settings.gradle index 0b96fb4..9d3994e 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -24,6 +24,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version '8.6.0' apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/pubspec.yaml b/pubspec.yaml index a953ed4..4ea740e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: intl: null loading_animation_widget: null network_info_plus: ^6.1.0 - package_info_plus: null + package_info_plus: ^8.0.0 path_provider: null shared_preferences: ^2.2.2 slugify: null