|
| 1 | +"""Send position report periodically |
| 2 | +
|
| 3 | +Extrernal (to X-Plane) application to detect OOOI ACARS message changes and generate appropriate message. |
| 4 | +
|
| 5 | +/data2/19//N/POSITION REPORT OVHD HABBS AT 1249Z/18700 PPOS:4512.2N/07425.3W AT 1249Z/18700 TO COMAU AT 1252Z NEXT MITIG WIND 325/23 SAT -20 ETA 1304Z SPEED 265 GND SPEED 354 VERT SPEED -2000FPM HDG 68 TRK 71 |
| 6 | +/data2/18//N/POSITION REPORT OVHD ARVIE AT 1247Z/FL221 PPOS:4507.0N/07437.2W AT 1247Z/FL221 TO HABBS AT 1249Z NEXT COMAU WIND 350/21 SAT -27 ETA 1304Z SPEED 260 GND SPEED 357 VERT SPEED -1900FPM HDG 69 TRK 73 |
| 7 | +
|
| 8 | +""" |
| 9 | + |
| 10 | +import logging |
| 11 | +import os |
| 12 | +from re import DEBUG |
| 13 | +import sys |
| 14 | +import threading |
| 15 | +from datetime import datetime, timezone |
| 16 | +from enum import StrEnum |
| 17 | +from typing import Dict, Any, Tuple |
| 18 | + |
| 19 | +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) |
| 20 | + |
| 21 | +from lat_lon_parser import to_deg_min |
| 22 | +from unitutil import convert |
| 23 | + |
| 24 | +import xpwebapi |
| 25 | + |
| 26 | +FORMAT = "[%(asctime)s] %(levelname)s %(threadName)s %(filename)s:%(funcName)s:%(lineno)d: %(message)s" |
| 27 | +logging.basicConfig(level=logging.INFO, format=FORMAT, datefmt="%H:%M:%S") |
| 28 | + |
| 29 | +logging.basicConfig(level=logging.INFO) |
| 30 | +logger = logging.getLogger(__name__) |
| 31 | +logger.setLevel(logging.INFO) |
| 32 | + |
| 33 | + |
| 34 | +version = "1.0.0" |
| 35 | + |
| 36 | + |
| 37 | +class DREFS(StrEnum): |
| 38 | + DAYS = "sim/time/local_date_days" |
| 39 | + ZULU_SECS = "sim/time/zulu_time_sec" |
| 40 | + GROUND_SPEED = "sim/flightmodel2/position/groundspeed" |
| 41 | + AGL = "sim/flightmodel/position/y_agl" |
| 42 | + ALT = "sim/cockpit/pressure/cabin_altitude_actual_ft" |
| 43 | + TRK = "sim/cockpit2/gauges/indicators/ground_track_mag_pilot" # The ground track of the aircraft in degrees magnetic |
| 44 | + HDG = "sim/cockpit2/gauges/indicators/compass_heading_deg_mag" # Indicated heading of the wet compass, in degrees. |
| 45 | + INDICATED_AIRSPEED = "sim/flightmodel/position/indicated_airspeed" |
| 46 | + VS = "sim/flightmodel/position/local_vy" |
| 47 | + LATITUDE = "sim/flightmodel/position/latitude" |
| 48 | + LONGITUDE = "sim/flightmodel/position/longitude" |
| 49 | + WINDDIR = "sim/weather/aircraft/wind_now_direction_degt" |
| 50 | + WINDSPD = "sim/weather/aircraft/wind_speed_kts" |
| 51 | + WINDSPD_M = "sim/weather/aircraft/wind_now_speed_msc" |
| 52 | + AIR_TEMP = "sim/weather/aircraft/temperature_ambient_deg_c" |
| 53 | + |
| 54 | + |
| 55 | +def now() -> datetime: |
| 56 | + return datetime.now(timezone.utc) |
| 57 | + |
| 58 | + |
| 59 | +class PositionReport: |
| 60 | + |
| 61 | + def __init__(self, api, frequency: int, callsign: str, logon: str, station: str, eta: datetime | None = None) -> None: |
| 62 | + self.name = type(self).__name__ |
| 63 | + self.ws = api |
| 64 | + |
| 65 | + self.datarefs = {path: self.ws.dataref(path) for path in self.get_dataref_names()} |
| 66 | + self._report_thread = threading.Thread(target=self.report_loop, name="ACARS Progress Report") |
| 67 | + self._report_run = threading.Event() |
| 68 | + self.frequency = frequency |
| 69 | + |
| 70 | + self.ws.add_callback(cbtype=xpwebapi.CALLBACK_TYPE.ON_DATAREF_UPDATE, callback=self.dataref_changed) |
| 71 | + |
| 72 | + def get_dataref_names(self) -> set: |
| 73 | + return DREFS |
| 74 | + |
| 75 | + def dataref_changed(self, dataref, value): |
| 76 | + self.datarefs[dataref].value = value |
| 77 | + |
| 78 | + def dataref_value(self, dataref: str): |
| 79 | + dref = self.datarefs.get(dataref) |
| 80 | + return dref.value if dref is not None else 0 |
| 81 | + |
| 82 | + def run(self): |
| 83 | + self._report_thread.start() |
| 84 | + ws.connect() |
| 85 | + ws.wait_connection() |
| 86 | + ws.monitor_datarefs(datarefs=self.datarefs, reason=self.name) |
| 87 | + self._report_thread.start() |
| 88 | + ws.start() |
| 89 | + |
| 90 | + def terminate(self): |
| 91 | + self._report_run.set() |
| 92 | + ws.unmonitor_datarefs(datarefs=self.datarefs, reason=self.name) |
| 93 | + self.ws.disconnect() |
| 94 | + |
| 95 | + def report(self) -> str: |
| 96 | + def f(dref: str, rnd: int = 0) -> int | float: |
| 97 | + val = self.dataref_value(dataref=dref) |
| 98 | + if val is not None: |
| 99 | + return int(val) if rnd == 0 else round(val, rnd) |
| 100 | + return 0 |
| 101 | + |
| 102 | + lat = self.dataref_value(DREFS.LATITUDE) |
| 103 | + ldeg, lmin = to_deg_min(lat) |
| 104 | + latstr = f"{ldeg:02d}{lmin:04.1f}{'N' if ldeg >=0 else 'S'}" |
| 105 | + lon = self.dataref_value(DREFS.LONGITUDE) |
| 106 | + ldeg, lmin = to_deg_min(lon) |
| 107 | + lonstr = f"{ldeg:03d}{lmin:04.1f}{'E' if ldeg >=0 else 'W'}" |
| 108 | + |
| 109 | + zulustr = now().strftime("%H%M") |
| 110 | + |
| 111 | + vs = self.dataref_value(DREFS.VS) |
| 112 | + vs = convert.ms_to_fpm(ms=vs) |
| 113 | + vs = round(vs/100) * 100 |
| 114 | + alt = self.dataref_value(DREFS.ALT) |
| 115 | + if alt < 8000: |
| 116 | + altstr = f"{round(alt/10) * 10}" |
| 117 | + else: |
| 118 | + altstr = convert.meters_to_fl(convert.feet_to_meters(ft=alt)) |
| 119 | + |
| 120 | + # find weather parameters for layer |
| 121 | + wind_dir = int(self.dataref_value(DREFS.WINDDIR)) |
| 122 | + wind_speed = int(self.dataref_value(DREFS.WINDSPD)) |
| 123 | + sat = int(self.dataref_value(DREFS.AIR_TEMP)) |
| 124 | + |
| 125 | + return " ".join([ |
| 126 | + "POSITION REPORT", |
| 127 | + f"PPOS:{latstr}/{lonstr} AT {zulustr}Z/{altstr}", |
| 128 | + f"WIND {wind_dir}/{wind_speed} SAT {saturation}", |
| 129 | + f"SPEED {f(DREFS.INDICATED_AIRSPEED)} GND SPEED {f(DREFS.GROUND_SPEED)} VERT SPEED {vs}FPM", |
| 130 | + f"HDG {f(DREFS.HDG)} TRK {f(DREFS.TRK)}", |
| 131 | + "PARTIAL AUTOGEN" |
| 132 | + ]) |
| 133 | + |
| 134 | + def report_loop(self): |
| 135 | + loop = True |
| 136 | + while loop: |
| 137 | + try: |
| 138 | + logger.debug(self.report()) |
| 139 | + except: |
| 140 | + logger.warning("error producing report") |
| 141 | + if self._report_run.wait(self.frequency): |
| 142 | + loop = False |
| 143 | + |
| 144 | + |
| 145 | +if __name__ == "__main__": |
| 146 | + ws = xpwebapi.ws_api() |
| 147 | + pr = PositionReport(ws, frequency=5, callsign="BEL034", logon="none", station="EBJA") |
| 148 | + try: |
| 149 | + pr.run() |
| 150 | + except KeyboardInterrupt: |
| 151 | + logger.warning("terminating..") |
| 152 | + pr.terminate() |
| 153 | + logger.warning("..terminated") |
0 commit comments