Skip to content

Instantly share code, notes, and snippets.

@ktemkin
Last active May 26, 2025 05:58
Show Gist options
  • Save ktemkin/be3672386fdb2b769662be913acbf8da to your computer and use it in GitHub Desktop.
Save ktemkin/be3672386fdb2b769662be913acbf8da to your computer and use it in GitHub Desktop.
script to grab garmin maps from Express without waiting for them to transfer

Proof I'm Terminally Impatient

Tired of waiting for Garmin to transfer those map files to your GPS over its incredible 12Mbps full speed USB? Tired of waiting for two hours for it to copy, only for the cable to be jostled just enough for it to fail early -- and then you're stuck with a bunch of map files but none of the gods-forsaken unlock codes that you need to use them?

Well, I am, and since this is my README, you don't get to judge me. That's just the way it is.

Usage

  • Start Garmin Express, and tell it to download the target maps to your device.
  • Wait for the message to change from "downloading files" to "copying files".
    • Okay, so you have to wait a little. Just a little. It's okay. Deep breaths.
  • Run the script, which will magically copy the relevaaent maps to your current working directory -- and synthesize up the unl and gma files you need to make your GPS actually unlock the maps.

Trivia

Did you know that "50% done" means "halfway done"? Yeah, well, neither did Garmin. The first 50%, naturally, is downloading the file, over your fast, juicy Gigabit fiber. Hell, you paid for that fiber, and now you're rewarded with that bar speeding to 50% like nobody's business. Wait, did Garmin actualy pay for a CDN?

Then the thing hits 50%, and suddently your 30GiB of map data has to make it across a 12Mbps USB full-speed connection. Just because I remember that being -really gods-damned impressive- back in 1998 doesn't mean I'm going to go back and live there. Now you have to wait, pateintly, for about 6 hours because Garmin didn't spring for an SoC with High Speed USB for your fancy new satellite communicator.

Or, you could go insane and write a Python script.

#!/usr/bin/env python3
from math import log
import os
import sys
import json
import base64
import shutil
from typing import Any
# The location where Garmin dumps its express logs on Windows.
# There's almost definitely an equivalent on MacOS.
LOG_FILENAME_WINDOWS = "C:\\ProgramData\\Garmin\\Logs\\Express\\ExpressDetailed.log"
# The marker for a rest client completion.
REST_MARKER = "OmtRestClient[] - Request completed in"
# The marker for where a file winds up. Tasty.
DESTINATION_MARKER = "Downloadable Destination: "
# When we see this in the log, we know that downloading is already done.
DONE_DOWNLOADING_MARKER = "Reporting phase Copying"
last_filename: str = "unknown"
def process_rest_line(line: str):
global last_filename
_, _, json_data = line.partition("Response: ")
# Strip awawy the outer layer of encapsulation, ignoring anything that failed.
data: dict[str, Any] = json.loads(json_data)
if data['statusCode'] != 200:
return
# Now, parse the payload, which is, of course, JSON in JSON.
content: dict[str, Any] = json.loads(data['content'])
# Handle issuing things for the actual data.
if 'UnlockInfos' in content:
for info in content['UnlockInfos']:
process_unlockinfo_payload(info)
def filename_for_part_number(pn: str, suffix=".img") -> str:
component = pn.split("-")
return f"{component[1]}{component[2]}0A{suffix}"
def process_unlockinfo_payload(content):
pn: str = content['PartNumber']
write_gma_file(content['Gma'], pn)
write_unlock_file(content['Unlock'], pn)
def write_unlock_file(unlock_code_utf8: str, part_number: str):
filename = filename_for_part_number(part_number, ".unl")
with open(filename, "w") as f:
f.write(unlock_code_utf8)
def write_gma_file(gma_contents_base64: str, part_number: str):
filename = filename_for_part_number(part_number, ".gma")
gma_contents = base64.standard_b64decode(gma_contents_base64)
with open(filename, "wb") as f:
f.write(gma_contents)
def copy_file_from_destination(log_line: str):
_, _, path_in_cache = log_line.partition("Destination: ")
path_in_cache = path_in_cache.rstrip()
target_path = os.path.basename(path_in_cache)
shutil.copy(path_in_cache, target_path)
def main():
f = open(LOG_FILENAME_WINDOWS)
# First, make sure we see that the we're down downloading, so we don't fail out early.
for line in f:
if DONE_DOWNLOADING_MARKER in line:
break
else:
print("The relevant maps don't appear to be done downloading, yet. Patience!", file=sys.stderr)
sys.exit(-1)
# Next, yoink the maps out of the cache and generate the relevant keys.
f.seek(0)
for line in f:
if REST_MARKER in line:
process_rest_line(line)
if DESTINATION_MARKER in line:
copy_file_from_destination(line)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment