semuconsulting/PyGPSClient

PyGPSClient becomes unresponsive if NMEA & UBX message rates are all set to zero.

semudev2 opened this issue · 1 comments

Describe the bug

PyGPSClient becomes unresponsive if opened with a null serial stream, or if protocol filtering is altered at runtime and no messages of the given protocol are output by the device. Applies to all devices and platforms.

Basically, it enters a race state in the while self._reading and self._serial_object loop within the _read_thread() function as the if self._serial_object.in_waiting condition is never met.

Might be worth considering refactoring serial_handler in PyGPSClient serial_handler to use multiprocessing and queues rather than threads and virtual events, though need to confirm how well this approach would play with tkinter on all platforms (especially MacOS).

To Reproduce

  1. Set all NMEA and UBX message rates to zero.

Expected Behavior

Application should remain responsive. Perhaps disconnect automatically after n minutes of inactivity?

Additional context

Only an issue in practice if the messages rates are all set to zero.

FYI we did have an earlier experiment with using multiprocessing processes as a core engine for PyGPSClient. it worked pretty well on Linux but bombed on MacOS due to issues with pickling Serial streams.

See, for example, following snippet:

"""
ubx_multiproc.py

Example of asynchronous UBXReader message parsing
using threads and multiprocessing queues.

Created on 7 Aug 2021

:author: semuadmin
:copyright: SEMU Consulting © 2021
:license: BSD 3-Clause
"""

import threading
import multiprocessing
import time
from datetime import datetime
from serial import Serial
from pyubx2 import UBXReader


def read_data(
    queue: multiprocessing.Queue,
    ubr: UBXReader,
    stop_event: threading.Event,
    benchmark: bool,
):
    """
    Read and parse data from stream using UBXReader.read() and place
    output (raw_data, parsed_data) data on queue.
    """

    print("Starting read_data\n")
    start = datetime.now()
    read_count = 0

    while not stop_event.is_set():

        ubr_data = None
        try:
            ubr_data = ubr.read()
        except Exception as err:  # pylint: disable=broad-except
            print(f"Error from UBXReader {err}")
            stop_event.set()
            break

        if ubr_data is not None:
            read_count += 1
            queue.put(ubr_data)
        else:
            queue.put("No data from UBXReader.read()")
        if benchmark:
            curr = datetime.now()
            diff = (curr - start).total_seconds()
            rate = int(read_count / diff)
            print(f"read count {read_count} rate {rate}")

    print("\nread_data finished")


def disp_data(
    queue: multiprocessing.Queue, stop_event: threading.Event, benchmark: bool
):
    """
    Get raw and parsed data from queue and display.
    """

    print("Starting disp_data\n")
    start = datetime.now()
    disp_count = 0

    while not stop_event.is_set():

        if queue.empty() is False:
            (_, parsed) = queue.get()
            print(parsed)
            if benchmark:
                disp_count += 1
                curr = datetime.now()
                diff = (curr - start).total_seconds()
                rate = int(disp_count / diff)
                print(f"disp count {disp_count} rate {rate} messages/sec")

    print("\ndisp_data finished")


def main(stream: Serial):
    """
    Main routine
    """

    ubr = UBXReader(stream)
    queue = multiprocessing.Queue()
    stop_event = threading.Event()
    # multiprocessing option works fine on Linux but not MacOS
    # MacOS has problem pickling serial stream
    # read_process = multiprocessing.Process(
    read_process = threading.Thread(
        target=read_data,
        args=(
            queue,
            ubr,
            stop_event,
            BENCHMARK,
        ),
    )
    # disp_process = multiprocessing.Process(
    disp_process = threading.Thread(
        target=disp_data,
        args=(
            queue,
            stop_event,
            BENCHMARK,
        ),
    )

    read_process.start()
    disp_process.start()

    while not stop_event.is_set():
        try:
            time.sleep(0.01)
        except KeyboardInterrupt:  # Capture Ctrl-C
            print("\nCaptured Ctrl-C")
            stop_event.set()

    stop_event.set()

    print("Stopped")
    read_process.join()
    disp_process.join()
    print("Done")


if __name__ == "__main__":

    PORT = "/dev/tty.usbmodem14101"
    # PORT = "COM3"
    BENCHMARK = False

    with Serial(PORT, 9600, timeout=0.1) as arg_stream:
        main(arg_stream)