Auto-detect a GPS source. Detection has four stages, in order of confidence: 0. gpsd socket — returns the sentinel string 'gpsd' if gpsd is running. This avoids port conflicts when gpsd already owns the serial device. 1. by-id symlinks containing GPS keywords (gps, u-blox,
(exclude_ports=None)
| 128 | ['udevadm', 'info', '-a', port], |
| 129 | capture_output=True, text=True, timeout=3 |
| 130 | ) |
| 131 | if r.returncode != 0: |
| 132 | return False |
| 133 | out = r.stdout.lower() |
| 134 | return 'espressif' in out |
| 135 | except Exception: |
| 136 | return False |
| 137 | |
| 138 | |
| 139 | def _probe_nmea(dev, timeout_per_baud=1.5): |
| 140 | """Try common GPS baud rates and look for NMEA sentences. |
| 141 | |
| 142 | Many modules ship at 9600 but Garmin and some older receivers default |
| 143 | to 4800; u-blox configured for higher data rates can be at 38400 or |
| 144 | 115200. We try each in order until we see a recognizable NMEA prefix |
| 145 | or run out of options. |
| 146 | |
| 147 | Returns True if NMEA was seen at any baud, False otherwise. |
| 148 | """ |
| 149 | try: |
| 150 | import serial as pyserial |
| 151 | except ImportError: |
| 152 | return False |
| 153 | for baud in (9600, 4800, 38400, 115200): |
| 154 | try: |
| 155 | with pyserial.Serial(dev, baud, timeout=timeout_per_baud) as ser: |
| 156 | data = ser.read(512).decode('ascii', errors='ignore') |
| 157 | if '$GP' in data or '$GN' in data or '$GL' in data: |
| 158 | return True |
| 159 | except Exception: |
| 160 | continue |
| 161 | return False |
| 162 | |
| 163 | |
| 164 | def _try_gpsd(host='127.0.0.1', port=2947, timeout=2): |
| 165 | """Return an open gpsd socket if gpsd is running and responsive, else None.""" |
| 166 | sock = None |
| 167 | try: |
| 168 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 169 | sock.settimeout(timeout) |
| 170 | sock.connect((host, port)) |
| 171 | sock.sendall(b'?WATCH={"enable":true,"json":true}\n') |
| 172 | data = sock.recv(2048).decode('utf-8', errors='ignore') |
| 173 | if '"class"' in data: |
| 174 | sock.settimeout(None) |
| 175 | return sock |
| 176 | except Exception: |
| 177 | pass |
| 178 | if sock: |
| 179 | try: |
| 180 | sock.close() |
| 181 | except Exception: |
| 182 | pass |
| 183 | return None |
| 184 | |
| 185 | |
| 186 | def detect_gps_device(exclude_ports=None): |
| 187 | """Auto-detect a GPS source. |
no test coverage detected