diff --git a/.gitignore b/.gitignore index 8f1413f5..9b4f4476 100644 --- a/.gitignore +++ b/.gitignore @@ -41,8 +41,8 @@ auto_rx/dfm09mod auto_rx/dft_detect auto_rx/fsk_demod auto_rx/imet1rs_dft +auto_rx/iq_dec auto_rx/lms6Xmod -auto_rx/lms6mod auto_rx/m10mod auto_rx/m20mod auto_rx/mk2a1680mod @@ -52,6 +52,7 @@ auto_rx/imet54mod auto_rx/mXXmod auto_rx/mp3h1mod auto_rx/mts01mod +auto_rx/weathex301d m10 meisei100mod @@ -59,7 +60,6 @@ mk2a_lms1680 demod/dfm09ecc demod/mod/dfm09mod demod/mod/lms6Xmod -demod/mod/lms6mod demod/mod/meisei100mod demod/mod/rs41mod demod/mod/rs92mod @@ -81,3 +81,4 @@ rs_module/rs41mod rs_module/rs92mod scan/reset_usb scan/rs_detect +weathex/weathex301d diff --git a/Makefile b/Makefile index 71addf12..8d3cca86 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ SUBDIRS := \ mk2a \ scan \ utils \ + weathex \ all: $(SUBDIRS) diff --git a/auto_rx/auto_rx.py b/auto_rx/auto_rx.py index 204374cc..85f69d41 100644 --- a/auto_rx/auto_rx.py +++ b/auto_rx/auto_rx.py @@ -8,6 +8,16 @@ # Refer github page for instructions on setup and usage. # https://github.com/projecthorus/radiosonde_auto_rx/ # + +# exit status codes: +# +# 0 - normal termination (ctrl-c) +# 1 - critical error, needs human attention to fix +# 2 - exit because continous running timeout reached +# 3 - exception occurred, can rerun after resetting SDR +# 4 - some of the threads failed to join, SDR reset and restart required +# this is mostly caused by hung external utilities + import argparse import datetime import logging @@ -28,7 +38,6 @@ from autorx.decode import SondeDecoder, VALID_SONDE_TYPES, DRIFTY_SONDE_TYPES from autorx.logger import TelemetryLogger from autorx.email_notification import EmailNotification -from autorx.habitat import HabitatUploader from autorx.aprs import APRSUploader from autorx.ozimux import OziUploader from autorx.sondehub import SondehubUploader @@ -44,6 +53,7 @@ start_flask, stop_flask, flask_emit_event, + flask_running, WebHandler, WebExporter, ) @@ -170,6 +180,7 @@ def start_scanner(): ppm=autorx.sdr_list[_device_idx]["ppm"], bias=autorx.sdr_list[_device_idx]["bias"], save_detection_audio=config["save_detection_audio"], + wideband_sondes=config["wideband_sondes"], temporary_block_list=temporary_block_list, temporary_block_time=config["temporary_block_time"], ) @@ -232,7 +243,7 @@ def start_decoder(freq, sonde_type, continuous=False): _exp_sonde_type = sonde_type if continuous: - _timeout = 0 + _timeout = 3600*6 # 6 hours before a 'continuous' decoder gets restarted automatically. else: _timeout = config["rx_timeout"] @@ -261,7 +272,8 @@ def start_decoder(freq, sonde_type, continuous=False): rs92_ephemeris=rs92_ephemeris, rs41_drift_tweak=config["rs41_drift_tweak"], experimental_decoder=config["experimental_decoders"][_exp_sonde_type], - save_raw_hex=config["save_raw_hex"] + save_raw_hex=config["save_raw_hex"], + wideband_sondes=config["wideband_sondes"] ) autorx.sdr_list[_device_idx]["task"] = autorx.task_list[freq]["task"] @@ -322,7 +334,7 @@ def handle_scan_results(): if (type(_key) == int) or (type(_key) == float): # Extract the currently decoded sonde type from the currently running decoder. _decoding_sonde_type = autorx.task_list[_key]["task"].sonde_type - + # Remove any inverted decoder information for the comparison. if _decoding_sonde_type.startswith("-"): _decoding_sonde_type = _decoding_sonde_type[1:] @@ -432,7 +444,8 @@ def clean_task_list(): else: # Shutdown the SDR, if required for the particular SDR type. - shutdown_sdr(config["sdr_type"], _task_sdr) + if _key != 'SCAN': + shutdown_sdr(config["sdr_type"], _task_sdr, sdr_hostname=config["sdr_hostname"], frequency=_key) # Release its associated SDR. autorx.sdr_list[_task_sdr]["in_use"] = False autorx.sdr_list[_task_sdr]["task"] = None @@ -492,6 +505,12 @@ def stop_all(): for _task in autorx.task_list.keys(): try: autorx.task_list[_task]["task"].stop() + + # Release the SDR channel if necessary + _task_sdr = autorx.task_list[_task]["device_idx"] + if _task != 'SCAN': + shutdown_sdr(config["sdr_type"], _task_sdr, sdr_hostname=config["sdr_hostname"], frequency=_task) + except Exception as e: logging.error("Error stopping task - %s" % str(e)) @@ -640,6 +659,7 @@ def telemetry_filter(telemetry): or ("LMS" in telemetry["type"]) or ("IMET" in telemetry["type"]) or ("MTS01" in telemetry["type"]) + or ("WXR" in telemetry["type"]) ): return "OK" else: @@ -745,9 +765,6 @@ def main(): ) args = parser.parse_args() - # Copy out timeout value, and convert to seconds, - _timeout = args.timeout * 60 - # Copy out RS92 ephemeris value, if provided. if args.ephemeris != "None": rs92_ephemeris = args.ephemeris @@ -770,7 +787,7 @@ def main(): autorx.logging_path = logging_path # Configure logging - _log_suffix = datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S_system.log") + _log_suffix = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d-%H%M%S_system.log") _log_path = os.path.join(logging_path, _log_suffix) system_log_enabled = False @@ -806,6 +823,16 @@ def main(): logging.getLogger("engineio").setLevel(logging.ERROR) logging.getLogger("geventwebsocket").setLevel(logging.ERROR) + # Copy out timeout value, and convert to seconds. + if args.timeout > 0: + logging.info(f"Will shut down automatically after {args.timeout} minutes.") + _timeout = args.timeout * 60 + + # Check all the RS utilities exist. + logging.debug("Checking if required binaries exist") + if not check_rs_utils(config): + sys.exit(1) + # Attempt to read in config file logging.info("Reading configuration file...") _temp_cfg = read_auto_rx_config(args.config) @@ -816,6 +843,7 @@ def main(): config = _temp_cfg autorx.sdr_list = config["sdr_settings"] + # Apply any logging changes based on configuration file settings. if config["save_system_log"]: # Enable system logging. @@ -844,9 +872,6 @@ def main(): web_handler = WebHandler() logging.getLogger().addHandler(web_handler) - # Check all the RS utilities exist. - if not check_rs_utils(): - sys.exit(1) # If a sonde type has been provided, insert an entry into the scan results, # and immediately start a decoder. This also sets the decoder time to 0, which @@ -874,7 +899,10 @@ def main(): # Start our exporter options # Telemetry Logger if config["per_sonde_log"]: - _logger = TelemetryLogger(log_directory=logging_path) + _logger = TelemetryLogger( + log_directory=logging_path, + save_cal_data=config["save_cal_data"] + ) exporter_objects.append(_logger) exporter_functions.append(_logger.add) @@ -897,6 +925,7 @@ def main(): ), launch_notifications=config["email_launch_notifications"], landing_notifications=config["email_landing_notifications"], + encrypted_sonde_notifications=config["email_encrypted_sonde_notifications"], landing_range_threshold=config["email_landing_range_threshold"], landing_altitude_threshold=config["email_landing_altitude_threshold"], ) @@ -905,30 +934,6 @@ def main(): exporter_objects.append(_email_notification) exporter_functions.append(_email_notification.add) - # Habitat Uploader - DEPRECATED - Sondehub DB now in use (>1.5.0) - # if config["habitat_enabled"]: - - # if config["habitat_upload_listener_position"] is False: - # _habitat_station_position = None - # else: - # _habitat_station_position = ( - # config["station_lat"], - # config["station_lon"], - # config["station_alt"], - # ) - - # _habitat = HabitatUploader( - # user_callsign=config["habitat_uploader_callsign"], - # user_antenna=config["habitat_uploader_antenna"], - # station_position=_habitat_station_position, - # synchronous_upload_time=config["habitat_upload_rate"], - # callsign_validity_threshold=config["payload_id_valid"], - # url=config["habitat_url"], - # ) - - # exporter_objects.append(_habitat) - # exporter_functions.append(_habitat.add) - # APRS Uploader if config["aprs_enabled"]: @@ -1073,7 +1078,7 @@ def main(): logging.info("Shutdown time reached. Closing.") stop_flask(host=config["web_host"], port=config["web_port"]) stop_all() - break + sys.exit(2) if __name__ == "__main__": @@ -1084,9 +1089,13 @@ def main(): # Upon CTRL+C, shutdown all threads and exit. stop_flask(host=config["web_host"], port=config["web_port"]) stop_all() + sys.exit(0) except Exception as e: # Upon exceptions, attempt to shutdown threads and exit. traceback.print_exc() print("Main Loop Error - %s" % str(e)) - stop_flask(host=config["web_host"], port=config["web_port"]) + if flask_running(): + stop_flask(host=config["web_host"], port=config["web_port"]) stop_all() + sys.exit(3) + diff --git a/auto_rx/auto_rx.service b/auto_rx/auto_rx.service index b7ad2f76..13db5ecf 100644 --- a/auto_rx/auto_rx.service +++ b/auto_rx/auto_rx.service @@ -3,7 +3,11 @@ Description=auto_rx After=syslog.target [Service] -ExecStart=/usr/bin/python3 /home/pi/radiosonde_auto_rx/auto_rx/auto_rx.py -t 0 +# For running outside of a python virtual environment +# ExecStart=/usr/bin/python3 /home/pi/radiosonde_auto_rx/auto_rx/auto_rx.py -t 0 + +# For running within a venv, located at /home/pi/radiosonde_auto_rx/auto_rx/venv/ +ExecStart=/home/pi/radiosonde_auto_rx/auto_rx/venv/bin/python3 /home/pi/radiosonde_auto_rx/auto_rx/auto_rx.py -t 0 Restart=always RestartSec=120 WorkingDirectory=/home/pi/radiosonde_auto_rx/auto_rx/ diff --git a/auto_rx/auto_rx.sh b/auto_rx/auto_rx.sh index a8a1b567..8b63b696 100755 --- a/auto_rx/auto_rx.sh +++ b/auto_rx/auto_rx.sh @@ -6,16 +6,12 @@ # NOTE: If running this from crontab, make sure to set the appropriate PATH env-vars, # else utilities like rtl_power and rtl_fm won't be found. # -# WARNING - THIS IS DEPRECATED - PLEASE USE THE SYSTEMD SERVICE +# WARNING - THIS IS DEPRECATED - PLEASE USE THE SYSTEMD SERVICE OR DOCKER IMAGE +# See: https://github.com/projecthorus/radiosonde_auto_rx/wiki#451-option-1---operation-as-a-systemd-service-recommended +# Or: https://github.com/projecthorus/radiosonde_auto_rx/wiki/Docker # # change into appropriate directory cd $(dirname $0) -# Clean up old files -rm log_power*.csv - -# Start auto_rx process with a 3 hour timeout. -# auto_rx will exit after this time. - -python3 auto_rx.py -t 180 +python3 auto_rx.py -t 180 \ No newline at end of file diff --git a/auto_rx/autorx/__init__.py b/auto_rx/autorx/__init__.py index 5460786e..e0fab7fb 100644 --- a/auto_rx/autorx/__init__.py +++ b/auto_rx/autorx/__init__.py @@ -12,7 +12,7 @@ # MINOR - New sonde type support, other fairly big changes that may result in telemetry or config file incompatability issus. # PATCH - Small changes, or minor feature additions. -__version__ = "1.6.1-beta2" +__version__ = "1.7.3" # Global Variables diff --git a/auto_rx/autorx/aprs.py b/auto_rx/autorx/aprs.py index eb864496..2af03532 100644 --- a/auto_rx/autorx/aprs.py +++ b/auto_rx/autorx/aprs.py @@ -214,7 +214,7 @@ def generate_station_object( _datum = "!w%s%s!" % (_lat_prec, _lon_prec) # Generate timestamp using current UTC time - _aprs_timestamp = datetime.datetime.utcnow().strftime("%H%M%S") + _aprs_timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%H%M%S") # Add version string to position comment, if requested. _aprs_comment = comment @@ -759,13 +759,19 @@ def close(self): # Wait for all threads to close. if self.upload_thread is not None: - self.upload_thread.join() + self.upload_thread.join(60) + if self.upload_thread.is_alive(): + self.log_error("aprs upload thread failed to join") if self.timer_thread is not None: - self.timer_thread.join() + self.timer_thread.join(60) + if self.timer_thread.is_alive(): + self.log_error("aprs timer thread failed to join") if self.input_thread is not None: - self.input_thread.join() + self.input_thread.join(60) + if self.input_thread.is_alive(): + self.log_error("aprs input thread failed to join") def log_debug(self, line): """ Helper function to log a debug message with a descriptive heading. @@ -801,10 +807,10 @@ def log_warning(self, line): # ['frame', 'id', 'datetime', 'lat', 'lon', 'alt', 'temp', 'type', 'freq', 'freq_float', 'datetime_dt'] test_telem = [ # These types of DFM serial IDs are deprecated - # {'id':'DFM06-123456', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()}, - # {'id':'DFM09-123456', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()}, - # {'id':'DFM15-123456', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()}, - # {'id':'DFM17-12345678', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.utcnow()}, + # {'id':'DFM06-123456', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.now(datetime.timezone.utc)}, + # {'id':'DFM09-123456', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.now(datetime.timezone.utc)}, + # {'id':'DFM15-123456', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.now(datetime.timezone.utc)}, + # {'id':'DFM17-12345678', 'frame':10, 'lat':-10.0, 'lon':10.0, 'alt':10000, 'temp':1.0, 'type':'DFM', 'freq':'401.520 MHz', 'freq_float':401.52, 'heading':0.0, 'vel_h':5.1, 'vel_v':-5.0, 'datetime_dt':datetime.datetime.now(datetime.timezone.utc)}, { "id": "DFM-19123456", "frame": 10, @@ -821,7 +827,7 @@ def log_warning(self, line): "heading": 0.0, "vel_h": 5.1, "vel_v": -5.0, - "datetime_dt": datetime.datetime.utcnow(), + "datetime_dt": datetime.datetime.now(datetime.timezone.utc), }, { "id": "DFM-123456", @@ -839,7 +845,7 @@ def log_warning(self, line): "heading": 0.0, "vel_h": 5.1, "vel_v": -5.0, - "datetime_dt": datetime.datetime.utcnow(), + "datetime_dt": datetime.datetime.now(datetime.timezone.utc), }, { "id": "N1234567", @@ -857,7 +863,7 @@ def log_warning(self, line): "heading": 0.0, "vel_h": 5.1, "vel_v": -5.0, - "datetime_dt": datetime.datetime.utcnow(), + "datetime_dt": datetime.datetime.now(datetime.timezone.utc), }, { "id": "M1234567", @@ -875,7 +881,7 @@ def log_warning(self, line): "heading": 0.0, "vel_h": 5.1, "vel_v": -5.0, - "datetime_dt": datetime.datetime.utcnow(), + "datetime_dt": datetime.datetime.now(datetime.timezone.utc), }, ] diff --git a/auto_rx/autorx/config.py b/auto_rx/autorx/config.py index 8aa77fc7..faf6d544 100644 --- a/auto_rx/autorx/config.py +++ b/auto_rx/autorx/config.py @@ -27,11 +27,10 @@ # Web interface credentials web_password = "none" -# Fixed minimum update rates for APRS & Habitat -# These are set to avoid congestion on the APRS-IS network, and on the Habitat server -# Please respect other users of these networks and leave these settings as they are. +# Fixed minimum update rate for APRS +# This is set to avoid congestion on the APRS-IS network +# Please respect other users of the network and leave this setting as it is. MINIMUM_APRS_UPDATE_RATE = 30 -MINIMUM_HABITAT_UPDATE_RATE = 30 def read_auto_rx_config(filename, no_sdr_test=False): @@ -98,12 +97,9 @@ def read_auto_rx_config(filename, no_sdr_test=False): "radius_temporary_block": False, # "sonde_time_threshold": 3, # Commented out to ensure warning message is shown. # Habitat Settings - "habitat_enabled": False, - "habitat_upload_rate": 30, "habitat_uploader_callsign": "SONDE_AUTO_RX", "habitat_uploader_antenna": "1/4-wave", "habitat_upload_listener_position": False, - "habitat_payload_callsign": "", # APRS Settings "aprs_enabled": False, "aprs_upload_rate": 30, @@ -165,16 +161,12 @@ def read_auto_rx_config(filename, no_sdr_test=False): "save_raw_hex": False, "save_system_log": False, "enable_debug_logging": False, - # URL for the Habitat DB Server. - # As of July 2018 we send via sondehub.org, which will allow us to eventually transition away - # from using the habhub.org tracker, and leave it for use by High-Altitude Balloon Hobbyists. - # For now, sondehub.org just acts as a proxy to habhub.org. - # This setting is not exposed to users as it's only used for unit/int testing - "habitat_url": "https://habitat.sondehub.org/", + "save_cal_data": False, # New Sondehub DB Settings "sondehub_enabled": True, "sondehub_upload_rate": 30, # "sondehub_contact_email": "none@none.com" # Commented out to ensure a warning message is shown on startup + "wideband_sondes": False, # Wideband sonde detection / decoding } try: @@ -296,12 +288,6 @@ def read_auto_rx_config(filename, no_sdr_test=False): auto_rx_config["max_altitude"] = config.getint("filtering", "max_altitude") auto_rx_config["max_radius_km"] = config.getint("filtering", "max_radius_km") - # Habitat Settings - # Deprecated from v1.5.0 - # auto_rx_config["habitat_enabled"] = config.getboolean( - # "habitat", "habitat_enabled" - # ) - # auto_rx_config["habitat_upload_rate"] = config.getint("habitat", "upload_rate") auto_rx_config["habitat_uploader_callsign"] = config.get( "habitat", "uploader_callsign" ) @@ -312,19 +298,6 @@ def read_auto_rx_config(filename, no_sdr_test=False): "habitat", "uploader_antenna" ).strip() - # try: # Use the default configuration if not found - # auto_rx_config["habitat_url"] = config.get("habitat", "url") - # except: - # pass - - # Deprecated from v1.5.0 - # if auto_rx_config["habitat_upload_rate"] < MINIMUM_HABITAT_UPDATE_RATE: - # logging.warning( - # "Config - Habitat Update Rate clipped to minimum of %d seconds. Please be respectful of other users of Habitat." - # % MINIMUM_HABITAT_UPDATE_RATE - # ) - # auto_rx_config["habitat_upload_rate"] = MINIMUM_HABITAT_UPDATE_RATE - # APRS Settings auto_rx_config["aprs_enabled"] = config.getboolean("aprs", "aprs_enabled") auto_rx_config["aprs_upload_rate"] = config.getint("aprs", "upload_rate") @@ -455,27 +428,31 @@ def read_auto_rx_config(filename, no_sdr_test=False): "MEISEI": True, "MTS01": False, # Until we test it "MRZ": False, # .... except for the MRZ, until we know it works. + "WXR301": True, + "WXRPN9": True, "UDP": False, } auto_rx_config["decoder_spacing_limit"] = config.getint( "advanced", "decoder_spacing_limit" ) - auto_rx_config["experimental_decoders"]["RS41"] = config.getboolean( - "advanced", "rs41_experimental" - ) - auto_rx_config["experimental_decoders"]["RS92"] = config.getboolean( - "advanced", "rs92_experimental" - ) - auto_rx_config["experimental_decoders"]["M10"] = config.getboolean( - "advanced", "m10_experimental" - ) - auto_rx_config["experimental_decoders"]["DFM"] = config.getboolean( - "advanced", "dfm_experimental" - ) - auto_rx_config["experimental_decoders"]["LMS6"] = config.getboolean( - "advanced", "lms6-400_experimental" - ) + # Use 'experimental' (not really, anymore!) decoders for RS41, RS92, M10, DFM and LMS6-400. + # Don't allow overriding to the FM based decoders. + # auto_rx_config["experimental_decoders"]["RS41"] = config.getboolean( + # "advanced", "rs41_experimental" + # ) + # auto_rx_config["experimental_decoders"]["RS92"] = config.getboolean( + # "advanced", "rs92_experimental" + # ) + # auto_rx_config["experimental_decoders"]["M10"] = config.getboolean( + # "advanced", "m10_experimental" + # ) + # auto_rx_config["experimental_decoders"]["DFM"] = config.getboolean( + # "advanced", "dfm_experimental" + # ) + # auto_rx_config["experimental_decoders"]["LMS6"] = config.getboolean( + # "advanced", "lms6-400_experimental" + # ) try: auto_rx_config["web_control"] = config.getboolean("web", "web_control") @@ -559,7 +536,7 @@ def read_auto_rx_config(filename, no_sdr_test=False): logging.warning( "Config - Did not find kml_refresh_rate setting, using default (10 seconds)." ) - auto_rx_config["kml_refresh_rate"] = 11 + auto_rx_config["kml_refresh_rate"] = 10 # New Sondehub db Settings try: @@ -683,8 +660,8 @@ def read_auto_rx_config(filename, no_sdr_test=False): # that this goes against the wishes of the radiosonde_auto_rx developers to not be part # of the bigger problem of APRS-IS congestion. - ALLOWED_APRS_SERVERS = ["radiosondy.info"] - ALLOWED_APRS_PORTS = [14590] + ALLOWED_APRS_SERVERS = ["radiosondy.info", "wettersonde.net", "localhost"] + ALLOWED_APRS_PORTS = [14580, 14590] if auto_rx_config["aprs_server"] not in ALLOWED_APRS_SERVERS: logging.warning( @@ -748,6 +725,39 @@ def read_auto_rx_config(filename, no_sdr_test=False): "Config - Did not find system / debug logging options, using defaults (disabled, unless set as a command-line option.)" ) + # 1.6.2 - Encrypted Sonde Email Notifications + try: + auto_rx_config["email_encrypted_sonde_notifications"] = config.getboolean( + "email", "encrypted_sonde_notifications" + ) + except: + logging.warning( + "Config - Did not find encrypted_sonde_notifications setting (new in v1.6.2), using default (True)" + ) + auto_rx_config["email_encrypted_sonde_notifications"] = True + + + # 1.6.3 - Weathex WXR301d support + try: + auto_rx_config["wideband_sondes"] = config.getboolean( + "advanced", "wideband_sondes" + ) + except: + logging.warning( + "Config - Missing wideband_sondes option (new in v1.6.3), using default (False)" + ) + auto_rx_config["wideband_sondes"] = False + + # 1.7.1 - Save RS41 Calibration Data + try: + auto_rx_config["save_cal_data"] = config.getboolean( + "logging", "save_cal_data" + ) + except: + logging.warning( + "Config - Missing save_cal_data option (new in v1.7.1), using default (False)" + ) + auto_rx_config["save_cal_data"] = False # If we are being called as part of a unit test, just return the config now. if no_sdr_test: @@ -829,7 +839,7 @@ def read_auto_rx_config(filename, no_sdr_test=False): return None for _n in range(1, auto_rx_config["sdr_quantity"] + 1): - _sdr_name = f"KA9Q{_n:02d}" + _sdr_name = f"KA9Q-{_n:02d}" auto_rx_config["sdr_settings"][_sdr_name] = { "ppm": 0, "gain": 0, @@ -838,8 +848,6 @@ def read_auto_rx_config(filename, no_sdr_test=False): "task": None, } - logging.critical("Config - KA9Q SDR Support not implemented yet - exiting.") - return None else: logging.critical(f"Config - Unknown SDR Type {auto_rx_config['sdr_type']} - exiting.") @@ -872,7 +880,7 @@ def read_auto_rx_config(filename, no_sdr_test=False): if len(auto_rx_config["sdr_settings"].keys()) == 0: # We have no SDRs to use!! logging.error("Config - No working SDRs! Cannot run...") - return None + raise SystemError("No working SDRs!") else: # Create a global copy of the configuration file at this point global_config = copy.deepcopy(auto_rx_config) @@ -891,7 +899,8 @@ def read_auto_rx_config(filename, no_sdr_test=False): web_password = auto_rx_config["web_password"] return auto_rx_config - + except SystemError as e: + raise e except: traceback.print_exc() logging.error("Could not parse config file.") diff --git a/auto_rx/autorx/decode.py b/auto_rx/autorx/decode.py index 61283b9f..5d2ce6dd 100644 --- a/auto_rx/autorx/decode.py +++ b/auto_rx/autorx/decode.py @@ -23,6 +23,7 @@ from .sonde_specific import fix_datetime, imet_unique_id from .fsk_demod import FSKDemodStats from .sdr_wrappers import test_sdr, get_sdr_iq_cmd, get_sdr_fm_cmd, get_sdr_name +from .email_notification import EmailNotification # Global valid sonde types list. VALID_SONDE_TYPES = [ @@ -39,6 +40,8 @@ "MRZ", "MTS01", "UDP", + "WXR301", + "WXRPN9" ] # Known 'Drifty' Radiosonde types @@ -118,6 +121,8 @@ class SondeDecoder(object): "MRZ", "MTS01", "UDP", + "WXR301", + "WXRPN9" ] def __init__( @@ -144,7 +149,8 @@ def __init__( rs92_ephemeris=None, rs41_drift_tweak=False, experimental_decoder=False, - save_raw_hex=False + save_raw_hex=False, + wideband_sondes=False ): """ Initialise and start a Sonde Decoder. @@ -183,6 +189,7 @@ def __init__( rs41_drift_tweak (bool): If True, add a high-pass filter in the decode chain, which can improve decode performance on drifty SDRs. experimental_decoder (bool): If True, use the experimental fsk_demod-based decode chain. save_raw_hex (bool): If True, save the raw hex output from the decoder to a file. + wideband_sondes (bool): If True, use a wider bandwidth for iMet sondes. Does not affect settings for any other radiosonde types. """ # Thread running flag self.decoder_running = True @@ -214,15 +221,19 @@ def __init__( self.experimental_decoder = experimental_decoder self.save_raw_hex = save_raw_hex self.raw_file = None + self.wideband_sondes = wideband_sondes # Raw hex filename if self.save_raw_hex: - _outfilename = f"{datetime.datetime.utcnow().strftime('%Y%m%d-%H%M%S')}_{self.sonde_type}_{int(self.sonde_freq)}.raw" + _outfilename = f"{datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%d-%H%M%S')}_{self.sonde_type}_{int(self.sonde_freq)}.raw" _outfilename = os.path.join(autorx.logging_path, _outfilename) self.raw_file_option = "-r" else: self.raw_file_option = "" + self.save_decode_iq_path = os.path.join(autorx.logging_path, f"decode_IQ_{self.sonde_freq}_{self.sonde_type}_{str(self.rtl_device_idx)}.raw") + self.save_decode_audio_path = os.path.join(autorx.logging_path, f"decode_audio_{self.sonde_freq}_{self.sonde_type}_{str(self.rtl_device_idx)}.wav") + # iMet ID store. We latch in the first iMet ID we calculate, to avoid issues with iMet-1-RS units # which don't necessarily have a consistent packet count to time increment ratio. # This is a tradeoff between being able to handle multiple iMet sondes on a single frequency, and @@ -240,6 +251,9 @@ def __init__( self.imet_prev_time = None self.imet_prev_frame = None + # Keep a record of which RS41 serials we have uploaded complete subframe data for. + self.rs41_subframe_uploads = [] + # This will become our decoder thread. self.decoder = None @@ -386,9 +400,9 @@ def generate_decoder_command(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_audio: - decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_audio_path} |" - decode_cmd += "./rs41mod --ptu2 --json 2>/dev/null" + decode_cmd += "./rs41mod --ptu2 --json --jsnsubfrm1 2>/dev/null" elif self.sonde_type == "RS92": # Decoding a RS92 requires either an ephemeris or an almanac file. @@ -452,7 +466,7 @@ def generate_decoder_command(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_audio: - decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_audio_path} |" decode_cmd += ( "./rs92mod -vx -v --crc --ecc --vel --json %s %s 2>/dev/null" @@ -485,7 +499,7 @@ def generate_decoder_command(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_audio: - decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_audio_path} |" # DFM decoder decode_cmd += "./dfm09mod -vv --ecc --json --dist --auto 2>/dev/null" @@ -512,7 +526,7 @@ def generate_decoder_command(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_audio: - decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_audio_path} |" # M10 decoder decode_cmd += "./m10mod --json --ptu -vvv 2>/dev/null" @@ -520,7 +534,11 @@ def generate_decoder_command(self): elif self.sonde_type == "IMET": # iMet-4 Sondes - _sample_rate = 48000 + # These samples rates probably need to be revisited. + if self.wideband_sondes: + _sample_rate = 96000 + else: + _sample_rate = 48000 decode_cmd = get_sdr_iq_cmd( sdr_type = self.sdr_type, @@ -537,10 +555,15 @@ def generate_decoder_command(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_iq: - decode_cmd += " tee decode_%s.raw |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_iq_path} |" + + if self.wideband_sondes: + _wideband = "--imet1" + else: + _wideband = "" # iMet-4 (IMET1RS) decoder - decode_cmd += f"./imet4iq --iq 0.0 --lpIQ --dc - {_sample_rate} 16 --json 2>/dev/null" + decode_cmd += f"./imet4iq --iq 0.0 --lpIQ --dc - {_sample_rate} 16 --json {_wideband} 2>/dev/null" elif self.sonde_type == "IMET5": # iMet-54 Sondes @@ -562,7 +585,7 @@ def generate_decoder_command(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_iq: - decode_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_iq_path} |" # iMet-54 Decoder decode_cmd += ( @@ -589,7 +612,7 @@ def generate_decoder_command(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_iq: - decode_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_iq_path} |" # MRZ decoder #decode_cmd += "./mp3h1mod --auto --json --ptu 2>/dev/null" @@ -626,7 +649,7 @@ def generate_decoder_command(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_iq: - decode_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_iq_path} |" # LMS6-1680 decoder decode_cmd += f"./mk2a1680mod --iq 0.0 --lpIQ --lpbw 160 --decFM --dc --crc --json {self.raw_file_option} - 240000 16 2>/dev/null" @@ -663,7 +686,7 @@ def generate_decoder_command(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_audio: - decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_audio_path} |" decode_cmd += "./lms6Xmod --json 2>/dev/null" @@ -687,7 +710,7 @@ def generate_decoder_command(self): # Add in tee command to save IQ to disk if debugging is enabled. if self.save_decode_iq: - decode_cmd += " tee decode_%s.raw |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_iq_path} |" # Meisei Decoder, in IQ input mode decode_cmd += f"./meisei100mod --IQ 0.0 --lpIQ --dc - {_sample_rate} 16 --json --ptu --ecc 2>/dev/null" @@ -712,11 +735,64 @@ def generate_decoder_command(self): # Add in tee command to save IQ to disk if debugging is enabled. if self.save_decode_iq: - decode_cmd += " tee decode_%s.raw |" % str(self.rtl_device_idx) + decode_cmd += f" tee {self.save_decode_iq_path} |" # Meteosis MTS01 decoder decode_cmd += f"./mts01mod --json --IQ 0.0 --lpIQ --dc - {_sample_rate} 16 2>/dev/null" + + elif self.sonde_type == "WXR301": + # Weathex WxR-301D + + _sample_rate = 96000 + _if_bw = 64 + + decode_cmd = get_sdr_iq_cmd( + sdr_type = self.sdr_type, + frequency = self.sonde_freq, + sample_rate = _sample_rate, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + ss_iq_path = self.ss_iq_path, + rtl_device_idx = self.rtl_device_idx, + ppm = self.ppm, + gain = self.gain, + bias = self.bias + ) + + # Add in tee command to save IQ to disk if debugging is enabled. + if self.save_decode_iq: + decode_cmd += f" tee {self.save_decode_iq_path} |" + + # WXR301, via iq_dec as a FM Demod. + decode_cmd += f"./iq_dec --FM --IFbw {_if_bw} --lpFM --wav --iq 0.0 - {_sample_rate} 16 2>/dev/null | ./weathex301d -b --json" + + elif self.sonde_type == "WXRPN9": + # Weathex WxR-301D (PN9) + + _sample_rate = 96000 + _if_bw = 64 + + decode_cmd = get_sdr_iq_cmd( + sdr_type = self.sdr_type, + frequency = self.sonde_freq, + sample_rate = _sample_rate, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + ss_iq_path = self.ss_iq_path, + rtl_device_idx = self.rtl_device_idx, + ppm = self.ppm, + gain = self.gain, + bias = self.bias + ) + + # Add in tee command to save IQ to disk if debugging is enabled. + if self.save_decode_iq: + decode_cmd += f" tee {self.save_decode_iq_path} |" + + # WXR301, via iq_dec as a FM Demod. + decode_cmd += f"./iq_dec --FM --IFbw {_if_bw} --lpFM --wav --iq 0.0 - {_sample_rate} 16 2>/dev/null | ./weathex301d -b --json --pn9" + elif self.sonde_type == "UDP": # UDP Input Mode. # Used only for testing of new decoders, prior to them being integrated into auto_rx. @@ -786,7 +862,7 @@ def generate_decoder_command_experimental(self): _baud_rate, ) - decode_cmd = f"./rs41mod --ptu2 --json --softin -i {self.raw_file_option} 2>/dev/null" + decode_cmd = f"./rs41mod --ptu2 --json --jsnsubfrm1 --softin -i {self.raw_file_option} 2>/dev/null" # RS41s transmit pulsed beacons - average over the last 2 frames, and use a peak-hold demod_stats = FSKDemodStats(averaging_time=2.0, peak_hold=True) @@ -851,7 +927,7 @@ def generate_decoder_command_experimental(self): # Add in tee command to save IQ to disk if debugging is enabled. if self.save_decode_iq: - demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + demod_cmd += f" tee {self.save_decode_iq_path} |" demod_cmd += "./fsk_demod --cs16 -b %d -u %d -s --stats=%d 2 %d %d - -" % ( _lower, @@ -954,7 +1030,7 @@ def generate_decoder_command_experimental(self): # Add in tee command to save IQ to disk if debugging is enabled. if self.save_decode_iq: - demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + demod_cmd += f" tee {self.save_decode_iq_path} |" demod_cmd += ( "./fsk_demod --cs16 -b %d -u %d -s -p %d --stats=%d 2 %d %d - -" @@ -996,7 +1072,7 @@ def generate_decoder_command_experimental(self): # Add in tee command to save IQ to disk if debugging is enabled. if self.save_decode_iq: - demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + demod_cmd += f" tee {self.save_decode_iq_path} |" demod_cmd += ( "./fsk_demod --cs16 -b %d -u %d -s -p %d --stats=%d 2 %d %d - -" @@ -1036,7 +1112,7 @@ def generate_decoder_command_experimental(self): # Add in tee command to save IQ to disk if debugging is enabled. if self.save_decode_iq: - demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + demod_cmd += f" tee {self.save_decode_iq_path} |" demod_cmd += "./fsk_demod --cs16 -b %d -u %d -s --stats=%d 2 %d %d - -" % ( _lower, @@ -1077,7 +1153,7 @@ def generate_decoder_command_experimental(self): ) # Add in tee command to save IQ to disk if debugging is enabled. if self.save_decode_iq: - demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + demod_cmd += f" tee {self.save_decode_iq_path} |" demod_cmd += "./fsk_demod --cs16 -b %d -u %d -s --stats=%d 2 %d %d - -" % ( _lower, @@ -1119,7 +1195,7 @@ def generate_decoder_command_experimental(self): # Add in tee command to save IQ to disk if debugging is enabled. if self.save_decode_iq: - demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + demod_cmd += f" tee {self.save_decode_iq_path} |" demod_cmd += "./fsk_demod --cs16 -s -b %d -u %d --stats=%d 2 %d %d - -" % ( _lower, @@ -1165,7 +1241,7 @@ def generate_decoder_command_experimental(self): # Add in tee command to save audio to disk if debugging is enabled. if self.save_decode_iq: - demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + demod_cmd += f" tee {self.save_decode_iq_path} |" # LMS6-1680 decoder demod_cmd += f"./mk2a1680mod --iq 0.0 --lpIQ --lpbw 160 --lpFM --dc --crc --json {self.raw_file_option} - 220000 16 2>/dev/null" @@ -1223,6 +1299,94 @@ def generate_decoder_command_experimental(self): demod_stats = FSKDemodStats(averaging_time=1.0, peak_hold=True) self.rx_frequency = self.sonde_freq + elif self.sonde_type == "WXR301": + # Weathex WxR-301D Sondes. + + _baud_rate = 4800 + _sample_rate = 96000 + + # Limit FSK estimator window to roughly +/- 40 kHz + _lower = -40000 + _upper = 40000 + + demod_cmd = get_sdr_iq_cmd( + sdr_type = self.sdr_type, + frequency = self.sonde_freq, + sample_rate = _sample_rate, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + ss_iq_path = self.ss_iq_path, + rtl_device_idx = self.rtl_device_idx, + ppm = self.ppm, + gain = self.gain, + bias = self.bias, + dc_block = True + ) + + # Add in tee command to save IQ to disk if debugging is enabled. + if self.save_decode_iq: + demod_cmd += f" tee {self.save_decode_iq_path} |" + + # Trying out using the mask estimator here to reduce issues with interference + demod_cmd += "./fsk_demod --cs16 -s -b %d -u %d --mask 50000 --stats=%d 2 %d %d - -" % ( + _lower, + _upper, + _stats_rate, + _sample_rate, + _baud_rate, + ) + + # Soft-decision decoding, inverted. + decode_cmd = f"./weathex301d --softin -i --json 2>/dev/null" + + # Weathex sondes transmit continuously - average over the last frame, and use a peak hold + demod_stats = FSKDemodStats(averaging_time=5.0, peak_hold=True) + self.rx_frequency = self.sonde_freq + + elif self.sonde_type == "WXRPN9": + # Weathex WxR-301D Sonde, PN9 variant + + _baud_rate = 5000 + _sample_rate = 100000 + + # Limit FSK estimator window to roughly +/- 40 kHz + _lower = -40000 + _upper = 40000 + + demod_cmd = get_sdr_iq_cmd( + sdr_type = self.sdr_type, + frequency = self.sonde_freq, + sample_rate = _sample_rate, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + ss_iq_path = self.ss_iq_path, + rtl_device_idx = self.rtl_device_idx, + ppm = self.ppm, + gain = self.gain, + bias = self.bias, + dc_block = True + ) + + # Add in tee command to save IQ to disk if debugging is enabled. + if self.save_decode_iq: + demod_cmd += f" tee {self.save_decode_iq_path} |" + + # Trying out using the mask estimator here to reduce issues with interference + demod_cmd += "./fsk_demod --cs16 -s -b %d -u %d --mask 50000 --stats=%d 2 %d %d - -" % ( + _lower, + _upper, + _stats_rate, + _sample_rate, + _baud_rate, + ) + + # Soft-decision decoding, inverted. + decode_cmd = f"./weathex301d --softin -i --json --pn9 2>/dev/null" + + # Weathex sondes transmit continuously - average over the last frame, and use a peak hold + demod_stats = FSKDemodStats(averaging_time=5.0, peak_hold=True) + self.rx_frequency = self.sonde_freq + else: return None @@ -1442,6 +1606,20 @@ def handle_decoder_line(self, data): "Radiosonde %s has encrypted telemetry (Possible encrypted RS41-SGM)! We cannot decode this, closing decoder." % _telemetry["id"] ) + + # Overwrite the datetime field to make the email notifier happy + _telemetry['datetime_dt'] = datetime.datetime.now(datetime.timezone.utc) + _telemetry["freq"] = "%.3f MHz" % (self.sonde_freq / 1e6) + + # Send this to only the Email Notifier, if it exists. + for _exporter in self.exporters: + try: + if _exporter.__self__.__module__ == EmailNotification.__module__: + _exporter(_telemetry) + except Exception as e: + self.log_error("Exporter Error %s" % str(e)) + + # Close the decoder. self.exit_state = "Encrypted" self.decoder_running = False return False @@ -1516,8 +1694,9 @@ def handle_decoder_line(self, data): # which is most likely an Ozone sensor (though could be something different!) # We append -Ozone to the sonde type field to indicate this. # TODO: Decode device ID from aux field to indicate what the aux payload actually is? - if "aux" in _telemetry: - _telemetry["type"] += "-Ozone" + # 2023-10 - disabled this addition. Can be too misleading. -XDATA now appended on the web interface only. + # if "aux" in _telemetry: + # _telemetry["type"] += "-Ozone" # iMet Specific actions if self.sonde_type == "IMET": @@ -1608,6 +1787,30 @@ def handle_decoder_line(self, data): "%Y-%m-%dT%H:%M:%SZ" ) + # Weathex Specific Actions + # Same datetime issues as with iMets, and LMS6 + if (self.sonde_type == "WXR301") or (self.sonde_type == "WXRPN9"): + # Fix up the time. + _telemetry["datetime_dt"] = fix_datetime(_telemetry["datetime"]) + # Re-generate the datetime string. + _telemetry["datetime"] = _telemetry["datetime_dt"].strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + + # RS41 Subframe Data Actions + # We only upload the subframe data once. + if 'rs41_calconf51x16' in _telemetry: + # Remove subframe data if we have already uploaded it once. + if _telemetry['id'] in self.rs41_subframe_uploads: + _telemetry.pop('rs41_calconf51x16') + else: + self.rs41_subframe_uploads.append(_telemetry['id']) + self.log_info(f"Received complete calibration dataset for {_telemetry['id']}.") + _telemetry['rs41_subframe'] = _telemetry['rs41_calconf51x16'] + _telemetry.pop('rs41_calconf51x16') + + + # Grab a snapshot of modem statistics, if we are using an experimental decoder. if self.demod_stats is not None: if self.demod_stats.snr != -999.0: @@ -1731,8 +1934,12 @@ def log_critical(self, line): f"Decoder ({_sdr_name}) {self.sonde_type} {self.sonde_freq/1e6:.3f} - {line}" ) - def stop(self, nowait=False): + def stop(self, nowait=False, temporary_lockout=False): """ Kill the currently running decoder subprocess """ + + if temporary_lockout: + self.exit_state = "TempBlock" + self.decoder_running = False if self.decoder is not None and (not nowait): @@ -1753,7 +1960,6 @@ def running(self): if __name__ == "__main__": # Test script. from .logger import TelemetryLogger - from .habitat import HabitatUploader logging.basicConfig( format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG @@ -1765,7 +1971,6 @@ def running(self): urllib3_log.setLevel(logging.CRITICAL) _log = TelemetryLogger(log_directory="./testlog/") - _habitat = HabitatUploader(user_callsign="VK5QI_AUTO_RX_DEV", inhibit=False) try: _decoder = SondeDecoder( @@ -1773,14 +1978,14 @@ def running(self): sonde_type="RS41", timeout=50, rtl_device_idx="00000002", - exporter=[_habitat.add, _log.add], + exporter=[_log.add], ) # _decoder2 = SondeDecoder(sonde_freq = 405.5*1e6, # sonde_type = "RS41", # timeout = 50, # rtl_device_idx="00000001", - # exporter=[_habitat.add, _log.add]) + # exporter=[_log.add]) while True: time.sleep(5) @@ -1793,5 +1998,4 @@ def running(self): traceback.print_exc() pass - _habitat.close() _log.close() diff --git a/auto_rx/autorx/email_notification.py b/auto_rx/autorx/email_notification.py index 63857e1f..e9661e3a 100644 --- a/auto_rx/autorx/email_notification.py +++ b/auto_rx/autorx/email_notification.py @@ -43,6 +43,7 @@ def __init__( station_position=None, launch_notifications=True, landing_notifications=True, + encrypted_sonde_notifications=True, landing_range_threshold=50, landing_altitude_threshold=1000, landing_descent_trip=10, @@ -60,6 +61,7 @@ def __init__( self.station_position = station_position self.launch_notifications = launch_notifications self.landing_notifications = landing_notifications + self.encrypted_sonde_notifications = encrypted_sonde_notifications self.landing_range_threshold = landing_range_threshold self.landing_altitude_threshold = landing_altitude_threshold self.landing_descent_trip = landing_descent_trip @@ -119,6 +121,7 @@ def process_telemetry(self, telemetry): self.sondes[_id] = { "last_time": time.time(), "descending_trip": 0, + "ascent_trip": False, "descent_notified": False, "track": GenericTrack(max_elements=20), } @@ -133,18 +136,44 @@ def process_telemetry(self, telemetry): } ) - if self.launch_notifications and telemetry["type"]=="RS11G": + if "encrypted" in telemetry: + if telemetry["encrypted"] and self.encrypted_sonde_notifications: + try: + # This is a new Encrypted Radiosonde, send an email. + msg = "Encrypted Radiosonde Detected:\n" + msg += "\n" + + if "subtype" in telemetry: + telemetry["type"] = telemetry["subtype"] + + msg += "Serial: %s\n" % _id + msg += "Type: %s\n" % telemetry["type"] + msg += "Frequency: %s\n" % telemetry["freq"] + msg += "Time Detected: %sZ\n" % telemetry["datetime_dt"].isoformat() + + # Construct subject + _subject = self.mail_subject + _subject = _subject.replace("", telemetry["id"]) + _subject = _subject.replace("", telemetry["type"]) + _subject = _subject.replace("", telemetry["freq"]) + + if "encrypted" in telemetry: + if telemetry["encrypted"] == True: + _subject += " - ENCRYPTED SONDE" + + self.send_notification_email(subject=_subject, message=msg) + + except Exception as e: + self.log_error("Error sending E-mail - %s" % str(e)) + + elif self.launch_notifications and telemetry["type"]=="RS11G": try: # This is a new sonde. Send the email. msg = "Sonde launch detected:\n" msg += "\n" - if "encrypted" in telemetry: - if telemetry["encrypted"] == True: - msg += "ENCRYPTED RADIOSONDE DETECTED!\n" - - msg += "Callsign: %s\n" % _id + msg += "Serial: %s\n" % _id msg += "Type: %s\n" % telemetry["type"] msg += "Frequency: %s\n" % telemetry["freq"] msg += "Position: %.5f,%.5f\n" % ( @@ -175,10 +204,6 @@ def process_telemetry(self, telemetry): _subject = _subject.replace("", telemetry["type"]) _subject = _subject.replace("", telemetry["freq"]) - if "encrypted" in telemetry: - if telemetry["encrypted"] == True: - _subject += " - ENCRYPTED SONDE" - self.send_notification_email(subject=_subject, message=msg) except Exception as e: @@ -200,14 +225,21 @@ def process_telemetry(self, telemetry): # We have seen this sonde recently. Let's check it's descending... if self.sondes[_id]["descent_notified"] == False and _sonde_state: + + # Set a flag if the sonde has passed above the landing altitude threshold. + # This is used along with the descending trip to trigger a landing email notification. + if (telemetry["alt"] > self.landing_altitude_threshold): + self.sondes[_id]["ascent_trip"] = True + # If the sonde is below our threshold altitude, *and* is descending at a reasonable rate, increment. if (telemetry["alt"] < self.landing_altitude_threshold) and ( _sonde_state["ascent_rate"] < -2.0 ): self.sondes[_id]["descending_trip"] += 1 - if self.sondes[_id]["descending_trip"] > self.landing_descent_trip: - # We've seen this sonde descending for enough time now. + if (self.sondes[_id]["descending_trip"] > self.landing_descent_trip) and self.sondes[_id]["ascent_trip"]: + # We've seen this sonde descending for enough time now AND we have also seen it go above the landing threshold, + # so it's likely been on a flight and isnt just bouncing around on the ground. # Note that we've passed the descent threshold, so we shouldn't analyze anything from this sonde anymore. self.sondes[_id]["descent_notified"] = True @@ -237,7 +269,7 @@ def process_telemetry(self, telemetry): msg = "Nearby sonde landing detected:\n\n" - msg += "Callsign: %s\n" % _id + msg += "Serial: %s\n" % _id msg += "Type: %s\n" % telemetry["type"] msg += "Frequency: %s\n" % telemetry["freq"] msg += "Position: %.5f,%.5f\n" % ( @@ -347,7 +379,9 @@ def close(self): self.input_processing_running = False if self.input_thread is not None: - self.input_thread.join() + self.input_thread.join(60) + if self.input_thread.is_alive(): + self.log_error("email notification input thread failed to join") def running(self): """ Check if the logging thread is running. @@ -429,7 +463,30 @@ def log_error(self, line): "heading": 0.0, "vel_h": 5.1, "vel_v": -5.0, - "datetime_dt": datetime.datetime.utcnow(), + "datetime_dt": datetime.datetime.now(datetime.timezone.utc), + } + ) + + time.sleep(10) + + print("Testing encrypted sonde alert.") + _email_notification.add( + { + "id": "R1234557", + "frame": 10, + "lat": 0.0, + "lon": 0.0, + "alt": 0, + "temp": 1.0, + "type": "RS41", + "subtype": "RS41-SGM", + "freq": "401.520 MHz", + "freq_float": 401.52, + "heading": 0.0, + "vel_h": 5.1, + "vel_v": -5.0, + "datetime_dt": datetime.datetime.now(datetime.timezone.utc), + "encrypted": True } ) @@ -438,10 +495,10 @@ def log_error(self, line): _test = { "id": "N1234557", - "frame": 10, + "frame": 11, "lat": -10.01, "lon": 10.01, - "alt": 800, + "alt": 1100, "temp": 1.0, "type": "RS41", "freq": "401.520 MHz", @@ -449,15 +506,18 @@ def log_error(self, line): "heading": 0.0, "vel_h": 5.1, "vel_v": -5.0, - "datetime_dt": datetime.datetime.utcnow(), + "datetime_dt": datetime.datetime.now(datetime.timezone.utc), } print("Testing landing alert.") - for i in range(20): - _email_notification.add(_test) - _test["alt"] = _test["alt"] - 5.0 - _test["datetime_dt"] = datetime.datetime.utcnow() - time.sleep(2) + for i in range(30): + _tosubmit = _test.copy() + _email_notification.add(_tosubmit) + _test["alt"] = _test["alt"] - 10.0 + _test["lat"] = _test["lat"] + 0.001 + _test["lon"] = _test["lon"] + 0.001 + _test["datetime_dt"] = datetime.datetime.now(datetime.timezone.utc) + time.sleep(1) time.sleep(60) diff --git a/auto_rx/autorx/emulation.py b/auto_rx/autorx/emulation.py index 9618d45c..1b92eb97 100644 --- a/auto_rx/autorx/emulation.py +++ b/auto_rx/autorx/emulation.py @@ -124,7 +124,7 @@ def emulate_telemetry(filename, port=55673, speed=1.0): _fields = _line.split(",") _telemetry_datetime = parse(_fields[0]) - _current_datetime = datetime.datetime.utcnow() + _current_datetime = datetime.datetime.now(datetime.timezone.utc) for _line in _f: _fields = _line.split(",") diff --git a/auto_rx/autorx/gps.py b/auto_rx/autorx/gps.py index 0f030e70..79226d80 100644 --- a/auto_rx/autorx/gps.py +++ b/auto_rx/autorx/gps.py @@ -17,7 +17,7 @@ def get_ephemeris(destination="ephemeris.dat"): logging.debug("GPS Grabber - Connecting to ESA's FTP Server...") ftp = ftplib.FTP("gssc.esa.int", timeout=10) ftp.login("anonymous", "anonymous") - ftp.cwd("gnss/data/daily/%s/" % datetime.datetime.utcnow().strftime("%Y")) + ftp.cwd("gnss/data/daily/%s/" % datetime.datetime.now(datetime.timezone.utc).strftime("%Y")) # Ideally we would grab this data from: YYYY/brdc/brdcDDD0.YYn.Z # .. but the ESA brdc folder seems to be getting of date. The daily directories are OK though! # So instead, we use: YYYY/DDD/brdcDDD0.YYn.Z diff --git a/auto_rx/autorx/gpsd.py b/auto_rx/autorx/gpsd.py index bdd8d8e1..ac19782a 100644 --- a/auto_rx/autorx/gpsd.py +++ b/auto_rx/autorx/gpsd.py @@ -335,7 +335,9 @@ def close(self): self.gpsd_thread_running = False # Wait for the thread to close. if self.gpsd_thread != None: - self.gpsd_thread.join() + self.gpsd_thread.join(60) + if self.gpsd_thread.is_alive(): + logging.error("GPS thread failed to join") def send_to_callback(self, data): """ diff --git a/auto_rx/autorx/habitat.py b/auto_rx/autorx/habitat.py deleted file mode 100644 index ebb4303b..00000000 --- a/auto_rx/autorx/habitat.py +++ /dev/null @@ -1,868 +0,0 @@ -#!/usr/bin/env python -# -# radiosonde_auto_rx - Habitat Exporter -# -# Copyright (C) 2018 Mark Jessop -# Released under GNU GPL v3 or later -# -import crcmod -import datetime -import logging -import random -import requests -import time -import traceback -import json -from base64 import b64encode -from hashlib import sha256 -from queue import Queue -from threading import Thread, Lock -from . import __version__ as auto_rx_version - -# These get replaced out after init -url_habitat_uuids = "" -url_habitat_db = "" -habitat_url = "" - -# CRC16 function -def crc16_ccitt(data): - """ - Calculate the CRC16 CCITT checksum of *data*. - (CRC16 CCITT: start 0xFFFF, poly 0x1021) - - Args: - data (str): String to be CRC'd. The string will be encoded to ASCII prior to CRCing. - - Return: - str: Resultant checksum as two bytes of hexadecimal. - - """ - crc16 = crcmod.predefined.mkCrcFun("crc-ccitt-false") - # Encode to ASCII. - _data_ascii = data.encode("ascii") - return hex(crc16(_data_ascii))[2:].upper().zfill(4) - - -def sonde_telemetry_to_sentence(telemetry, payload_callsign=None, comment=None): - """ Convert a telemetry data dictionary into a UKHAS-compliant telemetry sentence. - - Args: - telemetry (dict): A sonde telemetry dictionary. Refer to the description in the autorx.decode.SondeDecoder docs. - payload_callsign (str): If supplied, override the callsign field with this string. - comment (str): Optional data to add to the comment field of the output sentence. - - Returns: - str: UKHAS-compliant telemetry sentence for uploading to Habitat - - - """ - # We only want HH:MM:SS for uploading to habitat. - _short_time = telemetry["datetime_dt"].strftime("%H:%M:%S") - - if payload_callsign is None: - # If we haven't been supplied a callsign, we generate one based on the serial number. - _callsign = "RS_" + telemetry["id"] - else: - _callsign = payload_callsign - - _sentence = "$$%s,%d,%s,%.5f,%.5f,%d,%.1f,%.1f,%.1f" % ( - _callsign, - telemetry["frame"], - _short_time, - telemetry["lat"], - telemetry["lon"], - int(telemetry["alt"]), # Round to the nearest metre. - telemetry["vel_h"], - telemetry["temp"], - telemetry["humidity"], - ) - - if "f_centre" in telemetry: - # We have an estimate of the sonde's centre frequency from the modem, use this in place of - # the RX frequency. - # Round to 1 kHz - _freq = round(telemetry["f_centre"] / 1000.0) - # Convert to MHz. - _freq = "%.3f MHz" % (_freq / 1e3) - else: - # Otherwise, use the normal frequency. - _freq = telemetry["freq"] - - # Add in a comment field, containing the sonde type, serial number, and frequency. - _sentence += ",%s %s %s" % (telemetry["type"], telemetry["id"], _freq) - - # Add in pressure data, if valid (not -1) - if telemetry["pressure"] > 0.0: - _sentence += " %.1fhPa" % telemetry["pressure"] - - # Check for Burst/Kill timer data, and add in. - if "bt" in telemetry: - if (telemetry["bt"] != -1) and (telemetry["bt"] != 65535): - _sentence += " BT %s" % time.strftime( - "%H:%M:%S", time.gmtime(telemetry["bt"]) - ) - - # Add in battery voltage, if the field is valid (e.g. not -1) - if telemetry["batt"] > 0.0: - _sentence += " %.1fV" % telemetry["batt"] - - # Add on any custom comment data if provided. - if comment != None: - comment = comment.replace(",", "_") - _sentence += " " + comment - - _checksum = crc16_ccitt(_sentence[2:]) - _output = _sentence + "*" + _checksum + "\n" - return _output - - -# -# Functions for uploading a listener position to Habitat. -# Derived from https://raw.githubusercontent.com/rossengeorgiev/hab-tools/master/spot2habitat_chase.py -# -callsign_init = False - -uuids = [] - - -def check_callsign(callsign, timeout=10): - """ - Check if a payload document exists for a given callsign. - - This is done in a bit of a hack-ish way at the moment. We just check to see if there have - been any reported packets for the payload callsign on the tracker. - This should really be replaced with the correct call into the habitat tracker. - - Args: - callsign (str): Payload callsign to search for. - timeout (int): Timeout for the search, in seconds. Defaults to 10 seconds. - - Returns: - bool: True if callsign has been observed within the last 6 hour, False otherwise. - """ - - _url_check_callsign = "http://legacy-snus.habhub.org/tracker/datanew.php?mode=6hours&type=positions&format=json&max_positions=10&position_id=0&vehicle=%s" - - logging.debug("Habitat - Checking if %s has been observed recently..." % callsign) - # Perform the request - _r = requests.get(_url_check_callsign % callsign, timeout=timeout) - - try: - # Read the response in as JSON - _r_json = _r.json() - - # Read out the list of positions for the requested callsign - _positions = _r_json["positions"]["position"] - - # If there is at least one position returned, we assume there is a valid payload document. - if len(_positions) > 0: - logging.info( - "Habitat - Callsign %s already present in Habitat DB, not creating new payload doc." - % callsign - ) - return True - else: - # Otherwise, we don't, and go create one. - return False - - except Exception as e: - # Handle errors with JSON parsing. - logging.error( - "Habitat - Unable to request payload positions from legacy-snus.habhub.org - %s" - % str(e) - ) - return False - - -# Keep an internal cache for which payload docs we've created so we don't spam couchdb with updates -payload_config_cache = {} - - -def ISOStringNow(): - return "%sZ" % datetime.datetime.utcnow().isoformat() - - -def initPayloadDoc( - serial, description="Meteorology Radiosonde", frequency=401.5, timeout=20 -): - """Creates a payload in Habitat for the radiosonde before uploading""" - global url_habitat_db - - payload_data = { - "type": "payload_configuration", - "name": serial, - "time_created": ISOStringNow(), - "metadata": {"description": description}, - "transmissions": [ - { - "frequency": frequency, - "modulation": "RTTY", - "mode": "USB", - "encoding": "ASCII-8", - "parity": "none", - "stop": 2, - "shift": 350, - "baud": 50, - "description": "DUMMY ENTRY, DATA IS VIA radiosonde_auto_rx", - } - ], - "sentences": [ - { - "protocol": "UKHAS", - "callsign": serial, - "checksum": "crc16-ccitt", - "fields": [ - {"name": "sentence_id", "sensor": "base.ascii_int"}, - {"name": "time", "sensor": "stdtelem.time"}, - { - "name": "latitude", - "sensor": "stdtelem.coordinate", - "format": "dd.dddd", - }, - { - "name": "longitude", - "sensor": "stdtelem.coordinate", - "format": "dd.dddd", - }, - {"name": "altitude", "sensor": "base.ascii_int"}, - {"name": "speed", "sensor": "base.ascii_float"}, - {"name": "temperature_external", "sensor": "base.ascii_float"}, - {"name": "humidity", "sensor": "base.ascii_float"}, - {"name": "comment", "sensor": "base.string"}, - ], - "filters": { - "post": [ - {"filter": "common.invalid_location_zero", "type": "normal"} - ] - }, - "description": "radiosonde_auto_rx to Habitat Bridge", - } - ], - } - - # Perform the POST request to the Habitat DB. - try: - _r = requests.post(url_habitat_db, json=payload_data, timeout=timeout) - - if _r.json()["ok"] is True: - logging.info("Habitat - Created a payload document for %s" % serial) - return True - else: - logging.error( - "Habitat - Failed to create a payload document for %s" % serial - ) - return False - - except Exception as e: - logging.error( - "Habitat - Failed to create a payload document for %s - %s" - % (serial, str(e)) - ) - return False - - -def postListenerData(doc, timeout=10): - global uuids, url_habitat_db - # do we have at least one uuid, if not go get more - if len(uuids) < 1: - fetchUuids() - - # Attempt to add UUID and time data to document. - try: - doc["_id"] = uuids.pop() - except IndexError: - logging.error("Habitat - Unable to post listener data - no UUIDs available.") - return False - - doc["time_uploaded"] = ISOStringNow() - - try: - _r = requests.post(url_habitat_db, json=doc, timeout=timeout) - return True - except Exception as e: - logging.error("Habitat - Could not post listener data - %s" % str(e)) - return False - - -def fetchUuids(timeout=10): - global uuids, url_habitat_uuids - - _retries = 5 - - while _retries > 0: - try: - _r = requests.get(url_habitat_uuids % 10, timeout=timeout) - uuids.extend(_r.json()["uuids"]) - # logging.debug("Habitat - Got UUIDs") - return - except Exception as e: - logging.error( - "Habitat - Unable to fetch UUIDs, retrying in 10 seconds - %s" % str(e) - ) - time.sleep(10) - _retries = _retries - 1 - continue - - logging.error("Habitat - Gave up trying to get UUIDs.") - return - - -def initListenerCallsign(callsign, version="", antenna=""): - doc = { - "type": "listener_information", - "time_created": ISOStringNow(), - "data": { - "callsign": callsign, - "antenna": antenna, - "radio": "radiosonde_auto_rx %s" % version, - }, - } - - resp = postListenerData(doc) - - if resp is True: - # logging.debug("Habitat - Listener Callsign Initialized.") - return True - else: - logging.error("Habitat - Unable to initialize callsign.") - return False - - -def uploadListenerPosition(callsign, lat, lon, version="", antenna=""): - """ Initializer Listener Callsign, and upload Listener Position """ - - # Attempt to initialize the listeners callsign - resp = initListenerCallsign(callsign, version=version, antenna=antenna) - # If this fails, it means we can't contact the Habitat server, - # so there is no point continuing. - if resp is False: - return False - - doc = { - "type": "listener_telemetry", - "time_created": ISOStringNow(), - "data": { - "callsign": callsign, - "chase": False, - "latitude": lat, - "longitude": lon, - "altitude": 0, - "speed": 0, - }, - } - - # post position to habitat - resp = postListenerData(doc) - if resp is True: - logging.info("Habitat - Station position uploaded.") - return True - else: - logging.error("Habitat - Unable to upload station position.") - return False - - -# -# Habitat Uploader Class -# - - -class HabitatUploader(object): - """ - Queued Habitat Telemetry Uploader class - This performs uploads to the Habitat servers, and also handles generation of flight documents. - - Incoming telemetry packets are fed into queue, which is checked regularly. - If a new callsign is sighted, a payload document is created in the Habitat DB. - The telemetry data is then converted into a UKHAS-compatible format, before being added to queue to be - uploaded as network speed permits. - - If an upload attempt times out, the packet is discarded. - If the queue fills up (probably indicating no network connection, and a fast packet downlink rate), - it is immediately emptied, to avoid upload of out-of-date packets. - - Note that this uploader object is intended to handle telemetry from multiple sondes - """ - - # We require the following fields to be present in the incoming telemetry dictionary data - REQUIRED_FIELDS = [ - "frame", - "id", - "datetime", - "lat", - "lon", - "alt", - "temp", - "type", - "freq", - "freq_float", - "datetime_dt", - ] - - def __init__( - self, - user_callsign="N0CALL", - station_position=(0.0, 0.0, 0.0), - user_antenna="", - synchronous_upload_time=30, - callsign_validity_threshold=2, - upload_queue_size=16, - upload_timeout=10, - upload_retries=5, - upload_retry_interval=0.25, - user_position_update_rate=6, - inhibit=False, - url="http://habitat.sondehub.org/", - ): - """ Initialise a Habitat Uploader object. - - Args: - user_callsign (str): Callsign of the uploader. - station_position (tuple): Optional - a tuple consisting of (lat, lon, alt), which if populated, - is used to plot the listener's position on the Habitat map, both when this class is initialised, and - when a new sonde ID is observed. - - synchronous_upload_time (int): Upload the most recent telemetry when time.time()%synchronous_upload_time == 0 - This is done in an attempt to get multiple stations uploading the same telemetry sentence simultaneously, - and also acts as decimation on the number of sentences uploaded to Habitat. - callsign_validity_threshold (int): Only upload telemetry data if the callsign has been observed more than N times. Default = 5 - - upload_queue_size (int): Maximum umber of sentences to keep in the upload queue. If the queue is filled, - it will be emptied (discarding the queue contents). - upload_timeout (int): Timeout (Seconds) when performing uploads to Habitat. Default: 10 seconds. - upload_retries (int): Retry an upload up to this many times. Default: 5 - upload_retry_interval (int): Time interval between upload retries. Default: 0.25 seconds. - - user_position_update_rate (int): Time interval between automatic station position updates, hours. - Set to 6 hours by default, updating any more often than this is not really useful. - - inhibit (bool): Inhibit all uploads. Mainly intended for debugging. - - """ - - self.user_callsign = user_callsign - self.station_position = station_position - self.user_antenna = user_antenna - self.upload_timeout = upload_timeout - self.upload_retries = upload_retries - self.upload_retry_interval = upload_retry_interval - self.upload_queue_size = upload_queue_size - self.synchronous_upload_time = synchronous_upload_time - self.callsign_validity_threshold = callsign_validity_threshold - self.inhibit = inhibit - self.user_position_update_rate = user_position_update_rate - - # set the habitat upload url - global url_habitat_uuids, url_habitat_db, habitat_url - url_habitat_uuids = url + "_uuids?count=%d" - url_habitat_db = url + "habitat/" - habitat_url = url - - # Our two Queues - one to hold sentences to be upload, the other to temporarily hold - # input telemetry dictionaries before they are converted and processed. - self.habitat_upload_queue = Queue(upload_queue_size) - self.input_queue = Queue() - - # Dictionary where we store sorted telemetry data for upload when required. - # Elements will be named after payload IDs, and will contain: - # 'count' (int): Number of times this callsign has been observed. Uploads will only occur when - # this number rises above callsign_validity_threshold. - # 'data' (Queue): A queue of telemetry sentences to be uploaded. When the upload timer fires, - # this queue will be dumped, and the most recent telemetry uploaded. - # 'habitat_document' (bool): Indicates if a habitat document has been created for this payload ID. - # 'listener_updated' (bool): Indicates if the listener position has been updated for the start of this ID's flight. - self.observed_payloads = {} - - # Record of when we last uploaded a user station position to Habitat. - self.last_user_position_upload = 0 - - # Lock for dealing with telemetry uploads. - self.upload_lock = Lock() - - # Start the uploader thread. - self.upload_thread_running = True - self.upload_thread = Thread(target=self.habitat_upload_thread) - self.upload_thread.start() - - # Start the input queue processing thread. - self.input_processing_running = True - self.input_thread = Thread(target=self.process_queue) - self.input_thread.start() - - self.timer_thread_running = True - self.timer_thread = Thread(target=self.upload_timer) - self.timer_thread.start() - - def user_position_upload(self): - """ Upload the the station position to Habitat. """ - if self.station_position == None: - # Upload is successful, just flag it as OK and move on. - self.last_user_position_upload = time.time() - return False - - if (self.station_position[0] != 0.0) or (self.station_position[1] != 0.0): - _success = uploadListenerPosition( - self.user_callsign, - self.station_position[0], - self.station_position[1], - version=auto_rx_version, - antenna=self.user_antenna, - ) - self.last_user_position_upload = time.time() - return _success - else: - # No position set, just flag the update as successful. - self.last_user_position_upload = time.time() - return False - - def habitat_upload(self, sentence): - """ Upload a UKHAS-standard telemetry sentence to Habitat - - Args: - sentence (str): The UKHAS-standard telemetry sentence to upload. - """ - - if self.inhibit: - self.log_info("Upload inhibited.") - return - - # Generate payload to be uploaded - _sentence_b64 = b64encode( - sentence.encode("ascii") - ) # Encode to ASCII to be able to perform B64 encoding... - _date = datetime.datetime.utcnow().isoformat("T") + "Z" - _user_call = self.user_callsign - - _data = { - "type": "payload_telemetry", - "data": { - "_raw": _sentence_b64.decode( - "ascii" - ) # ... but decode back to a string to enable JSON serialisation. - }, - "receivers": { - _user_call: {"time_created": _date, "time_uploaded": _date,}, - }, - } - - # The URL to upload to. - _url = ( - habitat_url - + "habitat/_design/payload_telemetry/_update/add_listener/%s" - % sha256(_sentence_b64).hexdigest() - ) - - # Delay for a random amount of time between 0 and upload_retry_interval*2 seconds. - time.sleep(random.random() * self.upload_retry_interval * 2.0) - - _retries = 0 - - # When uploading, we have three possible outcomes: - # - Can't connect. No point immediately re-trying in this situation. - # - The packet is uploaded successfuly (201 / 403) - # - There is a upload conflict on the Habitat DB end (409). We can retry and it might work. - while _retries < self.upload_retries: - # Run the request. - try: - headers = {"User-Agent": "autorx-" + auto_rx_version} - _req = requests.put( - _url, - data=json.dumps(_data), - timeout=(self.upload_timeout, 6.1), - headers=headers, - ) - except Exception as e: - self.log_error("Upload Failed: %s" % str(e)) - return - - if _req.status_code == 201 or _req.status_code == 403: - # 201 = Success, 403 = Success, sentence has already seen by others. - self.log_info( - "Uploaded sentence to Habitat successfully: %s" % sentence.strip() - ) - _upload_success = True - break - elif _req.status_code == 409: - # 409 = Upload conflict (server busy). Sleep for a moment, then retry. - self.log_debug("Upload conflict.. retrying.") - time.sleep(random.random() * self.upload_retry_interval) - _retries += 1 - else: - self.log_error( - "Error uploading to Habitat. Status Code: %d %s." - % (_req.status_code, _req.text) - ) - break - - if _retries == self.upload_retries: - self.log_error( - "Upload conflict not resolved with %d retries." % self.upload_retries - ) - - return - - def habitat_upload_thread(self): - """ Handle uploading of packets to Habitat """ - - self.log_debug("Started Habitat Uploader Thread.") - - while self.upload_thread_running: - - if self.habitat_upload_queue.qsize() > 0: - # If the queue is completely full, jump to the most recent telemetry sentence. - if self.habitat_upload_queue.qsize() == self.upload_queue_size: - while not self.habitat_upload_queue.empty(): - try: - sentence = self.habitat_upload_queue.get_nowait() - except: - pass - - self.log_warning( - "Upload queue was full when reading from queue, now flushed - possible connectivity issue." - ) - else: - # Otherwise, get the first item in the queue. - sentence = self.habitat_upload_queue.get() - - # Attempt to upload it. - if sentence: - self.habitat_upload(sentence) - - else: - # Wait for a short time before checking the queue again. - time.sleep(0.1) - - self.log_debug("Stopped Habitat Uploader Thread.") - - def handle_telem_dict(self, telem, immediate=False): - # Try and convert it to a UKHAS sentence - try: - _sentence = sonde_telemetry_to_sentence(telem) - except Exception as e: - self.log_error("Error converting telemetry to sentence - %s" % str(e)) - return - - _callsign = "RS_" + telem["id"] - - # Wait for the upload_lock to be available, to ensure we don't end up with - # race conditions resulting in multiple payload docs being created. - self.upload_lock.acquire() - - # Habitat Payload document creation has been disabled as of 2020-03-20. - # We now use a common payload document for all radiosonde telemetry. - # - # # Create a habitat document if one does not already exist: - # if not self.observed_payloads[telem['id']]['habitat_document']: - # # Check if there has already been telemetry from this ID observed on Habhub - # _document_exists = check_callsign(_callsign) - # # If so, we don't need to create a new document - # if _document_exists: - # self.observed_payloads[telem['id']]['habitat_document'] = True - # else: - # # Otherwise, we attempt to create a new document. - # if self.inhibit: - # # If we have an upload inhibit, don't create a payload doc. - # _created = True - # else: - # _created = initPayloadDoc(_callsign, description="Meteorology Radiosonde", frequency=telem['freq_float']) - - # if _created: - # self.observed_payloads[telem['id']]['habitat_document'] = True - # else: - # self.log_error("Error creating payload document!") - # self.upload_lock.release() - # return - - if immediate: - self.log_info( - "Performing immediate upload for first telemetry sentence of %s." - % telem["id"] - ) - self.habitat_upload(_sentence) - - else: - # Attept to add it to the habitat uploader queue. - try: - if self.habitat_upload_queue.qsize() == self.upload_queue_size: - # Flush queue. - while not self.habitat_upload_queue.empty(): - try: - self.habitat_upload_queue.get_nowait() - except: - pass - - self.log_error( - "Upload queue was full when adding to queue, now flushed - possible connectivity issue." - ) - - self.habitat_upload_queue.put_nowait(_sentence) - self.log_debug( - "Upload queue size: %d" % self.habitat_upload_queue.qsize() - ) - except Exception as e: - self.log_error( - "Error adding sentence to queue, queue likely full. %s" % str(e) - ) - self.log_error("Queue Size: %d" % self.habitat_upload_queue.qsize()) - - self.upload_lock.release() - - def upload_timer(self): - """ Add packets to the habitat upload queue if it is time for us to upload. """ - - while self.timer_thread_running: - if int(time.time()) % self.synchronous_upload_time == 0: - # Time to upload! - for _id in self.observed_payloads.keys(): - # If no data, continue... - if self.observed_payloads[_id]["data"].empty(): - continue - else: - # Otherwise, dump the queue and keep the latest telemetry. - while not self.observed_payloads[_id]["data"].empty(): - _telem = self.observed_payloads[_id]["data"].get() - - self.handle_telem_dict(_telem) - - # Sleep a second so we don't hit the synchronous upload time again. - time.sleep(1) - else: - # Not yet time to upload, wait for a bit. - time.sleep(0.1) - - def process_queue(self): - """ Process packets from the input queue. - - This thread handles packets from the input queue (provided by the decoders) - Packets are sorted by ID, and a dictionary entry is created. - - """ - - while self.input_processing_running: - # Process everything in the queue. - while self.input_queue.qsize() > 0: - # Grab latest telem dictionary. - _telem = self.input_queue.get_nowait() - - _id = _telem["id"] - - if _id not in self.observed_payloads: - # We haven't seen this ID before, so create a new dictionary entry for it. - self.observed_payloads[_id] = { - "count": 1, - "data": Queue(), - "habitat_document": False, - "first_uploaded": False, - } - self.log_debug( - "New Payload %s. Not observed enough to allow upload." % _id - ) - # However, we don't yet add anything to the queue for this payload... - else: - # We have seen this payload before! - # Increment the 'seen' counter. - self.observed_payloads[_id]["count"] += 1 - - # If we have seen this particular ID enough times, add the data to the ID's queue. - if ( - self.observed_payloads[_id]["count"] - >= self.callsign_validity_threshold - ): - - # If this is the first time we have observed this payload, immediately upload the first position we got. - if self.observed_payloads[_id]["first_uploaded"] == False: - # Because receiving balloon telemetry appears to be a competition, immediately upload the - # first valid position received. - self.handle_telem_dict(_telem, immediate=True) - - self.observed_payloads[_id]["first_uploaded"] = True - - else: - # Otherwise, add the telemetry to the upload queue - self.observed_payloads[_id]["data"].put(_telem) - - else: - self.log_debug( - "Payload ID %s not observed enough to allow upload." % _id - ) - - # If we haven't uploaded our station position recently, re-upload it. - if ( - time.time() - self.last_user_position_upload - ) > self.user_position_update_rate * 3600: - self.user_position_upload() - - time.sleep(0.1) - - def add(self, telemetry): - """ Add a dictionary of telemetry to the input queue. - - Args: - telemetry (dict): Telemetry dictionary to add to the input queue. - - """ - - # Discard any telemetry which is indicated to be encrypted. - if "encrypted" in telemetry: - if telemetry["encrypted"] == True: - return - - # Check the telemetry dictionary contains the required fields. - for _field in self.REQUIRED_FIELDS: - if _field not in telemetry: - self.log_error("JSON object missing required field %s" % _field) - return - - # Add it to the queue if we are running. - if self.input_processing_running: - self.input_queue.put(telemetry) - else: - self.log_error("Processing not running, discarding.") - - def update_station_position(self, lat, lon, alt): - """ Update the internal station position record. Used when determining the station position by GPSD """ - self.station_position = (lat, lon, alt) - - def close(self): - """ Shutdown uploader and processing threads. """ - self.log_debug("Waiting for threads to close...") - self.input_processing_running = False - self.timer_thread_running = False - self.upload_thread_running = False - - # Wait for all threads to close. - if self.upload_thread is not None: - self.upload_thread.join() - - if self.timer_thread is not None: - self.timer_thread.join() - - if self.input_thread is not None: - self.input_thread.join() - - def log_debug(self, line): - """ Helper function to log a debug message with a descriptive heading. - Args: - line (str): Message to be logged. - """ - logging.debug("Habitat - %s" % line) - - def log_info(self, line): - """ Helper function to log an informational message with a descriptive heading. - Args: - line (str): Message to be logged. - """ - logging.info("Habitat - %s" % line) - - def log_error(self, line): - """ Helper function to log an error message with a descriptive heading. - Args: - line (str): Message to be logged. - """ - logging.error("Habitat - %s" % line) - - def log_warning(self, line): - """ Helper function to log a warning message with a descriptive heading. - Args: - line (str): Message to be logged. - """ - logging.warning("Habitat - %s" % line) diff --git a/auto_rx/autorx/ka9q.py b/auto_rx/autorx/ka9q.py new file mode 100644 index 00000000..bae7fb28 --- /dev/null +++ b/auto_rx/autorx/ka9q.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# +# radiosonde_auto_rx - SDR Abstraction - KA9Q-Radio +# +# Copyright (C) 2022 Mark Jessop +# Released under GNU GPL v3 or later +# + +import logging +import os.path +import platform +import subprocess +from .utils import timeout_cmd + + +def ka9q_setup_channel( + sdr_hostname, + frequency, + sample_rate +): + # tune --samprate 48000 --frequency 404m09 --mode iq --ssrc 404090000 --radio sonde.local + _cmd = ( + f"{timeout_cmd()} 5 " # Add a timeout, because connections to non-existing servers block for ages + f"tune " + f"--samprate {int(sample_rate)} " + f"--mode iq " + f"--frequency {int(frequency)} " + f"--ssrc {int(frequency)} " + f"--radio {sdr_hostname}" + ) + + logging.debug(f"KA9Q - Starting channel at {frequency} Hz, with command: {_cmd}") + + try: + _output = subprocess.check_output( + _cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + + if e.returncode == 124: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed while opening channel with a timeout. Is the server running?" + ) + elif e.returncode == 127: + logging.critical( + f"KA9Q ({sdr_hostname}) - Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed." + ) + else: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed while opening channel with return code {e.returncode}." + ) + # Look at the error output in a bit more details. + #_output = e.output.decode("ascii") + + # TODO - see if we can look in the output for any error messages. + return False + + return True + + +def ka9q_close_channel( + sdr_hostname, + frequency +): + + _cmd = ( + f"{timeout_cmd()} 5 " # Add a timeout, because connections to non-existing servers block for ages + f"tune " + f"--samprate 48000 " + f"--mode iq " + f"--frequency 0 " + f"--ssrc {int(frequency)} " + f"--radio {sdr_hostname}" + ) + + logging.debug(f"KA9Q - Closing channel at {frequency} Hz, with command: {_cmd}") + + try: + _output = subprocess.check_output( + _cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + + if e.returncode == 124: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed while closing channel with a timeout. Is the server running?" + ) + elif e.returncode == 127: + logging.critical( + f"KA9Q ({sdr_hostname}) - Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed." + ) + else: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed while closing chanel with return code {e.returncode}." + ) + # Look at the error output in a bit more details. + #_output = e.output.decode("ascii") + + # TODO - see if we can look in the output for any error messages. + return False + + return True + + +def ka9q_get_iq_cmd( + sdr_hostname, + frequency, + sample_rate +): + + # We need to setup a channel before we can use it! + _setup_success = ka9q_setup_channel(sdr_hostname, frequency, sample_rate) + + if not _setup_success: + logging.critical(f"KA9Q ({sdr_hostname}) - Could not setup rx channel! Decoder will likely timeout.") + + # Get the 'PCM' version of the server name, where as assume -pcm is added to the first part of the hostname. + _pcm_host = sdr_hostname.split('.')[0] + "-pcm." + ".".join(sdr_hostname.split(".")[1:]) + + # Example: pcmcat -s 404090000 sonde-pcm.local + # -2 option was removed sometime in early 2024. + _cmd = ( + f"pcmcat " + f"-s {int(frequency)} " + f"{_pcm_host} |" + ) + + return _cmd diff --git a/auto_rx/autorx/log_files.py b/auto_rx/autorx/log_files.py index 38cbdfc8..0bf414c5 100644 --- a/auto_rx/autorx/log_files.py +++ b/auto_rx/autorx/log_files.py @@ -14,6 +14,7 @@ import os.path import time import zipfile +import xml.etree.ElementTree as ET import numpy as np @@ -287,14 +288,17 @@ def log_quick_look(filename, quicklook_option=""): return _output -def list_log_files(quicklook=False, quicklook_option=""): +def list_log_files(quicklook=False, custom_log_dir=None): """ Look for all sonde log files within the logging directory """ # Output list, which will contain one object per log file, ordered by time _output = [] # Search for file matching the expected log file name - _log_mask = os.path.join(autorx.logging_path, "*_sonde.log") + if custom_log_dir: + _log_mask = os.path.join(custom_log_dir, "*_sonde.log") + else: + _log_mask = os.path.join(autorx.logging_path, "*_sonde.log") _log_files = glob.glob(_log_mask) # Sort alphanumerically, which will result in the entries being date ordered @@ -480,6 +484,11 @@ def calculate_skewt_data( while i < _burst_idx: i += decimation + + # If we've hit the end of our data, break. + if i > (len(datetime) - 1): + break + try: if temperature[i] < -260.0: # If we don't have any valid temp data, just skip this point @@ -542,9 +551,8 @@ def calculate_skewt_data( break except Exception as e: - print(str(e)) - - # Continue through the data.. + logging.exception(f"Exception {str(e)} in calculate_skewt_data") + raise return _skewt @@ -601,6 +609,126 @@ def zip_log_files(serial_list=None): return data +def coordinates_to_kml_placemark(lat, lon, alt, + name="Placemark Name", + description="Placemark Description", + absolute=False, + icon="https://maps.google.com/mapfiles/kml/shapes/placemark_circle.png", + scale=1.0): + """ Generate a generic placemark object """ + + placemark = ET.Element("Placemark") + + pm_name = ET.SubElement(placemark, "name") + pm_name.text = name + pm_desc = ET.SubElement(placemark, "description") + pm_desc.text = description + + style = ET.SubElement(placemark, "Style") + icon_style = ET.SubElement(style, "IconStyle") + icon_scale = ET.SubElement(icon_style, "scale") + icon_scale.text = str(scale) + pm_icon = ET.SubElement(icon_style, "Icon") + href = ET.SubElement(pm_icon, "href") + href.text = icon + + point = ET.SubElement(placemark, "Point") + if absolute: + altitude_mode = ET.SubElement(point, "altitudeMode") + altitude_mode.text = "absolute" + coordinates = ET.SubElement(point, "coordinates") + coordinates.text = f"{lon:.6f},{lat:.6f},{alt:.6f}" + + return placemark + + +def path_to_kml_placemark(flight_path, + name="Flight Path Name", + track_color="ff03bafc", + poly_color="8003bafc", + track_width=2.0, + absolute=True, + extrude=True): + ''' Produce a placemark object from a flight path array ''' + + placemark = ET.Element("Placemark") + + pm_name = ET.SubElement(placemark, "name") + pm_name.text = name + + style = ET.SubElement(placemark, "Style") + line_style = ET.SubElement(style, "LineStyle") + color = ET.SubElement(line_style, "color") + color.text = track_color + width = ET.SubElement(line_style, "width") + width.text = str(track_width) + if extrude: + poly_style = ET.SubElement(style, "PolyStyle") + color = ET.SubElement(poly_style, "color") + color.text = poly_color + fill = ET.SubElement(poly_style, "fill") + fill.text = "1" + outline = ET.SubElement(poly_style, "outline") + outline.text = "1" + + line_string = ET.SubElement(placemark, "LineString") + if absolute: + if extrude: + ls_extrude = ET.SubElement(line_string, "extrude") + ls_extrude.text = "1" + altitude_mode = ET.SubElement(line_string, "altitudeMode") + altitude_mode.text = "absolute" + else: + ls_tessellate = ET.SubElement(line_string, "tessellate") + ls_tessellate.text = "1" + coordinates = ET.SubElement(line_string, "coordinates") + coordinates.text = " ".join(f"{lon:.6f},{lat:.6f},{alt:.6f}" for lat, lon, alt in flight_path) + + return placemark + + +def _log_file_to_kml_folder(filename, absolute=True, extrude=True, last_only=False): + ''' Convert a single sonde log file to a KML Folder object ''' + + # Read file. + _flight_data = read_log_file(filename) + + _flight_serial = _flight_data["serial"] + _landing_time = _flight_data["last_time"] + _landing_pos = _flight_data["path"][-1] + + _folder = ET.Element("Folder") + _name = ET.SubElement(_folder, "name") + _name.text = _flight_serial + + # Generate the placemark & flight track. + _folder.append(coordinates_to_kml_placemark(_landing_pos[0], _landing_pos[1], _landing_pos[2], + name=_flight_serial, description=_landing_time, absolute=absolute)) + if not last_only: + _folder.append(path_to_kml_placemark(_flight_data["path"], name="Track", + absolute=absolute, extrude=extrude)) + + return _folder + + +def log_files_to_kml(file_list, kml_file, absolute=True, extrude=True, last_only=False): + """ Convert a collection of log files to a KML file """ + + kml_root = ET.Element("kml", xmlns="http://www.opengis.net/kml/2.2") + kml_doc = ET.SubElement(kml_root, "Document") + + for file in file_list: + logging.debug(f"Converting {file} to KML") + try: + kml_doc.append(_log_file_to_kml_folder(file, absolute=absolute, + extrude=extrude, last_only=last_only)) + except Exception: + logging.exception(f"Failed to convert {file} to KML") + + tree = ET.ElementTree(kml_root) + tree.write(kml_file, encoding="UTF-8", xml_declaration=True) + + if __name__ == "__main__": import sys import json diff --git a/auto_rx/autorx/logger.py b/auto_rx/autorx/logger.py index 1b0fa29f..ed7633f8 100644 --- a/auto_rx/autorx/logger.py +++ b/auto_rx/autorx/logger.py @@ -5,6 +5,7 @@ # Copyright (C) 2018 Mark Jessop # Released under GNU GPL v3 or later # +import codecs import datetime import glob import logging @@ -50,7 +51,9 @@ class TelemetryLogger(object): LOG_HEADER = "timestamp,serial,frame,lat,lon,alt,vel_v,vel_h,heading,temp,humidity,pressure,type,freq_mhz,snr,f_error_hz,sats,batt_v,burst_timer,aux_data\n" - def __init__(self, log_directory="./log"): + def __init__(self, + log_directory="./log", + save_cal_data=False): """ Initialise and start a sonde logger. Args: @@ -59,6 +62,7 @@ def __init__(self, log_directory="./log"): """ self.log_directory = log_directory + self.save_cal_data = save_cal_data # Dictionary to contain file handles. # Each sonde id is added as a unique key. Under each key are the contents: @@ -199,6 +203,9 @@ def write_telemetry(self, telemetry): _id = telemetry["id"] _type = telemetry["type"] + if 'aux' in telemetry: + _type += "-XDATA" + # If there is no log open for the current ID check to see if there is an existing (closed) log file, and open it. if _id not in self.open_logs: _search_string = os.path.join(self.log_directory, "*%s_*_sonde.log" % (_id)) @@ -211,11 +218,12 @@ def write_telemetry(self, telemetry): self.open_logs[_id] = { "log": open(_log_file_name, "a"), "last_time": time.time(), + "subframe_saved": False } else: # Create a new log file. _log_suffix = "%s_%s_%s_%d_sonde.log" % ( - datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S"), + datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d-%H%M%S"), _id, _type, int(telemetry["freq_float"] * 1e3), # Convert frequency to kHz @@ -226,6 +234,7 @@ def write_telemetry(self, telemetry): self.open_logs[_id] = { "log": open(_log_file_name, "a"), "last_time": time.time(), + "subframe_saved": False } # Write in a header line. @@ -241,6 +250,15 @@ def write_telemetry(self, telemetry): self.open_logs[_id]["last_time"] = time.time() self.log_debug("Wrote line: %s" % _log_line.strip()) + # Save out RS41 subframe data once, if we have it. + if ('rs41_subframe' in telemetry) and self.save_cal_data: + if self.open_logs[_id]['subframe_saved'] == False: + self.open_logs[_id]['subframe_saved'] = self.write_rs41_subframe(telemetry) + + + + + def cleanup_logs(self): """ Close any open logs that have not had telemetry added in X seconds. """ @@ -259,6 +277,42 @@ def cleanup_logs(self): except Exception as e: self.log_error("Error closing log for %s - %s" % (_id, str(e))) + def write_rs41_subframe(self, telemetry): + """ Write RS41 subframe data to disk """ + + _id = telemetry["id"] + _type = telemetry["type"] + + if 'aux' in telemetry: + _type += "-XDATA" + + _subframe_log_suffix = "%s_%s_%s_%d_subframe.bin" % ( + datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d-%H%M%S"), + _id, + _type, + int(telemetry["freq_float"] * 1e3), # Convert frequency to kHz + ) + _log_file_name = os.path.join(self.log_directory, _subframe_log_suffix) + + + try: + _subframe_data = codecs.decode(telemetry['rs41_subframe'], 'hex') + except Exception as e: + self.log_error("Error parsing RS41 subframe data") + + if _subframe_data: + _subframe_file = open(_log_file_name, 'wb') + _subframe_file.write(_subframe_data) + _subframe_file.close() + + self.log_info(f"Wrote subframe data for {telemetry['id']} to {_subframe_log_suffix}") + return True + else: + return False + + + + def close(self): """ Close input processing thread. """ self.input_processing_running = False diff --git a/auto_rx/autorx/ozimux.py b/auto_rx/autorx/ozimux.py index a3c8ce84..685a6910 100644 --- a/auto_rx/autorx/ozimux.py +++ b/auto_rx/autorx/ozimux.py @@ -252,7 +252,9 @@ def close(self): self.input_processing_running = False if self.input_thread is not None: - self.input_thread.join() + self.input_thread.join(60) + if self.input_thread.is_alive(): + self.log_error("ozimux input thread failed to join") def log_debug(self, line): """ Helper function to log a debug message with a descriptive heading. diff --git a/auto_rx/autorx/rotator.py b/auto_rx/autorx/rotator.py index 5301cf80..cadcac36 100644 --- a/auto_rx/autorx/rotator.py +++ b/auto_rx/autorx/rotator.py @@ -215,7 +215,12 @@ def move_rotator(self, azimuth, elevation): _curr_az = _pos[0] % 360.0 _curr_el = _pos[1] - if (abs(azimuth - _curr_az) > self.rotator_update_threshold) or ( + _azimuth_diff = abs(azimuth - _curr_az) + if (_azimuth_diff > 180.0): + _azimuth_diff = abs(_azimuth_diff - 360.0) + + + if (_azimuth_diff > self.rotator_update_threshold) or ( abs(elevation - _curr_el) > self.rotator_update_threshold ): # Move to the target position. @@ -320,7 +325,9 @@ def close(self): self.rotator_thread_running = False if self.rotator_thread is not None: - self.rotator_thread.join() + self.rotator_thread.join(60) + if self.rotator_thread.is_alive(): + self.log_error("rotator control thread failed to join") self.log_debug("Stopped rotator control thread.") diff --git a/auto_rx/autorx/scan.py b/auto_rx/autorx/scan.py index e23e317b..d571f4bf 100644 --- a/auto_rx/autorx/scan.py +++ b/auto_rx/autorx/scan.py @@ -5,10 +5,12 @@ # Copyright (C) 2018 Mark Jessop # Released under GNU GPL v3 or later # +import autorx import datetime import logging import numpy as np import os +import sys import platform import subprocess import time @@ -22,8 +24,9 @@ reset_rtlsdr_by_serial, reset_all_rtlsdrs, peak_decimation, + timeout_cmd ) -from .sdr_wrappers import test_sdr, reset_sdr, get_sdr_name, get_sdr_iq_cmd, get_sdr_fm_cmd, get_power_spectrum +from .sdr_wrappers import test_sdr, reset_sdr, get_sdr_name, get_sdr_iq_cmd, get_sdr_fm_cmd, get_power_spectrum, shutdown_sdr try: @@ -91,18 +94,10 @@ def run_rtl_power( if os.path.exists(filename): os.remove(filename) - # Add -k 30 option, to SIGKILL rtl_power 30 seconds after the regular timeout expires. - # Note that this only works with the GNU Coreutils version of Timeout, not the IBM version, - # which is provided with OSX (Darwin). - if "Darwin" in platform.platform(): - timeout_kill = "" - else: - timeout_kill = "-k 30 " - rtl_power_cmd = ( - "timeout %s%d %s %s-f %d:%d:%d -i %d -1 -c 25%% -p %d -d %s %s%s" + "%s %d %s %s-f %d:%d:%d -i %d -1 -c 25%% -p %d -d %s %s%s" % ( - timeout_kill, + timeout_cmd(), dwell + 10, rtl_power_path, bias_option, @@ -239,6 +234,7 @@ def detect_sonde( bias=False, save_detection_audio=False, ngp_tweak=False, + wideband_sondes=False ): """Receive some FM and attempt to detect the presence of a radiosonde. @@ -253,6 +249,7 @@ def detect_sonde( bias (bool): If True, enable the bias tee on the SDR. save_detection_audio (bool): Save the audio used in detection to a file. ngp_tweak (bool): When scanning in the 1680 MHz sonde band, use a narrower FM filter for better RS92-NGP detection. + wideband_sondes (bool): Use a wider detection filter to allow detection of Weathex and wideband iMet sondes. Returns: str/None: Returns None if no sonde found, otherwise returns a sonde type, from the following: @@ -267,7 +264,9 @@ def detect_sonde( """ # Notes: - # 400 MHz sondes: Use --bw 20 (20 kHz BW) + # 400 MHz sondes + # Normal mode: 48 kHz sample rate, 20 kHz IF BW + # Wideband mode: 96 kHz sample rate, 64 kHz IF BW # 1680 MHz RS92 Setting: --bw 32 # 1680 MHz LMS6-1680: Use FM demod. as usual. @@ -285,16 +284,20 @@ def detect_sonde( # Adjust the detection bandwidth based on the band the scanning is occuring in. if frequency < 1000e6: - # 400-406 MHz sondes - use a 20 kHz detection bandwidth. + # 400-406 MHz sondes _mode = "IQ" - _iq_bw = 48000 - _if_bw = 20 - - # Try and avoid the RTLSDR 403.2 MHz spur. - # Note that this is only goign to work if we are detecting on 403.210 or 403.190 MHz. - if (abs(403200000 - frequency) < 20000) and (sdr_type == "RTLSDR"): - logging.debug("Scanner - Narrowing detection IF BW to avoid RTLSDR spur.") - _if_bw = 15 + if wideband_sondes: + _iq_bw = 96000 + _if_bw = 64 + else: + _iq_bw = 48000 + _if_bw = 20 + + # Try and avoid the RTLSDR 403.2 MHz spur. + # Note that this is only goign to work if we are detecting on 403.210 or 403.190 MHz. + if (abs(403200000 - frequency) < 20000) and (sdr_type == "RTLSDR"): + logging.debug("Scanner - Narrowing detection IF BW to avoid RTLSDR spur.") + _if_bw = 15 else: # 1680 MHz sondes @@ -314,7 +317,7 @@ def detect_sonde( if _mode == "IQ": # IQ decoding - rx_test_command = f"timeout {dwell_time * 2} " + rx_test_command = f"{timeout_cmd()} {dwell_time * 2} " rx_test_command += get_sdr_iq_cmd( sdr_type=sdr_type, @@ -331,8 +334,9 @@ def detect_sonde( ) # rx_test_command = ( - # "timeout %ds %s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" + # "%s %ds %s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" # % ( + # timeout_cmd(), # dwell_time * 2, # rtl_fm_path, # bias_option, @@ -345,7 +349,8 @@ def detect_sonde( # ) # Saving of Debug audio, if enabled, if save_detection_audio: - rx_test_command += "tee detect_%s.raw | " % str(rtl_device_idx) + detect_iq_path = os.path.join(autorx.logging_path, f"detect_IQ_{frequency}_{_iq_bw}_{str(rtl_device_idx)}.raw") + rx_test_command += f" tee {detect_iq_path} |" rx_test_command += os.path.join( rs_path, "dft_detect" @@ -360,7 +365,7 @@ def detect_sonde( # Sample Source (rtl_fm) - rx_test_command = f"timeout {dwell_time * 2} " + rx_test_command = f"{timeout_cmd()} {dwell_time * 2} " rx_test_command += get_sdr_fm_cmd( sdr_type=sdr_type, @@ -379,8 +384,9 @@ def detect_sonde( ) # rx_test_command = ( - # "timeout %ds %s %s-p %d -d %s %s-M fm -F9 -s %d -f %d 2>/dev/null |" + # "%s %ds %s %s-p %d -d %s %s-M fm -F9 -s %d -f %d 2>/dev/null |" # % ( + # timeout_cmd(), # dwell_time * 2, # rtl_fm_path, # bias_option, @@ -399,7 +405,8 @@ def detect_sonde( # Saving of Debug audio, if enabled, if save_detection_audio: - rx_test_command += "tee detect_%s.wav | " % str(rtl_device_idx) + detect_audio_path = os.path.join(autorx.logging_path, f"detect_audio_{frequency}_{str(rtl_device_idx)}.wav") + rx_test_command += f" tee {detect_audio_path} |" # Sample decoding / detection # Note that we detect for dwell_time seconds, and timeout after dwell_time*2, to catch if no samples are being passed through. @@ -427,6 +434,10 @@ def detect_sonde( ret_output = subprocess.check_output(rx_test_command, shell=True, stderr=FNULL) FNULL.close() ret_output = ret_output.decode("utf8") + + # Release the SDR channel if necessary + shutdown_sdr(sdr_type, rtl_device_idx, sdr_hostname, frequency) + except subprocess.CalledProcessError as e: # dft_detect returns a code of 1 if no sonde is detected. # logging.debug("Scanner - dfm_detect return code: %s" % e.returncode) @@ -445,7 +456,7 @@ def detect_sonde( except Exception as e: # Something broke when running the detection function. logging.error( - f"Scanner ({_sdr_name}) - Error when running dft_detect - {sdr(e)}" + f"Scanner ({_sdr_name}) - Error when running dft_detect - {str(e)}" ) return (None, 0.0) @@ -479,6 +490,8 @@ def detect_sonde( else: _score = float(_score.strip()) _offset_est = 0.0 + + except Exception as e: logging.error( "Scanner - Error parsing dft_detect output: %s" % ret_output.strip() @@ -524,11 +537,13 @@ def detect_sonde( ) _sonde_type = "IMET" elif "IMET1" in _type: + # This could actually be a wideband iMet sonde. We treat this as a IMET4. logging.debug( - "Scanner (%s) - Detected a iMet Sonde! (Type %s - Unsupported) (Score: %.2f)" + "Scanner (%s) - Possible detection of a Wideband iMet Sonde! (Type %s) (Score: %.2f)" % (_sdr_name, _type, _score) ) - _sonde_type = "IMET1" + # Override the type to IMET4. + _sonde_type = "IMET" elif "IMETafsk" in _type: logging.debug( "Scanner (%s) - Detected a iMet Sonde! (Type %s - Unsupported) (Score: %.2f)" @@ -595,6 +610,25 @@ def detect_sonde( else: _sonde_type = "MTS01" + elif "WXR301" in _type: + logging.debug( + "Scanner (%s) - Detected a Weathex WxR-301D Sonde! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) + ) + _sonde_type = "WXR301" + # Clear out the offset estimate for WxR-301's as it's not accurate + # to do no whitening on the signal. + _offset_est = 0.0 + + elif "WXRPN9" in _type: + logging.debug( + "Scanner (%s) - Detected a Weathex WxR-301D Sonde (PN9 Variant)! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) + ) + _sonde_type = "WXRPN9" + # Clear out the offset estimate for WxR-301's as it's not accurate + # to do no whitening on the signal. + _offset_est = 0.0 else: _sonde_type = None @@ -649,6 +683,7 @@ def __init__( temporary_block_list={}, temporary_block_time=60, ngp_tweak=False, + wideband_sondes=False ): """Initialise a Sonde Scanner Object. @@ -698,6 +733,7 @@ def __init__( temporary_block_list (dict): A dictionary where each attribute represents a frequency that should be blocked for a set time. temporary_block_time (int): How long (minutes) frequencies in the temporary block list should remain blocked for. ngp_tweak (bool): Narrow the detection filter when searching for 1680 MHz sondes, to enhance detection of RS92-NGPs. + wideband_sondes (bool): Use a wider detection filter to allow detection of Weathex and wideband iMet sondes. """ # Thread flag. This is set to True when a scan is running. @@ -736,6 +772,7 @@ def __init__( self.callback = callback self.save_detection_audio = save_detection_audio + self.wideband_sondes = wideband_sondes # Temporary block list. self.temporary_block_list = temporary_block_list.copy() @@ -783,9 +820,9 @@ def __init__( def start(self): # Start the scan loop (if not already running) if self.sonde_scan_thread is None: - self.sonde_scanner_running = True self.sonde_scan_thread = Thread(target=self.scan_loop) self.sonde_scan_thread.start() + self.sonde_scanner_running = True else: self.log_warning("Sonde scan already running!") @@ -848,28 +885,42 @@ def scan_loop(self): self.log_warning("SDR produced no output... resetting and retrying.") self.error_retries += 1 # Attempt to reset the SDR, if possible. - reset_sdr( - self.sdr_type, - rtl_device_idx = self.rtl_device_idx, - sdr_hostname = self.sdr_hostname, - sdr_port = self.sdr_port - ) + try: + reset_sdr( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port + ) + except Exception as e: + self.log_error(f"Caught error when trying to reset SDR - {str(e)}") - time.sleep(10) + for _ in range(10): + if not self.sonde_scanner_running: + break + time.sleep(1) continue except Exception as e: traceback.print_exc() self.log_error("Caught other error: %s" % str(e)) - time.sleep(10) + for _ in range(10): + if not self.sonde_scanner_running: + break + time.sleep(1) else: # Scan completed successfuly! Reset the error counter. self.error_retries = 0 # Sleep before starting the next scan. - time.sleep(self.scan_delay) + for _ in range(self.scan_delay): + if not self.sonde_scanner_running: + self.log_debug("Breaking out of scan loop.") + break + time.sleep(1) self.log_info("Scanner Thread Closed.") self.sonde_scanner_running = False + self.sonde_scanner_thread = None def sonde_search(self, first_only=False): """Perform a frequency scan across a defined frequency range, and test each detected peak for the presence of a radiosonde. @@ -927,7 +978,7 @@ def sonde_search(self, first_only=False): (_freq_decimate, _power_decimate) = peak_decimation(freq / 1e6, power, 10) scan_result["freq"] = list(_freq_decimate) scan_result["power"] = list(_power_decimate) - scan_result["timestamp"] = datetime.datetime.utcnow().isoformat() + scan_result["timestamp"] = datetime.datetime.now(datetime.timezone.utc).isoformat() scan_result["peak_freq"] = [] scan_result["peak_lvl"] = [] @@ -1091,6 +1142,7 @@ def sonde_search(self, first_only=False): bias=self.bias, dwell_time=self.detect_dwell_time, save_detection_audio=self.save_detection_audio, + wideband_sondes=self.wideband_sondes ) if detected != None: @@ -1139,12 +1191,16 @@ def oneshot(self, first_only=False): def stop(self, nowait=False): """Stop the Scan Loop""" - self.log_info("Waiting for current scan to finish...") - self.sonde_scanner_running = False + if self.sonde_scanner_running: + self.log_info("Waiting for current scan to finish...") + self.sonde_scanner_running = False - # Wait for the sonde scanner thread to close, if there is one. - if self.sonde_scan_thread != None and (not nowait): - self.sonde_scan_thread.join() + # Wait for the sonde scanner thread to close, if there is one. + if self.sonde_scan_thread != None and (not nowait): + self.sonde_scan_thread.join(60) + if self.sonde_scan_thread.is_alive(): + self.log_error("Scanning thread did not finish, terminating") + sys.exit(4) def running(self): """Check if the scanner is running""" diff --git a/auto_rx/autorx/sdr_wrappers.py b/auto_rx/autorx/sdr_wrappers.py index 62af89d1..d3cce978 100644 --- a/auto_rx/autorx/sdr_wrappers.py +++ b/auto_rx/autorx/sdr_wrappers.py @@ -5,13 +5,15 @@ # Copyright (C) 2022 Mark Jessop # Released under GNU GPL v3 or later # +import autorx import logging import os.path import platform import subprocess import numpy as np -from .utils import rtlsdr_test, reset_rtlsdr_by_serial, reset_all_rtlsdrs +from .utils import rtlsdr_test, reset_rtlsdr_by_serial, reset_all_rtlsdrs, timeout_cmd +from .ka9q import * def test_sdr( @@ -51,13 +53,86 @@ def test_sdr( elif sdr_type == "KA9Q": - # To be implemented - _ok = False + # Test that a KA9Q server is working by attempting to start up a new narrowband channel on it. + + # Check for presence of KA9Q-radio binaries that we need + # if not os.path.isfile('tune'): + # logging.critical("Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed.") + # return False + # if not os.path.isfile('pcmcat'): + # logging.critical("Could not find KA9Q-Radio 'pcmcat' binary! This may need to be compiled and installed.") + # return False + # TBD - whatever we need for spectrum use. + # if not os.path.isfile('TBD'): + # logging.critical("Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed.") + # return False + + + # Try and configure a channel at check_freq Hz + # tune --samprate 48000 --frequency 404m09 --mode iq --ssrc 404090000 --radio sonde.local + _cmd = ( + f"{timeout_cmd()} 5 " # Add a timeout, because connections to non-existing servers block for ages + f"tune " + f"--samprate 48000 --mode iq " + f"--frequency {int(check_freq)} " + f"--ssrc {int(check_freq)}314 " + f"--radio {sdr_hostname}" + ) - if not _ok: - logging.error(f"KA9Q Server {sdr_hostname}:{sdr_port} non-functional.") + logging.debug(f"KA9Q - Testing using command: {_cmd}") - return _ok + try: + _output = subprocess.check_output( + _cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + + if e.returncode == 124: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed with a timeout. Is the server running?" + ) + elif e.returncode == 127: + logging.critical( + f"KA9Q ({sdr_hostname}) - Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed." + ) + else: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed with return code {e.returncode}." + ) + # Look at the error output in a bit more details. + #_output = e.output.decode("ascii") + + # TODO - see if we can look in the output for any error messages. + return False + + # Now close the channel we just opened by setting the frequency to 0 Hz. + _cmd = ( + f"{timeout_cmd()} 5 " # Add a timeout, because connections to non-existing servers block for ages + f"tune " + f"--samprate 48000 --mode iq " + f"--frequency 0 " + f"--ssrc {int(check_freq)}314 " + f"--radio {sdr_hostname}" + ) + + logging.debug(f"KA9Q - Closing testing channel using command: {_cmd}") + try: + _output = subprocess.check_output( + _cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call (closing channel) failed with return code {e.returncode}." + ) + # Look at the error output in a bit more details. + #_output = e.output.decode("ascii") + + # TODO - see if we can look in the output for any error messages. + return False + + return True elif sdr_type == "SpyServer": # Test connectivity to a SpyServer by trying to grab some samples. @@ -67,11 +142,11 @@ def test_sdr( return False _cmd = ( - f"timeout 10 " # Add a timeout, because connections to non-existing IPs seem to block. + f"{timeout_cmd()} 10 " # Add a timeout, because connections to non-existing IPs seem to block. f"{ss_iq_path} " f"-f {check_freq} " f"-s 48000 " - f"-r {sdr_hostname} -q {sdr_port} -n 48000 - > /dev/null 2> /dev/null" + f"-r {sdr_hostname} -q {sdr_port} -n 48000 - > /dev/null" ) logging.debug(f"SpyServer - Testing using command: {_cmd}") @@ -85,6 +160,12 @@ def test_sdr( logging.critical( f"SpyServer ({sdr_hostname}:{sdr_port}) - ss_iq call failed with return code {e.returncode}." ) + # Look at the error output in a bit more details. + _output = e.output.decode("ascii") + if "outside currently allowed range" in _output: + logging.critical( + f"SpyServer ({sdr_hostname}:{sdr_port}) - SpyServer does not cover required frequency {check_freq}, please check your SpyServer configuration!" + ) return False return True @@ -150,7 +231,7 @@ def get_sdr_name( return f"RTLSDR {rtl_device_idx}" elif sdr_type == "KA9Q": - return f"KA9Q {sdr_hostname}:{sdr_port}" + return f"KA9Q {sdr_hostname}" elif sdr_type == "SpyServer": return f"SpyServer {sdr_hostname}:{sdr_port}" @@ -161,7 +242,9 @@ def get_sdr_name( def shutdown_sdr( sdr_type: str, - sdr_id: str + sdr_id: str, + sdr_hostname = "", + frequency: int = None ): """ Function to trigger shutdown/cleanup of some SDR types. @@ -172,8 +255,8 @@ def shutdown_sdr( """ if sdr_type == "KA9Q": - # TODO - KA9Q Server channel cleanup. - logging.debug(f"TODO - Cleanup for SDR type {sdr_type}") + logging.debug(f"KA9Q - Closing Channel for {sdr_hostname} @ {frequency} Hz.") + ka9q_close_channel(sdr_hostname, frequency) pass else: logging.debug(f"No shutdown action required for SDR type {sdr_type}") @@ -272,6 +355,14 @@ def get_sdr_iq_cmd( _cmd += _dc_remove return _cmd + + if sdr_type == "KA9Q": + _cmd = ka9q_get_iq_cmd(sdr_hostname, frequency, sample_rate) + + if dc_block: + _cmd += _dc_remove + + return _cmd else: logging.critical(f"IQ Source - Unsupported SDR type {sdr_type}") @@ -474,23 +565,13 @@ def get_power_spectrum( # Use rtl_power to obtain power spectral density data # Create filename to output to. - _log_filename = f"log_power_{rtl_device_idx}.csv" + _log_filename = os.path.join(autorx.logging_path, f"log_power_{rtl_device_idx}.csv") # If the output log file exists, remove it. if os.path.exists(_log_filename): os.remove(_log_filename) - - # Add -k 30 option, to SIGKILL rtl_power 30 seconds after the regular timeout expires. - # Note that this only works with the GNU Coreutils version of Timeout, not the IBM version, - # which is provided with OSX (Darwin). - _platform = platform.system() - if "Darwin" in _platform: - _timeout_kill = "" - else: - _timeout_kill = "-k 30 " - - _timeout_cmd = f"timeout {_timeout_kill}{integration_time+10}" + _timeout_cmd = f"{timeout_cmd()} {integration_time+10} " _gain = "" if gain: @@ -558,23 +639,13 @@ def get_power_spectrum( # Use a spyserver to obtain power spectral density data # Create filename to output to. - _log_filename = f"log_power_spyserver.csv" + _log_filename = os.path.join(autorx.logging_path, f"log_power_spyserver.csv") # If the output log file exists, remove it. if os.path.exists(_log_filename): os.remove(_log_filename) - - # Add -k 30 option, to SIGKILL rtl_power 30 seconds after the regular timeout expires. - # Note that this only works with the GNU Coreutils version of Timeout, not the IBM version, - # which is provided with OSX (Darwin). - _platform = platform.system() - if "Darwin" in _platform: - _timeout_kill = "" - else: - _timeout_kill = "-k 30 " - - _timeout_cmd = f"timeout {_timeout_kill}{integration_time+10}" + _timeout_cmd = f"{timeout_cmd()} {integration_time+10} " _frequency_centre = int(frequency_start + (frequency_stop-frequency_start)/2.0) @@ -628,8 +699,9 @@ def get_power_spectrum( else: # Unsupported SDR Type - logging.critical(f"Get PSD - Unsupported SDR Type: {sdr_type}") - return (None, None, None) + logging.debug(f"Get PSD - Unsupported SDR Type: {sdr_type}") + return (np.array([0,1,2]),np.array([0,1,2]),1) + #return (None, None, None) if __name__ == "__main__": diff --git a/auto_rx/autorx/sonde_specific.py b/auto_rx/autorx/sonde_specific.py index 9ef531b8..d1b4d10c 100644 --- a/auto_rx/autorx/sonde_specific.py +++ b/auto_rx/autorx/sonde_specific.py @@ -16,7 +16,7 @@ def fix_datetime(datetime_str, local_dt_str=None): """ if local_dt_str is None: - _now = datetime.datetime.utcnow() + _now = datetime.datetime.now(datetime.timezone.utc) else: _now = parse(local_dt_str) diff --git a/auto_rx/autorx/sondehub.py b/auto_rx/autorx/sondehub.py index 141c59d0..804a9250 100644 --- a/auto_rx/autorx/sondehub.py +++ b/auto_rx/autorx/sondehub.py @@ -10,6 +10,8 @@ # Released under GNU GPL v3 or later # import autorx +import base64 +import codecs import datetime import glob import gzip @@ -118,7 +120,7 @@ def reformat_data(self, telemetry): "uploader_callsign": self.user_callsign, "uploader_position": self.user_position, "uploader_antenna": self.user_antenna, - "time_received": datetime.datetime.utcnow().strftime( + "time_received": datetime.datetime.now(datetime.timezone.utc).strftime( "%Y-%m-%dT%H:%M:%S.%fZ" ), } @@ -226,6 +228,21 @@ def reformat_data(self, telemetry): _output["type"] = "MTS01" _output["serial"] = telemetry["id"].split("-")[1] + elif telemetry["type"] == "WXR301": + _output["manufacturer"] = "Weathex" + _output["type"] = "WxR-301D" + _output["serial"] = telemetry["id"].split("-")[1] + + # Double check for the subtype being present, just in case... + if "subtype" in telemetry: + if telemetry["subtype"] == "WXR_PN9": + _output["subtype"] = "WxR-301D-5k" + + elif telemetry["type"] == "WXRPN9": + _output["manufacturer"] = "Weathex" + _output["type"] = "WxR-301D-5k" + _output["serial"] = telemetry["id"].split("-")[1] + else: self.log_error("Unknown Radiosonde Type %s" % telemetry["type"]) return None @@ -286,6 +303,22 @@ def reformat_data(self, telemetry): if "ref_datetime" in telemetry: _output["ref_datetime"] = telemetry["ref_datetime"] + if "rs41_mainboard" in telemetry: + _output["rs41_mainboard"] = telemetry["rs41_mainboard"] + + if "rs41_mainboard_fw" in telemetry: + _output["rs41_mainboard_fw"] = str(telemetry["rs41_mainboard_fw"]) + + if 'rs41_subframe' in telemetry: + # RS41 calibration subframe data. + # We try to base64 encode this. + try: + _calbytes = codecs.decode(telemetry['rs41_subframe'], 'hex') + _output['rs41_subframe'] = base64.b64encode(_calbytes).decode() + except Exception as e: + self.log_error(f"Error handling RS41 subframe data.") + + # Handle the additional SNR and frequency estimation if we have it if "snr" in telemetry: _output["snr"] = telemetry["snr"] diff --git a/auto_rx/autorx/static/css/autorx.css b/auto_rx/autorx/static/css/autorx.css index fcb0cd0d..2a99920c 100644 --- a/auto_rx/autorx/static/css/autorx.css +++ b/auto_rx/autorx/static/css/autorx.css @@ -60,3 +60,18 @@ .icon-cog:before { content: '\e802'; } /* '' */ .icon-angle-down:before { content: '\f107'; } /* '' */ .icon-history:before { content: '\f1da'; } /* '' */ + + +#task_status { + display: flex; + flex-wrap: wrap; + gap: 2px; +} + +.sdrinfo-element { + margin: 0px 4px; + padding: 4px; + + border: 2px solid rgb(135, 135, 135); + border-radius: 1px; +} \ No newline at end of file diff --git a/auto_rx/autorx/static/js/autorxapi.js b/auto_rx/autorx/static/js/autorxapi.js index f1fed89d..7cf7c141 100644 --- a/auto_rx/autorx/static/js/autorxapi.js +++ b/auto_rx/autorx/static/js/autorxapi.js @@ -2,7 +2,7 @@ function update_task_list(){ // Grab the latest task list. - $.getJSON("/get_task_list", function(data){ + $.getJSON("get_task_list", function(data){ var task_info = ""; $('#stop-frequency-select').children().remove(); @@ -10,15 +10,14 @@ function update_task_list(){ added_decoders = false; for (_task in data){ - // Append the current task to the task list text. - if(task_info!=""){ - task_info += "/ "; - } + // Append the current task to the task list. if(_task.includes("SPY")){ - task_info += _task + ": " + data[_task]["task"] + " "; - } else { - task_info += "SDR #" + _task + ": " + data[_task]["task"] + " "; + task_detail = _task + " - " + }else{ + task_detail = "SDR:" + _task + " - " } + + if(data[_task]["freq"] > 0.0){ $('#stop-frequency-select') .append($("") @@ -26,7 +25,22 @@ function update_task_list(){ .text( (parseFloat( data[_task]["freq"] )/1e6).toFixed(3))); added_decoders = true; + + task_detail += (parseFloat( data[_task]["freq"] )/1e6).toFixed(3); + + if (data[_task].hasOwnProperty("type")){ + task_detail += " " + data[_task]["type"]; + } + + } else { + if(data[_task]["task"] == "Scanning"){ + task_detail += "Scan"; + } else { + task_detail += "Idle"; + } } + + task_info += "
" + task_detail + "
" } if(added_decoders == false){ @@ -37,7 +51,7 @@ function update_task_list(){ } // Update page with latest task. - $('#task_status').text(task_info); + $('#task_status').html(task_info); setTimeout(resume_web_controls,2000); }); @@ -48,6 +62,7 @@ function disable_web_controls(){ $("#verify-password").prop('disabled', true); $("#start-decoder").prop('disabled', true); $("#stop-decoder").prop('disabled', true); + $("#stop-decoder-lockout").prop('disabled', true); $("#enable-scanner").prop('disabled', true); $("#disable-scanner").prop('disabled', true); $("#frequency-input").prop('disabled', true); @@ -61,6 +76,7 @@ function pause_web_controls() { $("#verify-password").prop('disabled', true); $("#start-decoder").prop('disabled', true); $("#stop-decoder").prop('disabled', true); + $("#stop-decoder-lockout").prop('disabled', true); $("#enable-scanner").prop('disabled', true); $("#disable-scanner").prop('disabled', true); $("#frequency-input").prop('disabled', true); @@ -72,6 +88,7 @@ function resume_web_controls() { $("#verify-password").prop('disabled', false); $("#start-decoder").prop('disabled', false); $("#stop-decoder").prop('disabled', false); + $("#stop-decoder-lockout").prop('disabled', false); $("#enable-scanner").prop('disabled', false); $("#disable-scanner").prop('disabled', false); $("#frequency-input").prop('disabled', false); @@ -103,7 +120,7 @@ function verify_password(){ // Do the request $.post( - "/check_password", + "check_password", {"password": _api_password}, function(data){ // If OK, update the header to indicate the password was OK. @@ -132,7 +149,7 @@ function disable_scanner(){ // Do the request $.post( - "/disable_scanner", + "disable_scanner", {"password": _api_password}, function(data){ //console.log(data); @@ -169,7 +186,7 @@ function enable_scanner(){ // Do the request $.post( - "/enable_scanner", + "enable_scanner", {"password": _api_password}, function(data){ //console.log(data); @@ -201,7 +218,7 @@ function stop_decoder(){ // Do the request $.post( - "/stop_decoder", + "stop_decoder", {password: _api_password, freq: _decoder}, function(data){ //console.log(data); @@ -221,6 +238,41 @@ function stop_decoder(){ }); } +function stop_decoder_lockout(){ + // Stop the decoder on the requested frequency, and lockout frequency + + // Re-verify the password. This will occur async, so wont stop the main request from going ahead, + // but will at least present an error for the user. + verify_password(); + + // Grab the password + _api_password = getCookie("password"); + + // Grab the selected frequency + _decoder = $('#stop-frequency-select').val(); + + // Do the request + $.post( + "stop_decoder", + {password: _api_password, freq: _decoder, lockout: 1}, + function(data){ + //console.log(data); + pause_web_controls(); + setTimeout(resume_web_controls,10000); + // Need to figure out where to put this data.. + } + ).fail(function(xhr, status, error){ + console.log(error); + // Otherwise, we probably got a 403 error (forbidden) which indicates the password was bad. + if(error == "FORBIDDEN"){ + $("#password-header").html("

Incorrect Password

"); + } else if (error == "NOT FOUND"){ + // Scanner isn't running. Don't do anything. + alert("Decoder on supplied frequency not running!"); + } + }); +} + function start_decoder(){ // Start a decoder on the requested frequency @@ -252,7 +304,7 @@ function start_decoder(){ // Do the request $.post( - "/start_decoder", + "start_decoder", {password: _api_password, freq: _freq_hz, type: _type}, function(data){ alert("Added requested decoder to results queue.") @@ -266,4 +318,4 @@ function start_decoder(){ $("#password-header").html("

Incorrect Password

"); } }); -} \ No newline at end of file +} diff --git a/auto_rx/autorx/static/js/scan_chart.js b/auto_rx/autorx/static/js/scan_chart.js index f2a7520a..9d31db39 100644 --- a/auto_rx/autorx/static/js/scan_chart.js +++ b/auto_rx/autorx/static/js/scan_chart.js @@ -105,6 +105,7 @@ function redraw_scan_chart(){ var date_converted = date.toLocaleString(window.navigator.language,{hourCycle:'h23', year:"numeric", month:"2-digit", day:'2-digit', hour:'2-digit',minute:'2-digit', second:'2-digit'}) + ' ' + timezone; $('#scan_results').html('Latest Scan: ' + date_converted); } else { - $('#scan_results').html('Latest Scan: ' + (scan_chart_latest_timestamp.slice(0, -3) + 'Z').replace("T", " ").replace("Z", "").slice(0, -4) + ' UTC'); + var date_converted = scan_chart_latest_timestamp.slice(0, 19).replace("T", " ") + ' UTC' } -} \ No newline at end of file + $('#scan_results').html('Latest Scan: ' + date_converted); +} diff --git a/auto_rx/autorx/static/js/utils.js b/auto_rx/autorx/static/js/utils.js index 8ecb224c..270281cb 100644 --- a/auto_rx/autorx/static/js/utils.js +++ b/auto_rx/autorx/static/js/utils.js @@ -16,12 +16,12 @@ var sondeDescentIcons = {}; // TODO: Make these /static URLS be filled in with templates (or does it not matter?) for (_col in colour_values){ sondeAscentIcons[colour_values[_col]] = L.icon({ - iconUrl: "/static/img/balloon-" + colour_values[_col] + '.png', + iconUrl: "static/img/balloon-" + colour_values[_col] + '.png', iconSize: [46, 85], iconAnchor: [23, 76] }); sondeDescentIcons[colour_values[_col]] = L.icon({ - iconUrl: "/static/img/parachute-" + colour_values[_col] + '.png', + iconUrl: "static/img/parachute-" + colour_values[_col] + '.png', iconSize: [46, 84], iconAnchor: [23, 76] }); diff --git a/auto_rx/autorx/stats.py b/auto_rx/autorx/stats.py index 81bc7ccb..8e4508d4 100644 --- a/auto_rx/autorx/stats.py +++ b/auto_rx/autorx/stats.py @@ -74,10 +74,10 @@ def radio_horizon_plot(log_files, min_range_km=10, max_range_km=1000, save_figur plt.grid() -def normalised_snr(log_files, min_range_km=10, max_range_km=1000, maxsnr=False, meansnr=True, normalise=True): +def normalised_snr(log_files, min_range_km=10, max_range_km=1000, maxsnr=False, meansnr=True, normalise=True, norm_range=50): """ Read in ALL log files and store snr data into a set of bins, normalised to 50km range. """ - _norm_range = 50 # km + _norm_range = norm_range # km _snr_count = 0 @@ -198,6 +198,12 @@ def normalised_snr(log_files, min_range_km=10, max_range_km=1000, maxsnr=False, default=False, help="Generate Normalised SNR Map (Maximum SNR)" ) + parser.add_argument( + "--normrange", + type=float, + default=50, + help="Normalistion Range (km, default=50)" + ) parser.add_argument( "-v", "--verbose", help="Enable debug output.", action="store_true" @@ -213,6 +219,8 @@ def normalised_snr(log_files, min_range_km=10, max_range_km=1000, maxsnr=False, format="%(asctime)s %(levelname)s:%(message)s", level=_log_level ) + autorx.logging_path = args.log + # Read in the config and make it available to other functions _temp_cfg = read_auto_rx_config(args.config, no_sdr_test=True) autorx.config.global_config = _temp_cfg @@ -220,7 +228,7 @@ def normalised_snr(log_files, min_range_km=10, max_range_km=1000, maxsnr=False, # Read in the log files. logging.info("Quick-Looking Log Files") - log_list = list_log_files(quicklook=True) + log_list = list_log_files(quicklook=True, custom_log_dir=args.log) logging.info(f"Loaded in {len(log_list)} log files.") @@ -228,13 +236,13 @@ def normalised_snr(log_files, min_range_km=10, max_range_km=1000, maxsnr=False, radio_horizon_plot(log_list) if args.snrmap: - normalised_snr(log_list) + normalised_snr(log_list, norm_range=args.normrange) if args.snrmapmax: normalised_snr(log_list, meansnr=False, maxsnr=True, normalise=False) if args.snrmapmaxnorm: - normalised_snr(log_list, meansnr=False, maxsnr=True, normalise=True) + normalised_snr(log_list, meansnr=False, maxsnr=True, normalise=True, norm_range=args.normrange) plt.show() diff --git a/auto_rx/autorx/templates/historical.html b/auto_rx/autorx/templates/historical.html index 986feb96..5dd416ef 100644 --- a/auto_rx/autorx/templates/historical.html +++ b/auto_rx/autorx/templates/historical.html @@ -48,6 +48,9 @@ quickLandings = false; namespace = '/update_status'; + var socket_path = "{{ url_for("static", filename="") }}".replace('static/', 'socket.io') + var socket = io.connect(location.origin+namespace, {'path': socket_path}); + var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace); @@ -69,7 +72,7 @@ $.ajax({ // Get station.cfg file. - url: "/get_config", + url: "get_config", dataType: 'json', async: false, success: function(data) { @@ -153,7 +156,11 @@ }}, {title:"Last H", field:"min_height", width:75, resizable:false, headerTooltip:"Last Observed Height (m)", formatter:function(cell, formatterParams, onRendered){ - return cell.getValue() + " m"; //return the contents of the cell; + if (getCookie('imperial') == 'true') { + return cell.getValue()*3.28084.toFixed(0) + " ft"; + } else { + return cell.getValue() + " m"; + } }}, {title:"Min R", field:"min_range", width:75, resizable:false, headerTooltip:"Min Observed Range (km)", formatter:function(cell, formatterParams, onRendered){ @@ -165,11 +172,19 @@ }}, {title:"Last R", field:"last_range", width:75, resizable:false, headerTooltip:"Last Observed Range (km)", formatter:function(cell, formatterParams, onRendered){ - return cell.getValue() + " km"; //return the contents of the cell; + if (getCookie('imperial') == 'true') { + return cell.getValue()*0.621371.toFixed(0) + " mi"; + } else { + return cell.getValue() + " km"; + } }}, - {title:"Max R", field:"max_range", width:73, resizable:false, headerTooltip:"Maximum Observed Range (km))", + {title:"Max R", field:"max_range", width:73, resizable:false, headerTooltip:"Maximum Observed Range", formatter:function(cell, formatterParams, onRendered){ - return cell.getValue() + " km"; //return the contents of the cell; + if (getCookie('imperial') == 'true') { + return cell.getValue()*0.621371.toFixed(0) + " mi"; + } else { + return cell.getValue() + " km"; + } }}, {formatter:"rowSelection", titleFormatter:"rowSelection", align:"center", width:40, headerSort:false, titleFormatter: function(cell, formatterParams, onRendered){}} ], @@ -206,6 +221,7 @@ $("#showsonde-skew").prop('disabled', true); $("#hidesonde-skew").prop('disabled', true); $("#download-logs").prop('disabled', true); + $("#generate-kml").prop('disabled', true); } async function enableMenu () { @@ -219,6 +235,7 @@ $("#showsonde-skew").prop('disabled', false); $("#hidesonde-skew").prop('disabled', false); $("#download-logs").prop('disabled', false); + $("#generate-kml").prop('disabled', false); } if ((window.innerWidth/window.innerHeight) < 1) { @@ -501,7 +518,7 @@ highest = i; } $.ajax({ - url: "/get_log_by_serial/" + selectedrows[i]['serial'], + url: "get_log_by_serial/" + selectedrows[i]['serial'], dataType: 'json', async: true, success: function(data) { @@ -770,7 +787,7 @@ table.selectRow(); mymap.eachLayer(function(layer){ try { - if (layer['options']['icon']['options']['iconUrl'] == "/static/img/landing_marker.png" || layer['options']['icon']['options']['iconUrl'] == "/static/img/launch_marker.png") { + if (layer['options']['icon']['options']['iconUrl'] == "static/img/landing_marker.png" || layer['options']['icon']['options']['iconUrl'] == "static/img/launch_marker.png") { new_icon = layer['options']['icon']; new_icon.options.iconSize = [20, 20]; new_icon.options.iconAnchor = [10, 10]; @@ -785,7 +802,7 @@ table.deselectRow(); mymap.eachLayer(function(layer){ try { - if (layer['options']['icon']['options']['iconUrl'] == "/static/img/landing_marker.png" || layer['options']['icon']['options']['iconUrl'] == "/static/img/launch_marker.png") { + if (layer['options']['icon']['options']['iconUrl'] == "static/img/landing_marker.png" || layer['options']['icon']['options']['iconUrl'] == "static/img/launch_marker.png") { new_icon = layer['options']['icon']; new_icon.options.iconSize = [15, 15]; new_icon.options.iconAnchor = [7.5, 7.5]; @@ -809,7 +826,7 @@ mymap.eachLayer(function(layer){ try { - if (layer['options']['icon']['options']['iconUrl'] == "/static/img/landing_marker.png" || layer['options']['icon']['options']['iconUrl'] == "/static/img/launch_marker.png") { + if (layer['options']['icon']['options']['iconUrl'] == "static/img/landing_marker.png" || layer['options']['icon']['options']['iconUrl'] == "static/img/launch_marker.png") { if (layer['options']['icon']['options']['iconSize'][0] == 15) { if (!shown.includes(layer['options']['title'])) { mymap.removeLayer(layer); @@ -896,7 +913,7 @@ _serial = selectedrows[selectedrows.length-1]['serial']; _type = selectedrows[selectedrows.length-1]['type']; $.post( - "/get_log_detail", + "get_log_detail", {serial: _serial, decimation:decimation}, async function(data){ try { @@ -1001,13 +1018,13 @@ if(_serial_list.length == table.getData().length){ // Request all log files - window.open("/export_all_log_files" , '_blank'); + window.open("export_all_log_files" , '_blank'); }else { // Just request the selected ones. // Convert the list to JSON, and then to base64 b64 = btoa(JSON.stringify(_serial_list)); // Make the request in a new tab - window.open("/export_log_files/"+b64 , '_blank'); + window.open("export_log_files/"+b64 , '_blank'); } } } @@ -1016,6 +1033,41 @@ downloadLogs(); }); + function generateKML() { + // Generate a KML file from a set of log files. + selectedrows = table.getSelectedData(); + if (selectedrows.length > 0) { + // Create the list of log files. + _serial_list = []; + for (let i = 0; i < selectedrows.length; i++){ + _serial_list.push(selectedrows[i]['serial']); + } + + if(_serial_list.length>50){ + if (confirm("Warning - downloading lots of log may take some time. Are you sure?")) { + // Just continue on. + } else { + return; + } + } + + if(_serial_list.length == table.getData().length){ + // Request all log files + window.open("generate_kml" , '_blank'); + }else { + // Just request the selected ones. + // Convert the list to JSON, and then to base64 + b64 = btoa(JSON.stringify(_serial_list)); + // Make the request in a new tab + window.open("generate_kml/"+b64 , '_blank'); + } + } + } + + $("#generate-kml").click(function(){ + generateKML(); + }); + // List of available map layers. var Mapnik = L.tileLayer.provider("OpenStreetMap.Mapnik", {edgeBufferTiles: 2}); var DarkMatter = L.tileLayer.provider("CartoDB.DarkMatter", {edgeBufferTiles: 2}); @@ -1549,6 +1601,7 @@

Sonde List

+
@@ -1629,4 +1682,4 @@

" + "" + ""; @@ -689,17 +693,6 @@ sonde_id_data.vel_h = (sonde_id_data.vel_h*3.6).toFixed(1); - // Add a link to HabHub if we have habitat enabled. - // if (autorx_config.sondehub_enabled == true) { - // sonde_id_data.id = "" + sonde_id + ""; - // // These links are only going to work for Vaisala radiosondes since the APRS callsign is never passed through to the web interface, - // // and the APRS callsigns for everything other than RS41s and RS92s is different to the 'full' serials - // } else if (autorx_config.aprs_enabled == true && autorx_config.aprs_server == "radiosondy.info") { - // sonde_id_data.id = "" + sonde_id + ""; - // } else if (autorx_config.aprs_enabled == true) { - // sonde_id_data.id = "" + sonde_id + ""; - // } - sonde_id_data.realid = sonde_id; // Add SNR data, if it exists. @@ -709,6 +702,14 @@ // Add data into the 'other' field. sonde_id_data.other = ""; + + if(sonde_id_data.hasOwnProperty('rs41_mainboard')){ + // Only print mainboard type if it's not the 'original' mainboard. + if(sonde_id_data.rs41_mainboard !== 'RSM412'){ + sonde_id_data.other += sonde_id_data.rs41_mainboard + " "; + } + } + // Burst timer for RS41s if (sonde_id_data.hasOwnProperty('bt')){ if ((sonde_id_data.bt >= 0) && (sonde_id_data.bt < 65535)) { @@ -719,6 +720,11 @@ sonde_id_data.other += sonde_id_data.batt.toFixed(1) + " V"; } + if (sonde_id_data.hasOwnProperty('aux')){ + sonde_id_data.type += "-XDATA"; + } + + telem_data.push(sonde_id_data); }); } @@ -736,7 +742,7 @@ var initial_load_complete = false; selected_sonde = ""; $.ajax({ // Get archived data. - url: "/get_telemetry_archive", + url: "get_telemetry_archive", dataType: 'json', async: true, success: function(data) { @@ -1571,7 +1577,7 @@

Show Software Version

Live KML

 
- +


@@ -1592,7 +1598,7 @@

Controls

Historical View

 
- +


@@ -1611,7 +1617,7 @@

Historical View

Station: ???

-

Current Task: ???

+

Tasking:

@@ -1658,6 +1664,8 @@

Decoder Control

+ +
@@ -1670,6 +1678,9 @@

Decoder Control

+
+ +

Scanner Control

Scanner

diff --git a/auto_rx/autorx/templates/skewt_test.html b/auto_rx/autorx/templates/skewt_test.html index 7fe28d63..ea2ceba9 100644 --- a/auto_rx/autorx/templates/skewt_test.html +++ b/auto_rx/autorx/templates/skewt_test.html @@ -59,7 +59,7 @@

auto_rx SkewT Plot Testing

_decim = $('#decimation-input').val(); $.post( - "/get_log_detail", + "get_log_detail", {serial: _serial, decimation:_decim}, function(data){ console.log(data); diff --git a/auto_rx/autorx/utils.py b/auto_rx/autorx/utils.py index 8e502faa..ac5f3c5e 100644 --- a/auto_rx/autorx/utils.py +++ b/auto_rx/autorx/utils.py @@ -19,6 +19,7 @@ import time import numpy as np import semver +import shutil from dateutil.parser import parse from datetime import datetime, timedelta from math import radians, degrees, sin, cos, atan2, sqrt, pi @@ -42,11 +43,27 @@ "m20mod", "imet4iq", "mts01mod", - "iq_dec" + "iq_dec", + "weathex301d" ] +_timeout_cmd = None -def check_rs_utils(): +def timeout_cmd(): + global _timeout_cmd + if not _timeout_cmd: + t=shutil.which("gtimeout") + if t: + _timeout_cmd = "gtimeout -k 30 " + else: + if not shutil.which("timeout"): + logging.critical("timeout command-line tool not present in system. try installing gtimeout.") + sys.exit(1) + else: + _timeout_cmd = "timeout -k 30 " + return _timeout_cmd + +def check_rs_utils(config): """ Check the required RS decoder binaries exist Currently we just check there is a file present - we don't check functionality. """ @@ -54,6 +71,7 @@ def check_rs_utils(): if not os.path.isfile(_file): logging.critical("Binary %s does not exist - did you run build.sh?" % _file) return False + _ = timeout_cmd() return True @@ -142,7 +160,7 @@ def strip_sonde_serial(serial): """ Strip off any leading sonde type that may be present in a serial number """ # Look for serials with prefixes matching the following known sonde types. - _re = re.compile("^(DFM|M10|M20|IMET|IMET5|IMET54|MRZ|LMS6|IMS100|RS11G|MTS01)-") + _re = re.compile("^(DFM|M10|M20|IMET|IMET5|IMET54|MRZ|LMS6|IMS100|RS11G|MTS01|WXR)-") # If we have a match, return the trailing part of the serial, re-adding # any - separators if they exist. @@ -178,6 +196,8 @@ def short_type_lookup(type_name): return "Lockheed Martin LMS6-1680" elif type_name == "IMET": return "Intermet Systems iMet-1/4" + elif type_name == "IMET-XDATA": + return "Intermet Systems iMet-1/4 + XDATA" elif type_name == "IMET5": return "Intermet Systems iMet-5x" elif type_name == "MEISEI": @@ -190,6 +210,10 @@ def short_type_lookup(type_name): return "Meteo-Radiy MRZ" elif type_name == "MTS01": return "Meteosis MTS01" + elif type_name == "WXR301": + return "Weathex WxR-301D" + elif type_name == "WXRPN9": + return "Weathex WxR-301D (PN9 Variant)" else: return "Unknown" @@ -218,6 +242,8 @@ def short_short_type_lookup(type_name): return "LMS6-1680" elif type_name == "IMET": return "iMet-1/4" + elif type_name == "IMET-XDATA": + return "iMet-1/4" elif type_name == "IMET5": return "iMet-5x" elif type_name == "MEISEI": @@ -230,6 +256,10 @@ def short_short_type_lookup(type_name): return "MRZ" elif type_name == "MTS01": return "MTS01" + elif type_name == "WXR301": + return "WXR301" + elif type_name == "WXRPN9": + return "WXR301(PN9)" else: return "Unknown" @@ -284,6 +314,12 @@ def generate_aprs_id(sonde_data): _id_suffix = int(sonde_data["id"].split("-")[1]) _id_hex = hex(_id_suffix).upper() _object_name = "LMS6" + _id_hex[-5:] + + elif "WXR" in sonde_data["type"]: + # Use the last 6 hex digits of the sonde ID. + _id_suffix = int(sonde_data["id"].split("-")[1]) + _id_hex = hex(_id_suffix).upper() + _object_name = "WXR" + _id_hex[-6:] elif "MEISEI" in sonde_data["type"] or "IMS100" in sonde_data["type"] or "RS11G" in sonde_data["type"]: # Convert the serial number to an int @@ -758,8 +794,10 @@ def reset_usb(bus, device): try: fcntl.ioctl(usb_file, _USBDEVFS_RESET) - except IOError: - logging.error("RTLSDR - USB Reset Failed.") + # This was just catching IOError, just catch everything and print. + except Exception as e: + logging.error(f"RTLSDR - USB Reset Failed - {str(e)}") + def is_rtlsdr(vid, pid): @@ -776,10 +814,10 @@ def is_rtlsdr(vid, pid): def reset_rtlsdr_by_serial(serial): """ Attempt to reset a RTLSDR with a provided serial number """ - # If not Linux, return immediately. + # If not Linux, raise exception and let auto_rx.py convert it to exit status code. if is_not_linux(): logging.debug("RTLSDR - Not a native Linux system, skipping reset attempt.") - return + raise SystemError("SDR unresponsive") lsusb_info = lsusb() bus_num = None @@ -853,10 +891,10 @@ def find_rtlsdr(serial=None): def reset_all_rtlsdrs(): """ Reset all RTLSDR devices found in the lsusb tree """ - # If not Linux, return immediately. + # If not Linux, raise exception and let auto_rx.py convert it to exit status code. if is_not_linux(): logging.debug("RTLSDR - Not a native Linux system, skipping reset attempt.") - return + raise SystemError("SDR unresponsive") lsusb_info = lsusb() bus_num = None @@ -906,11 +944,12 @@ def rtlsdr_test(device_idx="0", rtl_sdr_path="rtl_sdr", retries=5): logging.debug("RTLSDR - TCP Device, skipping RTLSDR test step.") return True - _rtl_cmd = "timeout 5 %s -d %s -n 200000 - > /dev/null" % ( + _rtl_cmd = "%s 5 %s -d %s -f 400000000 -n 200000 - > /dev/null" % ( + timeout_cmd(), rtl_sdr_path, str(device_idx), ) - + # First, check if the RTLSDR with a provided serial number is present. if device_idx == "0": # Check for the presence of any RTLSDRs. @@ -936,10 +975,16 @@ def rtlsdr_test(device_idx="0", rtl_sdr_path="rtl_sdr", retries=5): FNULL = open(os.devnull, "w") # Inhibit stderr output _ret_code = subprocess.check_call(_rtl_cmd, shell=True, stderr=FNULL) FNULL.close() - except subprocess.CalledProcessError: + except subprocess.CalledProcessError as e: # This exception means the subprocess has returned an error code of one. - # This indicates either the RTLSDR doesn't exist, or - pass + # This indicates either the RTLSDR doesn't exist, or some other error. + if e.returncode == 127: + # 127 = File not found + logging.critical("rtl_sdr utilities (rtl_sdr, rtl_fm, rtl_power) not found!") + return False + else: + logging.warning(f"rtl_sdr test call resulted in return code of {e.returncode}.") + pass else: # rtl-sdr returned OK. We can return True now. time.sleep(1) @@ -954,7 +999,7 @@ def rtlsdr_test(device_idx="0", rtl_sdr_path="rtl_sdr", retries=5): # Decrement out retry count, then wait a bit before looping _rtlsdr_retries -= 1 - time.sleep(2) + time.sleep(5) # If we run out of retries, clearly the RTLSDR isn't working. logging.error( diff --git a/auto_rx/autorx/web.py b/auto_rx/autorx/web.py index e6a525cc..36f39b54 100644 --- a/auto_rx/autorx/web.py +++ b/auto_rx/autorx/web.py @@ -8,33 +8,37 @@ import base64 import copy import datetime +import glob +import io import json import logging +import os import random import requests import time import traceback import sys +import xml.etree.ElementTree as ET import autorx import autorx.config import autorx.scan from autorx.geometry import GenericTrack from autorx.utils import check_autorx_versions -from autorx.log_files import list_log_files, read_log_by_serial, zip_log_files +from autorx.log_files import ( + list_log_files, + read_log_by_serial, + zip_log_files, + log_files_to_kml, + coordinates_to_kml_placemark, + path_to_kml_placemark +) +from autorx.decode import SondeDecoder from queue import Queue from threading import Thread import flask from flask import request, abort, make_response, send_file from flask_socketio import SocketIO -import re - -try: - from simplekml import Kml, AltitudeMode -except ImportError: - print( - "Could not import simplekml! Try running: sudo pip3 install -r requirements.txt" - ) - sys.exit(1) +from werkzeug.middleware.proxy_fix import ProxyFix # Inhibit Flask warning message about running a development server... (we know!) @@ -43,13 +47,14 @@ # Instantiate our Flask app. app = flask.Flask(__name__) +app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_prefix=1) app.config["SECRET_KEY"] = "secret!" app.config["TEMPLATES_AUTO_RELOAD"] = True app.jinja_env.auto_reload = True # This thread will hold the currently running flask application thread. flask_app_thread = None # A key that needs to be matched to allow shutdown. -flask_shutdown_key = "temp" +flask_shutdown_key = None # SocketIO instance socketio = SocketIO(app, async_mode="threading") @@ -105,6 +110,7 @@ def flask_get_version(): def flask_get_task_list(): """ Return the current list of active SDRs, and their active task names """ + # Read in the task list, index by SDR ID. _task_list = {} for _task in autorx.task_list.keys(): @@ -124,9 +130,16 @@ def flask_get_task_list(): "task": "Decoding (%.3f MHz)" % (_task_list[str(_sdr)] / 1e6), "freq": _task_list[str(_sdr)], } + except: _sdr_list[str(_sdr)] = {"task": "Decoding (?? MHz)", "freq": 0} + # Try and add on sonde type. + try: + _sdr_list[str(_sdr)]['type'] = autorx.task_list[_task_list[str(_sdr)]]['task'].sonde_type + except: + pass + # Convert the task list to a JSON blob, and return. return json.dumps(_sdr_list) @@ -135,47 +148,60 @@ def flask_get_task_list(): def flask_get_kml(): """ Return KML with autorefresh """ - _config = autorx.config.global_config - kml = Kml() - netlink = kml.newnetworklink(name="Radiosonde Auto-RX Live Telemetry") - netlink.open = 1 - netlink.link.href = flask.request.host_url + "rs_feed.kml" - try: - netlink.link.refreshinterval = _config["kml_refresh_rate"] - except KeyError: - netlink.link.refreshinterval = 10 - netlink.link.refreshmode = "onInterval" - return kml.kml(), 200, {"content-type": "application/vnd.google-earth.kml+xml"} + kml_root = ET.Element("kml", xmlns="http://www.opengis.net/kml/2.2") + kml_doc = ET.SubElement(kml_root, "Document") + + network_link = ET.SubElement(kml_doc, "NetworkLink") + + name = ET.SubElement(network_link, "name") + name.text = "Radiosonde Auto-RX Live Telemetry" + + open = ET.SubElement(network_link, "open") + open.text = "1" + + link = ET.SubElement(network_link, "Link") + + href = ET.SubElement(link, "href") + href.text = flask.request.url_root + "rs_feed.kml" + + refresh_mode = ET.SubElement(link, "refreshMode") + refresh_mode.text = "onInterval" + + refresh_interval = ET.SubElement(link, "refreshInterval") + refresh_interval.text = str(autorx.config.global_config["kml_refresh_rate"]) + + kml_string = ET.tostring(kml_root, encoding="UTF-8", xml_declaration=True) + return kml_string, 200, {"content-type": "application/vnd.google-earth.kml+xml"} @app.route("/rs_feed.kml") def flask_get_kml_feed(): """ Return KML with RS telemetry """ - kml = Kml() - kml.resetidcounter() - kml.document.name = "Track" - kml.document.open = 1 + kml_root = ET.Element("kml", xmlns="http://www.opengis.net/kml/2.2") + kml_doc = ET.SubElement(kml_root, "Document") + + name = ET.SubElement(kml_doc, "name") + name.text = "Track" + open = ET.SubElement(kml_doc, "open") + open.text = "1" + # Station Placemark - pnt = kml.newpoint( - name="Ground Station", - altitudemode=AltitudeMode.absolute, + kml_doc.append(coordinates_to_kml_placemark( + autorx.config.global_config["station_lat"], + autorx.config.global_config["station_lon"], + autorx.config.global_config["station_alt"], + name=autorx.config.global_config["habitat_uploader_callsign"], description="AutoRX Ground Station", - ) - pnt.open = 1 - pnt.iconstyle.icon.href = flask.request.host_url + "static/img/antenna-green.png" - pnt.coords = [ - ( - autorx.config.global_config["station_lon"], - autorx.config.global_config["station_lat"], - autorx.config.global_config["station_alt"], - ) - ] + absolute=True, + icon=flask.request.url_root + "static/img/antenna-green.png" + )) + for rs_id in flask_telemetry_store: try: coordinates = [] for tp in flask_telemetry_store[rs_id]["track"].track_history: - coordinates.append((tp[2], tp[1], tp[3])) + coordinates.append((tp[1], tp[2], tp[3])) rs_data = """\ {type}/{subtype} @@ -183,67 +209,70 @@ def flask_get_kml_feed(): Altitude: {alt:.1f} m Heading: {heading:.1f} degrees Ground Speed: {vel_h:.2f} m/s - Ascent Rate: {vel_v:.2} m/s + Ascent Rate: {vel_v:.2f} m/s Temperature: {temp:.1f} C Humidity: {humidity:.1f} % Pressure: {pressure:.1f} hPa """ if flask_telemetry_store[rs_id]["latest_telem"]["vel_v"] > -5: - icon = flask.request.host_url + "static/img/balloon-green.png" + icon = flask.request.url_root + "static/img/balloon-green.png" else: - icon = flask.request.host_url + "static/img/parachute-green.png" + icon = flask.request.url_root + "static/img/parachute-green.png" # Add folder - fol = kml.newfolder(name=rs_id) + folder = ET.SubElement(kml_doc, "Folder", id=f"folder_{rs_id}") + name = ET.SubElement(folder, "name") + name.text = rs_id + open = ET.SubElement(folder, "open") + open.text = "1" + # HAB Placemark - pnt = fol.newpoint( + folder.append(coordinates_to_kml_placemark( + flask_telemetry_store[rs_id]["latest_telem"]["lat"], + flask_telemetry_store[rs_id]["latest_telem"]["lon"], + flask_telemetry_store[rs_id]["latest_telem"]["alt"], name=rs_id, - altitudemode=AltitudeMode.absolute, - description=rs_data.format( - **flask_telemetry_store[rs_id]["latest_telem"] - ), - ) - pnt.iconstyle.icon.href = icon - pnt.coords = [ + description=rs_data.format(**flask_telemetry_store[rs_id]["latest_telem"]), + absolute=True, + icon=icon + )) + + # Track + folder.append(path_to_kml_placemark( + coordinates, + name="Track", + absolute=True, + extrude=True + )) + + # LOS line + coordinates = [ ( - flask_telemetry_store[rs_id]["latest_telem"]["lon"], - flask_telemetry_store[rs_id]["latest_telem"]["lat"], - flask_telemetry_store[rs_id]["latest_telem"]["alt"], - ) - ] - linestring = fol.newlinestring(name="Track") - linestring.coords = coordinates - linestring.altitudemode = AltitudeMode.absolute - linestring.extrude = 1 - linestring.stylemap.normalstyle.linestyle.color = "ff03bafc" - linestring.stylemap.highlightstyle.linestyle.color = "ff03bafc" - linestring.stylemap.normalstyle.polystyle.color = "AA03bafc" - linestring.stylemap.highlightstyle.polystyle.color = "CC03bafc" - # Add LOS line - linestring = fol.newlinestring(name="LOS") - linestring.altitudemode = AltitudeMode.absolute - linestring.coords = [ - ( - autorx.config.global_config["station_lon"], autorx.config.global_config["station_lat"], + autorx.config.global_config["station_lon"], autorx.config.global_config["station_alt"], ), ( - flask_telemetry_store[rs_id]["latest_telem"]["lon"], flask_telemetry_store[rs_id]["latest_telem"]["lat"], + flask_telemetry_store[rs_id]["latest_telem"]["lon"], flask_telemetry_store[rs_id]["latest_telem"]["alt"], ), ] + folder.append(path_to_kml_placemark( + coordinates, + name="LOS", + track_color="ffffffff", + absolute=True, + extrude=False + )) + except Exception as e: logging.error( "KML - Could not parse data from RS %s - %s" % (rs_id, str(e)) ) - return ( - re.sub("", "", kml.kml()), - 200, - {"content-type": "application/vnd.google-earth.kml+xml"}, - ) + kml_string = ET.tostring(kml_root, encoding="UTF-8", xml_declaration=True) + return kml_string, 200, {"content-type": "application/vnd.google-earth.kml+xml"} @app.route("/get_config") @@ -279,7 +308,11 @@ def shutdown_flask(shutdown_key): global flask_shutdown_key # Only shutdown if the supplied key matches our shutdown key if shutdown_key == flask_shutdown_key: - flask.request.environ.get("werkzeug.server.shutdown")() + shutdown_function = flask.request.environ.get("werkzeug.server.shutdown") + if shutdown_function: + shutdown_function() + else: + logging.debug("Unable to stop this version of Werkzeug, continuing...") return "" @@ -289,6 +322,9 @@ def flask_get_log_list(quicklook_option): """ Return a list of log files, as a list of objects """ return json.dumps(list_log_files(quicklook=True, quicklook_option=quicklook_option)) +def flask_running(): + global flask_shutdown_key + return flask_shutdown_key is not None @app.route("/get_log_by_serial/") def flask_get_log_by_serial(serial): @@ -317,19 +353,20 @@ def flask_get_log_by_serial_detail(): return json.dumps(read_log_by_serial(_serial, skewt_decimation=_decim)) +@app.route("/export_all_log_files") @app.route("/export_log_files/") -def flask_export_selected_log_files(serialb64): +def flask_export_log_files(serialb64=None): """ Zip and download a set of log files. The list of log files is provided in the URL as a base64-encoded JSON list. """ try: - _serial_list = json.loads(base64.b64decode(serialb64)) + _serial_list = json.loads(base64.b64decode(serialb64)) if serialb64 else None _zip = zip_log_files(_serial_list) - _ts = datetime.datetime.strftime(datetime.datetime.utcnow(), "%Y%m%d-%H%M%SZ") + _ts = datetime.datetime.strftime(datetime.datetime.now(datetime.timezone.utc), "%Y%m%d-%H%M%SZ") response = make_response( flask.send_file( @@ -351,23 +388,41 @@ def flask_export_selected_log_files(serialb64): abort(400) -@app.route("/export_all_log_files") -def flask_export_all_log_files(): +@app.route("/generate_kml") +@app.route("/generate_kml/") +def flask_generate_kml(serialb64=None): """ - Zip and download all log files. This may take some time. + Generate a KML file from a set of log files. + The list of log files is provided in the URL as a base64-encoded JSON list. """ try: - _zip = zip_log_files() + if serialb64: + _serial_list = json.loads(base64.b64decode(serialb64)) + _log_files = [] + for _serial in _serial_list: + _log_mask = os.path.join(autorx.logging_path, f"*_*{_serial}_*_sonde.log") + _matching_files = glob.glob(_log_mask) + + if len(_matching_files) >= 1: + _log_files.append(_matching_files[0]) + else: + _log_mask = os.path.join(autorx.logging_path, "*_sonde.log") + _log_files = glob.glob(_log_mask) + + _kml_file = io.BytesIO() + _log_files.sort(reverse=True) + log_files_to_kml(_log_files, _kml_file) + _kml_file.seek(0) - _ts = datetime.datetime.strftime(datetime.datetime.utcnow(), "%Y%m%d-%H%M%SZ") + _ts = datetime.datetime.strftime(datetime.datetime.now(datetime.timezone.utc), "%Y%m%d-%H%M%SZ") response = make_response( flask.send_file( - _zip, - mimetype="application/zip", + _kml_file, + mimetype="application/vnd.google-earth.kml+xml", as_attachment=True, - download_name=f"autorx_logfiles_{autorx.config.global_config['habitat_uploader_callsign']}_{_ts}.zip", + download_name=f"autorx_logfiles_{autorx.config.global_config['habitat_uploader_callsign']}_{_ts}.kml", ) ) @@ -378,7 +433,7 @@ def flask_export_all_log_files(): return response except Exception as e: - logging.error("Web - Error handling Zip request:" + str(e)) + logging.error("Web - Error handling KML request:" + str(e)) abort(400) # @@ -446,7 +501,11 @@ def flask_start_decoder(): def flask_stop_decoder(): """ Request that a decoder process be halted. Example: - curl -d "freq=403250000" -X POST http://localhost:5000/stop_decoder + + curl -d "freq=403250000&password=foobar" -X POST http://localhost:5000/stop_decoder + + Stop decoder and lockout for temporary_block_time + curl -d "freq=403250000&password=foobar&lockout=1" -X POST http://localhost:5000/stop_decoder """ if request.method == "POST" and autorx.config.global_config["web_control"]: @@ -458,10 +517,15 @@ def flask_stop_decoder(): ): _freq = float(request.form["freq"]) + _lockout = False + if "lockout" in request.form: + if int(request.form["lockout"]) == 1: + _lockout = True + logging.info("Web - Got decoder stop request: %f" % (_freq)) if _freq in autorx.task_list: - autorx.task_list[_freq]["task"].stop(nowait=True) + autorx.task_list[_freq]["task"].stop(nowait=True, temporary_lockout=_lockout) return "OK" else: # If we aren't running a decoder, 404. @@ -572,6 +636,8 @@ def start_flask(host="0.0.0.0", port=5000): # Start up Flask flask_app_thread = Thread(target=flask_thread, kwargs={"host": host, "port": port}) + # Set thread to be a daemon, so python will quit nicely. + flask_app_thread.daemon = True flask_app_thread.start() logging.info("Started Flask server on http://%s:%d" % (host, port)) @@ -601,7 +667,7 @@ def emit(self, record): # Convert log record into a dictionary log_data = { "level": record.levelname, - "timestamp": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), + "timestamp": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "msg": record.msg, } # Emit to all socket.io clients diff --git a/auto_rx/build.sh b/auto_rx/build.sh index 0c84746a..817030f7 100755 --- a/auto_rx/build.sh +++ b/auto_rx/build.sh @@ -2,6 +2,8 @@ # # Auto Sonde Decoder build script. +set -e + # Get the auto-rx version. AUTO_RX_VERSION="\"$(python3 -m autorx.version 2>/dev/null || python -m autorx.version)\"" @@ -38,5 +40,6 @@ mv ../demod/mod/imet54mod . mv ../demod/mod/mp3h1mod . mv ../demod/mod/mts01mod . mv ../demod/mod/iq_dec . +mv ../weathex/weathex301d . echo "Done!" diff --git a/auto_rx/requirements.txt b/auto_rx/requirements.txt index 48fde231..9b35b269 100644 --- a/auto_rx/requirements.txt +++ b/auto_rx/requirements.txt @@ -1,8 +1,7 @@ -crcmod python-dateutil flask==2.2.2 flask-socketio==5.2.0 numpy requests semver -simplekml +simple-websocket diff --git a/auto_rx/station.cfg.example b/auto_rx/station.cfg.example index c5dbca5f..b5ba124c 100644 --- a/auto_rx/station.cfg.example +++ b/auto_rx/station.cfg.example @@ -21,8 +21,8 @@ # # EXPERIMENTAL / NOT IMPLEMENTED options: # SpyServer - Use an Airspy SpyServer -# KA9Q - Use a KA9Q SDR Server (Not yet implemented) -# WARNING: These are still under development and may not work. +# KA9Q - Use a KA9Q-Radio Server +# WARNING: These are still under development and may not work correctly. # sdr_type = RTLSDR @@ -43,9 +43,9 @@ sdr_quantity = 1 # # Network SDR Connection Details # -# If using either a KA9Q or SpyServer network server, the hostname and port -# of the server needs to be defined below. Usually this will be running on the -# same machine as auto_rx, so the defaults are usually fine. +# If using a spyserver, the hostname and port need to be defined below. +# Is using KA9Q-Radio, the hostname of the 'radio' server (e.g. sonde.local) needs to be +# defined, and the port number is unused. # sdr_hostname = localhost sdr_port = 5555 @@ -200,6 +200,9 @@ sondehub_enabled = True # How often to push data to the SondeHub Database. (seconds) # All received positions are cached and uploaded every X seconds. # Uploads are gzip compressed, so don't require much data transfer. +# Users receiving Graw DFM sondes may want to set this to 30 to improve +# the chances of uploads not being rejected by our Z-check. +# (Refer: https://github.com/projecthorus/sondehub-infra/wiki/DFM-radiosonde-above-1000-and-not-enough-data-to-perform-z-check ) sondehub_upload_rate = 15 # An optional contact e-mail address. @@ -362,6 +365,9 @@ launch_notifications = True # Send e-mails when a radiosonde is detected descending near your station location landing_notifications = True +# Send e-mails when an encrypted radiosonde is detected. +encrypted_sonde_notifications = True + # Range threshold for Landing notifications (km from your station location) landing_range_threshold = 30 @@ -452,12 +458,16 @@ save_system_log = False # auto_rx operational issues. enable_debug_logging = False +# Enable logging of RS41 Calibration data ('subframe' data) +# This is saved as a binary file with file suffix _subframe.bin +save_cal_data = False ########################### # WEB INTERFACE SETTINNGS # ########################### [web] # Server Host - Can be set to :: to listen on IPv6 +# Leave this at 0.0.0.0 to have the web server listen on all interfaces. web_host = 0.0.0.0 # Server Port - Ports below 1024 can only be used if you run auto_rx as root (not recommended) web_port = 5000 @@ -584,8 +594,6 @@ lms6-1680_experimental = False # If someone can confirm that this works, I'll set it to True by default! mrz_experimental = False - - # Note: As iMet-4 sondes use AFSK, using fsk_demod does not give any advantage, so there is no experimental decoder for them. # Optimize 1680 MHz Scanning for RS92-NGP Sondes @@ -594,6 +602,14 @@ mrz_experimental = False # Set this to True if you are sure that only RS92-NGPs are flying in your area. ngp_tweak = False +# Wideband Radiosonde Detection: +# Enables some tweaks to better handle detection and decoding of the following radiosonde types: +# - Intermet iMet-1/4 (Wideband versions) +# - Weathex WxR-301d +# If this is enabled in areas with 'narrowband' sondes as well (RS41, DFM, narrowband iMet, etc...) there will likely +# be degradation in detection and decode performance. +wideband_sondes = False + ###################### # POSITION FILTERING # diff --git a/auto_rx/station.cfg.example.network b/auto_rx/station.cfg.example.network index a0062617..77a4f596 100644 --- a/auto_rx/station.cfg.example.network +++ b/auto_rx/station.cfg.example.network @@ -22,8 +22,8 @@ # # EXPERIMENTAL / NOT IMPLEMENTED options: # SpyServer - Use an Airspy SpyServer -# KA9Q - Use a KA9Q SDR Server (Not yet implemented) -# WARNING: These are still under development and may not work. +# KA9Q - Use a KA9Q-Radio Server +# WARNING: These are still under development and may not work correctly. # sdr_type = SpyServer @@ -44,9 +44,9 @@ sdr_quantity = 5 # # Network SDR Connection Details # -# If using either a KA9Q or SpyServer network server, the hostname and port -# of the server needs to be defined below. Usually this will be running on the -# same machine as auto_rx, so the defaults are usually fine. +# If using a spyserver, the hostname and port need to be defined below. +# Is using KA9Q-Radio, the hostname of the 'radio' server (e.g. sonde.local) needs to be +# defined, and the port number is unused. # sdr_hostname = localhost sdr_port = 5555 @@ -201,6 +201,9 @@ sondehub_enabled = True # How often to push data to the SondeHub Database. (seconds) # All received positions are cached and uploaded every X seconds. # Uploads are gzip compressed, so don't require much data transfer. +# Users receiving Graw DFM sondes may want to set this to 30 to improve +# the chances of uploads not being rejected by our Z-check. +# (Refer: https://github.com/projecthorus/sondehub-infra/wiki/DFM-radiosonde-above-1000-and-not-enough-data-to-perform-z-check ) sondehub_upload_rate = 15 # An optional contact e-mail address. @@ -363,6 +366,9 @@ launch_notifications = True # Send e-mails when a radiosonde is detected descending near your station location landing_notifications = True +# Send e-mails when an encrypted radiosonde is detected. +encrypted_sonde_notifications = True + # Range threshold for Landing notifications (km from your station location) landing_range_threshold = 30 @@ -452,11 +458,16 @@ save_system_log = False # auto_rx operational issues. enable_debug_logging = False +# Enable logging of RS41 Calibration data ('subframe' data) +# This is saved as a binary file with file suffix _subframe.bin +save_cal_data = False + ########################### # WEB INTERFACE SETTINNGS # ########################### [web] # Server Host - Can be set to :: to listen on IPv6 +# Leave this at 0.0.0.0 to have the web server listen on all interfaces. web_host = 0.0.0.0 # Server Port - Ports below 1024 can only be used if you run auto_rx as root (not recommended) web_port = 5000 @@ -591,6 +602,13 @@ mrz_experimental = False # Set this to True if you are sure that only RS92-NGPs are flying in your area. ngp_tweak = False +# Wideband Radiosonde Detection: +# Enables some tweaks to better handle detection and decoding of the following radiosonde types: +# - Intermet iMet-1/4 (Wideband versions) +# - Weathex WxR-301d +# If this is enabled in areas with 'narrowband' sondes as well (RS41, DFM, narrowband iMet, etc...) there will likely +# be degradation in detection and decode performance. +wideband_sondes = False ###################### # POSITION FILTERING # diff --git a/auto_rx/test/plot_fsk_demod_stats.py b/auto_rx/test/plot_fsk_demod_stats.py index 17b5261f..33022966 100644 --- a/auto_rx/test/plot_fsk_demod_stats.py +++ b/auto_rx/test/plot_fsk_demod_stats.py @@ -48,10 +48,7 @@ _fest2.append(_data['f2_est']) _ppm.append(_data['ppm']) - if _time == []: - _time = [0] - else: - _time.append(_time[-1]+1.0/_sps) + _time.append(_data['samples']) _ebno_max = pd.Series(_ebno).rolling(10).max().dropna().tolist() diff --git a/auto_rx/test/test_demod.py b/auto_rx/test/test_demod.py index e51d16f5..b09998b3 100644 --- a/auto_rx/test/test_demod.py +++ b/auto_rx/test/test_demod.py @@ -612,38 +612,6 @@ } -# LMS6 - 400 MHz version -_fm_rate = 22000 -# Calculate the necessary conversions -_rtlfm_oversampling = 8.0 # Viproz's hacked rtl_fm oversamples by 8x. -_shift = -2.0*_fm_rate/_sample_fs # rtl_fm tunes 'up' by rate*2, so we need to shift the signal down by this amount. - -_resample = (_fm_rate*_rtlfm_oversampling)/_sample_fs - -if _resample != 1.0: - # We will need to resample. - _resample_command = "csdr convert_f_s16 | ./tsrc - - %.4f | csdr convert_s16_f |" % _resample - _shift = (-2.0*_fm_rate)/(_sample_fs*_resample) -else: - _resample_command = "" - -_demod_command = "| %s csdr shift_addition_cc %.5f 2>/dev/null | csdr convert_f_u8 |" % (_resample_command, _shift) -_demod_command += " ./rtl_fm_stdin -M fm -f 401000000 -F9 -s %d 2>/dev/null|" % (int(_fm_rate)) -_demod_command += " sox -t raw -r %d -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 2>/dev/null |" % int(_fm_rate) - - -processing_type['lms6-400_rtlfm'] = { - 'demod': _demod_command, - # Decode using rs92ecc - 'decode': "../lms6mod 2>/dev/null", - #'decode': "../rs92ecc -vx -v --crc --ecc -r --vel 2>/dev/null", # For measuring No-ECC performance - # Count the number of telemetry lines. - "post_process" : " | wc -l", - #"post_process" : " | grep \"errors: 0\" | wc -l", - 'files' : "./generated/lms6-400*.bin" -} - - # # LMS6 - 1680 _fm_rate = 200000 _sample_fs = 480000 diff --git a/auto_rx/utils/listener_nmea_crlf.py b/auto_rx/utils/listener_nmea_crlf.py index 7284f6f1..29233877 100644 --- a/auto_rx/utils/listener_nmea_crlf.py +++ b/auto_rx/utils/listener_nmea_crlf.py @@ -12,7 +12,7 @@ import socket, json, sys, traceback from threading import Thread from dateutil.parser import parse -from datetime import datetime, timedelta +import datetime from io import StringIO import time @@ -26,7 +26,7 @@ def fix_datetime(datetime_str, local_dt_str = None): ''' if local_dt_str is None: - _now = datetime.utcnow() + _now = datetime.datetime.now(datetime.timezone.utc) else: _now = parse(local_dt_str) @@ -54,18 +54,18 @@ def fix_datetime(datetime_str, local_dt_str = None): # We are within the window, and need to adjust the day backwards or forwards based on the sonde time. if _telem_dt.hour == 23 and _now.hour == 0: # Assume system clock running slightly fast, and subtract a day from the telemetry date. - _telem_dt = _telem_dt - timedelta(days=1) + _telem_dt = _telem_dt - datetime.timedelta(days=1) elif _telem_dt.hour == 00 and _now.hour == 23: # System clock running slow. Add a day. - _telem_dt = _telem_dt + timedelta(days=1) + _telem_dt = _telem_dt + datetime.timedelta(days=1) return _telem_dt def udp_listener_nmea_callback(info): ''' Handle a Payload Summary Message from UDPListener ''' - dateRS = datetime.strptime(info['time'], '%H:%M:%S') + dateRS = datetime.datetime.strptime(info['time'], '%H:%M:%S') hms = dateRS.hour*10000.0+dateRS.minute*100.0+dateRS.second+dateRS.microsecond; dateNMEA = dateRS.year%100+dateRS.month*100+dateRS.day*10000 diff --git a/auto_rx/utils/log_to_kml.py b/auto_rx/utils/log_to_kml.py index d5003796..d7537c7f 100644 --- a/auto_rx/utils/log_to_kml.py +++ b/auto_rx/utils/log_to_kml.py @@ -5,275 +5,37 @@ # # 2018-02 Mark Jessop # -# Note: This utility requires the fastkml and shapely libraries, which can be installed using: -# sudo pip install fastkml shapely -# -import sys -import time -import datetime -import traceback import argparse import glob -import os -import fastkml -from dateutil.parser import parse -from shapely.geometry import Point, LineString - -def read_telemetry_csv(filename, - datetime_field = 0, - latitude_field = 3, - longitude_field = 4, - altitude_field = 5, - delimiter=','): - ''' - Read in a radiosonde_auto_rx generated telemetry CSV file. - Fields to use can be set as arguments to this function. - These have output like the following: - 2017-12-27T23:21:59.560,M2913374,982,-34.95143,138.52471,719.9,-273.0,RS92,401.520 - ,,,,,,,, - - Note that the datetime field must be parsable by dateutil.parsers.parse. - - If any fields are missing, or invalid, this function will return None. - - The output data structure is in the form: - [ - [datetime (as a datetime object), latitude, longitude, altitude, raw_line], - [datetime (as a datetime object), latitude, longitude, altitude, raw_line], - ... - ] - ''' - - output = [] - - f = open(filename,'r') - - for line in f: - try: - # Split line by comma delimiters. - _fields = line.split(delimiter) - - if _fields[0] == 'timestamp': - # First line in file - header line. - continue - - # Attempt to parse fields. - _datetime = parse(_fields[datetime_field]) - _latitude = float(_fields[latitude_field]) - _longitude = float(_fields[longitude_field]) - _altitude = float(_fields[altitude_field]) - - output.append([_datetime, _latitude, _longitude, _altitude, line]) - except: - traceback.print_exc() - return None - - f.close() - - return output - - -def flight_burst_position(flight_path): - ''' Search through flight data for the burst position and return it. ''' - - # Read through array and hunt for max altitude point. - current_alt = 0.0 - current_index = 0 - for i in range(len(flight_path)): - if flight_path[i][3] > current_alt: - current_alt = flight_path[i][3] - current_index = i - - return flight_path[current_index] - - -ns = '{http://www.opengis.net/kml/2.2}' - -def new_placemark(lat, lon, alt, - placemark_id="Placemark ID", - name="Placemark Name", - absolute = False, - icon = "http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png", - scale = 1.0): - """ Generate a generic placemark object """ - - if absolute: - _alt_mode = 'absolute' - else: - _alt_mode = 'clampToGround' - - flight_icon_style = fastkml.styles.IconStyle( - ns=ns, - icon_href=icon, - scale=scale) - - flight_style = fastkml.styles.Style( - ns=ns, - styles=[flight_icon_style]) - - flight_placemark = fastkml.kml.Placemark( - ns=ns, - id=placemark_id, - name=name, - description="", - styles=[flight_style]) - - flight_placemark.geometry = fastkml.geometry.Geometry( - ns=ns, - geometry=Point(lon, lat, alt), - altitude_mode=_alt_mode) - - return flight_placemark - - - -def flight_path_to_geometry(flight_path, - placemark_id="Flight Path ID", - name="Flight Path Name", - track_color="aaffffff", - poly_color="20000000", - track_width=2.0, - absolute = True, - extrude = True, - tessellate = True): - ''' Produce a fastkml geometry object from a flight path array ''' - - # Handle selection of absolute altitude mode - if absolute: - _alt_mode = 'absolute' - else: - _alt_mode = 'clampToGround' - - # Convert the flight path array [time, lat, lon, alt, comment] into a LineString object. - track_points = [] - for _point in flight_path: - # Flight path array is in lat,lon,alt order, needs to be in lon,lat,alt - track_points.append([_point[2],_point[1],_point[3]]) - - _flight_geom = LineString(track_points) - - # Define the Line and Polygon styles, which are used for the flight path, and the extrusions (if enabled) - flight_track_line_style = fastkml.styles.LineStyle( - ns=ns, - color=track_color, - width=track_width) - - flight_extrusion_style = fastkml.styles.PolyStyle( - ns=ns, - color=poly_color) - - flight_track_style = fastkml.styles.Style( - ns=ns, - styles=[flight_track_line_style, flight_extrusion_style]) - - # Generate the Placemark which will contain the track data. - flight_line = fastkml.kml.Placemark( - ns=ns, - id=placemark_id, - name=name, - styles=[flight_track_style]) - - # Add the track data to the Placemark - flight_line.geometry = fastkml.geometry.Geometry( - ns=ns, - geometry=_flight_geom, - altitude_mode=_alt_mode, - extrude=extrude, - tessellate=tessellate) - - return flight_line - - -def write_kml(geom_objects, - filename="output.kml", - comment=""): - """ Write out flight path geometry objects to a kml file. """ - - kml_root = fastkml.kml.KML() - kml_doc = fastkml.kml.Document( - ns=ns, - name=comment) - - if type(geom_objects) is not list: - geom_objects = [geom_objects] - - for _flight in geom_objects: - kml_doc.append(_flight) - - with open(filename,'w') as kml_file: - kml_file.write(kml_doc.to_string()) - kml_file.close() - - -def convert_single_file(filename, absolute=True, tessellate=True, last_only=False): - ''' Convert a single sonde log file to a fastkml KML Folder object ''' - - # Read file. - _flight_data = read_telemetry_csv(filename) - - # Extract the flight's serial number and launch time from the first line in the file. - _first_line = _flight_data[0][4] - _flight_serial = _first_line.split(',')[1] # Serial number is the second field in the line. - _launch_time = _flight_data[0][0].strftime("%Y%m%d-%H%M%SZ") - # Generate a comment line to use in the folder and placemark descriptions - _track_comment = "%s %s" % (_launch_time, _flight_serial) - _landing_comment = "%s Last Position" % (_flight_serial) - - # Grab burst and last-seen positions - _burst_pos = flight_burst_position(_flight_data) - _landing_pos = _flight_data[-1] - - # Generate the placemark & flight track. - _flight_geom = flight_path_to_geometry(_flight_data, name=_track_comment, absolute=absolute, tessellate=tessellate, extrude=tessellate) - _landing_geom = new_placemark(_landing_pos[1], _landing_pos[2], _landing_pos[3], name=_landing_comment, absolute=absolute) - - _folder = fastkml.kml.Folder(ns, _flight_serial, _track_comment, 'Radiosonde Flight Path') - if last_only == False: - _folder.append(_flight_geom) - _folder.append(_landing_geom) +import sys +from os.path import dirname, abspath - return _folder +parent_dir = dirname(dirname(abspath(__file__))) +sys.path.append(parent_dir) +from autorx.log_files import log_files_to_kml if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("-i", "--input", type=str, default="../log/*_sonde.log", - help="Path to log file. May include wildcards, though the path must be wrapped in quotes. Default=../log/*_sonde.log") - parser.add_argument("-o", "--output", type=str, default="sondes.kml", help="KML output file name. Default=sondes.kml") - parser.add_argument('--clamp', action="store_false", default=True, help="Clamp tracks to ground instead of showing absolute altitudes.") - parser.add_argument('--noextrude', action="store_false", default=True, help="Disable Extrusions for absolute flight paths.") - parser.add_argument('--lastonly', action="store_true", default=False, help="Only plot last-seen sonde positions, not the flight paths.") + parser.add_argument("-i", "--input", type=str, default="../log/*_sonde.log", + help="Path to log file. May include wildcards, though the path " + "must be wrapped in quotes. Default=../log/*_sonde.log") + parser.add_argument("-o", "--output", type=str, default="sondes.kml", + help="KML output file name. Default=sondes.kml") + parser.add_argument('--clamp', action="store_false", default=True, + help="Clamp tracks to ground instead of showing absolute altitudes.") + parser.add_argument('--noextrude', action="store_false", default=True, + help="Disable Extrusions for absolute flight paths.") + parser.add_argument('--lastonly', action="store_true", default=False, + help="Only plot last-seen sonde positions, not the flight paths.") args = parser.parse_args() _file_list = glob.glob(args.input) + _file_list.sort(reverse=True) - _placemarks = [] - - for _file in _file_list: - print("Processing: %s" % _file) - try: - _placemarks.append(convert_single_file(_file, absolute=args.clamp, tessellate=args.noextrude, last_only=args.lastonly)) - except: - print("Failed to process: %s" % _file) - - write_kml(_placemarks, filename=args.output) + with open(args.output, "wb") as kml_file: + log_files_to_kml(_file_list, kml_file, absolute=args.clamp, + extrude=args.noextrude, last_only=args.lastonly) print("Output saved to: %s" % args.output) - - - - - - - - - - - - - - - - - diff --git a/auto_rx/utils/plot_rtl_power.py b/auto_rx/utils/plot_rtl_power.py index b3832be1..2de70c62 100644 --- a/auto_rx/utils/plot_rtl_power.py +++ b/auto_rx/utils/plot_rtl_power.py @@ -7,7 +7,7 @@ # import matplotlib.pyplot as plt import numpy as np -from StringIO import StringIO +from io import StringIO import sys # Need to keep this in sync with auto_rx.py as we're not set up to do relative imports yet. @@ -59,13 +59,15 @@ def read_rtl_power(filename): if __name__ == '__main__': - filename = sys.argv[1] + filename = sys.argv[1] - (freq, power, freq_step) = read_rtl_power(filename) + (freq, power, freq_step) = read_rtl_power(filename) - plt.plot(freq/1e6, power) - plt.xlabel("Frequency (MHz)") - plt.ylabel("Power (dB?)") - plt.title("rtl_power output: %s" % filename) - plt.show() + print(f"Median value: {np.median(power):.2f} dB") + + plt.plot(freq/1e6, power) + plt.xlabel("Frequency (MHz)") + plt.ylabel("Power (dB?)") + plt.title("rtl_power output: %s" % filename) + plt.show() diff --git a/auto_rx/utils/plot_sonde_log.py b/auto_rx/utils/plot_sonde_log.py index c15bde2f..d829f7da 100644 --- a/auto_rx/utils/plot_sonde_log.py +++ b/auto_rx/utils/plot_sonde_log.py @@ -417,7 +417,7 @@ def process_directory(log_dir, output_dir, status_file, time_limit = 60): # Calculate the age of the last data point in minutes. - _data_age = (pytz.utc.localize(datetime.datetime.utcnow()) - parse(last_time)).total_seconds() / 60.0 + _data_age = (datetime.datetime.now(datetime.timezone.utc) - parse(last_time)).total_seconds() / 60.0 if burst or (_data_age > time_limit): # We consider this file to be finished. _log_status[_basename]['complete'] = True diff --git a/auto_rx/utils/rs41cal.py b/auto_rx/utils/rs41cal.py new file mode 100644 index 00000000..9ae65c32 --- /dev/null +++ b/auto_rx/utils/rs41cal.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python +# +# RS41 SubFrame Handling Utilities +# +import base64 +import json +import logging +import struct +import requests + +class RS41Subframe(object): + def __init__(self, filename=None, raw_bytes=None): + """ + RS41 Subframe Storage and data extraction + """ + + # Output data + self.data = {} + + self.raw_data = b'' + + if filename: + # Read in binary file + _f = open(filename, 'rb') + self.raw_data = _f.read() + _f.close() + + if raw_bytes: + self.raw_data = raw_bytes + + if (filename is None) and (raw_bytes is None): + raise IOError("Either a filename, or raw_bytes must be provided!") + + if (len(self.raw_data) != 800) and (len(self.raw_data) != 816): + raise IOError("Subframe data must be either 800 or 816 bytes in length!") + + # TODO - Check CRC16 + + # Extract main cal fields + self.parse_subframe() + + # Extract runtime-variable area + if len(self.raw_data) == 816: + self.parse_runtime_variable() + + pass + + + def check_subframe_crc(self): + pass + + def parse_subframe(self): + """ + Extract all available fields from the binary subframe data + + Reference: https://github.com/einergehtnochrein/ra-firmware/blob/master/src/rs41/rs41private.h#L233 + """ + + self.data['frequency'] = 400 + 0.01*self.extract_uint16(0x002)/64 + self.data['startupTxPower'] = self.extract_uint8(0x004) + self.data['optionFlags'] = self.extract_uint16(0x007) + self.data['serial'] = self.extract_string(0x00D, 8).rstrip(b'\x00').decode() + self.data['firmwareVersion'] = self.extract_uint32(0x015) + self.data['minHeight4Flight'] = self.extract_uint16(0x019) + self.data['lowBatVoltageThreshold'] = self.extract_uint8(0x01B)*0.1 # Volts - Guess! + self.data['nfcDetectorThreshold'] = self.extract_uint8(0x01C)*0.025 # Volts + self.data['refTemperatureTarget'] = self.extract_int8(0x021) + self.data['lowBatCapacityThreshold'] = self.extract_uint8(0x022) + self.data['flightKillFrames'] = self.extract_int16(0x027) + self.data['burstKill'] = self.extract_uint8(0x02B) # Convert to enum + self.data['freshBatteryCapacity'] = self.extract_uint16(0x02E) + self.data['allowXdata'] = self.extract_uint8(0x032) + self.data['ubloxHwVersionHigh'] = self.extract_uint16(0x033) + self.data['ubloxHwVersionLow'] = self.extract_uint16(0x035) + self.data['ubloxSwVersion'] = self.extract_uint16(0x037) + self.data['ubloxSwBuild'] = self.extract_uint16(0x039) + self.data['ubloxConfigErrors'] = self.extract_uint8(0x03B) + self.data['radioVersionCode'] = self.extract_uint8(0x03C) + # Main PTU Calibration Fields + self.data['refResistorLow'] = self.extract_float(0x03D) + self.data['refResistorHigh'] = self.extract_float(0x041) + self.data['refCapLow'] = self.extract_float(0x045) + self.data['refCapHigh'] = self.extract_float(0x049) + self.data['taylorT'] = self.extract_float_array(0x04D,3) + self.data['calT'] = self.extract_float(0x059) + self.data['polyT'] = self.extract_float_array(0x05D, 6) + self.data['calibU'] = self.extract_float_array(0x075, 2) + self.data['matrixU'] = self.extract_float_array(0x07D, 7, 6) + self.data['taylorTU'] = self.extract_float_array(0x125, 3) + self.data['calTU'] = self.extract_float(0x131) + self.data['polyTrh'] = self.extract_float_array(0x135, 6) + # Other status/config fields + self.data['startIWDG'] = self.extract_uint8(0x1EC) + self.data['parameterSetupDone'] = self.extract_uint8(0x1ED) + self.data['enableTestMode'] = self.extract_uint8(0x1EE) + self.data['enableTx'] = self.extract_uint8(0x1EF) + self.data['pressureLaunchSite'] = self.extract_float_array(0x210, 2) # Unsure if this is right? + # Board version/serial numbers + self.data['variant'] = self.extract_string(0x218, 10).rstrip(b'\x00').decode() + self.data['mainboard_version'] = self.extract_string(0x222, 10).rstrip(b'\x00').decode() + self.data['mainboard_serial'] = self.extract_string(0x22C, 9).rstrip(b'\x00').decode() + self.data['pressureSensor_serial'] = self.extract_string(0x243, 8).rstrip(b'\x00').decode() + # More status/config fields + self.data['xdataUartBaud'] = self.extract_uint8(0x253) # Convert to enum + self.data['cpuTempSensorVoltageAt25deg'] = self.extract_float(0x255) + # Pressure sensor calibration data + self.data['matrixP'] = self.extract_float_array(0x25E, 18) + self.data['vectorBp'] = self.extract_float_array(0x2A6, 3) + self.data['matrixBt'] = self.extract_float_array(0x2BA, 12) + # More settings + self.data['burstKillFrames'] = self.extract_int16(0x316) + + + + def parse_runtime_variable(self): + """ + Extract all available runtime-variable fields from the binary subframe data + + Reference: https://github.com/einergehtnochrein/ra-firmware/blob/master/src/rs41/rs41private.h#L233 + """ + + self.data['killCountdown'] = self.extract_int16(0x320) + self.data['launchAltitude'] = self.extract_int16(0x322) + self.data['heightOfFlightStart'] = self.extract_uint16(0x324) + self.data['lastTxPowerLevel'] = self.extract_uint8(0x326) + self.data['numSoftwareResets'] = self.extract_uint8(0x327) + self.data['intTemperatureCpu'] = self.extract_int8(0x328) + self.data['intTemperatureRadio'] = self.extract_int8(0x329) + self.data['remainingBatteryCapacity'] = self.extract_uint16(0x32A) + self.data['numUbxDiscarded'] = self.extract_uint8(0x32C) + self.data['numUbxStall'] = self.extract_uint8(0x32D) + + + def extract_uint16(self, address): + _r = struct.unpack(' - `rs41mod.c`, `rs92mod.c`, `dfm09mod.c`, `m10mod.c`, `lms6mod.c`, `lms6Xmod.c`, `meisei100mod.c`,
+ `rs41mod.c`, `rs92mod.c`, `dfm09mod.c`, `m10mod.c`, `lms6Xmod.c`, `meisei100mod.c`,
`bch_ecc_mod.c`, `bch_ecc_mod.h` #### Compile diff --git a/demod/mod/dfm09mod.c b/demod/mod/dfm09mod.c index ea5e52e9..e248b491 100644 --- a/demod/mod/dfm09mod.c +++ b/demod/mod/dfm09mod.c @@ -704,6 +704,7 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { ui32_t SN6, SN; ui8_t dfm6typ; ui8_t sn2_ch, sn_ch; + int dfm17_0xA = 0; conf_id = bits2val(conf_bits, 4); @@ -777,9 +778,9 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { gpx->SN = SN; gpx->ptu_out = 0; - if (sn_ch == 0xA /*&& (sn2_ch & 0xF) == 0xC*/) gpx->ptu_out = sn_ch; // <+> DFM-09 + if (sn_ch == 0xA /*&& (sn2_ch & 0xF) == 0xC*/) gpx->ptu_out = sn_ch; // <+> DFM-09 (T+) / <-> DFM-17 (T-) if (sn_ch == 0xB /*&& (sn2_ch & 0xF) == 0xC*/) gpx->ptu_out = sn_ch; // <-> DFM-17 - if (sn_ch == 0xC) gpx->ptu_out = sn_ch; // <+> DFM-09P(?) , <-> DFM-17TU(?) + if (sn_ch == 0xC) gpx->ptu_out = sn_ch; // <+> DFM-09P / <-> DFM-17TU if (sn_ch == 0xD) gpx->ptu_out = sn_ch; // <-> DFM-17P(?) // PS-15 ? (sn2_ch & 0xF) == 0x0 : gpx->ptu_out = 0 // <-> PS-15 @@ -800,6 +801,8 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { ret = (gpx->sonde_typ & 0xF); } + // 23038743, 2307.... + dfm17_0xA = (gpx->SN >= 23000000 && gpx->option.inv); // detected Manchester type/polarity could depend on receiver/sdr if (conf_id >= 0 && conf_id <= 8 && ec == 0) { gpx->cfgchk24[conf_id] = 1; @@ -827,6 +830,7 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { gpx->sensortyp = 'P'; // gpx->meas24[0] > 2e5 ? } if ( ((gpx->ptu_out == 0xB || gpx->ptu_out == 0xC) && gpx->sensortyp == 'T') || gpx->ptu_out >= 0xD) gpx->Rf = 332e3; // DFM-17 ? + if (gpx->ptu_out == 0xA && gpx->sensortyp == 'T' && dfm17_0xA) gpx->Rf = 332e3; // DFM-17 ? if (gpx->ptu_out == 6 && (gpx->sonde_typ & 0xF) == 8) { gpx->sensortyp = 'P'; @@ -846,7 +850,7 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { val = bits2val(conf_bits+8, 4*4); gpx->status[1] = val/100.0; } - if (conf_id == 0x7+ofs && gpx->Rf > 300e3) { // DFM17 counter + if (conf_id == 0x7+ofs && gpx->Rf > 300e3) { // DFM17 counter (also DFM09?) val = bits2val(conf_bits+8, 4*4); gpx->status[2] = val/1.0; // sec counter } @@ -862,6 +866,7 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { V/Ti Tf012 Rf 0xA DFM-09 5/6 0,3,4 'T+' 220k 0xC DFM-09P 7/8 1,5,6 'P+' 220k + 0xA DFM-17 5/6 0,3,4 'T-' 332k 0xB DFM-17 5/6 0,3,4 'T-' 332k 0xC DFM-17TU 5/6 0,3,4 'T-' 332k 0xD DFM-17P 7/8 1,5,6 'P-' 332k @@ -874,7 +879,8 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { case 0x8: if (gpx->SN6) gpx->dfmtyp = DFM_types[DFM06P]; //gpx->sensortyp == 'P' else gpx->dfmtyp = DFM_types[PS15]; break; - case 0xA: gpx->dfmtyp = DFM_types[DFM09]; + case 0xA: if (dfm17_0xA) gpx->dfmtyp = DFM_types[DFM17]; + else gpx->dfmtyp = DFM_types[DFM09]; break; case 0xB: gpx->dfmtyp = DFM_types[DFM17]; break; @@ -1364,10 +1370,7 @@ int main(int argc, char **argv) { } else if ( (strcmp(*argv, "--ecc" ) == 0) ) { option_ecc = 1; } else if ( (strcmp(*argv, "--ecc2") == 0) ) { option_ecc = 2; } - else if ( (strcmp(*argv, "--ptu") == 0) ) { - option_ptu = 1; - //gpx.ptu_out = 1; // force ptu (non PS-15) - } + else if ( (strcmp(*argv, "--ptu") == 0) ) { option_ptu = 1; } //gpx.ptu_out = 1; // force ptu (non PS-15) else if ( (strcmp(*argv, "--spike") == 0) ) { spike = 1; } diff --git a/demod/mod/lms6mod.c b/demod/mod/lms6mod.c deleted file mode 100644 index a0d11dfc..00000000 --- a/demod/mod/lms6mod.c +++ /dev/null @@ -1,1058 +0,0 @@ - -/* - * LMS6 - * (403 MHz) - * - * sync header: correlation/matched filter - * files: lms6mod.c demod_mod.c demod_mod.h bch_ecc_mod.c bch_ecc_mod.h - * compile, either (a) or (b): - * (a) - * gcc -c demod_mod.c - * gcc -DINCLUDESTATIC lms6mod.c demod_mod.o -lm -o lms6mod - * (b) - * gcc -c demod_mod.c - * gcc -c bch_ecc_mod.c - * gcc lms6mod.c demod_mod.o bch_ecc_mod.o -lm -o lms6mod - * - * usage: - * ./lms6mod --vit --ecc - * ( --vit recommended) - * author: zilog80 - */ - -#include -#include -#include -#include - -#ifdef CYGWIN - #include // cygwin: _setmode() - #include -#endif - - -//typedef unsigned char ui8_t; -//typedef unsigned short ui16_t; -//typedef unsigned int ui32_t; - -#include "demod_mod.h" - -//#define INCLUDESTATIC 1 -#ifdef INCLUDESTATIC - #include "bch_ecc_mod.c" -#else - #include "bch_ecc_mod.h" -#endif - - -typedef struct { - i8_t vbs; // verbose output - i8_t raw; // raw frames - i8_t crc; // CRC check output - i8_t ecc; // Reed-Solomon ECC - i8_t sat; // GPS sat data - i8_t ptu; // PTU: temperature - i8_t inv; - i8_t vit; - i8_t jsn; // JSON output (auto_rx) -} option_t; - - -/* -------------------------------------------------------------------------- */ - -#define BAUD_RATE 4800 - -#define BITS 8 -#define HEADOFS 0 //16 -#define HEADLEN ((4*16)-HEADOFS) - -#define SYNC_LEN 5 -#define FRM_LEN (223) -#define PAR_LEN (32) -#define FRMBUF_LEN (3*FRM_LEN) -#define BLOCKSTART (SYNC_LEN*BITS*2) -#define BLOCK_LEN (FRM_LEN+PAR_LEN+SYNC_LEN) // 255+5 = 260 -#define RAWBITBLOCK_LEN ((BLOCK_LEN+1)*BITS*2) // (+1 tail) - -#define FRAME_LEN (300) // 4800baud, 16bits/byte -#define BITFRAME_LEN (FRAME_LEN*BITS) -#define RAWBITFRAME_LEN (BITFRAME_LEN*2) -#define OVERLAP 64 -#define OFS 4 - - -static char rawheader[] = "0101011000001000""0001110010010111""0001101010100111""0011110100111110"; // (c0,inv(c1)) -// (00) 58 f3 3f b8 -// char header[] = "0000001101011101""0100100111000010""0100111111110010""0110100001101011"; // (c0,c1) -static ui8_t rs_sync[] = { 0x00, 0x58, 0xf3, 0x3f, 0xb8}; -// 0x58f33fb8 little-endian <-> 0x1ACFFC1D big-endian bytes - -// (00) 58 f3 3f b8 -static char blk_syncbits[] = "0000000000000000""0000001101011101""0100100111000010""0100111111110010""0110100001101011"; - -static ui8_t frm_sync[] = { 0x24, 0x54, 0x00, 0x00}; - - -#define L 7 // d_f=10 -static char polyA[] = "1001111"; // 0x4f: x^6+x^3+x^2+x+1 -static char polyB[] = "1101101"; // 0x6d: x^6+x^5+x^3+x^2+1 -/* -// d_f=6 -qA[] = "1110011"; // 0x73: x^6+x^5+x^4+x+1 -qB[] = "0011110"; // 0x1e: x^4+x^3+x^2+x -pA[] = "10010101"; // 0x95: x^7+x^4+x^2+1 = (x+1)(x^6+x^5+x^4+x+1) = (x+1)qA -pB[] = "00100010"; // 0x22: x^5+x = (x+1)(x^4+x^3+x^2+x)=x(x+1)^3 = (x+1)qB -polyA = qA + x*qB -polyB = qA + qB -*/ - -#define N (1 << L) -#define M (1 << (L-1)) - -typedef struct { - ui8_t bIn; - ui8_t codeIn; - ui8_t prevState; // 0..M=64 - int w; // > 255 : if (w>250): w=250 ? - //float sw; -} states_t; - -typedef struct { - char rawbits[RAWBITFRAME_LEN+OVERLAP*BITS*2 +8]; - states_t state[RAWBITFRAME_LEN+OVERLAP +8][M]; - states_t d[N]; -} VIT_t; - -typedef struct { - int frnr; - int sn; - int week; int gpstow; int gpssec; - int jahr; int monat; int tag; - int wday; - int std; int min; float sek; - double lat; double lon; double alt; - double vH; double vD; double vV; - double vE; double vN; double vU; - char blk_rawbits[RAWBITBLOCK_LEN+SYNC_LEN*BITS*2 +8]; - ui8_t frame[FRM_LEN]; // = { 0x24, 0x54, 0x00, 0x00}; // dataheader - int frm_pos; // ecc_blk <-> frm_blk - int sf; - option_t option; - RS_t RS; - VIT_t *vit; -} gpx_t; - - -/* ------------------------------------------------------------------------------------ */ -static int gpstow_start = -1; -static double time_elapsed_sec = 0.0; - -/* - * Convert GPS Week and Seconds to Modified Julian Day. - * - Adapted from sci.astro FAQ. - * - Ignores UTC leap seconds. - */ -// in : week, gpssec -// out: jahr, monat, tag -static void Gps2Date(gpx_t *gpx) { - long GpsDays, Mjd; - long _J, _C, _Y, _M; - - GpsDays = gpx->week * 7 + (gpx->gpssec / 86400); - Mjd = 44244 + GpsDays; - - _J = Mjd + 2468570; - _C = 4 * _J / 146097; - _J = _J - (146097 * _C + 3) / 4; - _Y = 4000 * (_J + 1) / 1461001; - _J = _J - 1461 * _Y / 4 + 31; - _M = 80 * _J / 2447; - gpx->tag = _J - 2447 * _M / 80; - _J = _M / 11; - gpx->monat = _M + 2 - (12 * _J); - gpx->jahr = 100 * (_C - 49) + _Y + _J; -} -/* ------------------------------------------------------------------------------------ */ - -// ------------------------------------------------------------------------ - -static ui8_t vit_code[N]; -static vitCodes_init = 0; - -static int vit_initCodes(gpx_t *gpx) { - int cA, cB; - int i, bits; - - VIT_t *pv = calloc(1, sizeof(VIT_t)); - if (pv == NULL) return -1; - gpx->vit = pv; - - if ( vitCodes_init == 0 ) { - for (bits = 0; bits < N; bits++) { - cA = 0; - cB = 0; - for (i = 0; i < L; i++) { - cA ^= (polyA[L-1-i]&1) & ((bits >> i)&1); - cB ^= (polyB[L-1-i]&1) & ((bits >> i)&1); - } - vit_code[bits] = (cA<<1) | cB; - } - vitCodes_init = 1; - } - - return 0; -} - -static int vit_dist(int c, char *rc) { - return (((c>>1)^rc[0])&1) + ((c^rc[1])&1); -} - -static int vit_start(VIT_t *vit, char *rc) { - int t, m, j, c, d; - - t = L-1; - m = M; - while ( t > 0 ) { // t=0..L-2: nextStatestate[t][j].prevState = j/2; - } - t--; - m /= 2; - } - - m = 2; - for (t = 1; t < L; t++) { - for (j = 0; j < m; j++) { - c = vit_code[j]; - vit->state[t][j].bIn = j % 2; - vit->state[t][j].codeIn = c; - d = vit_dist( c, rc+2*(t-1) ); - vit->state[t][j].w = vit->state[t-1][vit->state[t][j].prevState].w + d; - } - m *= 2; - } - - return t; -} - -static int vit_next(VIT_t *vit, int t, char *rc) { - int b, nstate; - int j, index; - - for (j = 0; j < M; j++) { - for (b = 0; b < 2; b++) { - nstate = j*2 + b; - vit->d[nstate].bIn = b; - vit->d[nstate].codeIn = vit_code[nstate]; - vit->d[nstate].prevState = j; - vit->d[nstate].w = vit->state[t][j].w + vit_dist( vit->d[nstate].codeIn, rc ); - } - } - - for (j = 0; j < M; j++) { - - if ( vit->d[j].w <= vit->d[j+M].w ) index = j; else index = j+M; - - vit->state[t+1][j] = vit->d[index]; - } - - return 0; -} - -static int vit_path(VIT_t *vit, int j, int t) { - int c; - - vit->rawbits[2*t] = '\0'; - while (t > 0) { - c = vit->state[t][j].codeIn; - vit->rawbits[2*t -2] = 0x30 + ((c>>1) & 1); - vit->rawbits[2*t -1] = 0x30 + (c & 1); - j = vit->state[t][j].prevState; - t--; - } - - return 0; -} - -static int viterbi(VIT_t *vit, char *rc) { - int t, tmax; - int j, j_min, w_min; - - vit_start(vit, rc); - - tmax = strlen(rc)/2; - - for (t = L-1; t < tmax; t++) - { - vit_next(vit, t, rc+2*t); - } - - w_min = -1; - for (j = 0; j < M; j++) { - if (w_min < 0) { - w_min = vit->state[tmax][j].w; - j_min = j; - } - if (vit->state[tmax][j].w < w_min) { - w_min = vit->state[tmax][j].w; - j_min = j; - } - } - vit_path(vit, j_min, tmax); - - return 0; -} - -// ------------------------------------------------------------------------ - -static int deconv(char* rawbits, char *bits) { - - int j, n, bitA, bitB; - char *p; - int len; - int errors = 0; - int m = L-1; - - len = strlen(rawbits); - for (j = 0; j < m; j++) bits[j] = '0'; - n = 0; - while ( 2*(m+n) < len ) { - p = rawbits+2*(m+n); - bitA = bitB = 0; - for (j = 0; j < m; j++) { - bitA ^= (bits[n+j]&1) & (polyA[j]&1); - bitB ^= (bits[n+j]&1) & (polyB[j]&1); - } - if ( (bitA^(p[0]&1))==(polyA[m]&1) && (bitB^(p[1]&1))==(polyB[m]&1) ) bits[n+m] = '1'; - else if ( (bitA^(p[0]&1))==0 && (bitB^(p[1]&1))==0 ) bits[n+m] = '0'; - else { - if ( (bitA^(p[0]&1))!=(polyA[m]&1) && (bitB^(p[1]&1))==(polyB[m]&1) ) bits[n+m] = 0x39; - else bits[n+m] = 0x38; - errors = n; - break; - } - n += 1; - } - bits[n+m] = '\0'; - - return errors; -} - -// ------------------------------------------------------------------------ - -static int crc16_0(ui8_t frame[], int len) { - int crc16poly = 0x1021; - int rem = 0x0, i, j; - int byte; - - for (i = 0; i < len; i++) { - byte = frame[i]; - rem = rem ^ (byte << 8); - for (j = 0; j < 8; j++) { - if (rem & 0x8000) { - rem = (rem << 1) ^ crc16poly; - } - else { - rem = (rem << 1); - } - rem &= 0xFFFF; - } - } - return rem; -} - -static int check_CRC(ui8_t frame[]) { - ui32_t crclen = 0, - crcdat = 0; - - crclen = 221; - crcdat = (frame[crclen]<<8) | frame[crclen+1]; - if ( crcdat != crc16_0(frame, crclen) ) { - return 1; // CRC NO - } - else return 0; // CRC OK -} - -// ------------------------------------------------------------------------ - -static int bits2bytes(char *bitstr, ui8_t *bytes) { - int i, bit, d, byteval; - int len = strlen(bitstr)/8; - int bitpos, bytepos; - - bitpos = 0; - bytepos = 0; - - while (bytepos < len) { - - byteval = 0; - d = 1; - for (i = 0; i < BITS; i++) { - bit=*(bitstr+bitpos+i); /* little endian */ - //bit=*(bitstr+bitpos+7-i); /* big endian */ - if ((bit == '1') || (bit == '9')) byteval += d; - else /*if ((bit == '0') || (bit == '8'))*/ byteval += 0; - d <<= 1; - } - bitpos += BITS; - bytes[bytepos++] = byteval & 0xFF; - } - - //while (bytepos < FRAME_LEN+OVERLAP) bytes[bytepos++] = 0; - - return bytepos; -} - -/* -------------------------------------------------------------------------- */ - - -#define pos_SondeSN (OFS+0x00) // ?4 byte 00 7A.... -#define pos_FrameNb (OFS+0x04) // 2 byte -//GPS Position -#define pos_GPSTOW (OFS+0x06) // 4 byte -#define pos_GPSlat (OFS+0x0E) // 4 byte -#define pos_GPSlon (OFS+0x12) // 4 byte -#define pos_GPSalt (OFS+0x16) // 4 byte -//GPS Velocity East-North-Up (ENU) -#define pos_GPSvO (OFS+0x1A) // 3 byte -#define pos_GPSvN (OFS+0x1D) // 3 byte -#define pos_GPSvV (OFS+0x20) // 3 byte - - -static int get_SondeSN(gpx_t *gpx) { - unsigned byte; - - byte = (gpx->frame[pos_SondeSN]<<24) | (gpx->frame[pos_SondeSN+1]<<16) - | (gpx->frame[pos_SondeSN+2]<<8) | gpx->frame[pos_SondeSN+3]; - gpx->sn = byte & 0xFFFFFF; - - return 0; -} - -static int get_FrameNb(gpx_t *gpx) { - int i; - unsigned byte; - ui8_t frnr_bytes[2]; - int frnr; - - for (i = 0; i < 2; i++) { - byte = gpx->frame[pos_FrameNb + i]; - frnr_bytes[i] = byte; - } - - frnr = (frnr_bytes[0] << 8) + frnr_bytes[1] ; - gpx->frnr = frnr; - - return 0; -} - - -//char weekday[7][3] = { "So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"}; -static char weekday[7][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; - -static int get_GPStime(gpx_t *gpx, int crc_err) { - int i; - unsigned byte; - ui8_t gpstime_bytes[4]; - int gpstime = 0, // 32bit - day; - float ms; - - for (i = 0; i < 4; i++) { - byte = gpx->frame[pos_GPSTOW + i]; - gpstime_bytes[i] = byte; - } - gpstime = 0; - for (i = 0; i < 4; i++) { - gpstime |= gpstime_bytes[i] << (8*(3-i)); - } - - if (gpstow_start < 0 && !crc_err) { - gpstow_start = gpstime; // time elapsed since start-up? - if (gpx->week > 0 && gpstime/1000.0 < time_elapsed_sec) gpx->week += 1; - } - gpx->gpstow = gpstime; - - ms = gpstime % 1000; - gpstime /= 1000; - gpx->gpssec = gpstime; - - day = gpstime / (24 * 3600); - gpstime %= (24*3600); - - if ((day < 0) || (day > 6)) return -1; - - gpx->wday = day; - gpx->std = gpstime / 3600; - gpx->min = (gpstime % 3600) / 60; - gpx->sek = gpstime % 60 + ms/1000.0; - - return 0; -} - -static double B60B60 = (1<<30)/90.0; // 2^32/360 = 2^30/90 = 0xB60B60.711x - -static int get_GPSlat(gpx_t *gpx) { - int i; - unsigned byte; - ui8_t gpslat_bytes[4]; - int gpslat; - double lat; - - for (i = 0; i < 4; i++) { - byte = gpx->frame[pos_GPSlat + i]; - gpslat_bytes[i] = byte; - } - - gpslat = 0; - for (i = 0; i < 4; i++) { - gpslat |= gpslat_bytes[i] << (8*(3-i)); - } - lat = gpslat / B60B60; - gpx->lat = lat; - - return 0; -} - -static int get_GPSlon(gpx_t *gpx) { - int i; - unsigned byte; - ui8_t gpslon_bytes[4]; - int gpslon; - double lon; - - for (i = 0; i < 4; i++) { - byte = gpx->frame[pos_GPSlon + i]; - gpslon_bytes[i] = byte; - } - - gpslon = 0; - for (i = 0; i < 4; i++) { - gpslon |= gpslon_bytes[i] << (8*(3-i)); - } - lon = gpslon / B60B60; - gpx->lon = lon; - - return 0; -} - -static int get_GPSalt(gpx_t *gpx) { - int i; - unsigned byte; - ui8_t gpsheight_bytes[4]; - int gpsheight; - double height; - - for (i = 0; i < 4; i++) { - byte = gpx->frame[pos_GPSalt + i]; - gpsheight_bytes[i] = byte; - } - - gpsheight = 0; - for (i = 0; i < 4; i++) { - gpsheight |= gpsheight_bytes[i] << (8*(3-i)); - } - height = gpsheight / 1000.0; - gpx->alt = height; - - if (height < -200 || height > 60000) return -1; - return 0; -} - -static int get_GPSvel24(gpx_t *gpx) { - int i; - unsigned byte; - ui8_t gpsVel_bytes[3]; - int vel24; - double vx, vy, vz, dir; //, alpha; - - for (i = 0; i < 3; i++) { - byte = gpx->frame[pos_GPSvO + i]; - gpsVel_bytes[i] = byte; - } - vel24 = gpsVel_bytes[0] << 16 | gpsVel_bytes[1] << 8 | gpsVel_bytes[2]; - if (vel24 > (0x7FFFFF)) vel24 -= 0x1000000; - vx = vel24 / 1e3; // ost - - for (i = 0; i < 3; i++) { - byte = gpx->frame[pos_GPSvN + i]; - gpsVel_bytes[i] = byte; - } - vel24 = gpsVel_bytes[0] << 16 | gpsVel_bytes[1] << 8 | gpsVel_bytes[2]; - if (vel24 > (0x7FFFFF)) vel24 -= 0x1000000; - vy= vel24 / 1e3; // nord - - for (i = 0; i < 3; i++) { - byte = gpx->frame[pos_GPSvV + i]; - gpsVel_bytes[i] = byte; - } - vel24 = gpsVel_bytes[0] << 16 | gpsVel_bytes[1] << 8 | gpsVel_bytes[2]; - if (vel24 > (0x7FFFFF)) vel24 -= 0x1000000; - vz = vel24 / 1e3; // hoch - - gpx->vE = vx; - gpx->vN = vy; - gpx->vU = vz; - - - gpx->vH = sqrt(vx*vx+vy*vy); -/* - alpha = atan2(vy, vx)*180/M_PI; // ComplexPlane (von x-Achse nach links) - GeoMeteo (von y-Achse nach rechts) - dir = 90-alpha; // z=x+iy= -> i*conj(z)=y+ix=re(i(pi/2-t)), Achsen und Drehsinn vertauscht - if (dir < 0) dir += 360; // atan2(y,x)=atan(y/x)=pi/2-atan(x/y) , atan(1/t) = pi/2 - atan(t) - gpx->vD2 = dir; -*/ - dir = atan2(vx, vy) * 180 / M_PI; - if (dir < 0) dir += 360; - gpx->vD = dir; - - gpx->vV = vz; - - return 0; -} - - -// RS(255,223)-CCSDS -#define rs_N 255 -#define rs_K 223 -#define rs_R (rs_N-rs_K) // 32 - -static int lms6_ecc(gpx_t *gpx, ui8_t *cw) { - int errors; - ui8_t err_pos[rs_R], - err_val[rs_R]; - - errors = rs_decode(&gpx->RS, cw, err_pos, err_val); - - return errors; -} - -static void print_frame(gpx_t *gpx, int crc_err, int len) { - int err=0; - - if (gpx->frame[0] != 0) - { - //if ((gpx->frame[pos_SondeSN+1] & 0xF0) == 0x70) // ? beginnen alle SNs mit 0x7A.... bzw 80..... ? - if ( gpx->frame[pos_SondeSN+1] ) - { - get_SondeSN(gpx); - get_FrameNb(gpx); - printf(" (%7d) ", gpx->sn); - printf(" [%5d] ", gpx->frnr); - err = get_GPStime(gpx, crc_err); - if (!err) printf("%s ", weekday[gpx->wday]); - if (gpx->week > 0) { - if (gpx->gpstow < gpstow_start && !crc_err) { - gpx->week += 1; // week roll-over - gpstow_start = gpx->gpstow; - } - Gps2Date(gpx); - fprintf(stdout, "%04d-%02d-%02d ", gpx->jahr, gpx->monat, gpx->tag); - } - printf("%02d:%02d:%06.3f ", gpx->std, gpx->min, gpx->sek); // falls Rundung auf 60s: Ueberlauf - - get_GPSlat(gpx); - get_GPSlon(gpx); - err = get_GPSalt(gpx); - if (!err) { - printf(" lat: %.5f ", gpx->lat); - printf(" lon: %.5f ", gpx->lon); - printf(" alt: %.2fm ", gpx->alt); - get_GPSvel24(gpx); - //if (gpx->option.vbs == 2) printf(" (%.1f ,%.1f,%.1f) ", gpx->vE, gpx->vN, gpx->vU); - printf(" vH: %.1fm/s D: %.1f vV: %.1fm/s ", gpx->vH, gpx->vD, gpx->vV); - } - - if (crc_err==0) printf(" [OK]"); else printf(" [NO]"); - - printf("\n"); - - - if (gpx->option.jsn) { - // Print JSON output required by auto_rx. - if (crc_err==0) { // CRC-OK - // UTC oder GPS? - printf("{ \"frame\": %d, \"id\": \"LMS6-%d\", \"datetime\": \"", gpx->frnr, gpx->sn ); - //if (gpx->week > 0) printf("%04d-%02d-%02dT", gpx->jahr, gpx->monat, gpx->tag ); - printf("%02d:%02d:%06.3fZ\", \"lat\": %.5f, \"lon\": %.5f, \"alt\": %.5f, \"vel_h\": %.5f, \"heading\": %.5f, \"vel_v\": %.5f", - gpx->std, gpx->min, gpx->sek, gpx->lat, gpx->lon, gpx->alt, gpx->vH, gpx->vD, gpx->vV ); - printf(", \"gpstow\": %d", gpx->gpstow ); - printf(" }\n"); - printf("\n"); - } - } - - } - } -} - - -static void proc_frame(gpx_t *gpx, int len) { - int blk_pos = SYNC_LEN; - ui8_t block_bytes[BLOCK_LEN+8]; - ui8_t rs_cw[rs_N]; - char frame_bits[BITFRAME_LEN+OVERLAP*BITS +8]; // init L-1 bits mit 0 - char *rawbits = NULL; - int i, j; - int err = 0; - int errs = 0; - int crc_err = 0; - int flen, blen; - - - if ((len % 8) > 4) { - while (len % 8) gpx->blk_rawbits[len++] = '0'; - } - gpx->blk_rawbits[len] = '\0'; - - flen = len / (2*BITS); - - if (gpx->option.vit == 1) { - viterbi(gpx->vit, gpx->blk_rawbits); - rawbits = gpx->vit->rawbits; - } - else rawbits = gpx->blk_rawbits; - - err = deconv(rawbits, frame_bits); - - if (err) { for (i=err; i < RAWBITBLOCK_LEN/2; i++) frame_bits[i] = 0; } - - - blen = bits2bytes(frame_bits, block_bytes); - for (j = blen; j < BLOCK_LEN+8; j++) block_bytes[j] = 0; - - - if (gpx->option.ecc) { - for (j = 0; j < rs_N; j++) rs_cw[rs_N-1-j] = block_bytes[SYNC_LEN+j]; - errs = lms6_ecc(gpx, rs_cw); - for (j = 0; j < rs_N; j++) block_bytes[SYNC_LEN+j] = rs_cw[rs_N-1-j]; - } - - if (gpx->option.raw == 2) { - for (i = 0; i < flen; i++) printf("%02x ", block_bytes[i]); - if (gpx->option.ecc) printf("(%d)", errs); - printf("\n"); - } - else if (gpx->option.raw == 4 && gpx->option.ecc) { - for (i = 0; i < rs_N; i++) printf("%02x", block_bytes[SYNC_LEN+i]); - printf(" (%d)", errs); - printf("\n"); - } - else if (gpx->option.raw == 8) { - if (gpx->option.vit == 1) { - for (i = 0; i < len; i++) printf("%c", gpx->vit->rawbits[i]); printf("\n"); - } - else { - for (i = 0; i < len; i++) printf("%c", gpx->blk_rawbits[i]); printf("\n"); - } - } - - blk_pos = SYNC_LEN; - - while ( blk_pos-SYNC_LEN < FRM_LEN ) { - - if (gpx->sf == 0) { - while ( blk_pos-SYNC_LEN < FRM_LEN ) { - gpx->sf = 0; - for (j = 0; j < 4; j++) gpx->sf += (block_bytes[blk_pos+j] == frm_sync[j]); - if (gpx->sf == 4) { - gpx->frm_pos = 0; - break; - } - blk_pos++; - } - } - - if ( gpx->sf && gpx->frm_pos < FRM_LEN ) { - gpx->frame[gpx->frm_pos] = block_bytes[blk_pos]; - gpx->frm_pos++; - blk_pos++; - } - - if (gpx->frm_pos == FRM_LEN) { - - crc_err = check_CRC(gpx->frame); - - if (gpx->option.raw == 1) { - for (i = 0; i < FRM_LEN; i++) printf("%02x ", gpx->frame[i]); - if (crc_err==0) printf(" [OK]"); else printf(" [NO]"); - printf("\n"); - } - - if (gpx->option.raw == 0) print_frame(gpx, crc_err, len); - - gpx->frm_pos = 0; - gpx->sf = 0; - } - - } - -} - - -int main(int argc, char **argv) { - - int option_inv = 0; // invertiert Signal - int option_iq = 0; - int option_lp = 0; - int option_dc = 0; - int wavloaded = 0; - int sel_wavch = 0; // audio channel: left - int gpsweek = 0; - - FILE *fp = NULL; - char *fpname = NULL; - - int k; - - int bit, rbit; - int bitpos = 0; - int bitQ; - int pos; - //int headerlen = 0; - - int header_found = 0; - - float thres = 0.76; - float _mv = 0.0; - - int symlen = 1; - int bitofs = 1; // +1 .. +2 - int shift = 0; - - unsigned int bc = 0; - - pcm_t pcm = {0}; - dsp_t dsp = {0}; //memset(&dsp, 0, sizeof(dsp)); -/* - // gpx_t _gpx = {0}; gpx_t *gpx = &_gpx; // stack size ... - gpx_t *gpx = NULL; - gpx = calloc(1, sizeof(gpx_t)); - //memset(gpx, 0, sizeof(gpx_t)); -*/ - gpx_t _gpx = {0}; gpx_t *gpx = &_gpx; - - -#ifdef CYGWIN - _setmode(fileno(stdin), _O_BINARY); // _setmode(_fileno(stdin), _O_BINARY); -#endif - setbuf(stdout, NULL); - - - fpname = argv[0]; - ++argv; - while ((*argv) && (!wavloaded)) { - if ( (strcmp(*argv, "-h") == 0) || (strcmp(*argv, "--help") == 0) ) { - fprintf(stderr, "%s [options] audio.wav\n", fpname); - fprintf(stderr, " options:\n"); - fprintf(stderr, " -v, --verbose\n"); - fprintf(stderr, " -r, --raw\n"); - fprintf(stderr, " --vit (Viterbi)\n"); - fprintf(stderr, " --ecc (Reed-Solomon)\n"); - return 0; - } - else if ( (strcmp(*argv, "-v") == 0) || (strcmp(*argv, "--verbose") == 0) ) { - gpx->option.vbs = 1; - } - else if ( (strcmp(*argv, "-r") == 0) || (strcmp(*argv, "--raw") == 0) ) { - gpx->option.raw = 1; // bytes - rs_ecc_codewords - } - else if ( (strcmp(*argv, "-r0") == 0) || (strcmp(*argv, "--raw0") == 0) ) { - gpx->option.raw = 2; // bytes: sync + codewords - } - else if ( (strcmp(*argv, "-rc") == 0) || (strcmp(*argv, "--rawecc") == 0) ) { - gpx->option.raw = 4; // rs_ecc_codewords - } - else if ( (strcmp(*argv, "-R") == 0) || (strcmp(*argv, "--RAW") == 0) ) { - gpx->option.raw = 8; // rawbits - } - else if (strcmp(*argv, "--ecc" ) == 0) { gpx->option.ecc = 1; } // RS-ECC - else if (strcmp(*argv, "--vit" ) == 0) { gpx->option.vit = 1; } // viterbi - else if ( (strcmp(*argv, "--gpsweek") == 0) ) { - ++argv; - if (*argv) { - gpsweek = atoi(*argv); - if (gpsweek < 1024 || gpsweek > 3072) gpsweek = 0; - } - else return -1; - } - else if ( (strcmp(*argv, "-i") == 0) || (strcmp(*argv, "--invert") == 0) ) { - option_inv = 1; // nicht noetig - } - else if ( (strcmp(*argv, "--dc") == 0) ) { - option_dc = 1; - } - else if ( (strcmp(*argv, "--ch2") == 0) ) { sel_wavch = 1; } // right channel (default: 0=left) - else if ( (strcmp(*argv, "--ths") == 0) ) { - ++argv; - if (*argv) { - thres = atof(*argv); - } - else return -1; - } - else if ( (strcmp(*argv, "-d") == 0) ) { - ++argv; - if (*argv) { - shift = atoi(*argv); - if (shift > 4) shift = 4; - if (shift < -4) shift = -4; - } - else return -1; - } - else if (strcmp(*argv, "--iq0") == 0) { option_iq = 1; } // differential/FM-demod - else if (strcmp(*argv, "--iq2") == 0) { option_iq = 2; } - else if (strcmp(*argv, "--iq3") == 0) { option_iq = 3; } // iq2==iq3 - else if (strcmp(*argv, "--IQ") == 0) { // fq baseband -> IF (rotate from and decimate) - double fq = 0.0; // --IQ , -0.5 < fq < 0.5 - ++argv; - if (*argv) fq = atof(*argv); - else return -1; - if (fq < -0.5) fq = -0.5; - if (fq > 0.5) fq = 0.5; - dsp.xlt_fq = -fq; // S(t) -> S(t)*exp(-f*2pi*I*t) - option_iq = 5; - } - else if (strcmp(*argv, "--lp") == 0) { option_lp = 1; } // IQ lowpass - else if (strcmp(*argv, "--json") == 0) { - gpx->option.jsn = 1; - gpx->option.ecc = 1; - gpx->option.vit = 1; - } - else { - fp = fopen(*argv, "rb"); - if (fp == NULL) { - fprintf(stderr, "%s konnte nicht geoeffnet werden\n", *argv); - return -1; - } - wavloaded = 1; - } - ++argv; - } - if (!wavloaded) fp = stdin; - - - if (gpx->option.raw == 4) gpx->option.ecc = 1; - - // init gpx - memcpy(gpx->blk_rawbits, blk_syncbits, sizeof(blk_syncbits)); - memcpy(gpx->frame, frm_sync, sizeof(frm_sync)); - gpx->frm_pos = 0; // ecc_blk <-> frm_blk - gpx->sf = 0; - - gpx->option.inv = option_inv; // irrelevant - - gpx->week = gpsweek; - - if (option_iq) sel_wavch = 0; - - pcm.sel_ch = sel_wavch; - k = read_wav_header(&pcm, fp); - if ( k < 0 ) { - fclose(fp); - fprintf(stderr, "error: wav header\n"); - return -1; - } - - symlen = 1; - - // init dsp - // - dsp.fp = fp; - dsp.sr = pcm.sr; - dsp.bps = pcm.bps; - dsp.nch = pcm.nch; - dsp.ch = pcm.sel_ch; - dsp.br = (float)BAUD_RATE; - dsp.sps = (float)dsp.sr/dsp.br; - dsp.symlen = symlen; - dsp.symhd = 1; - dsp._spb = dsp.sps*symlen; - dsp.hdr = rawheader; - dsp.hdrlen = strlen(rawheader); - dsp.BT = 1.5; // bw/time (ISI) // 1.0..2.0 - dsp.h = 0.9; // 1.0 modulation index - dsp.lpIQ_bw = 8e3; - dsp.opt_iq = option_iq; - dsp.opt_lp = option_lp; - - if ( dsp.sps < 8 ) { - fprintf(stderr, "note: sample rate low (%.1f sps)\n", dsp.sps); - } - - //headerlen = dsp.hdrlen; - - k = init_buffers(&dsp); - if ( k < 0 ) { - fprintf(stderr, "error: init buffers\n"); - return -1; - }; - - - if (gpx->option.vit) { - k = vit_initCodes(gpx); - if (k < 0) return -1; - } - if (gpx->option.ecc) { - rs_init_RS255ccsds(&gpx->RS); // bch_ecc.c - } - - - bitofs += shift; - - - while ( 1 ) - { - - header_found = find_header(&dsp, thres, 3, bitofs, option_dc); - _mv = dsp.mv; - - if (header_found == EOF) break; - - // mv == correlation score - if (_mv*(0.5-gpx->option.inv) < 0) { - gpx->option.inv ^= 0x1; // LMS6: irrelevant - } - - if (header_found) { - - bitpos = 0; - pos = BLOCKSTART; - - if (_mv > 0) bc = 0; else bc = 1; - - while ( pos < RAWBITBLOCK_LEN ) { - - bitQ = read_slbit(&dsp, &rbit, 0, bitofs, bitpos, -1, 0); // symlen=1 - - if (bitQ == EOF) { break; } - - bit = rbit ^ (bc%2); // (c0,inv(c1)) - gpx->blk_rawbits[pos] = 0x30 + bit; - - bc++; - pos++; - bitpos += 1; - } - - gpx->blk_rawbits[pos] = '\0'; - - time_elapsed_sec = dsp.sample_in / (double)dsp.sr; - proc_frame(gpx, pos); - - if (pos < RAWBITBLOCK_LEN) break; - - pos = BLOCKSTART; - header_found = 0; - } - - } - - - free_buffers(&dsp); - if (gpx->vit) { free(gpx->vit); gpx->vit = NULL; } - - fclose(fp); - - return 0; -} - diff --git a/demod/mod/m20mod.c b/demod/mod/m20mod.c index 072a7ebe..2802e043 100644 --- a/demod/mod/m20mod.c +++ b/demod/mod/m20mod.c @@ -77,12 +77,13 @@ dduudduudduudduu duduudduuduudduu ddududuudduduudd uduuddududududud uudduduuddu //"0111011010011111"; // M10: 76 9F , w/ aux-data //"0110010001001001"; // M10-dop: 64 49 09 //"0110010010101111"; // M10+: 64 AF w/ gtop-GPS + //"0100010100100000"; // M20: 45 20 (baud=9600) static char rawheader[] = "10011001100110010100110010011001"; #define FRAME_LEN (100+1) // 0x64+1 #define BITFRAME_LEN (FRAME_LEN*BITS) -#define AUX_LEN 20 +#define AUX_LEN 64 #define BITAUX_LEN (AUX_LEN*BITS) @@ -103,8 +104,10 @@ typedef struct { double vH; double vD; double vV; double vx; double vy; double vD2; float T; float RH; float TH; float P; + float batV; ui8_t numSV; - ui8_t utc_ofs; + //ui8_t utc_ofs; + ui8_t fwVer; char SN[12+4]; ui8_t SNraw[3]; ui8_t frame_bytes[FRAME_LEN+AUX_LEN+4]; @@ -192,12 +195,12 @@ frame[0x08..0x0A]: GPS altitude frame[0x0B..0x0E]: GPS hor.Vel. (velE,velN) frame[0x0F..0x11]: GPS TOW frame[0x15]: counter -frame[0x16..0x17]: block check - +frame[0x16..0x17]: block check (fwVer < 0x06) ; frame[0x16]: SPI1 P[0] (fwVer >= 0x07), frame[0x17]=0x00 frame[0x18..0x19]: GPS ver.Vel. (velU) frame[0x1A..0x1B]: GPS week frame[0x1C..0x1F]: GPS latitude frame[0x20..0x23]: GPS longitude +frame[0x24..0x25]: SPI1 P[1..2] (if pressure sensor) frame[0x44..0x45]: frame check */ @@ -217,7 +220,8 @@ frame[0x44..0x45]: frame check #define pos_SN 0x12 // 3 byte #define pos_CNT 0x15 // 1 byte #define pos_BlkChk 0x16 // 2 byte -#define pos_Check (stdFLEN-1) // 2 byte +#define pos_stdFW 0x43 // 1 byte +#define pos_stdCheck (stdFLEN-1) // 2 byte #define len_BlkChk 0x16 // frame[0x02..0x17] , incl. chk16 @@ -249,6 +253,10 @@ frame[0x44..0x45]: frame check #define col_CSoo "\x1b[38;5;220m" #define col_CSno "\x1b[38;5;1m" #define col_CNST "\x1b[38;5;58m" // 3 byte +#define col_ptuP "\x1b[38;5;180m" +#define col_ptuT "\x1b[38;5;110m" +#define col_ptuU "\x1b[38;5;120m" +#define col_ptuTH "\x1b[38;5;115m" /* $ for code in {0..255} @@ -256,6 +264,9 @@ frame[0x44..0x45]: frame check > done */ +#define COLOPT(tcol) ((gpx->option.col)?(tcol):("")) + + static int get_GPSweek(gpx_t *gpx) { int i; unsigned byte; @@ -618,7 +629,9 @@ static float get_Temp(gpx_t *gpx) { x = (4095.0-ADC_RT)/ADC_RT; // (Vcc-Vout)/Vout = Vcc/Vout - 1 R = Rs[scT] /( x - Rs[scT]/Rp[scT] ); - if (R > 0) T = 1/( p0 + p1*log(R) + p2*log(R)*log(R) + p3*log(R)*log(R)*log(R) ); + if (R > 0) T = 1.0/( p0 + p1*log(R) + p2*log(R)*log(R) + p3*log(R)*log(R)*log(R) ); + + if (T-273.15 < -120.0 || T-273.15 > 60.0) T = 0; // T < -120C, T > 60C invalid return T - 273.15; // Celsius } @@ -641,7 +654,7 @@ static float get_Tntc2(gpx_t *gpx) { ADC_ntc0 = (gpx->frame_bytes[0x07] << 8) | gpx->frame_bytes[0x06]; // M10: 0x40,0x3F x = (4095.0 - ADC_ntc0)/ADC_ntc0; // (Vcc-Vout)/Vout R = Rs / x; - if (R > 0) T = 1/(1/T25 + 1/b * log(R/R25)); + if (R > 0) T = 1.0/(1.0/T25 + 1.0/b * log(R/R25)); //if (R > 0) T = 1/( p0 + p1*log(R) + p2*log(R)*log(R) + p3*log(R)*log(R)*log(R) ); return T - 273.15; @@ -685,9 +698,11 @@ static float get_RH(gpx_t *gpx) { RH = -1.0f; if (humval < 48000) { - RH = x; - if (RH < 0.0f ) RH = 0.0f; - if (RH > 100.0f) RH = 100.0f; + if (x > -20.0f && x < 120.f) { + RH = x; + if (RH < 0.0f ) RH = 0.0f; + if (RH > 100.0f) RH = 100.0f; + } } // (Hyland and Wexler) Tntc2 (T_RH) <-> Tmain ? @@ -696,18 +711,36 @@ static float get_RH(gpx_t *gpx) { } static float get_P(gpx_t *gpx) { -// cf. DF9DQ // float hPa = 0.0f; - ui16_t val = (gpx->frame_bytes[0x25] << 8) | gpx->frame_bytes[0x24]; + ui32_t val = (gpx->frame_bytes[0x25] << 8) | gpx->frame_bytes[0x24]; // cf. DF9DQ + ui8_t p0 = 0x00; + + if (gpx->fwVer >= 0x07) { // SPI1_P[0] + p0 = gpx->frame_bytes[0x16]; + } + val = (val << 8) | p0; if (val > 0) { - hPa = val/16.0f; + hPa = val/(float)(16*256); // 4096=0x1000 + } + if (hPa > 2560.0f) { // val > 0xA00000 + hPa = -1.0f; } return hPa; } +static float get_BatV(gpx_t *gpx) { + float batV = 0.0f; + ui8_t val = gpx->frame_bytes[0x26]; // cf. DF9DQ + + batV = val * (3.3f/255); // upper 8 bits ADC + + return batV; +} + + /* -------------------------------------------------------------------------- */ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { @@ -730,94 +763,65 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { get_SN(gpx); if (gpx->option.ptu && csOK) { - gpx->T = get_Temp(gpx); // temperature - gpx->TH = get_Tntc2(gpx); // rel. humidity sensor temperature + gpx->T = get_Temp(gpx); // temperature + gpx->TH = get_Tntc2(gpx); // rel. humidity sensor temperature gpx->RH = get_RH(gpx); // relative humidity gpx->P = get_P(gpx); // (optional) pressure } + gpx->batV = get_BatV(gpx); // battery V + if ( !gpx->option.slt ) { - if (gpx->option.col) { - fprintf(stdout, col_TXT); - if (gpx->option.vbs >= 3) { - fprintf(stdout, "[%3d]", gpx->frame_bytes[pos_CNT]); - fprintf(stdout, " (W "col_GPSweek"%d"col_TXT") ", gpx->week); - } - fprintf(stdout, col_GPSTOW"%s"col_TXT" ", weekday[gpx->wday]); - fprintf(stdout, col_GPSdate"%04d-%02d-%02d"col_TXT" "col_GPSTOW"%02d:%02d:%06.3f"col_TXT" ", - gpx->jahr, gpx->monat, gpx->tag, gpx->std, gpx->min, gpx->sek); - fprintf(stdout, " lat: "col_GPSlat"%.5f"col_TXT" ", gpx->lat); - fprintf(stdout, " lon: "col_GPSlon"%.5f"col_TXT" ", gpx->lon); - fprintf(stdout, " alt: "col_GPSalt"%.2f"col_TXT" ", gpx->alt); - if (!err2) { - fprintf(stdout, " vH: "col_GPSvel"%4.1f"col_TXT" D: "col_GPSvel"%5.1f"col_TXT" vV: "col_GPSvel"%3.1f"col_TXT" ", gpx->vH, gpx->vD, gpx->vV); - } - if (gpx->option.vbs >= 1 && (bcOK || csOK)) { // SN - fprintf(stdout, " SN: "col_SN"%s"col_TXT, gpx->SN); - } - if (gpx->option.vbs >= 1) { - fprintf(stdout, " # "); - if (bcOK > 0) fprintf(stdout, " "col_CSok"(ok)"col_TXT); - else if (bcOK < 0) fprintf(stdout, " "col_CSoo"(oo)"col_TXT); - else fprintf(stdout, " "col_CSno"(no)"col_TXT); - // - if (csOK) fprintf(stdout, " "col_CSok"[OK]"col_TXT); - else fprintf(stdout, " "col_CSno"[NO]"col_TXT); - } - if (gpx->option.ptu && csOK) { - fprintf(stdout, " "); - if (gpx->T > -273.0f) fprintf(stdout, " T:%.1fC", gpx->T); - if (gpx->RH > -0.5f) fprintf(stdout, " RH=%.0f%%", gpx->RH); - if (gpx->option.vbs >= 2) { - if (gpx->TH > -273.0f) fprintf(stdout, " TH:%.1fC", gpx->TH); - } - if (gpx->P > 0.0f) { - if (gpx->P < 100.0f) fprintf(stdout, " P=%.2fhPa ", gpx->P); - else fprintf(stdout, " P=%.1fhPa ", gpx->P); - } - } - fprintf(stdout, ANSI_COLOR_RESET""); + fprintf(stdout, "%s", COLOPT(col_TXT)); + if (gpx->option.vbs >= 3) { + fprintf(stdout, "[%3d]", gpx->frame_bytes[pos_CNT]); + fprintf(stdout, " (W %s%d%s) ", COLOPT(col_GPSweek), gpx->week, COLOPT(col_TXT)); } - else { - if (gpx->option.vbs >= 3) { - fprintf(stdout, "[%3d]", gpx->frame_bytes[pos_CNT]); - fprintf(stdout, " (W %d) ", gpx->week); - } - fprintf(stdout, "%s ", weekday[gpx->wday]); - fprintf(stdout, "%04d-%02d-%02d %02d:%02d:%06.3f ", - gpx->jahr, gpx->monat, gpx->tag, gpx->std, gpx->min, gpx->sek); - fprintf(stdout, " lat: %.5f ", gpx->lat); - fprintf(stdout, " lon: %.5f ", gpx->lon); - fprintf(stdout, " alt: %.2f ", gpx->alt); - if (!err2) { - fprintf(stdout, " vH: %4.1f D: %5.1f vV: %3.1f ", gpx->vH, gpx->vD, gpx->vV); - } - if (gpx->option.vbs >= 1 && (bcOK || csOK)) { // SN - fprintf(stdout, " SN: %s", gpx->SN); + fprintf(stdout, "%s%s%s ", COLOPT(col_GPSTOW), weekday[gpx->wday], COLOPT(col_TXT)); + fprintf(stdout, "%s%04d-%02d-%02d%s %s%02d:%02d:%06.3f%s ", + COLOPT(col_GPSdate), gpx->jahr, gpx->monat, gpx->tag, COLOPT(col_TXT), + COLOPT(col_GPSTOW), gpx->std, gpx->min, gpx->sek, COLOPT(col_TXT)); + fprintf(stdout, " lat: %s%.5f%s ", COLOPT(col_GPSlat), gpx->lat, COLOPT(col_TXT)); + fprintf(stdout, " lon: %s%.5f%s ", COLOPT(col_GPSlon), gpx->lon, COLOPT(col_TXT)); + fprintf(stdout, " alt: %s%.2f%s ", COLOPT(col_GPSalt), gpx->alt, COLOPT(col_TXT)); + if (!err2) { + fprintf(stdout, " vH: %s%4.1f%s D: %s%5.1f%s vV: %s%3.1f%s ", + COLOPT(col_GPSvel), gpx->vH, COLOPT(col_TXT), + COLOPT(col_GPSvel), gpx->vD, COLOPT(col_TXT), + COLOPT(col_GPSvel), gpx->vV, COLOPT(col_TXT)); + } + if (gpx->option.vbs >= 1 && (bcOK || csOK)) { // SN + fprintf(stdout, " SN: %s%s%s", COLOPT(col_SN), gpx->SN, COLOPT(col_TXT)); + } + if (gpx->option.vbs >= 1) { + fprintf(stdout, " # "); + if (gpx->fwVer < 0x07) { + if (bcOK > 0) fprintf(stdout, " %s(ok)%s", COLOPT(col_CSok), COLOPT(col_TXT)); + else if (bcOK < 0) fprintf(stdout, " %s(oo)%s", COLOPT(col_CSoo), COLOPT(col_TXT)); + else fprintf(stdout, " %s(no)%s", COLOPT(col_CSno), COLOPT(col_TXT)); } - if (gpx->option.vbs >= 1) { - fprintf(stdout, " # "); - //if (bcOK) fprintf(stdout, " (ok)"); else fprintf(stdout, " (no)"); - if (bcOK > 0) fprintf(stdout, " (ok)"); - else if (bcOK < 0) fprintf(stdout, " (oo)"); - else fprintf(stdout, " (no)"); - // - if (csOK) fprintf(stdout, " [OK]"); else fprintf(stdout, " [NO]"); + if (csOK) fprintf(stdout, " %s[OK]%s", COLOPT(col_CSok), COLOPT(col_TXT)); + else fprintf(stdout, " %s[NO]%s", COLOPT(col_CSno), COLOPT(col_TXT)); + } + if (gpx->option.ptu && csOK) { + fprintf(stdout, " "); + if (gpx->T > -273.0f) fprintf(stdout, " T:%.1fC", gpx->T); + if (gpx->RH > -0.5f) fprintf(stdout, " RH=%.0f%%", gpx->RH); + if (gpx->option.vbs >= 2) { + if (gpx->TH > -273.0f) fprintf(stdout, " TH:%.1fC", gpx->TH); } - if (gpx->option.ptu && csOK) { - fprintf(stdout, " "); - if (gpx->T > -273.0f) fprintf(stdout, " T:%.1fC", gpx->T); - if (gpx->RH > -0.5f) fprintf(stdout, " RH=%.0f%%", gpx->RH); - if (gpx->option.vbs >= 2) { - if (gpx->TH > -273.0f) fprintf(stdout, " TH:%.1fC", gpx->TH); - } - if (gpx->P > 0.0f) { - if (gpx->P < 100.0f) fprintf(stdout, " P=%.2fhPa ", gpx->P); - else fprintf(stdout, " P=%.1fhPa ", gpx->P); - } + if (gpx->P > 0.0f) { + if (gpx->P < 10.0f) fprintf(stdout, " P=%.3fhPa ", gpx->P); + else if (gpx->P < 100.0f) fprintf(stdout, " P=%.2fhPa ", gpx->P); + else fprintf(stdout, " P=%.1fhPa ", gpx->P); } } + if (gpx->option.vbs >= 3 && csOK) { + fprintf(stdout, " (bat:%.2fV)", gpx->batV); + } + fprintf(stdout, "%s", COLOPT(ANSI_COLOR_RESET)); + fprintf(stdout, "\n"); } @@ -841,6 +845,7 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { if (gpx->RH > -0.5f) fprintf(stdout, ", \"humidity\": %.1f", gpx->RH ); if (gpx->P > 0.0f) fprintf(stdout, ", \"pressure\": %.2f", gpx->P ); } + fprintf(stdout, ", \"batt\": %.2f", gpx->batV); fprintf(stdout, ", \"rawid\": \"M20_%02X%02X%02X\"", gpx->frame_bytes[pos_SN], gpx->frame_bytes[pos_SN+1], gpx->frame_bytes[pos_SN+2]); // gpx->type fprintf(stdout, ", \"subtype\": \"0x%02X\"", gpx->type); if (gpx->jsn_freq > 0) { @@ -870,7 +875,9 @@ static int print_frame(gpx_t *gpx, int pos, int b2B) { ui8_t byte; int cs1, cs2; int bc1, bc2, bc; - int flen = stdFLEN; // stdFLEN=0x64, auxFLEN=0x76; M20:0x45 ? + int flen = stdFLEN; // M10:stdFLEN=0x64,auxFLEN=0x76; M20:stdFLEN=0x45,auxFLEN=0x6F ? + int pos_fw = pos_stdFW; + int pos_check = pos_stdCheck; if (b2B) { bits2bytes(gpx->frame_bits, gpx->frame_bytes); @@ -880,10 +887,21 @@ static int print_frame(gpx_t *gpx, int pos, int b2B) { else { gpx->auxlen = flen - stdFLEN; //if (gpx->auxlen < 0 || gpx->auxlen > AUX_LEN) gpx->auxlen = 0; // 0x43,0x45 + if (gpx->auxlen < 0) { + gpx->auxlen = 0; + pos_fw = flen-2; // only if flen < stdFLEN + } + else if (gpx->auxlen > AUX_LEN) { + gpx->auxlen = AUX_LEN; + flen = stdFLEN+AUX_LEN; + } } + pos_check = flen-1; + gpx->fwVer = gpx->frame_bytes[pos_fw]; + if (gpx->fwVer > 0x20) gpx->fwVer = 0; - cs1 = (gpx->frame_bytes[pos_Check+gpx->auxlen] << 8) | gpx->frame_bytes[pos_Check+gpx->auxlen+1]; - cs2 = checkM10(gpx->frame_bytes, pos_Check+gpx->auxlen); + cs1 = (gpx->frame_bytes[pos_check] << 8) | gpx->frame_bytes[pos_check+1]; + cs2 = checkM10(gpx->frame_bytes, pos_check); bc1 = (gpx->frame_bytes[pos_BlkChk] << 8) | gpx->frame_bytes[pos_BlkChk+1]; bc2 = blk_checkM10(len_BlkChk, gpx->frame_bytes+2); // len(essentialBlock+chk16) = 0x16 @@ -901,35 +919,46 @@ static int print_frame(gpx_t *gpx, int pos, int b2B) { if (gpx->option.raw) { - if (gpx->option.col /* && gpx->frame_bytes[1] != 0x49 */) { - fprintf(stdout, col_FRTXT); + if (1 /*&& gpx->frame_bytes[1] != 0x49 */) { + fprintf(stdout, "%s", COLOPT(col_FRTXT)); for (i = 0; i < flen+1; i++) { byte = gpx->frame_bytes[i]; - if (i == 1) fprintf(stdout, col_Mtype); - if ((i >= pos_GPSTOW) && (i < pos_GPSTOW+3)) fprintf(stdout, col_GPSTOW); - if ((i >= pos_GPSlat) && (i < pos_GPSlat+4)) fprintf(stdout, col_GPSlat); - if ((i >= pos_GPSlon) && (i < pos_GPSlon+4)) fprintf(stdout, col_GPSlon); - if ((i >= pos_GPSalt) && (i < pos_GPSalt+3)) fprintf(stdout, col_GPSalt); - if ((i >= pos_GPSweek) && (i < pos_GPSweek+2)) fprintf(stdout, col_GPSweek); - if ((i >= pos_GPSvE) && (i < pos_GPSvE+2)) fprintf(stdout, col_GPSvel); - if ((i >= pos_GPSvN) && (i < pos_GPSvN+2)) fprintf(stdout, col_GPSvel); - if ((i >= pos_GPSvU) && (i < pos_GPSvU+2)) fprintf(stdout, col_GPSvel); - if ((i >= pos_SN) && (i < pos_SN+3)) fprintf(stdout, col_SN); - if (i == pos_CNT) fprintf(stdout, col_CNT); - if ((i >= pos_BlkChk) && (i < pos_BlkChk+2)) fprintf(stdout, col_Check); - if ((i >= pos_Check+gpx->auxlen) && (i < pos_Check+gpx->auxlen+2)) fprintf(stdout, col_Check); + if (i == 1) fprintf(stdout, "%s", COLOPT(col_Mtype)); + if ((i >= pos_GPSTOW) && (i < pos_GPSTOW+3)) fprintf(stdout, "%s", COLOPT(col_GPSTOW)); + if ((i >= pos_GPSlat) && (i < pos_GPSlat+4)) fprintf(stdout, "%s", COLOPT(col_GPSlat)); + if ((i >= pos_GPSlon) && (i < pos_GPSlon+4)) fprintf(stdout, "%s", COLOPT(col_GPSlon)); + if ((i >= pos_GPSalt) && (i < pos_GPSalt+3)) fprintf(stdout, "%s", COLOPT(col_GPSalt)); + if ((i >= pos_GPSweek) && (i < pos_GPSweek+2)) fprintf(stdout, "%s", COLOPT(col_GPSweek)); + if ((i >= pos_GPSvE) && (i < pos_GPSvE+2)) fprintf(stdout, "%s", COLOPT(col_GPSvel)); + if ((i >= pos_GPSvN) && (i < pos_GPSvN+2)) fprintf(stdout, "%s", COLOPT(col_GPSvel)); + if ((i >= pos_GPSvU) && (i < pos_GPSvU+2)) fprintf(stdout, "%s", COLOPT(col_GPSvel)); + if ((i >= pos_SN) && (i < pos_SN+3)) fprintf(stdout, "%s", COLOPT(col_SN)); + if (i == pos_CNT) fprintf(stdout, "%s", COLOPT(col_CNT)); + if (gpx->fwVer < 0x07) { + if ((i >= pos_BlkChk) && (i < pos_BlkChk+2)) fprintf(stdout, "%s", COLOPT(col_Check)); + } else { + if ((i >= pos_BlkChk+1) && (i < pos_BlkChk+2)) fprintf(stdout, "%s", COLOPT(col_Check)); + } + if (i >= 0x02 && i <= 0x03) fprintf(stdout, "%s", COLOPT(col_ptuU)); + if (i >= 0x04 && i <= 0x05) fprintf(stdout, "%s", COLOPT(col_ptuT)); + if (i >= 0x06 && i <= 0x07) fprintf(stdout, "%s", COLOPT(col_ptuTH)); + if (i == 0x16 && gpx->fwVer >= 0x07 || i >= 0x24 && i <= 0x25) fprintf(stdout, "%s", COLOPT(col_ptuP)); + + if ((i >= pos_check) && (i < pos_check+2)) fprintf(stdout, "%s", COLOPT(col_Check)); fprintf(stdout, "%02x", byte); - fprintf(stdout, col_FRTXT); + fprintf(stdout, "%s", COLOPT(col_FRTXT)); } if (gpx->option.vbs) { - fprintf(stdout, " # "col_Check"%04x"col_FRTXT, cs2); - if (bc > 0) fprintf(stdout, " "col_CSok"(ok)"col_TXT); - else if (bc < 0) fprintf(stdout, " "col_CSoo"(oo)"col_TXT); - else fprintf(stdout, " "col_CSno"(no)"col_TXT); - if (cs1 == cs2) fprintf(stdout, " "col_CSok"[OK]"col_TXT); - else fprintf(stdout, " "col_CSno"[NO]"col_TXT); + fprintf(stdout, " # %s%04x%s", COLOPT(col_Check), cs2, COLOPT(col_FRTXT)); + if (gpx->fwVer < 0x07) { + if (bc > 0) fprintf(stdout, " %s(ok)%s", COLOPT(col_CSok), COLOPT(col_TXT)); + else if (bc < 0) fprintf(stdout, " %s(oo)%s", COLOPT(col_CSoo), COLOPT(col_TXT)); + else fprintf(stdout, " %s(no)%s", COLOPT(col_CSno), COLOPT(col_TXT)); + } + if (cs1 == cs2) fprintf(stdout, " %s[OK]%s", COLOPT(col_CSok), COLOPT(col_TXT)); + else fprintf(stdout, " %s[NO]%s", COLOPT(col_CSno), COLOPT(col_TXT)); } - fprintf(stdout, ANSI_COLOR_RESET"\n"); + fprintf(stdout, "%s\n", COLOPT(ANSI_COLOR_RESET)); } else { for (i = 0; i < flen+1; i++) { @@ -938,14 +967,16 @@ static int print_frame(gpx_t *gpx, int pos, int b2B) { } if (gpx->option.vbs) { fprintf(stdout, " # %04x", cs2); - if (bc > 0) fprintf(stdout, " (ok)"); - else if (bc < 0) fprintf(stdout, " (oo)"); - else fprintf(stdout, " (no)"); + if (gpx->fwVer < 0x07) { + if (bc > 0) fprintf(stdout, " (ok)"); + else if (bc < 0) fprintf(stdout, " (oo)"); + else fprintf(stdout, " (no)"); + } if (cs1 == cs2) fprintf(stdout, " [OK]"); else fprintf(stdout, " [NO]"); } fprintf(stdout, "\n"); } - if (gpx->option.slt /*&& gpx->option.jsn*/) { + if (gpx->option.slt /*&& gpx->option.jsn && gpx->frame_bytes[1] != 0x49*/) { print_pos(gpx, bc, cs1 == cs2); } } @@ -956,6 +987,7 @@ static int print_frame(gpx_t *gpx, int pos, int b2B) { byte = gpx->frame_bytes[i]; fprintf(stdout, "%02x", byte); } + if (cs1 == cs2) fprintf(stdout, " [OK]"); else fprintf(stdout, " [NO]"); fprintf(stdout, "\n"); } } diff --git a/demod/mod/meisei100mod.c b/demod/mod/meisei100mod.c index 19eba26d..923c14cd 100644 --- a/demod/mod/meisei100mod.c +++ b/demod/mod/meisei100mod.c @@ -136,6 +136,7 @@ e.g. -b --br 2398 typedef struct { int frnr; int frnr1; + int ref_yr; int jahr; int monat; int tag; int std; int min; float sek; double lat; double lon; double alt; @@ -326,11 +327,27 @@ static int reset_gpx(gpx_t *gpx) { /* -------------------------------------------------------------------------- */ +static int est_year_ims100(int _y, int _yr) { + int yr_rollover = 20; // default: 2020..2029 + int yr_offset = 20; + if (_yr > 2003 && _yr < 2100) { + yr_rollover = _yr - 2004; + yr_offset = (yr_rollover / 10) * 10; + } + _y %= 10; + _y += yr_offset; + if (_y < yr_rollover) _y += 10; + return 2000+_y; +} + +/* -------------------------------------------------------------------------- */ + int main(int argc, char **argv) { int option_verbose = 0, option_raw = 0, + option_dbg = 0, option_inv = 0, option_ecc = 0, // BCH(63,51) option_jsn = 0; // JSON output (auto_rx) @@ -427,6 +444,7 @@ int main(int argc, char **argv) { return 0; } else if ( (strcmp(*argv, "-r") == 0) ) { option_raw = 1; } + else if ( (strcmp(*argv, "--dbg") == 0) ) { option_dbg = 1; } else if ( (strcmp(*argv, "-i") == 0) || (strcmp(*argv, "--invert") == 0) ) { option_inv = 1; // nicht noetig } @@ -445,7 +463,7 @@ int main(int argc, char **argv) { ++argv; if (*argv) { baudrate = atof(*argv); - if (baudrate < 2200 || baudrate > 2400) baudrate = 2400; // default: 2400 + if (baudrate < 2200 || baudrate > 2600) baudrate = 2400; // default: 2400 } else return -1; } @@ -507,6 +525,12 @@ int main(int argc, char **argv) { if (frq < 300000000) frq = -1; cfreq = frq; } + else if (strcmp(*argv, "--year") == 0) { + int _yr = 0; + ++argv; + if (*argv) _yr = atoi(*argv); else return -1; + if (_yr > 2003 && _yr < 2100) gpx.ref_yr = _yr; + } else if (strcmp(*argv, "-") == 0) { int sample_rate = 0, bits_sample = 0, channels = 0; ++argv; @@ -546,6 +570,9 @@ int main(int argc, char **argv) { if (cfreq > 0) gpx.jsn_freq = (cfreq+500)/1000; + // ims100: default ref. year + if (gpx.ref_yr < 2000) gpx.ref_yr = 2024; // -> 2020..2029 + #ifdef EXT_FSK if (!option_softin) { @@ -783,6 +810,10 @@ int main(int argc, char **argv) { | ( (w16[0]&0xFF00)>>8 | (w16[0]&0xFF)<<8 ); fw32 = f32e2(w32); + if (option_dbg) { + printf(" # [%02d] %08x : %.1f # ", counter % 64, w32, fw32); + } + if (err_blks == 0) // err_frm zu schwach { gpx.cfg[counter%64] = fw32; @@ -1016,6 +1047,10 @@ int main(int argc, char **argv) { w16[0] = bits2val(subframe_bits+HEADLEN+46*1 , 16); w16[1] = bits2val(subframe_bits+HEADLEN+46*1+17, 16); w32 = (w16[1]<<16) | w16[0]; + + if (option_dbg) { + printf(" # [%02d] %08x : %.1f # ", counter % 64, w32, *fcfg); + } // counter ok and w16[] ok (max 1 error) if (err_frm == 0 && block_err[0] < 2 && block_err[1] < 2) { @@ -1117,12 +1152,9 @@ int main(int argc, char **argv) { dat2 = bits2val(subframe_bits+HEADLEN, 16); gpx.tag = dat2/1000; gpx.monat = (dat2/10)%100; - _y = (dat2%10)+10; - if (_y < 14) _y += 10; // 2020 - gpx.jahr = 2000 + _y; - //if (option_verbose) printf("%05u ", dat2); - //printf("(%02d-%02d-%02d) ", gpx.tag, gpx.monat, gpx.jahr%100); // 2020: +20 ? - printf("(%04d-%02d-%02d) ", gpx.jahr, gpx.monat, gpx.tag); // 2020: +20 ? + _y = dat2 % 10; + gpx.jahr = est_year_ims100(_y, gpx.ref_yr); + printf("(%04d-%02d-%02d) ", gpx.jahr, gpx.monat, gpx.tag); lat1 = bits2val(subframe_bits+HEADLEN+46*0+17, 16); lat2 = bits2val(subframe_bits+HEADLEN+46*1 , 16); diff --git a/demod/mod/mp3h1mod.c b/demod/mod/mp3h1mod.c index ccb553cf..5c7caf1c 100644 --- a/demod/mod/mp3h1mod.c +++ b/demod/mod/mp3h1mod.c @@ -63,12 +63,16 @@ typedef struct { } option_t; -#define BITFRAME_LEN ((51*16)/2) // ofs=8: 52..53: AA AA (1..5) or 00 00 (6) +#define CRCLEN_ECEF 45 // default +#define CRCLEN_LATLON 42 + +#define BITFRAME_LEN ((CRCLEN_ECEF+6)*8) //8=16/2 // ofs=8: 52..53: AA AA (1..5) or 00 00 (6) #define RAWBITFRAME_LEN (BITFRAME_LEN*2) #define FRAMESTART (HEADOFS+HEADLEN) #define FRAME_LEN (BITFRAME_LEN/8) + typedef struct { ui8_t subcnt1; ui8_t subcnt2; @@ -83,15 +87,19 @@ typedef struct { float calC; // C(ntc) float A_adcT; float B_adcT; float C_adcT; float A_adcH; float B_adcH; float C_adcH; + float Tadc; float RHadc; + float T; float RH; ui8_t frame[FRAME_LEN+16]; char frame_bits[BITFRAME_LEN+16]; ui32_t cfg[16]; ui32_t snC; ui32_t snD; - float T; float RH; ui8_t cfg_ntc; ui8_t cfg_T; ui8_t cfg_H; ui8_t crcOK; // + int crclen; + int bitfrm_len; + // int sec_day; int sec_day_prev; int gps_cnt; @@ -250,11 +258,22 @@ static i16_t i2(ui8_t *bytes) { // 16bit signed int #define pos_GPSecefZ (OFS+16) // 4 byte #define pos_GPSecefV (OFS+20) // 3*2 byte #define pos_GPSnSats (OFS+26) // 1 byte (num Sats ?) -#define pos_PTU1 (OFS+35) // 4 byte -#define pos_PTU2 (OFS+39) // 4 byte +#define pos_T16 (OFS+29) // 2 byte +#define pos_H16 (OFS+31) // 2 byte +#define pos_FFFF (OFS+33) // 2 byte +#define pos_ADCT (OFS+35) // 4 byte +#define pos_ADCH (OFS+39) // 4 byte #define pos_CNT2 (OFS+43) // 1 byte (0x01..0x10 ?) #define pos_CFG (OFS+44) // 2/4 byte -#define pos_CRC (OFS+48) // 2 byte +#define pos_CRC_ECEF (OFS+CRCLEN_ECEF+1) // 2 byte +#define pos_CRC_LATLON (OFS+CRCLEN_LATLON+1) // 2 byte + +#define pos_GPSlat (OFS+ 7) // 4 byte +#define pos_GPSlon (OFS+11) // 4 byte +#define pos_GPSalt (OFS+15) // 4 byte +#define pos_GPSvH (OFS+19) // 2 byte +#define pos_GPSvD (OFS+21) // 2 byte + // ----------------------------------------------------------------------------- @@ -280,10 +299,10 @@ static int crc16rev(gpx_t *gpx, int start, int len) { } return rem; } -static int check_CRC(gpx_t *gpx) { - ui32_t crclen = 45; +static int check_CRC(gpx_t *gpx, ui32_t crclen) { + //ui32_t crclen = 45; // 45/42 ui32_t crcdat = 0; - crcdat = u2(gpx->frame+pos_CRC); + crcdat = u2(gpx->frame+crclen+3); if ( crcdat != crc16rev(gpx, pos_CNT1, crclen) ) { return 1; // CRC NO } @@ -321,7 +340,7 @@ static void ecef2elli(double X[], double *lat, double *lon, double *alt) { *lon = lam*180/M_PI; } -static int get_GPSkoord(gpx_t *gpx) { +static int get_GPSkoord_ecef(gpx_t *gpx) { int k; int XYZ; // 32bit double X[3], lat, lon, alt; @@ -348,7 +367,7 @@ static int get_GPSkoord(gpx_t *gpx) { gpx->lat = lat; gpx->lon = lon; gpx->alt = alt; - if ((alt < -1000.0) || (alt > 80000.0)) return -3; // plausibility-check: altitude, if ecef=(0,0,0) + if (alt < -1000.0 || alt > 80000.0) return -3; // plausibility-check: altitude, if ecef=(0,0,0) // ECEF-Velocities @@ -374,6 +393,38 @@ static int get_GPSkoord(gpx_t *gpx) { return 0; } +static int get_GPSkoord_latlon(gpx_t *gpx) { + int XYZ; // 32bit + short vH, vV; // 16bit + unsigned short vD; + + + memcpy(&XYZ, gpx->frame+pos_GPSlat, 4); + gpx->lat = XYZ * 1e-6; + + memcpy(&XYZ, gpx->frame+pos_GPSlon, 4); + gpx->lon = XYZ * 1e-6; + + memcpy(&XYZ, gpx->frame+pos_GPSalt, 4); + gpx->alt = XYZ * 1e-2; + + if (gpx->alt < -1000.0 || gpx->alt > 80000.0) return -3; // plausibility-check: altitude + + vH = gpx->frame[pos_GPSvH] | (gpx->frame[pos_GPSvH+1] << 8); + vD = gpx->frame[pos_GPSvD] | (gpx->frame[pos_GPSvD+1] << 8); + + gpx->vH = vH / 100.0; + gpx->vD = vD / 100.0; + gpx->vV = 0; + + //TODO: Sats + // num Sats solution ? GLONASS + GPS ? + gpx->numSats = gpx->frame[pos_GPSnSats-3]; // ? + + + return 0; +} + static int reset_time(gpx_t *gpx) { gpx->gps_cnt = 0; @@ -428,7 +479,7 @@ static float f32(ui32_t w) { return f; } -static int get_ptu(gpx_t *gpx) { +static int get_ptu(gpx_t *gpx, int ofs) { // cf. МРЗ-3МК documentation float t = -273.15f; @@ -436,10 +487,10 @@ static int get_ptu(gpx_t *gpx) { float ADC_MAX = 32767.0; //32767=(1<<15)? 32767? - int ADCT = u4(gpx->frame+pos_PTU1); // u3? + int ADCT = u4(gpx->frame+pos_ADCT+ofs); // u3? float adc_t = ADCT/100.0; - int ADCH = u4(gpx->frame+pos_PTU2); // u3? + int ADCH = u4(gpx->frame+pos_ADCH+ofs); // u3? float adc_h = ADCH/100.0; @@ -454,15 +505,15 @@ static int get_ptu(gpx_t *gpx) { } } } - gpx->T = t; + gpx->Tadc = t; - if (gpx->T > -273.0f) + if (gpx->Tadc > -273.0f) { if (gpx->cfg_H == 0x7) { float poly2 = adc_h*adc_h * gpx->A_adcH + adc_h * gpx->B_adcH + gpx->A_adcH; float K = poly2/ADC_MAX; - rh = (K - 0.1515) / (0.00636*(1.05460 - 0.00216*gpx->T)); // if T = 273.15, set T=0 ? + rh = (K - 0.1515) / (0.00636*(1.05460 - 0.00216*gpx->Tadc)); // if T = 273.15, set T=0 ? if (rh < -10.0f || rh > 120.0f) rh = -1.0f; else { if (rh < 0.0f) rh = 0.0f; @@ -470,19 +521,23 @@ static int get_ptu(gpx_t *gpx) { } } } - gpx->RH = rh; + gpx->RHadc = rh; + + + gpx->T = i2(gpx->frame+pos_T16+ofs) / 100.0; + gpx->RH = i2(gpx->frame+pos_H16+ofs) / 100.0; return 0; } -static int get_cfg(gpx_t *gpx) { +static int get_cfg(gpx_t *gpx, int ofs) { gpx->subcnt1 = (gpx->frame[pos_CNT1] & 0xF); - gpx->subcnt2 = gpx->frame[pos_CNT2] ; // ? subcnt2 == subcnt1 + 1 ? + gpx->subcnt2 = gpx->frame[pos_CNT2+ofs] ; // ? subcnt2 == subcnt1 + 1 ? if (gpx->crcOK) { - ui32_t cfg32 = u4(gpx->frame+pos_CFG); + ui32_t cfg32 = u4(gpx->frame+pos_CFG+ofs); gpx->cfg[gpx->subcnt1] = cfg32; switch (gpx->subcnt1) { // or use subcnt2 ? @@ -576,13 +631,16 @@ static void print_gpx(gpx_t *gpx, int crcOK) { //printf(" :%6.1f: ", sample_count/(double)sample_rate); // + int ofs_ptucfg = (gpx->crclen == CRCLEN_ECEF) ? 0 : -3; + gpx->crcOK = crcOK; - get_cfg(gpx); + get_cfg(gpx, ofs_ptucfg); get_time(gpx); - get_GPSkoord(gpx); + if (ofs_ptucfg) get_GPSkoord_latlon(gpx); + else get_GPSkoord_ecef(gpx); - get_ptu(gpx); + get_ptu(gpx, ofs_ptucfg); if (gpx->sec_day != gpx->sec_day_prev || !gpx->option.unq) { @@ -593,14 +651,38 @@ static void print_gpx(gpx_t *gpx, int crcOK) { printf(" lat: %.5f ", gpx->lat); printf(" lon: %.5f ", gpx->lon); printf(" alt: %.2f ", gpx->alt); - printf(" vH: %4.1f D: %5.1f vV: %3.1f ", gpx->vH, gpx->vD, gpx->vV); + + printf(" vH: %4.1f D: %5.1f ", gpx->vH, gpx->vD); + if ( !ofs_ptucfg ) { + printf(" vV: %3.1f ", gpx->vV); + } + if (gpx->option.vbs > 1) printf(" sats: %d ", gpx->numSats); + if (gpx->option.vbs > 1 && ofs_ptucfg < 0) + { + static float alt0; + static int t0; + if (gpx->crcOK && gpx->sec_day > t0) { + if (t0 > 0 && gpx->sec_day < t0+10) { + printf(" (d_alt: %+4.1f) ", (gpx->alt - alt0)/(float)(gpx->sec_day - t0) ); + } + alt0 = gpx->alt; + t0 = gpx->sec_day; + } + } + if (gpx->option.ptu) { if (gpx->T > -273.0f || gpx->RH > -0.5f) printf(" "); - if (gpx->T > -273.0f) printf(" T=%.1fC", gpx->T); - if (gpx->RH > -0.5f) printf(" RH=%.0f%%", gpx->RH); + if (gpx->T > -273.0f) printf(" T=%.2fC", gpx->T); + if (gpx->RH > -0.5f) printf(" RH=%.2f%%", gpx->RH); if (gpx->T > -273.0f || gpx->RH > -0.5f) printf(" "); + if (gpx->option.vbs > 1) { + if (gpx->Tadc > -273.0f || gpx->RHadc > -0.5f) printf(" ("); + if (gpx->Tadc > -273.0f) printf(" T0=%.1fC", gpx->Tadc); + if (gpx->RHadc > -0.5f) printf(" RH0=%.0f%%", gpx->RHadc); + if (gpx->Tadc > -273.0f || gpx->RHadc > -0.5f) printf(" ) "); + } } if (gpx->option.col) { @@ -664,8 +746,14 @@ static void print_gpx(gpx_t *gpx, int crcOK) { char *ver_jsn = NULL; printf("{ \"type\": \"%s\"", "MRZ"); printf(", \"frame\": %lu, ", (unsigned long)gpx->gps_cnt); // sec_gps0+0.5 - printf("\"id\": \"MRZ-%d-%d\", \"datetime\": \"%04d-%02d-%02dT%02d:%02d:%02dZ\", \"lat\": %.5f, \"lon\": %.5f, \"alt\": %.5f, \"vel_h\": %.5f, \"heading\": %.5f, \"vel_v\": %.5f, \"sats\": %d", - gpx->snC, gpx->snD, gpx->yr, gpx->mth, gpx->day, gpx->hrs, gpx->min, gpx->sec, gpx->lat, gpx->lon, gpx->alt, gpx->vH, gpx->vD, gpx->vV, gpx->numSats); + printf("\"id\": \"MRZ-%d-%d\", \"datetime\": \"%04d-%02d-%02dT%02d:%02d:%02dZ\", \"lat\": %.5f, \"lon\": %.5f, \"alt\": %.5f", + gpx->snC, gpx->snD, gpx->yr, gpx->mth, gpx->day, gpx->hrs, gpx->min, gpx->sec, gpx->lat, gpx->lon, gpx->alt); + printf(", \"vel_h\": %.5f, \"heading\": %.5f", gpx->vH, gpx->vD); + if ( !ofs_ptucfg ) { + printf(", \"vel_v\": %.5f", gpx->vV); + } + printf(", \"sats\": %d", gpx->numSats); + if (gpx->option.ptu) { if (gpx->T > -273.0f) { fprintf(stdout, ", \"temp\": %.1f", gpx->T ); @@ -680,7 +768,7 @@ static void print_gpx(gpx_t *gpx, int crcOK) { // Reference time/position printf(", \"ref_datetime\": \"%s\"", "UTC" ); // {"GPS", "UTC"} GPS-UTC=leap_sec - printf(", \"ref_position\": \"%s\"", "GPS" ); // {"GPS", "MSL"} GPS=ellipsoid , MSL=geoid + printf(", \"ref_position\": \"%s\"", !ofs_ptucfg ? "GPS" : "MSL" ); // {"GPS", "MSL"} GPS=ellipsoid , MSL=geoid #ifdef VER_JSN_STR ver_jsn = VER_JSN_STR; @@ -717,7 +805,14 @@ static void print_frame(gpx_t *gpx, int pos, int b2B) { int frmlen = (pos-bits_ofs)/8; bits2bytes(gpx->frame_bits+bits_ofs, gpx->frame, frmlen); - crcOK = (check_CRC(gpx) == 0); + if (u2(gpx->frame+30) == 0xFFFF) gpx->crclen = CRCLEN_LATLON; + else gpx->crclen = CRCLEN_ECEF; + + crcOK = (check_CRC(gpx, gpx->crclen) == 0); + + if (crcOK) { + gpx->bitfrm_len = (gpx->crclen+6)*8; + } if (gpx->option.raw == 1) { //printf(" :%6.1f: ", sample_count/(double)sample_rate); @@ -741,7 +836,14 @@ static void print_frame(gpx_t *gpx, int pos, int b2B) { { int frmlen = pos; - crcOK = (check_CRC(gpx) == 0); + if (u2(gpx->frame+30) == 0xFFFF) gpx->crclen = CRCLEN_LATLON; + else gpx->crclen = CRCLEN_ECEF; + + crcOK = (check_CRC(gpx, gpx->crclen) == 0); + + if (crcOK) { + gpx->bitfrm_len = (gpx->crclen+6)*8; + } if (gpx->option.raw) { //printf(" :%6.1f: ", sample_count/(double)sample_rate); @@ -955,6 +1057,16 @@ int main(int argc, char **argv) { if (cfreq > 0) gpx.jsn_freq = (cfreq+500)/1000; + // init frame/type + if ((CRCLEN_ECEF+6)*8 > BITFRAME_LEN) { + if (fp) fclose(fp); + fprintf(stderr, "error: int frame\n"); + return -1; + } + gpx.crclen = CRCLEN_ECEF; + gpx.bitfrm_len = (gpx.crclen+6)*8; + + #ifdef EXT_FSK if (!option_softin) { option_softin = 1; @@ -1082,7 +1194,7 @@ int main(int argc, char **argv) { bitpos = 0; pos = FRAMESTART/2; - while ( pos < BITFRAME_LEN ) + while ( pos < gpx.bitfrm_len ) { if (option_softin) { float s1 = 0.0; @@ -1122,7 +1234,7 @@ int main(int argc, char **argv) { gpx.frame_bits[pos] = '\0'; print_frame(&gpx, pos, 1); - if (pos < BITFRAME_LEN) break; + if (pos < gpx.bitfrm_len) break; header_found = 0; } diff --git a/demod/mod/rs41mod.c b/demod/mod/rs41mod.c index 38a5cc3a..937f7ed1 100644 --- a/demod/mod/rs41mod.c +++ b/demod/mod/rs41mod.c @@ -65,6 +65,7 @@ typedef struct { i8_t aut; i8_t jsn; // JSON output (auto_rx) i8_t slt; // silent (only raw/json) + i8_t cal; // json cal/conf } option_t; typedef struct { @@ -117,6 +118,9 @@ typedef struct { ui8_t dfrm_bitscore[FRAME_LEN]; ui8_t calibytes[51*16]; ui8_t calfrchk[51]; + ui8_t calconf_complete; + ui8_t calconf_sent; + ui8_t *calconf_subfrm; // 1+16 byte cal/conf subframe float ptu_Rf1; // ref-resistor f1 (750 Ohm) float ptu_Rf2; // ref-resistor f2 (1100 Ohm) float ptu_co1[3]; // { -243.911 , 0.187654 , 8.2e-06 } @@ -139,6 +143,7 @@ typedef struct { ui16_t conf_cd; // kill countdown (sec) (kt or bt) ui8_t conf_bk; // burst kill char rstyp[9]; // RS41-SG, RS41-SGP + char rsm[10]; // RSM421 int aux; char xdata[XDATA_LEN+16]; // xdata: aux_str1#aux_str2 ... option_t option; @@ -266,15 +271,15 @@ float r4(ui8_t *bytes) { } */ -static int crc16(gpx_t *gpx, int start, int len) { +static int crc16(ui8_t data[], int len) { int crc16poly = 0x1021; int rem = 0xFFFF, i, j; int byte; - if (start+len+2 > FRAME_LEN) return -1; + //if (start+len+2 > FRAME_LEN) return -1; for (i = 0; i < len; i++) { - byte = gpx->frame[start+i]; + byte = data[i]; rem = rem ^ (byte << 8); for (j = 0; j < 8; j++) { if (rem & 0x8000) { @@ -297,7 +302,7 @@ static int check_CRC(gpx_t *gpx, ui32_t pos, ui32_t pck) { crclen = gpx->frame[pos+1]; if (pos + crclen + 4 > FRAME_LEN) return -1; crcdat = u2(gpx->frame+pos+2+crclen); - if ( crcdat != crc16(gpx, pos+2, crclen) ) { + if ( crcdat != crc16(gpx->frame+pos+2, crclen) ) { return 1; // CRC NO } else return 0; // CRC OK @@ -330,7 +335,8 @@ GPS chip: ublox UBX-G6010-ST #define pos_Calburst 0x05E // 1 byte, calfr 0x02 // ? #define pos_Caltimer 0x05A // 2 byte, calfr 0x02 ? #define pos_CalRSTyp 0x05B // 8 byte, calfr 0x21 (+2 byte in 0x22?) - // weitere chars in calfr 0x22/0x23; weitere ID + // weitere chars in calfr 0x22/0x23; weitere ID (RSM) +#define pos_CalRSM 0x055 // 6 byte, calfr 0x22 #define crc_PTU (1<<1) #define xor_PTU 0xE388 // ^0x99A2=0x0x7A2A @@ -448,6 +454,9 @@ static int get_SondeID(gpx_t *gpx, int crc, int ofs) { memset(gpx->calfrchk, 0, 51); // 0x00..0x32 // reset conf data memset(gpx->rstyp, 0, 9); + memset(gpx->rsm, 0, 10); + gpx->calconf_complete = 0; + gpx->calconf_sent = 0; gpx->freq = 0; gpx->conf_fw = 0; gpx->conf_bt = 0; @@ -501,6 +510,19 @@ static int get_FrameConf(gpx_t *gpx, int ofs) { gpx->ecdat.last_calfrm = calfr; gpx->ecdat.last_calfrm_ts = gpx->ecdat.ts; + + if ( !gpx->calconf_complete ) { + int sum = 0; + for (i = 0; i < 51; i++) { // 0x00..0x32 + sum += gpx->calfrchk[i]; + } + if (sum == 51) { // count all subframes + int calconf_dat = gpx->calibytes[0] | (gpx->calibytes[1]<<8); + int calconf_crc = crc16(gpx->calibytes+2, 50*16-2); // subframe 0x32 not included (variable) + + if (calconf_dat == calconf_crc) gpx->calconf_complete = 1; + } + } } return err; @@ -1296,7 +1318,7 @@ static int get_Aux(gpx_t *gpx, int out, int pos) { auxlen = gpx->frame[pos+1]; auxcrc = gpx->frame[pos+2+auxlen] | (gpx->frame[pos+2+auxlen+1]<<8); - if ( auxcrc == crc16(gpx, pos+2, auxlen) ) { + if ( pos + auxlen + 4 <= FRAME_LEN && auxcrc == crc16(gpx->frame+pos+2, auxlen) ) { if (count7E == 0) { if (out) fprintf(stdout, "\n # xdata = "); } @@ -1366,8 +1388,11 @@ static int get_Calconf(gpx_t *gpx, int out, int ofs) { ui16_t fw = 0; int freq = 0, f0 = 0, f1 = 0; char sondetyp[9]; + char rsmtyp[10]; int err = 0; + gpx->calconf_subfrm = gpx->frame+pos_CalData+ofs; + byte = gpx->frame[pos_CalData+ofs]; calfr = byte; err = check_CRC(gpx, pos_FRAME+ofs, pck_FRAME); @@ -1451,6 +1476,17 @@ static int get_Calconf(gpx_t *gpx, int out, int ofs) { } } } + + if (calfr == 0x22) { + for (i = 0; i < 10; i++) rsmtyp[i] = 0; + for (i = 0; i < 8; i++) { + byte = gpx->frame[pos_CalRSM+ofs + i]; + if ((byte >= 0x20) && (byte < 0x7F)) rsmtyp[i] = byte; + else /*if (byte == 0x00)*/ rsmtyp[i] = '\0'; + } + if (out && gpx->option.vbs) fprintf(stdout, ": %s ", rsmtyp); + strcpy(gpx->rsm, rsmtyp); + } } return 0; @@ -2088,6 +2124,45 @@ static int print_position(gpx_t *gpx, int ec) { if (gpx->freq > 0) fq_kHz = gpx->freq; fprintf(stdout, ", \"freq\": %d", fq_kHz); } + if (*gpx->rsm) { // RSM type + fprintf(stdout, ", \"rs41_mainboard\": \"%s\"", gpx->rsm); + } + if (gpx->conf_fw) { // firmware + fprintf(stdout, ", \"rs41_mainboard_fw\": %d", gpx->conf_fw); + } + + if (gpx->option.cal == 1) { // cal/conf + int _j; + if ( !gpx->calconf_sent && gpx->calconf_complete ) { + /* + fprintf(stdout, ", \"rs41_calconf320h\": \""); // only constant/crc part + for (int _j = 0; _j < 50*16; _j++) { + fprintf(stdout, "%02X", gpx->calibytes[_j]); + } + */ + fprintf(stdout, ", \"rs41_calconf51x16\": \""); + for (_j = 0; _j < 51*16; _j++) { + fprintf(stdout, "%02X", gpx->calibytes[_j]); + } + fprintf(stdout, "\""); + gpx->calconf_sent = 1; + } + if (gpx->calconf_subfrm[0] == 0x32) { + fprintf(stdout, ", \"rs41_conf0x32\": \""); + for (_j = 0; _j < 16; _j++) { + fprintf(stdout, "%02X", gpx->calconf_subfrm[1+_j]); + } + fprintf(stdout, "\""); + } + } + if (gpx->option.cal == 2) { // cal/conf + int _j; + fprintf(stdout, ", \"rs41_subfrm\": \"0x%02X:", gpx->calconf_subfrm[0]); + for (_j = 0; _j < 16; _j++) { + fprintf(stdout, "%02X", gpx->calconf_subfrm[1+_j]); + } + fprintf(stdout, "\""); + } // Include frequency derived from subframe information if available. if (gpx->freq > 0) { @@ -2423,6 +2498,8 @@ int main(int argc, char *argv[]) { if (frq < 300000000) frq = -1; cfreq = frq; } + else if (strcmp(*argv, "--jsnsubfrm1") == 0) { gpx.option.cal = 1; } // json cal/conf + else if (strcmp(*argv, "--jsnsubfrm2") == 0) { gpx.option.cal = 2; } // json cal/conf else if (strcmp(*argv, "--rawhex") == 0) { rawhex = 2; } // raw hex input else if (strcmp(*argv, "--xorhex") == 0) { rawhex = 2; xorhex = 1; } // raw xor input else if (strcmp(*argv, "-") == 0) { @@ -2474,6 +2551,13 @@ int main(int argc, char *argv[]) { // init gpx memcpy(gpx.frame, rs41_header_bytes, sizeof(rs41_header_bytes)); // 8 header bytes + gpx.calconf_subfrm = gpx.frame+pos_CalData; + if (gpx.option.cal) { + gpx.option.jsn = 1; + gpx.option.ecc = 2; + gpx.option.crc = 1; + } + if (cfreq > 0) gpx.jsn_freq = (cfreq+500)/1000; diff --git a/scan/dft_detect.c b/scan/dft_detect.c index 31989718..701196fe 100644 --- a/scan/dft_detect.c +++ b/scan/dft_detect.c @@ -32,6 +32,7 @@ static int option_verbose = 0, // ausfuehrliche Anzeige option_dc = 0, option_silent = 0, option_cont = 0, + option_d2 = 0, option_pcmraw = 0, option_singleLpIQ = 0, wavloaded = 0; @@ -107,8 +108,17 @@ static char imet1rs_header[] = // C34/C50: 2400 baud, 1:2900Hz/0:4800Hz static char c34_preheader[] = -"01010101010101010101010101010101"; // 2900 Hz tone -// dft, dB-max(1000Hz..5000Hz) = 2900Hz ? + "01010101010101010101010101010101"; // 2900 Hz tone + // dft, dB-max(1000Hz..5000Hz) = 2900Hz ? + + +static char weathex_header[] = + "10101010""10101010""10101010" // AA AA AA (preamble) + "00101101""11010100"; //"10101010"; // 2D D4 55/AA + +static char wxr2pn9_header[] = + "10101010""10101010""10101010" // AA AA AA (preamble) + "11000001""10010100"; //"11000001"; // C1 94 C1 typedef struct { @@ -134,27 +144,29 @@ static float lpFM_bw[2] = { 4e3, 10e3 }; // FM-audio lowpass bandwidth static float lpIQ_bw[N_bwIQ] = { 6e3, 12e3, 22e3, 200e3 }; // IF iq lowpass bandwidth static float set_lpIQ = 0.0; -#define tn_DFM 2 -#define tn_RS41 3 -#define tn_RS92 4 -#define tn_M10 5 -#define tn_M20 6 -#define tn_LMS6 8 -#define tn_MEISEI 9 -#define tn_MRZ 12 -#define tn_MTS01 13 -#define tn_C34C50 15 -#define tn_MK2LMS 18 -#define tn_IMET5 24 -#define tn_IMETa 25 -#define tn_IMET4 26 -#define tn_IMET1rs 28 -#define tn_IMET1ab 29 - -#define Nrs 15 -#define idxIMETafsk 12 -#define idxRS 13 -#define idxI4 14 +#define tn_DFM 2 +#define tn_RS41 3 +#define tn_RS92 4 +#define tn_M10 5 +#define tn_M20 6 +#define tn_LMS6 8 +#define tn_MEISEI 9 +#define tn_MRZ 12 +#define tn_MTS01 13 +#define tn_C34C50 15 +#define tn_WXR301 16 +#define tn_WXRpn9 17 +#define tn_MK2LMS 18 +#define tn_IMET5 24 +#define tn_IMETa 25 +#define tn_IMET4 26 +#define tn_IMET1rs 28 +#define tn_IMET1ab 29 + +#define Nrs 17 +#define idxIMETafsk 14 +#define idxRS 15 +#define idxI4 16 static rsheader_t rs_hdr[Nrs] = { { 2500, 0, 0, dfm_header, 1.0, 0.0, 0.65, 2, NULL, "DFM9", tn_DFM, 0, 1, 0.0, 0.0}, // DFM6: -2 ? { 4800, 0, 0, rs41_header, 0.5, 0.0, 0.70, 2, NULL, "RS41", tn_RS41, 0, 1, 0.0, 0.0}, @@ -167,6 +179,8 @@ static rsheader_t rs_hdr[Nrs] = { { 2400, 0, 0, mrz_header, 1.5, 0.0, 0.80, 2, NULL, "MRZ", tn_MRZ, 0, 1, 0.0, 0.0}, { 1200, 0, 0, mts01_header, 1.0, 0.0, 0.65, 2, NULL, "MTS01", tn_MTS01, 0, 0, 0.0, 0.0}, { 5800, 0, 0, c34_preheader, 1.5, 0.0, 0.80, 2, NULL, "C34C50", tn_C34C50, 0, 2, 0.0, 0.0}, // C34/C50 2900 Hz tone + { 4800, 0, 0, weathex_header, 1.0, 0.0, 0.65, 2, NULL, "WXR301", tn_WXR301, 0, 3, 0.0, 0.0}, + { 5000, 0, 0, wxr2pn9_header, 1.0, 0.0, 0.65, 2, NULL, "WXRPN9", tn_WXRpn9, 0, 3, 0.0, 0.0}, { 9600, 0, 0, imet1ab_header, 1.0, 0.0, 0.80, 2, NULL, "IMET1AB", tn_IMET1ab, 1, 3, 0.0, 0.0}, // (rs_hdr[idxAB]) { 9600, 0, 0, imet_preamble, 0.5, 0.0, 0.80, 4, NULL, "IMETafsk", tn_IMETa , 1, 1, 0.0, 0.0}, // IMET1AB, IMET1RS (IQ)IMET4 { 9600, 0, 0, imet1rs_header, 0.5, 0.0, 0.80, 2, NULL, "IMET1RS", tn_IMET1rs, 0, 3, 0.0, 0.0}, // (rs_hdr[idxRS]) IMET4: lpIQ=0 ... @@ -175,9 +189,28 @@ static rsheader_t rs_hdr[Nrs] = { static int idx_MTS01 = -1, idx_C34C50 = -1, + idx_WXR301 = -1, + idx_WXRPN9 = -1, idx_IMET1AB = -1; +static int rs_detect2[Nrs]; + +static int rs_d2() { + int tn = 0; + for (tn = 0; tn < Nrs; tn++) { + if ( rs_detect2[tn] > 1 ) break; + } + return tn; +} + +static int reset_d2() { + int n = 0; + for (n = 0; n < Nrs; n++) rs_detect2[n] = 0; + return 0; +} + + /* // m10-false-positive: // m10-preamble similar to rs41-preamble, parts of rs92/imet1ab, imet1ab; diffs: @@ -985,9 +1018,12 @@ static int init_buffers() { float f_lp; // dec_lowpass: lowpass_bw/2 float t_bw; // dec_lowpass: transition_bw int taps; // dec_lowpass: taps + int wideIF = 0; if (set_lpIQ > IF_sr) IF_sr = set_lpIQ; + wideIF = IF_sr > 60e3; + sr_base = sample_rate; if (option_min) IF_sr = IF_SAMPLE_RATE_MIN; @@ -997,8 +1033,13 @@ static int init_buffers() { decM = sr_base / IF_sr; } - f_lp = (IF_sr+20e3)/(4.0*sr_base); + f_lp = (IF_sr+20e3)/(4.0*sr_base); // IF=48k t_bw = (IF_sr-20e3)/*/2.0*/; + if (wideIF) { // IF=96k + f_lp = (IF_sr+60e3)/(4.0*sr_base); + t_bw = (IF_sr-60e3)/*/2.0*/; + } + else if (option_min) { t_bw = (IF_sr-12e3); } @@ -1115,6 +1156,10 @@ static int init_buffers() { #ifdef NOC34C50 if ( strncmp(rs_hdr[j].type, "C34C50", 6) == 0 ) idx_C34C50 = j; #endif + #ifdef NOWXR301 + if ( strncmp(rs_hdr[j].type, "WXR301", 5) == 0 ) idx_WXR301 = j; + if ( strncmp(rs_hdr[j].type, "WXRPN9", 5) == 0 ) idx_WXRPN9 = j; + #endif #ifdef NOIMET1AB if ( strncmp(rs_hdr[j].type, "IMET1AB", 7) == 0 ) idx_IMET1AB = j; #endif @@ -1124,7 +1169,7 @@ static int init_buffers() { rs_hdr[j].spb = sample_rate/(float)rs_hdr[j].sps; rs_hdr[j].hLen = strlen(rs_hdr[j].header); rs_hdr[j].L = rs_hdr[j].hLen * rs_hdr[j].spb + 0.5; - if (j != idx_MTS01 && j != idx_C34C50 && j != idx_IMET1AB) { + if (j != idx_MTS01 && j != idx_C34C50 && j != idx_WXR301 && j != idx_WXRPN9 && j != idx_IMET1AB) { if (rs_hdr[j].hLen > hLen) hLen = rs_hdr[j].hLen; if (rs_hdr[j].L > Lmax) Lmax = rs_hdr[j].L; } @@ -1302,6 +1347,8 @@ int main(int argc, char **argv) { int j_max; float mv_max; + int d2_tn = Nrs; + #ifdef CYGWIN _setmode(fileno(stdin), _O_BINARY); // _setmode(_fileno(stdin), _O_BINARY); @@ -1364,6 +1411,9 @@ int main(int argc, char **argv) { if (*argv) tl = atof(*argv); else return -50; } + else if ( (strcmp(*argv, "-d2") == 0) ) { + option_d2 = 1; + } else if ( (strcmp(*argv, "--ch2") == 0) ) { wav_channel = 1; } // right channel (default: 0=left) else if ( (strcmp(*argv, "--ths") == 0) ) { ++argv; @@ -1397,6 +1447,9 @@ int main(int argc, char **argv) { } if (!wavloaded) fp = stdin; + if (option_d2) { + option_cont = 0; + } if (option_pcmraw == 0) { j = read_wav_header(fp, wav_channel); @@ -1439,6 +1492,8 @@ int main(int argc, char **argv) { if ( j == idx_MTS01 ) continue; // only ifdef NOMTS01 if ( j == idx_C34C50 ) continue; // only ifdef NOC34C50 + if ( j == idx_WXR301 ) continue; // only ifdef NOWXR301 + if ( j == idx_WXRPN9 ) continue; // only ifdef NOWXR301 if ( j == idx_IMET1AB ) continue; // only ifdef NOIMET1AB mv0_pos[j] = mv_pos[j]; @@ -1547,16 +1602,23 @@ int main(int argc, char **argv) { if (header_found) { if (!option_silent && (mv[j] > rs_hdr[j].thres || mv[j] < -rs_hdr[j].thres)) { - if (option_verbose) fprintf(stdout, "sample: %d\n", mv_pos[j]); - fprintf(stdout, "%s: %.4f", rs_hdr[j].type, mv[j]); - if (option_dc && option_iq) { - fprintf(stdout, " , %+.1fHz", rs_hdr[j].df*sr_base); - if (option_verbose) { - fprintf(stdout, " [ fq-ofs: %+.6f", rs_hdr[j].df); - fprintf(stdout, " = %+.1fHz ]", rs_hdr[j].df*sr_base); + if (option_d2) { + rs_detect2[j] += 1; + d2_tn = rs_d2(); + if ( d2_tn == Nrs ) header_found = 0; + } + if ( !option_d2 || j == d2_tn ) { + if (option_verbose) fprintf(stdout, "sample: %d\n", mv_pos[j]); + fprintf(stdout, "%s: %.4f", rs_hdr[j].type, mv[j]); + if (option_dc && option_iq) { + fprintf(stdout, " , %+.1fHz", rs_hdr[j].df*sr_base); + if (option_verbose) { + fprintf(stdout, " [ fq-ofs: %+.6f", rs_hdr[j].df); + fprintf(stdout, " = %+.1fHz ]", rs_hdr[j].df*sr_base); + } } + fprintf(stdout, "\n"); } - fprintf(stdout, "\n"); } // if ((j < 3) && mv[j] < 0) header_found = -1; @@ -1570,7 +1632,7 @@ int main(int argc, char **argv) { } } - if (header_found && !option_cont) break; + if (header_found && !option_cont || d2_tn < Nrs) break; header_found = 0; for (j = 0; j < Nrs; j++) mv[j] = 0.0; } diff --git a/utils/fsk_demod.c b/utils/fsk_demod.c index 3def541a..ecd57421 100644 --- a/utils/fsk_demod.c +++ b/utils/fsk_demod.c @@ -54,6 +54,7 @@ int main(int argc,char *argv[]){ struct FSK *fsk; struct MODEM_STATS stats; int Fs,Rs,M,P,stats_ctr,stats_loop; + long sample_count; float loop_time; int enable_stats = 0; FILE *fin,*fout; @@ -280,6 +281,7 @@ int main(int argc,char *argv[]){ for(i=0;ippm); + fprintf(stderr,"\"secs\": %ld, \"samples\": %ld, \"EbNodB\": %5.1f, \"ppm\": %4d,",seconds, sample_count, stats.snr_est, (int)fsk->ppm); float *f_est; if (fsk->freq_est_type) f_est = fsk->f2_est; diff --git a/weathex/Makefile b/weathex/Makefile new file mode 100644 index 00000000..f4d21f5b --- /dev/null +++ b/weathex/Makefile @@ -0,0 +1,11 @@ +CFLAGS += -Ofast +LDLIBS = -lm + +PROGRAMS := weathex301d + +all: $(PROGRAMS) + +weathex301d: weathex301d.o + +clean: + $(RM) $(PROGRAMS) $(PROGRAMS:=.o) diff --git a/weathex/weathex301d.c b/weathex/weathex301d.c new file mode 100644 index 00000000..d50a770d --- /dev/null +++ b/weathex/weathex301d.c @@ -0,0 +1,715 @@ + +/* + Weathex WxR-301D (64kHz wide) + UAII2022 Lindenberg: w/ PN9, 5000 baud + Malaysia: w/o PN9, 4800 baud +*/ + +#include +#include +#include + + +// optional JSON "version" +// (a) set global +// gcc -DVERSION_JSN [-I] ... +#ifdef VERSION_JSN + #include "version_jsn.h" +#endif +// or +// (b) set local compiler option, e.g. +// gcc -DVER_JSN_STR=\"0.0.2\" ... + + +typedef unsigned char ui8_t; +typedef short i16_t; +typedef unsigned int ui32_t; + + +int option_verbose = 0, + option_raw = 0, + option_inv = 0, + option_b = 0, + option_json = 0, + option_timestamp = 0, + option_softin = 0, + wavloaded = 0; +int wav_channel = 0; // audio channel: left + +int option_pn9 = 0; + +#define BAUD_RATE 4800.0 +#define BAUD_RATE_PN9 5000.0 //(4997.2) // 5000 + +#define FRAMELEN 69 //64 +#define BITFRAMELEN (8*FRAMELEN) + +#define HEADLEN 40 +#define HEADOFS 0 +char header_pn9[] = "10101010""10101010""10101010"//"10101010" // AA AA AA (preamble) + "11000001""10010100"; //"11000001""11000110"; // C1 94 (C1 C6) + +char header[] = "10101010""10101010""10101010" // AA AA AA (preamble) + "00101101""11010100"; //"10101010"; // 2D D4 (55/AA) + +char buf[HEADLEN+1] = "xxxxxxxxxx\0"; +int bufpos = 0; + +char frame_bits[BITFRAMELEN+1]; +ui8_t frame_bytes[FRAMELEN+1]; +ui8_t xframe[FRAMELEN+1]; + +float baudrate = BAUD_RATE; + +/* ------------------------------------------------------------------------------------ */ + +int sample_rate = 0, bits_sample = 0, channels = 0; +float samples_per_bit = 0; + +int findstr(char *buff, char *str, int pos) { + int i; + for (i = 0; i < 4; i++) { + if (buff[(pos+i)%4] != str[i]) break; + } + return i; +} + +int read_wav_header(FILE *fp) { + char txt[4+1] = "\0\0\0\0"; + unsigned char dat[4]; + int byte, p=0; + + if (fread(txt, 1, 4, fp) < 4) return -1; + if (strncmp(txt, "RIFF", 4) && strncmp(txt, "RF64", 4)) return -1; + if (fread(txt, 1, 4, fp) < 4) return -1; + // pos_WAVE = 8L + if (fread(txt, 1, 4, fp) < 4) return -1; + if (strncmp(txt, "WAVE", 4)) return -1; + // pos_fmt = 12L + for ( ; ; ) { + if ( (byte=fgetc(fp)) == EOF ) return -1; + txt[p % 4] = byte; + p++; if (p==4) p=0; + if (findstr(txt, "fmt ", p) == 4) break; + } + if (fread(dat, 1, 4, fp) < 4) return -1; + if (fread(dat, 1, 2, fp) < 2) return -1; + + if (fread(dat, 1, 2, fp) < 2) return -1; + channels = dat[0] + (dat[1] << 8); + + if (fread(dat, 1, 4, fp) < 4) return -1; + memcpy(&sample_rate, dat, 4); //sample_rate = dat[0]|(dat[1]<<8)|(dat[2]<<16)|(dat[3]<<24); + + if (fread(dat, 1, 4, fp) < 4) return -1; + if (fread(dat, 1, 2, fp) < 2) return -1; + //byte = dat[0] + (dat[1] << 8); + + if (fread(dat, 1, 2, fp) < 2) return -1; + bits_sample = dat[0] + (dat[1] << 8); + + // pos_dat = 36L + info + for ( ; ; ) { + if ( (byte=fgetc(fp)) == EOF ) return -1; + txt[p % 4] = byte; + p++; if (p==4) p=0; + if (findstr(txt, "data", p) == 4) break; + } + if (fread(dat, 1, 4, fp) < 4) return -1; + + + fprintf(stderr, "sample_rate: %d\n", sample_rate); + fprintf(stderr, "bits : %d\n", bits_sample); + fprintf(stderr, "channels : %d\n", channels); + + if (bits_sample != 8 && bits_sample != 16 && bits_sample != 32) return -1; + + if (sample_rate == 900001) sample_rate -= 1; + + samples_per_bit = sample_rate/(float)baudrate; + + fprintf(stderr, "samples/bit: %.2f\n", samples_per_bit); + + return 0; +} + +unsigned long sample_count = 0; +double bitgrenze = 0; + +int f32read_signed_sample(FILE *fp, float *s) { + int i; + unsigned int word = 0; + short *b = (short*)&word; + float *f = (float*)&word; + + for (i = 0; i < channels; i++) { + + if (fread( &word, bits_sample/8, 1, fp) != 1) return EOF; + + if (i == wav_channel) { // i = 0: links bzw. mono + //if (bits_sample == 8) sint = b-128; // 8bit: 00..FF, centerpoint 0x80=128 + //if (bits_sample == 16) sint = (short)b; + + if (bits_sample == 32) { + *s = *f; + } + else { + if (bits_sample == 8) { *b -= 128; } + *s = *b/128.0; + if (bits_sample == 16) { *s /= 256.0; } + } + } + } + + sample_count++; + + return 0; +} + +int par=1, par_alt=1; + +int read_bits_fsk(FILE *fp, int *bit, int *len) { + int n; + float s; + float l; + + n = 0; + do { + if (f32read_signed_sample(fp, &s) == EOF) return EOF; + //sample_count++; // in f32read_signed_sample() + par_alt = par; + par = (s >= 0) ? 1 : -1; + n++; + } while (par*par_alt > 0); + + l = (float)n / samples_per_bit; + + *len = (int)(l+0.5); + + if (!option_inv) *bit = (1+par_alt)/2; // oben 1, unten -1 + else *bit = (1-par_alt)/2; // sdr#= 0) *bit = 1; + else *bit = 0; + + if (option_inv) *bit ^= 1; + + return 0; +} + + +int f32soft_read(FILE *fp, float *s) { + unsigned int word = 0; + short *b = (short*)&word; + float *f = (float*)&word; + int bps = 32; + + if (fread( &word, bps/8, 1, fp) != 1) return EOF; + + if (bps == 32) { + *s = *f; + } + else { + if (bps == 8) { *b -= 128; } + *s = *b/128.0; + if (bps == 16) { *s /= 256.0; } + } + + return 0; +} + + +int compare(char *hdr) { + int i=0; + while ((i < HEADLEN) && (buf[(bufpos+i) % HEADLEN] == hdr[HEADLEN+HEADOFS-1-i])) { + i++; + } + return i; +} + +char inv(char c) { + if (c == '0') return '1'; + if (c == '1') return '0'; + return c; +} + +int compare2(char *hdr) { + int i=0; + while ((i < HEADLEN) && (buf[(bufpos+i) % HEADLEN] == inv(hdr[HEADLEN+HEADOFS-1-i]))) { + i++; + } + return i; +} + +int bits2bytes(char *bitstr, ui8_t *bytes) { + int i, bit, d, byteval; + int bitpos, bytepos; + + bitpos = 0; + bytepos = 0; + + while (bytepos < FRAMELEN) { + + byteval = 0; + d = 1; + for (i = 0; i < 8; i++) { + //bit = bitstr[bitpos+i]; /* little endian */ + bit = bitstr[bitpos+7-i]; /* big endian */ + if (bit == '1') byteval += d; + else /*if ((bit == '0') */ byteval += 0; + d <<= 1; + } + bitpos += 8; + bytes[bytepos++] = byteval; + + } + + //while (bytepos < FRAME_LEN) bytes[bytepos++] = 0; + + return 0; +} + + +// PN9 Data Whitening +// cf. https://www.ti.com/lit/an/swra322/swra322.pdf +// https://destevez.net/2019/07/lucky-7-decoded/ +// +// counter low byte: frame[ofs+4] XOR 0xCC +// zero bytes, frame[ofs+30]: 0C CA C9 FB 49 37 E5 A8 +// +ui8_t PN9b[64] = { 0xFF, 0x87, 0xB8, 0x59, 0xB7, 0xA1, 0xCC, 0x24, + 0x57, 0x5E, 0x4B, 0x9C, 0x0E, 0xE9, 0xEA, 0x50, + 0x2A, 0xBE, 0xB4, 0x1B, 0xB6, 0xB0, 0x5D, 0xF1, + 0xE6, 0x9A, 0xE3, 0x45, 0xFD, 0x2C, 0x53, 0x18, + 0x0C, 0xCA, 0xC9, 0xFB, 0x49, 0x37, 0xE5, 0xA8, + 0x51, 0x3B, 0x2F, 0x61, 0xAA, 0x72, 0x18, 0x84, + 0x02, 0x23, 0x23, 0xAB, 0x63, 0x89, 0x51, 0xB3, + 0xE7, 0x8B, 0x72, 0x90, 0x4C, 0xE8, 0xFb, 0xC1}; + + +ui32_t xor8sum(ui8_t bytes[], int len) { + int j; + ui8_t xor8 = 0; + ui8_t sum8 = 0; + + for (j = 0; j < len; j++) { + xor8 ^= bytes[j]; + sum8 += bytes[j]; + } + //sum8 &= 0xFF; + + return (xor8 << 8) | sum8; +} + + +typedef struct { + ui32_t sn1; + ui32_t cnt1; + int chk1ok; + // + ui32_t sn2; + ui32_t cnt2; + int chk2ok; // GPS subframe + ui8_t hrs; + ui8_t min; + ui8_t sec; + float lat; + float lon; + float alt; + // + int jsn_freq; // freq/kHz (SDR) +} gpx_t; + +gpx_t gpx; + + +// xPN9: OFS=8, 5000 baud ; w/o PN9: OFS=6, 4800 baud +#define OFS 6 +#define OFS_PN9 8 +int ofs = OFS; + +int print_frame() { + int j; + int chkdat, chkval, chk_ok; + + bits2bytes(frame_bits, frame_bytes); + + for (j = 0; j < FRAMELEN; j++) { + ui8_t b = frame_bytes[j]; + if (option_pn9) { + if (j >= 6) b ^= PN9b[(j-6)%64]; + } + xframe[j] = b; + } + + chkval = xor8sum(xframe+ofs, 53); + chkdat = (xframe[ofs+53]<<8) | xframe[ofs+53+1]; + chk_ok = (chkdat == chkval); + + if (option_raw) { + if (option_raw == 1) { + for (j = 0; j < FRAMELEN; j++) { + //printf("%02X ", frame_bytes[j]); + printf("%02X ", xframe[j]); + } + printf(" # %s", chk_ok ? "[OK]" : "[NO]"); + if (option_verbose) printf(" # [%04X:%04X]", chkdat, chkval); + } + else { + for (j = 0; j < BITFRAMELEN; j++) { + printf("%c", frame_bits[j]); + //if (j % 8 == 7) printf(" "); + } + } + printf("\n"); + } + else { + + ui32_t sn; + ui32_t cnt; + int val; + + // SN + sn = xframe[ofs] | (xframe[ofs+1]<<8) | (xframe[ofs+2]<<16) | (xframe[ofs+3]<<24); + + // counter + cnt = xframe[ofs+4] | (xframe[ofs+5]<<8); + + ui8_t frid = xframe[ofs+6]; + + if (frid == 1) + { + gpx.chk1ok = chk_ok; + gpx.sn1 = sn; + gpx.cnt1 = cnt; + + if (option_verbose) { + + printf(" (%u) ", sn); //printf(" (0x%08X) ", sn); + printf(" [%5d] ", cnt); + + printf(" %s", chk_ok ? "[OK]" : "[NO]"); + if (option_verbose) printf(" # [%04X:%04X]", chkdat, chkval); + + printf("\n"); + } + } + else if (frid == 2) + { + gpx.chk2ok = chk_ok; + gpx.sn2 = sn; + gpx.cnt2 = cnt; + + // SN + printf(" (%u) ", sn); //printf(" (0x%08X) ", sn); + + // counter + printf(" [%5d] ", cnt); + + // time/UTC + int hms; + hms = xframe[ofs+7] | (xframe[ofs+8]<<8) | (xframe[ofs+9]<<16); + hms &= 0x3FFFF; + //printf(" (%6d) ", hms); + ui8_t h = hms / 10000; + ui8_t m = (hms % 10000) / 100; + ui8_t s = hms % 100; + printf(" %02d:%02d:%02d ", h, m, s); // UTC + gpx.hrs = h; + gpx.min = m; + gpx.sec = s; + + // alt + val = xframe[ofs+13] | (xframe[ofs+14]<<8) | (xframe[ofs+15]<<16); + val >>= 4; + val &= 0x7FFFF; // int19 ? + //if (val & 0x40000) val -= 0x80000; ?? or sign bit ? + float alt = val / 10.0f; + printf(" alt: %.1f ", alt); // MSL + gpx.alt = alt; + int val_alt = val; + + // lat + val = xframe[ofs+15] | (xframe[ofs+16]<<8) | (xframe[ofs+17]<<16) | (xframe[ofs+18]<<24); + val >>= 7; + val &= 0x1FFFFFF; // int25 ? ?? sign NMEA N/S ? + //if (val & 0x1000000) val -= 0x2000000; // sign bit ? (or 90 -> -90 wrap ?) + float lat = val / 1e5f; + printf(" lat: %.4f ", lat); + gpx.lat = lat; + int val_lat = val; + + // lon + val = xframe[ofs+19] | (xframe[ofs+20]<<8) | (xframe[ofs+21]<<16)| (xframe[ofs+22]<<24); + val &= 0x3FFFFFF; // int26 ? ?? sign NMEA E/W ? + //if (val & 0x2000000) val -= 0x4000000; // or sign bit ? (or 180 -> -180 wrap ?) + float lon = val / 1e5f; + printf(" lon: %.4f ", lon); + gpx.lon = lon; + int val_lon = val; + + int zero_pos = val_alt == 0 && val_lat == 0 && val_lon == 0; + + // checksum + printf(" %s", chk_ok ? "[OK]" : "[NO]"); + if (option_verbose) printf(" # [%04X:%04X]", chkdat, chkval); + + printf("\n"); + + // JSON + if (option_json && gpx.chk2ok && !zero_pos) { + if (gpx.chk1ok && gpx.sn2 == gpx.sn1 && gpx.cnt2 == gpx.cnt1) // double check, unreliable checksums + { + char *ver_jsn = NULL; + fprintf(stdout, "{ \"type\": \"%s\"", "WXR301"); + fprintf(stdout, ", \"frame\": %u", gpx.cnt2); + fprintf(stdout, ", \"id\": \"WXR-%u\"", gpx.sn2); + fprintf(stdout, ", \"datetime\": \"%02d:%02d:%02dZ\", \"lat\": %.5f, \"lon\": %.5f, \"alt\": %.2f", + gpx.hrs, gpx.min, gpx.sec, gpx.lat, gpx.lon, gpx.alt); + + // if data from subframe1, + // check gpx.chk1ok && gpx.sn1==gpx.sn2 && gpx.cnt1==gpx.cnt2 + + if (option_pn9) { + fprintf(stdout, ", \"subtype\": \"WXR_PN9\""); + } + + if (gpx.jsn_freq > 0) { + fprintf(stdout, ", \"freq\": %d", gpx.jsn_freq ); + } + + // Reference time/position + // (WxR-301D PN9) + fprintf(stdout, ", \"ref_datetime\": \"%s\"", "UTC" ); // {"GPS", "UTC"} GPS-UTC=leap_sec + fprintf(stdout, ", \"ref_position\": \"%s\"", "MSL" ); // {"GPS", "MSL"} GPS=ellipsoid , MSL=geoid + + #ifdef VER_JSN_STR + ver_jsn = VER_JSN_STR; + #endif + if (ver_jsn && *ver_jsn != '\0') fprintf(stdout, ", \"version\": \"%s\"", ver_jsn); + fprintf(stdout, " }\n"); + fprintf(stdout, "\n"); + } + } + + } + } + + return 0; +} + +int main(int argc, char **argv) { + + FILE *fp; + char *fpname; + + int i, j, h, bit, len; + int bit_count, frames; + int header_found = 0; + int cfreq = -1; + + char *hdr = header; + + fpname = argv[0]; + ++argv; + while ((*argv) && (!wavloaded)) { + if ( (strcmp(*argv, "-h") == 0) || (strcmp(*argv, "--help") == 0) ) { + fprintf(stderr, "%s [options] audio.wav\n", fpname); + fprintf(stderr, " options:\n"); + fprintf(stderr, " -i\n"); + fprintf(stderr, " -b\n"); + return 0; + } + else if (strcmp(*argv, "--pn9") == 0) { option_pn9 = 1; } + else if ( (strcmp(*argv, "-i") == 0) || (strcmp(*argv, "--invert") == 0) ) { + option_inv = 1; + } + else if ( (strcmp(*argv, "-v") == 0) || (strcmp(*argv, "--verbose") == 0) ) { + option_verbose = 1; + } + else if (strcmp(*argv, "--softin") == 0) { option_softin = 1; } // float32 soft input + else if (strcmp(*argv, "-b" ) == 0) { option_b = 1; } + else if (strcmp(*argv, "-t" ) == 0) { option_timestamp = 1; } + else if ( (strcmp(*argv, "-r") == 0) || (strcmp(*argv, "--raw") == 0) ) { + option_raw = 1; + } + else if ( (strcmp(*argv, "-R") == 0) || (strcmp(*argv, "--RAW") == 0) ) { + option_raw = 2; + } + else if ( (strcmp(*argv, "--json") == 0) ) { + option_json = 1; + } + else if ( (strcmp(*argv, "--jsn_cfq") == 0) ) { + int frq = -1; // center frequency / Hz + ++argv; + if (*argv) frq = atoi(*argv); else return -1; + if (frq < 300000000) frq = -1; + cfreq = frq; + } + else { + fp = fopen(*argv, "rb"); + if (fp == NULL) { + fprintf(stderr, "%s konnte nicht geoeffnet werden\n", *argv); + return -1; + } + wavloaded = 1; + } + ++argv; + } + if (!wavloaded) fp = stdin; + + if (option_pn9) { + baudrate = BAUD_RATE_PN9; + hdr = header_pn9; + ofs = OFS_PN9; + } + + if ( !option_softin ) { + i = read_wav_header(fp); + if (i) return -1; + } + + + if (cfreq > 0) gpx.jsn_freq = (cfreq+500)/1000; + + + bit_count = 0; + frames = 0; + + if (option_softin) + { + float s = 0.0f; + int bit = 0; + sample_rate = baudrate; + sample_count = 0; + + while (!f32soft_read(fp, &s)) { + + bit = option_inv ? (s<=0.0f) : (s>=0.0f); // softbit s: bit=0 <=> s<0 , bit=1 <=> s>=0 + + bufpos--; + if (bufpos < 0) bufpos = HEADLEN-1; + buf[bufpos] = 0x30 + bit; + + if (!header_found) + { + h = compare(hdr); //h2 = compare2(hdr); + if ((h >= HEADLEN)) { + header_found = 1; + fflush(stdout); + if (option_timestamp) printf("<%8.3f> ", sample_count/(double)sample_rate); + strncpy(frame_bits, hdr, HEADLEN); + bit_count += HEADLEN; + frames++; + } + } + else + { + frame_bits[bit_count] = 0x30 + bit; + bit_count += 1; + } + + if (bit_count >= BITFRAMELEN) { + bit_count = 0; + header_found = 0; + + print_frame(); + } + sample_count += 1; + } + } + else + { + while (!read_bits_fsk(fp, &bit, &len)) { + + if (len == 0) { + bufpos--; + if (bufpos < 0) bufpos = HEADLEN-1; + buf[bufpos] = 'x'; + continue; + } + + + for (j = 0; j < len; j++) { + bufpos--; + if (bufpos < 0) bufpos = HEADLEN-1; + buf[bufpos] = 0x30 + bit; + + if (!header_found) + { + h = compare(hdr); //h2 = compare2(hdr); + if ((h >= HEADLEN)) { + header_found = 1; + fflush(stdout); + if (option_timestamp) printf("<%8.3f> ", sample_count/(double)sample_rate); + strncpy(frame_bits, hdr, HEADLEN); + bit_count += HEADLEN; + frames++; + } + } + else + { + frame_bits[bit_count] = 0x30 + bit; + bit_count += 1; + } + + if (bit_count >= BITFRAMELEN) { + bit_count = 0; + header_found = 0; + + print_frame(); + } + + } + if (header_found && option_b) { + bitstart = 1; + + while ( bit_count < BITFRAMELEN ) { + if (read_rawbit(fp, &bit) == EOF) break; + frame_bits[bit_count] = 0x30 + bit; + bit_count += 1; + } + + bit_count = 0; + header_found = 0; + + print_frame(); + } + } + } + + printf("\n"); + + fclose(fp); + + return 0; +} +