gpsd_json/protocol/v3/
response.rs

1//! GPSD Protocol v3 response message types
2//!
3//! This module defines all the response messages that GPSD can send to clients.
4//! Each message type corresponds to a specific class of GPS data or status information.
5//!
6//! Response messages are identified by their "class" field in the JSON structure.
7//! Common message types include:
8//! - TPV (Time-Position-Velocity): Core GPS fix data
9//! - SKY: Satellite visibility and signal strength
10//! - GST: GPS pseudorange error statistics
11//! - ATT: Attitude/orientation data
12//! - DEVICE/DEVICES: GPS receiver information
13//! - VERSION: GPSD daemon version information
14//!
15//! All timestamps use the ISO 8601 format and are represented as `DateTime<Utc>`.
16
17use chrono::{DateTime, Utc};
18use serde::Deserialize;
19
20use super::types::*;
21
22/// Time-Position-Velocity (TPV) report
23///
24/// The TPV message is the core GPS fix report, containing time, position, and velocity data.
25/// This is the primary message type for navigation applications.
26///
27/// Reference: [json_tpv_read](https://gitlab.com/gpsd/gpsd/-/blob/master/libgps/libgps_json.c?ref_type=heads#L34)
28#[derive(Debug, Clone, PartialEq, Deserialize)]
29pub struct Tpv {
30    /// Altitude in meters (deprecated, use altMSL or altHAE)
31    pub alt: Option<f64>,
32    /// Altitude, height above ellipsoid, in meters
33    #[serde(rename = "altHAE")]
34    pub alt_hae: Option<f64>,
35    /// Altitude, MSL (mean sea level) in meters
36    #[serde(rename = "altMSL")]
37    pub alt_msl: Option<f64>,
38    /// Antenna status (OK, OPEN, SHORT)
39    pub ant: Option<AntennaStatus>,
40    /// RTK baseline information (flattened)
41    #[serde(flatten)]
42    pub base: Baseline,
43    /// Climb/sink rate in meters per second
44    pub climb: Option<f64>,
45    /// Geodetic datum (usually WGS84)
46    pub datum: Option<String>,
47    /// Device path that provided this data
48    pub device: Option<String>,
49    /// Depth below mean sea level in meters
50    pub depth: Option<f64>,
51    /// Age of DGPS corrections in seconds
52    #[serde(rename = "dgpsAge")]
53    pub dgps_age: Option<f64>,
54    /// DGPS station ID
55    #[serde(rename = "dgpsSta")]
56    pub dgps_sta: Option<i32>,
57    /// ECEF coordinates and velocities (flattened)
58    #[serde(flatten)]
59    pub ecef: Ecef,
60    /// Estimated climb error in meters/second
61    pub epc: Option<f64>,
62    /// Estimated track error in degrees
63    pub epd: Option<f64>,
64    /// Estimated horizontal position error in meters
65    pub eph: Option<f64>,
66    /// Estimated speed error in meters/second
67    pub eps: Option<f64>,
68    /// Estimated time error in seconds
69    pub ept: Option<f64>,
70    /// Longitude error estimate in meters
71    pub epx: Option<f64>,
72    /// Latitude error estimate in meters
73    pub epy: Option<f64>,
74    /// Estimated vertical error in meters
75    pub epv: Option<f64>,
76    /// Geoid separation (height of geoid above WGS84 ellipsoid) in meters
77    #[serde(rename = "geoidSep")]
78    pub geoid_sep: Option<f64>,
79    /// Latitude in degrees (positive = North)
80    pub lat: Option<f64>,
81    /// Jamming indicator
82    pub jam: Option<i32>,
83    /// Current leap seconds (GPS-UTC offset)
84    pub leapseconds: Option<i32>,
85    /// Longitude in degrees (positive = East)
86    pub lon: Option<f64>,
87    /// Magnetic track (course over ground relative to magnetic north)
88    pub magtrack: Option<f64>,
89    /// Magnetic variation in degrees
90    pub magvar: Option<f64>,
91    /// GPS fix mode (NoFix, 2D, 3D)
92    pub mode: FixMode,
93    /// NED velocity components (flattened)
94    #[serde(flatten)]
95    pub ned: Ned,
96    /// Temperature in degrees Celsius
97    pub temp: Option<f64>,
98    /// GPS time of fix
99    pub time: Option<DateTime<Utc>>,
100    /// True track (course over ground) in degrees
101    pub track: Option<f64>,
102    /// Spherical error probability in meters
103    pub sep: Option<f64>,
104    /// Speed over ground in meters/second
105    pub speed: Option<f64>,
106    /// GPS fix status (standard, DGPS, RTK, etc.)
107    pub status: Option<FixStatus>,
108    /// Wind angle magnetic in degrees
109    pub wanglem: Option<f64>,
110    /// Wind angle relative in degrees
111    pub wangler: Option<f64>,
112    /// Wind angle true in degrees
113    pub wanglet: Option<f64>,
114    /// Wind speed relative in meters/second
115    pub wspeedr: Option<f64>,
116    /// Wind speed true in meters/second
117    pub wspeedt: Option<f64>,
118    /// Water temperature in degrees Celsius
119    pub wtemp: Option<f64>,
120    /// Reception time (when enabled by timing policy)
121    #[serde(rename = "rtime")]
122    #[serde(default, deserialize_with = "f64_to_datetime")]
123    pub rtime: Option<DateTime<Utc>>,
124    /// PPS edge time (when enabled by timing policy)
125    #[serde(default, deserialize_with = "f64_to_datetime")]
126    pub pps: Option<DateTime<Utc>>,
127    /// Start of response time (when enabled by timing policy)
128    #[serde(default, deserialize_with = "f64_to_datetime")]
129    pub sor: Option<DateTime<Utc>>,
130    /// Character count in the sentence
131    pub chars: Option<u64>,
132    /// Number of satellites used in solution
133    pub sats: Option<i32>,
134    /// GPS week number
135    pub week: Option<u16>,
136    /// GPS time of week in seconds
137    pub tow: Option<f64>,
138    /// GPS week rollover count
139    pub rollovers: Option<i32>,
140    #[cfg(feature = "extra-fields")]
141    /// Additional fields not explicitly defined
142    #[serde(flatten)]
143    extra: std::collections::HashMap<String, serde_json::Value>,
144}
145
146/// Satellite Sky View (SKY) report
147///
148/// The SKY message reports the satellites visible to the GPS receiver,
149/// including signal strength, elevation, azimuth, and usage status.
150#[derive(Debug, Clone, PartialEq, Deserialize)]
151pub struct Sky {
152    /// Device path that provided this data
153    pub device: Option<String>,
154    /// Dilution of precision values (flattened)
155    #[serde(flatten)]
156    pub dop: Dop,
157    /// GPS time of this sky view
158    pub time: Option<DateTime<Utc>>,
159    /// Number of satellites visible
160    #[serde(rename = "nSat")]
161    pub n_sat: Option<i32>,
162    /// Number of satellites used in navigation solution
163    #[serde(rename = "uSat")]
164    pub u_sat: Option<i32>,
165    /// List of visible satellites with their properties
166    pub satellites: Vec<Satellite>,
167    #[cfg(feature = "extra-fields")]
168    /// Additional fields not explicitly defined
169    #[serde(flatten)]
170    extra: std::collections::HashMap<String, serde_json::Value>,
171}
172
173/// GPS Pseudorange Error Statistics (GST)
174///
175/// The GST message provides GPS pseudorange noise statistics,
176/// including RMS values of standard deviation ranges.
177///
178/// Reference: [json_noise_read](https://gitlab.com/gpsd/gpsd/-/blob/master/libgps/libgps_json.c?ref_type=heads#L175)
179#[derive(Debug, Clone, PartialEq, Deserialize)]
180pub struct Gst {
181    /// Device path that provided this data
182    pub device: Option<String>,
183    /// GPS time of these statistics
184    pub time: Option<DateTime<Utc>>,
185    /// Altitude error in meters (1-sigma)
186    pub alt: Option<f64>,
187    /// Latitude error in meters (1-sigma)
188    pub lat: Option<f64>,
189    /// Longitude error in meters (1-sigma)
190    pub lon: Option<f64>,
191    /// Semi-major axis of error ellipse in meters
192    pub major: Option<f64>,
193    /// Semi-minor axis of error ellipse in meters
194    pub minor: Option<f64>,
195    /// Orientation of error ellipse in degrees from true north
196    pub orient: Option<f64>,
197    /// RMS value of standard deviation ranges
198    pub rms: Option<f64>,
199    /// East velocity error in meters/second (1-sigma)
200    pub ve: Option<f64>,
201    /// North velocity error in meters/second (1-sigma)
202    pub vn: Option<f64>,
203    /// Up velocity error in meters/second (1-sigma)
204    pub vu: Option<f64>,
205    #[cfg(feature = "extra-fields")]
206    /// Additional fields not explicitly defined
207    #[serde(flatten)]
208    extra: std::collections::HashMap<String, serde_json::Value>,
209}
210
211/// Attitude/orientation data
212///
213/// Reports the orientation of the device in 3D space.
214/// Currently a placeholder for future implementation.
215/// Reference: [json_att_read](https://gitlab.com/gpsd/gpsd/-/blob/master/libgps/libgps_json.c?ref_type=heads#L404)
216#[derive(Debug, Clone, PartialEq, Deserialize)]
217pub struct Attitude {
218    pub device: Option<String>,
219    pub acc_len: Option<f64>,
220    pub acc_x: Option<f64>,
221    pub acc_y: Option<f64>,
222    pub acc_z: Option<f64>,
223    #[serde(flatten)]
224    pub base: Baseline,
225    pub depth: Option<f64>,
226    pub dip: Option<f64>,
227    // pub gyro_temp: Option<f64>,
228    pub gyro_x: Option<f64>,
229    pub gyro_y: Option<f64>,
230    pub gyro_z: Option<f64>,
231    pub heading: Option<f64>,
232    pub mag_len: Option<f64>,
233    pub mag_x: Option<f64>,
234    pub mag_y: Option<f64>,
235    pub mag_z: Option<f64>,
236    pub mheading: Option<f64>,
237    pub msg: Option<String>,
238    pub pitch_st: Option<StatusCode>,
239    pub pitch: Option<f64>,
240    pub roll_st: Option<StatusCode>,
241    pub roll: Option<f64>,
242    pub temp: Option<f64>,
243    pub time: Option<DateTime<Utc>>,
244    #[serde(rename = "timeTag")]
245    pub time_tag: Option<String>,
246    pub yaw_st: Option<StatusCode>,
247    pub yaw: Option<f64>,
248    #[cfg(feature = "extra-fields")]
249    /// Additional fields not explicitly defined
250    #[serde(flatten)]
251    extra: std::collections::HashMap<String, serde_json::Value>,
252}
253
254/// Inertial Measurement Unit data
255///
256/// Reports accelerometer and gyroscope readings.
257/// Currently a placeholder for future implementation.
258/// Reference: [json_imu_read](https://gitlab.com/gpsd/gpsd/-/blob/master/libgps/libgps_json.c?ref_type=heads#L487)
259#[derive(Debug, Clone, PartialEq, Deserialize)]
260pub struct Imu {
261    pub device: Option<String>,
262    pub acc_len: Option<f64>,
263    pub acc_x: Option<f64>,
264    pub acc_y: Option<f64>,
265    pub acc_z: Option<f64>,
266    pub depth: Option<f64>,
267    pub dip: Option<f64>,
268    // pub gyro_temp: Option<f64>,
269    pub gyro_x: Option<f64>,
270    pub gyro_y: Option<f64>,
271    pub gyro_z: Option<f64>,
272    pub heading: Option<f64>,
273    pub mag_len: Option<f64>,
274    pub mag_x: Option<f64>,
275    pub mag_y: Option<f64>,
276    pub mag_z: Option<f64>,
277    pub mheading: Option<f64>,
278    pub msg: Option<String>,
279    pub pitch_st: Option<StatusCode>,
280    pub pitch: Option<f64>,
281    pub roll_st: Option<StatusCode>,
282    pub roll: Option<f64>,
283    pub temp: Option<f64>,
284    pub time: Option<DateTime<Utc>>,
285    #[serde(rename = "timeTag")]
286    pub time_tag: Option<String>,
287    pub yaw_st: Option<StatusCode>,
288    pub yaw: Option<f64>,
289    #[cfg(feature = "extra-fields")]
290    /// Additional fields not explicitly defined
291    #[serde(flatten)]
292    extra: std::collections::HashMap<String, serde_json::Value>,
293}
294
295/// Time Offset report
296///
297/// Reports the offset between system clock and GPS time.
298///
299/// Reference: [json_toff_read](https://gitlab.com/gpsd/gpsd/-/blob/master/libgps/libgps_json.c?ref_type=heads#L667)
300#[derive(Debug, Clone, PartialEq)]
301pub struct TimeOffset {
302    /// Device path that provided this data
303    pub device: Option<String>,
304    /// GPS time
305    pub real: Option<DateTime<Utc>>,
306    /// System clock time
307    pub clock: Option<DateTime<Utc>>,
308}
309
310impl<'de> Deserialize<'de> for TimeOffset {
311    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
312    where
313        D: serde::Deserializer<'de>,
314    {
315        #[derive(Debug, Deserialize)]
316        struct RawTimeOffset {
317            pub device: Option<String>,
318            pub real_sec: Option<i64>,
319            pub real_nsec: Option<i64>,
320            pub clock_sec: Option<i64>,
321            pub clock_nsec: Option<i64>,
322        }
323
324        let raw = RawTimeOffset::deserialize(deserializer)?;
325
326        Ok(TimeOffset {
327            device: raw.device,
328            real: deserialize_to_datetime(raw.real_sec, raw.real_nsec),
329            clock: deserialize_to_datetime(raw.clock_sec, raw.clock_nsec),
330        })
331    }
332}
333
334/// Pulse-Per-Second (PPS) timing report
335///
336/// Reports precise timing information from PPS-capable GPS receivers.
337#[derive(Debug, Clone, PartialEq)]
338pub struct Pps {
339    /// Device path that provided this data
340    pub device: Option<String>,
341    /// GPS time of PPS edge
342    pub real: Option<DateTime<Utc>>,
343    /// System clock time of PPS edge
344    pub clock: Option<DateTime<Utc>>,
345    /// Clock precision in nanoseconds
346    pub precision: Option<i32>,
347    /// Quantization error of PPS signal
348    pub q_err: Option<i32>,
349}
350
351impl<'de> Deserialize<'de> for Pps {
352    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
353    where
354        D: serde::Deserializer<'de>,
355    {
356        #[derive(Deserialize)]
357        struct RawPps {
358            pub device: Option<String>,
359            pub real_sec: Option<i64>,
360            pub real_nsec: Option<i64>,
361            pub clock_sec: Option<i64>,
362            pub clock_nsec: Option<i64>,
363            pub precision: Option<i32>,
364            #[serde(rename = "qErr")]
365            pub q_err: Option<i32>,
366        }
367
368        let raw = RawPps::deserialize(deserializer)?;
369        Ok(Pps {
370            device: raw.device,
371            real: deserialize_to_datetime(raw.real_sec, raw.real_nsec),
372            clock: deserialize_to_datetime(raw.clock_sec, raw.clock_nsec),
373            precision: raw.precision,
374            q_err: raw.q_err,
375        })
376    }
377}
378
379/// Oscillator/clock discipline status
380///
381/// Reports the status of the system's precision time reference.
382#[derive(Debug, Clone, PartialEq, Deserialize)]
383pub struct Oscillator {
384    /// Device path of the oscillator
385    pub device: String,
386    /// Whether the oscillator is running
387    pub running: bool,
388    /// Whether this is the reference clock
389    pub reference: bool,
390    /// Whether the clock is disciplined (synchronized)
391    pub disciplined: bool,
392    // delta: field commented out in original
393}
394
395/// GPSD daemon version information
396///
397/// Reports version and protocol information about the GPSD server.
398#[derive(Debug, Clone, PartialEq, Deserialize)]
399pub struct Version {
400    /// GPSD release version string
401    pub release: String,
402    /// Git revision hash
403    pub rev: String,
404    /// Protocol major version number
405    pub proto_major: i32,
406    /// Protocol minor version number
407    pub proto_minor: i32,
408    /// Remote server URL (if applicable)
409    pub remote: Option<String>,
410}
411
412/// List of GPS devices known to GPSD
413///
414/// Contains information about all GPS receivers connected to GPSD.
415#[derive(Debug, Clone, PartialEq)]
416pub struct DeviceList {
417    /// List of available GPS devices
418    pub devices: Vec<Device>,
419}
420
421impl<'de> Deserialize<'de> for DeviceList {
422    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
423    where
424        D: serde::Deserializer<'de>,
425    {
426        #[derive(Deserialize)]
427        struct RawSubDevice {
428            pub path: Option<String>,
429            pub activated: Option<serde_json::Value>,
430            pub flags: Option<PropertyFlags>,
431            pub driver: Option<String>,
432            pub hexdata: Option<String>,
433            pub sernum: Option<String>,
434            pub subtype: Option<String>,
435            pub subtype1: Option<String>,
436            pub native: Option<i32>,
437            pub bps: Option<i32>,
438            pub parity: Option<Parity>,
439            pub stopbits: Option<u32>,
440            pub cycle: Option<f64>,
441            pub mincycle: Option<f64>,
442        }
443
444        #[derive(Deserialize)]
445        struct RawDeviceList {
446            pub devices: Vec<RawSubDevice>,
447        }
448
449        let raw = RawDeviceList::deserialize(deserializer)?;
450
451        let mut devices = Vec::with_capacity(raw.devices.len());
452        for device in raw.devices.into_iter() {
453            let activated = device.activated;
454            let mut device = Device {
455                path: device.path,
456                activated: None,
457                flags: device.flags,
458                driver: device.driver,
459                hexdata: device.hexdata,
460                sernum: device.sernum,
461                subtype: device.subtype,
462                subtype1: device.subtype1,
463                native: device.native,
464                bps: device.bps,
465                parity: device.parity,
466                stopbits: device.stopbits,
467                cycle: device.cycle,
468                mincycle: device.mincycle,
469            };
470
471            match activated {
472                Some(serde_json::Value::String(iso_time)) => {
473                    device.activated = DateTime::parse_from_rfc3339(&iso_time)
474                        .ok()
475                        .map(|dt| dt.with_timezone(&Utc));
476                }
477                Some(serde_json::Value::Number(unix_time)) => {
478                    if let Some(secs) = unix_time.as_f64() {
479                        device.activated = DateTime::<Utc>::from_timestamp(
480                            secs.trunc() as i64,
481                            ((secs.fract()) * 1e9) as u32,
482                        )
483                    }
484                }
485                Some(_) => {
486                    return Err(serde::de::Error::custom(
487                        "Invalid type for 'activated' field",
488                    ));
489                }
490                None => {}
491            }
492
493            devices.push(device);
494        }
495
496        Ok(DeviceList { devices })
497    }
498}
499
500/// Poll response with current GPS state
501///
502/// Returns a snapshot of the current GPS fix data from all active devices.
503#[derive(Debug, Clone, PartialEq, Deserialize)]
504pub struct Poll {
505    /// Number of active devices
506    active: Option<i32>,
507    /// Timestamp of this poll
508    time: Option<DateTime<Utc>>,
509    /// TPV data from active devices
510    tpv: Vec<Tpv>,
511    /// GST data from active devices
512    gst: Vec<Gst>,
513    /// Sky view from active devices
514    sky: Vec<Sky>,
515}
516
517/// Error notification from GPSD
518///
519/// Reports errors that occur during GPSD operation.
520#[derive(Debug, Clone, PartialEq, Deserialize)]
521pub struct Error {
522    /// Error message text
523    pub message: String,
524}
525
526/// RTCM2 differential correction data
527///
528/// Real Time Correction Messages version 2.
529/// Currently a placeholder for future implementation.
530#[derive(Debug, Clone, PartialEq, Deserialize)]
531pub struct Rtcm2 {}
532
533/// RTCM3 differential correction data
534///
535/// Real Time Correction Messages version 3.
536/// Currently a placeholder for future implementation.
537#[derive(Debug, Clone, PartialEq, Deserialize)]
538pub struct Rtcm3 {}
539
540// https://gitlab.com/gpsd/gpsd/-/blob/master/libgps/libgps_json.c#L959
541// #[cfg(feature = "ais")]
542// #[derive(Debug, Clone, PartialEq, Deserialize)]
543// pub struct Aivdm {}
544
545/// Raw GPS receiver data
546///
547/// Contains raw measurement data from the GPS receiver,
548/// including pseudoranges, carrier phases, and signal strengths.
549///
550/// Reference: [json_raw_read](https://gitlab.com/gpsd/gpsd/-/blob/master/libgps/libgps_json.c#L219)
551#[derive(Debug, Clone, PartialEq)]
552pub struct Raw {
553    /// Device path that provided this data
554    pub device: Option<String>,
555    /// GPS time of these measurements
556    pub time: Option<DateTime<Utc>>,
557    /// Raw measurement data for each satellite
558    pub rawdata: Vec<Measurement>,
559}
560
561impl<'de> Deserialize<'de> for Raw {
562    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
563    where
564        D: serde::Deserializer<'de>,
565    {
566        #[derive(Deserialize)]
567        struct RawRaw {
568            pub device: Option<String>,
569            pub time: Option<f64>,
570            pub nsec: Option<f64>,
571            pub rawdata: Vec<Measurement>,
572        }
573
574        let raw = RawRaw::deserialize(deserializer)?;
575        let time = match (raw.time, raw.nsec) {
576            (Some(sec), Some(nsec)) => Some(DateTime::<Utc>::from_timestamp(
577                sec.trunc() as i64,
578                ((sec.fract()) * 1e9) as u32 + nsec as u32,
579            )),
580            (Some(sec), None) => Some(DateTime::<Utc>::from_timestamp(
581                sec.trunc() as i64,
582                ((sec.fract()) * 1e9) as u32,
583            )),
584            _ => None,
585        }
586        .flatten();
587
588        Ok(Raw {
589            device: raw.device,
590            time,
591            rawdata: raw.rawdata,
592        })
593    }
594}
595
596/// GPSD response message types
597///
598/// This enum represents all possible response messages from GPSD.
599/// Each variant corresponds to a specific "class" value in the JSON response.
600/// - [libgps_json_unpack](https://gitlab.com/gpsd/gpsd/-/blob/master/libgps/libgps_json.c#L792)
601#[allow(clippy::large_enum_variant)]
602#[derive(Debug, Clone, PartialEq, Deserialize)]
603#[serde(tag = "class", rename_all = "UPPERCASE")]
604pub enum Message {
605    /// Time-Position-Velocity report
606    Tpv(Tpv),
607    /// GPS pseudorange error statistics
608    Gst(Gst),
609    /// Satellite sky view report
610    Sky(Sky),
611    /// Attitude/orientation data
612    Att(Attitude),
613    /// Inertial measurement unit data
614    Imu(Imu),
615    /// List of available GPS devices
616    Devices(DeviceList),
617    /// Single GPS device information
618    Device(Device),
619    /// Current watch settings
620    Watch(Watch),
621    /// GPSD version information
622    Version(Version),
623    /// RTCM2 differential correction data
624    Rtcm2(Rtcm2),
625    /// RTCM3 differential correction data
626    Rtcm3(Rtcm3),
627    // AIS vessel data (commented out)
628    // Ais(Aivdm),
629    /// Error message from GPSD
630    Error(Error),
631    /// Time offset report
632    Toff(TimeOffset),
633    /// Pulse-per-second timing report
634    Pps(Pps),
635    /// Oscillator/clock discipline status
636    Osc(Oscillator),
637    /// Raw GPS receiver data
638    Raw(Raw),
639    /// Poll response with current fixes
640    Poll(Poll),
641    /// Unknown/unsupported message type
642    #[serde(untagged)]
643    Other(String),
644}
645
646/// Helper function to deserialize floating-point Unix timestamps to DateTime
647///
648/// Converts a floating-point number representing seconds since Unix epoch
649/// to a DateTime<Utc> object, preserving sub-second precision.
650fn f64_to_datetime<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
651where
652    D: serde::Deserializer<'de>,
653{
654    let opt = Option::<f64>::deserialize(deserializer)?;
655    Ok(opt.and_then(|float| {
656        DateTime::<Utc>::from_timestamp(float.trunc() as i64, ((float.fract()) * 1e9) as u32)
657    }))
658}
659
660/// Helper function to convert separate seconds and nanoseconds to DateTime
661///
662/// Combines Unix timestamp seconds and nanoseconds into a DateTime<Utc> object.
663fn deserialize_to_datetime(sec: Option<i64>, nsec: Option<i64>) -> Option<DateTime<Utc>> {
664    match (sec, nsec) {
665        (Some(sec), Some(nsec)) => DateTime::<Utc>::from_timestamp(sec, nsec as u32),
666        _ => None,
667    }
668}