From 6e8ab5d3001369f4ff20948eedef600dacf6763f Mon Sep 17 00:00:00 2001 From: riasramadan <143983266+riasramadan@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:17:38 +0800 Subject: [PATCH] add DJI drone flight log parsers (TXT and DAT) for ALEAPP artifacts --- scripts/artifacts/DroneDAT.py | 190 ++++++++++++++++++ scripts/artifacts/DroneTXT.py | 359 ++++++++++++++++++++++++++++++++++ 2 files changed, 549 insertions(+) create mode 100644 scripts/artifacts/DroneDAT.py create mode 100644 scripts/artifacts/DroneTXT.py diff --git a/scripts/artifacts/DroneDAT.py b/scripts/artifacts/DroneDAT.py new file mode 100644 index 00000000..9fa61820 --- /dev/null +++ b/scripts/artifacts/DroneDAT.py @@ -0,0 +1,190 @@ +__artifacts_v2__ = { + "DroneFlightDATFiles": { + "name": "DroneFlightDATFiles", + "description": "Extracts cool data from database files", + "author": "@username", # Replace with the actual author's username or name + "version": "0.1", # Version number + "date": "2022-10-25", # Date of the latest version + "requirements": "none", + "category": "A2", + "notes": "", + "paths": ("**/*.DAT"), + "function": "DroneFlightDATFiles" + } +} + +import glob +import struct +import zlib,struct,os +import traceback +from datetime import datetime +import csv +from scripts.artifact_report import ArtifactHtmlReport +import scripts.ilapfuncs + +acconfig = {5: ("M100", 8.5E7), 6:("P3I1", 600.0), 9:("P4", 4500000.0), + 14: ("M600", 4500000.0), 16:("MavicPro", 4500000.0), 17:("I2", 4500000.0), + 18: ("P4P", 4500000.0), 20:("S900", 4500000.0), 21:("SPARK", 4500000.0), + 23: ("M600", 4500000.0), 24:("MavicAir", 3850000.0), 25:("M200", 4500000.0), + 27: ("P4A", 4500000.0), 28: ("Matrice", 4500000.0), 36:("P4PV2", 4500000.0), + 39:("Tello", 4500000.0), 40:("P4RTK", 4500000.0), 41:("Mavic2", 3847700.0), + 51:("MavicEnterprise", 3847700), 53:("MavicMini", 8014713.88506), 58:("MavicAir2", 3830000.0), + 85:("MavicAir2", 3830000.0), 63:("MavicMini2", 8014713.88506) } + +crc_table = [0x0000,0x1189,0x2312,0x329B,0x4624,0x57AD,0x6536,0x74BF,0x8C48,0x9DC1,0xAF5A,0xBED3,0xCA6C,0xDBE5,0xE97E,0xF8F7, + 0x1081,0x0108,0x3393,0x221A,0x56A5,0x472C,0x75B7,0x643E,0x9CC9,0x8D40,0xBFDB,0xAE52,0xDAED,0xCB64,0xF9FF,0xE876, + 0x2102,0x308B,0x0210,0x1399,0x6726,0x76AF,0x4434,0x55BD,0xAD4A,0xBCC3,0x8E58,0x9FD1,0xEB6E,0xFAE7,0xC87C,0xD9F5, + 0x3183,0x200A,0x1291,0x0318,0x77A7,0x662E,0x54B5,0x453C,0xBDCB,0xAC42,0x9ED9,0x8F50,0xFBEF,0xEA66,0xD8FD,0xC974, + 0x4204,0x538D,0x6116,0x709F,0x0420,0x15A9,0x2732,0x36BB,0xCE4C,0xDFC5,0xED5E,0xFCD7,0x8868,0x99E1,0xAB7A,0xBAF3, + 0x5285,0x430C,0x7197,0x601E,0x14A1,0x0528,0x37B3,0x263A,0xDECD,0xCF44,0xFDDF,0xEC56,0x98E9,0x8960,0xBBFB,0xAA72, + 0x6306,0x728F,0x4014,0x519D,0x2522,0x34AB,0x0630,0x17B9,0xEF4E,0xFEC7,0xCC5C,0xDDD5,0xA96A,0xB8E3,0x8A78,0x9BF1, + 0x7387,0x620E,0x5095,0x411C,0x35A3,0x242A,0x16B1,0x0738,0xFFCF,0xEE46,0xDCDD,0xCD54,0xB9EB,0xA862,0x9AF9,0x8B70, + 0x8408,0x9581,0xA71A,0xB693,0xC22C,0xD3A5,0xE13E,0xF0B7,0x0840,0x19C9,0x2B52,0x3ADB,0x4E64,0x5FED,0x6D76,0x7CFF, + 0x9489,0x8500,0xB79B,0xA612,0xD2AD,0xC324,0xF1BF,0xE036,0x18C1,0x0948,0x3BD3,0x2A5A,0x5EE5,0x4F6C,0x7DF7,0x6C7E, + 0xA50A,0xB483,0x8618,0x9791,0xE32E,0xF2A7,0xC03C,0xD1B5,0x2942,0x38CB,0x0A50,0x1BD9,0x6F66,0x7EEF,0x4C74,0x5DFD, + 0xB58B,0xA402,0x9699,0x8710,0xF3AF,0xE226,0xD0BD,0xC134,0x39C3,0x284A,0x1AD1,0x0B58,0x7FE7,0x6E6E,0x5CF5,0x4D7C, + 0xC60C,0xD785,0xE51E,0xF497,0x8028,0x91A1,0xA33A,0xB2B3,0x4A44,0x5BCD,0x6956,0x78DF,0x0C60,0x1DE9,0x2F72,0x3EFB, + 0xD68D,0xC704,0xF59F,0xE416,0x90A9,0x8120,0xB3BB,0xA232,0x5AC5,0x4B4C,0x79D7,0x685E,0x1CE1,0x0D68,0x3FF3,0x2E7A, + 0xE70E,0xF687,0xC41C,0xD595,0xA12A,0xB0A3,0x8238,0x93B1,0x6B46,0x7ACF,0x4854,0x59DD,0x2D62,0x3CEB,0x0E70,0x1FF9, + 0xF78F,0xE606,0xD49D,0xC514,0xB1AB,0xA022,0x92B9,0x8330,0x7BC7,0x6A4E,0x58D5,0x495C,0x3DE3,0x2C6A,0x1EF1,0x0F78] +def check_sum(data): + v = 13970 + for i in data: + v = (v >> 8) ^ crc_table[(i ^ v) & 0xFF] + return v + +class DatRecord: + def __init__(self): + self.start = 0 + self.len=0 + self.type = 0 + self.tickt_no = 0 + self.actual_ticket_no =0 + self.payload = b"" + #self.status = Record_Status_OK + def __len__(self): + return self.len + def __repr__(self): + return f"start:{self.start} len:{self.len} type:{self.type} ticket_no:{self.ticket_no} status:{self.status}" + +class DATFile: + def __init__(self, data): + self.data = data + if self.data[16:21] == b"BUILD": + if self.data[242:252] == b"DJI_LOG_V3": + self.record_start_pos = 256 + else: + self.record_start_pos = 128 + def find_next55(self, start): + try: + return self.data.index(0x55, start) + except: + return -1 + + def parse_records(self): + record_count = 0 + record_list = [] + cur_pos = self.record_start_pos + while True: + if cur_pos >= len(self.data): + break + try: + if self.data[cur_pos] != 0x55: + cur_pos += 1 + raise Exception(f"Corrupted data at pos:{cur_pos-1}") + record_len = self.data[cur_pos+1] + if record_len < 10 or cur_pos+record_len>len(self.data): + cur_pos += 2 + raise Exception(f"Corrupted record length at pos:{cur_pos-1}") + crc = check_sum(self.data[cur_pos:cur_pos+record_len-2]) + if crc&0xFF != self.data[cur_pos+record_len-2] or crc>>8 != self.data[cur_pos+record_len-1]: + cur_pos += record_len + raise Exception(f"crc error at pos:{cur_pos-2}") + record = DatRecord() + record.start = cur_pos + record.len = record_len + record.ticket_no = struct.unpack(" len(self._data): + raise OutOfDataException() + self._cur += size + return self._data[self._cur-size:self._cur] + def skip(self, size): + if self._cur + size > len(self._data): + raise OutOfDataException() + self._cur += size + def read_string(self, size): + return self.read_data(size).rstrip(b"\x00").decode("gbk", errors="ignore") + def read_uint8(self): + return struct.unpack("> 8) + return crc + +class RecordType(Enum): + OSD = 0x01 + HOME = 0x02 + GIMBAL = 0x03 + RC = 0x04 + CUSTOM = 0x05 + DEFORM = 0x06 + CENTER_BATTERY = 0x07 + SMART_BATTERY = 0x08 + APP_TIP = 0x09 + APP_WARN = 0x0A + RC_GPS = 0x0B + RC_DEBUG = 0x0C + RECOVER = 0x0D + APP_GPS = 0x0E + FIRMWARE = 0x0F + OFDM_DEBUG = 0x10 + VISION_GROUP = 0x11 + VISION_WARN = 0x12 + MC_PARAM = 0x13 + APP_OPERATION = 0x14 + APP_SER_WARN = 0x18 + COMPONENT = 0x28 + JPEG = 0x39 + OTHER = 0xFE + +def GetScrambleBytes(recordType, keyByte): + dataForBuffer = (0x123456789ABCDEF0 * keyByte)&0xFFFFFFFFFFFFFFFF + bufferToCRC = struct.pack("= 18: + hSpeed,distance,updateTime = struct.unpack("= 24: + longitude,latitude,height,xSpeed,ySpeed,zSpeed = struct.unpack("= 0x0C: + self.recordsAreaStart = self.headerSize + self.detailsAreaSize + self.detailsAreaStart = self.headerSize + self.detailsAreaEnd = self.detailsAreaStart + self.detailsAreaSize + else: + self.recordsAreaStart = self.headerSize + self.detailsAreaStart = self.recordsAreaEnd + self.detailsAreaEnd = self.detailsAreaStart + self.detailsAreaSize + + if self.recordsAreaEnd > len(self.data) or self.detailsAreaEnd > len(self.data): + raise ValueError(f"File header indicates size larger than actual file size: {path}") + + def ParseRecord(self): + data = self.data[self.recordsAreaStart:self.recordsAreaEnd] + curIndex = 0 + location_records = [] + last_gps = None + while curIndex < len(data) - 2: + try: + recordType = data[curIndex] + recordLength = data[curIndex+1] + + if curIndex + 2 + recordLength + 1 > len(data): + break + + curIndex += 2 + + if data[curIndex+recordLength]!=0xFF: + if curIndex+recordLength+1 < len(data) and data[curIndex+recordLength+1] == 0xFF: + recordLength += 1 + else: + curIndex += (recordLength+1) + continue + + recordData = b"" + if self.isScrambled: + scrambleBytes = GetScrambleBytes(recordType, data[curIndex]) # Proses dekripsi scramble bytes pada droneTXTfiles.py + rawRecordData = data[curIndex+1:curIndex+recordLength] + unscrambledRecord = [(i^scrambleBytes[index%8]) for index, i in enumerate(rawRecordData)] + recordData = bytes(unscrambledRecord) + else: + recordData = data[curIndex:curIndex+recordLength] + + if recordType == RecordType.CUSTOM.value: + custom_data = ParseCustom(recordData) + if custom_data and last_gps: + update_time, speed, distance = custom_data + location_records.append([update_time, last_gps[0], last_gps[1], last_gps[2], distance]) + elif recordType == RecordType.OSD.value: + osd_data = ParseOSD(recordData) + if osd_data: + last_gps = osd_data + + curIndex += (recordLength+1) + except Exception: + break + return location_records + + def ParseDetail(self): + data = self.data[self.detailsAreaStart:self.detailsAreaEnd] + version = self.fileVersionNumber + detail = {} + eater = Eater(data) + try: + detail["cityPart"] = eater.read_string(20) + detail["street"] = eater.read_string(20) + detail["city"] = eater.read_string(20) + detail["area"] = eater.read_string(20) + detail["isFavorite"] = eater.read_uint8() + detail["isNew"] = eater.read_uint8() + detail["needUpload"] = eater.read_uint8() + detail["recordLineCount"] = eater.read_uint32() + eater.skip(4) + detail["timestamp"] = eater.read_uint64() + detail["longitude"] = eater.read_double() + detail["latitude"] = eater.read_double() + detail["totalDistance"] = eater.read_float() + detail["totalTime"] = eater.read_uint32() + detail["maxHeight"] = eater.read_float() + detail["maxHorizontalSpeed"] = eater.read_float() + detail["maxVerticalSpeed"] = eater.read_float() + detail["photoNum"] = eater.read_uint32() + detail["videoTime"] = eater.read_uint32() + if version < 0x06: + eater.skip(124) + detail["aircraftSn"] = eater.read_string(10) + eater.skip(1) + detail["aircraftName"] = eater.read_string(25) + eater.skip(7) + detail["activeTimestamp"] = eater.read_uint64() + detail["cameraSn"] = eater.read_string(10) + detail["rcSn"] = eater.read_string(10) + detail["batterySn"] = eater.read_string(10) + else: + eater.skip(137) + detail["aircraftName"] = eater.read_string(32) + detail["aircraftSn"] = eater.read_string(16) + detail["cameraSn"] = eater.read_string(16) + detail["rcSn"] = eater.read_string(16) + detail["batterySn"] = eater.read_string(16) + detail["appType.RAW"] = eater.read_uint8() + detail["appVersion"] = struct.unpack(">I", b"\x00" + eater.read_data(3))[0] + except OutOfDataException: + pass + return detail + +DetailNameMap = {"aircraftName":"飞行器名称", "aircraftSn":"飞行器序列号", "city":"城市", + "longitude":"经度", "latitude":"纬度", "totalDistance":"总飞行距离", + "totalTime":"飞行总时长", "maxHeight":"最大高度(m)", "photoNum":"照片数量", + "vedioTime":"视频时长", "timestamp":"时间"} \ No newline at end of file