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:
- there is a
Nat byte 1- where indexing starts at 0
- matching the ’north’ latitude direction
- 4 bytes later there is an
E- matching the ’east’ longitude direction
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:
- bytes
1and2-5are the latitude- where byte
1is perhapsNfor north andSfor south - and
45440e42is the latitude encoded in 4 bytes
- where byte
- bytes
6and7-10are the longitude- where byte
6is perhapsEfor east andWfor west - and
a5ae0b4is the longitude encoded in 4 bytes
- where byte
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:
int32_t: signed 32-bit integeruint32_t: unsigned 32-bit integerfloat32: 32-bit single precision floating point number
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:
- 2 bytes for year (range 0-2025+)
- 0.5 byte for month (range 1-12)
- 1 byte for day (range 1-31)
- 1 byte for hour (range 0-59)
- 1 byte for minute (range 0-59)
- 1 byte for second (range 0-59)
- 6.5 bytes total
- only 4 bytes, 2.5 byte deficit
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 |
-
Synthesised data for privacy protection. ↩︎