Surprisingly to me the script that I haphazardly put together turned out to be very popular. I decided to quickly update it (after multiple feature and bug fixes requests)…
The highlights are:
Automatic finding of the correct position of the data block (more on that below).
Introduction of -m flag, which creates separete GPX file per input file (default behaviour is a single GPX output file for multiple input files).
More robust argument parsing (now it should always display help if wrong arguments are used)
Introduction of -d flag, which allows to de-obfuscate GPS coordinates.
The biggest problem with the script (in my opinion) is that it relied on rigid position of the GPS data block inside of the GPS atom. For some silly reason camera manufacturers occasionally move this position around, even with the same camera model. So I decided not to go for special cases instead just to find the damn block by starting from the end and looking for A{N,S}{E,W} pattern (eg: ASE). Where A stands for Active (I assume), and N,S,W,E are hemisphere indicators.
In reality as end user you are probably better off using ExifTool, see this. This is probably the last update…
It appears that not only my quick hack of a script is popular but also that the dashcam manufacturers keep changing things.
In this case the biggest change (beyond utterly stupid data obfuscation) is the switch to the TS format.
In retrospect TS format is an obvious choice for something like dashcam as it is very resilient to crashes (no pun intended).
TS format unlike the MP4/MOV format is very simple, it is pretty much made up of fixed 188 byte segments which have ASCII ‘G’ as a header. The actual payload lives in last 184 bytes while the first 4 bytes are reserved for header (of course this is all simplified).
The biggest difficulty of adding TS functionality is that Blueskysea B4K camera was engineered by sadists and they split the payload into two packets (arbitrary cutting off at the seconds 4 bytes). If you are the person at the BlueSkysea who made this obfuscation – fuck you.
In anyway this update is massive rewrite. Just to make it easy here is the script: nvtk_mp42gpx.py
Recently I was contacted regarding my dash cam GPS coordinate extraction script. The request was regarding wrong data being produced. I have addressed something similar in the past, so I decided to have a go at this one (I like a challenge).
Poking about I realised the data is not simply obfuscated…
Eventually I stumbled upon this: https://exiftool.org/forum/index.php?topic=11320.0
According to exiftool forum the player uses an internet service to decode the data, which is a huge bummer.
I will try to decompile the player but at this stage I don’t believe I will have any success.
To add an insult to injury the dash cam manufacturer asked the dash cam owner not a small amount of money to decrypt these coordinates. Actually you could outfit a small fleet with new cameras from Viofo for the amount of money they asked.
So here are the brands so far to avoid (do not buy them):
AkasoCar ANKEWAY
or any brand that requires their own player to playback the coordinates.
For a while I was looking a simple camera that I could use with micropython and ESP32 board.
Initially I looked at Arducam 2MP camera, but that turned out to be a real pain to interface with, and it took up way to many IO pins for my liking. The micropython is barely fast enough to actually do handle the packets. Of course there weren’t off the shelf library to do what I wanted. In addition the image quality was not great, even though it was a 2MP camera.
I decided to try a low cost UART (serial) camera instead. Downside is the camera only 640×480, the upside is that the power consumption is around 30mA (half of what the Arducam is). Another upside that the camera is physically smaller (specifically the lens). Most importantly the UART protocol the camera uses is very simple, and it only needs two IO pins to operate.
PTC06 UART Camera (front)PTC06 UART Camera (back)
Here is the mycropython code:
from machine import UART
"""
PTC06 Camera interface
Copyright GPL3.0 sergei.nz.
https://www.mouser.com/datasheet/2/737/adafruit_english%20camera-1217461.pdf
"""
RESET = b'\x56\x00\x26\x00'
START_CAPTURE = b'\x56\x00\x36\x01\x00'
GET_IMGSIZE = b'\x56\x00\x34\x01\x00'
GET_FRAME_START = b'\x56\x00\x32\x0C\x00\x0A'
GET_FRAME_END = b'\x00\x0A'
STOP_CAPTURE = b'\x56\x00\x36\x01\x03'
GET_RESOLUTION = b'\x56\x00\x30\x04\x04\x01\x00\x19'
GET_VERSION = b'\x56\x00\x11\x00'
SET_RES640 = b'\x56\x00\x31\x05\x04\x01\x00\x19\x00'
SET_RES320 = b'\x56\x00\x31\x05\x04\x01\x00\x19\x11'
SET_COMPRESSION = b'\x56\x00\x31\x05\x01\x01\x12\x04' # concat with a byte of value in range 0x00..0xFF
UART_BUS = 2
BAUD_RATE = 115200
BUFFER_SIZE = 1024
def capture():
"""
This is a generator that produces data "stream" that is meant to be process in a for loop.
Not bothering with GET_RESOLUTION/SET_RESnnn because default resolution is 640x480.
Also not bothering with setting SET_COMPRESSION, as default (0x36) seems fine.
"""
size = 0
uart = UART(UART_BUS, BAUD_RATE, timeout=100)
uart.write(RESET)
ack = uart.read(BUFFER_SIZE)
print("Sent 'RESET' command. Received AK: %s" % ack)
# a loop to make sure the camera doesn't sent anymore data, indicating a full reset.
while True:
data = uart.read(BUFFER_SIZE)
if not data:
break
ack = uart.read(BUFFER_SIZE)
print("Finished initialisation. Payload: %s" % ack)
uart.write(START_CAPTURE)
ack = uart.read(BUFFER_SIZE)
print("Sent 'START_CAPTURE' command. Received ACK: %s" % ack)
uart.write(GET_IMGSIZE)
size = int.from_bytes(uart.read()[-4:], "big")
print("Sent 'GET_IMGSIZE' command. Image size is %d" % size)
# forming a GETFRAME command out of GET_FRAME_START/beginning, zero padding, size, GET_FRAME_END/end
GET_FRAME = GET_FRAME_START \
+ (0).to_bytes(4, 'big') \
+ size.to_bytes(4, 'big') \
+ GET_FRAME_END
uart.write(GET_FRAME)
print("Sent 'GET_FRAME' command...")
# discarding ACK header via print, otherwise MIME type is incorrect.
ack = uart.read(5)
print("HEADER: %s" % ack) # header
# derive number of chunks and last chunk size from the payload size
chunk_count = int(size / BUFFER_SIZE)
last_chunk_size = (size % BUFFER_SIZE)
print("Receiving data (%d chunks of %d bytes, 1 chunk of %d bytes)..." \
% (chunk_count, BUFFER_SIZE, last_chunk_size))
while True:
if chunk_count == 0:
data = uart.read(last_chunk_size)
yield data
ack = uart.read(5)
print("FOOTER: %s" % ack)
break
data = uart.read(BUFFER_SIZE)
yield data
chunk_count -= 1
# not really necessary as we are resetting the camera each time
uart.write(STOP_CAPTURE)
ack = uart.read(BUFFER_SIZE)
print("Sent 'STOP_CAPTURE' command. Received ACK: %s" % ack)
To use this generator one would simply call this way:
s = socket.socket()
s.connect(('192.168.1.69', 6969))
for data in capture():
s.send(data)
s.close()
A quick way to test it is to have nc with dd on other end in this way (running before calling the capture()):
Here is a sample image (I have refocused camera for a closer range):
PTC06 UART Camera test shot (~20cm focus)
The below are the pictures of the set up I am using this camera with. It housed in a re-purposed front half of the enclosure from a cheap reversing analogue camera and a custom made aluminium back plate. The LED array is build using LEDs out of cheap LED strip, basically there are 4 parallel strings of 3 LEDs and 39 Ohm resistors. These add up to about ~3W. The whole thing is controlled by two high side MOSFET switches (12V and 5V rails). I didn’t want to use low side switching because it would split grounds and increase pin/wire count.
Re-purposed front part of an reversing camera enclosure.Side view of the LED array board + Camera stack mounted on the back plateThe back of camera+ LED Array board stackMagnetic baseTesting LED array.