An ESP32 that feeds GPS positions to a camera over Bluetooth LE — without the manufacturer's phone app. The reverse-engineered protocol and a working, hardware-verified implementation for the Panasonic LUMIX S5 are included. The firmware is camera-agnostic: model-specific logic lives behind a small interface, so adding another camera means adding one file (see below).
The official LUMIX Sync app barely reconnects after the camera has been off (Android scan throttling / Doze / app kills), and leaving the camera running while hiking is no option. A dedicated, never-sleeping BLE central closes exactly that reconnect gap: it scans continuously and grabs the camera the moment it advertises again.
- ✅ LUMIX S5 BLE injection — done & verified on hardware. Protocol decoded from Android HCI snoop, replayed app-less, no pairing/bonding required. Auto-reconnect verified across power-cycle and sleep.
- ✅ Live GNSS — done. Real GPS from a SIM868 module feeds
currentFix(); actual coordinates land in the photo's EXIF, confirmed end-to-end. - ✅ Camera clock + GPS date correct. Time packet is built from real GPS time; the GPS date field uses the GPS epoch (see PROTOCOL.md).
- ✅ On-device status display (SSD1306 OLED) and optional WiFi telemetry
(
nc <ip> 3333) for field debugging.
- Any ESP32 (developed on a LilyGo T-PCIE, ESP32-WROVER-B) + SIM868 modem/GNSS.
- GNSS is tapped directly off the SIM868's MT3333 GPS die (TP14 =
GPS_TXD). The module's internal MT2503↔MT3333 link is dead (AT+CGNSINFreturns nothing), so the GSM side never sees a fix — but the GPS die itself emits clean 9600-baud NMEA. The 2.8 V tap is level-shifted to 3.3 V with a single 2N2222 (common- emitter; the ESP inverts the UART in software) → readable NMEA on GPIO33. - SSD1306 128×32 OLED on I²C (GPIO21/22, shares the bus with the AXP2101 PMU); optional push-button on GPIO34.
A dedicated u-blox GNSS module over UART would be the tidier source; the SIM868 tap is what was on hand and it works. On a newer LTE board (e.g. SIM7670G) the GNSS comes cleanly over AT and the tap/level-shifter are unnecessary.
The BLE core is camera-agnostic. It scans, connects, reconnects and pushes positions — and knows nothing camera-specific. Everything else sits behind small modules:
firmware/src/
main.cpp BLE central + reconnect loop + GNSS/display/telemetry glue
camera.h interface CameraLink + struct GpsFix (camera-agnostic)
panasonic_s5.* LUMIX S5 implementation (UUIDs, handshake, GPS/time packets)
gnss.* SIM868 GNSS on GPIO33 (inverted) -> TinyGPS++ -> GpsFix
display.* SSD1306 status screen + button + AXP2101 battery
telemetry.* optional WiFi/TCP log mirror (-DTELEMETRY)
CameraLink has three methods: matches(), initSession() and
pushLocation(). Adding a camera = implementing CameraLink in a new file.
Capture your camera's protocol (see docs/CAPTURE.md) and let
an AI agent or yourself decode it — CLAUDE.md is a ready-made brief.
cd firmware
pio run -e t-pcie -t upload # field build: BLE + GNSS + display
pio run -e t-pcie-debug -t upload # + WiFi telemetry (nc <ip> 3333)For the debug build, create firmware/src/wifi_secrets.h (git-ignored):
#pragma once
#define WIFI_SSID "your-ssid"
#define WIFI_PASS "your-pass"The gnss-*, gps-*, display-test and oled-test environments are the
hardware bring-up diagnostics (direct NMEA tap, baud/GPIO scan, display checks) —
not needed for normal use, kept because they document how the GNSS was brought up.
PROTOCOL.md— decoded LUMIX S5 BLE protocol (GATT UUIDs, GPS/time packets).parse_hci.py— btsnoop HCI parser.gps_packet.py— GPS packet codec.docs/CAPTURE.md— how to capture your own camera's BLE protocol.CLAUDE.md— brief for an AI agent to decode a capture and add a camera.
Raw captures contain real GPS coordinates and are
.gitignored. Don't commit them.
MIT — see LICENSE. Reverse engineering for interoperability; not
affiliated with or endorsed by Panasonic. Use at your own risk.
