gpsd_json/client/
blocking.rs

1//! Blocking (synchronous) GPSD client implementation
2//!
3//! This module provides a synchronous version of the GPSD client for
4//! applications that don't require asynchronous I/O. It offers the same
5//! functionality as the async client but with blocking operations.
6
7use std::io::BufRead;
8use std::net::{TcpStream, ToSocketAddrs};
9
10use crate::client::{Json, Nmea, Raw, StreamFormat, StreamOptions};
11use crate::error::GpsdJsonError;
12use crate::protocol::{GpsdJsonDecode, GpsdJsonEncode, v3};
13use crate::{Result, client::GpsdJsonProtocol};
14
15/// Core implementation of a blocking GPSD client
16///
17/// This struct provides the fundamental functionality for synchronous
18/// communication with a GPSD server. It handles protocol negotiation,
19/// message serialization/deserialization, and maintains the connection state.
20///
21/// # Type Parameters
22/// * `Stream` - The underlying I/O stream type (e.g., TcpStream)
23/// * `Proto` - The GPSD protocol version implementation
24#[derive(Debug)]
25pub struct GpsdClientCore<Stream, Proto> {
26    reader: std::io::BufReader<Stream>,
27    buf: Vec<u8>,
28    _proto: std::marker::PhantomData<Proto>,
29}
30
31impl<Stream, Proto> GpsdClientCore<Stream, Proto>
32where
33    Proto: GpsdJsonProtocol,
34{
35    /// Opens a new GPSD client connection using the provided stream
36    ///
37    /// This method initializes the client with the given I/O stream and
38    /// performs protocol version negotiation with the GPSD server.
39    ///
40    /// # Arguments
41    /// * `stream` - The I/O stream for communication with GPSD
42    ///
43    /// # Returns
44    /// * `Ok(client)` - Successfully connected and negotiated protocol
45    /// * `Err(_)` - Connection or protocol negotiation failed
46    pub fn open(stream: Stream) -> Result<Self>
47    where
48        Stream: std::io::Read + std::io::Write,
49    {
50        let reader = std::io::BufReader::new(stream);
51        let mut client = GpsdClientCore {
52            reader,
53            buf: Vec::new(),
54            _proto: std::marker::PhantomData,
55        };
56
57        client.ensure_version()?;
58        Ok(client)
59    }
60
61    /// Sends a request message to the GPSD server
62    fn send(&mut self, msg: &Proto::Request) -> Result<()>
63    where
64        Stream: std::io::Write,
65    {
66        self.reader.get_mut().write_request(msg)
67    }
68
69    /// Receives a response message from the GPSD server
70    ///
71    /// Returns `None` if the connection is closed.
72    fn recv(&mut self) -> Result<Option<Proto::Response>>
73    where
74        Stream: std::io::Read,
75    {
76        self.buf.clear();
77        match self.reader.read_response(&mut self.buf)? {
78            Some(resp) => Ok(Some(resp)),
79            None => Ok(None), // EOF reached
80        }
81    }
82
83    /// Ensures the connected GPSD server supports this protocol version
84    ///
85    /// Reads the version message from GPSD and verifies compatibility.
86    /// The client requires the major version to match exactly and the
87    /// minor version to be greater than or equal to the expected version.
88    fn ensure_version(&mut self) -> Result<()>
89    where
90        Stream: std::io::Read,
91    {
92        self.buf.clear();
93        if let Ok(Some(v3::ResponseMessage::Version(version))) =
94            self.reader.read_response(&mut self.buf)
95        {
96            if Proto::API_VERSION_MAJOR != version.proto_major
97                || Proto::API_VERSION_MINOR < version.proto_minor
98            {
99                Err(GpsdJsonError::UnsupportedProtocolVersion((
100                    version.proto_major,
101                    version.proto_minor,
102                )))
103            } else {
104                Ok(())
105            }
106        } else {
107            Err(GpsdJsonError::ProtocolError(
108                "Failed to read version message from GPSD",
109            ))
110        }
111    }
112}
113
114impl<Proto> GpsdClientCore<TcpStream, Proto>
115where
116    Proto: GpsdJsonProtocol,
117{
118    /// Connects to a GPSD server over TCP
119    ///
120    /// Creates a TCP connection to the specified address and initializes
121    /// a GPSD client with protocol negotiation.
122    ///
123    /// # Arguments
124    /// * `addr` - Socket address of the GPSD server (e.g., "127.0.0.1:2947")
125    ///
126    /// # Returns
127    /// * `Ok(client)` - Successfully connected to GPSD
128    /// * `Err(_)` - Connection failed or protocol negotiation failed
129    ///
130    /// # Example
131    /// ```no_run
132    /// # use gpsd_json::client::blocking::GpsdClient;
133    /// let client = GpsdClient::connect("127.0.0.1:2947").unwrap();
134    /// ```
135    pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<Self> {
136        let stream = TcpStream::connect(addr).map_err(GpsdJsonError::IoError)?;
137        Self::open(stream)
138    }
139}
140
141impl<Proto> TryFrom<TcpStream> for GpsdClientCore<TcpStream, Proto>
142where
143    Proto: GpsdJsonProtocol,
144{
145    type Error = GpsdJsonError;
146
147    fn try_from(stream: TcpStream) -> Result<Self> {
148        Self::open(stream)
149    }
150}
151
152/// Type alias for a GPSD client using protocol version 3
153///
154/// This is the most common client type and should be used for
155/// connecting to modern GPSD servers (version 3.x).
156#[cfg(feature = "proto-v3")]
157pub type GpsdClient<Stream> = GpsdClientCore<Stream, v3::V3>;
158
159impl<Stream> GpsdClient<Stream>
160where
161    Stream: std::io::Read + std::io::Write,
162{
163    /// Requests version information from the GPSD server
164    ///
165    /// Returns details about the GPSD server version, protocol version,
166    /// and capabilities.
167    pub fn version(&mut self) -> Result<v3::response::Version> {
168        self.send(&v3::RequestMessage::Version)?;
169        if let Some(v3::ResponseMessage::Version(version)) = self.recv()? {
170            Ok(version)
171        } else {
172            Err(GpsdJsonError::ProtocolError(
173                "Expected version response from GPSD",
174            ))
175        }
176    }
177
178    /// Lists all GPS devices known to the GPSD server
179    ///
180    /// Returns information about each connected GPS receiver including
181    /// device paths, driver information, and current status.
182    pub fn devices(&mut self) -> Result<v3::response::DeviceList> {
183        self.send(&v3::RequestMessage::Devices)?;
184        if let Some(v3::ResponseMessage::Devices(devices)) = self.recv()? {
185            Ok(devices)
186        } else {
187            Err(GpsdJsonError::ProtocolError(
188                "Expected devices response from GPSD",
189            ))
190        }
191    }
192
193    /// Gets information about the currently active GPS device
194    ///
195    /// Returns detailed information about the device currently being
196    /// used for GPS data.
197    pub fn device(&mut self) -> Result<v3::types::Device> {
198        self.send(&v3::RequestMessage::Device(None))?;
199        if let Some(v3::ResponseMessage::Device(device)) = self.recv()? {
200            Ok(device)
201        } else {
202            Err(GpsdJsonError::ProtocolError(
203                "Expected device response from GPSD",
204            ))
205        }
206    }
207
208    /// Enables data streaming from GPSD with default settings
209    ///
210    /// Returns the current watch configuration and list of available devices.
211    /// After calling this method, GPS data will be streamed from the server.
212    pub fn watch(&mut self) -> Result<(v3::types::Watch, v3::response::DeviceList)> {
213        self.send(&v3::RequestMessage::Watch(None))?;
214        let Some(v3::ResponseMessage::Devices(devices)) = self.recv()? else {
215            return Err(GpsdJsonError::ProtocolError(
216                "Expected devices response from GPSD",
217            ));
218        };
219        let Some(v3::ResponseMessage::Watch(watch)) = self.recv()? else {
220            return Err(GpsdJsonError::ProtocolError(
221                "Expected watch response from GPSD",
222            ));
223        };
224
225        Ok((watch, devices))
226    }
227
228    /// Polls for the current GPS fix data
229    ///
230    /// Returns the most recent GPS fix information available from
231    /// all active devices.
232    pub fn poll(&mut self) -> Result<v3::response::Poll> {
233        self.send(&v3::RequestMessage::Poll)?;
234        if let Some(v3::ResponseMessage::Poll(poll)) = self.recv()? {
235            Ok(poll)
236        } else {
237            Err(GpsdJsonError::ProtocolError(
238                "Expected poll response from GPSD",
239            ))
240        }
241    }
242
243    /// Enables or disables data streaming mode
244    ///
245    /// # Arguments
246    /// * `enable` - true to start streaming, false to stop
247    pub fn watch_mode(&mut self, enable: bool) -> Result<()> {
248        let (watch, _devices) = self.set_watch(v3::types::Watch {
249            enable: Some(enable),
250            ..Default::default()
251        })?;
252
253        assert_eq!(watch.enable, Some(enable));
254        Ok(())
255    }
256
257    /// Starts a data stream with the specified format and options
258    ///
259    /// This method consumes the client and returns a stream iterator
260    /// that yields GPS data in the requested format.
261    ///
262    /// # Arguments
263    /// * `opts` - Stream configuration options
264    ///
265    /// # Example
266    /// ```no_run
267    /// # use gpsd_json::client::blocking::GpsdClient;
268    /// # use gpsd_json::client::StreamOptions;
269    /// # let client = GpsdClient::connect("127.0.0.1:2947").unwrap();
270    /// let stream = client.stream(StreamOptions::json()).unwrap();
271    /// ```
272    pub fn stream<Format: StreamFormat>(
273        mut self,
274        opts: StreamOptions<Format>,
275    ) -> Result<GpsdDataStream<Stream, v3::V3, Format>> {
276        let (watch, _devices) = self.set_watch(opts.inner)?;
277        assert_eq!(watch.enable, Some(true));
278
279        Ok(GpsdDataStream {
280            inner: self,
281            _format: std::marker::PhantomData,
282        })
283    }
284
285    /// Configures watch mode settings
286    ///
287    /// Internal method to set watch parameters and receive confirmation.
288    fn set_watch(
289        &mut self,
290        watch: v3::types::Watch,
291    ) -> Result<(v3::types::Watch, v3::response::DeviceList)> {
292        self.send(&v3::RequestMessage::Watch(Some(watch)))?;
293        let Some(v3::ResponseMessage::Devices(devices)) = self.recv()? else {
294            return Err(GpsdJsonError::ProtocolError(
295                "Expected devices response from GPSD",
296            ));
297        };
298        let Some(v3::ResponseMessage::Watch(watch)) = self.recv()? else {
299            return Err(GpsdJsonError::ProtocolError(
300                "Expected watch response from GPSD",
301            ));
302        };
303
304        Ok((watch, devices))
305    }
306}
307
308/// Iterator for streaming GPS data from GPSD
309///
310/// This struct provides an iterator interface for receiving continuous
311/// GPS data from a GPSD server. The format of the data depends on the
312/// stream format type parameter.
313///
314/// The stream continues until explicitly closed or an error occurs.
315pub struct GpsdDataStream<Stream, Proto, Format>
316where
317    Proto: GpsdJsonProtocol,
318    Format: StreamFormat,
319{
320    inner: GpsdClientCore<Stream, Proto>,
321    _format: std::marker::PhantomData<Format>,
322}
323
324impl<Stream, Format> GpsdDataStream<Stream, v3::V3, Format>
325where
326    Stream: std::io::Read + std::io::Write,
327    Format: StreamFormat,
328{
329    /// Closes the data stream and returns the client
330    ///
331    /// This method stops the GPS data stream and returns the underlying
332    /// client for further operations.
333    pub fn close(mut self) -> Result<GpsdClient<Stream>> {
334        let watch = v3::types::Watch::default();
335
336        let (watch, _devices) = self.inner.set_watch(watch)?;
337        assert_eq!(watch.enable, Some(false));
338
339        Ok(self.inner)
340    }
341}
342
343impl<Stream, Proto> Iterator for GpsdDataStream<Stream, Proto, Json>
344where
345    Stream: std::io::Read,
346    Proto: GpsdJsonProtocol,
347{
348    type Item = Result<Proto::Response>;
349
350    fn next(&mut self) -> Option<Self::Item> {
351        self.inner.recv().transpose()
352    }
353}
354
355impl<Stream, Proto> Iterator for GpsdDataStream<Stream, Proto, Nmea>
356where
357    Stream: std::io::Read,
358    Proto: GpsdJsonProtocol,
359{
360    type Item = Result<String>;
361
362    fn next(&mut self) -> Option<Self::Item> {
363        self.inner.buf.clear();
364
365        match self.inner.reader.read_until(b'\n', &mut self.inner.buf) {
366            Ok(0) => None, // EOF reached
367            Ok(_) => Some(Ok(String::from_utf8_lossy(&self.inner.buf)
368                .trim_end()
369                .to_string())),
370            Err(e) => Some(Err(GpsdJsonError::IoError(e))),
371        }
372    }
373}
374
375impl<Stream, Proto> Iterator for GpsdDataStream<Stream, Proto, Raw>
376where
377    Stream: std::io::Read,
378    Proto: GpsdJsonProtocol,
379{
380    type Item = Result<String>;
381
382    fn next(&mut self) -> Option<Self::Item> {
383        self.inner.buf.clear();
384
385        match self.inner.reader.read_until(b'\n', &mut self.inner.buf) {
386            Ok(0) => None, // EOF reached
387            Ok(_) => Some(Ok(String::from_utf8_lossy(&self.inner.buf)
388                .trim_end()
389                .to_string())),
390            Err(e) => Some(Err(GpsdJsonError::IoError(e))),
391        }
392    }
393}