diff --git a/.gitignore b/.gitignore index 8f1413f..62ec1fe 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ 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 diff --git a/auto_rx/auto_rx.py b/auto_rx/auto_rx.py index 204374c..83610a4 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 @@ -44,6 +54,7 @@ from autorx.web import ( start_flask, stop_flask, flask_emit_event, + flask_running, WebHandler, WebExporter, ) @@ -322,7 +333,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:] @@ -806,6 +817,11 @@ def main(): logging.getLogger("engineio").setLevel(logging.ERROR) logging.getLogger("geventwebsocket").setLevel(logging.ERROR) + # Check all the RS utilities exist. + logging.debug("Checking if utils exist") + if not check_rs_utils(): + sys.exit(1) + # Attempt to read in config file logging.info("Reading configuration file...") _temp_cfg = read_auto_rx_config(args.config) @@ -844,9 +860,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 @@ -897,6 +910,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"], ) @@ -1073,7 +1087,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 +1098,13 @@ if __name__ == "__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.sh b/auto_rx/auto_rx.sh index a8a1b56..6c901cd 100755 --- a/auto_rx/auto_rx.sh +++ b/auto_rx/auto_rx.sh @@ -6,7 +6,9 @@ # 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 @@ -15,7 +17,4 @@ 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 55fe294..375b8a8 100644 --- a/auto_rx/autorx/__init__.py +++ b/auto_rx/autorx/__init__.py @@ -12,7 +12,7 @@ from queue import Queue # 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" +__version__ = "1.6.2" # Global Variables diff --git a/auto_rx/autorx/aprs.py b/auto_rx/autorx/aprs.py index eb86449..747eeaa 100644 --- a/auto_rx/autorx/aprs.py +++ b/auto_rx/autorx/aprs.py @@ -759,13 +759,19 @@ class APRSUploader(object): # 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. diff --git a/auto_rx/autorx/config.py b/auto_rx/autorx/config.py index 8aa77fc..ff04798 100644 --- a/auto_rx/autorx/config.py +++ b/auto_rx/autorx/config.py @@ -683,8 +683,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 +748,17 @@ 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 + # If we are being called as part of a unit test, just return the config now. if no_sdr_test: @@ -872,7 +883,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 +902,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 1e77251..bf46e39 100644 --- a/auto_rx/autorx/decode.py +++ b/auto_rx/autorx/decode.py @@ -23,6 +23,7 @@ from .gps import get_ephemeris, get_almanac 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 = [ @@ -1424,6 +1425,20 @@ class SondeDecoder(object): "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.utcnow() + _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 diff --git a/auto_rx/autorx/email_notification.py b/auto_rx/autorx/email_notification.py index 0b2db5a..6e66b4b 100644 --- a/auto_rx/autorx/email_notification.py +++ b/auto_rx/autorx/email_notification.py @@ -43,6 +43,7 @@ class EmailNotification(object): 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 @@ class EmailNotification(object): 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 @@ class EmailNotification(object): 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 @@ class EmailNotification(object): } ) - if self.launch_notifications: + 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: 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 @@ class EmailNotification(object): _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 @@ class EmailNotification(object): # 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 @@ class EmailNotification(object): 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 @@ class EmailNotification(object): 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. @@ -433,6 +467,29 @@ if __name__ == "__main__": } ) + 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.utcnow(), + "encrypted": True + } + ) + # Wait a little bit before shutting down. time.sleep(5) diff --git a/auto_rx/autorx/gpsd.py b/auto_rx/autorx/gpsd.py index bdd8d8e..ac19782 100644 --- a/auto_rx/autorx/gpsd.py +++ b/auto_rx/autorx/gpsd.py @@ -335,7 +335,9 @@ class GPSDAdaptor(object): 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 index ebb4303..f76c429 100644 --- a/auto_rx/autorx/habitat.py +++ b/auto_rx/autorx/habitat.py @@ -831,13 +831,20 @@ class HabitatUploader(object): # 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("habitat 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("habitat 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("habitat 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/log_files.py b/auto_rx/autorx/log_files.py index 6a595bf..95eefde 100644 --- a/auto_rx/autorx/log_files.py +++ b/auto_rx/autorx/log_files.py @@ -455,9 +455,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 diff --git a/auto_rx/autorx/ozimux.py b/auto_rx/autorx/ozimux.py index a3c8ce8..685a691 100644 --- a/auto_rx/autorx/ozimux.py +++ b/auto_rx/autorx/ozimux.py @@ -252,7 +252,9 @@ class OziUploader(object): 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 5301cf8..82ee374 100644 --- a/auto_rx/autorx/rotator.py +++ b/auto_rx/autorx/rotator.py @@ -320,7 +320,9 @@ class Rotator(object): 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 e23e317..fe71746 100644 --- a/auto_rx/autorx/scan.py +++ b/auto_rx/autorx/scan.py @@ -9,6 +9,7 @@ import datetime import logging import numpy as np import os +import sys import platform import subprocess import time @@ -22,6 +23,7 @@ from .utils import ( 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 @@ -91,18 +93,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, @@ -314,7 +308,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 +325,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, @@ -360,7 +355,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 +374,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, @@ -783,9 +779,9 @@ class SondeScanner(object): 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!") @@ -854,22 +850,32 @@ class SondeScanner(object): sdr_hostname = self.sdr_hostname, sdr_port = self.sdr_port ) - - 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. @@ -1139,12 +1145,16 @@ class SondeScanner(object): 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 62af89d..2b187ad 100644 --- a/auto_rx/autorx/sdr_wrappers.py +++ b/auto_rx/autorx/sdr_wrappers.py @@ -11,7 +11,7 @@ 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 def test_sdr( @@ -67,7 +67,7 @@ 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 " @@ -480,17 +480,7 @@ def get_power_spectrum( 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: @@ -564,17 +554,7 @@ def get_power_spectrum( 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) diff --git a/auto_rx/autorx/static/css/autorx.css b/auto_rx/autorx/static/css/autorx.css index fcb0cd0..2a99920 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 8897ece..340cfcf 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,8 +10,14 @@ function update_task_list(){ added_decoders = false; for (_task in data){ - // Append the current task to the task list text. - task_info += "SDR #" + _task + ": " + data[_task]["task"] + " "; + // Append the current task to the task list. + if(_task.includes("SPY")){ + task_detail = _task + " - " + }else{ + task_detail = "SDR:" + _task + " - " + } + + if(data[_task]["freq"] > 0.0){ $('#stop-frequency-select') .append($("") @@ -19,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){ @@ -30,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); }); @@ -96,7 +117,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. @@ -125,7 +146,7 @@ function disable_scanner(){ // Do the request $.post( - "/disable_scanner", + "disable_scanner", {"password": _api_password}, function(data){ //console.log(data); @@ -162,7 +183,7 @@ function enable_scanner(){ // Do the request $.post( - "/enable_scanner", + "enable_scanner", {"password": _api_password}, function(data){ //console.log(data); @@ -194,7 +215,7 @@ function stop_decoder(){ // Do the request $.post( - "/stop_decoder", + "stop_decoder", {password: _api_password, freq: _decoder}, function(data){ //console.log(data); @@ -245,7 +266,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.") @@ -259,4 +280,4 @@ function start_decoder(){ $("#password-header").html("

Incorrect Password

"); } }); -} \ No newline at end of file +} diff --git a/auto_rx/autorx/static/js/utils.js b/auto_rx/autorx/static/js/utils.js index 8ecb224..270281c 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/templates/historical.html b/auto_rx/autorx/templates/historical.html index 0355190..95d7b57 100644 --- a/auto_rx/autorx/templates/historical.html +++ b/auto_rx/autorx/templates/historical.html @@ -46,12 +46,13 @@ 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); - $.ajax({ // Get station.cfg file. - url: "/get_config", + url: "get_config", dataType: 'json', async: false, success: function(data) { @@ -64,7 +65,7 @@ $.ajax({ // Get list of sonde. - url: "/get_log_list", + url: "get_log_list", dataType: 'json', async: false, success: function(data) { @@ -462,7 +463,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) { @@ -731,7 +732,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]; @@ -746,7 +747,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]; @@ -770,7 +771,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); @@ -857,7 +858,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 { @@ -947,13 +948,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'); } } } @@ -1529,4 +1530,4 @@ - \ No newline at end of file + diff --git a/auto_rx/autorx/templates/index.html b/auto_rx/autorx/templates/index.html index b2189ac..2bf751d 100644 --- a/auto_rx/autorx/templates/index.html +++ b/auto_rx/autorx/templates/index.html @@ -99,13 +99,13 @@ $( document ).ready(function() { - namespace = '/update_status'; - - var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace); - + namespace = '/update_status'; + var socket_path = "{{ url_for("static", filename="") }}".replace('static/', 'socket.io') + var socket = io.connect(location.origin+namespace, {'path': socket_path}); + $.ajax({ // Get station.cfg file. - url: "/get_config", + url: "get_config", dataType: 'json', async: false, success: function(data) { @@ -129,7 +129,7 @@ $.ajax({ // Get local version number. - url: "/get_version", + url: "get_version", dataType: 'json', async: true, success: function(data) { @@ -196,7 +196,7 @@ socket.on('scan_event', function(msg) { // There is Scan data ready for us! // Grab the latest set of data. - $.getJSON("/get_scan_data", function(data){ + $.getJSON("get_scan_data", function(data){ // Load the data into our data stores. scan_chart_spectra.columns[0] = ['x_spectra'].concat(data.freq); scan_chart_spectra.columns[1] = ['Spectra'].concat(data.power); @@ -736,7 +736,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 +1571,7 @@

Live KML

 
- +


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

Historical View

 
- +


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

Station: ???

-

Current Task: ???

+

Tasking:

diff --git a/auto_rx/autorx/templates/skewt_test.html b/auto_rx/autorx/templates/skewt_test.html index 7fe28d6..ea2ceba 100644 --- a/auto_rx/autorx/templates/skewt_test.html +++ b/auto_rx/autorx/templates/skewt_test.html @@ -59,7 +59,7 @@ _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 8e502fa..9a016af 100644 --- a/auto_rx/autorx/utils.py +++ b/auto_rx/autorx/utils.py @@ -19,6 +19,7 @@ import threading 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 @@ -45,6 +46,21 @@ REQUIRED_RS_UTILS = [ "iq_dec" ] +_timeout_cmd = None + +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(): """ Check the required RS decoder binaries exist @@ -54,7 +70,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 @@ -776,10 +792,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 +869,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 +922,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. diff --git a/auto_rx/autorx/web.py b/auto_rx/autorx/web.py index 4457d4e..34a40b9 100644 --- a/auto_rx/autorx/web.py +++ b/auto_rx/autorx/web.py @@ -21,11 +21,13 @@ 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.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 +from werkzeug.middleware.proxy_fix import ProxyFix import re try: @@ -43,13 +45,14 @@ cli.show_server_banner = lambda *x: None # 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 +108,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 +128,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) @@ -139,7 +150,7 @@ def flask_get_kml(): kml = Kml() netlink = kml.newnetworklink(name="Radiosonde Auto-RX Live Telemetry") netlink.open = 1 - netlink.link.href = flask.request.host_url + "rs_feed.kml" + netlink.link.href = flask.request.url_root + "rs_feed.kml" try: netlink.link.refreshinterval = _config["kml_refresh_rate"] except KeyError: @@ -162,7 +173,7 @@ def flask_get_kml_feed(): description="AutoRX Ground Station", ) pnt.open = 1 - pnt.iconstyle.icon.href = flask.request.host_url + "static/img/antenna-green.png" + pnt.iconstyle.icon.href = flask.request.url_root + "static/img/antenna-green.png" pnt.coords = [ ( autorx.config.global_config["station_lon"], @@ -183,15 +194,15 @@ 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) @@ -289,6 +300,9 @@ def flask_get_log_list(): """ Return a list of log files, as a list of objects """ return json.dumps(list_log_files(quicklook=True)) +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): diff --git a/auto_rx/requirements.txt b/auto_rx/requirements.txt index c9ebba6..3f1bb37 100644 --- a/auto_rx/requirements.txt +++ b/auto_rx/requirements.txt @@ -6,3 +6,4 @@ numpy requests semver simplekml +simple-websocket diff --git a/auto_rx/station.cfg.example b/auto_rx/station.cfg.example index c5dbca5..081f3e5 100644 --- a/auto_rx/station.cfg.example +++ b/auto_rx/station.cfg.example @@ -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 diff --git a/auto_rx/station.cfg.example.network b/auto_rx/station.cfg.example.network index a006261..d82cebd 100644 --- a/auto_rx/station.cfg.example.network +++ b/auto_rx/station.cfg.example.network @@ -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 diff --git a/demod/mod/m20mod.c b/demod/mod/m20mod.c index 17897ca..9d496e8 100644 --- a/demod/mod/m20mod.c +++ b/demod/mod/m20mod.c @@ -104,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]; @@ -193,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 */ @@ -218,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 @@ -250,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} @@ -701,18 +708,33 @@ 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 } 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) { @@ -741,6 +763,8 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { gpx->P = get_P(gpx); // (optional) pressure } + gpx->batV = get_BatV(gpx); // battery V + if ( !gpx->option.slt ) { if (gpx->option.col) { @@ -763,10 +787,11 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { } 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 (gpx->fwVer < 0x07) { + 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); } @@ -778,10 +803,14 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { 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 < 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, ANSI_COLOR_RESET""); } else { @@ -803,11 +832,12 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { } 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 (gpx->fwVer < 0x07) { + //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 (gpx->option.ptu && csOK) { @@ -818,10 +848,14 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { 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 < 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, "\n"); } @@ -846,6 +880,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) { @@ -876,6 +911,8 @@ static int print_frame(gpx_t *gpx, int pos, int b2B) { int cs1, cs2; int bc1, bc2, bc; int flen = stdFLEN; // stdFLEN=0x64, auxFLEN=0x76; M20:0x45 ? + int pos_fw = pos_stdFW; + int pos_check = pos_stdCheck; if (b2B) { bits2bytes(gpx->frame_bits, gpx->frame_bytes); @@ -885,10 +922,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 @@ -921,16 +969,27 @@ static int print_frame(gpx_t *gpx, int pos, int b2B) { 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 (gpx->fwVer < 0x07) { + if ((i >= pos_BlkChk) && (i < pos_BlkChk+2)) fprintf(stdout, col_Check); + } else { + if ((i >= pos_BlkChk+1) && (i < pos_BlkChk+2)) fprintf(stdout, col_Check); + } + if (i >= 0x02 && i <= 0x03) fprintf(stdout, col_ptuU); + if (i >= 0x04 && i <= 0x05) fprintf(stdout, col_ptuT); + if (i >= 0x06 && i <= 0x07) fprintf(stdout, col_ptuTH); + if (i == 0x16 && gpx->fwVer >= 0x07 || i >= 0x24 && i <= 0x25) fprintf(stdout, col_ptuP); + + if ((i >= pos_check) && (i < pos_check+2)) fprintf(stdout, col_Check); fprintf(stdout, "%02x", byte); fprintf(stdout, 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 (gpx->fwVer < 0x07) { + 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); } @@ -943,9 +1002,11 @@ 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");