500 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			500 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
| """
 | |
| NMEA0183 verarbeiten
 | |
| 
 | |
| TODO Multi-Sentence verarbeiten
 | |
|   TXT Textmeldungen (Alarm)
 | |
|   RTE Routendaten
 | |
|   AIS-Sentences mit Binärdaten
 | |
| """
 | |
| 
 | |
| import serial
 | |
| from setproctitle import setthreadtitle
 | |
| import pynmea2
 | |
| 
 | |
| # Empfangsthread
 | |
| def rxd_0183(appdata,boatdata, devname):
 | |
|     # Prüfe ob NMEA0183-Port vorhanden ist und sich öffnen läßt
 | |
|     try:
 | |
|         ser = serial.Serial(devname, 115200, timeout=3)
 | |
|     except serial.SerialException as e:
 | |
|         print("NMEA0183 serial port not available")
 | |
|         return
 | |
|     setthreadtitle("0183listener")
 | |
|     while not appdata.shutdown:
 | |
|         raw = ser.readline().decode('ascii')
 | |
|         if len(raw.strip()) == 0:
 | |
|             continue
 | |
|         try:
 | |
|             msg = pynmea2.parse(raw)
 | |
|         except pynmea2.nmea.ParseError:
 | |
|             print(f"NMEA0183: Parse-Error: {raw}", end='')
 | |
|             continue
 | |
|         # sentence_type kann fehlen
 | |
|         try:
 | |
|             stype = msg.sentence_type
 | |
|         except:
 | |
|             print(f"NMEA0183: Sentence type missing: {raw}")
 | |
|             continue
 | |
|         # WIP Neuer Code aus Modul
 | |
|         # TODO Filter mit gewünschen Satztypen
 | |
|         # if stype in stypefilter:
 | |
|         #     continue
 | |
|         if stype in decoder:
 | |
|             decoder[stype](boatdata, msg)
 | |
|         else:
 | |
|             # Hier unbekannter Satztyp: protokollieren und ignorieren
 | |
|             """
 | |
|             ['checksum', 'data', 'fields', 'identifier', 'name_to_idx', 'parse',
 | |
|              'proprietary_re', 'query_re', 'render', 'sentence_re',
 | |
|              'sentence_type', 'sentence_types', 'talker', 'talker_re']
 | |
|             """
 | |
|             print(f"Nicht implementiert: '{stype}' from {msg.talker}")
 | |
| 
 | |
|     ser.close()
 | |
| 
 | |
| def DBS(boatdata, msg):
 | |
|     # Wassertiefe unter der Oberfläche
 | |
|     pass
 | |
|     #boatdata.setValue("DBS", msg.depth)
 | |
| 
 | |
| def DBT(boatdata, msg):
 | |
|     # Wassertiefe unter Geber
 | |
|     pass
 | |
|     #boatdata.setValue("DBT", msg.depth)
 | |
| 
 | |
| def DPT(boatdata, msg):
 | |
|     # Depth
 | |
|     print("-> DPT")
 | |
|     print(msg.fields)
 | |
|     print(msg.data)
 | |
|     #boatdata.setValue("DBT", msg.depth)
 | |
| 
 | |
| def GBS(boatdata, msg):
 | |
|     # GNSS satellite fault detection
 | |
|     """
 | |
| (('Timestamp', 'timestamp', <function timestamp at 0x7f59cb0b65c0>), ('Expected error in latitude', 'lat_err'), ('Expected error in longitude', 'lon_err'), ('Expected error in altitude', 'alt_err'), ('PRN of most likely failed satellite', 'sat_prn_num_f'), ('Probability of missed detection for most likely failed satellite', 'pro_miss', <class 'decimal.Decimal'>), ('Estimate of bias in meters on most likely failed satellite', 'est_bias'), ('Standard deviation of bias estimate', 'est_bias_dev'))
 | |
| ['213024.00', '0.9', '0.6', '2.5', '', '', '', '']
 | |
| NMEA0183: Parse-Error: !AIVDM,1,1,,A,H3ti3hPpDhiT0    """
 | |
|     print("-> GBS")
 | |
|     print(msg.fields)
 | |
|     print(msg.data)
 | |
|     #boatdata.setValue("LAT", msg.latitude)
 | |
| 
 | |
| def GGA(boatdata, msg):
 | |
|     # Time, position, and fix related data
 | |
|     # msg.num_sats
 | |
|     # msg.timestamp
 | |
|     if msg.gps_qual == 0:
 | |
|         # No fix
 | |
|         return
 | |
|     if msg.lat_dir == 'N':
 | |
|         boatdata.setValue("LAT", msg.latitude)
 | |
|     else:
 | |
|         boatdata.setValue("LAT", msg.latitude * -1)
 | |
|     if msg.lon_dir == 'E':
 | |
|         boatdata.setValue("LON", msg.longitude)
 | |
|     else:
 | |
|         boatdata.setValue("LON", msg.longitude * -1)
 | |
|     boatdata.setValue("HDOP", msg.horizontal_dil)
 | |
| 
 | |
| def GLL(boatdata, msg):
 | |
|     # Position data: position fix, time of position fix, and status
 | |
|     if not msg.status == 'A':
 | |
|         return
 | |
|     lat_fac = 1 if msg.lat_dir == 'N' else -1
 | |
|     lat_deg = int(msg.lat[0:2])
 | |
|     lat_min = float(msg.lat[2:])
 | |
|     boatdata.setValue("LAT", lat_fac * lat_deg + lat_min / 60)
 | |
|     lon_fac = 1 if msg.lon_dir == 'E' else -1
 | |
|     lon_deg = int(msg.lon[0:3])
 | |
|     lon_min = float(msg.lon[3:])
 | |
|     boatdata.setValue("LON", lon_fac * lon_deg + lon_min / 60)
 | |
|     boatdata.setValue("TSPOS", msg.timestamp) # datetime.time, UTC
 | |
| 
 | |
| def GSA(boatdata, msg):
 | |
|     # Satellites
 | |
|     for i in range(1, 13):
 | |
|         satno = getattr(msg, f"sv_id{i:02}")
 | |
|         if (len(satno) > 0) and not satno in boatdata.sat:
 | |
|             boatdata.addSatellite(int(satno))
 | |
|     boatdata.setValue("PDOP", float(msg.pdop))
 | |
|     boatdata.setValue("PDOP", float(msg.hdop))
 | |
|     boatdata.setValue("PDOP", float(msg.vdop))
 | |
| 
 | |
| def GSV(boatdata, msg):
 | |
|     # Satellites in view
 | |
|     # mgs_num msg.num_messages # Nachricht n von m
 | |
|     # msg.num_sv_in_view # Anzahl sichtbarer Satelliten
 | |
|     rres = None  # range residuals
 | |
|     for i in range(1, 5):
 | |
|         prn_num = getattr(msg, f"sv_prn_num_{i}")
 | |
|         if len(prn_num) > 0:
 | |
|             elevation = float(getattr(msg, f"elevation_deg_{i}"))
 | |
|             azimuth = float(getattr(msg, f"azimuth_{i}"))
 | |
|             snr = getattr(msg, f"snr_{i}")
 | |
|             if len(snr) == 0:
 | |
|                 snr = 0
 | |
|                 status = 1 # prnusage tracked
 | |
|             else:
 | |
|                 status = 2 # prnusage used
 | |
|             boatdata.updateSatellite(int(prn_num), elevation, azimuth, int(snr), rres, status)
 | |
|     """
 | |
|     if msg.sv_prn_num_1:
 | |
|         if msg.snr_1 == '':
 | |
|             status = 1
 | |
|             msg.snr_1 = 0
 | |
|         boatdata.updateSatellite(int(msg.sv_prn_num_1), float(msg.elevation_deg_1), float(msg.azimuth_1), int(msg.snr_1), rres, status)
 | |
|     if msg.sv_prn_num_2:
 | |
|         if msg.snr_2 == '':
 | |
|             status = 1
 | |
|             msg.snr_2 = 0
 | |
|         boatdata.updateSatellite(int(msg.sv_prn_num_2), float(msg.elevation_deg_2), float(msg.azimuth_2), int(msg.snr_2), rres, status)
 | |
|     if msg.sv_prn_num_3:
 | |
|         if msg.snr_3 == '':
 | |
|             status = 1
 | |
|             msg.snr_3 = 0
 | |
|         boatdata.updateSatellite(int(msg.sv_prn_num_3), float(msg.elevation_deg_3), float(msg.azimuth_3), int(msg.snr_3), rres, status)
 | |
|     if msg.sv_prn_num_4:
 | |
|         if msg.snr_4 == '':
 | |
|             status = 1
 | |
|             msg.snr_4 = 0
 | |
|         boatdata.updateSatellite(int(msg.sv_prn_num_4), float(msg.elevation_deg_4), float(msg.azimuth_4), int(msg.snr_4), rres, status)
 | |
|     """
 | |
| 
 | |
| def HDG(boatdata, msg):
 | |
|     # UNUSED: Heading  - Deviation & Variation
 | |
|     # Magnetic Sensor heading in degrees
 | |
|     # msg.heading
 | |
|     # .deviation, dev_dir E/W
 | |
|     # .variation, var_dir E/W
 | |
|     pass
 | |
| 
 | |
| def HDM(boatdata, msg):
 | |
|     # Heading magnetic
 | |
|     if msg.magnetic == 'M':
 | |
|         boatdata.setValue("HDM", msg.heading)
 | |
|     else:
 | |
|         print("HDM: M not set!")
 | |
| 
 | |
| def HDT(boatdata, msg):
 | |
|     # Heading True
 | |
|     if msg.hdg_true == 'T':
 | |
|         boatdata.setValue("HDT", msg.heading)
 | |
|     else:
 | |
|         print("HDT: T not set!")
 | |
| 
 | |
| def HTD(boatdata, msg):
 | |
|     # Heading/Track control data
 | |
|     # e.g. $YDHTD,V,1.5,,R,N,,,,,,,,,A,,,*48
 | |
|     print("-> HTD")
 | |
|     print(msg.fields)
 | |
| 
 | |
| def MWV(boatdata, msg):
 | |
|     # Windgeschwindigkeit und -winkel
 | |
|     print(f"Wind: {msg.wind_angle}° {msg.wind_speed}kt")
 | |
|     boatdata.setValue("AWA", msg.wind_angle)
 | |
|     boatdata.setValue("AWS", msg.wind_speed)
 | |
| 
 | |
| def MTW(boatdata, msg):
 | |
|     # Wassertemperatur
 | |
|     # boatdata.setValue("WTemp", msg.xxx)
 | |
|     print("-> MTW Wassertemperatur")
 | |
| 
 | |
| def RMB(boatdata, msg):
 | |
|     # Recommended Minimum Navigation Information
 | |
|     # Informationen bzgl. Erreichen des nächsten Wegepunkts
 | |
|     #
 | |
|     # (('Status', 'status'),
 | |
|     # ('Cross Track Error', 'cross_track_error'),
 | |
|     #('Cross Track Error, direction to corrent', 'cte_correction_dir'),
 | |
|     #('Origin Waypoint ID', 'origin_waypoint_id'),
 | |
|     # ('Destination Waypoint ID', 'dest_waypoint_id'),
 | |
|     #('Destination Waypoint Latitude', 'dest_lat'),
 | |
|     #('Destination Waypoint Lat Direction', 'dest_lat_dir'),
 | |
|     # ('Destination Waypoint Longitude', 'dest_lon'),
 | |
|     #('Destination Waypoint Lon Direction', 'dest_lon_dir'),
 | |
|     #('Range to Destination', 'dest_range'),
 | |
|     #('True Bearing to Destination', 'dest_true_bearing'),
 | |
|     #('Velocity Towards Destination', 'dest_velocity'),
 | |
|     #('Arrival Alarm', 'arrival_alarm'))
 | |
|     print("-> RMB")
 | |
|     if not msg.status == 'A':
 | |
|         return
 | |
|     lat_fac = 1 if msg.dest_lat_dir == 'N' else -1
 | |
|     lat_deg = int(msg.dest_lat[0:2])
 | |
|     lat_min = float(msg.dest_lat[2:])
 | |
|     lon_fac = 1 if msg.dest_lon_dir == 'E' else -1
 | |
|     lon_deg = int(msg.dest_lon[0:3])
 | |
|     lon_min = float(msg.dest_lon[3:])
 | |
|     boatdata.setValue("WPLat", lat_fac * lat_deg + lat_min / 60)
 | |
|     boatdata.setValue("WPLon", lon_fac * lon_deg + lon_min / 60)
 | |
|     boatdata.setValue("WPname", msg.dest_waypoint_id)
 | |
|     boatdata.setValue("DTW", float(msg.dest_range))
 | |
|     boatdata.setValue("BTW", float(msg.dest_true_bearing))
 | |
| 
 | |
| def RMC(boatdata, msg):
 | |
|     # Recommended Minimum Navigation Information
 | |
|     #print("-> RMC")
 | |
|     # print(msg.timestamp, msg.datestamp)
 | |
|     # print(msg.status) # V=Warning, P=Precise, A=OK
 | |
|     # mag_variation, mag_var_dir E/W
 | |
|     # TODO nav_status welche Bedeutung?
 | |
|     if not msg.status == 'A':
 | |
|         return
 | |
|     lat_fac = 1 if msg.lat_dir == 'N' else -1
 | |
|     lat_deg = int(msg.lat[0:2])
 | |
|     lat_min = float(msg.lat[2:])
 | |
|     boatdata.setValue("LAT", lat_fac * lat_deg + lat_min / 60)
 | |
|     lon_fac = 1 if msg.lon_dir == 'E' else -1
 | |
|     lon_deg = int(msg.lon[0:3])
 | |
|     lon_min = float(msg.lon[3:])
 | |
|     boatdata.setValue("LON", lon_fac * lon_deg + lon_min / 60)
 | |
|     if msg.spd_over_grnd:
 | |
|         boatdata.setValue("SOG", float(msg.spd_over_grnd))
 | |
|     if msg.true_course:
 | |
|         boatdata.setValue("COG", float(msg.true_course))
 | |
| 
 | |
| def ROT(boatdata, msg):
 | |
|     # Rate Of Turn
 | |
|     # print("-> ROT")
 | |
|     if msg.status == 'A':
 | |
|         boatdata.setValue("ROT", msg.rate_of_turn)
 | |
| 
 | |
| def RSA(boatdata, msg):
 | |
|     # Rudder Sensor Angle
 | |
|     # negative Werte bedeuten Backbord
 | |
|     #print("-> RSA")
 | |
|     # Boatdata: RPOS primär, PRPOS sekundär
 | |
|     if msg.rsa_starboard_status== 'A':
 | |
|         boatdata.setValue("RPOS", msg.rsa_starboard)
 | |
|     if msg.rsa_port_status == 'A':
 | |
|         boatdata.setValue("PRPOS", msg.rsa_port)
 | |
| 
 | |
| rte_curr = 0
 | |
| rte_max = 0
 | |
| rte_wpl = []
 | |
| 
 | |
| def RTE(boatdata, msg):
 | |
|     # Route: List of Waypoints
 | |
|     # num_in_seq, sen_num, start_type, active_route_id
 | |
|     global rte_curr, rte_max
 | |
|     nmax = int(msg.sen_num)
 | |
|     n = int(msg.num_in_seq)
 | |
|     if nmax > 1:
 | |
|         if rte_curr == 0 and n == 1:
 | |
|             # neue Nachricht
 | |
|             pass
 | |
|         else:
 | |
|             # Fortsetzung
 | |
|             pass
 | |
|     else:
 | |
|         pass
 | |
|     print("-> RTE")
 | |
|     print(msg.fields)
 | |
|     print(msg.waypoint_list)
 | |
| 
 | |
| txt_msg = ''
 | |
| txt_type = None
 | |
| txt_curr = 0
 | |
| txt_max = 0
 | |
| 
 | |
| def TXT(boatdata, msg):
 | |
|     # Text Transmission (e.G. Alarms)
 | |
|     global txt_msg, txt_type, txt_curr, txt_max
 | |
|     #print("-> TXT")
 | |
|     nmax = int(msg.num_msg)
 | |
|     n = int(msg.msg_num)
 | |
|     if nmax > 1:
 | |
|         if txt_curr == 0 and n == 1:
 | |
|             # neue Nachricht
 | |
|             txt_msg = msg.text.rstrip('\r\n')
 | |
|             txt_type = msg.msg_type
 | |
|             txt_curr = 1
 | |
|             txt_max = nmax
 | |
|         else:
 | |
|             # Fortsetzung
 | |
|             if txt_curr == n - 1 and txt_type == msg.msg_type:
 | |
|                 txt_msg += msg.text.rstrip('\r\n')
 | |
|                 txt_curr = n
 | |
|             if n == nmax:
 | |
|                 # Vollständig!
 | |
|                 print(f"TXT: {msg.msg_type} - {txt_msg}")
 | |
|                 if not boatdata.alarm:
 | |
|                     # Momentan wird kein bereits anstehender Alarm überschrieben
 | |
|                     boatdata.alarm_msg = txt_msg.strip()
 | |
|                     boatdata.alarm = True
 | |
|                     boatdata.alarm_id = msg.msg_type
 | |
|                     boatdata.alarm_src = "NMEA0183"
 | |
|                 txt_curr = 0
 | |
|                 txt_max = 0
 | |
|     else:
 | |
|         print(f"TXT: {msg.msg_type} - {msg.text}", end='')
 | |
|         if not boatdata.alarm:
 | |
|             # Momentan wird kein bereits anstehender Alarm überschrieben
 | |
|             boatdata.alarm_msg = msg.text.strip()
 | |
|             boatdata.alarm = True
 | |
|             boatdata.alarm_id = msg.msg_type
 | |
|             boatdata.alarm_src = "NMEA0183"
 | |
| 
 | |
| def VBW(boatdata, msg):
 | |
|     print("-> VBW")
 | |
| 
 | |
| def VHW(boatdata, msg):
 | |
|     # Heading und Geschwindigkeit durch das Wasser
 | |
|     # Aktuelle Meßdaten von Sensoren
 | |
|     # msg.heading_true # degrees decimal
 | |
|     # msg.heading_magnetic degrees decimal
 | |
|     # print("-> VHW")
 | |
|     if msg.heading_true is not None:
 | |
|         boatdata.setValue("HDT", float(msg.heading_true))
 | |
|     if msg.heading_magnetic is not None:
 | |
|         boatdata.setValue("HDM", float(msg.heading_magnetic))
 | |
|     boatdata.setValue("STW", float(msg.water_speed_knots))
 | |
| 
 | |
| def VPW(boatdata, msg):
 | |
|     # UNUSED: Speed - Measured Parallel to Wind
 | |
|     # print(f"-> VPW: {msg.speed_kn} kn")
 | |
|     pass
 | |
| 
 | |
| def VTG(boatdata, msg):
 | |
|     """
 | |
|     Track made good and speed over ground
 | |
|     Calculated from previous GPS positions
 | |
|     Only true track is used at the moment. magnetic is ignored
 | |
|     """
 | |
|     if msg.faa_mode != 'A':
 | |
|         return
 | |
|     #TODO klären was für Typen hier ankommen können
 | |
|     # bytearray, str, decimal.Decimal?
 | |
|     #str von OpenCPN: sog = float(msg.spd_over_grnd_kts[:-1])
 | |
|     #Ggf. ist OpenCPN buggy!
 | |
|     cog = float(msg.true_track)        # in Grad
 | |
|     sog = float(msg.spd_over_grnd_kts) # in Knoten
 | |
|     boatdata.setValue("COG", cog)
 | |
|     boatdata.setValue("SOG", sog)
 | |
| 
 | |
| def VWR(boatdata, msg):
 | |
|     # Relative Wind Speed and Angle
 | |
|     #print("-> VWR")
 | |
|     if msg.l_r == "R":
 | |
|         angle = msg.deg_r
 | |
|     else:
 | |
|         angle = 360 - msg.deg_r 
 | |
|     boatdata.setValue("AWA", angle)
 | |
|     boatdata.setValue("AWS", msg.wind_speed_ms)
 | |
| 
 | |
| def WPL(boatdata, msg):
 | |
|     # Waypoint
 | |
|     # lat, lat_dir
 | |
|     # lon, lon_dir
 | |
|     # waypoint_id (name)
 | |
|     print("-> WPL")
 | |
|     print(msg.fields)
 | |
| 
 | |
|     lat_fac = 1 if msg.lat_dir == 'N' else -1
 | |
|     lat_deg = int(msg.lat[0:2])
 | |
|     lat_min = float(msg.lat[2:])
 | |
|     #boatdata.setValue("LAT", lat_fac * lat_deg + lat_min / 60)
 | |
|     lon_fac = 1 if msg.lon_dir == 'E' else -1
 | |
|     lon_deg = int(msg.lon[0:3])
 | |
|     lon_min = float(msg.lon[3:])
 | |
|     #boatdata.setValue("LON", lon_fac * lon_deg + lon_min / 60)
 | |
| 
 | |
|     # Prüfe, ob der Wegepunkt schon existiert
 | |
|     if msg.waypoint_id in boatdata.wps:
 | |
|         # Wegepunkt aktualisieren
 | |
|         pass
 | |
|     else:
 | |
|         # neuen Wegepunkt anlegen
 | |
|         # boatdata.wps[waypoint_id]
 | |
|         pass
 | |
| 
 | |
| 
 | |
| def VWT(boatdata, msg):
 | |
|     # True Wind Speed and Angle
 | |
|     if msg.direction == "R":
 | |
|         angle = msg.wind_angle_vessel
 | |
|     else:
 | |
|         angle = 360 - msg.wind_angle_vessel
 | |
|     boatdata.setValue("TWA", angle)
 | |
|     boatdata.setValue("TWS", msg.wind_speed_meters)
 | |
| 
 | |
| def XDR(boatdata, msg):
 | |
|     # Extra sensor data / Transducer Measurement
 | |
|     # type, value, units, id
 | |
|     # type: A
 | |
|     # units: D
 | |
|     # id: Yaw
 | |
|     if msg.id.lower() == 'yaw':
 | |
|         boatdata.setValue("YAW", float(msg.value))
 | |
|     elif msg.id.lower() == 'ptch':
 | |
|         boatdata.setValue("PTCH", float(msg.value))
 | |
|     elif msg.id.lower() == 'roll':
 | |
|         boatdata.setValue("ROLL", float(msg.value))
 | |
|     elif msg.id.lower() == 'barometer':
 | |
|         boatdata.setValue("xdrPress", float(msg.value))
 | |
|     else:
 | |
|         print(f"-> XDR: {msg.type}, {msg.value}, {msg.units}, {msg.id}")
 | |
| 
 | |
| def XTE(boatdata, msg):
 | |
|     # Cross Track error measured
 | |
|     print("-> XTE")
 | |
| 
 | |
| def XTR(boatdata, msg):
 | |
|     # Cross Track error gekoppelt
 | |
|     print("-> XTR")
 | |
| 
 | |
| def ZDA(boatdata, msg):
 | |
|     # Time and date
 | |
|     print("-> XTE")
 | |
|     #boatdata.gpsd
 | |
|     #boatdata.gpst
 | |
| 
 | |
| # AIS
 | |
| def VDM(boatdata, msg):
 | |
|     print("-> VDM")
 | |
|     print(msg.fields)
 | |
|     print(msg.data)
 | |
| 
 | |
| def VDO(boatdata, msg):
 | |
|     print("-> VDO")
 | |
| 
 | |
| 
 | |
| # Aus Performancegründen eine direkte Sprungtabelle, ggf. können
 | |
| # zukünftig außer der Funktion noch weitere Daten gespeichert werdeb
 | |
| decoder = {
 | |
|     "DBS": DBS,
 | |
|     "DBT": DBT,
 | |
|     "DPT": DPT,
 | |
|     "GBS": GBS,
 | |
|     "GGA": GGA,
 | |
|     "GLL": GLL,
 | |
|     "GSA": GSA,
 | |
|     "GSV": GSV,
 | |
|     "HDG": HDG,
 | |
|     "HDM": HDM,
 | |
|     "HDT": HDT,
 | |
|     "HTD": HTD,
 | |
|     "MWV": MWV,
 | |
|     "MTW": MTW,
 | |
|     "RMB": RMB,
 | |
|     "ROT": ROT,
 | |
|     "RMC": RMC,
 | |
|     "RSA": RSA,
 | |
|     "RTE": RTE,
 | |
|     "TXT": TXT,
 | |
|     "VBW": VBW,
 | |
|     "VHW": VHW,
 | |
|     "VPW": VPW,
 | |
|     "VTG": VTG,
 | |
|     "VWR": VWR,
 | |
|     "VWT": VWT,
 | |
|     "WPL": WPL,
 | |
|     "XDR": XDR,
 | |
|     "XTE": XTE,
 | |
|     "XTR": XTR,
 | |
|     "ZDA": ZDA,
 | |
| 
 | |
|     "VDM": VDM
 | |
| }
 |