@@ -193,6 +193,50 @@ def _parse_nmea_lines(
193193 yield epoch_ms , message
194194
195195
196+ def _detect_timezone_offset (
197+ parsed_lines : list [tuple [float , pynmea2 .NMEASentence ]],
198+ ) -> float :
199+ """
200+ Detect timezone offset between camera clock and GPS time.
201+
202+ Tries RMC messages first (most reliable - has full date+time),
203+ then falls back to GGA/GLL (less reliable - time only, no date).
204+ Returns 0.0 if no offset could be determined.
205+ """
206+ first_valid_gga_gll : tuple [float , pynmea2 .NMEASentence ] | None = None
207+
208+ for epoch_sec , message in parsed_lines :
209+ if message .sentence_type == "RMC" :
210+ if hasattr (message , "is_valid" ) and message .is_valid :
211+ offset = _compute_timezone_offset_from_rmc (epoch_sec , message )
212+ if offset is not None :
213+ LOG .debug (
214+ "Computed timezone offset %.1fs from RMC (%s %s)" ,
215+ offset ,
216+ message .datestamp ,
217+ message .timestamp ,
218+ )
219+ return offset
220+
221+ if first_valid_gga_gll is None and message .sentence_type in ["GGA" , "GLL" ]:
222+ if hasattr (message , "is_valid" ) and message .is_valid :
223+ first_valid_gga_gll = (epoch_sec , message )
224+
225+ # Fallback: if no RMC found, try GGA/GLL (less reliable - no date info)
226+ if first_valid_gga_gll is not None :
227+ epoch_sec , message = first_valid_gga_gll
228+ offset = _compute_timezone_offset_from_time_only (epoch_sec , message )
229+ if offset is not None :
230+ LOG .debug (
231+ "Computed timezone offset %.1fs from %s (fallback, no date info)" ,
232+ offset ,
233+ message .sentence_type ,
234+ )
235+ return offset
236+
237+ return 0.0
238+
239+
196240def _parse_gps_box (gps_data : bytes ) -> list [telemetry .GPSPoint ]:
197241 """
198242 >>> list(_parse_gps_box(b"[1623057074211]$GPGGA,202530.00,5109.0262,N,11401.8407,W,5,40,0.5,1097.36,M,-17.00,M,18,TSTR*61"))
@@ -210,83 +254,47 @@ def _parse_gps_box(gps_data: bytes) -> list[telemetry.GPSPoint]:
210254 >>> list(_parse_gps_box(b"[1623057074211]$GPVTG,,T,,M,0.078,N,0.144,K,D*28[1623057075215]"))
211255 []
212256 """
213- timezone_offset : float | None = None
214257 parsed_lines : list [tuple [float , pynmea2 .NMEASentence ]] = []
215- first_valid_gga_gll : tuple [float , pynmea2 .NMEASentence ] | None = None
216258
217- # First pass: collect parsed_lines and compute timezone offset from the first valid RMC message
259+ # First pass: collect parsed_lines
218260 for epoch_ms , message in _parse_nmea_lines (gps_data ):
219261 # Rounding needed to avoid floating point precision issues
220262 epoch_sec = round (epoch_ms / 1000 , 3 )
221263 parsed_lines .append ((epoch_sec , message ))
222- if timezone_offset is None and message .sentence_type == "RMC" :
223- if hasattr (message , "is_valid" ) and message .is_valid :
224- timezone_offset = _compute_timezone_offset_from_rmc (epoch_sec , message )
225- if timezone_offset is not None :
226- LOG .debug (
227- "Computed timezone offset %.1fs from RMC (%s %s)" ,
228- timezone_offset ,
229- message .datestamp ,
230- message .timestamp ,
231- )
232- # Track first valid GGA/GLL for fallback
233- if first_valid_gga_gll is None and message .sentence_type in ["GGA" , "GLL" ]:
234- if hasattr (message , "is_valid" ) and message .is_valid :
235- first_valid_gga_gll = (epoch_sec , message )
236-
237- # Fallback: if no RMC found, try GGA/GLL (less reliable - no date info)
238- if timezone_offset is None and first_valid_gga_gll is not None :
239- epoch_sec , message = first_valid_gga_gll
240- timezone_offset = _compute_timezone_offset_from_time_only (epoch_sec , message )
241- if timezone_offset is not None :
242- LOG .debug (
243- "Computed timezone offset %.1fs from %s (fallback, no date info)" ,
244- timezone_offset ,
245- message .sentence_type ,
246- )
247264
248- # If no offset could be determined, use 0 (camera clock assumed correct)
249- if timezone_offset is None :
250- timezone_offset = 0.0
265+ timezone_offset = _detect_timezone_offset (parsed_lines )
251266
252267 points_by_sentence_type : dict [str , list [telemetry .GPSPoint ]] = {}
253268
254269 # Second pass: apply offset to all GPS points
255270 for epoch_sec , message in parsed_lines :
271+ if message .sentence_type not in ("GGA" , "RMC" , "GLL" ):
272+ continue
273+ if not message .is_valid :
274+ continue
275+
256276 corrected_epoch = round (epoch_sec + timezone_offset , 3 )
257277
258- # https://tavotech.com/gps-nmea-sentence-structure/
259- if message .sentence_type in ["GGA" ]:
260- if not message .is_valid :
261- continue
262- point = telemetry .GPSPoint (
263- time = corrected_epoch ,
264- lat = message .latitude ,
265- lon = message .longitude ,
266- alt = message .altitude ,
267- angle = None ,
268- epoch_time = corrected_epoch ,
269- fix = telemetry .GPSFix .FIX_3D if message .gps_qual >= 1 else None ,
270- precision = None ,
271- ground_speed = None ,
272- )
273- points_by_sentence_type .setdefault (message .sentence_type , []).append (point )
274-
275- elif message .sentence_type in ["RMC" , "GLL" ]:
276- if not message .is_valid :
277- continue
278- point = telemetry .GPSPoint (
279- time = corrected_epoch ,
280- lat = message .latitude ,
281- lon = message .longitude ,
282- alt = None ,
283- angle = None ,
284- epoch_time = corrected_epoch ,
285- fix = None ,
286- precision = None ,
287- ground_speed = None ,
288- )
289- points_by_sentence_type .setdefault (message .sentence_type , []).append (point )
278+ # GGA has altitude and fix; RMC and GLL do not
279+ if message .sentence_type == "GGA" :
280+ alt = message .altitude
281+ fix = telemetry .GPSFix .FIX_3D if message .gps_qual >= 1 else None
282+ else :
283+ alt = None
284+ fix = None
285+
286+ point = telemetry .GPSPoint (
287+ time = corrected_epoch ,
288+ lat = message .latitude ,
289+ lon = message .longitude ,
290+ alt = alt ,
291+ angle = None ,
292+ epoch_time = corrected_epoch ,
293+ fix = fix ,
294+ precision = None ,
295+ ground_speed = None ,
296+ )
297+ points_by_sentence_type .setdefault (message .sentence_type , []).append (point )
290298
291299 # This is the extraction order in exiftool
292300 if "RMC" in points_by_sentence_type :
0 commit comments