Canon GPS Reverse Engineering

Reverse Engineering Binary Formats

Attempting to decode an unknown format into known output is akin to solving a jigsaw puzzle. The initial state is an incoherent jumble, however, as you slowly simmer off the entropy, a much clearer image emerges.

The satisfaction derived from solving both types of puzzle is also comparable.

I recently collaborated with a fellow developer in decoding the Canon GPS format over Bluetooth Low Energy and decided to describe the process.

Predestination

The most straightforward puzzles involve a relatively stable starting position and a reasonably well known outcome. In the jigsaw puzzle analogy, there are hundreds of pieces to start and a picture on the box to achieve.

In the Canon GPS scenario, both the pieces and the picture were provided as follows.

The transmitted binary packet1 (captured via sniffer, in hexadecimal) was:

044e45440e4245a5ae0b432bae47494285ac6868

whilst the camera was showing:

Latitude N35°34'0.0"
Longitude E139°40'55.9"
Elevation 50m
UTC 2025/07/05 04:39:33

Size Matters

In hexadecimal each byte is composed of two characters from 0 to f. In this case, there are 40 characters, thus there are 20 bytes of data.

From previous efforts, we know the first byte (0x04) is the step prefix. For Canon cameras, this is the fourth message in sequence, leaving 19 bytes of data.

A sanity check on data sizes will ensure we are indeed looking at the correct data. For instance, it would be concerning (impossible) if the data packet was a single byte, information theory forbids it. Having past experience with the data domain is also helpful.

Latitude and longitude are typically stored and transmitted in 32-bit (4 byte) fields, this is sufficient to represent -180 to 180 degrees with suitable resolution. The time would also likely need at least 32-bits (4 bytes) probably more. Furthermore, elevation would need at least 16-bits (2 bytes). Thus, the estimated minimum byte count is 4 + 4 + 4 + 2 == 14 bytes, well within the remaining 19 bytes.

This seems sane, let’s keep going.

Measure Twice, Slash Randomly

Wireshark2 is the tool for traffic analysis. The default view displays an ASCII representation of the data like so:

.NED.BE...C+.GIB..hh

where non-printable characters are just .. It is very interesting to note the following:

It is also possible that these ASCII printable bytes are coincidentally3 printable and are not print-meaningful. Nevertheless, we proceed with the assumption and splice the packet like so:

index 0 1 2-5 6 7-10 11-19
bytes 1 1 4 1 4 9
hex 04 4e 45440e42 45 a5ae0b43 2bae47494285ac6868
decode 4 N ??? E ??? ???

At this point, the hypothesis is:

Data Presentations

At the lowest (embedded) levels of software engineering, knowing about data sizes and presentations is critical. If we assume the latitude is indeed a 32-bit value, there are a few common formats:

We can quickly experiment with the conversions using the Python struct module.

import struct
struct.unpack('i', bytes.fromhex('45440e42'))[0]
1108231237

where i is a 4 byte integer (and the result is large and meaningless).

We must also consider endianness. The Python struct module supports decoding little and big endian.

Little endian (native on my x86 system):

struct.unpack('<i', bytes.fromhex('45440e42'))[0]
1108231237

Big endian:

struct.unpack('<i', bytes.fromhex('45440e42'))[0]
1162088002

Let’s just unpack it in every common format:

hex format unpack endian type
45440e42 <i 1108231237 little int32_t
45440e42 >i 1162088002 big int32_t
45440e42 <I 1108231237 little uint32_t
45440e42 >I 1162088002 big uint32_t
45440e42 <f 35.56666946411133 little float32
45440e42 >f 3136.89111328125 big float32

Scanning that table, one format stands out, little endian float32. The latitude was 35°34'0.0" DMS (degrees-minutes-seconds), whilst we have 35.56666946411133, that seems awfully close3.

There’s a myriad of online calculators for converting degrees to degrees minutes seconds, so we jump online, pick one and plug it in:

35.56666946411133 == 35°34'0.01"

Bingo!

Rinse Repeat

Repeat for the longitude:

45a5ae0b == 139.6822052001953 == 139°40'55.9"

Two down, two to go.

Elevation

It is interesting to note that Canon used a float32 representation which is inherently signed, but chose to encode the north, south, east and west as additional prefix ASCII characters. Learning from this, we could reasonably assume the next byte, 0x2b, which is ASCII +, is positive altitude and negative altitude would be prefixed with 0x2d, ASCII -. We could further guess that the altitude is also a little endian float32:

ae474942 == 50.31999969482422

and indeed it is (accounting for rounding error).

Three down, one to go.

Time’s Up

The packet format is now mostly decoded, so the remaining bytes must be the date and time:

step N/S Latitude E/W Longitude +/- Elevation Date/Time
04 4e 45440e42 45 a5ae0b43 2b ae474942 85ac6868

Once more, familiarity with embedded data formats is useful. Most date/time formats pack the year, month, day, hour, minute and second as separate bytes. However, considering sizes, this seems unlikely as we need at least:

Furthermore, the year is ‘2025’, which is 0xe907 in hex and appears nowhere (little or big endian) in the remaining bytes.

Given the byte deficit, it’s certain the date/time is not packed as separate fields and alternate encodings need consideration. Another common date/time representation is number of seconds since epoch. As there are 4 bytes remaining, let’s try unpacking as an unsigned 32-bit integer (uint32_t):

struct.unpack('<I', bytes.fromhex('85ac6868'))[0]
1751690373

and use good old date from coreutils:

$ TZ=UTC date -d @1751690373
Sat 05 Jul 2025 04:39:33 UTC

Perfect. Four down, zero to go.

Finally

One final snippet of Python to fully decode the Canon GPS format:

import datetime
import math
import struct
import sys

def d2dms(degrees):
  m,d = math.modf(degrees)
  m = m * 60
  s,m = math.modf(m)
  s = s * 60

  return (int(d), int(m), s)

message = sys.argv[1]

(step, lat_dir, lat, lon_dir, lon, x, elevation, timestamp) = \
   struct.unpack('<ccfcfcfI', bytearray.fromhex(message))

print("Packet: {:s}".format(message))
print("Step: {:d}".format(int.from_bytes(step)))

(d,m,s) = d2dms(lat)
print("Latitude: {:s} {:d}°{:d}\"{:.2f}'".format((lat_dir(.decode("ascii"), d, m, s))

(d,m,s) = d2dms(lon)
print("Longitude: {:s} {:d}°{:d}\"{:.2f}'".format(lon_dir.decode("ascii"), d, m, s))

print("Elevation: {:s} {:.2f} m".format(x.decode("ascii"), elevation))
print("Time = {}".format(datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)))

invoked:

$ python canon_gps.py 044e45440e4245a5ae0b432bae47494285ac6868
Packet: 044e45440e4245a5ae0b432bae47494285ac6868
Step: 4
Latitude: N 35°34"0.01'
Longitude: E 139°40"55.94'
Elevation: + 50.32 m
Time = 2025-07-05 04:39:33+00:00

and tabulated:

index size (bytes) format field comment
0 1 uint8_t step message index
1 1 char direction ‘N’ or ‘S’
2-5 4 float32 latitude degrees
6 1 char direction ‘E’ or ‘W’
7-10 4 float32 longitude degrees
11 1 char direction ‘+’ or ‘-’
12-15 4 float32 elevation metres
16-19 4 uint32_t timestamp epoch seconds

  1. Synthesised data for privacy protection. ↩︎

  2. https://www.wireshark.org/ ↩︎

  3. Rule 39: There is no such thing as coincidence. ↩︎ ↩︎