From 1df3c869994f733579be4ff6df7e8bddf8c46c56 Mon Sep 17 00:00:00 2001 From: George Hotz <72895+geohot@users.noreply.github.com> Date: Thu, 24 Mar 2022 23:07:26 -0700 Subject: [PATCH] sensord: add support for Quectel raw gps (#24027) * connecting to rawgps * dumping rawfps packets * this works, but it might be useless * fix parsing * parsing 0x1476 * compare * compare better * refactoring * parsing and broadcasting GPS packet * glonass support * compare using submaster * fix compare for glonass * update cereal * make linter happy * other linter issue * last mypy complaints * add support for publishing gpsLocationExternal * set flags to 1 to match ubloxd * ready to ship * qcomdiag * use unused variables * last one Co-authored-by: Comma Device Co-authored-by: Adeeb Shihadeh --- selfdrive/manager/process_config.py | 3 + selfdrive/sensord/rawgps/compare.py | 66 ++++++++ selfdrive/sensord/rawgps/modemdiag.py | 101 ++++++++++++ selfdrive/sensord/rawgps/rawgpsd.py | 201 +++++++++++++++++++++++ selfdrive/sensord/rawgps/structs.py | 224 ++++++++++++++++++++++++++ 5 files changed, 595 insertions(+) create mode 100755 selfdrive/sensord/rawgps/compare.py create mode 100644 selfdrive/sensord/rawgps/modemdiag.py create mode 100755 selfdrive/sensord/rawgps/rawgpsd.py create mode 100644 selfdrive/sensord/rawgps/structs.py diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index bc88154c2cb444..da4284571ffadc 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -42,6 +42,9 @@ PythonProcess("rtshield", "selfdrive.rtshield", enabled=EON), PythonProcess("shutdownd", "selfdrive.hardware.eon.shutdownd", enabled=EON), PythonProcess("androidd", "selfdrive.hardware.eon.androidd", enabled=EON, persistent=True), + + # Experimental + PythonProcess("rawgpsd", "selfdrive.sensord.rawgps.rawgpsd", enabled=os.path.isfile("/persist/comma/use-quectel-rawgps")), ] managed_processes = {p.name: p for p in procs} diff --git a/selfdrive/sensord/rawgps/compare.py b/selfdrive/sensord/rawgps/compare.py new file mode 100755 index 00000000000000..073647499bcf52 --- /dev/null +++ b/selfdrive/sensord/rawgps/compare.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import cereal.messaging as messaging +from laika import constants + +if __name__ == "__main__": + sm = messaging.SubMaster(['ubloxGnss', 'qcomGnss']) + + meas = None + while 1: + sm.update() + if sm['ubloxGnss'].which() == "measurementReport": + meas = sm['ubloxGnss'].measurementReport.measurements + if not sm.updated['qcomGnss'] or meas is None: + continue + report = sm['qcomGnss'].measurementReport + if report.source not in [0, 1]: + continue + GLONASS = report.source == 1 + recv_time = report.milliseconds / 1000 + + car = [] + print("qcom has ", list(sorted([x.svId for x in report.sv]))) + print("ublox has", list(sorted([x.svId for x in meas if x.gnssId == (6 if GLONASS else 0)]))) + for i in report.sv: + # match to ublox + tm = None + for m in meas: + if i.svId == m.svId and m.gnssId == 0 and m.sigId == 0 and not GLONASS: + tm = m + if (i.svId-64) == m.svId and m.gnssId == 6 and m.sigId == 0 and GLONASS: + tm = m + if tm is None: + continue + + if not i.measurementStatus.measurementNotUsable and i.measurementStatus.satelliteTimeIsKnown: + sat_time = (i.unfilteredMeasurementIntegral + i.unfilteredMeasurementFraction + i.latency) / 1000 + ublox_psuedorange = tm.pseudorange + qcom_psuedorange = (recv_time - sat_time)*constants.SPEED_OF_LIGHT + if GLONASS: + glonass_freq = tm.glonassFrequencyIndex - 7 + ublox_speed = -(constants.SPEED_OF_LIGHT / (constants.GLONASS_L1 + glonass_freq*constants.GLONASS_L1_DELTA)) * (tm.doppler) + else: + ublox_speed = -(constants.SPEED_OF_LIGHT / constants.GPS_L1) * tm.doppler + qcom_speed = i.unfilteredSpeed + car.append((i.svId, tm.pseudorange, ublox_speed, qcom_psuedorange, qcom_speed, tm.cno)) + + if len(car) == 0: + print("nothing to compare") + continue + + pr_err, speed_err = 0., 0. + for c in car: + ublox_psuedorange, ublox_speed, qcom_psuedorange, qcom_speed = c[1:5] + pr_err += ublox_psuedorange - qcom_psuedorange + speed_err += ublox_speed - qcom_speed + pr_err /= len(car) + speed_err /= len(car) + print("avg psuedorange err %f avg speed err %f" % (pr_err, speed_err)) + for c in sorted(car, key=lambda x: abs(x[1] - x[3] - pr_err)): + svid, ublox_psuedorange, ublox_speed, qcom_psuedorange, qcom_speed, cno = c + print("svid: %3d pseudorange: %10.2f m speed: %8.2f m/s meas: %12.2f speed: %10.2f meas_err: %10.3f speed_err: %8.3f cno: %d" % + (svid, ublox_psuedorange, ublox_speed, qcom_psuedorange, qcom_speed, + ublox_psuedorange - qcom_psuedorange - pr_err, ublox_speed - qcom_speed - speed_err, cno)) + + + diff --git a/selfdrive/sensord/rawgps/modemdiag.py b/selfdrive/sensord/rawgps/modemdiag.py new file mode 100644 index 00000000000000..21e39f4f37a487 --- /dev/null +++ b/selfdrive/sensord/rawgps/modemdiag.py @@ -0,0 +1,101 @@ +import os +import time +from serial import Serial +from crcmod import mkCrcFun +from struct import pack, unpack_from, calcsize + +class ModemDiag: + def __init__(self): + self.serial = self.open_serial() + + def open_serial(self): + def op(): + return Serial("/dev/ttyUSB0", baudrate=115200, rtscts=True, dsrdtr=True) + try: + serial = op() + except Exception: + # TODO: this is a hack to get around modemmanager's exclusive open + print("unlocking serial...") + os.system('sudo su -c \'echo "1-1.1:1.0" > /sys/bus/usb/drivers/option/unbind\'') + os.system('sudo su -c \'echo "1-1.1:1.0" > /sys/bus/usb/drivers/option/bind\'') + time.sleep(0.5) + os.system("sudo chmod 666 /dev/ttyUSB0") + serial = op() + serial.flush() + return serial + + ccitt_crc16 = mkCrcFun(0x11021, initCrc=0, xorOut=0xffff) + ESCAPE_CHAR = b'\x7d' + TRAILER_CHAR = b'\x7e' + + def hdlc_encapsulate(self, payload): + payload += pack('= 3 + assert payload[-1:] == self.TRAILER_CHAR + payload = payload[:-1] + payload = payload.replace(bytes([self.ESCAPE_CHAR[0], self.TRAILER_CHAR[0] ^ 0x20]), self.TRAILER_CHAR) + payload = payload.replace(bytes([self.ESCAPE_CHAR[0], self.ESCAPE_CHAR[0] ^ 0x20]), self.ESCAPE_CHAR) + assert payload[-2:] == pack(' NoReturn: + unpack_gps_meas, size_gps_meas = dict_unpacker(gps_measurement_report, True) + unpack_gps_meas_sv, size_gps_meas_sv = dict_unpacker(gps_measurement_report_sv, True) + + unpack_glonass_meas, size_glonass_meas = dict_unpacker(glonass_measurement_report, True) + unpack_glonass_meas_sv, size_glonass_meas_sv = dict_unpacker(glonass_measurement_report_sv, True) + + log_types = [ + LOG_GNSS_GPS_MEASUREMENT_REPORT, + LOG_GNSS_GLONASS_MEASUREMENT_REPORT, + ] + pub_types = ['qcomGnss'] + if int(os.getenv("PUBLISH_EXTERNAL", "0")) == 1: + unpack_position, _ = dict_unpacker(position_report) + log_types.append(LOG_GNSS_POSITION_REPORT) + pub_types.append("gpsLocationExternal") + + os.system("mmcli -m 0 --location-enable-gps-raw --location-enable-gps-nmea") + diag = ModemDiag() + + def try_setup_logs(diag, log_types): + for _ in range(5): + try: + setup_logs(diag, log_types) + break + except Exception: + pass + + def disable_logs(sig, frame): + os.system("mmcli -m 0 --location-disable-gps-raw --location-disable-gps-nmea") + cloudlog.warning("rawgpsd: shutting down") + try_setup_logs(diag, []) + cloudlog.warning("rawgpsd: logs disabled") + sys.exit(0) + signal.signal(signal.SIGINT, disable_logs) + try_setup_logs(diag, log_types) + cloudlog.warning("rawgpsd: setup logs done") + + pm = messaging.PubMaster(pub_types) + + while 1: + opcode, payload = diag.recv() + assert opcode == DIAG_LOG_F + (pending_msgs, log_outer_length), inner_log_packet = unpack_from(' 0: + cloudlog.debug("have %d pending messages" % pending_msgs) + assert log_outer_length == len(inner_log_packet) + (log_inner_length, log_type, log_time), log_payload = unpack_from(' Accelerometer BIT[1] 0x00000002 <96> Gyro 0x0000FFFC - Reserved A bit set to 1 indicates that certain fields as defined by the SENSOR_AIDING_MASK were aided with sensor data*/ + uint32 q_SensorAidMask; /* Denotes which component of the position report was assisted with additional sensors defined in SENSOR_DATA_USAGE_MASK BIT[0] 0x00000001 <96> Heading aided with sensor data BIT[1] 0x00000002 <96> Speed aided with sensor data BIT[2] 0x00000004 <96> Position aided with sensor data BIT[3] 0x00000008 <96> Velocity aided with sensor data 0xFFFFFFF0 <96> Reserved */ + uint8 u_NumGpsSvsUsed; /* The number of GPS SVs used in the fix */ + uint8 u_TotalGpsSvs; /* Total number of GPS SVs detected by searcher, including ones not used in position calculation */ + uint8 u_NumGloSvsUsed; /* The number of Glonass SVs used in the fix */ + uint8 u_TotalGloSvs; /* Total number of Glonass SVs detected by searcher, including ones not used in position calculation */ + uint8 u_NumBdsSvsUsed; /* The number of BeiDou SVs used in the fix */ + uint8 u_TotalBdsSvs; /* Total number of BeiDou SVs detected by searcher, including ones not used in position calculation */ +""" + +def name_to_camelcase(nam): + ret = [] + i = 0 + while i < len(nam): + if nam[i] == "_": + ret.append(nam[i+1].upper()) + i += 2 + else: + ret.append(nam[i]) + i += 1 + return ''.join(ret) + +def parse_struct(ss): + st = "<" + nams = [] + for l in ss.strip().split("\n"): + typ, nam = l.split(";")[0].split() + #print(typ, nam) + if typ == "float" or '_Flt' in nam: + st += "f" + elif typ == "double" or '_Dbl' in nam: + st += "d" + elif typ in ["uint8", "uint8_t"]: + st += "B" + elif typ in ["int8", "int8_t"]: + st += "b" + elif typ in ["uint32", "uint32_t"]: + st += "I" + elif typ in ["int32", "int32_t"]: + st += "i" + elif typ in ["uint16", "uint16_t"]: + st += "H" + elif typ in ["int16", "int16_t"]: + st += "h" + elif typ == "uint64": + st += "Q" + else: + print("unknown type", typ) + assert False + if '[' in nam: + cnt = int(nam.split("[")[1].split("]")[0]) + st += st[-1]*(cnt-1) + for i in range(cnt): + nams.append("%s[%d]" % (nam.split("[")[0], i)) + else: + nams.append(nam) + return st, nams + +def dict_unpacker(ss, camelcase = False): + st, nams = parse_struct(ss) + if camelcase: + nams = [name_to_camelcase(x) for x in nams] + sz = calcsize(st) + return lambda x: dict(zip(nams, unpack_from(st, x))), sz