├── .gitignore ├── LICENSE ├── README.md ├── assets ├── flight_logs │ ├── mavic2 │ │ ├── mavic2_0.txt │ │ ├── mavic2_1.txt │ │ ├── mavic2_10.txt │ │ ├── mavic2_11.txt │ │ ├── mavic2_12.txt │ │ ├── mavic2_13.txt │ │ ├── mavic2_14.txt │ │ ├── mavic2_15.txt │ │ ├── mavic2_16.txt │ │ ├── mavic2_17.txt │ │ ├── mavic2_2.txt │ │ ├── mavic2_3.txt │ │ ├── mavic2_4.txt │ │ ├── mavic2_5.txt │ │ ├── mavic2_6.txt │ │ ├── mavic2_7.txt │ │ ├── mavic2_8.txt │ │ └── mavic2_9.txt │ ├── phantom3 │ │ ├── phantom3_0.txt │ │ ├── phantom3_1.txt │ │ ├── phantom3_2.txt │ │ ├── phantom3_3.txt │ │ ├── phantom3_4.txt │ │ ├── phantom3_5.txt │ │ ├── phantom3_6.txt │ │ └── phantom3_7.txt │ ├── phantom4 │ │ └── phantom4_0.txt │ ├── rename_testfiles.py │ └── spark │ │ └── spark_0.txt ├── test.csv └── test.json ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── commands │ ├── Command.ts │ ├── JpegExtractCommand.ts │ ├── OutputCommand.ts │ ├── ParseRecordsCommand.ts │ ├── PrintInfoCommand.ts │ ├── ReadFileCommand.ts │ ├── Records2CsvCommand.ts │ ├── Records2JsonCommand.ts │ ├── SerializeRecordsCommand.ts │ ├── ShowTypeCommand.ts │ ├── UnscrambleCommand.ts │ └── index.ts ├── common │ ├── CliArguments.ts │ ├── ServiceManager.ts │ ├── Version.ts │ └── lazy_loading.ts ├── node-djiparsetxt.ts ├── services │ ├── BaseService.ts │ ├── BinaryParserService.ts │ ├── BinaryParserTable.ts │ ├── CacheTransformService.ts │ ├── CsvService.ts │ ├── FileInfoService.spec.ts │ ├── FileInfoService.ts │ ├── FileParsingService.ts │ ├── InterpretationTable.ts │ ├── RecordTypes.ts │ ├── ScrambleTable.ts │ └── ScrambleTableService.ts ├── shared │ └── interfaces.ts └── template │ └── kml-template.ejs ├── tests ├── ReadFileCommand.test.ts ├── ServiceManager.mock.ts └── file_version.test.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | .vscode/ 3 | node_modules 4 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christian Velez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-djiparsetxt 2 | ================ 3 | 4 | ![npm](https://img.shields.io/npm/v/node-djiparsetxt.svg) 5 | ![NPM](https://img.shields.io/npm/l/node-djiparsetxt.svg) 6 | 7 | Decrypts and parse DJI logs and outputs csv files, along other things. Based on 8 | [`djiparsetxt`](http://djilogs.live555.com/). 9 | 10 | This package requires node version 10 or older. Basically the stable release. 11 | 12 | Usage 13 | ===== 14 | 15 | ## From the terminal 16 | 17 | The main use case for is through a terminal to create json version of logs. 18 | 19 | The cli's format is: 20 | 21 | node-djiparsetxt FILE [FILE...] [OPTIONS] 22 | 23 | Type `node-djiparsetxt --help` for more info on options. 24 | 25 | 26 | Example to create a json file from a text log: 27 | 28 | node-djiparsetxt log1.txt > log1.json 29 | 30 | If you want to output csv: 31 | 32 | node-djiparsetext log1.txt --csv > log1.csv 33 | 34 | ## From a script 35 | 36 | `node-djiparsetxt` supports usage as a library to integrate it to a bigger 37 | workflow or to create batch processing of log files. 38 | 39 | Example script that prints preformatted json file from a log file: 40 | 41 | ```javascript 42 | const djiparsetxt = require('node-djiparsetxt'); 43 | const fs = require('fs'); 44 | 45 | const file_path = "path_to_log.txt"; 46 | 47 | fs.readFile(file_path, (err, data) => { 48 | if (err) throw err; 49 | console.log(JSON.stringify(djiparsetxt.parse_file(data), null, 4)); 50 | }); 51 | ``` 52 | 53 | `node-djiparsetxt` Module 54 | ========================= 55 | 56 | `parse_file(buf: Buffer, filter: (IRowObject) => boolean): IRowObject[]` 57 | 58 | Parse a given buffer and return an array of `IRowObject` instances. 59 | 60 | If a `filter` parameter is given, then only the rows that return `filter(row)` true 61 | is returned in the array. 62 | 63 | #### Parameters 64 | 65 | - *`buf`*: `Buffer`: Buffer instance of the file to parse. 66 | - *`filter`*: `(IRowObject) => boolean`: Filter function to specify what rows to include. 67 | 68 | #### Returns 69 | 70 | An array of with the rows extracted from the file. 71 | 72 | ---- 73 | 74 | `get_details(buf: Buffer): any` 75 | 76 | Get the details section of the given file. 77 | 78 | #### Parameters 79 | 80 | - *`buf`*: `Buffer`: Buffer instance of the file to parse. 81 | 82 | #### Returns 83 | 84 | An object with properties and values from the details area. 85 | 86 | ---- 87 | 88 | `get_header(buf: Buffer): IHeaderInfo` 89 | 90 | Get the header of the given file. 91 | 92 | #### Parameters 93 | 94 | - *`buf`*: `Buffer`: Buffer instance of the file to parse. 95 | 96 | #### Returns 97 | 98 | `IHeaderInfo` structure from the file. 99 | 100 | ---- 101 | 102 | `get_kml(buf: Buffer, image?: string, removeNoSignalRecords: boolean = false): Promise` 103 | 104 | Returns a kml string from a given file buffer. 105 | 106 | #### Parameters 107 | 108 | - *`buf`*: `Buffer`: Buffer instance of the file to parse. 109 | - *`image?`*: `string`: Image to use as background for the kml. 110 | - *`removeNoSignalRecords`*: `boolean`: If to remove rows where there was no GPS signal. 111 | 112 | #### Returns 113 | 114 | A `string` with the kml file. -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_0.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_0.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_1.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_10.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_10.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_11.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_11.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_12.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_12.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_13.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_13.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_14.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_14.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_15.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_15.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_16.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_16.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_17.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_17.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_2.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_3.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_3.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_4.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_4.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_5.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_5.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_6.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_6.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_7.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_7.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_8.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_8.txt -------------------------------------------------------------------------------- /assets/flight_logs/mavic2/mavic2_9.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/mavic2/mavic2_9.txt -------------------------------------------------------------------------------- /assets/flight_logs/phantom3/phantom3_0.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/phantom3/phantom3_0.txt -------------------------------------------------------------------------------- /assets/flight_logs/phantom3/phantom3_1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/phantom3/phantom3_1.txt -------------------------------------------------------------------------------- /assets/flight_logs/phantom3/phantom3_2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/phantom3/phantom3_2.txt -------------------------------------------------------------------------------- /assets/flight_logs/phantom3/phantom3_3.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/phantom3/phantom3_3.txt -------------------------------------------------------------------------------- /assets/flight_logs/phantom3/phantom3_4.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/phantom3/phantom3_4.txt -------------------------------------------------------------------------------- /assets/flight_logs/phantom3/phantom3_5.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/phantom3/phantom3_5.txt -------------------------------------------------------------------------------- /assets/flight_logs/phantom3/phantom3_6.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/phantom3/phantom3_6.txt -------------------------------------------------------------------------------- /assets/flight_logs/phantom3/phantom3_7.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/phantom3/phantom3_7.txt -------------------------------------------------------------------------------- /assets/flight_logs/phantom4/phantom4_0.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/phantom4/phantom4_0.txt -------------------------------------------------------------------------------- /assets/flight_logs/rename_testfiles.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from pathlib import Path 4 | 5 | 6 | def rename_files(dir): 7 | base = os.path.basename(dir) 8 | regex = re.compile(f"{base}_([0-9]+)") 9 | listing = os.listdir(dir) 10 | 11 | print(f" Processing '{dir}'\n") 12 | 13 | change_queue = [] 14 | index = -1 15 | 16 | for filename in listing: 17 | src = os.path.join(dir, filename) 18 | 19 | # if folder, ignore 20 | if os.path.isdir(src): 21 | print(f" Ignoring subdir {src}") 22 | continue 23 | 24 | # check if already renamed 25 | match = regex.match(filename) 26 | if match: 27 | match_index = int(match.group(1)) 28 | print(f" Already renamed {filename} with index {match_index}") 29 | index = max(index, match_index) 30 | continue 31 | 32 | ext = "".join(Path(src).suffixes) 33 | file_options = { "base": base, "ext": ext } 34 | change_queue.append((src, file_options)) 35 | 36 | index += 1 37 | for src, options in change_queue: 38 | dst = os.path.join(dir, f"{options['base']}_{index}{options['ext']}") 39 | print(f" Renaming '{os.path.basename(src)}' -> '{os.path.basename(dst)}'") 40 | os.rename(src, dst) 41 | index += 1 42 | print() 43 | 44 | def main(): 45 | # get all the dirs in current dir 46 | script_folder = os.path.dirname(__file__) 47 | 48 | print(f"Processing subdirs of dir '{script_folder}'\n") 49 | 50 | # for each folder in directory of script 51 | for entry in os.listdir(script_folder): 52 | # create full path to subfolder 53 | dir_path = os.path.join(script_folder, entry) 54 | 55 | # only process if a directory 56 | if os.path.isdir(dir_path): 57 | rename_files(dir_path) 58 | 59 | if __name__ == "__main__": 60 | main() -------------------------------------------------------------------------------- /assets/flight_logs/spark/spark_0.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/flight_logs/spark/spark_0.txt -------------------------------------------------------------------------------- /assets/test.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvm/node-djiparsetxt/2d9a63bc4ec308aa61a12f4f1e62c9b7905d776d/assets/test.csv -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { compilerOptions } = require('./tsconfig.json'); 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | moduleNameMapper: { 8 | '^@tests/(.*)$': '/tests/$1' 9 | }, 10 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-djiparsetxt", 3 | "version": "0.2.12", 4 | "description": "command-line application that reads a DJI '.txt' file and outputs a json.", 5 | "main": "dist/node-djiparsetxt.js", 6 | "types": "dist/node-djiparsetxt.d.ts", 7 | "files": [ 8 | "dist/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest", 12 | "build": "tsc && copyfiles --flat src/template/* dist/template" 13 | }, 14 | "author": "cvelez ", 15 | "license": "MIT", 16 | "dependencies": { 17 | "bignum": "^0.13.1", 18 | "binary-parser": "^1.6.2", 19 | "ejs": "^2.7.4", 20 | "lodash": "^4.17.15", 21 | "minimist": "^1.2.5" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/chrisvm/node-djiparsetxt" 26 | }, 27 | "devDependencies": { 28 | "@types/bignum": "0.0.29", 29 | "@types/binary-parser": "^1.5.0", 30 | "@types/ejs": "^2.7.0", 31 | "@types/fs-extra": "^8.1.1", 32 | "@types/jest": "^25.2.3", 33 | "@types/lodash": "^4.14.151", 34 | "@types/minimist": "^1.2.0", 35 | "@types/node": "^11.15.12", 36 | "copyfiles": "^2.2.0", 37 | "jest": "^26.0.1", 38 | "ts-jest": "^26.0.0", 39 | "ts-node": "^8.10.1", 40 | "typescript": "^3.9.2" 41 | }, 42 | "keywords": [ 43 | "dji", 44 | "djiparsetxt", 45 | "quad", 46 | "drones", 47 | "logs", 48 | "parse" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /src/commands/Command.ts: -------------------------------------------------------------------------------- 1 | import {ServiceManager} from "../common/ServiceManager"; 2 | 3 | export abstract class Command { 4 | private _logs: string[] = []; 5 | 6 | constructor(protected serviceMan: ServiceManager) {} 7 | public abstract exec(params: ParamsType): ReturnType; 8 | 9 | protected log(...args: any[]) { 10 | const printed = args.map((val) => val.toString()); 11 | this._logs.push(printed.join(" ")); 12 | } 13 | 14 | protected getLog(): string { 15 | return this._logs.join("\n"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/JpegExtractCommand.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { IRecordCache } from "../shared/interfaces"; 3 | import { RecordTypes } from "../services/RecordTypes"; 4 | import { Command } from "./Command"; 5 | 6 | export interface IJpegExtractOptions { 7 | records: IRecordCache; 8 | } 9 | 10 | export class JpegExtractCommand extends Command { 11 | public exec(options: IJpegExtractOptions): void { 12 | const recordsCache = options.records; 13 | 14 | for (const record of recordsCache.records) { 15 | if (record.type === RecordTypes.JPEG) { 16 | let index = 0; 17 | for (const buf of record.data) { 18 | // ignore zero length jpeg records 19 | if (buf.length === 0) continue; 20 | 21 | const path = `jpeg_${index}.jpeg`; 22 | fs.writeFileSync(path, buf); 23 | console.log(`Wrote ${path} to disk`); 24 | index += 1; 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/OutputCommand.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { Command } from "./Command"; 4 | 5 | export interface IOutputOptions { 6 | file: string; 7 | buffer: Buffer | string; 8 | output: string | null; 9 | } 10 | 11 | export class OutputCommand extends Command { 12 | public exec(options: IOutputOptions): void { 13 | let file = options.file; 14 | const buffer = options.buffer; 15 | 16 | // use -o option to output to file or dir 17 | if (options.output !== null && options.output !== undefined) { 18 | // check if output opt is dir or path to file 19 | const basename = path.basename(file); 20 | file = path.join(options.output, basename); 21 | fs.writeFileSync(file, buffer); 22 | return; 23 | } 24 | 25 | console.log(buffer.toString()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/ParseRecordsCommand.ts: -------------------------------------------------------------------------------- 1 | import { ServiceTypes } from "../common/ServiceManager"; 2 | import { FileParsingService } from "../services/FileParsingService"; 3 | import { Command } from "./Command"; 4 | import { IFile, IRecordCache } from "../shared/interfaces"; 5 | 6 | export interface IParseRecordsOptions { 7 | file: IFile; 8 | } 9 | 10 | export class ParseRecordsCommand extends Command { 11 | public exec(options: IParseRecordsOptions): IRecordCache { 12 | const serviceMan = this.serviceMan; 13 | const fileParsingService = serviceMan.get_service( 14 | ServiceTypes.FileParsing, 15 | ); 16 | 17 | const file = options.file; 18 | 19 | if (file.buffer === null) { 20 | return fileParsingService.createEmptyCache(); 21 | } 22 | 23 | const recordsCache = fileParsingService.parse_records(file.buffer); 24 | return recordsCache; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/PrintInfoCommand.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager, ServiceTypes } from "../common/ServiceManager"; 2 | import { FileInfoService } from "../services/FileInfoService"; 3 | import { FileParsingService } from "../services/FileParsingService"; 4 | import { RecordTypes } from "../services/RecordTypes"; 5 | import { Command } from "./Command"; 6 | import { IFile, IRecordCache } from "../shared/interfaces"; 7 | 8 | export interface IPrintInfoOptions { 9 | printHeader: boolean; 10 | printRecords: boolean; 11 | printDetails: boolean; 12 | printDistribution: boolean; 13 | file: IFile; 14 | } 15 | 16 | export class PrintInfoCommand extends Command { 17 | 18 | public exec(options: IPrintInfoOptions): string { 19 | const serviceMan = this.serviceMan; 20 | const fileInfoService = serviceMan.get_service(ServiceTypes.FileInfo); 21 | const fileParsingService = serviceMan.get_service(ServiceTypes.FileParsing); 22 | 23 | const file = options.file; 24 | 25 | if (file.buffer === null) { 26 | return ""; 27 | } 28 | 29 | // show header details 30 | this.log(`file "${file.path}"`); 31 | if (options.printHeader) { 32 | const headerInfo = fileInfoService.get_header_info(file.buffer); 33 | this.log(" Header Info:"); 34 | this.log(` file size = ${headerInfo.file_size} B`); 35 | this.log(` records area size = ${headerInfo.records_size} B`); 36 | this.log(` details area size = ${headerInfo.details_size} B`); 37 | this.log(" version:", headerInfo.version); 38 | } 39 | 40 | let records: IRecordCache | null = null; 41 | if (options.printRecords) { 42 | this.log(" Records Info:"); 43 | records = fileParsingService.parse_records(file.buffer); 44 | if (records !== null) { 45 | const stats = records.stats; 46 | this.log(` records area size = ${stats.records_area_size} B`); 47 | this.log(` record count = ${stats.record_count} Records`); 48 | this.log(` invalid records = ${stats.invalid_records}`); 49 | this.log(` Records in File:`); 50 | this.print_type_count_table(stats.type_count, " "); 51 | } 52 | } 53 | 54 | if (options.printDetails) { 55 | this.log(" Details:"); 56 | const details = fileInfoService.get_details(file.buffer); 57 | 58 | for (const key in details) { 59 | if (details.hasOwnProperty(key)) { 60 | this.log(` ${key} = ${details[key]}`); 61 | } 62 | } 63 | } 64 | 65 | if (options.printDistribution) { 66 | if (records === null) { 67 | records = fileParsingService.parse_records(file.buffer); 68 | } 69 | if (records !== null) { 70 | this.log(" Record Distribution:"); 71 | this.log(records.records.map((val) => val.type)); 72 | } 73 | } 74 | 75 | return this.getLog(); 76 | } 77 | 78 | private print_type_count_table(typeCount: { [type: number]: number; }, indent: string): void { 79 | const maxWidth = Object.keys(typeCount).reduce((acc, val) => { 80 | const name = RecordTypes[parseInt(val, 10)]; 81 | if (name === undefined) { return acc; } 82 | return Math.max(acc, name.length); 83 | }, 0); 84 | 85 | // hacky way of aligning 86 | for (const key in typeCount) { 87 | if (typeCount.hasOwnProperty(key)) { 88 | let hexRep = parseInt(key, 10).toString(16); 89 | if (hexRep.length === 1) { hexRep = "0" + hexRep; } 90 | let part = `(${RecordTypes[key]})`; 91 | if (maxWidth - (part.length - 2) !== 0) { 92 | part += " ".repeat(maxWidth - part.length + 2); 93 | } 94 | console.log(`${indent}0x${hexRep}`, part, `= ${typeCount[key]}`); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/commands/ReadFileCommand.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { IFile } from "../shared/interfaces"; 3 | import { Command } from "./Command"; 4 | 5 | export class ReadFileCommand extends Command { 6 | public exec(filePaths: string[]): IFile[] { 7 | const files: IFile[] = []; 8 | 9 | for (const path of filePaths) { 10 | try { 11 | const buffer = fs.readFileSync(path); 12 | files.push({ path, buffer }); 13 | } catch (e) { 14 | const buffer = null; 15 | files.push({ path, buffer }); 16 | } 17 | } 18 | 19 | return files; 20 | } 21 | } 22 | 23 | export { IFile }; 24 | -------------------------------------------------------------------------------- /src/commands/Records2CsvCommand.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { ServiceTypes } from "../common/ServiceManager"; 3 | import { CacheTransformService } from "../services/CacheTransformService"; 4 | import { CsvService } from "../services/CsvService"; 5 | import { IRecordCache } from "../shared/interfaces"; 6 | import { Command } from "./Command"; 7 | 8 | export interface IRecords2CsvOptions { 9 | output: string | null; 10 | records: IRecordCache; 11 | } 12 | 13 | /** 14 | * Prints a csv representation of a IRecordCache object. 15 | */ 16 | export class Records2CsvCommand extends Command { 17 | public exec(options: IRecords2CsvOptions): string { 18 | const serviceMan = this.serviceMan; 19 | 20 | const cacheTransService = serviceMan.get_service( 21 | ServiceTypes.CacheTransform, 22 | ); 23 | 24 | const csvService = serviceMan.get_service(ServiceTypes.CsvService); 25 | 26 | const recordsCache = options.records; 27 | cacheTransService.unscramble(recordsCache); 28 | const unscrambledRows = cacheTransService.cache_as_rows(recordsCache); 29 | const parsedRows = cacheTransService.rows_to_json(unscrambledRows); 30 | 31 | // print json object to csv representation 32 | const headerDef = csvService.getRowHeaders(parsedRows); 33 | this.log(csvService.createHeader(headerDef)); 34 | this.log(csvService.printRowValues(parsedRows, headerDef)); 35 | 36 | return this.getLog(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/Records2JsonCommand.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { ServiceTypes } from "../common/ServiceManager"; 3 | import { CacheTransformService } from "../services/CacheTransformService"; 4 | import { IRecordCache } from "../shared/interfaces"; 5 | import { Command } from "./Command"; 6 | 7 | export interface IRecords2JsonOptions { 8 | prettyPrint: boolean; 9 | output: string | null; 10 | records: IRecordCache; 11 | } 12 | 13 | export class Records2JsonCommand extends Command { 14 | public exec(options: IRecords2JsonOptions): string { 15 | const serviceMan = this.serviceMan; 16 | 17 | const cacheTransService = serviceMan.get_service( 18 | ServiceTypes.CacheTransform, 19 | ); 20 | 21 | const recordsCache = options.records; 22 | cacheTransService.unscramble(recordsCache); 23 | const unscrambledRows = cacheTransService.cache_as_rows(recordsCache); 24 | const parsedRows = cacheTransService.rows_to_json(unscrambledRows); 25 | 26 | let output: string; 27 | if (options.prettyPrint) { 28 | output = JSON.stringify(parsedRows, null, 2); 29 | } else { 30 | output = JSON.stringify(parsedRows); 31 | } 32 | 33 | return output; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/SerializeRecordsCommand.ts: -------------------------------------------------------------------------------- 1 | import { RecordTypes } from "../services/RecordTypes"; 2 | import { Command } from "./Command"; 3 | import { IFile, IRecordCache } from "../shared/interfaces"; 4 | 5 | export interface ISerializeRecordsOptions { 6 | file: IFile; 7 | records: IRecordCache; 8 | } 9 | 10 | export class SerializeRecordsCommand extends Command { 11 | public exec(options: ISerializeRecordsOptions): Buffer { 12 | const file = options.file; 13 | const recordsCache = options.records; 14 | 15 | if (file.buffer === null) { 16 | return Buffer.alloc(0); 17 | } 18 | 19 | const unscrambledBuf = Buffer.alloc(file.buffer.length); 20 | file.buffer.copy(unscrambledBuf, 0, 0, 100); 21 | 22 | let offset = 100; 23 | 24 | for (const record of recordsCache.records) { 25 | 26 | unscrambledBuf.writeUInt8(record.type, offset++); 27 | unscrambledBuf.writeUInt8(record.length, offset++); 28 | 29 | // jpeg records don't have scrambling, treat accordingly 30 | if (record.type !== RecordTypes.JPEG) { 31 | for (const buff of record.data) { 32 | buff.copy(unscrambledBuf, offset); 33 | offset += buff.length; 34 | } 35 | unscrambledBuf.writeUInt8(0xff, offset++); 36 | } else { 37 | unscrambledBuf.writeUInt8(0, offset++); 38 | unscrambledBuf.writeUInt8(0, offset++); 39 | for (const buf of record.data) { 40 | for (let ii = 0; ii < buf.length; ii++) { 41 | unscrambledBuf.writeUInt8(buf[ii], offset++); 42 | } 43 | } 44 | } 45 | } 46 | 47 | file.buffer.copy(unscrambledBuf, offset, offset); 48 | return unscrambledBuf; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/commands/ShowTypeCommand.ts: -------------------------------------------------------------------------------- 1 | import { ServiceTypes } from "../common/ServiceManager"; 2 | import { FileParsingService } from "../services/FileParsingService"; 3 | import { RecordTypes } from "../services/RecordTypes"; 4 | import { ScrambleTableService } from "../services/ScrambleTableService"; 5 | import { Command } from "./Command"; 6 | import { IRecordCache } from "../shared/interfaces"; 7 | 8 | export interface IShowTypeOptions { 9 | records: IRecordCache; 10 | output: string | null; 11 | type: RecordTypes; 12 | file: string; 13 | } 14 | 15 | export class ShowTypeCommand extends Command { 16 | 17 | public exec(options: IShowTypeOptions): string { 18 | const serviceMan = this.serviceMan; 19 | const fileParsingService = serviceMan.get_service(ServiceTypes.FileParsing); 20 | const scrambleTableService = serviceMan.get_service(ServiceTypes.ScrambleTable); 21 | 22 | const records = options.records; 23 | const type = options.type; 24 | const recordsOfType = fileParsingService.filter_records(records, type); 25 | const file = options.file; 26 | 27 | const typeName = RecordTypes[type]; 28 | 29 | if (typeName) { 30 | this.log(`file '${file}' and type = ${typeName}:`); 31 | 32 | recordsOfType.forEach((record) => { 33 | const unscrambledRec = scrambleTableService.unscramble_record(record); 34 | const subParsed = fileParsingService.parse_record_by_type(unscrambledRec, type); 35 | this.log(subParsed); 36 | }); 37 | 38 | return this.getLog(); 39 | } 40 | 41 | throw new Error(`type '${type}' not recognized`); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/UnscrambleCommand.ts: -------------------------------------------------------------------------------- 1 | import { ServiceTypes } from "../common/ServiceManager"; 2 | import { IRecordCache } from "../shared/interfaces"; 3 | import { ScrambleTableService } from "../services/ScrambleTableService"; 4 | import { Command } from "./Command"; 5 | 6 | export interface IUnscrambleOptions { 7 | records: IRecordCache; 8 | } 9 | 10 | export class UnscrambleCommand extends Command { 11 | public exec(options: IUnscrambleOptions): IRecordCache { 12 | const serviceMan = this.serviceMan; 13 | 14 | const scrambleTableService = serviceMan.get_service( 15 | ServiceTypes.ScrambleTable, 16 | ); 17 | 18 | const recordsCache = options.records; 19 | recordsCache.records = recordsCache.records.map((val) => scrambleTableService.unscramble_record(val)); 20 | return recordsCache; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Command"; 2 | export * from "./PrintInfoCommand"; 3 | export * from "./ShowTypeCommand"; 4 | export * from "./Records2JsonCommand"; 5 | export * from "./UnscrambleCommand"; 6 | export * from "./ReadFileCommand"; 7 | export * from "./ParseRecordsCommand"; 8 | export * from "./SerializeRecordsCommand"; 9 | export * from "./OutputCommand"; 10 | export * from "./Records2CsvCommand"; 11 | export * from "./JpegExtractCommand"; 12 | -------------------------------------------------------------------------------- /src/common/CliArguments.ts: -------------------------------------------------------------------------------- 1 | import minimist from "minimist"; 2 | 3 | interface IOptionDescription { 4 | short_name: string; 5 | long_name: string; 6 | description: string; 7 | param_name?: string; 8 | } 9 | 10 | export class CliArguments { 11 | 12 | public get isEmpty(): boolean { 13 | return this.isEmpty; 14 | } 15 | 16 | public get print_header(): boolean { 17 | return this.argv.header || this.argv.h; 18 | } 19 | 20 | public get print_records(): boolean { 21 | return this.argv.records || this.argv.r; 22 | } 23 | 24 | public get file_paths(): string[] { 25 | return this.argv._; 26 | } 27 | 28 | public get details(): boolean { 29 | return this.argv.details || this.argv.d; 30 | } 31 | 32 | public get output(): string | null { 33 | if (this.argv.output) { return this.argv.output; } 34 | return this.argv.o; 35 | } 36 | 37 | public get unscramble(): boolean { 38 | return this.argv.unscramble || this.argv.u; 39 | } 40 | 41 | public get show_record(): number | null { 42 | return this.argv.show_type || this.argv.s; 43 | } 44 | 45 | public get pretty_print(): boolean { 46 | return this.argv.pretty || this.argv.p; 47 | } 48 | 49 | public get distrib(): boolean { 50 | return this.argv.distribution || this.argv.D; 51 | } 52 | 53 | public get csv(): boolean { 54 | return this.argv.csv || this.argv.c; 55 | } 56 | 57 | public get jpeg(): boolean { 58 | return this.argv.j || this.argv.jpeg; 59 | } 60 | 61 | public static print_usage(): void { 62 | console.log("Usage: node-djiparsetext FILE [FILE...] [OPTIONS]\n"); 63 | } 64 | 65 | public static print_help(): void { 66 | CliArguments.print_usage(); 67 | 68 | console.log("Options:"); 69 | 70 | for (const option of CliArguments.optionsDescriptions) { 71 | if (option.param_name) { 72 | console.log(` --${option.long_name} ${option.param_name},` + 73 | ` -${option.short_name} ${option.param_name}: ${option.description}`); 74 | continue; 75 | } 76 | console.log(` --${option.long_name}, -${option.short_name}:` + 77 | ` ${option.description}`); 78 | } 79 | } 80 | 81 | private static optionsDescriptions: IOptionDescription[] = [ 82 | { 83 | short_name: "u", 84 | long_name: "unscramble", 85 | description: "Create a copy of the file with the records unscrambled.", 86 | }, 87 | { 88 | short_name: "h", 89 | long_name: "header", 90 | description: "Print header info to stdout.", 91 | }, 92 | { 93 | short_name: "r", 94 | long_name: "records", 95 | description: "Print records info to stdout.", 96 | }, 97 | { 98 | short_name: "d", 99 | long_name: "details", 100 | description: "Print the details section to stdout.", 101 | }, 102 | { 103 | short_name: "o", 104 | long_name: "output", 105 | description: "Path to use for output files.", 106 | }, 107 | { 108 | short_name: "s", 109 | long_name: "show-type", 110 | description: "Show the records of the given type.", 111 | param_name: "type", 112 | }, 113 | { 114 | short_name: "D", 115 | long_name: "distribution", 116 | description: "Print the record types as they appear in the file.", 117 | }, 118 | { 119 | short_name: "c", 120 | long_name: "csv", 121 | description: "Output the parsed file in csv form", 122 | }, 123 | { 124 | short_name: "p", 125 | long_name: "pretty", 126 | description: "Pretty print the json output.", 127 | }, 128 | { 129 | short_name: "j", 130 | long_name: "jpeg", 131 | description: "Extract the found jpeg records to the current dir", 132 | }, 133 | ]; 134 | 135 | private argv: minimist.ParsedArgs; 136 | private _isEmpty: boolean; 137 | 138 | constructor(args: string[]) { 139 | this._isEmpty = false; 140 | if (args.length === 0) { 141 | this._isEmpty = true; 142 | } 143 | 144 | this.argv = minimist(args); 145 | } 146 | 147 | public assert_args(): boolean { 148 | const argv = this.argv; 149 | 150 | if (argv.help === true || argv.h === true) { 151 | CliArguments.print_help(); 152 | return true; 153 | } 154 | 155 | // if argument list empty (no filenames given) 156 | if (argv._.length === 0) { 157 | console.log(`node-djiparsetxt: No files given`); 158 | CliArguments.print_usage(); 159 | return true; 160 | } 161 | 162 | return false; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/common/ServiceManager.ts: -------------------------------------------------------------------------------- 1 | import BaseService from "../services/BaseService"; 2 | import { BinaryParserService } from "../services/BinaryParserService"; 3 | import { CacheTransformService } from "../services/CacheTransformService"; 4 | import { CsvService } from "../services/CsvService"; 5 | import { FileInfoService } from "../services/FileInfoService"; 6 | import { FileParsingService } from "../services/FileParsingService"; 7 | import { ScrambleTableService } from "../services/ScrambleTableService"; 8 | import { ILazyLoadingEntry } from "../shared/interfaces"; 9 | 10 | export enum ServiceTypes { 11 | Parsers = "parsers", 12 | FileInfo = "file_info", 13 | ScrambleTable = "scramble_table", 14 | FileParsing = "file_parsing", 15 | CacheTransform = "cache_transform", 16 | CsvService = "csv", 17 | } 18 | 19 | export class ServiceManager { 20 | 21 | protected services: {[name: string]: ILazyLoadingEntry}; 22 | 23 | constructor() { 24 | this.services = { 25 | parsers: { 26 | instance: null, 27 | factory: () => new BinaryParserService(this), 28 | }, 29 | file_info: { 30 | instance: null, 31 | factory: () => new FileInfoService(this), 32 | }, 33 | scramble_table: { 34 | instance: null, 35 | factory: () => new ScrambleTableService(this), 36 | }, 37 | file_parsing: { 38 | instance: null, 39 | factory: () => new FileParsingService(this), 40 | }, 41 | cache_transform: { 42 | instance: null, 43 | factory: () => new CacheTransformService(this), 44 | }, 45 | csv: { 46 | instance: null, 47 | factory: () => new CsvService(this), 48 | }, 49 | }; 50 | } 51 | 52 | public get_service(type: ServiceTypes): T { 53 | const service = this.services[type]; 54 | 55 | if (service == null) { 56 | throw new Error(`service ${type} not found`); 57 | } 58 | 59 | if (service.instance == null) { 60 | service.instance = service.factory(); 61 | } 62 | 63 | return service.instance as T; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/common/Version.ts: -------------------------------------------------------------------------------- 1 | export class Version { 2 | public static CreateEmpty(): Version { 3 | const ver = 0x00000000; 4 | return new Version(ver); 5 | } 6 | 7 | public ver: number[]; 8 | 9 | constructor(ver: number) { 10 | this.ver = [ 11 | ver & 0x000000FF, 12 | ver & 0x0000FF00, 13 | ver & 0x00FF0000, 14 | ver & 0xFF000000, 15 | ]; 16 | } 17 | 18 | public toString(): string { 19 | const ver = this.ver; 20 | return `${ver[0]}.${ver[1]}.${ver[2]}.${ver[3]}`; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/common/lazy_loading.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/node-djiparsetxt.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import ejs from "ejs"; 3 | import fs from "fs"; 4 | import _ from "lodash"; 5 | import path from "path"; 6 | const DEFAULT_IMAGE: string = "https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png"; 7 | 8 | import { 9 | IFile, 10 | JpegExtractCommand, 11 | OutputCommand, 12 | ParseRecordsCommand, 13 | PrintInfoCommand, 14 | ReadFileCommand, 15 | Records2CsvCommand, 16 | Records2JsonCommand, 17 | SerializeRecordsCommand, 18 | ShowTypeCommand, 19 | UnscrambleCommand, 20 | } from "./commands"; 21 | import { CliArguments } from "./common/CliArguments"; 22 | import { ServiceManager, ServiceTypes } from "./common/ServiceManager"; 23 | import { CacheTransformService } from "./services/CacheTransformService"; 24 | import { FileInfoService } from "./services/FileInfoService"; 25 | import { FileParsingService } from "./services/FileParsingService"; 26 | import { RecordTypes } from "./services/RecordTypes"; 27 | import { IRowObject, IHeaderInfo, IRecord } from "./shared/interfaces"; 28 | 29 | function execute_cli(args: string[]) { 30 | const argv = new CliArguments(args); 31 | 32 | // assert cli args 33 | if (argv.assert_args()) { 34 | return; 35 | } 36 | 37 | // create managers 38 | const serviceMan = new ServiceManager(); 39 | let command; 40 | let output; 41 | 42 | // read files from arguments 43 | const files: IFile[] = new ReadFileCommand(serviceMan).exec(argv.file_paths); 44 | 45 | for (const file of files) { 46 | if (argv.print_header || argv.print_records || argv.details || argv.distrib) { 47 | command = new PrintInfoCommand(serviceMan); 48 | output = command.exec({ 49 | file, 50 | printHeader: argv.print_header, 51 | printRecords: argv.print_records, 52 | printDetails: argv.details, 53 | printDistribution: argv.distrib, 54 | }); 55 | 56 | command = new OutputCommand(serviceMan); 57 | command.exec({ file: file.path, buffer: output, output: argv.output }); 58 | continue; 59 | } 60 | 61 | command = new ParseRecordsCommand(serviceMan); 62 | const records = command.exec({ file }); 63 | 64 | if (records.isEmpty) { 65 | continue; 66 | } 67 | 68 | if (argv.unscramble) { 69 | command = new UnscrambleCommand(serviceMan); 70 | command.exec({ records }); 71 | 72 | command = new SerializeRecordsCommand(serviceMan); 73 | const buffer = command.exec({ file, records }); 74 | 75 | command = new OutputCommand(serviceMan); 76 | output = argv.output === undefined ? path.dirname(file.path) : argv.output; 77 | command.exec({ file: file.path + ".unscrambled", buffer, output}); 78 | continue; 79 | } 80 | 81 | if (argv.show_record != null) { 82 | const type = argv.show_record as RecordTypes; 83 | command = new ShowTypeCommand(serviceMan); 84 | const buffer = command.exec({ type, records, file: file.path, output: argv.output }); 85 | 86 | command = new OutputCommand(serviceMan); 87 | output = argv.output; 88 | command.exec({ file: file.path, buffer, output}); 89 | continue; 90 | } 91 | 92 | if (argv.jpeg) { 93 | command = new JpegExtractCommand(serviceMan); 94 | command.exec({ records }); 95 | continue; 96 | } 97 | 98 | let outputString: string; 99 | if (argv.csv) { 100 | command = new Records2CsvCommand(serviceMan); 101 | outputString = command.exec({ records, output: argv.output }); 102 | } else { 103 | command = new Records2JsonCommand(serviceMan); 104 | outputString = command.exec({ 105 | records, 106 | output: argv.output, 107 | prettyPrint: argv.pretty_print, 108 | }); 109 | } 110 | 111 | command = new OutputCommand(serviceMan); 112 | output = argv.output ? argv.output : null; 113 | command.exec({ file: file.path, buffer: outputString, output}); 114 | } 115 | } 116 | 117 | // this is what runs when called as a tool 118 | if (require.main === module) { 119 | try { 120 | const args = process.argv.slice(2); 121 | execute_cli(args); 122 | } catch (e) { 123 | const processName = "node-djiparsetxt"; 124 | console.log(`${processName}: ${e}`); 125 | console.log(e.stack); 126 | } 127 | } 128 | 129 | //#region Public API 130 | 131 | /** 132 | * Parse the record from the given file. 133 | * @param buf File buffer of a log. 134 | * @param filter Function to use as a filter for the rows, only IRecord's that return true are returned. 135 | * @returns Array of rows with each row being 136 | * an object where the keys are the record type. 137 | */ 138 | export function parse_file(buf: Buffer, filter?: (row: IRowObject) => boolean): IRowObject[] { 139 | const serviceMan = new ServiceManager(); 140 | 141 | const fileParsingService = serviceMan.get_service( 142 | ServiceTypes.FileParsing, 143 | ); 144 | 145 | const cacheTransService = serviceMan.get_service( 146 | ServiceTypes.CacheTransform, 147 | ); 148 | 149 | const recordsCache = fileParsingService.parse_records(buf); 150 | cacheTransService.unscramble(recordsCache); 151 | const unscrambledRows = cacheTransService.cache_as_rows(recordsCache); 152 | let parsedRows = cacheTransService.rows_to_json(unscrambledRows); 153 | 154 | // if a filter is given, apply it to the rows 155 | if (filter !== undefined && filter !== null) { 156 | parsedRows = parsedRows.filter((val) => filter(val)); 157 | } 158 | 159 | return parsedRows; 160 | } 161 | 162 | /** 163 | * Get details section of the given file. 164 | * @param buf File buffer of a log. 165 | * @returns Object with the parsed details section. 166 | */ 167 | export function get_details(buf: Buffer): any { 168 | const serviceMan = new ServiceManager(); 169 | const fileInfoService = serviceMan.get_service(ServiceTypes.FileInfo); 170 | return fileInfoService.get_details(buf); 171 | } 172 | 173 | /** 174 | * Get header section of the given file. 175 | * @param buf File buffer of a log. 176 | * @returns Object with the parsed header section. 177 | */ 178 | export function get_header(buf: Buffer): IHeaderInfo { 179 | const serviceMan = new ServiceManager(); 180 | const fileInfoService = serviceMan.get_service(ServiceTypes.FileInfo); 181 | return fileInfoService.get_header_info(buf); 182 | } 183 | 184 | /** 185 | * Returns a KML string. 186 | * @param buf File buffer of a log 187 | * @param image Optional image param for the kml 188 | * @returns Array of jpeg buffers. 189 | */ 190 | export async function get_kml(buf: Buffer, image?: string, removeNoSignalRecords: boolean = false): Promise { 191 | let filter = (row: IRowObject) => true; 192 | 193 | // if removeNoSignalRecords is set, we remove the frames with OSD.gps_level on zero. 194 | if (removeNoSignalRecords) { 195 | filter = (row: IRowObject) => row.OSD.gps_level !== 0; 196 | } 197 | 198 | const parsedRows = parse_file(buf, filter); 199 | 200 | let results: string = ""; 201 | let homeCoordinates: string = ""; 202 | let imageURL: string = ""; 203 | if (!image) { 204 | imageURL = DEFAULT_IMAGE; 205 | } else { 206 | imageURL = image; 207 | } 208 | 209 | for (let index = 0; index < parsedRows.length; index += 1) { 210 | const rec = parsedRows[index]; 211 | const long: number = rec.OSD.longitude; 212 | const lat: number = rec.OSD.latitude; 213 | const location: string = long + "," + lat + " "; 214 | results += location; 215 | 216 | // if first coord, set home to it 217 | if (index === 0) { 218 | homeCoordinates = location; 219 | } 220 | } 221 | 222 | const templateFilePath = path.join(__dirname, "/template/kml-template.ejs"); 223 | const kmlTemplate = fs.readFileSync(templateFilePath, "utf8"); 224 | const kml: string = await ejs.render(kmlTemplate, { 225 | imageurl: imageURL, 226 | homeCoordinates, 227 | coordinates: results, 228 | }); 229 | 230 | return kml; 231 | } 232 | 233 | /** 234 | * Get the jpegs in file. 235 | * @param buf File buffer of a log 236 | * @returns Array of jpeg buffers. 237 | */ 238 | export function get_jpegs(buf: Buffer): Buffer[] { 239 | let jpegs: Buffer[] = []; 240 | const serviceMan = new ServiceManager(); 241 | 242 | const fileParsingService = serviceMan.get_service( 243 | ServiceTypes.FileParsing, 244 | ); 245 | 246 | const cacheTransService = serviceMan.get_service( 247 | ServiceTypes.CacheTransform, 248 | ); 249 | 250 | const cache = fileParsingService.parse_records(buf); 251 | cacheTransService.unscramble(cache); 252 | const jpegRecords = cache.records.filter((rec) => rec.type === RecordTypes.JPEG); 253 | for (const record of jpegRecords) { 254 | // ignore zero bytes images 255 | if (record.data[0].length === 0) continue; 256 | jpegs = jpegs.concat(record.data); 257 | } 258 | return jpegs; 259 | } 260 | 261 | export * from "./shared/interfaces"; 262 | //#endregion 263 | -------------------------------------------------------------------------------- /src/services/BaseService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from "../common/ServiceManager"; 2 | 3 | export default abstract class BaseService { 4 | protected serviceMan: ServiceManager; 5 | 6 | constructor(serviceMan: ServiceManager) { 7 | this.serviceMan = serviceMan; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/services/BinaryParserService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceTypes } from "../common/ServiceManager"; 2 | import BaseService from "./BaseService"; 3 | import { PARSER_TABLE } from "./BinaryParserTable"; 4 | import { RecordTypes } from "./RecordTypes"; 5 | 6 | export enum ParserTypes { 7 | Header = "header", 8 | BaseRecord = "base_record", 9 | StartRecord = "start_record", 10 | Details = "details", 11 | OsdRecord = "osd_record", 12 | HomeRecord = "home_record", 13 | GimbalRecord = "gimbal_record", 14 | RcRecord = "rc_record", 15 | CustomRecord = "custom_record", 16 | DeformRecord = "deform_record", 17 | CenterBatteryRecord = "center_battery_record", 18 | SmartBatteryRecord = "smart_battery_record", 19 | AppTipRecord = "app_tip_record", 20 | AppWarnRecord = "app_warn_record", 21 | RecoverRecord = "recover_record", 22 | AppGpsRecord = "app_gps_record", 23 | FirmwareRecord = "firmware_record", 24 | } 25 | 26 | export class BinaryParserService extends BaseService { 27 | 28 | /** 29 | * Returns an instance of the requested parser by the type of parser. 30 | * @param type Entry in ParserTypes enum for the requested parser. 31 | */ 32 | public get_parser(type: ParserTypes): any { 33 | if (PARSER_TABLE[type] == null) { 34 | return undefined; 35 | } 36 | 37 | if (PARSER_TABLE[type].instance == null) { 38 | const factory = PARSER_TABLE[type].factory; 39 | PARSER_TABLE[type].instance = factory(); 40 | } 41 | 42 | return PARSER_TABLE[type].instance; 43 | } 44 | 45 | /** 46 | * Returns an instance of a parser based on given record type. This 47 | * parser should be able to parse the given record type. 48 | * @param recordType The type of the record parsed by the parser. 49 | */ 50 | public get_record_parser(recordType: RecordTypes): any { 51 | const parserService = this.serviceMan.get_service( 52 | ServiceTypes.Parsers, 53 | ) as BinaryParserService; 54 | 55 | const parserType = this.parser_record_mapping(recordType); 56 | 57 | if (parserType === null) { 58 | throw new Error(`record type '${recordType}' not recognized`); 59 | } 60 | 61 | return parserService.get_parser(parserType); 62 | } 63 | 64 | /** 65 | * Mapping between record type and the parser that can parse them. 66 | * @param recordType The type of the record to get its corresponding parser type. 67 | */ 68 | public parser_record_mapping(recordType: RecordTypes): ParserTypes | null { 69 | switch (recordType) { 70 | case RecordTypes.OSD: 71 | return ParserTypes.OsdRecord; 72 | case RecordTypes.CUSTOM: 73 | return ParserTypes.CustomRecord; 74 | case RecordTypes.RC: 75 | return ParserTypes.RcRecord; 76 | case RecordTypes.GIMBAL: 77 | return ParserTypes.GimbalRecord; 78 | case RecordTypes.HOME: 79 | return ParserTypes.HomeRecord; 80 | case RecordTypes.DEFORM: 81 | return ParserTypes.DeformRecord; 82 | case RecordTypes.CENTER_BATTERY: 83 | return ParserTypes.CenterBatteryRecord; 84 | case RecordTypes.SMART_BATTERY: 85 | return ParserTypes.SmartBatteryRecord; 86 | case RecordTypes.APP_TIP: 87 | return ParserTypes.AppTipRecord; 88 | case RecordTypes.APP_WARN: 89 | return ParserTypes.AppWarnRecord; 90 | case RecordTypes.RECOVER: 91 | return ParserTypes.RecoverRecord; 92 | case RecordTypes.APP_GPS: 93 | return ParserTypes.AppGpsRecord; 94 | case RecordTypes.FIRMWARE: 95 | return ParserTypes.FirmwareRecord; 96 | default: 97 | return null; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/services/BinaryParserTable.ts: -------------------------------------------------------------------------------- 1 | import bignum = require("bignum"); 2 | import { Parser } from "binary-parser"; 3 | import { ILazyLoadingEntry } from "../shared/interfaces"; 4 | import { Version } from "../common/Version"; 5 | import { 6 | DEFORM_MODE, 7 | DEFORM_STATUS, 8 | DETAILS_APP_TYPE, 9 | GIMBAL_MODE, 10 | HOME_IOC_MODE, 11 | NO_MATCH, 12 | OSD_RECORD_BATTERY_TYPE, 13 | OSD_RECORD_DRONE_TYPE, 14 | OSD_RECORD_FLIGHT_ACTION, 15 | OSD_RECORD_FLYCCOMMAND, 16 | OSD_RECORD_FLYCSTATE, 17 | OSD_RECORD_GO_HOME_STATUS, 18 | OSD_RECORD_GROUND_OR_SKY, 19 | OSD_RECORD_IMU_INIT_FAIL_REASON, 20 | OSD_RECORD_MOTOR_START_FAILED_CAUSE, 21 | OSD_RECORD_NON_GPS_CAUSE, 22 | RECOVERY_APP_TYPE, 23 | RECOVERY_DRONE_TYPE, 24 | SMART_BATTERY_GO_HOME_STATUS, 25 | SMART_BATTERY_STATUS, 26 | } from "./InterpretationTable"; 27 | 28 | const radiants2degree = (val: any) => val * 57.2958; 29 | 30 | export interface IParserLookUpTable { 31 | [type: string]: ILazyLoadingEntry; 32 | } 33 | 34 | export function bignum_convert_buffer(buffer: any): bignum { 35 | return bignum.fromBuffer(buffer as Buffer, { endian: "little", size: 8 }); 36 | } 37 | 38 | export const PARSER_TABLE: IParserLookUpTable = { 39 | header: { 40 | instance: null, 41 | factory: () => { 42 | const parser = new Parser() 43 | .uint32le("header_record_size_lo") 44 | .uint32le("header_record_size_hi") 45 | .uint32be("file_version"); 46 | 47 | const dummy: any = { parser }; 48 | dummy.parse = (buf: Buffer) => { 49 | const parsed = dummy.parser.parse(buf); 50 | parsed.file_version = new Version(parsed.file_version); 51 | return parsed; 52 | }; 53 | return dummy; 54 | }, 55 | }, 56 | base_record: { 57 | instance: null, 58 | factory: () => { 59 | return new Parser() 60 | .uint8("type") 61 | .uint8("length") 62 | .buffer("data", { 63 | length: "length", 64 | }) 65 | .uint8("marker"); 66 | }, 67 | }, 68 | start_record: { 69 | instance: null, 70 | factory: () => { 71 | return new Parser().uint8("type").uint8("length"); 72 | }, 73 | }, 74 | details: { 75 | instance: null, 76 | factory: () => { 77 | const parser = new Parser() 78 | .buffer("city_part", { length: 20, formatter: (val) => (val as Buffer).toString("ascii").replace(/\0/g, "") }) 79 | .buffer("street", { length: 20, formatter: (val) => (val as Buffer).toString("ascii").replace(/\0/g, "") }) 80 | .buffer("city", { length: 20, formatter: (val) => (val as Buffer).toString("ascii").replace(/\0/g, "") }) 81 | .buffer("area", { length: 20, formatter: (val) => (val as Buffer).toString("ascii").replace(/\0/g, "") }) 82 | .uint8("is_favorite") 83 | .uint8("is_new") 84 | .uint8("needs_upload") 85 | .uint32le("record_line_count") 86 | .skip(4) 87 | .buffer("timestamp", { length: 8 }) 88 | .doublele("longitude", { formatter: radiants2degree }) 89 | .doublele("latitude", { formatter: radiants2degree }) 90 | .floatle("total_distance") 91 | .uint32le("total_time", { 92 | formatter: (time) => (time as number) / 1000, 93 | }) 94 | .floatle("max_height") 95 | .floatle("max_hor_speed") 96 | .floatle("max_vert_speed") 97 | .uint32le("photo_count") 98 | .uint32le("video_time") 99 | // TODO: finish implementing parser for diff versions 100 | .skip(137) 101 | .string("aircraft_name", { length: 32, formatter: (val) => (val as string).replace(/\0/g, "") }) 102 | .string("aircraft_sn", { length: 16, formatter: (val) => (val as string).replace(/\0/g, "") }) 103 | .string("camera_sn", { length: 16, formatter: (val) => (val as string).replace(/\0/g, "") }) 104 | .string("rc_sn", { length: 16, formatter: (val) => (val as string).replace(/\0/g, "") }) 105 | .string("battery_sn", { length: 16, formatter: (val) => (val as string).replace(/\0/g, "") }) 106 | .uint8("app_type") 107 | .buffer("app_version", { length: 3 }); 108 | 109 | const dummy: any = { parser }; 110 | dummy.parse = (buf: Buffer): any => { 111 | const parsed = dummy.parser.parse(buf); 112 | parsed.app_type = DETAILS_APP_TYPE[parsed.app_type] || NO_MATCH; 113 | const timestamp = bignum_convert_buffer(parsed.timestamp); 114 | parsed.timestamp = new Date(parseInt(timestamp.toString())).toISOString(); 115 | parsed.app_version = new Version(parsed.app_version); 116 | return parsed; 117 | }; 118 | return dummy; 119 | }, 120 | }, 121 | osd_record: { 122 | // todo: deal with file versions 123 | instance: null, 124 | factory: () => { 125 | const dummy: any = { 126 | parser: new Parser() 127 | .doublele("longitude", { formatter: radiants2degree }) 128 | .doublele("latitude", { formatter: radiants2degree }) 129 | .int16le("height", { 130 | formatter: (val: any) => val * 0.1, 131 | }) 132 | .int16le("x_speed", { 133 | formatter: (val: any) => val * 0.1, 134 | }) 135 | .int16le("y_speed", { 136 | formatter: (val: any) => val * 0.1, 137 | }) 138 | .int16le("z_speed", { 139 | formatter: (val: any) => val * 0.1, 140 | }) 141 | .int16le("pitch", { 142 | formatter: (val: any) => val * 0.1, 143 | }) 144 | .int16le("roll", { 145 | formatter: (val: any) => val * 0.1, 146 | }) 147 | .int16le("yaw", { 148 | formatter: (val: any) => val * 0.1, 149 | }) 150 | .bit1("rc_state") 151 | .bit7("fly_state") 152 | .uint8("fly_command") 153 | .bit3("go_home_status") 154 | .bit1("is_swave_work") 155 | .bit1("is_motor_up") 156 | .bit2("ground_or_sky") 157 | .bit1("can_ioc_work") 158 | .bit1("unknown") 159 | .bit2("mode_channel") 160 | .bit1("is_imu_preheated") 161 | .bit1("unknown") 162 | .bit2("voltage_warning") 163 | .bit1("is_vision_used") 164 | .bit2("battery_type") 165 | .bit4("gps_level") 166 | .bit1("wave_error") 167 | .bit1("compass_error") 168 | .bit1("is_accelerator_over_range") 169 | .bit1("is_vibrating") 170 | .bit1("is_barometer_dead_in_air") 171 | .bit1("is_motor_blocked") 172 | .bit1("is_not_enough_force") 173 | .bit1("is_propeller_catapult") 174 | .bit1("is_go_home_height_modified") 175 | .bit1("is_out_of_limit") 176 | .uint8("gps_num") 177 | .uint8("flight_action") 178 | .uint8("motor_start_failed_cause") 179 | .bit3("unknown") 180 | .bit1("waipoint_limit_mode") 181 | .bit4("non_gps_cause") 182 | .uint8("battery") 183 | .uint8("swave_height") 184 | .uint16le("fly_time") 185 | .uint8("motor_revolution") 186 | .skip(2) 187 | .uint8("flyc_version") 188 | .uint8("drone_type") 189 | .uint8("imu_init_fail_reason"), 190 | }; 191 | dummy.parse = (buf: Buffer): any => { 192 | const parsed = dummy.parser.parse(buf); 193 | parsed.fly_state = OSD_RECORD_FLYCSTATE[parsed.fly_state] || NO_MATCH; 194 | parsed.fly_command = OSD_RECORD_FLYCCOMMAND[parsed.fly_command] || NO_MATCH; 195 | parsed.ground_or_sky = OSD_RECORD_GROUND_OR_SKY[parsed.ground_or_sky] || NO_MATCH; 196 | parsed.go_home_status = OSD_RECORD_GO_HOME_STATUS[parsed.go_home_status] || NO_MATCH; 197 | parsed.battery_type = OSD_RECORD_BATTERY_TYPE[parsed.battery_type] || NO_MATCH; 198 | parsed.flight_action = OSD_RECORD_FLIGHT_ACTION[parsed.flight_action] || NO_MATCH; 199 | parsed.motor_start_failed_cause = OSD_RECORD_MOTOR_START_FAILED_CAUSE[parsed.motor_start_failed_cause] || NO_MATCH; 200 | parsed.non_gps_cause = OSD_RECORD_NON_GPS_CAUSE[parsed.non_gps_cause] || NO_MATCH; 201 | parsed.imu_init_fail_reason = OSD_RECORD_IMU_INIT_FAIL_REASON[parsed.imu_init_fail_reason] || NO_MATCH; 202 | parsed.drone_type = OSD_RECORD_DRONE_TYPE[parsed.drone_type] || NO_MATCH; 203 | return parsed; 204 | }; 205 | return dummy; 206 | }, 207 | }, 208 | custom_record: { 209 | instance: null, 210 | factory: () => { 211 | const dummy: any = { 212 | parser: new Parser() 213 | .skip(2) 214 | .floatle("hspeed") 215 | .floatle("distance") 216 | .buffer("updateTime", { length: 8 }), 217 | }; 218 | 219 | // override the parse method to also convert the buffer 220 | // into a bignum obj 221 | dummy.parse = (buf: Buffer): any => { 222 | const parsed = dummy.parser.parse(buf); 223 | const updateTime = bignum_convert_buffer(parsed.updateTime); 224 | parsed.updateTime = new Date(parseInt(updateTime.toString())).toISOString(); 225 | return parsed; 226 | }; 227 | 228 | return dummy; 229 | }, 230 | }, 231 | rc_record: { 232 | instance: null, 233 | factory: () => { 234 | // todo: implement data transformations 235 | return new Parser() 236 | .int16le("aileron", { 237 | formatter: (val: any) => (val - 1024) / 0.066, 238 | }) 239 | .int16le("elevator", { 240 | formatter: (val: any) => (val - 1024) / 0.066, 241 | }) 242 | .int16le("throttle", { 243 | formatter: (val: any) => (val - 1024) / 0.066, 244 | }) 245 | .int16le("rudder", { 246 | formatter: (val: any) => (val - 1024) / 0.066, 247 | }) 248 | .int16le("gimbal", { 249 | formatter: (val: any) => (val - 1024) / 0.066, 250 | }) 251 | .bit2("unknown") 252 | .bit5("wheel_offset") 253 | .bit1("unknown") 254 | .bit2("unknown") 255 | .bit2("mode") 256 | .bit1("go_home") 257 | .bit3("unknown") 258 | .bit3("unknown") 259 | .bit1("custom2") 260 | .bit1("custom1") 261 | .bit1("playback") 262 | .bit1("shutter") 263 | .bit1("record"); 264 | }, 265 | }, 266 | gimbal_record: { 267 | instance: null, 268 | factory: () => { 269 | const dummy: any = { 270 | parser: new Parser() 271 | .int16le("pitch", { formatter: (val: any) => val / 10 }) 272 | .int16le("roll", { formatter: (val: any) => val / 10 }) 273 | .int16le("yaw", { formatter: (val: any) => val / 10 }) 274 | .bit2("mode") 275 | .bit6("unknown") 276 | .int8("roll_adjust", { formatter: (val: any) => val / 10 }) 277 | .int16le("yaw_angle", { formatter: (val: any) => val / 10 }) 278 | .bit1("is_pitch_in_limit") 279 | .bit1("is_roll_in_limit") 280 | .bit1("is_yaw_in_limit") 281 | .bit1("is_auto_calibration") 282 | .bit1("auto_calibration_results") 283 | .bit1("unknown") 284 | .bit1("is_stuck") 285 | .bit1("unknown") 286 | .bit1("is_single_click") 287 | .bit1("is_triple_click") 288 | .bit1("is_double_click") 289 | .bit1("unknown") 290 | .bit4("version"), 291 | }; 292 | dummy.parse = (buf: Buffer): any => { 293 | const parsed = dummy.parser.parse(buf); 294 | parsed.mode = GIMBAL_MODE[parsed.mode] || NO_MATCH; 295 | return parsed; 296 | }; 297 | 298 | return dummy; 299 | 300 | }, 301 | }, 302 | home_record: { 303 | instance: null, 304 | factory: () => { 305 | const dummy: any = { 306 | parser: new Parser() 307 | .doublele("longitude", { formatter: radiants2degree }) 308 | .doublele("latitude", { formatter: radiants2degree }) 309 | .int16le("height", { formatter: (val: any) => val / 10 }) 310 | .bit1("has_go_home") 311 | .bit3("go_home_status") 312 | .bit1("is_dyn_home_point_enabled") 313 | .bit1("aircraft_head_direction") 314 | .bit1("go_home_mode") 315 | .bit1("is_home_record") 316 | .bit3("ioc_mode") 317 | .bit1("ioc_enabled") 318 | .bit1("is_beginners_mode") 319 | .bit1("is_compass_celeing") 320 | .bit2("compass_cele_status") 321 | .uint16le("go_home_height") 322 | .int16le("course_lock_angle", { 323 | formatter: (val: any) => val / 10, 324 | }) 325 | .uint8("data_recorder_status") 326 | .uint8("data_recorder_remain_capacity") 327 | .uint16le("data_recorder_remain_time") 328 | .uint16le("data_recorder_file_index"), 329 | }; 330 | dummy.parse = (buf: Buffer): any => { 331 | const parsed = dummy.parser.parse(buf); 332 | parsed.ioc_mode = HOME_IOC_MODE[parsed.ioc_mode] || NO_MATCH; 333 | return parsed; 334 | }; 335 | 336 | return dummy; 337 | 338 | }, 339 | }, 340 | deform_record: { 341 | instance: null, 342 | factory: () => { 343 | const dummy: any = { 344 | parser: new Parser() 345 | .bit2("unknown") 346 | .bit2("deform_mode") 347 | .bit3("deform_status") 348 | .bit1("is_deform_protected"), 349 | }; 350 | dummy.parse = (buf: Buffer): any => { 351 | const parsed = dummy.parser.parse(buf); 352 | parsed.deform_status = DEFORM_STATUS[parsed.deform_status] || NO_MATCH; 353 | parsed.deform_mode = DEFORM_MODE[parsed.deform_mode] || NO_MATCH; 354 | return parsed; 355 | }; 356 | 357 | return dummy; 358 | 359 | }, 360 | }, 361 | center_battery_record: { 362 | instance: null, 363 | factory: () => { 364 | return new Parser() 365 | .uint8("relative_capacity") 366 | .uint16le("current_pv", { formatter: (val: any) => val / 1000 }) 367 | .uint16le("current_capacity") 368 | .uint16le("full_capacity") 369 | .uint8("life") 370 | .uint16le("loop_num") 371 | .uint32le("error_type") 372 | .uint16le("current", { formatter: (val: any) => val / 1000 }) 373 | .uint16le("voltage_cel_1", { formatter: (val: any) => val / 1000 }) 374 | .uint16le("voltage_cel_2", { formatter: (val: any) => val / 1000 }) 375 | .uint16le("voltage_cel_3", { formatter: (val: any) => val / 1000 }) 376 | .uint16le("voltage_cel_4", { formatter: (val: any) => val / 1000 }) 377 | .uint16le("voltage_cel_5", { formatter: (val: any) => val / 1000 }) 378 | .uint16le("voltage_cel_6", { formatter: (val: any) => val / 1000 }) 379 | .uint16le("serial_no") 380 | .uint16le("product_date", { 381 | formatter: (val: any) => { 382 | return { 383 | year: ((val & 0xfe00) >> 9) + 1980, 384 | month: (val & 0x01e0) >> 5, 385 | day: val & 0x001f, 386 | }; 387 | }, 388 | }) 389 | .uint16le("temperature", { formatter: (val: any) => val / 100 }) 390 | .uint8("conn_status"); 391 | }, 392 | }, 393 | smart_battery_record: { 394 | instance: null, 395 | factory: () => { 396 | const dummy: any = { 397 | parser: new Parser() 398 | .uint16le("useful_time") 399 | .uint16le("go_home_time") 400 | .uint16le("land_time") 401 | .uint16le("go_home_battery") 402 | .uint16le("landing_battery") 403 | .uint32le("safe_fly_radius") 404 | .floatle("volume_console") 405 | .uint32le("status") 406 | .uint8("go_home_status") 407 | .uint8("go_home_countdown") 408 | .uint16le("voltage", { formatter: (val: any) => val / 1000 }) 409 | .uint8("battery") 410 | .bit1("low_warning_go_home") 411 | .bit7("low_warning") 412 | .bit1("serious_low_warning_landing") 413 | .bit7("serious_low_warning") 414 | .uint8("voltage_percent"), 415 | }; 416 | dummy.parse = (buf: Buffer): any => { 417 | const parsed = dummy.parser.parse(buf); 418 | parsed.status = SMART_BATTERY_STATUS[parsed.status] || NO_MATCH; 419 | parsed.go_home_status = SMART_BATTERY_GO_HOME_STATUS[parsed.go_home_status] || NO_MATCH; 420 | return parsed; 421 | }; 422 | 423 | return dummy; 424 | 425 | }, 426 | }, 427 | app_tip_record: { 428 | instance: null, 429 | factory: () => { 430 | return new Parser().string("tip", { zeroTerminated: true }); 431 | }, 432 | }, 433 | app_warn_record: { 434 | instance: null, 435 | factory: () => { 436 | return new Parser().string("warn", { zeroTerminated: true }); 437 | }, 438 | }, 439 | recover_record: { 440 | instance: null, 441 | factory: () => { 442 | // todo: implement versioning for this record 443 | const dummy: any = { 444 | parser: new Parser() 445 | .uint8("drone_type") 446 | .uint8("app_type") 447 | .buffer("app_version", { length: 3 }) 448 | .string("aircraft_sn", { length: 10 }) 449 | .string("aircraft_name", { length: 24 }) 450 | .skip(22) 451 | .string("camera_sn", { length: 10 }) 452 | .string("rc_sn", { length: 10 }) 453 | .string("battery_sn", { length: 10 }), 454 | }; 455 | 456 | dummy.parse = (buf: Buffer): any => { 457 | const parsed = dummy.parser.parse(buf); 458 | parsed.app_type = RECOVERY_APP_TYPE[parsed.app_type] || NO_MATCH; 459 | parsed.drone_type = RECOVERY_DRONE_TYPE[parsed.drone_type] || NO_MATCH; 460 | const appVer = parsed.app_version; 461 | parsed.app_version = `${appVer[2]}.${appVer[1]}.${appVer[0]}`; 462 | return parsed; 463 | }; 464 | 465 | return dummy; 466 | }, 467 | }, 468 | app_gps_record: { 469 | instance: null, 470 | factory: () => { 471 | return new Parser() 472 | .doublele("latitude", { formatter: radiants2degree }) 473 | .doublele("longitude", { formatter: radiants2degree }) 474 | .floatle("accuracy"); 475 | }, 476 | }, 477 | firmware_record: { 478 | instance: null, 479 | factory: () => { 480 | const dummy: any = { 481 | parser: new Parser() 482 | .skip(2) 483 | .buffer("version", { length: 3 }) 484 | .skip(109) 485 | }; 486 | dummy.parse = (buf: Buffer): any => { 487 | const parsed = dummy.parser.parse(buf); 488 | const version = parsed.version; 489 | parsed.version = `${version[2]}.${version[1]}.${version[0]}`; 490 | return parsed; 491 | }; 492 | return dummy; 493 | }, 494 | }, 495 | }; 496 | -------------------------------------------------------------------------------- /src/services/CacheTransformService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceTypes } from "../common/ServiceManager"; 2 | import BaseService from "./BaseService"; 3 | import { BinaryParserService } from "./BinaryParserService"; 4 | import { IRecord, IRecordCache, IRowObject } from "../shared/interfaces"; 5 | import { FileParsingService } from "./FileParsingService"; 6 | import { RecordTypes } from "./RecordTypes"; 7 | import { ScrambleTableService } from "./ScrambleTableService"; 8 | 9 | export class CacheTransformService extends BaseService { 10 | 11 | public unscramble(recordsCache: IRecordCache): void { 12 | const scrambleTableService = this.serviceMan.get_service( 13 | ServiceTypes.ScrambleTable, 14 | ); 15 | 16 | recordsCache.records = recordsCache.records.map((rec) => scrambleTableService.unscramble_record(rec)); 17 | } 18 | 19 | public cache_as_rows(recordsCache: IRecordCache): IRecord[][] { 20 | const parserService = this.serviceMan.get_service(ServiceTypes.Parsers); 21 | 22 | const rows: IRecord[][] = []; 23 | const records = recordsCache.records; 24 | 25 | let consumed = 0; 26 | let row: IRecord[] = []; 27 | 28 | while (consumed < records.length) { 29 | 30 | const record = records[consumed]; 31 | 32 | // ignore the records for which we don't know the format 33 | if (parserService.parser_record_mapping(record.type) === null) { 34 | consumed++; 35 | continue; 36 | } 37 | 38 | // we create a row for each OSD record type 39 | if (record.type !== RecordTypes.OSD) { 40 | row.push(record); 41 | consumed++; 42 | continue; 43 | } 44 | 45 | if (row.length > 0) { 46 | rows.push(row); 47 | } 48 | 49 | row = [record]; 50 | consumed++; 51 | } 52 | 53 | return rows; 54 | } 55 | 56 | public rows_to_json(rows: IRecord[][]): IRowObject[] { 57 | const fileParsingService = this.serviceMan.get_service( 58 | ServiceTypes.FileParsing, 59 | ); 60 | 61 | const row2json = (row: IRecord[]): IRowObject => { 62 | const newRow: IRowObject = {}; 63 | for (const record of row) { 64 | newRow[RecordTypes[record.type]] = fileParsingService.parse_record_by_type(record, record.type); 65 | } 66 | return newRow; 67 | }; 68 | 69 | return rows.map(row2json); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/services/CsvService.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { IRowObject, IRowHeader } from "../shared/interfaces"; 3 | import BaseService from "./BaseService"; 4 | 5 | 6 | export const RECORD_ORDER: string[] = [ 7 | "CUSTOM", 8 | "OSD", 9 | "HOME", 10 | "GIMBAL", 11 | "RC", 12 | "DEFORM", 13 | "CENTER_BATTERY", 14 | "SMART_BATTERY", 15 | "APP_TIP", 16 | "APP_WARN", 17 | "RC_GPS", 18 | "RC_DEBUG", 19 | "RECOVER", 20 | "APP_GPS", 21 | "FIRMWARE", 22 | "OFDM_DEBUG", 23 | "VISION_GROUP", 24 | "VISION_WARN", 25 | "MC_PARAM", 26 | "APP_OPERATION", 27 | "APP_SER_WARN", 28 | // "JPEG", 29 | "OTHER", 30 | ]; 31 | 32 | export class CsvService extends BaseService { 33 | /** 34 | * Create header rows data for csv production. Each row object contains properties 35 | * that are stored to be able to create a header of the form 'row.property'. 36 | * @param rows Array of rows to extract the header info from. 37 | */ 38 | public getRowHeaders(rows: IRowObject[]): IRowHeader[] { 39 | const presentTypes = new Set(); 40 | const typeProps: { [type: string]: string[] } = {}; 41 | 42 | for (const row of rows) { 43 | for (const type of _.keys(row)) { 44 | presentTypes.add(type); 45 | typeProps[type] = _.keys(row[type]); 46 | } 47 | } 48 | 49 | const headers: IRowHeader[] = []; 50 | for (const type of RECORD_ORDER) { 51 | if (presentTypes.has(type)) { 52 | const props = typeProps[type]; 53 | headers.push({ type, props }); 54 | } 55 | } 56 | 57 | return headers; 58 | } 59 | 60 | /** 61 | * Prints the given rows in `rows` in csv format. 62 | * @param rows Array of rows to print the values of. 63 | * @param headerDef The header definition already extracted from the rows. 64 | */ 65 | public printRowValues(rows: IRowObject[], headerDef: IRowHeader[]): string { 66 | const lines: string[] = []; 67 | for (const datarow of rows) { 68 | const values: string[] = []; 69 | for (const header of headerDef) { 70 | for (const prop of header.props) { 71 | const path = `${header.type}.${prop}`; 72 | if (_.has(datarow, path)) { 73 | values.push(_.get(datarow, path).toString()); 74 | } else { 75 | values.push(""); 76 | } 77 | } 78 | } 79 | lines.push(values.join(",")); 80 | } 81 | return lines.join("\n"); 82 | } 83 | 84 | /** 85 | * Prints the header for the first line of the csv file. 86 | * @param headerDef The header definiton to print. 87 | */ 88 | public createHeader(headerDef: IRowHeader[]): string { 89 | const headers: string[] = []; 90 | for (const header of headerDef) { 91 | for (const prop of header.props) { 92 | headers.push(`${header.type}.${prop}`); 93 | } 94 | } 95 | return headers.join(","); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/services/FileInfoService.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { FileInfoService } from "./FileInfoService"; 4 | import { ServiceManagerMock } from "@tests/ServiceManager.mock"; 5 | 6 | describe("FileInfoService", () => { 7 | const filePath = path.join(__dirname, "../../assets/flight_logs/mavic2/mavic2_0.txt"); 8 | const fileBuff = fs.readFileSync(filePath); 9 | const serviceMan = new ServiceManagerMock(); 10 | const fileInfoService = new FileInfoService(serviceMan); 11 | 12 | describe("File Details", () => { 13 | it("should correctly parse strings without null char", () => { 14 | const stringProps: string[] = [ 15 | "city_part", 16 | "street", 17 | "city", 18 | "area", 19 | "aircraft_name", 20 | "aircraft_sn", 21 | "camera_sn", 22 | "rc_sn", 23 | "battery_sn", 24 | ]; 25 | 26 | const deets = fileInfoService.get_details(fileBuff); 27 | 28 | expect(deets).not.toEqual(null); 29 | 30 | for (const prop of stringProps) { 31 | const val = deets[prop]; 32 | expect(typeof val).toBe("string"); 33 | expect(val).not.toEqual("\u0000"); 34 | } 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/services/FileInfoService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceTypes } from "../common/ServiceManager"; 2 | import BaseService from "./BaseService"; 3 | import { 4 | BinaryParserService, 5 | ParserTypes, 6 | } from "./BinaryParserService"; 7 | import { FileParsingService } from "./FileParsingService"; 8 | import { IHeaderInfo, IRecordStats, IFileInfo 9 | } from "../shared/interfaces"; 10 | 11 | const newHeaderSize = 100; 12 | const oldHeaderSize = 12; 13 | 14 | export class FileInfoService extends BaseService { 15 | 16 | public name: string = "file_info"; 17 | 18 | public get_header_info(buffer: Buffer): IHeaderInfo { 19 | const parserService = this.serviceMan.get_service( 20 | ServiceTypes.Parsers, 21 | ) as BinaryParserService; 22 | const headerParser = parserService.get_parser(ParserTypes.Header); 23 | 24 | // get first 100 bytes and parse them. 25 | const header = headerParser.parse(buffer); 26 | let headerSize; 27 | if (header.file_version.ver[2] < 6) { 28 | headerSize = oldHeaderSize; 29 | } else { 30 | headerSize = newHeaderSize; 31 | } 32 | 33 | // calculate details 34 | const fileSize = buffer.length; 35 | const headerRecordsAreaSize = header.header_record_size_lo; 36 | const recordsAreaSize = headerRecordsAreaSize - headerSize; 37 | const detailsAreaSize = fileSize - headerRecordsAreaSize; 38 | 39 | // create version string 40 | const version = header.file_version; 41 | return { 42 | file_size: fileSize, 43 | header_size: headerSize, 44 | records_size: recordsAreaSize, 45 | details_size: detailsAreaSize, 46 | version, 47 | }; 48 | } 49 | 50 | public get_records_info(buffer: Buffer): IRecordStats { 51 | const fileParsingService = this.serviceMan.get_service( 52 | ServiceTypes.FileParsing, 53 | ) as FileParsingService; 54 | 55 | return fileParsingService.parse_records(buffer).stats; 56 | } 57 | 58 | public get_file_info(buffer: Buffer): IFileInfo { 59 | const fileParsingService = this.serviceMan.get_service( 60 | ServiceTypes.FileParsing, 61 | ) as FileParsingService; 62 | const headerInfo = this.get_header_info(buffer); 63 | const recordStats = fileParsingService.parse_records(buffer, headerInfo) 64 | .stats; 65 | return { header_info: headerInfo, records_info: recordStats }; 66 | } 67 | 68 | public get_details(buffer: Buffer): any { 69 | const headerInfo = this.get_header_info(buffer); 70 | const detailsStart = headerInfo.header_size + headerInfo.records_size; 71 | const detailsBuf = buffer.slice(detailsStart); 72 | const parserService = this.serviceMan.get_service( 73 | ServiceTypes.Parsers, 74 | ) as BinaryParserService; 75 | const detailsParser = parserService.get_parser(ParserTypes.Details); 76 | const details = detailsParser.parse(detailsBuf); 77 | return details; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/services/FileParsingService.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { ServiceTypes } from "../common/ServiceManager"; 3 | import { Version } from "../common/Version"; 4 | import BaseService from "./BaseService"; 5 | import { BinaryParserService, ParserTypes } from "./BinaryParserService"; 6 | import { FileInfoService } from "./FileInfoService"; 7 | import { RecordTypes } from "./RecordTypes"; 8 | import { IHeaderInfo, IRecord, IRecordCache } from "../shared/interfaces"; 9 | 10 | 11 | 12 | function is_jpeg_soi(buffer: Buffer, offset: number): boolean { 13 | return ( 14 | buffer.readUInt8(offset) === 0xff && buffer.readUInt8(offset + 1) === 0xd8 15 | ); 16 | } 17 | 18 | function is_jpeg_eoi(buffer: Buffer, offset: number): boolean { 19 | return ( 20 | buffer.readUInt8(offset) === 0xff && buffer.readUInt8(offset + 1) === 0xd9 21 | ); 22 | } 23 | 24 | export class FileParsingService extends BaseService { 25 | public name: string = "file_parsing"; 26 | 27 | /** 28 | * Parses the 29 | * @param buffer File buffer to parse. 30 | * @param headerInfo The parsed values from the header part of the file. 31 | */ 32 | public parse_records( 33 | buffer: Buffer, 34 | headerInfo?: IHeaderInfo, 35 | ): IRecordCache { 36 | if (headerInfo === undefined) { 37 | const fileInfoService = this.serviceMan.get_service( 38 | ServiceTypes.FileInfo, 39 | ) as FileInfoService; 40 | headerInfo = fileInfoService.get_header_info(buffer); 41 | } 42 | 43 | const recordsBuff = buffer.slice(100); 44 | const limit = headerInfo.records_size; 45 | const records = this.get_record_cache(recordsBuff, limit, headerInfo.version); 46 | return records; 47 | } 48 | 49 | public filter_records(records: IRecordCache, type: RecordTypes): IRecord[] { 50 | return records.records.filter((val) => val.type === type); 51 | } 52 | 53 | public createEmptyCache(): IRecordCache { 54 | const version = Version.CreateEmpty(); 55 | 56 | return { 57 | records: [], 58 | version, 59 | isEmpty: true, 60 | stats: { 61 | records_area_size: 0, 62 | record_count: 0, 63 | type_count: {}, 64 | invalid_records: 0, 65 | }, 66 | }; 67 | } 68 | 69 | public parse_record_by_type( 70 | record: IRecord, 71 | recordType: RecordTypes, 72 | ): any { 73 | const parserService = this.serviceMan.get_service(ServiceTypes.Parsers); 74 | try { 75 | return parserService.get_record_parser(recordType).parse(record.data[0]); 76 | } catch (e) { 77 | console.log(`Record type ${RecordTypes[recordType]} had error parsing`); 78 | throw e; 79 | } 80 | } 81 | 82 | private get_record_cache(buffer: Buffer, limit: number, version: Version): IRecordCache { 83 | const parserService = this.serviceMan.get_service(ServiceTypes.Parsers); 84 | const recordParser = parserService.get_parser(ParserTypes.BaseRecord); 85 | const recordStartParser = parserService.get_parser( 86 | ParserTypes.StartRecord, 87 | ); 88 | 89 | const recordCache: IRecordCache = { 90 | records: [], 91 | version, 92 | stats: { 93 | records_area_size: buffer.length, 94 | record_count: 0, 95 | type_count: {}, 96 | invalid_records: 0, 97 | }, 98 | }; 99 | 100 | let start = 0; 101 | while (start < limit) { 102 | const recStart = recordStartParser.parse(buffer.slice(start)); 103 | 104 | let record: IRecord | null; 105 | if (recStart.type === RecordTypes.JPEG) { 106 | // check for starting zeros 107 | const zeroWatermarkLo = buffer.readUInt8(start + 2); 108 | const zeroWatermarkHi = buffer.readUInt8(start + 3); 109 | 110 | if ((zeroWatermarkHi | zeroWatermarkLo) !== 0) { 111 | throw Error("No zero watermark while parsing jpeg record"); 112 | } 113 | 114 | if (is_jpeg_soi(buffer, start + 4)) { 115 | // handle jpeg record with jpegs in them 116 | const jpegs = []; 117 | let startOfJpeg = start + 4; 118 | let endOfJpeg = startOfJpeg; 119 | 120 | while (is_jpeg_soi(buffer, startOfJpeg)) { 121 | endOfJpeg = this.getJpegEoiIndex(buffer, startOfJpeg + 2); 122 | 123 | if (endOfJpeg === -1) { 124 | throw new Error("No JPEG_EOI found after JPEG_SOI"); 125 | } 126 | 127 | jpegs.push(buffer.slice(startOfJpeg, endOfJpeg)); 128 | startOfJpeg = endOfJpeg; 129 | } 130 | 131 | record = { 132 | type: RecordTypes.JPEG, 133 | length: recStart.length, 134 | data: jpegs, 135 | }; 136 | 137 | start = endOfJpeg; 138 | } else { 139 | // handle an empty jpeg record 140 | record = { 141 | type: RecordTypes.JPEG, 142 | length: recStart.length, 143 | data: [Buffer.alloc(0)], 144 | }; 145 | start += 4; 146 | } 147 | } else { 148 | const parsedRecord = recordParser.parse(buffer.slice(start)); 149 | record = { 150 | length: parsedRecord.length, 151 | type: parsedRecord.type, 152 | data: [parsedRecord.data], 153 | }; 154 | start += record.length + 3; 155 | } 156 | 157 | if (record !== null) { 158 | this.addRecordToCache(recordCache, record); 159 | } 160 | } 161 | 162 | return recordCache; 163 | } 164 | 165 | private getJpegEoiIndex(buffer: Buffer, index: number): number { 166 | let iter = index; 167 | while (iter < buffer.length - 1) { 168 | if (is_jpeg_eoi(buffer, iter)) { 169 | return iter + 2; 170 | } 171 | iter += 1; 172 | } 173 | return -1; 174 | } 175 | 176 | private addRecordToCache(cache: IRecordCache, record: any) { 177 | cache.records.push(record); 178 | cache.stats.record_count += 1; 179 | 180 | if (record.type !== RecordTypes.JPEG && record.marker !== 0xff) { 181 | cache.stats.invalid_records += 1; 182 | return; 183 | } 184 | 185 | if (cache.stats.type_count[record.type] === null) { 186 | cache.stats.type_count[record.type] = 1; 187 | return; 188 | } 189 | 190 | cache.stats.type_count[record.type] += 1; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/services/InterpretationTable.ts: -------------------------------------------------------------------------------- 1 | 2 | export const OSD_RECORD_FLYCSTATE = { 3 | 0: "Manual", 4 | 1: "Atti", 5 | 2: "Atti_CL", 6 | 3: "Atti_Hover", 7 | 4: "Hover", 8 | 5: "GPS_Blake", 9 | 6: "GPS_Atti", 10 | 7: "GPS_CL", 11 | 8: "GPS_HomeLock", 12 | 9: "GPS_HotPoint", 13 | 10: "AssistedTakeoff", 14 | 11: "AutoTakeoff", 15 | 12: "AutoLanding", 16 | 13: "AttiLanding", 17 | 14: "NaviGo", 18 | 15: "GoHome", 19 | 16: "ClickGo", 20 | 17: "Joystick", 21 | 18: "GPS_Atti_Wristband", 22 | 19: "Cinematic", 23 | 23: "Atti_Limited", 24 | 24: "GPS_Atti_Limited", 25 | 25: "NaviMissionFollow", 26 | 26: "NaviSubMode_Tracking", 27 | 27: "NaviSubMode_Pointing", 28 | 28: "PANO", 29 | 29: "Farming", 30 | 30: "FPV", 31 | 31: "Sport", 32 | 32: "Novice", 33 | 33: "ForceLanding", 34 | 35: "TerrainTracking", 35 | 36: "NaviAdvGoHome", 36 | 37: "NaviAdvLanding", 37 | 38: "TripodGPS", 38 | 39: "TrackHeadlock", 39 | 41: "EngineStart", 40 | 43: "GentleGPS", 41 | }; 42 | 43 | export const OSD_RECORD_FLYCCOMMAND = { 44 | 1: "AutoFly", 45 | 2: "AutoLanding", 46 | 3: "HomePointNow", 47 | 4: "HomePointHot", 48 | 5: "HomePointLock", 49 | 6: "GoHome", 50 | 7: "StartMotor", 51 | 8: "StopMotor", 52 | 9: "Calibration", 53 | 10: "DeformProtecClose", 54 | 11: "DeformProtecOpen", 55 | 12: "DropGoHome", 56 | 13: "DropTakeOff", 57 | 14: "DropLanding", 58 | 15: "DynamicHomePointOpen", 59 | 16: "DynamicHomePointClose", 60 | 17: "FollowFunctionOpen", 61 | 18: "FollowFunctionClose", 62 | 19: "IOCOpen", 63 | 20: "IOCClose", 64 | 21: "DropCalibration", 65 | 22: "PackMode", 66 | 23: "UnPackMode", 67 | 24: "EnterManualMode", 68 | 25: "StopDeform", 69 | 28: "DownDeform", 70 | 29: "UpDeform", 71 | 30: "ForceLanding", 72 | 31: "ForceLanding2", 73 | }; 74 | 75 | export const OSD_RECORD_GROUND_OR_SKY = { 76 | 0: "Ground", 77 | 1: "Ground", 78 | 2: "Sky", 79 | 3: "Sky", 80 | }; 81 | 82 | export const OSD_RECORD_GO_HOME_STATUS = { 83 | 0: "Standby", 84 | 1: "Preascending", 85 | 2: "Align", 86 | 3: "Ascending", 87 | 4: "Cruise", 88 | 5: "Braking", 89 | 6: "Bypassing", 90 | }; 91 | 92 | export const OSD_RECORD_BATTERY_TYPE = { 93 | 1: "Non Smart", 94 | 2: "Smart", 95 | }; 96 | export const OSD_RECORD_FLIGHT_ACTION = { 97 | 0: "None", 98 | 1: "Warning Power Go Home", 99 | 2: "Warning Power Landing", 100 | 3: "Smart Power Go Home", 101 | 4: "Smart Power Landing", 102 | 5: "Low Voltage Landing", 103 | 6: "Low Voltage GoHome", 104 | 7: "Serious Low Voltage Landing", 105 | 8: "RC_Onekey Go Home", 106 | 9: "RC_Assistant Takeoff", 107 | 10: "RC_Auto Takeoff", 108 | 11: "RC_Auto Landing", 109 | 12: "AppAuto Go Home", 110 | 13: "AppAuto Landing", 111 | 14: "AppAuto Takeoff", 112 | 15: "Out Of Control Go Home", 113 | 16: "Api Auto Takeoff", 114 | 17: "Api Auto Landing", 115 | 18: "Api Auto GoHome", 116 | 19: "Avoid Ground Landing", 117 | 20: "Airport Avoid Landing", 118 | 21: "Too Close Go Home Landing", 119 | 22: "Too Far Go Home Landing", 120 | 23: "App_WP_Mission", 121 | 24: "WP_Auto Takeoff", 122 | 25: "Go Home Avoid", 123 | 26: "GoHome Finish", 124 | 27: "Vert Low Limit Landing", 125 | 28: "Battery Force Landing", 126 | 29: "MC_ProtectGoHome", 127 | 30: "Motor block Landing", 128 | 31: "App Request Force Landing", 129 | 32: "Fake Battery Landing", 130 | 33: "RTH_ComingObstacleLanding", 131 | 34: "IMU Error RTH", 132 | }; 133 | 134 | export const OSD_RECORD_MOTOR_START_FAILED_CAUSE = { 135 | 0: "None", 136 | 1: "CompassError", 137 | 2: "AssistantProtected", 138 | 3: "DeviceLocked", 139 | 4: "DistanceLimit", 140 | 5: "IMUNeedCalibration", 141 | 6: "IMUSNError", 142 | 7: "IMUWarning", 143 | 8: "CompassCalibrating", 144 | 9: "AttiError", 145 | 10: "NoviceProtected", 146 | 11: "BatteryCellError", 147 | 12: "BatteryCommuniteError", 148 | 13: "SeriousLowVoltage", 149 | 14: "SeriousLowPower", 150 | 15: "LowVoltage", 151 | 16: "TempureVolLow", 152 | 17: "SmartLowToLand", 153 | 18: "BatteryNotReady", 154 | 19: "SimulatorMode", 155 | 20: "PackMode", 156 | 21: "AttitudeAbnormal", 157 | 22: "UnActive", 158 | 23: "FlyForbiddenError", 159 | 24: "BiasError", 160 | 25: "EscError", 161 | 26: "ImuInitError", 162 | 27: "SystemUpgrade", 163 | 28: "SimulatorStarted", 164 | 29: "ImuingError", 165 | 30: "AttiAngleOver", 166 | 31: "GyroscopeError", 167 | 32: "AcceleratorError", 168 | 33: "CompassFailed", 169 | 34: "BarometerError", 170 | 35: "BarometerNegative", 171 | 36: "CompassBig", 172 | 37: "GyroscopeBiasBig", 173 | 38: "AcceleratorBiasBig", 174 | 39: "CompassNoiseBig", 175 | 40: "BarometerNoiseBig", 176 | 41: "InvalidSn", 177 | 44: "FlashOperating", 178 | 45: "GPSdisconnect", 179 | 47: "SDCardException", 180 | 61: "IMUNoconnection", 181 | 62: "RCCalibration", 182 | 63: "RCCalibrationException", 183 | 64: "RCCalibrationUnfinished", 184 | 65: "RCCalibrationException2", 185 | 66: "RCCalibrationException3", 186 | 67: "AircraftTypeMismatch", 187 | 68: "FoundUnfinishedModule", 188 | 70: "CyroAbnormal", 189 | 71: "BaroAbnormal", 190 | 72: "CompassAbnormal", 191 | 73: "GPS_Abnormal", 192 | 74: "NS_Abnormal", 193 | 75: "TopologyAbnormal", 194 | 76: "RC_NeedCali", 195 | 77: "InvalidFloat", 196 | 78: "M600_BAT_TOO_LITTLE", 197 | 79: "M600_BAT_AUTH_ERR", 198 | 80: "M600_BAT_COMM_ERR", 199 | 81: "M600_BAT_DIF_VOLT_LARGE_1", 200 | 82: "M600_BAT_DIF_VOLT_LARGE_2", 201 | 83: "InvalidVersion", 202 | 84: "GimbalGyroAbnormal", 203 | 85: "GimbalESC_PitchNonData", 204 | 86: "GimbalESC_RollNonData", 205 | 87: "GimbalESC_YawNonData", 206 | 88: "GimbalFirmwIsUpdating", 207 | 89: "GimbalDisorder", 208 | 90: "GimbalPitchShock", 209 | 91: "GimbalRollShock", 210 | 92: "GimbalYawShock", 211 | 93: "IMUcCalibrationFinished", 212 | 101: "BattVersionError", 213 | 102: "RTK_BadSignal", 214 | 103: "RTK_DeviationError", 215 | 112: "ESC_Calibrating", 216 | 113: "GPS_SignInvalid", 217 | 114: "GimbalIsCalibrating", 218 | 115: "LockByApp", 219 | 116: "StartFlyHeightError", 220 | 117: "ESC_VersionNotMatch", 221 | 118: "IMU_ORI_NotMatch", 222 | 119: "StopByApp", 223 | 120: "CompassIMU_ORI_NotMatch", 224 | 122: "CompassIMU_ORI_NotMatch", 225 | 123: "Battery Over Temperature", 226 | 124: "Battery nstall Error", 227 | 125: "Be Impact", 228 | }; 229 | 230 | export const OSD_RECORD_NON_GPS_CAUSE = { 231 | 0: "Already", 232 | 1: "Forbid", 233 | 2: "Gps Num Not Enough", 234 | 3: "Gps Hdop Large", 235 | 4: "Gps Position NonMatch", 236 | 5: "Speed Error Large", 237 | 6: "Yaw Error Large", 238 | 7: "Compass Error Large", 239 | }; 240 | 241 | export const OSD_RECORD_DRONE_TYPE = { 242 | 1: "Inspire 1", 243 | 2: "P3 Advanced", 244 | 3: "P3 Professional", 245 | 4: "P3 Standard", 246 | 5: "OpenFrame", 247 | 6: "AceOne", 248 | 7: "WKM", 249 | 8: "Naza", 250 | 9: "A2", 251 | 10: "A3", 252 | 11: "P4", 253 | 14: "Matrice 600", 254 | 15: "P3 4K", 255 | 16: "Mavic", 256 | 17: "Inspire 2", 257 | 18: "P4 Professional", 258 | 20: "N3", 259 | 21: "Spark", 260 | 23: "Matrice 600 Pro", 261 | 24: "Mavic Air", 262 | 25: "Matrice 200", 263 | 27: "P4 Advanced", 264 | 28: "Matrice 210", 265 | 29: "P3SE", 266 | 30: "Matrice 210MTK", 267 | }; 268 | 269 | export const OSD_RECORD_IMU_INIT_FAIL_REASON = { 270 | 0: "MonitorError", 271 | 1: "CollectingData", 272 | 3: "AcceDead", 273 | 4: "Compass Dead", 274 | 5: "Barometer Dead", 275 | 6: "Barometer Negative", 276 | 7: "Compass Mod Too Large", 277 | 8: "Gyro Bias Too Large", 278 | 9: "Acce Bias Too Large", 279 | 10: "Compass Noise Too Large", 280 | 11: "Barometer Noise Too Large", 281 | 12: "Waiting McStationary", 282 | 13: "Acce Move Too Large", 283 | 14: "Mc Header Moved", 284 | 15: "Mc Vibrated", 285 | }; 286 | 287 | export const OSD_RECORD_MOTOR_FAIL_REASON = { 288 | 94: "Takeoff Exception", 289 | 95: "ESC_Stall Near Ground", 290 | 96: "ESC_Unbalance On Ground", 291 | 97: "ESC_PART_EMPTY On Ground", 292 | 98: "Engine Start Failed", 293 | 99: "Auto Takeoff LaunchFailed", 294 | 100: "Roll Over On Ground", 295 | }; 296 | 297 | export const OSD_RECORD_CTRL_DEVICE = { 298 | 0: "RC", 299 | 1: "App", 300 | 2: "OnboardDevice", 301 | 3: "Camera", 302 | }; 303 | 304 | export const GIMBAL_MODE = { 305 | 0: "YawNoFollow", 306 | 1: "FPV", 307 | 2: "YawFollow", 308 | }; 309 | 310 | export const SMART_BATTERY_STATUS = { 311 | 0: "None", 312 | }; 313 | 314 | export const SMART_BATTERY_GO_HOME_STATUS = { 315 | 0: "Non Go Home", 316 | 1: "Go Home", 317 | 2: "Go Home Already", 318 | }; 319 | 320 | export const DEFORM_STATUS = { 321 | 1: "FoldComplete", 322 | 2: "Folding", 323 | 3: "StretchComplete", 324 | 4: "Stretching", 325 | 5: "StopDeformation", 326 | }; 327 | 328 | export const DEFORM_MODE = { 329 | 0: "Pack", 330 | 1: "Protect", 331 | 2: "Normal", 332 | }; 333 | 334 | export const HOME_IOC_MODE = { 335 | 1: "Course Lock", 336 | 2: "Home Lock", 337 | 3: "Hotspot Surround", 338 | }; 339 | export const RECOVERY_DRONE_TYPE = { 340 | 1: "Inspire 1", 341 | 2: "P3 Standard", 342 | 3: "P3 Advanced", 343 | 4: "P3 Professional", 344 | 5: "OSMO", 345 | 6: "Matrice 100", 346 | 7: "P4", 347 | 8: "LB2", 348 | 9: "Inspire 1 Pro", 349 | 10: "A3", 350 | 11: "Matrice 600", 351 | 12: "P3 4K", 352 | 13: "Mavic Pro", 353 | 14: "Zenmuse XT", 354 | 15: "Inspire 1 RAW", 355 | 16: "A2", 356 | 17: "Inspire 2", 357 | 18: "OSMO Pro", 358 | 19: "OSMO Raw", 359 | 20: "SMO+", 360 | 21: "Mavic", 361 | 22: "OSMO Mobile", 362 | 23: "OrangeCV600", 363 | 24: "P4 Professional", 364 | 25: "N3 FC", 365 | 26: "Spark", 366 | 27: "Matrice 600 Pro", 367 | 28: "P4 Advanced", 368 | 30: "AG405", 369 | 31: "Matrice 200", 370 | 33: "Matrice 210", 371 | 34: "Matrice 210RTK", 372 | 38: "Mavic Air", 373 | }; 374 | 375 | export const RECOVERY_APP_TYPE = { 376 | 1: "iOS", 377 | 2: "Android", 378 | }; 379 | 380 | export const DETAILS_APP_TYPE = { 381 | 1: "iOS", 382 | 2: "Android", 383 | }; 384 | 385 | export const NO_MATCH = "Other"; 386 | -------------------------------------------------------------------------------- /src/services/RecordTypes.ts: -------------------------------------------------------------------------------- 1 | export enum RecordTypes { 2 | OSD = 0x01, 3 | HOME = 0x02, 4 | GIMBAL = 0x03, 5 | RC = 0x04, 6 | CUSTOM = 0x05, 7 | DEFORM = 0x06, 8 | CENTER_BATTERY = 0x07, 9 | SMART_BATTERY = 0x08, 10 | APP_TIP = 0x09, 11 | APP_WARN = 0x0A, 12 | RC_GPS = 0x0B, 13 | RC_DEBUG = 0x0C, 14 | RECOVER = 0x0D, 15 | APP_GPS = 0x0E, 16 | FIRMWARE = 0x0F, 17 | OFDM_DEBUG = 0x10, 18 | VISION_GROUP = 0x11, 19 | VISION_WARN = 0x12, 20 | MC_PARAM = 0x13, 21 | APP_OPERATION = 0x14, 22 | // What is record type = 0x16,? ##### 23 | APP_SER_WARN = 0x18, 24 | // What is record type = 0x19,? ##### 25 | // What is record type = 0x1a,? ##### 26 | // What is record type = 0x1e,? ##### 27 | // What is record type = 0x28,? ##### 28 | JPEG = 0x39, 29 | OTHER = 0xFE, 30 | } 31 | -------------------------------------------------------------------------------- /src/services/ScrambleTableService.ts: -------------------------------------------------------------------------------- 1 | import BaseService from "./BaseService"; 2 | import { IRecord } from "../shared/interfaces"; 3 | import { RecordTypes } from "./RecordTypes"; 4 | import { scrambleTable } from "./ScrambleTable"; 5 | export class ScrambleTableService extends BaseService { 6 | private _scrambleTable: number[][] = []; 7 | 8 | public get scrambleTable() { 9 | if (this._scrambleTable.length === 0) { 10 | this._scrambleTable = scrambleTable; 11 | } 12 | return this._scrambleTable; 13 | } 14 | 15 | public unscramble_record(record: IRecord): IRecord { 16 | let data: Buffer[]; 17 | 18 | if (record.type === RecordTypes.JPEG) { 19 | data = record.data; 20 | } else { 21 | const uscrm = this.unscramble_buffer(record.data[0], record.type); 22 | if (uscrm == null) { 23 | data = record.data; 24 | } else { 25 | data = uscrm; 26 | } 27 | } 28 | 29 | return { 30 | type: record.type, 31 | length: record.length, 32 | data, 33 | }; 34 | } 35 | 36 | private unscramble_buffer(buf: Buffer, type: RecordTypes): Buffer[] | null { 37 | const firstByte = buf.readUInt8(0); 38 | const scrambleKey = ((type - 1) << 8) | firstByte; 39 | const scrambleBytes = this.scrambleTable[scrambleKey]; 40 | 41 | if (scrambleBytes === undefined) { 42 | return null; 43 | } 44 | 45 | const unscrambledBuf = Buffer.alloc(buf.length - 1); 46 | 47 | for (let writeOffset = 1; writeOffset < buf.length; writeOffset++) { 48 | const scrambleEntry = scrambleBytes[(writeOffset - 1) % 8]; 49 | const val = buf.readUInt8(writeOffset) ^ scrambleEntry; 50 | unscrambledBuf.writeUInt8(val, writeOffset - 1); 51 | } 52 | 53 | return [unscrambledBuf]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Version } from "../common/Version"; 2 | import { RecordTypes } from "../services/RecordTypes"; 3 | 4 | export interface IFile { 5 | path: string; 6 | buffer: Buffer | null; 7 | } 8 | 9 | export interface ILazyLoadingEntry { 10 | instance: t | null; 11 | factory: (options?: any) => t; 12 | } 13 | 14 | export interface IRowObject { 15 | [type: string]: any; 16 | } 17 | 18 | export interface IRowHeader { 19 | type: string; 20 | props: string[]; 21 | } 22 | 23 | export interface IHeaderInfo { 24 | file_size: number; 25 | header_size: number; 26 | records_size: number; 27 | details_size: number; 28 | version: Version; 29 | } 30 | 31 | export interface IFileInfo { 32 | header_info: IHeaderInfo; 33 | records_info: IRecordStats; 34 | } 35 | 36 | export interface IRecord { 37 | type: RecordTypes; 38 | length: number; 39 | data: Buffer[]; 40 | } 41 | 42 | export interface IRecordStats { 43 | records_area_size: number; 44 | record_count: number; 45 | type_count: { [type: number]: number }; 46 | invalid_records: number; 47 | } 48 | 49 | export interface IRecordCache { 50 | records: IRecord[]; 51 | version: Version; 52 | stats: IRecordStats; 53 | isEmpty?: boolean; 54 | } 55 | -------------------------------------------------------------------------------- /src/template/kml-template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 19 | 20 | Home 21 | #home 22 | 23 | 24 | absolute 25 | <%=homeCoordinates%> 26 | 27 | 28 | 29 | #copterLineStyle 30 | 31 | absolute 32 | <%=coordinates%> 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/ReadFileCommand.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { ReadFileCommand } from "../src/commands"; 4 | import { ServiceManagerMock } from "./ServiceManager.mock"; 5 | 6 | describe("ReadFileCommand", () => { 7 | const serviceMan = new ServiceManagerMock(); 8 | const cmd = new ReadFileCommand(serviceMan); 9 | 10 | it("should return empty array given an empty array of filepaths", () => { 11 | const filepaths: string[] = []; 12 | const files = cmd.exec(filepaths); 13 | 14 | expect(files.length).toEqual(0); 15 | }); 16 | 17 | it("should have file.buffer set as null when file not found", () => { 18 | const filepaths: string[] = ["asdasdasda"]; 19 | const files = cmd.exec(filepaths); 20 | 21 | expect(files.length).toEqual(1); 22 | expect(files[0].buffer).toBeNull(); 23 | }); 24 | 25 | it("should return an IFile obj when a file is found", () => { 26 | const testFile = path.join(__dirname, "../assets/flight_logs/mavic2/mavic2_0.txt"); 27 | const filepaths: string[] = [ testFile ]; 28 | const files = cmd.exec(filepaths); 29 | 30 | expect(files.length).toEqual(1); 31 | expect(files[0].path).toEqual(testFile); 32 | expect(files[0].buffer).not.toEqual(null); 33 | 34 | const stats = fs.statSync(testFile); 35 | if (files[0].buffer !== null) { 36 | expect(files[0].buffer.length).toEqual(stats.size); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/ServiceManager.mock.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from "../src/common/ServiceManager"; 2 | 3 | export class ServiceManagerMock extends ServiceManager { 4 | 5 | } -------------------------------------------------------------------------------- /tests/file_version.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { parse_file, IRowObject, get_jpegs, IRecord } from "../src/node-djiparsetxt"; 4 | import { RecordTypes } from "../src/services/RecordTypes"; 5 | 6 | const junk = (item: string) => !(/(^|\/)\.[^\/\.]/g).test(item); 7 | 8 | describe("Mavic 2 Log Tests", () => { 9 | const filesDir = path.join(__dirname, "../assets/flight_logs/mavic2"); 10 | createTestFromDir(filesDir); 11 | }); 12 | 13 | describe("Phantom 3 Log Tests", () => { 14 | const filesDir = path.join(__dirname, "../assets/flight_logs/phantom3"); 15 | createTestFromDir(filesDir); 16 | }); 17 | 18 | describe("Phantom 4 Log Tests", () => { 19 | const filesDir = path.join(__dirname, "../assets/flight_logs/phantom4"); 20 | createTestFromDir(filesDir); 21 | }); 22 | 23 | describe("Spark Log Tests", () => { 24 | const filesDir = path.join(__dirname, "../assets/flight_logs/spark"); 25 | createTestFromDir(filesDir); 26 | }); 27 | 28 | function filterFromKeys(row: IRowObject, keys: string[]): IRowObject { 29 | const filtered: IRowObject = { filterCount: 0 }; 30 | 31 | if (keys.length == 0) return filtered; 32 | 33 | for (let key of keys) { 34 | if (key in row) { 35 | filtered[key] = row[key]; 36 | filtered.filterCount += 1; 37 | } 38 | } 39 | 40 | return filtered; 41 | } 42 | 43 | function createTestFromDir(filePath: string) { 44 | const fileList = fs.readdirSync(filePath).filter(junk); 45 | 46 | for (let fileName of fileList) { 47 | describe(fileName, () => { 48 | // parse file for this tests 49 | const completeFileName = path.join(filePath, fileName); 50 | const buffer = fs.readFileSync(completeFileName); 51 | const rows = parse_file(buffer); 52 | 53 | it(`should parse file '${fileName}'`, () => { 54 | expect(Array.isArray(rows)).toBe(true); 55 | expect(rows.length).toBeGreaterThan(0); 56 | for (const row of rows) { 57 | expect(row).toHaveProperty("OSD"); 58 | } 59 | }); 60 | 61 | it('should remove the intial garbage created by the APP_GPS data (remove [0, 0] intial coord)', () => { 62 | for (let row of rows) { 63 | const key = 'APP_GPS'; 64 | if (key in row) { 65 | const gps = row[key]; 66 | expect(Math.abs(gps.latitude)).toBeGreaterThan(0.0); 67 | expect(Math.abs(gps.longitude)).toBeGreaterThan(0.0); 68 | } 69 | } 70 | }); 71 | 72 | it('should not have values for gps that are exactly 0 in OSD record if filter provided', () => { 73 | const filter = (val: IRowObject) => val['OSD'].longitude > 0.0 && val['OSD'].latitude > 0.0; 74 | const parsedRows = parse_file(buffer, filter); 75 | for (let row of parsedRows) { 76 | const key = 'OSD'; 77 | if (key in row) { 78 | const gps = row[key]; 79 | expect(Math.abs(gps.latitude)).toBeGreaterThan(0.0); 80 | expect(Math.abs(gps.longitude)).toBeGreaterThan(0.0); 81 | } 82 | } 83 | }); 84 | 85 | it('should not parse zero byte images', () => { 86 | const jpegs = get_jpegs(buffer); 87 | for (let jpeg of jpegs) { 88 | expect(jpeg.length).toBeGreaterThan(0); 89 | } 90 | }); 91 | }); 92 | } 93 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "sourceMap": true, 7 | "outDir": "dist", 8 | "lib": ["es2017", "es7", "es6", "dom"], 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "suppressImplicitAnyIndexErrors": true, 13 | "baseUrl": "./", 14 | "paths": { 15 | "@tests/*": ["tests/*"] 16 | } 17 | }, 18 | "files": [ 19 | "src/node-djiparsetxt.ts" 20 | ], 21 | "include": [ 22 | "src/*", 23 | "src/common/ServiceManager.ts", 24 | "src/common/CommandManager.ts", 25 | "src/common/CliArguments.ts" 26 | ], 27 | "exclude": [ 28 | "tests/*", 29 | "*.spec.ts", 30 | "node_modules", 31 | "dist" 32 | ] 33 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "indent": [true, "tabs"], 9 | "no-console": [false], 10 | "object-literal-sort-keys": false, 11 | "no-bitwise": false, 12 | "variable-name": { 13 | "options": [ 14 | "ban-keywords", 15 | "check-format", 16 | "allow-leading-underscore" 17 | ] 18 | } 19 | }, 20 | "rulesDirectory": [] 21 | } --------------------------------------------------------------------------------