You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
a little script that converts XCSoar's and FLARM's ".igc" flight logs into Tacview's ".acmi" format.
2
+
A set of tools to convert [XCSoar](https://github.com/XCSoar/XCSoar#readme)'s, [FLARM](https://flarm.com/)'s and multiple other vendors' `.igc` GPS flight logs into [Tacview](https://www.tacview.net/)'s `.acmi` format.
3
+
4
+
## The Goal
5
+
`.igc` is simple format, useful for recording the flight of a single aircraft, but has its limits.
6
+
7
+
`.klm`, which can be parsed from `.igc` using other [online utilities](http://cunimb.net/igc2kml.php), may be more visually appealing when viewed in Google Earth, but still isn't as useful for flight debriefing as I'd like to.
8
+
9
+
For these reasons, I decided to develop igc2acmi, it converts one or more `.igc` logs into [Tacview](https://www.tacview.net/)'s `.acmi` flight recording format, which is specifically designed to efficiently store multiple GPS tracks, its software also provides very useful debriefing features such as:
10
+
- customizable labels to display AMSL/AGL Altitude, Vertical Speed, Calibrated Air Speed, True Airspeed, etc.
11
+
- dynamic distance indication between 2 aircraft
12
+
- custom [terrain textures](https://www.tacview.net/documentation/terrain/en/) and [3D shapes](https://www.tacview.net/documentation/staticobjects/en/), which can be used to display aviation charts and airspace models:
13
+

14
+
- online debriefs, where multiple people can observe a flight from a shared perspective
15
+
16
+
## The Programs
17
+
the `ProgramName.exe``-h`/`--help` command will explain the different command-line arguments of each program.
18
+
19
+
### igc2acmi
20
+
converts a single `.igc` file into `.acmi`, useful when you want to convert a specific file using specific settings.
21
+
22
+
### convert-all-igcs
23
+
converts all `.igc` files in the program's directory (or a given input path) into an equal number of `.acmi` files. Useful when you need to convert multiple flights at once, all with the same settings.
24
+
25
+
### combine-igcs
26
+
arguably the most complex utility of the 3, it finds all `.igc` files in the program's directory (or a given input path), sorts them by date of flight and combines all flights started on the same date into a single `.acmi` file, the timeline starts with the first GPS fix of the first flight and ends with the last fix of the last one.
27
+
28
+
Aeroclubs may find the greatest utility in this feature, for example by collecting everyone's `.igc` (with pilot consent of course) every day and combining them, to store/archive flights for collective debriefs and/or longer-term analysis of pilot activity.
29
+
30
+
## Known limitations
31
+
Until now, I have only tested my project on `.igc` logs recorded by XCSoar (my own) and FLARM (a collegue's). All of the FLARM logs had an invalid date field of "000000" in their header, which the programs are not capable of handling (yet).
32
+
33
+
Maybe that has been resolved by newer FLARM versions, or was caused by incorrect setup of the FLARM module. In the meantime, make sure your `.igc` files have a valid date in their "HFDTE" line, using the "DDMMYY" format (i.e: "HFDTE221121" for 22nd November 2021).
34
+
35
+
## Sources and How to Contribute
36
+
Development of this project was significantly aided by available documentation for the [igc](https://xp-soaring.github.io/igc_file_format/igc_format_2008.html) and [acmi](https://www.tacview.net/documentation/acmi/en/) file formats, many thanks to their authors.
37
+
38
+
The [released executables](https://github.com/RUSHER0600/igc2acmi/releases) are compiled with [pyinstaller](https://www.pyinstaller.org/). I use a batch file at the project root with 3 lines like this (last argument differs) to build all programs:
Feel free to fork this project for your own uses, any Pull Requests back to the original repository would be greatly appreciated!
44
+
45
+
## To-Do List
46
+
-[ ] Add `--version` command that displays version number (and maybe release date)
47
+
-[ ] Implement parsing of `igc`[tasks](https://xp-soaring.github.io/igc_file_format/igc_format_2008.html#link_3.6) and [events](https://xp-soaring.github.io/igc_file_format/igc_format_2008.html#link_4.2) into `acmi`[events](https://www.tacview.net/documentation/acmi/en/#Events)
48
+
-[ ] Improve and augment error handling, i.e: for flights with missing/invalid date fields in their header
49
+
-[ ] Add a way for the user to determine the output file's name or name template
deflistIGCsByDate(path, igcs): # builds a dict of igc files, the key is the date of flight, the value is a list containing all igc filepaths recorded on that date
6
+
ret= {}
7
+
forigcinigcs:
8
+
date=fnc.getFlightDate(path+igc+".igc")
9
+
ifdateinret:
10
+
ret[date].append(path+igc+".igc")
11
+
else:
12
+
ret[date] = [path+igc+".igc"]
13
+
returnret
14
+
15
+
defgetLowestRefTime(igcs): # from a list of igc filepaths, find the one with the lowest first fix time and return that time as a time object
16
+
lowest=dt.time(23, 59, 59)
17
+
18
+
forigcpathinigcs:
19
+
withopen(igcpath) asigc:
20
+
forlineinigc:
21
+
ifline.startswith("B"):
22
+
fixTime=dt.time(
23
+
int(line[1:3]),
24
+
int(line[3:5]),
25
+
int(line[5:7])
26
+
)
27
+
break
28
+
iffixTime<lowest:
29
+
lowest=fixTime
30
+
31
+
returnlowest
32
+
33
+
defgetLastFix(acs): # returns the highest occuring "lastfix" value among all aircraft dicts
34
+
lastfix=0
35
+
foridinacs:
36
+
ifacs[id]["lastfix"] >lastfix:
37
+
lastfix=acs[id]["lastfix"]
38
+
39
+
returnlastfix
40
+
41
+
defgetFixes(filename, refTime): # returns a list of fix dictionaries: key is the time offset from reference time in seconds, values are lat/lon/alt
42
+
ret= {}
43
+
withopen(filename) asigc:
44
+
forlineinigc:
45
+
ifline.startswith("B"):
46
+
fix=fnc.parseFixLocation(line)
47
+
fixTime=fnc.parseFixTime(line)
48
+
ret[fnc.timeDiff(refTime, fixTime)] = {
49
+
"lat": fix[0],
50
+
"lon": fix[1],
51
+
"alt": fix[2]
52
+
}
53
+
returnret
54
+
55
+
defbuildAircraft(igcs, refTime): # builds the main data dict of aircraft, with one element for each IGC file read.
56
+
i=0
57
+
ac= {}
58
+
59
+
forigcPathinigcs:
60
+
i+=1
61
+
fixes=getFixes(igcPath, refTime)
62
+
ac[1000+i] = {
63
+
"header": fnc.getFlightHeader(igcPath),
64
+
"fixes": fixes,
65
+
"lastfix": list(fixes.keys())[-1]
66
+
}
67
+
68
+
returnac
69
+
70
+
71
+
defmain():
72
+
parser=argparse.ArgumentParser(
73
+
"combine-igcs.exe",
74
+
description="Use it to combine multiple .igc files recorded on the same day into a single .acmi, containing all aircraft"
75
+
)
76
+
# import common arguments
77
+
parser=a.addDefaultArgs(parser)
78
+
# set program-specific arguments
79
+
parser.add_argument(
80
+
"-i",
81
+
"--input",
82
+
type=str,
83
+
default="",
84
+
help="Input path for igc files"
85
+
)
86
+
parser.add_argument(
87
+
"-n",
88
+
"--name",
89
+
type=str,
90
+
default="",
91
+
help="Desired filename for ACMI file, will only be applied to the first file to be handled"
92
+
)
93
+
# parse all arguments
94
+
args=parser.parse_args()
95
+
96
+
try:
97
+
# define input path
98
+
inPath=a.parseArgPath(args.input, "Input")
99
+
# define output path
100
+
outPath=a.parseArgPath(args.output, "Output")
101
+
102
+
# list available .igc files by their date and parse an acmi for each day
103
+
igcs=listIGCsByDate(inPath, fnc.listIGCs(inPath))
104
+
ifNoneinigcs: # if .igc files with no valid date header line have been found, list them as corrupted and remove them from the dictionary
105
+
c.warn("The following files have no valid date header line and will not be converted:")
106
+
forfileinigcs[None]:
107
+
print("\""+file+"\"")
108
+
igcs.pop(None)
109
+
110
+
fordateinigcs:
111
+
try:
112
+
# initialize data structure of flights on x date
113
+
refTime=dt.datetime.combine(
114
+
dt.datetime.strptime(str(date), "%y%m%d"),
115
+
getLowestRefTime(igcs[date])
116
+
)
117
+
c.info("Handling flights of date: "+refTime.strftime("%d %b %Y"))
118
+
acs=buildAircraft(igcs[date], refTime.time())
119
+
120
+
# default to ACMI name "YYYY-MM-DD_Callsign1_Callsign2_..." if filename argument is not set or already taken
c.info("Combined the following files into \""+outPath+acmiName+ext+"\":")
166
+
forigcinigcs[date]:
167
+
print("\""+igc+"\"")
168
+
169
+
except:
170
+
if"acmi"inlocals(): # clean up initialized ".tmpacmi" file in case of a crash
171
+
ifnotacmi.closed: # if tmpacmi is still open, close it
172
+
acmi.close()
173
+
pl.Path(outPath+acmiName+".tmpacmi").unlink()
174
+
ifargs.debug: # if debug flag is set, print normal error message
175
+
c.err("Unable to combine flights of date "+refTime.strftime("%d %b %Y")+", error:\n"+tb.format_exc())
176
+
else: # default crashdump writing
177
+
c.err("Unable to convert flights of date "+refTime.strftime("%d %b %Y")+", a crashlog for it has been saved in \""+dump.fileDump(acmiName, "combine-igcs")+"\"")
178
+
179
+
except:
180
+
ifargs.debug: # if debug flag is set, print normal error message
181
+
c.err(tb.format_exc())
182
+
else: # default crashdump writing
183
+
c.err("An error occured in this program's execution, a crash report has been saved to \""+dump.progDump(args, "combine-igcs")+"\"")
0 commit comments