GPS Cockpit Tool (ThinkPad T520)

Dieses Python-Programm liest GPS-Daten über ein internes Modem aus und zeigt sie in einer grafischen Oberfläche mit Karte an.

⚠️ Dieses Tool ist speziell für Lenovo ThinkPad T520 mit GPS-Modem entwickelt.
Die Nutzung erfolgt auf eigene Gefahr.

Voraussetzungen

Start

python3 gps_dashboard.py

Kompletter Code

#!/usr/bin/env python3

import sys
import serial
import time
from PyQt5 import QtWidgets, QtCore, QtGui, QtWebEngineWidgets
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer

# --- WEB SERVER ---
class WebServerThread(QtCore.QThread):
    def __init__(self, port=8000):
        super().__init__()
        self.port = port
        self.httpd = None

    def run(self):
        class ReusableTCPServer(TCPServer):
            allow_reuse_address = True
        handler = SimpleHTTPRequestHandler
        try:
            self.httpd = ReusableTCPServer(("127.0.0.1", self.port), handler)
            self.httpd.serve_forever()
        except: pass

    def stop(self):
        if self.httpd:
            self.httpd.shutdown()

# --- GPS POLLER ---
class GPSPoller(QtCore.QThread):
    data = QtCore.pyqtSignal(dict)
    debug = QtCore.pyqtSignal(str)

    def __init__(self, port="/dev/ttyACM0"):
        super().__init__()
        self.port = port
        self.running = True
        self.state = {
            "lat": None, "lon": None, "speed": 0, "course": 0,
            "hdop": None, "fix": False, "alt": None, "utc": None, "sats": []
        }
        self.tmp_sats = []

    def run(self):
        try:
            # Wichtig: Höherer Timeout für Initialisierung
            self.ser = serial.Serial(self.port, 115200, timeout=1.0)
            self.debug.emit(f" Port offen: {self.port}")
        except Exception as e:
            self.debug.emit(f" Fehler: {e}")
            return

        # Wake up
        for _ in range(2):
            self.ser.write(b"AT\r")
            time.sleep(0.1)

        # Initialisierungsbefehle
        cmds = [b"AT+CFUN=1\r", b"AT*E2GPSCTL=1,1,1\r", b"AT*E2GPSNMEA=1\r"]
        for c in cmds:
            self.ser.write(c)
            time.sleep(0.5)
            r = self.ser.read_all().decode(errors="ignore").replace("\r", " ").replace("\n", " ")
            self.debug.emit(f"CMD {c.decode().strip()} -> {r}")

        self.ser.timeout = 0.1 # Zurück auf schnelles Lesen
        buffer = ""

        while self.running:
            try:
                if self.ser.in_waiting:
                    raw = self.ser.read(self.ser.in_waiting).decode(errors="ignore")
                    buffer += raw

                    # Wir suchen nach vollständigen NMEA Sätzen
                    while "$" in buffer and "\n" in buffer:
                        # Alles vor dem ersten $ wegwerfen
                        start_idx = buffer.find("$")
                        buffer = buffer[start_idx:]
                        
                        # Ende der Zeile suchen
                        end_idx = buffer.find("\n")
                        if end_idx == -1: break # Unvollständig
                        
                        line = buffer[:end_idx].strip()
                        buffer = buffer[end_idx+1:]

                        if line:
                            # self.debug.emit(f"RAW: {line}") # Zum Testen einkommentieren
                            parsed = self.parse_nmea(line)
                            if parsed:
                                self.data.emit(parsed)
            except Exception as e:
                self.debug.emit(f"Loop Fehler: {e}")
            time.sleep(0.05)

        self.ser.write(b"AT*E2GPSCTL=0,1,1\r")
        self.ser.close()

    def parse_nmea(self, line):
        # Wir splitten und entfernen mögliche Checksummen-Reste
        if "*" in line:
            line = line.split("*")[0]
        p = line.split(",")
        
        try:
            if "GGA" in p[0] and len(p) > 9:
                # GGA: $GPGGA,time,lat,N,lon,E,fix,sats,hdop,alt...
                self.state["lat"] = self.conv(p[2], p[3])
                self.state["lon"] = self.conv(p[4], p[5])
                self.state["fix"] = int(p[6]) > 0
                if p[8]: self.state["hdop"] = float(p[8])
                if p[9]: self.state["alt"] = float(p[9])
                
            elif "RMC" in p[0] and len(p) > 9:
                if p[7]: self.state["speed"] = float(p[7]) * 1.852
                if p[8]: self.state["course"] = float(p[8])
                
                if p[1] and len(p[1]) >= 6:
                    # Wir extrahieren nur HH, MM, SS
                    hh = p[1][0:2]
                    mm = p[1][2:4]
                    ss = p[1][4:6]
                    # Das Datum ignorieren wir komplett
                    self.state["utc"] = f"{hh}:{mm}:{ss}"
  
            elif "GSV" in p[0] and len(p) > 3:
                msg_num = int(p[2])
                if msg_num == 1: self.tmp_sats = []
                for i in range(4, len(p) - 3, 4):
                    if i+3 < len(p) and p[i]:
                        self.tmp_sats.append({"prn": p[i], "elev": p[i+1], "az": p[i+2], "snr": p[i+3]})
                if msg_num == int(p[1]):
                    self.state["sats"] = self.tmp_sats
        except: pass
        return self.state.copy()

    def conv(self, val, d):
        if not val or len(val) < 4: return None
        try:
            dot = val.find(".")
            deg = float(val[:dot-2])
            minutes = float(val[dot-2:])
            res = deg + (minutes / 60)
            if d in ["S", "W"]: res *= -1
            return res
        except: return None

# --- UI (Vereinfacht für Stabilität) ---
class Cockpit(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.web_port = 8000
        self.server = WebServerThread(self.web_port)
        self.server.start()
        self.init_ui()

    def init_ui(self):
        self.setWindowTitle("ThinkPad GPS Cockpit PRO")
        layout = QtWidgets.QGridLayout()
        
        self.lbl_pos = QtWidgets.QLabel("Suche Satelliten...")
        self.lbl_utc = QtWidgets.QLabel("UTC: --:--:--")
        self.lbl_fix = QtWidgets.QLabel("NO FIX")
        self.lbl_fix.setStyleSheet("color: red; font-weight: bold")
        
        layout.addWidget(self.lbl_utc, 0, 0)
        layout.addWidget(self.lbl_fix, 0, 1)
        layout.addWidget(self.lbl_pos, 1, 0, 1, 2)

        self.map = QtWebEngineWidgets.QWebEngineView()
        self.write_map()
        self.map.load(QtCore.QUrl(f"http://127.0.0.1:{self.web_port}/map.html"))
        layout.addWidget(self.map, 2, 0, 1, 4)

        self.log = QtWidgets.QTextEdit()
        self.log.setMaximumHeight(100)
        layout.addWidget(self.log, 3, 0, 1, 4)

        self.btn = QtWidgets.QPushButton("Start GPS")
        self.btn.clicked.connect(self.toggle)
        layout.addWidget(self.btn, 4, 0, 1, 4)

        self.setLayout(layout)
        self.active = False

    def write_map(self):
        with open("map.html", "w") as f:
            f.write("""
""") def toggle(self): if not self.active: self.poller = GPSPoller("/dev/ttyACM0") # HIER PORT ANPASSEN FALLS NÖTIG self.poller.data.connect(self.upd) self.poller.debug.connect(lambda m: self.log.append(m)) self.poller.start() self.btn.setText("Stop GPS") self.active = True else: self.poller.running = False self.btn.setText("Start GPS") self.active = False def upd(self, d): if d["lat"]: self.lbl_pos.setText(f"LAT: {d['lat']:.6f} | LON: {d['lon']:.6f} | ALT: {d['alt']}m") self.map.page().runJavaScript(f"update({d['lat']},{d['lon']})") # Nur die Zeit anzeigen, falls vorhanden if d["utc"]: self.lbl_utc.setText(f"UTC Zeit: {d['utc']}") else: self.lbl_utc.setText("UTC Zeit: --:--:--") #if d["utc"]: self.lbl_utc.setText(f"UTC: {d['utc']}") self.lbl_fix.setText("FIX OK" if d["fix"] else "NO FIX") self.lbl_fix.setStyleSheet("color: green" if d["fix"] else "color: red") if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) c = Cockpit() c.resize(900, 600) c.show() sys.exit(app.exec_())

Hinweis

Dieses Projekt wurde mit Unterstützung von KI (ChatGPT) sowie eigenen Anpassungen entwickelt.