diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 9e644684..f9d69724 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -18,46 +18,42 @@ jobs: - name: Calculate Container Metadata id: meta - uses: crazy-max/ghaction-docker-meta@v1 + uses: docker/metadata-action@v4 with: images: ghcr.io/${{ github.repository }} - tag-semver: | - {{version}} - name: Setup QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Setup Buildx - uses: docker/setup-buildx-action@v1 - - - name: Cache Layers - uses: actions/cache@v2 - with: - path: /tmp/buildx-cache - key: buildx-cache-${{ github.sha }} - restore-keys: | - buildx-cache- + uses: docker/setup-buildx-action@v2 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - if: github.event_name != 'pull_request' + if: ${{ github.event_name != 'pull_request' }} + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build Images + if: ${{ github.event_name == 'pull_request' }} + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64, linux/386, linux/arm64, linux/arm/v6, linux/arm/v7 + cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + - name: Build and Push Images - uses: docker/build-push-action@v2 + if: ${{ github.event_name != 'pull_request' }} + uses: docker/build-push-action@v3 with: context: . platforms: linux/amd64, linux/386, linux/arm64, linux/arm/v6, linux/arm/v7 - cache-from: type=local,src=/tmp/buildx-cache - cache-to: type=local,dest=/tmp/buildx-cache-new,mode=max - push: ${{ github.event_name != 'pull_request' }} + cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache + cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - - - name: Move Cache - run: | - rm -rf /tmp/buildx-cache - mv /tmp/buildx-cache-new /tmp/buildx-cache diff --git a/.gitignore b/.gitignore index 012cb969..8f1413f5 100644 --- a/.gitignore +++ b/.gitignore @@ -44,10 +44,14 @@ auto_rx/imet1rs_dft auto_rx/lms6Xmod auto_rx/lms6mod auto_rx/m10mod +auto_rx/m20mod +auto_rx/mk2a1680mod auto_rx/rs_detect +auto_rx/imet4iq auto_rx/imet54mod auto_rx/mXXmod auto_rx/mp3h1mod +auto_rx/mts01mod m10 meisei100mod @@ -60,12 +64,15 @@ demod/mod/meisei100mod demod/mod/rs41mod demod/mod/rs92mod demod/mod/m10mod +demod/mod/m20mod demod/mod/imet54mod demod/mod/mXXmod demod/mod/mp3h1mod imet/imet1rs_dft +imet/imet4iq m10/m10 mk2a/mk2a_lms1680 +mk2a/mk2a1680mod scan/dft_detect utils/fsk_demod rs41/rs41ecc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b3ee3462 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-executables-have-shebangs diff --git a/Dockerfile b/Dockerfile index fc4d6fe2..ca1f6ee9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # ------------------- # The build container # ------------------- -FROM debian:buster-slim AS build +FROM debian:bullseye-slim AS build # Upgrade base packages. RUN apt-get update && \ @@ -11,6 +11,7 @@ RUN apt-get update && \ cmake \ git \ libatlas-base-dev \ + libsamplerate0-dev \ libusb-1.0-0-dev \ pkg-config \ python3 \ @@ -20,6 +21,15 @@ RUN apt-get update && \ python3-wheel && \ rm -rf /var/lib/apt/lists/* +# Copy in requirements.txt. +COPY auto_rx/requirements.txt \ + /root/radiosonde_auto_rx/auto_rx/requirements.txt + +# Install Python packages. +RUN --mount=type=cache,target=/root/.cache/pip pip3 install \ + --user --no-warn-script-location --ignore-installed --no-binary numpy \ + -r /root/radiosonde_auto_rx/auto_rx/requirements.txt + # Compile rtl-sdr from source. RUN git clone https://github.com/steve-m/librtlsdr.git /root/librtlsdr && \ mkdir -p /root/librtlsdr/build && \ @@ -29,26 +39,22 @@ RUN git clone https://github.com/steve-m/librtlsdr.git /root/librtlsdr && \ make install && \ rm -rf /root/librtlsdr -# Copy in requirements.txt. -COPY auto_rx/requirements.txt \ - /root/radiosonde_auto_rx/auto_rx/requirements.txt - -# Install Python packages. -RUN --mount=type=cache,target=/root/.cache/pip pip3 install \ - --user --no-warn-script-location --ignore-installed --no-binary numpy \ - -r /root/radiosonde_auto_rx/auto_rx/requirements.txt +# Compile spyserver_client from source. +RUN git clone https://github.com/miweber67/spyserver_client.git /root/spyserver_client && \ + cd /root/spyserver_client && \ + make # Copy in radiosonde_auto_rx. COPY . /root/radiosonde_auto_rx -# Build the binaries. +# Build the radiosonde_auto_rx binaries. WORKDIR /root/radiosonde_auto_rx/auto_rx RUN /bin/sh build.sh # ------------------------- # The application container # ------------------------- -FROM debian:buster-slim +FROM debian:bullseye-slim EXPOSE 5000/tcp @@ -58,6 +64,7 @@ RUN apt-get update && \ apt-get install -y --no-install-recommends \ libatlas3-base \ libatomic1 \ + libsamplerate0 \ python3 \ rng-tools \ sox \ @@ -76,6 +83,11 @@ COPY --from=build /root/.local /root/.local COPY --from=build /root/radiosonde_auto_rx/LICENSE /opt/auto_rx/ COPY --from=build /root/radiosonde_auto_rx/auto_rx/ /opt/auto_rx/ +# Copy ss_client from the build container and create links +COPY --from=build /root/spyserver_client/ss_client /opt/auto_rx/ +RUN ln -s ss_client /opt/auto_rx/ss_iq && \ + ln -s ss_client /opt/auto_rx/ss_power + # Set the working directory. WORKDIR /opt/auto_rx diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index 3523f3a5..00000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,48 +0,0 @@ -FROM debian:buster-slim - -EXPOSE 5000/tcp - -# Upgrade base packages and install dependencies. -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y --no-install-recommends \ - build-essential \ - cmake \ - git \ - libatlas-base-dev \ - libatomic1 \ - libsamplerate-dev \ - libusb-1.0-0-dev \ - pkg-config \ - python3 \ - python3-dev \ - python3-pip \ - python3-setuptools \ - rng-tools \ - sox \ - tini \ - usbutils && \ - rm -rf /var/lib/apt/lists/* - -# Compile rtl-sdr from source and install. -RUN git clone https://github.com/steve-m/librtlsdr.git /root/librtlsdr && \ - mkdir -p /root/librtlsdr/build && \ - cd /root/librtlsdr/build && \ - cmake -Wno-dev ../ && \ - make && \ - make install && \ - rm -rf /root/librtlsdr && \ - ldconfig - -# Copy in requirements.txt. -COPY auto_rx/requirements.txt \ - /tmp/requirements.txt - -# Install Python packages. -RUN --mount=type=cache,target=/root/.cache/pip pip3 install \ - --no-warn-script-location --no-binary numpy \ - -r /root/radiosonde_auto_rx/auto_rx/requirements.txt - -# Run bash. -WORKDIR /root -CMD ["/bin/bash"] diff --git a/Makefile b/Makefile index 15f5436b..71addf12 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,24 @@ -# Toplevel makefile to build all software +AUTO_RX_VERSION := $(shell PYTHONPATH=./auto_rx python3 -m autorx.version 2>/dev/null || python -m autorx.version) -PROGRAMS := scan/dft_detect demod/mod/rs41mod demod/mod/dfm09mod demod/mod/rs92mod demod/mod/lms6mod demod/mod/lms6Xmod demod/mod/meisei100mod demod/mod/m10mod demod/mod/mXXmod demod/mod/imet54mod mk2a/mk2a_lms1680 imet/imet1rs_dft utils/fsk_demod +# Uncomment to use clang as a compiler. +#CC = clang +#export CC -all: - $(MAKE) -C demod/mod - $(MAKE) -C imet - $(MAKE) -C utils - $(MAKE) -C scan - $(MAKE) -C mk2a - cp $(PROGRAMS) auto_rx/ +CFLAGS = -O3 -w -Wno-unused-variable -DVER_JSN_STR=\"$(AUTO_RX_VERSION)\" +export CFLAGS -.PHONY: -clean: - $(MAKE) -C demod/mod clean - $(MAKE) -C imet clean - $(MAKE) -C utils clean - $(MAKE) -C scan clean - $(MAKE) -C mk2a clean +SUBDIRS := \ + demod/mod \ + imet \ + mk2a \ + scan \ + utils \ + +all: $(SUBDIRS) + +clean: $(SUBDIRS) + +$(SUBDIRS): + make -C $@ $(MAKECMDGOALS) + +.PHONY: all clean $(SUBDIRS) diff --git a/README.md b/README.md index 33025acd..e7691e13 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ **Please refer to the [auto_rx wiki](https://github.com/projecthorus/radiosonde_auto_rx/wiki) for the latest information.** -This fork of [rs1279's RS](https://github.com/rs1729/RS) codebase provides a set of utilities ('auto_rx') to allow automatic reception and uploading of [Radiosonde](https://en.wikipedia.org/wiki/Radiosonde) positions to multiple services, including: +This project is built around [rs1279's RS](https://github.com/rs1729/RS) demodulators, and provides a set of utilities ('auto_rx') to allow automatic reception and uploading of [Radiosonde](https://en.wikipedia.org/wiki/Radiosonde) positions to multiple services, including: * The [SondeHub Radiosonde Tracker](https://tracker.sondehub.org) - a tracking website specifically designed for tracking radiosondes! -* APRS-IS (for display on sites such as [radiosondy.info](https://radiosondy.info) and [aprs.fi](https://aprs.fi) +* APRS-IS, for display on sites such as [radiosondy.info](https://radiosondy.info). (Note that aprs.fi now blocks radiosonde traffic.) * [ChaseMapper](https://github.com/projecthorus/chasemapper) for mobile radiosonde chasing. @@ -18,15 +18,16 @@ Manufacturer | Model | Position | Temperature | Humidity | Pressure | XDATA -------------|-------|----------|-------------|----------|----------|------ Vaisala | RS92-SGP/NGP | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: Vaisala | RS41-SG/SGP/SGM | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: (for -SGP) | :heavy_check_mark: -Graw | DFM06/09/17 | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | :x: +Graw | DFM06/09/17 | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: Meteomodem | M10 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | Not Sent | :x: Meteomodem | M20 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: (For some models) | :x: -Intermet Systems | iMet-1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | Not Sent | :heavy_check_mark: -Intermet Systems | iMet-4 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | Not Sent | :heavy_check_mark: +Intermet Systems | iMet-4 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: Intermet Systems | iMet-54 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | Not Sent | :x: Lockheed Martin | LMS6-400/1680 | :heavy_check_mark: | :x: | :x: | :x: | Not Sent -Meisei | iMS-100 | :heavy_check_mark: | :x: | :x: | :x: | Not Sent +Meisei | iMS-100 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | Not Sent +Meisei | RS11G | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | Not Sent Meteo-Radiy | MRZ-H1 (400 MHz) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | Not Sent +Meteosis | MTS01 | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | Not Sent Support for other radiosondes may be added as required - please send us sondes to test with! If you have any information about telemetry formats, we'd love to hear from you (see our contact details below). @@ -47,11 +48,10 @@ https://groups.google.com/forum/#!forum/radiosonde_auto_rx * [Mark Jessop](https://github.com/darksidelemm) - vk5qi@rfhead.net * [Michaela Wheeler](https://github.com/TheSkorm) - radiosonde@michaela.lgbt - ## Licensing Information All software within this repository is licensed under the GNU General Public License v3. Refer this repositories LICENSE file for the full license text. Radiosonde telemetry data captured via this software and uploaded into the [Sondehub](https://sondehub.org/) Database system is licensed under [Creative Commons BY-SA v2.0](https://creativecommons.org/licenses/by-sa/2.0/). Telemetry data uploaded into the APRS-IS network is generally considered to be released into the public domain. -By uploading data into these systems (by enabling the relevant uploaders within the `station.cfg` file) you as the user agree for your data to be made available under these licenses. Note that uploading to Sondehub is enabled by default. +By uploading data into these systems (by enabling the relevant uploaders within the `station.cfg` file) you as the user agree for your data to be made available under these licenses. Note that uploading to Sondehub is enabled by default. \ No newline at end of file diff --git a/auto_rx/auto_rx.py b/auto_rx/auto_rx.py index e2755fea..204374cc 100644 --- a/auto_rx/auto_rx.py +++ b/auto_rx/auto_rx.py @@ -17,6 +17,7 @@ import traceback import os from dateutil.parser import parse +from queue import Queue if sys.version_info < (3, 6): print("CRITICAL - radiosonde_auto_rx requires Python 3.6 or newer!") @@ -47,13 +48,7 @@ WebExporter, ) from autorx.gpsd import GPSDAdaptor - -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue +from autorx.sdr_wrappers import shutdown_sdr # Logging level @@ -162,9 +157,15 @@ def start_scanner(): detect_dwell_time=config["detect_dwell_time"], max_peaks=config["max_peaks"], rs_path=RS_PATH, - sdr_power=config["sdr_power"], - sdr_fm=config["sdr_fm"], - device_idx=_device_idx, + sdr_type=config["sdr_type"], + # Network SDR Options + sdr_hostname=config["sdr_hostname"], + sdr_port=config["sdr_port"], + ss_iq_path=config["ss_iq_path"], + ss_power_path=config["ss_power_path"], + rtl_power_path=config["sdr_power"], + rtl_fm_path=config["sdr_fm"], + rtl_device_idx=_device_idx, gain=autorx.sdr_list[_device_idx]["gain"], ppm=autorx.sdr_list[_device_idx]["ppm"], bias=autorx.sdr_list[_device_idx]["bias"], @@ -199,12 +200,13 @@ def stop_scanner(): autorx.task_list.pop("SCAN") -def start_decoder(freq, sonde_type): +def start_decoder(freq, sonde_type, continuous=False): """Attempt to start a decoder thread for a given sonde. Args: freq (float): Radiosonde frequency in Hz. sonde_type (str): The radiosonde type ('RS41', 'RS92', 'DFM', 'M10, 'iMet') + continuous (bool): If true, don't use a decode timeout. """ global config, RS_PATH, exporter_functions, rs92_ephemeris, temporary_block_list @@ -229,20 +231,32 @@ def start_decoder(freq, sonde_type): else: _exp_sonde_type = sonde_type + if continuous: + _timeout = 0 + else: + _timeout = config["rx_timeout"] + # Initialise a decoder. autorx.task_list[freq]["task"] = SondeDecoder( sonde_type=sonde_type, sonde_freq=freq, rs_path=RS_PATH, - sdr_fm=config["sdr_fm"], - device_idx=_device_idx, + sdr_type=config["sdr_type"], + # Network SDR Options + sdr_hostname=config["sdr_hostname"], + sdr_port=config["sdr_port"], + ss_iq_path=config["ss_iq_path"], + # RTLSDR Options + rtl_fm_path=config["sdr_fm"], + rtl_device_idx=_device_idx, gain=autorx.sdr_list[_device_idx]["gain"], ppm=autorx.sdr_list[_device_idx]["ppm"], bias=autorx.sdr_list[_device_idx]["bias"], + # Other options save_decode_audio=config["save_decode_audio"], save_decode_iq=config["save_decode_iq"], exporter=exporter_functions, - timeout=config["rx_timeout"], + timeout=_timeout, telem_filter=telemetry_filter, rs92_ephemeris=rs92_ephemeris, rs41_drift_tweak=config["rs41_drift_tweak"], @@ -277,6 +291,28 @@ def handle_scan_results(): continue else: + # Handle an inverted sonde detection. + if _type.startswith("-"): + _inverted = " (Inverted)" + _check_type = _type[1:] + else: + _check_type = _type + _inverted = "" + + # Note: We don't indicate if it's been detected as inverted here. + logging.info( + "Task Manager - Detected new %s sonde on %.3f MHz!" + % (_check_type, _freq / 1e6) + ) + + # Break if we don't support this sonde type. + if _check_type not in VALID_SONDE_TYPES: + logging.warning( + "Task Manager - Unsupported sonde type: %s" % _check_type + ) + # TODO - Potentially add the frequency of the unsupported sonde to the temporary block list? + continue + # Check that we are not attempting to start a decoder too close to an existing decoder for known 'drifty' radiosonde types. # 'Too close' is defined by the 'decoder_spacing_limit' advanced coniguration option. _too_close = False @@ -286,10 +322,14 @@ 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:] # Only check the frequency spacing if we have a known 'drifty' sonde type, *and* the new sonde type is of the same type. if (_decoding_sonde_type in DRIFTY_SONDE_TYPES) and ( - _decoding_sonde_type == _type + _decoding_sonde_type == _check_type ): if abs(_key - _freq) < config["decoder_spacing_limit"]: # At this point, we can be pretty sure that there is another decoder already decoding this particular sonde ID. @@ -328,27 +368,6 @@ def handle_scan_results(): ) temporary_block_list.pop(_freq) - # Handle an inverted sonde detection. - if _type.startswith("-"): - _inverted = " (Inverted)" - _check_type = _type[1:] - else: - _check_type = _type - _inverted = "" - - # Note: We don't indicate if it's been detected as inverted here. - logging.info( - "Task Manager - Detected new %s sonde on %.3f MHz!" - % (_check_type, _freq / 1e6) - ) - - # Break if we don't support this sonde type. - if _check_type not in VALID_SONDE_TYPES: - logging.warning( - "Task Manager - Unsupported sonde type: %s" % _check_type - ) - # TODO - Potentially add the frequency of the unsupported sonde to the temporary block list? - continue if allocate_sdr(check_only=True) is not None: # There is a SDR free! Start the decoder on that SDR @@ -412,6 +431,8 @@ def clean_task_list(): email_error(_error_msg) else: + # Shutdown the SDR, if required for the particular SDR type. + shutdown_sdr(config["sdr_type"], _task_sdr) # Release its associated SDR. autorx.sdr_list[_task_sdr]["in_use"] = False autorx.sdr_list[_task_sdr]["task"] = None @@ -443,6 +464,26 @@ def clean_task_list(): # We have a SDR free, and we are not running a scan thread. Start one. start_scanner() + # Always-on decoders. + if len(config["always_decode"]) > 0: + for _entry in config["always_decode"]: + try: + _freq_hz = float(_entry[0])*1e6 + _type = str(_entry[1]) + except: + logging.warning(f"Task Manager - Invalid entry found in always_decode list, skipping.") + continue + + if _freq_hz in autorx.task_list: + # Already running a decoder here. + continue + else: + # Try and start up a decoder. + if (allocate_sdr(check_only=True) is not None): + logging.info(f"Task Manager - Starting Always-On Decoder: {_type}, {_freq_hz/1e6:.3f} MHz") + start_decoder(_freq_hz, _type, continuous=True) + + def stop_all(): """Shut-down all decoders, scanners, and exporters.""" @@ -578,7 +619,7 @@ def telemetry_filter(telemetry): # Check Meisei sonde callsigns for validity. # meisei_ims returns a callsign of IMS100-xxxxxx until it receives the serial number, so we filter based on the x's being present or not. - if "MEISEI" in telemetry["type"]: + if "MEISEI" in telemetry["type"] or "IMS100" in telemetry["type"] or "RS11G" in telemetry["type"]: meisei_callsign_valid = "x" not in _serial.split("-")[1] else: meisei_callsign_valid = False @@ -588,7 +629,7 @@ def telemetry_filter(telemetry): else: mrz_callsign_valid = False - # If Vaisala or DFMs, check the callsigns are valid. If M10, iMet or LMS6, just pass it through - we get callsigns immediately and reliably from these. + # If Vaisala or DFMs, check the callsigns are valid. If M10/M20, iMet, MTS01 or LMS6, just pass it through - we get callsigns immediately and reliably from these. if ( vaisala_callsign_valid or dfm_callsign_valid @@ -598,6 +639,7 @@ def telemetry_filter(telemetry): or ("M20" in telemetry["type"]) or ("LMS" in telemetry["type"]) or ("IMET" in telemetry["type"]) + or ("MTS01" in telemetry["type"]) ): return "OK" else: @@ -731,6 +773,8 @@ def main(): _log_suffix = datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S_system.log") _log_path = os.path.join(logging_path, _log_suffix) + system_log_enabled = False + if args.systemlog: # Only write out a logs to a system log file if we have been asked to. # Systemd will capture and logrotate our logs anyway, so writing to our own log file is less useful. @@ -745,6 +789,7 @@ def main(): stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(stdout_format) logging.getLogger().addHandler(stdout_handler) + system_log_enabled = True else: # Otherwise, we only need the stdout logger, which if we don't specify a filename to logging.basicConfig, # is the default... @@ -752,9 +797,6 @@ def main(): format="%(asctime)s %(levelname)s:%(message)s", level=logging_level ) - # Add the web interface logging handler. - web_handler = WebHandler() - logging.getLogger().addHandler(web_handler) # Set the requests/socketio loggers (and related) to only display critical log messages. logging.getLogger("requests").setLevel(logging.CRITICAL) @@ -774,6 +816,34 @@ 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. + if system_log_enabled == False: + # Clear all existing handlers, and add new ones. + logging.basicConfig( + format="%(asctime)s %(levelname)s:%(message)s", + filename=_log_path, + level=logging_level, + force=True # This removes all existing handlers before adding new ones. + ) + # Also add a separate stdout logger. + stdout_format = logging.Formatter("%(asctime)s %(levelname)s:%(message)s") + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setFormatter(stdout_format) + logging.getLogger().addHandler(stdout_handler) + system_log_enabled = True + logging.info("Opened new system log file: %s" % _log_path) + + if config["enable_debug_logging"]: + # Set log level to logging.DEBUG + logging.getLogger().setLevel(logging.DEBUG) + logging.debug("Log level set to DEBUG based on configuration file setting.") + + # Add the web interface logging handler. + web_handler = WebHandler() + logging.getLogger().addHandler(web_handler) + # Check all the RS utilities exist. if not check_rs_utils(): sys.exit(1) @@ -819,6 +889,7 @@ def main(): mail_from=config["email_from"], mail_to=config["email_to"], mail_subject=config["email_subject"], + mail_nearby_landing_subject=config["email_nearby_landing_subject"], station_position=( config["station_lat"], config["station_lon"], diff --git a/auto_rx/autorx/__init__.py b/auto_rx/autorx/__init__.py index 22257866..7f052440 100644 --- a/auto_rx/autorx/__init__.py +++ b/auto_rx/autorx/__init__.py @@ -5,19 +5,14 @@ # Copyright (C) 2018 Mark Jessop # Released under GNU GPL v3 or later # -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue +from queue import Queue # Now using Semantic Versioning (https://semver.org/) MAJOR.MINOR.PATCH # MAJOR - Only updated when something huge changes to the project (new decode chain, etc) # 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.5.10" +__version__ = "1.6.0" # Global Variables diff --git a/auto_rx/autorx/aprs.py b/auto_rx/autorx/aprs.py index 9f4dbf9d..eb864496 100644 --- a/auto_rx/autorx/aprs.py +++ b/auto_rx/autorx/aprs.py @@ -11,17 +11,11 @@ import time import traceback import socket +from queue import Queue from threading import Thread, Lock from . import __version__ as auto_rx_version from .utils import strip_sonde_serial -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue - def telemetry_to_aprs_position( sonde_data, object_name="", aprs_comment="BOM Balloon", position_report=False diff --git a/auto_rx/autorx/config.py b/auto_rx/autorx/config.py index 62d78443..8aa77fc7 100644 --- a/auto_rx/autorx/config.py +++ b/auto_rx/autorx/config.py @@ -11,7 +11,8 @@ import os import traceback import json -from .utils import rtlsdr_test +from configparser import RawConfigParser +from .sdr_wrappers import test_sdr # Dummy initial config with some parameters we need to make the web interface happy. global_config = { @@ -26,13 +27,6 @@ # Web interface credentials web_password = "none" -try: - # Python 2 - from ConfigParser import RawConfigParser -except ImportError: - # Python 3 - from configparser import RawConfigParser - # 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. @@ -70,9 +64,15 @@ def read_auto_rx_config(filename, no_sdr_test=False): "email_from": "sonde@localhost", "email_to": None, "email_subject": " Sonde launch detected on : ", + "email_nearby_landing_subject": "Nearby Radiosonde Landing Detected - ", # SDR Settings + "sdr_type": "RTLSDR", + "sdr_hostname": "localhost", + "sdr_port": 5555, "sdr_fm": "rtl_fm", "sdr_power": "rtl_power", + "ss_iq_path": "./ss_iq", + "ss_power_path": "./ss_power", "sdr_quantity": 1, # Search Parameters "min_freq": 400.4, @@ -81,6 +81,7 @@ def read_auto_rx_config(filename, no_sdr_test=False): "only_scan": [], "never_scan": [], "always_scan": [], + "always_decode": [], # Location Settings "station_lat": 0.0, "station_lon": 0.0, @@ -162,6 +163,8 @@ def read_auto_rx_config(filename, no_sdr_test=False): "save_decode_audio": False, "save_decode_iq": 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. @@ -449,7 +452,8 @@ def read_auto_rx_config(filename, no_sdr_test=False): "IMET5": True, "LMS6": True, "MK2LMS": False, - "MEISEI": False, + "MEISEI": True, + "MTS01": False, # Until we test it "MRZ": False, # .... except for the MRZ, until we know it works. "UDP": False, } @@ -652,6 +656,16 @@ def read_auto_rx_config(filename, no_sdr_test=False): ) auto_rx_config["experimental_decoders"]["MK2LMS"] = False + try: + auto_rx_config["email_nearby_landing_subject"] = config.get( + "email", "nearby_landing_subject" + ) + except: + logging.warning( + "Config - Did not find email_nearby_landing_subject setting, using default" + ) + auto_rx_config["email_nearby_landing_subject"] = "Nearby Radiosonde Landing Detected - " + # As of auto_rx version 1.5.10, we are limiting APRS output to only radiosondy.info, # and only on the non-forwarding port. @@ -685,45 +699,151 @@ def read_auto_rx_config(filename, no_sdr_test=False): auto_rx_config["aprs_port"] = 14590 + # 1.6.0 - New SDR options + if not config.has_option("sdr", "sdr_type"): + logging.warning( + "Config - Missing sdr_type configuration option, defaulting to RTLSDR." + ) + auto_rx_config["sdr_type"] = "RTLSDR" + else: + auto_rx_config["sdr_type"] = config.get("sdr", "sdr_type") + + try: + auto_rx_config["sdr_hostname"] = config.get("sdr", "sdr_hostname") + auto_rx_config["sdr_port"] = config.getint("sdr", "sdr_port") + auto_rx_config["ss_iq_path"] = config.get("advanced", "ss_iq_path") + auto_rx_config["ss_power_path"] = config.get("advanced", "ss_power_path") + except: + logging.debug("Config - Did not find new sdr_type associated options.") + + try: + auto_rx_config["always_decode"] = json.loads( + config.get("search_params", "always_decode") + ) + except: + logging.debug( + "Config - No always_decode settings, defaulting to none." + ) + auto_rx_config["always_decode"] = [] + + try: + auto_rx_config["experimental_decoders"]["MEISEI"] = config.getboolean( + "advanced", "meisei_experimental" + ) + except: + logging.warning( + "Config - Did not find meisei_experimental setting, using default (enabled)" + ) + auto_rx_config["experimental_decoders"]["MEISEI"] = True + + try: + auto_rx_config["save_system_log"] = config.getboolean( + "logging", "save_system_log" + ) + auto_rx_config["enable_debug_logging"] = config.getboolean( + "logging", "enable_debug_logging" + ) + except: + logging.warning( + "Config - Did not find system / debug logging options, using defaults (disabled, unless set as a command-line option.)" + ) + + # If we are being called as part of a unit test, just return the config now. if no_sdr_test: return auto_rx_config - # Now we attempt to read in the individual SDR parameters. + # Now we enumerate our SDRs. auto_rx_config["sdr_settings"] = {} - for _n in range(1, auto_rx_config["sdr_quantity"] + 1): - _section = "sdr_%d" % _n - try: - _device_idx = config.get(_section, "device_idx") - _ppm = round(config.getfloat(_section, "ppm")) - _gain = config.getfloat(_section, "gain") - _bias = config.getboolean(_section, "bias") - - if (auto_rx_config["sdr_quantity"] > 1) and (_device_idx == "0"): - logging.critical( - "Config - SDR Device ID of 0 used with a multi-SDR configuration. Go read the warning in the config file!" + if auto_rx_config["sdr_type"] == "RTLSDR": + # Multiple RTLSDRs in use - we need to read in each SDRs settings. + for _n in range(1, auto_rx_config["sdr_quantity"] + 1): + _section = "sdr_%d" % _n + try: + _device_idx = config.get(_section, "device_idx") + _ppm = round(config.getfloat(_section, "ppm")) + _gain = config.getfloat(_section, "gain") + _bias = config.getboolean(_section, "bias") + + if (auto_rx_config["sdr_quantity"] > 1) and (_device_idx == "0"): + logging.critical( + "Config - RTLSDR Device ID of 0 used with a multi-SDR configuration. Go read the warning in the config file!" + ) + return None + + # See if the SDR exists. + _sdr_valid = test_sdr(sdr_type = "RTLSDR", rtl_device_idx = _device_idx) + if _sdr_valid: + auto_rx_config["sdr_settings"][_device_idx] = { + "ppm": _ppm, + "gain": _gain, + "bias": _bias, + "in_use": False, + "task": None, + } + logging.info("Config - Tested RTLSDR #%s OK" % _device_idx) + else: + logging.warning("Config - RTLSDR #%s invalid." % _device_idx) + except Exception as e: + logging.error( + "Config - Error parsing RTLSDR %d config - %s" % (_n, str(e)) ) - return None - - # See if the SDR exists. - _sdr_valid = rtlsdr_test(_device_idx) - if _sdr_valid: - auto_rx_config["sdr_settings"][_device_idx] = { - "ppm": _ppm, - "gain": _gain, - "bias": _bias, - "in_use": False, - "task": None, - } - logging.info("Config - Tested SDR #%s OK" % _device_idx) - else: - logging.warning("Config - SDR #%s invalid." % _device_idx) - except Exception as e: - logging.error( - "Config - Error parsing SDR %d config - %s" % (_n, str(e)) - ) - continue + continue + + elif auto_rx_config["sdr_type"] == "SpyServer": + # Test access to the SpyServer + _sdr_ok = test_sdr( + sdr_type=auto_rx_config["sdr_type"], + sdr_hostname=auto_rx_config["sdr_hostname"], + sdr_port=auto_rx_config["sdr_port"], + ss_iq_path=auto_rx_config["ss_iq_path"], + ss_power_path=auto_rx_config["ss_power_path"], + check_freq=1e6*(auto_rx_config["max_freq"]+auto_rx_config["min_freq"])/2.0, + ) + + if not _sdr_ok: + logging.critical(f"Config - Could not contact SpyServer {auto_rx_config['sdr_hostname']}:{auto_rx_config['sdr_port']}. Exiting.") + return None + + for _n in range(1, auto_rx_config["sdr_quantity"] + 1): + _sdr_name = f"SPY{_n:02d}" + auto_rx_config["sdr_settings"][_sdr_name] = { + "ppm": 0, + "gain": 0, + "bias": 0, + "in_use": False, + "task": None, + } + + elif auto_rx_config["sdr_type"] == "KA9Q": + # Test access to the SpyServer + _sdr_ok = test_sdr( + sdr_type=auto_rx_config["sdr_type"], + sdr_hostname=auto_rx_config["sdr_hostname"], + sdr_port=auto_rx_config["sdr_port"] + ) + + if not _sdr_ok: + logging.critical(f"Config - Could not contact KA9Q Server {auto_rx_config['sdr_hostname']}:{auto_rx_config['sdr_port']}. Exiting.") + return None + + for _n in range(1, auto_rx_config["sdr_quantity"] + 1): + _sdr_name = f"KA9Q{_n:02d}" + auto_rx_config["sdr_settings"][_sdr_name] = { + "ppm": 0, + "gain": 0, + "bias": 0, + "in_use": 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.") + return None # Sanity checks when using more than one SDR if (len(auto_rx_config["sdr_settings"].keys()) > 1) and ( diff --git a/auto_rx/autorx/decode.py b/auto_rx/autorx/decode.py index 6f989970..1e77251b 100644 --- a/auto_rx/autorx/decode.py +++ b/auto_rx/autorx/decode.py @@ -20,8 +20,9 @@ from types import FunctionType, MethodType from .utils import AsynchronousFileReader, rtlsdr_test, position_info, generate_aprs_id from .gps import get_ephemeris, get_almanac -from .sonde_specific import * +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 # Global valid sonde types list. VALID_SONDE_TYPES = [ @@ -36,6 +37,7 @@ "LMS6", "MEISEI", "MRZ", + "MTS01", "UDP", ] @@ -114,6 +116,7 @@ class SondeDecoder(object): "LMS6", "MEISEI", "MRZ", + "MTS01", "UDP", ] @@ -121,9 +124,13 @@ def __init__( self, sonde_type="None", sonde_freq=400000000.0, + sdr_type="RTLSDR", + sdr_hostname="localhost", + sdr_port=12345, + ss_iq_path="./ss_iq", rs_path="./", - sdr_fm="rtl_fm", - device_idx=0, + rtl_fm_path="rtl_fm", + rtl_device_idx=0, ppm=0, gain=-1, bias=False, @@ -142,14 +149,22 @@ def __init__( Args: sonde_type (str): The radiosonde type, as returned by SondeScanner. Valid types listed in VALID_SONDE_TYPES sonde_freq (int/float): The radiosonde frequency, in Hz. - - rs_path (str): Path to the RS binaries (i.e rs_detect). Defaults to ./ - sdr_fm (str): Path to rtl_fm, or drop-in equivalent. Defaults to 'rtl_fm' - device_idx (int or str): Device index or serial number of the RTLSDR. Defaults to 0 (the first SDR found). + + sdr_type (str): 'RTLSDR', 'Spyserver' or 'KA9Q' + + Arguments for KA9Q SDR Server / SpyServer: + sdr_hostname (str): Hostname of KA9Q Server + sdr_port (int): Port number of KA9Q Server + + Arguments for RTLSDRs: + rtl_fm_path (str): Path to rtl_fm, or drop-in equivalent. Defaults to 'rtl_fm' + rtl_device_idx (int or str): Device index or serial number of the RTLSDR. Defaults to 0 (the first SDR found). ppm (int): SDR Frequency accuracy correction, in ppm. gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. bias (bool): If True, enable the bias tee on the SDR. + rs_path (str): Path to the RS binaries (i.e rs_detect). Defaults to ./ + save_decode_audio (bool): If True, save the FM-demodulated audio to disk to decode_.wav. Note: This may use up a lot of disk space! save_decode_iq (bool): If True, save the decimated IQ stream (48 or 96k complex s16 samples) to disk to decode_IQ_.bin @@ -174,9 +189,15 @@ def __init__( self.sonde_type = sonde_type self.sonde_freq = sonde_freq + self.sdr_type = sdr_type + + self.sdr_hostname = sdr_hostname + self.sdr_port = sdr_port + self.ss_iq_path = ss_iq_path + self.rs_path = rs_path - self.sdr_fm = sdr_fm - self.device_idx = device_idx + self.rtl_fm_path = rtl_fm_path + self.rtl_device_idx = rtl_device_idx self.ppm = ppm self.gain = gain self.bias = bias @@ -238,12 +259,18 @@ def __init__( self.decoder_running = False return - # Test if the supplied RTLSDR is working. - _rtlsdr_ok = rtlsdr_test(device_idx) + # Test if the supplied SDR is working. + _sdr_ok = test_sdr( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + ss_iq_path = self.ss_iq_path, + check_freq = self.sonde_freq + ) - # TODO: How should this error be handled? - if not _rtlsdr_ok: - self.log_error("RTLSDR #%s non-functional - exiting." % device_idx) + if not _sdr_ok: + # test_sdr will provide an error message self.decoder_running = False self.exit_state = "FAILED SDR" return @@ -335,33 +362,28 @@ def generate_decoder_command(self): if self.sonde_type == "RS41": # RS41 Decoder command. - # rtl_fm -p 0 -g -1 -M fm -F9 -s 15k -f 405500000 | sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - lowpass 2600 2>/dev/null | ./rs41ecc --crc --ecc --ptu - # Note: Have removed a 'highpass 20' filter from the sox line, will need to re-evaluate if adding that is useful in the future. - decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null | " % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, - ) - - # If selected by the user, we can add a highpass filter into the sox command. This helps handle up to about 5ppm of receiver drift - # before performance becomes significantly degraded. By default this is off, as it is not required with TCXO RTLSDRs, and actually - # slightly degrades performance. - if self.rs41_drift_tweak: - _highpass = "highpass 20 " - else: - _highpass = "" - decode_cmd += ( - "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - %slowpass 2600 2>/dev/null | " - % _highpass + _sample_rate = 48000 + _filter_bandwidth = 15000 + + decode_cmd = get_sdr_fm_cmd( + sdr_type = self.sdr_type, + frequency = self.sonde_freq, + filter_bandwidth=_filter_bandwidth, + sample_rate = _sample_rate, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + rtl_device_idx = self.rtl_device_idx, + ppm = self.ppm, + gain = self.gain, + bias = self.bias, + highpass = 20, + lowpass = 2600 ) # 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.device_idx) + decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) decode_cmd += "./rs41mod --ptu2 --json 2>/dev/null" @@ -407,25 +429,27 @@ def generate_decoder_command(self): # No PTU data availble for RS92-NGP sondes. _ptu_opts = "--ngp --ptu" - # Now construct the decoder command. - # rtl_fm -p 0 -g 26.0 -M fm -F9 -s 12k -f 400500000 | sox -t raw -r 12k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 lowpass 2500 2>/dev/null | ./rs92ecc -vx -v --crc --ecc --vel -e ephemeris.dat - decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s %d -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - _rx_bw, - self.sonde_freq, - ) - decode_cmd += ( - "sox -t raw -r %d -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - lowpass 2500 highpass 20 2>/dev/null |" - % _rx_bw + + _sample_rate = 48000 + + decode_cmd = get_sdr_fm_cmd( + sdr_type = self.sdr_type, + frequency = self.sonde_freq, + filter_bandwidth=_rx_bw, + sample_rate = _sample_rate, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + rtl_device_idx = self.rtl_device_idx, + ppm = self.ppm, + gain = self.gain, + bias = self.bias, + highpass = 20, + lowpass = 2500 ) # 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.device_idx) + decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) decode_cmd += ( "./rs92mod -vx -v --crc --ecc --vel --json %s %s 2>/dev/null" @@ -438,20 +462,27 @@ def generate_decoder_command(self): # so we don't need to specify an invert flag. # 2019-02-27: Added the --dist flag, which should reduce bad positions a bit. - # Note: Have removed a 'highpass 20' filter from the sox line, will need to re-evaluate if adding that is useful in the future. - decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, + _sample_rate = 48000 + _filter_bandwidth = 15000 + + decode_cmd = get_sdr_fm_cmd( + sdr_type = self.sdr_type, + frequency = self.sonde_freq, + filter_bandwidth=_filter_bandwidth, + sample_rate = _sample_rate, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + rtl_device_idx = self.rtl_device_idx, + ppm = self.ppm, + gain = self.gain, + bias = self.bias, + highpass = 20, + lowpass = 2000 ) - decode_cmd += "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 lowpass 2000 2>/dev/null |" # 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.device_idx) + decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) # DFM decoder decode_cmd += "./dfm09mod -vv --ecc --json --dist --auto 2>/dev/null" @@ -459,19 +490,26 @@ def generate_decoder_command(self): elif self.sonde_type == "M10": # M10 Sondes - decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 22k -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, + _sample_rate = 48000 + _filter_bandwidth = 22000 + + decode_cmd = get_sdr_fm_cmd( + sdr_type = self.sdr_type, + frequency = self.sonde_freq, + filter_bandwidth=_filter_bandwidth, + sample_rate = _sample_rate, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + rtl_device_idx = self.rtl_device_idx, + ppm = self.ppm, + gain = self.gain, + bias = self.bias, + highpass = 20 ) - decode_cmd += "sox -t raw -r 22k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 2>/dev/null |" # 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.device_idx) + decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) # M10 decoder decode_cmd += "./m10mod --json --ptu -vvv 2>/dev/null" @@ -479,38 +517,49 @@ def generate_decoder_command(self): elif self.sonde_type == "IMET": # iMet-4 Sondes - decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, + _sample_rate = 48000 + + 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 ) - decode_cmd += "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 2>/dev/null |" # 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.device_idx) + if self.save_decode_iq: + decode_cmd += " tee decode_%s.raw |" % str(self.rtl_device_idx) # iMet-4 (IMET1RS) decoder - decode_cmd += f"./imet1rs_dft --json {self.raw_file_option} 2>/dev/null" + decode_cmd += f"./imet4iq --iq 0.0 --lpIQ --dc - {_sample_rate} 16 --json 2>/dev/null" elif self.sonde_type == "IMET5": # iMet-54 Sondes - decode_cmd = "%s %s-p %d -d %s %s-M raw -F9 -s 48k -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, + _sample_rate = 48000 + + 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 audio to disk if debugging is enabled. if self.save_decode_iq: - decode_cmd += " tee decode_IQ_%s.bin |" % str(self.device_idx) + decode_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) # iMet-54 Decoder decode_cmd += ( @@ -520,22 +569,28 @@ def generate_decoder_command(self): elif self.sonde_type == "MRZ": # Meteo-Radiy MRZ Sondes - decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, + _sample_rate = 48000 + + 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 ) - decode_cmd += "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 2>/dev/null |" # 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.device_idx) + if self.save_decode_iq: + decode_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) # MRZ decoder - decode_cmd += "./mp3h1mod --auto --json --ptu 2>/dev/null" + #decode_cmd += "./mp3h1mod --auto --json --ptu 2>/dev/null" + decode_cmd += "./mp3h1mod --IQ 0.0 --lp - 48000 16 --json --ptu 2>/dev/null" elif self.sonde_type == "MK2LMS": # 1680 MHz LMS6 sondes, using 9600 baud MK2A-format telemetry. @@ -545,24 +600,33 @@ def generate_decoder_command(self): # Notes: # - Have dropped the low-leakage FIR filter (-F9) to save a bit of CPU # Have scaled back sample rate to 220 kHz to again save CPU. - # mk2mod runs at ~90% CPU on a RPi 3, with rtl_fm using ~50% of another core. + # mk2a1680mod runs at ~90% CPU on a RPi 3, with rtl_fm using ~50% of another core. # Update 2021-07-24: Updated version with speedups now taking 240 kHz BW and only using 50% of a core. - decode_cmd = "%s %s-p %d -d %s %s-M raw -s 240k -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, + + _baud_rate = 4800 + _sample_rate = 240000 + + 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, + fast_filter = True ) # 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.device_idx) + decode_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) # LMS6-1680 decoder - decode_cmd += f"./mk2mod --iq 0.0 --lpIQ --lpbw 160 --decFM --dc --crc --json {self.raw_file_option} - 240000 16 2>/dev/null" + decode_cmd += f"./mk2a1680mod --iq 0.0 --lpIQ --lpbw 160 --decFM --dc --crc --json {self.raw_file_option} - 240000 16 2>/dev/null" # Settings for old decoder, which cares about FM inversion. # if self.inverted: # self.log_debug("Using inverted MK2A decoder.") @@ -575,54 +639,80 @@ def generate_decoder_command(self): # LMS6 Decoder command. # rtl_fm -p 0 -g -1 -M fm -F9 -s 15k -f 405500000 | sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - lowpass 2600 2>/dev/null | ./rs41ecc --crc --ecc --ptu # Note: Have removed a 'highpass 20' filter from the sox line, will need to re-evaluate if adding that is useful in the future. - decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null | " % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, - ) - # If selected by the user, we can add a highpass filter into the sox command. This helps handle up to about 5ppm of receiver drift - # before performance becomes significantly degraded. By default this is off, as it is not required with TCXO RTLSDRs, and actually - # slightly degrades performance. - if self.rs41_drift_tweak: - _highpass = "highpass 20 " - else: - _highpass = "" - - decode_cmd += ( - "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - %slowpass 2600 2>/dev/null | " - % _highpass + _sample_rate = 48000 + _filter_bandwidth = 15000 + + decode_cmd = get_sdr_fm_cmd( + sdr_type = self.sdr_type, + frequency = self.sonde_freq, + filter_bandwidth=_filter_bandwidth, + sample_rate = _sample_rate, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + rtl_device_idx = self.rtl_device_idx, + ppm = self.ppm, + gain = self.gain, + bias = self.bias, + highpass = 20, + lowpass = 2600 ) # 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.device_idx) + decode_cmd += " tee decode_%s.wav |" % str(self.rtl_device_idx) decode_cmd += "./lms6Xmod --json 2>/dev/null" elif self.sonde_type == "MEISEI": # Meisei IMS-100 Sondes - # Starting out with a 15 kHz bandwidth filter. - - decode_cmd = "%s %s-p %d -d %s %s-M fm -F9 -s 15k -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, + + _sample_rate = 48000 + + 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 ) - decode_cmd += "sox -t raw -r 15k -e s -b 16 -c 1 - -r 48000 -b 8 -t wav - highpass 20 2>/dev/null |" - # 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.device_idx) + # 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) - # Meisei IMS-100 decoder - decode_cmd += f"./meisei100mod --json 2>/dev/null" + # Meisei Decoder, in IQ input mode + decode_cmd += f"./meisei100mod --IQ 0.0 --lpIQ --dc - {_sample_rate} 16 --json --ptu --ecc 2>/dev/null" + + elif self.sonde_type == "MTS01": + # Meteosis MTS-01 + + _sample_rate = 48000 + + 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 += " tee decode_%s.raw |" % str(self.rtl_device_idx) + + # Meteosis MTS01 decoder + decode_cmd += f"./mts01mod --json --IQ 0.0 --lpIQ --dc - {_sample_rate} 16 2>/dev/null" elif self.sonde_type == "UDP": # UDP Input Mode. @@ -658,34 +748,38 @@ def generate_decoder_command_experimental(self): _stats_rate = 5 if self.sonde_type == "RS41": - # RS41 Decoder command. - _sdr_rate = 48000 # IQ rate. Lower rate = lower CPU usage, but less frequency tracking ability. + # RS41 Decoder + _baud_rate = 4800 - _offset = 0.25 # Place the sonde frequency in the centre of the passband. - _lower = int( - 0.025 * _sdr_rate - ) # Limit the frequency estimation window to not include the passband edges. - _upper = int(0.475 * _sdr_rate) - _freq = int(self.sonde_freq - _sdr_rate * _offset) - - demod_cmd = "%s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - _sdr_rate, - _freq, + _sample_rate = 48000 # 10x Oversampling + + # Limit FSK estimator window to roughly +/- 10 kHz + _lower = -10000 + _upper = 10000 + + 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 += " tee decode_IQ_%s.bin |" % str(self.device_idx) + demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) demod_cmd += "./fsk_demod --cs16 -b %d -u %d -s --stats=%d 2 %d %d - -" % ( _lower, _upper, _stats_rate, - _sdr_rate, + _sample_rate, _baud_rate, ) @@ -693,7 +787,7 @@ def generate_decoder_command_experimental(self): # 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) - self.rx_frequency = _freq + self.rx_frequency = self.sonde_freq elif self.sonde_type == "RS92": # Decoding a RS92 requires either an ephemeris or an almanac file. @@ -724,43 +818,43 @@ def generate_decoder_command_experimental(self): else: _rs92_gps_data = "-e %s" % self.rs92_ephemeris + _baud_rate = 4800 + if self.sonde_freq > 1000e6: - # Use a higher IQ rate for 1680 MHz sondes, at the expense of some CPU usage. - _sdr_rate = 96000 + _sample_rate = 96000 _ptu_ops = "--ngp --ptu" + _lower = -10000 + _upper = 10000 else: - # On 400 MHz, use 48 khz - RS92s dont drift far enough to need any more than this. - _sdr_rate = 48000 + _sample_rate = 48000 _ptu_ops = "--ptu" - - _output_rate = 48000 - _baud_rate = 4800 - _offset = 0.25 # Place the sonde frequency in the centre of the passband. - _lower = int( - 0.025 * _sdr_rate - ) # Limit the frequency estimation window to not include the passband edges. - _upper = int(0.475 * _sdr_rate) - _freq = int(self.sonde_freq - _sdr_rate * _offset) - - demod_cmd = "%s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - _sdr_rate, - _freq, + _lower = -20000 + _upper = 20000 + + + 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 += " tee decode_IQ_%s.bin |" % str(self.device_idx) + demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) demod_cmd += "./fsk_demod --cs16 -b %d -u %d -s --stats=%d 2 %d %d - -" % ( _lower, _upper, _stats_rate, - _sdr_rate, + _sample_rate, _baud_rate, ) @@ -771,39 +865,47 @@ def generate_decoder_command_experimental(self): # RS92s transmit continuously - average over the last 2 frames, and use a mean demod_stats = FSKDemodStats(averaging_time=2.0, peak_hold=True) - self.rx_frequency = _freq + self.rx_frequency = self.sonde_freq elif self.sonde_type == "DFM": - # DFM06/DFM09 Sondes. + # DFM06/DFM09/DFM17 Sondes. - _sdr_rate = 50000 _baud_rate = 2500 - _offset = 0.25 # Place the sonde frequency in the centre of the passband. - _lower = int( - 0.025 * _sdr_rate - ) # Limit the frequency estimation window to not include the passband edges. - _upper = int(0.475 * _sdr_rate) - _freq = int(self.sonde_freq - _sdr_rate * _offset) - - demod_cmd = "%s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - _sdr_rate, - _freq, + _sample_rate = 50000 # 10x Oversampling + + # Limit FSK estimator window to roughly +/- 10 kHz + _lower = -10000 + _upper = 10000 + + if (abs(403200000 - self.sonde_freq) < 20000) and (self.sdr_type == "RTLSDR"): + # Narrow up the frequency estimator window if we are close to + # the 403.2 MHz RTLSDR Spur. + _lower = -8000 + _upper = 8000 + + 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 += " tee decode_IQ_%s.bin |" % str(self.device_idx) + demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) demod_cmd += "./fsk_demod --cs16 -b %d -u %d -s --stats=%d 2 %d %d - -" % ( _lower, _upper, _stats_rate, - _sdr_rate, + _sample_rate, _baud_rate, ) @@ -818,38 +920,42 @@ def generate_decoder_command_experimental(self): # DFM sondes transmit continuously - average over the last 2 frames, and peak hold demod_stats = FSKDemodStats(averaging_time=2.0, peak_hold=True) - self.rx_frequency = _freq + self.rx_frequency = self.sonde_freq elif self.sonde_type == "M10": # M10 Sondes # These have a 'weird' baud rate, and as fsk_demod requires the input sample rate to be an integer multiple of the baud rate, # our required sample rate is correspondingly weird! - _sdr_rate = 48080 + _baud_rate = 9616 - _offset = 0.25 # Place the sonde frequency in the centre of the passband. - _lower = int( - 0.025 * _sdr_rate - ) # Limit the frequency estimation window to not include the passband edges. - _upper = int(0.475 * _sdr_rate) - _freq = int(self.sonde_freq - _sdr_rate * _offset) - - demod_cmd = "%s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - _sdr_rate, - _freq, + _sample_rate = 48080 + _p = 5 # Override the oversampling rate + + # Limit FSK estimator window to roughly +/- 10 kHz + _lower = -10000 + _upper = 10000 + + 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 += " tee decode_IQ_%s.bin |" % str(self.device_idx) + demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) demod_cmd += ( - "./fsk_demod --cs16 -b %d -u %d -s -p 5 --stats=%d 2 %d %d - -" - % (_lower, _upper, _stats_rate, _sdr_rate, _baud_rate) + "./fsk_demod --cs16 -b %d -u %d -s -p %d --stats=%d 2 %d %d - -" + % (_lower, _upper, _p, _stats_rate, _sample_rate, _baud_rate) ) # M10 decoder @@ -857,37 +963,41 @@ def generate_decoder_command_experimental(self): # M10 sondes transmit in short, irregular pulses - average over the last 2 frames, and use a peak hold demod_stats = FSKDemodStats(averaging_time=2.0, peak_hold=True) - self.rx_frequency = _freq + self.rx_frequency = self.sonde_freq elif self.sonde_type == "M20": # M20 Sondes # 9600 baud. - _sdr_rate = 48000 + _baud_rate = 9600 - _offset = 0.25 # Place the sonde frequency in the centre of the passband. - _lower = int( - 0.025 * _sdr_rate - ) # Limit the frequency estimation window to not include the passband edges. - _upper = int(0.475 * _sdr_rate) - _freq = int(self.sonde_freq - _sdr_rate * _offset) - - demod_cmd = "%s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - _sdr_rate, - _freq, + _sample_rate = 48000 + _p = 5 + + # Limit FSK estimator window to roughly +/- 10 kHz + _lower = -10000 + _upper = 10000 + + 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 += " tee decode_IQ_%s.bin |" % str(self.device_idx) + demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) demod_cmd += ( - "./fsk_demod --cs16 -b %d -u %d -s -p 5 --stats=%d 2 %d %d - -" - % (_lower, _upper, _stats_rate, _sdr_rate, _baud_rate) + "./fsk_demod --cs16 -b %d -u %d -s -p %d --stats=%d 2 %d %d - -" + % (_lower, _upper, _p, _stats_rate, _sample_rate, _baud_rate) ) # M20 decoder @@ -895,38 +1005,41 @@ def generate_decoder_command_experimental(self): # M20 sondes transmit in short, irregular pulses - average over the last 2 frames, and use a peak hold demod_stats = FSKDemodStats(averaging_time=2.0, peak_hold=True) - self.rx_frequency = _freq + self.rx_frequency = self.sonde_freq elif self.sonde_type.startswith("LMS"): # LMS6 (400 MHz variant) Decoder command. - _sdr_rate = 48000 # IQ rate. Lower rate = lower CPU usage, but less frequency tracking ability. - _output_rate = 48000 + _baud_rate = 4800 - _offset = 0.25 # Place the sonde frequency in the centre of the passband. - _lower = int( - 0.025 * _sdr_rate - ) # Limit the frequency estimation window to not include the passband edges. - _upper = int(0.475 * _sdr_rate) - _freq = int(self.sonde_freq - _sdr_rate * _offset) - - demod_cmd = "%s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - _sdr_rate, - _freq, + _sample_rate = 48000 + + # Limit FSK estimator window to roughly +/- 10 kHz + _lower = -10000 + _upper = 10000 + + 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 += " tee decode_IQ_%s.bin |" % str(self.device_idx) + demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) demod_cmd += "./fsk_demod --cs16 -b %d -u %d -s --stats=%d 2 %d %d - -" % ( _lower, _upper, _stats_rate, - _sdr_rate, + _sample_rate, _baud_rate, ) @@ -934,38 +1047,40 @@ def generate_decoder_command_experimental(self): # LMS sondes transmit continuously - average over the last 2 frames, and use a peak hold demod_stats = FSKDemodStats(averaging_time=2.0, peak_hold=True) - self.rx_frequency = _freq + self.rx_frequency = self.sonde_freq elif self.sonde_type == "IMET5": # iMet-54 Decoder command. - _sdr_rate = 48000 # IQ rate. Lower rate = lower CPU usage, but less frequency tracking ability. - _output_rate = 48000 + _baud_rate = 4800 - _offset = 0.25 # Place the sonde frequency in the centre of the passband. - _lower = int( - 0.025 * _sdr_rate - ) # Limit the frequency estimation window to not include the passband edges. - _upper = int(0.475 * _sdr_rate) - _freq = int(self.sonde_freq - _sdr_rate * _offset) - - demod_cmd = "%s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - _sdr_rate, - _freq, + _sample_rate = 48000 + + # Limit FSK estimator window to roughly +/- 10 kHz + _lower = -10000 + _upper = 10000 + + 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 += " tee decode_IQ_%s.bin |" % str(self.device_idx) + demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) demod_cmd += "./fsk_demod --cs16 -b %d -u %d -s --stats=%d 2 %d %d - -" % ( _lower, _upper, _stats_rate, - _sdr_rate, + _sample_rate, _baud_rate, ) @@ -973,39 +1088,41 @@ def generate_decoder_command_experimental(self): # iMet54 sondes transmit in bursts. Use a peak hold. demod_stats = FSKDemodStats(averaging_time=2.0, peak_hold=True) - self.rx_frequency = _freq + self.rx_frequency = self.sonde_freq elif self.sonde_type == "MRZ": # MRZ Sondes. - _sdr_rate = 48000 _baud_rate = 2400 - _offset = 0.25 # Place the sonde frequency in the centre of the passband. - _lower = int( - 0.025 * _sdr_rate - ) # Limit the frequency estimation window to not include the passband edges. - _upper = int(0.475 * _sdr_rate) - _freq = int(self.sonde_freq - _sdr_rate * _offset) - - demod_cmd = "%s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - _sdr_rate, - _freq, + _sample_rate = 48000 + + # Limit FSK estimator window to roughly +/- 10 kHz + _lower = -10000 + _upper = 10000 + + 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 += " tee decode_IQ_%s.bin |" % str(self.device_idx) + demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) demod_cmd += "./fsk_demod --cs16 -s -b %d -u %d --stats=%d 2 %d %d - -" % ( _lower, _upper, _stats_rate, - _sdr_rate, + _sample_rate, _baud_rate, ) @@ -1014,7 +1131,7 @@ def generate_decoder_command_experimental(self): # MRZ sondes transmit continuously - average over the last frame, and use a peak hold demod_stats = FSKDemodStats(averaging_time=1.0, peak_hold=True) - self.rx_frequency = _freq + self.rx_frequency = self.sonde_freq elif self.sonde_type == "MK2LMS": # 1680 MHz LMS6 sondes, using 9600 baud MK2A-format telemetry. @@ -1024,25 +1141,34 @@ def generate_decoder_command_experimental(self): # Notes: # - Have dropped the low-leakage FIR filter (-F9) to save a bit of CPU # Have scaled back sample rate to 220 kHz to again save CPU. - # mk2mod runs at ~90% CPU on a RPi 3, with rtl_fm using ~50% of another core. - - demod_cmd = "%s %s-p %d -d %s %s-M raw -s 220k -f %d 2>/dev/null |" % ( - self.sdr_fm, - bias_option, - int(self.ppm), - str(self.device_idx), - gain_param, - self.sonde_freq, + # mk2a1680mod runs at ~90% CPU on a RPi 3, with rtl_fm using ~50% of another core. + + _baud_rate = 4800 + _sample_rate = 220000 + + 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, + fast_filter = True # Don't use -F9 ) # 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.device_idx) + demod_cmd += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) # LMS6-1680 decoder - demod_cmd += f"./mk2mod --iq 0.0 --lpIQ --lpbw 160 --lpFM --dc --crc --json {self.raw_file_option} - 220000 16 2>/dev/null" + demod_cmd += f"./mk2a1680mod --iq 0.0 --lpIQ --lpbw 160 --lpFM --dc --crc --json {self.raw_file_option} - 220000 16 2>/dev/null" decode_cmd = None demod_stats = None + self.rx_frequency = self.sonde_freq # Settings for old decoder, which cares about FM inversion. # if self.inverted: # self.log_debug("Using inverted MK2A decoder.") @@ -1050,6 +1176,49 @@ def generate_decoder_command_experimental(self): # decode_cmd += f"./mk2a_lms1680 -i --json {self.raw_file_option} 2>/dev/null" # else: # decode_cmd += f"./mk2a_lms1680 --json {self.raw_file_option} 2>/dev/null" + + elif self.sonde_type == "MEISEI": + # Meisei iMS100 Sondes. + + _baud_rate = 2400 + _sample_rate = 48000 + + # Limit FSK estimator window to roughly +/- 15 kHz + _lower = -15000 + _upper = 15000 + + 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 += " tee decode_IQ_%s.bin |" % str(self.rtl_device_idx) + + demod_cmd += "./fsk_demod --cs16 -s -b %d -u %d --stats=%d 2 %d %d - -" % ( + _lower, + _upper, + _stats_rate, + _sample_rate, + _baud_rate, + ) + + decode_cmd = f"./meisei100mod --softin --json --ptu --ecc 2>/dev/null" + + # Meisei sondes transmit continuously - average over the last frame, and use a peak hold + demod_stats = FSKDemodStats(averaging_time=1.0, peak_hold=True) + self.rx_frequency = self.sonde_freq + else: return None @@ -1297,8 +1466,6 @@ def handle_decoder_line(self, data): _telemetry["type"] = "DFM" _telemetry["subtype"] = "DFM" - - # Check frame ID here to ensure we are on dfm09mod version with the frame number fixes (2020-12). if _telemetry["frame"] < 256: self.log_error( @@ -1306,6 +1473,10 @@ def handle_decoder_line(self, data): ) return False + elif self.sonde_type == "MEISEI": + # For meisei sondes, we are provided a subtype that distinguishes iMS-100 and RS11G sondes. + _telemetry["type"] = _telemetry["subtype"] + else: # For other sonde types, we leave the type field as it is, even if we are provided # a subtype field. (This shouldn't happen) @@ -1320,7 +1491,7 @@ def handle_decoder_line(self, data): _telemetry["freq"] = "%.3f MHz" % (self.sonde_freq / 1e6) # Add in information about the SDR used. - _telemetry["sdr_device_idx"] = self.device_idx + _telemetry["sdr_device_idx"] = self.rtl_device_idx # Check for an 'aux' field, this indicates that the sonde has an auxilliary payload, # which is most likely an Ozone sensor (though could be something different!) @@ -1486,9 +1657,14 @@ def log_debug(self, line): Args: line (str): Message to be logged. """ + _sdr_name = get_sdr_name( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port + ) logging.debug( - "Decoder #%s %s %.3f - %s" - % (str(self.device_idx), self.sonde_type, self.sonde_freq / 1e6, line) + f"Decoder ({_sdr_name}) {self.sonde_type} {self.sonde_freq/1e6:.3f} - {line}" ) def log_info(self, line): @@ -1496,9 +1672,14 @@ def log_info(self, line): Args: line (str): Message to be logged. """ + _sdr_name = get_sdr_name( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port + ) logging.info( - "Decoder #%s %s %.3f - %s" - % (str(self.device_idx), self.sonde_type, self.sonde_freq / 1e6, line) + f"Decoder ({_sdr_name}) {self.sonde_type} {self.sonde_freq/1e6:.3f} - {line}" ) def log_error(self, line): @@ -1506,9 +1687,14 @@ def log_error(self, line): Args: line (str): Message to be logged. """ + _sdr_name = get_sdr_name( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port + ) logging.error( - "Decoder #%s %s %.3f - %s" - % (str(self.device_idx), self.sonde_type, self.sonde_freq / 1e6, line) + f"Decoder ({_sdr_name}) {self.sonde_type} {self.sonde_freq/1e6:.3f} - {line}" ) def log_critical(self, line): @@ -1516,9 +1702,14 @@ def log_critical(self, line): Args: line (str): Message to be logged. """ + _sdr_name = get_sdr_name( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port + ) logging.critical( - "Decoder #%s %s %.3f - %s" - % (str(self.device_idx), self.sonde_type, self.sonde_freq / 1e6, line) + f"Decoder ({_sdr_name}) {self.sonde_type} {self.sonde_freq/1e6:.3f} - {line}" ) def stop(self, nowait=False): @@ -1562,14 +1753,14 @@ def running(self): sonde_freq=401.5 * 1e6, sonde_type="RS41", timeout=50, - device_idx="00000002", + rtl_device_idx="00000002", exporter=[_habitat.add, _log.add], ) # _decoder2 = SondeDecoder(sonde_freq = 405.5*1e6, # sonde_type = "RS41", # timeout = 50, - # device_idx="00000001", + # rtl_device_idx="00000001", # exporter=[_habitat.add, _log.add]) while True: diff --git a/auto_rx/autorx/email_notification.py b/auto_rx/autorx/email_notification.py index 61195c32..0b2db5a0 100644 --- a/auto_rx/autorx/email_notification.py +++ b/auto_rx/autorx/email_notification.py @@ -11,19 +11,12 @@ import smtplib from email.mime.text import MIMEText from email.utils import formatdate +from queue import Queue from threading import Thread from .config import read_auto_rx_config from .utils import position_info, strip_sonde_serial from .geometry import GenericTrack -try: - # Python 2 - from Queue import Queue - -except ImportError: - # Python 3 - from queue import Queue - class EmailNotification(object): """ Radiosonde Email Notification Class. @@ -46,6 +39,7 @@ def __init__( mail_from=None, mail_to=None, mail_subject=None, + mail_nearby_landing_subject=None, station_position=None, launch_notifications=True, landing_notifications=True, @@ -62,6 +56,7 @@ def __init__( self.mail_from = mail_from self.mail_to = mail_to self.mail_subject = mail_subject + self.mail_nearby_landing_subject = mail_nearby_landing_subject self.station_position = station_position self.launch_notifications = launch_notifications self.landing_notifications = landing_notifications @@ -266,7 +261,11 @@ def process_telemetry(self, telemetry): % strip_sonde_serial(_id) ) - _subject = "Nearby Radiosonde Landing Detected - %s" % _id + # Construct subject + _subject = self.mail_nearby_landing_subject + _subject = _subject.replace("", _id) + _subject = _subject.replace("", telemetry["type"]) + _subject = _subject.replace("", telemetry["freq"]) self.send_notification_email(subject=_subject, message=msg) @@ -401,6 +400,7 @@ def log_error(self, line): mail_from=config["email_from"], mail_to=config["email_to"], mail_subject=config["email_subject"], + mail_nearby_landing_subject=config["email_nearby_landing_subject"], station_position=(-10.0, 10.0, 0.0,), landing_notifications=True, launch_notifications=True, diff --git a/auto_rx/autorx/habitat.py b/auto_rx/autorx/habitat.py index ee453de1..ebb4303b 100644 --- a/auto_rx/autorx/habitat.py +++ b/auto_rx/autorx/habitat.py @@ -15,16 +15,10 @@ 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 -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue - # These get replaced out after init url_habitat_uuids = "" url_habitat_db = "" diff --git a/auto_rx/autorx/logger.py b/auto_rx/autorx/logger.py index 40697b25..1b0fa29f 100644 --- a/auto_rx/autorx/logger.py +++ b/auto_rx/autorx/logger.py @@ -10,15 +10,9 @@ import logging import os import time +from queue import Queue from threading import Thread -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue - class TelemetryLogger(object): """ Radiosonde Telemetry Logger Class. diff --git a/auto_rx/autorx/ozimux.py b/auto_rx/autorx/ozimux.py index 19630c3d..a3c8ce84 100644 --- a/auto_rx/autorx/ozimux.py +++ b/auto_rx/autorx/ozimux.py @@ -10,15 +10,9 @@ import logging import socket import time +from queue import Queue from threading import Thread -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue - class OziUploader(object): """ Push radiosonde telemetry out via UDP broadcast to the Horus Chase-Car Utilities @@ -50,7 +44,7 @@ class OziUploader(object): ] # Extra fields we can pass on to other programs. - EXTRA_FIELDS = ["bt", "humidity", "sats", "batt", "snr", "fest", "f_centre", "ppm"] + EXTRA_FIELDS = ["bt", "humidity", "sats", "batt", "snr", "fest", "f_centre", "ppm", "subtype", "sdr_device_idx"] def __init__( self, diff --git a/auto_rx/autorx/rotator.py b/auto_rx/autorx/rotator.py index 8d5a52f8..5301cf80 100644 --- a/auto_rx/autorx/rotator.py +++ b/auto_rx/autorx/rotator.py @@ -10,16 +10,10 @@ import time import numpy as np +from queue import Queue from threading import Thread, Lock from autorx.utils import position_info -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue - def read_rotator(rotctld_host="localhost", rotctld_port=4533, timeout=5): """ Attempt to read a position from a rotctld server. diff --git a/auto_rx/autorx/scan.py b/auto_rx/autorx/scan.py index 85fa436d..e23e317b 100644 --- a/auto_rx/autorx/scan.py +++ b/auto_rx/autorx/scan.py @@ -13,6 +13,7 @@ import subprocess import time import traceback +from io import StringIO from threading import Thread, Lock from types import FunctionType, MethodType from .utils import ( @@ -22,13 +23,8 @@ reset_all_rtlsdrs, peak_decimation, ) +from .sdr_wrappers import test_sdr, reset_sdr, get_sdr_name, get_sdr_iq_cmd, get_sdr_fm_cmd, get_power_spectrum -try: - # Python 2 - from StringIO import StringIO -except ImportError: - # Python 3 - from io import StringIO try: from .web import flask_emit_event @@ -56,7 +52,7 @@ def run_rtl_power( step, filename="log_power.csv", dwell=20, - sdr_power="rtl_power", + rtl_power_path="rtl_power", device_idx=0, ppm=0, gain=-1, @@ -70,7 +66,7 @@ def run_rtl_power( step (int): Search step, in Hz. filename (str): Output results to this file. Defaults to ./log_power.csv dwell (int): How long to average on the frequency range for. - sdr_power (str): Path to the rtl_power utility. + rtl_power_path (str): Path to the rtl_power utility. device_idx (int or str): Device index or serial number of the RTLSDR. Defaults to 0 (the first SDR found). ppm (int): SDR Frequency accuracy correction, in ppm. gain (float): SDR Gain setting, in dB. @@ -80,7 +76,7 @@ def run_rtl_power( bool: True if rtl_power ran successfuly, False otherwise. """ - # Example: rtl_power -f 400400000:403500000:800 -i20 -1 -c 20% -p 0 -d 0 -g 26.0 log_power.csv + # Example: rtl_power -f 400400000:403500000:800 -i20 -1 -c 25% -p 0 -d 0 -g 26.0 log_power.csv # Add a -T option if bias is enabled bias_option = "-T " if bias else "" @@ -104,11 +100,11 @@ def run_rtl_power( timeout_kill = "-k 30 " rtl_power_cmd = ( - "timeout %s%d %s %s-f %d:%d:%d -i %d -1 -c 20%% -p %d -d %s %s%s" + "timeout %s%d %s %s-f %d:%d:%d -i %d -1 -c 25%% -p %d -d %s %s%s" % ( timeout_kill, dwell + 10, - sdr_power, + rtl_power_path, bias_option, start, stop, @@ -232,8 +228,12 @@ def detect_sonde( frequency, rs_path="./", dwell_time=10, - sdr_fm="rtl_fm", - device_idx=0, + sdr_type="RTLSDR", + sdr_hostname="localhost", + sdr_port=5555, + ss_iq_path = "./ss_iq", + rtl_fm_path="rtl_fm", + rtl_device_idx=0, ppm=0, gain=-1, bias=False, @@ -246,8 +246,8 @@ def detect_sonde( frequency (int): Frequency to perform the detection on, in Hz. rs_path (str): Path to the RS binaries (i.e rs_detect). Defaults to ./ dwell_time (int): Timeout before giving up detection. - sdr_fm (str): Path to rtl_fm, or drop-in equivalent. Defaults to 'rtl_fm' - device_idx (int or str): Device index or serial number of the RTLSDR. Defaults to 0 (the first SDR found). + rtl_fm_path (str): Path to rtl_fm, or drop-in equivalent. Defaults to 'rtl_fm' + rtl_device_idx (int or str): Device index or serial number of the RTLSDR. Defaults to 0 (the first SDR found). ppm (int): SDR Frequency accuracy correction, in ppm. gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. bias (bool): If True, enable the bias tee on the SDR. @@ -285,10 +285,17 @@ 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 22 kHz detection bandwidth. + # 400-406 MHz sondes - use a 20 kHz detection bandwidth. _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 + else: # 1680 MHz sondes # Both the RS92-NGP and 1680 MHz LMS6 have a much wider bandwidth than their 400 MHz counterparts. @@ -307,23 +314,38 @@ def detect_sonde( if _mode == "IQ": # IQ decoding - # Sample source (rtl_fm, in IQ mode) - rx_test_command = ( - "timeout %ds %s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" - % ( - dwell_time * 2, - sdr_fm, - bias_option, - int(ppm), - str(device_idx), - gain_param, - _iq_bw, - frequency, - ) + rx_test_command = f"timeout {dwell_time * 2} " + + rx_test_command += get_sdr_iq_cmd( + sdr_type=sdr_type, + frequency=frequency, + sample_rate=_iq_bw, + rtl_device_idx = rtl_device_idx, + rtl_fm_path = rtl_fm_path, + ppm = ppm, + gain = gain, + bias = bias, + sdr_hostname = sdr_hostname, + sdr_port = sdr_port, + ss_iq_path = ss_iq_path ) + + # rx_test_command = ( + # "timeout %ds %s %s-p %d -d %s %s-M raw -F9 -s %d -f %d 2>/dev/null |" + # % ( + # dwell_time * 2, + # rtl_fm_path, + # bias_option, + # int(ppm), + # str(device_idx), + # gain_param, + # _iq_bw, + # frequency, + # ) + # ) # Saving of Debug audio, if enabled, if save_detection_audio: - rx_test_command += "tee detect_%s.raw | " % str(device_idx) + rx_test_command += "tee detect_%s.raw | " % str(rtl_device_idx) rx_test_command += os.path.join( rs_path, "dft_detect" @@ -337,28 +359,47 @@ def detect_sonde( # FM decoding # Sample Source (rtl_fm) - rx_test_command = ( - "timeout %ds %s %s-p %d -d %s %s-M fm -F9 -s %d -f %d 2>/dev/null |" - % ( - dwell_time * 2, - sdr_fm, - bias_option, - int(ppm), - str(device_idx), - gain_param, - _rx_bw, - frequency, - ) - ) - # Sample filtering - rx_test_command += ( - "sox -t raw -r %d -e s -b 16 -c 1 - -r 48000 -t wav - highpass 20 2>/dev/null | " - % _rx_bw + + rx_test_command = f"timeout {dwell_time * 2} " + + rx_test_command += get_sdr_fm_cmd( + sdr_type=sdr_type, + frequency=frequency, + filter_bandwidth=_rx_bw, + sample_rate=48000, + highpass = 20, + lowpass = None, + rtl_device_idx = rtl_device_idx, + rtl_fm_path = rtl_fm_path, + ppm = ppm, + gain = gain, + bias = bias, + sdr_hostname = "", + sdr_port = 1234, ) + # rx_test_command = ( + # "timeout %ds %s %s-p %d -d %s %s-M fm -F9 -s %d -f %d 2>/dev/null |" + # % ( + # dwell_time * 2, + # rtl_fm_path, + # bias_option, + # int(ppm), + # str(device_idx), + # gain_param, + # _rx_bw, + # frequency, + # ) + # ) + # # Sample filtering + # rx_test_command += ( + # "sox -t raw -r %d -e s -b 16 -c 1 - -r 48000 -t wav - highpass 20 2>/dev/null | " + # % _rx_bw + # ) + # Saving of Debug audio, if enabled, if save_detection_audio: - rx_test_command += "tee detect_%s.wav | " % str(device_idx) + rx_test_command += "tee detect_%s.wav | " % str(rtl_device_idx) # 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. @@ -366,12 +407,18 @@ def detect_sonde( os.path.join(rs_path, "dft_detect") + " -t %d 2>/dev/null" % dwell_time ) + _sdr_name = get_sdr_name( + sdr_type, + rtl_device_idx = rtl_device_idx, + sdr_hostname = sdr_hostname, + sdr_port = sdr_port + ) + logging.debug( - "Scanner #%s - Using detection command: %s" % (str(device_idx), rx_test_command) + f"Scanner ({_sdr_name}) - Using detection command: {rx_test_command}" ) logging.debug( - "Scanner #%s - Attempting sonde detection on %.3f MHz" - % (str(device_idx), frequency / 1e6) + f"Scanner ({_sdr_name})- Attempting sonde detection on {frequency/1e6 :.3f} MHz" ) try: @@ -384,23 +431,21 @@ def detect_sonde( # dft_detect returns a code of 1 if no sonde is detected. # logging.debug("Scanner - dfm_detect return code: %s" % e.returncode) if e.returncode == 124: - logging.error("Scanner #%s - dft_detect timed out." % str(device_idx)) - raise IOError("Possible RTLSDR lockup.") + logging.error(f"Scanner ({_sdr_name}) - dft_detect timed out.") + raise IOError("Possible SDR lockup.") elif e.returncode >= 2: ret_output = e.output.decode("utf8") else: _runtime = time.time() - _start logging.debug( - "Scanner #%s - dft_detect exited in %.1f seconds with return code %d." - % (str(device_idx), _runtime, e.returncode) + f"Scanner ({_sdr_name}) - dft_detect exited in {_runtime:.1f} seconds with return code {e.returncode}." ) return (None, 0.0) except Exception as e: # Something broke when running the detection function. logging.error( - "Scanner #%s - Error when running dft_detect - %s" - % (str(device_idx), str(e)) + f"Scanner ({_sdr_name}) - Error when running dft_detect - {sdr(e)}" ) return (None, 0.0) @@ -444,74 +489,74 @@ def detect_sonde( if "RS41" in _type: logging.debug( - "Scanner #%s - Detected a RS41! (Score: %.2f, Offset: %.1f Hz)" - % (str(device_idx), _score, _offset_est) + "Scanner (%s) - Detected a RS41! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) ) _sonde_type = "RS41" elif "RS92" in _type: logging.debug( - "Scanner #%s - Detected a RS92! (Score: %.2f, Offset: %.1f Hz)" - % (str(device_idx), _score, _offset_est) + "Scanner (%s) - Detected a RS92! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) ) _sonde_type = "RS92" elif "DFM" in _type: logging.debug( - "Scanner #%s - Detected a DFM Sonde! (Score: %.2f, Offset: %.1f Hz)" - % (str(device_idx), _score, _offset_est) + "Scanner (%s) - Detected a DFM Sonde! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) ) _sonde_type = "DFM" elif "M10" in _type: logging.debug( - "Scanner #%s - Detected a M10 Sonde! (Score: %.2f, Offset: %.1f Hz)" - % (str(device_idx), _score, _offset_est) + "Scanner (%s) - Detected a M10 Sonde! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) ) _sonde_type = "M10" elif "M20" in _type: logging.debug( - "Scanner #%s - Detected a M20 Sonde! (Score: %.2f, Offset: %.1f Hz)" - % (str(device_idx), _score, _offset_est) + "Scanner (%s) - Detected a M20 Sonde! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) ) _sonde_type = "M20" elif "IMET4" in _type: logging.debug( - "Scanner #%s - Detected a iMet-4 Sonde! (Score: %.2f, Offset: %.1f Hz)" - % (str(device_idx), _score, _offset_est) + "Scanner (%s) - Detected a iMet-4 Sonde! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) ) _sonde_type = "IMET" elif "IMET1" in _type: logging.debug( - "Scanner #%s - Detected a iMet Sonde! (Type %s - Unsupported) (Score: %.2f)" - % (str(device_idx), _type, _score) + "Scanner (%s) - Detected a iMet Sonde! (Type %s - Unsupported) (Score: %.2f)" + % (_sdr_name, _type, _score) ) _sonde_type = "IMET1" elif "IMETafsk" in _type: logging.debug( - "Scanner #%s - Detected a iMet Sonde! (Type %s - Unsupported) (Score: %.2f)" - % (str(device_idx), _type, _score) + "Scanner (%s) - Detected a iMet Sonde! (Type %s - Unsupported) (Score: %.2f)" + % (_sdr_name, _type, _score) ) _sonde_type = "IMET1" elif "IMET5" in _type: logging.debug( - "Scanner #%s - Detected a iMet-54 Sonde! (Score: %.2f)" - % (str(device_idx), _score) + "Scanner (%s) - Detected a iMet-54 Sonde! (Score: %.2f)" + % (_sdr_name, _score) ) _sonde_type = "IMET5" elif "LMS6" in _type: logging.debug( - "Scanner #%s - Detected a LMS6 Sonde! (Score: %.2f, Offset: %.1f Hz)" - % (str(device_idx), _score, _offset_est) + "Scanner (%s) - Detected a LMS6 Sonde! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) ) _sonde_type = "LMS6" elif "C34" in _type: logging.debug( - "Scanner #%s - Detected a Meteolabor C34/C50 Sonde! (Not yet supported...) (Score: %.2f)" - % (str(device_idx), _score) + "Scanner (%s) - Detected a Meteolabor C34/C50 Sonde! (Not yet supported...) (Score: %.2f)" + % (_sdr_name, _score) ) _sonde_type = "C34C50" elif "MRZ" in _type: logging.debug( - "Scanner #%s - Detected a Meteo-Radiy MRZ Sonde! (Score: %.2f)" - % (str(device_idx), _score) + "Scanner (%s) - Detected a Meteo-Radiy MRZ Sonde! (Score: %.2f)" + % (_sdr_name, _score) ) if _score < 0: _sonde_type = "-MRZ" @@ -520,8 +565,8 @@ def detect_sonde( elif "MK2LMS" in _type: logging.debug( - "Scanner #%s - Detected a 1680 MHz LMS6 Sonde (MK2A Telemetry)! (Score: %.2f, Offset: %.1f Hz)" - % (str(device_idx), _score, _offset_est) + "Scanner (%s) - Detected a 1680 MHz LMS6 Sonde (MK2A Telemetry)! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) ) if _score < 0: _sonde_type = "-MK2LMS" @@ -530,14 +575,26 @@ def detect_sonde( elif "MEISEI" in _type: logging.debug( - "Scanner #%s - Detected a Meisei Sonde! (Score: %.2f, Offset: %.1f Hz)" - % (str(device_idx), _score, _offset_est) + "Scanner (%s) - Detected a Meisei Sonde! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) ) # Not currently sure if we expect to see inverted Meisei sondes. if _score < 0: _sonde_type = "-MEISEI" else: _sonde_type = "MEISEI" + + elif "MTS01" in _type: + logging.debug( + "Scanner (%s) - Detected a Meteosis MTS01 Sonde! (Score: %.2f, Offset: %.1f Hz)" + % (_sdr_name, _score, _offset_est) + ) + # Not currently sure if we expect to see inverted Meteosis sondes. + if _score < 0: + _sonde_type = "-MTS01" + else: + _sonde_type = "MTS01" + else: _sonde_type = None @@ -549,7 +606,7 @@ def detect_sonde( # class SondeScanner(object): """Radiosonde Scanner - Continuously scan for radiosondes using a RTLSDR, and pass results onto a callback function + Continuously scan for radiosondes using a SDR, and pass results onto a callback function """ # Allow up to X consecutive scan errors before giving up. @@ -574,12 +631,20 @@ def __init__( max_peaks=10, scan_check_interval=10, rs_path="./", - sdr_power="rtl_power", - sdr_fm="rtl_fm", - device_idx=0, + + sdr_type="RTLSDR", + sdr_hostname="localhost", + sdr_port=5555, + ss_iq_path = "./ss_iq", + ss_power_path = "./ss_power", + + rtl_power_path="rtl_power", + rtl_fm_path="rtl_fm", + rtl_device_idx=0, gain=-1, ppm=0, bias=False, + save_detection_audio=False, temporary_block_list={}, temporary_block_time=60, @@ -609,12 +674,26 @@ def __init__( max_peaks (int): Maximum number of peaks to search over. Peaks are ordered by signal power before being limited to this number. scan_check_interval (int): If we are using a only_scan list, re-check the RTLSDR works every X scan runs. rs_path (str): Path to the RS binaries (i.e rs_detect). Defaults to ./ - sdr_power (str): Path to rtl_power, or drop-in equivalent. Defaults to 'rtl_power' - sdr_fm (str): Path to rtl_fm, or drop-in equivalent. Defaults to 'rtl_fm' + + sdr_type (str): 'RTLSDR', 'Spyserver' or 'KA9Q' + + Arguments for KA9Q SDR Server / SpyServer: + sdr_hostname (str): Hostname of KA9Q Server + sdr_port (int): Port number of KA9Q Server + + Arguments for RTLSDRs: + rtl_power_path (str): Path to rtl_power, or drop-in equivalent. Defaults to 'rtl_power' + rtl_fm_path (str): Path to rtl_fm, or drop-in equivalent. Defaults to 'rtl_fm' + rtl_device_idx (int or str): Device index or serial number of the RTLSDR. Defaults to 0 (the first SDR found). + ppm (int): SDR Frequency accuracy correction, in ppm. + gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. + bias (bool): If True, enable the bias tee on the SDR. + device_idx (int): SDR Device index. Defaults to 0 (the first SDR found). ppm (int): SDR Frequency accuracy correction, in ppm. gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. bias (bool): If True, enable the bias tee on the SDR. + save_detection_audio (bool): Save the audio used in each detecton to detect_.wav 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. @@ -640,12 +719,21 @@ def __init__( self.scan_delay = scan_delay self.max_peaks = max_peaks self.rs_path = rs_path - self.sdr_power = sdr_power - self.sdr_fm = sdr_fm - self.device_idx = device_idx + + self.sdr_type = sdr_type + + self.sdr_hostname = sdr_hostname + self.sdr_port = sdr_port + self.ss_iq_path = ss_iq_path + self.ss_power_path = ss_power_path + + self.rtl_power_path = rtl_power_path + self.rtl_fm_path = rtl_fm_path + self.rtl_device_idx = rtl_device_idx self.gain = gain self.ppm = ppm self.bias = bias + self.callback = callback self.save_detection_audio = save_detection_audio @@ -672,12 +760,17 @@ def __init__( # This will become our scanner thread. self.sonde_scan_thread = None - # Test if the supplied RTLSDR is working. - _rtlsdr_ok = rtlsdr_test(device_idx) + # Test if the supplied SDR is working. + _sdr_ok = test_sdr( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + ss_iq_path = self.ss_iq_path, + check_freq = 1e6*(self.max_freq+self.min_freq)/2.0 + ) - # TODO: How should this error be handled? - if not _rtlsdr_ok: - self.log_error("RTLSDR #%s non-functional - exiting." % device_idx) + if not _sdr_ok: self.sonde_scanner_running = False self.exit_state = "FAILED SDR" return @@ -729,11 +822,20 @@ def scan_loop(self): if len(self.only_scan) > 0: self.scan_counter += 1 if (self.scan_counter % self.scan_check_interval) == 0: - self.log_debug("Performing periodic check of RTLSDR.") - _rtlsdr_ok = rtlsdr_test(self.device_idx) - if not _rtlsdr_ok: + self.log_debug("Performing periodic check of SDR.") + + _sdr_ok = test_sdr( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port, + ss_iq_path = self.ss_iq_path, + check_freq = 1e6*(self.max_freq+self.min_freq)/2.0 + ) + + if not _sdr_ok: self.log_error( - "Unrecoverable RTLSDR error. Closing scan thread." + "Unrecoverable SDR error. Closing scan thread." ) break @@ -741,17 +843,17 @@ def scan_loop(self): _results = self.sonde_search() except (IOError, ValueError) as e: - # No log file produced. Reset the RTLSDR and try again. + # No log file produced. Reset the SDR and try again. # traceback.print_exc() - self.log_warning("RTLSDR produced no output... resetting and retrying.") + self.log_warning("SDR produced no output... resetting and retrying.") self.error_retries += 1 - # Attempt to reset the RTLSDR. - if self.device_idx == "0": - # If the device ID is 0, we assume we only have a single RTLSDR on this system. - reset_all_rtlsdrs() - else: - # Otherwise, we reset the specific RTLSDR - reset_rtlsdr_by_serial(self.device_idx) + # 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 + ) time.sleep(10) continue @@ -794,31 +896,32 @@ def sonde_search(self, first_only=False): if len(self.only_scan) == 0: # No only_scan frequencies provided - perform a scan. - run_rtl_power( - self.min_freq * 1e6, - self.max_freq * 1e6, - self.search_step, - filename="log_power_%s.csv" % self.device_idx, - dwell=self.scan_dwell_time, - sdr_power=self.sdr_power, - device_idx=self.device_idx, + + (freq, power, step) = get_power_spectrum( + sdr_type=self.sdr_type, + frequency_start=self.min_freq * 1e6, + frequency_stop=self.max_freq * 1e6, + step=self.search_step, + integration_time=self.scan_dwell_time, + rtl_device_idx=self.rtl_device_idx, + rtl_power_path=self.rtl_power_path, ppm=self.ppm, gain=self.gain, bias=self.bias, + sdr_hostname=self.sdr_hostname, + sdr_port=self.sdr_port, + ss_power_path = self.ss_power_path ) # Exit opportunity. if self.sonde_scanner_running == False: return [] - # Read in result. - # This step will throw an IOError if the file does not exist. - (freq, power, step) = read_rtl_power("log_power_%s.csv" % self.device_idx) # Sanity check results. - if step == 0 or len(freq) == 0 or len(power) == 0: + if step == None or len(freq) == 0 or len(power) == 0: # Otherwise, if a file has been written but contains no data, it can indicate # an issue with the RTLSDR. Sometimes these issues can be resolved by issuing a usb reset to the RTLSDR. - raise ValueError("Invalid Log File") + raise ValueError("Error getting PSD") # Update the global scan result (_freq_decimate, _power_decimate) = peak_decimation(freq / 1e6, power, 10) @@ -829,7 +932,9 @@ def sonde_search(self, first_only=False): scan_result["peak_lvl"] = [] # Rough approximation of the noise floor of the received power spectrum. - power_nf = np.mean(power) + # Switched to use a Median instead of a Mean 2022-04-02. Should remove outliers better. + power_nf = np.median(power) + logging.debug(f"Noise Floor Estimate: {power_nf:.1f} dB uncal") # Pass the threshold data to the web client for plotting scan_result["threshold"] = power_nf @@ -862,6 +967,13 @@ def sonde_search(self, first_only=False): _, peak_idx = np.unique(peak_frequencies, return_index=True) peak_frequencies = peak_frequencies[np.sort(peak_idx)] + # Remove outside min_freq and max_freq. + _index = np.argwhere( + (peak_frequencies < (self.min_freq * 1e6 - (self.quantization / 2.0))) | + (peak_frequencies > (self.max_freq * 1e6 + (self.quantization / 2.0))) + ) + peak_frequencies = np.delete(peak_frequencies, _index) + # Never scan list & Temporary block list behaviour change as of v1.2.3 # Was: peak_frequencies==_frequency (This only matched an exact frequency in the never_scan list) # Now (1.2.3): Block if the peak frequency is within +/-quantization/2.0 of a never_scan or blocklist frequency. @@ -968,8 +1080,12 @@ def sonde_search(self, first_only=False): (detected, offset_est) = detect_sonde( _freq, - sdr_fm=self.sdr_fm, - device_idx=self.device_idx, + sdr_type=self.sdr_type, + sdr_hostname=self.sdr_hostname, + sdr_port=self.sdr_port, + ss_iq_path = self.ss_iq_path, + rtl_fm_path=self.rtl_fm_path, + rtl_device_idx=self.rtl_device_idx, ppm=self.ppm, gain=self.gain, bias=self.bias, @@ -1054,28 +1170,52 @@ def log_debug(self, line): Args: line (str): Message to be logged. """ - logging.debug("Scanner #%s - %s" % (self.device_idx, line)) + _sdr_name = get_sdr_name( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port + ) + logging.debug(f"Scanner ({_sdr_name}) - {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("Scanner #%s - %s" % (self.device_idx, line)) + _sdr_name = get_sdr_name( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port + ) + logging.info(f"Scanner ({_sdr_name}) - {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("Scanner #%s - %s" % (self.device_idx, line)) + _sdr_name = get_sdr_name( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port + ) + logging.error(f"Scanner ({_sdr_name}) - {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("Scanner #%s - %s" % (self.device_idx, line)) + _sdr_name = get_sdr_name( + self.sdr_type, + rtl_device_idx = self.rtl_device_idx, + sdr_hostname = self.sdr_hostname, + sdr_port = self.sdr_port + ) + logging.warning(f"Scanner ({_sdr_name}) - {line}") if __name__ == "__main__": diff --git a/auto_rx/autorx/sdr_wrappers.py b/auto_rx/autorx/sdr_wrappers.py new file mode 100644 index 00000000..62af89d1 --- /dev/null +++ b/auto_rx/autorx/sdr_wrappers.py @@ -0,0 +1,669 @@ +#!/usr/bin/env python +# +# radiosonde_auto_rx - SDR Abstraction +# +# Copyright (C) 2022 Mark Jessop +# Released under GNU GPL v3 or later +# +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 + + +def test_sdr( + sdr_type: str, + rtl_device_idx = "0", + sdr_hostname = "", + sdr_port = 5555, + ss_iq_path = "./ss_iq", + ss_power_path = "./ss_power", + check_freq = 401500000 +): + """ + Test the prescence / functionality of a SDR. + + sdr_type (str): 'RTLSDR', 'SpyServer' or 'KA9Q' + + Arguments for RTLSDRs: + rtl_device_id (str) - Device ID for a RTLSDR + + Arguments for KA9Q SDR Server / SpyServer: + sdr_hostname (str): Hostname of KA9Q Server + sdr_port (int): Port number of KA9Q Server + + Arguments for SpyServer Client: + ss_iq_path (str): Path to spyserver IQ client utility. + ss_power_path (str): Path to spyserver power utility. + """ + + if sdr_type == "RTLSDR": + # Use the existing rtlsdr_test code, which tries to read some samples + # from the specified RTLSDR + _ok = rtlsdr_test(rtl_device_idx) + if not _ok: + logging.error(f"RTLSDR #{rtl_device_idx} non-functional.") + + return _ok + + + elif sdr_type == "KA9Q": + # To be implemented + _ok = False + + if not _ok: + logging.error(f"KA9Q Server {sdr_hostname}:{sdr_port} non-functional.") + + return _ok + + elif sdr_type == "SpyServer": + # Test connectivity to a SpyServer by trying to grab some samples. + + if not os.path.isfile(ss_iq_path): + logging.critical("Could not find ss_iq binary! This may need to be compiled.") + return False + + _cmd = ( + f"timeout 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" + ) + + logging.debug(f"SpyServer - Testing 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"SpyServer ({sdr_hostname}:{sdr_port}) - ss_iq call failed with return code {e.returncode}." + ) + return False + + return True + + else: + logging.error(f"Test SDR: Unknown SDR Type {sdr_type}") + return False + + + +def reset_sdr( + sdr_type: str, + rtl_device_idx = "0", + sdr_hostname = "", + sdr_port = 5555 + ): + """ + Attempt to reset a SDR. Only used for RTLSDRs. + + sdr_type (str): 'RTLSDR', 'Spyserver' or 'KA9Q' + + Arguments for RTLSDRs: + rtl_device_id (str) - Device ID for a RTLSDR + + Arguments for KA9Q SDR Server / SpyServer: + sdr_hostname (str): Hostname of KA9Q Server + sdr_port (int): Port number of KA9Q Server + """ + + if sdr_type == "RTLSDR": + # Attempt to reset the RTLSDR. + if rtl_device_idx == "0": + # If the device ID is 0, we assume we only have a single RTLSDR on this system. + reset_all_rtlsdrs() + else: + # Otherwise, we reset the specific RTLSDR + reset_rtlsdr_by_serial(rtl_device_idx) + + else: + logging.debug(f"No reset ability for SDR type {sdr_type}") + + +def get_sdr_name( + sdr_type: str, + rtl_device_idx = "0", + sdr_hostname = "", + sdr_port = 5555 + ): + """ + Get a human-readable name of the currenrt SDR + + sdr_type (str): 'RTLSDR', 'Spyserver' or 'KA9Q' + + Arguments for RTLSDRs: + rtl_device_id (str) - Device ID for a RTLSDR + + Arguments for KA9Q SDR Server / SpyServer: + sdr_hostname (str): Hostname of KA9Q Server + sdr_port (int): Port number of KA9Q Server + """ + + if sdr_type == "RTLSDR": + return f"RTLSDR {rtl_device_idx}" + + elif sdr_type == "KA9Q": + return f"KA9Q {sdr_hostname}:{sdr_port}" + + elif sdr_type == "SpyServer": + return f"SpyServer {sdr_hostname}:{sdr_port}" + + else: + return f"UNKNOWN {sdr_type}" + + +def shutdown_sdr( + sdr_type: str, + sdr_id: str + ): + """ + Function to trigger shutdown/cleanup of some SDR types. + Currently only required for the KA9Q server. + + sdr_type (str): 'RTLSDR', 'Spyserver' or 'KA9Q' + sdr_id (str): The global ID of the SDR to be shut down. + """ + + if sdr_type == "KA9Q": + # TODO - KA9Q Server channel cleanup. + logging.debug(f"TODO - Cleanup for SDR type {sdr_type}") + pass + else: + logging.debug(f"No shutdown action required for SDR type {sdr_type}") + + return + + + +def get_sdr_iq_cmd( + sdr_type: str, + frequency: int, + sample_rate: int, + rtl_device_idx = "0", + rtl_fm_path = "rtl_fm", + fast_filter: bool = False, + dc_block: bool = False, + ppm = 0, + gain = None, + bias = False, + sdr_hostname = "", + sdr_port = 5555, + ss_iq_path = "./ss_iq" +): + """ + Get a command-line argument to get IQ (signed 16-bit) from a SDR + for a given frequency and bandwidth. + + sdr_type (str): 'RTLSDR', 'Spyserver' or 'KA9Q' + frequency (int): Centre frequency in Hz + sample_rate (int): Sample rate in Hz + + Arguments for RTLSDRs: + rtl_device_idx (str) - Device ID for a RTLSDR + rtl_fm_path (str) - Path to rtl_fm. Defaults to just "rtl_fm" + ppm (int): SDR Frequency accuracy correction, in ppm. + gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. + bias (bool): If True, enable the bias tee on the SDR. + fast_filter (bool): If true, drop the -F9 higher quality filter for rtl_fm + dc_block (bool): If true, enable a DC block step. + + Arguments for KA9Q SDR Server / SpyServer: + sdr_hostname (str): Hostname of KA9Q Server + sdr_port (int): Port number of KA9Q Server + + Arguments for SpyServer Client: + ss_iq_path (str): Path to spyserver IQ client utility. + + """ + + # DC removal commmand, using rs1729's IQ_dec utility. + # This helps remove the residual DC offset in the 16-bit outputs from + # both rtl_fm and ss_iq. + # We currently only use this on narrowband sondes. + if sample_rate > 80000: + _dc_ifbw = f"--IFbw {int(sample_rate/1000)} " + else: + _dc_ifbw = "" + _dc_remove = f"./iq_dec --bo 16 {_dc_ifbw}- {int(sample_rate)} 16 2>/dev/null |" + + if sdr_type == "RTLSDR": + _gain = "" + _agc = "" + if gain: + if gain >= 0: + _gain = f"-g {gain:.1f} " + elif gain == -2: + _agc = f"-E agc " + + _cmd = ( + f"{rtl_fm_path} -M raw " + f"{'' if fast_filter else '-F9 '}" + f"{'-T ' if bias else ''}" + f"-p {int(ppm)} " + f"-d {str(rtl_device_idx)} " + f"{_gain}" + f"{_agc}" + f"-s {int(sample_rate)} " + f"-f {int(frequency)} " + f"- 2>/dev/null | " + ) + + if dc_block: + _cmd += _dc_remove + + return _cmd + + if sdr_type == "SpyServer": + _cmd = ( + f"{ss_iq_path} " + f"-f {frequency} " + f"-s {int(sample_rate)} " + f"-r {sdr_hostname} -q {sdr_port} - 2>/dev/null|" + ) + + if dc_block: + _cmd += _dc_remove + + return _cmd + + else: + logging.critical(f"IQ Source - Unsupported SDR type {sdr_type}") + return "false |" + + + +def get_sdr_fm_cmd( + sdr_type: str, + frequency: int, + filter_bandwidth: int, + sample_rate: int, + highpass = None, + lowpass = None, + rtl_device_idx = "0", + rtl_fm_path = "rtl_fm", + ppm = 0, + gain = -1, + bias = False, + sdr_hostname = "", + sdr_port = 1234, +): + """ + Get a command-line argument to get FM demodulated audio (signed 16-bit) from a SDR + for a given frequency and bandwidth. + + sdr_type (str): 'RTLSDR', 'Spyserver' or 'KA9Q' + frequency (int): Centre frequency in Hz + filter_bandwidth (int): FM Demodulator filter bandwidth in Hz + sample_rate (int): Output sample rate in Hz + + Optional arguments + highpass (int): If provided, add a high-pass filter after the FM demodulator. + lowpass (int): If provided, add a low-pass filter after the FM demodulator. + + Arguments for RTLSDRs: + rtl_device_idx (str) - Device ID for a RTLSDR + rtl_fm_path (str) - Path to rtl_fm. Defaults to just "rtl_fm" + ppm (int): SDR Frequency accuracy correction, in ppm. + gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. + bias (bool): If True, enable the bias tee on the SDR. + + Arguments for KA9Q SDR Server / SpyServer: + sdr_hostname (str): Hostname of KA9Q Server + sdr_port (int): Port number of KA9Q Server + + """ + + + if sdr_type == "RTLSDR": + _gain = "" + if gain: + if gain >= 0: + _gain = f"-g {gain:.1f} " + + _cmd = ( + f"{rtl_fm_path} -M fm -F9 " + f"{'-T ' if bias else ''}" + f"-p {int(ppm)} " + f"-d {str(rtl_device_idx)} " + f"{_gain}" + f"-s {int(filter_bandwidth)} " + f"-f {int(frequency)} " + f"2>/dev/null | " + ) + + # Add in resampling / convertion to wav using sox. + _cmd += f"sox -t raw -r {int(filter_bandwidth)} -e s -b 16 -c 1 - -r {int(sample_rate)} -b 16 -t wav - " + + if highpass: + _cmd += f"highpass {int(highpass)} " + + if lowpass: + _cmd += f"lowpass {int(lowpass)} " + + _cmd += "2> /dev/null | " + + return _cmd + + else: + logging.critical(f"FM Demod Source - Unsupported SDR type {sdr_type}") + return "false |" + + +def read_rtl_power_log(log_filename, sdr_name): + """ + Read in a rtl_power compatible log output file. + + Arguments: + log_filename (str): Filename to read + sdr_name (str): SDR name used for logging errors. + """ + + # OK, now try to read in the saved data. + # Output buffers. + freq = np.array([]) + power = np.array([]) + + freq_step = 0 + + # Open file. + f = open(log_filename, "r") + + # rtl_power log files are csv's, with the first 6 fields in each line describing the time and frequency scan parameters + # for the remaining fields, which contain the power samples. + + for line in f: + # Split line into fields. + fields = line.split(",", 6) + + if len(fields) < 6: + logging.error( + f"Scanner ({sdr_name}) - Invalid number of samples in input file - corrupt?" + ) + raise Exception( + f"Scanner ({sdr_name}) - Invalid number of samples in input file - corrupt?" + ) + + start_date = fields[0] + start_time = fields[1] + start_freq = float(fields[2]) + stop_freq = float(fields[3]) + freq_step = float(fields[4]) + n_samples = int(fields[5]) + # freq_range = np.arange(start_freq,stop_freq,freq_step) + samples = np.fromstring(fields[6], sep=",") + freq_range = np.linspace(start_freq, stop_freq, len(samples)) + + # Add frequency range and samples to output buffers. + freq = np.append(freq, freq_range) + power = np.append(power, samples) + + f.close() + + # Sanitize power values, to remove the nan's that rtl_power puts in there occasionally. + power = np.nan_to_num(power) + + return (freq, power, freq_step) + + +def get_power_spectrum( + sdr_type: str, + frequency_start: int = 400050000, + frequency_stop: int = 403000000, + step: int = 800, + integration_time: int = 20, + rtl_device_idx = "0", + rtl_power_path = "rtl_power", + ppm = 0, + gain = None, + bias = False, + sdr_hostname = "", + sdr_port = 5555, + ss_power_path = "./ss_power" +): + """ + Get power spectral density data from a SDR. + + Arguments: + + sdr_type (str): 'RTLSDR', 'Spyserver' or 'KA9Q' + + frequency_start (int): Start frequency for the PSD, Hz + frequency_stop (int): Stop frequency for the PSD, Hz + step (int): Requested frequency step for the PSD, Hz. May not always be honoured. + integration_time (int): Integration time in seconds. + + Arguments for RTLSDRs: + rtl_device_idx (str): Device ID for a RTLSDR + rtl_power_path (str): Path to rtl_power. Defaults to just "rtl_power" + ppm (int): SDR Frequency accuracy correction, in ppm. + gain (int): SDR Gain setting, in dB. A gain setting of -1 enables the RTLSDR AGC. + bias (bool): If True, enable the bias tee on the SDR. + + Arguments for KA9Q SDR Server / SpyServer: + sdr_hostname (str): Hostname of KA9Q Server + sdr_port (int): Port number of KA9Q Server + + Arguments for SpyServer Client: + ss_power_path (str): Path to spyserver power utility. + ss_iq_path (str): Path to spyserver IQ client utility. + + + Returns: + (freq, power, step) Tuple + + freq (np.array): Array of frequencies, in Hz + power (np.array): Array of uncalibrated power estimates, in Hz + step (float): Frequency step of the output data, in Hz. + + Returns (None, None, None) if an error occurs. + + """ + + # No support for getting spectrum data on any other SDR source right now. + # Override sdr selection. + + + if sdr_type == "RTLSDR": + # Use rtl_power to obtain power spectral density data + + # Create filename to output to. + _log_filename = 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}" + + _gain = "" + if gain: + if gain >= 0: + _gain = f"-g {gain:.1f} " + + _rtl_power_cmd = ( + f"{_timeout_cmd} {rtl_power_path} " + f"{'-T ' if bias else ''}" + f"-p {int(ppm)} " + f"-d {str(rtl_device_idx)} " + f"{_gain}" + f"-f {frequency_start}:{frequency_stop}:{step} " + f"-i {integration_time} -1 -c 25% " + f"{_log_filename}" + ) + + _sdr_name = get_sdr_name( + sdr_type=sdr_type, + rtl_device_idx=rtl_device_idx, + sdr_hostname=sdr_hostname, + sdr_port=sdr_port + ) + + logging.info(f"Scanner ({_sdr_name}) - Running frequency scan.") + logging.debug( + f"Scanner ({_sdr_name}) - Running command: {_rtl_power_cmd}" + ) + + try: + _output = subprocess.check_output( + _rtl_power_cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + logging.critical( + f"Scanner ({_sdr_name}) - rtl_power call failed with return code {e.returncode}." + ) + # Look at the error output in a bit more details. + _output = e.output.decode("ascii") + if "No supported devices found" in _output: + logging.critical( + f"Scanner ({_sdr_name}) - rtl_power could not find device with ID {rtl_device_idx}, is your configuration correct?" + ) + elif "illegal option" in _output: + if bias: + logging.critical( + f"Scanner ({_sdr_name}) - rtl_power reported an illegal option was used. Are you using a rtl_power version with bias tee support?" + ) + else: + logging.critical( + f"Scanner ({_sdr_name}) - rtl_power reported an illegal option was used. (This shouldn't happen... are you running an ancient version?)" + ) + else: + # Something else odd happened, dump the entire error output to the log for further analysis. + logging.critical( + f"Scanner ({_sdr_name}) - rtl_power reported error: {_output}" + ) + + return (None, None, None) + + return read_rtl_power_log(_log_filename, _sdr_name) + + elif sdr_type == "SpyServer": + # Use a spyserver to obtain power spectral density data + + # Create filename to output to. + _log_filename = 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}" + + _frequency_centre = int(frequency_start + (frequency_stop-frequency_start)/2.0) + + # Note we are using the '-o' option here, which allows us to still get + # spectrum data even if we have specified a frequency which is out of + # the range of a locked spyserver. + _ss_power_cmd = ( + f"{_timeout_cmd} {ss_power_path} " + f"-f {_frequency_centre} " + f"-i {integration_time} -1 -o " + f"-r {sdr_hostname} -q {sdr_port} " + f"{_log_filename}" + ) + + _sdr_name = get_sdr_name( + sdr_type=sdr_type, + rtl_device_idx=rtl_device_idx, + sdr_hostname=sdr_hostname, + sdr_port=sdr_port + ) + + logging.info(f"Scanner ({_sdr_name}) - Running frequency scan.") + logging.debug( + f"Scanner ({_sdr_name}) - Running command: {_ss_power_cmd}" + ) + + try: + _output = subprocess.check_output( + _ss_power_cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + logging.critical( + f"Scanner ({_sdr_name}) - ss_power 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"Scanner ({_sdr_name}) - Centre of scan range ({_frequency_centre} Hz) outside of allowed SpyServer tuning range." + ) + else: + logging.critical( + f"Scanner ({_sdr_name}) - Other Error: {_output}" + ) + + return (None, None, None) + + return read_rtl_power_log(_log_filename, _sdr_name) + + else: + # Unsupported SDR Type + logging.critical(f"Get PSD - Unsupported SDR Type: {sdr_type}") + return (None, None, None) + +if __name__ == "__main__": + + logging.basicConfig( + format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG + ) + + _sdr_type = "RTLSDR" + + #print(f"Test RTLSDR 0: {test_sdr(_sdr_type)}") + + _freq = 401500000 + _sample_rate = 48000 + _fm_bw = 15000 + _device_idx = "00000004" + + print(f"RTLSDR IQ (AGC): {get_sdr_iq_cmd(_sdr_type, _freq, _sample_rate)}") + print(f"RTLSDR IQ (AGC + Fixed device): {get_sdr_iq_cmd(_sdr_type, _freq, _sample_rate, rtl_device_idx=_device_idx)}") + print(f"RTLSDR IQ (Fixed Gain): {get_sdr_iq_cmd(_sdr_type, _freq, _sample_rate, gain=30.0, bias=True)}") + + print(f"RTLSDR FM (AGC): {get_sdr_fm_cmd(_sdr_type, _freq, _fm_bw, _sample_rate)}") + print(f"RTLSDR FM (Fixed Gain): {get_sdr_fm_cmd(_sdr_type, _freq, _fm_bw, _sample_rate, gain=30.0, bias=True, highpass=20)}") + + # (freq, power, step) = get_power_spectrum( + # sdr_type="RTLSDR" + # ) + + (freq, power, step) = get_power_spectrum( + sdr_type="SpyServer", + sdr_hostname="10.0.0.222", + sdr_port=5555, + frequency_start=400100000, + frequency_stop=404900000 + ) + print(freq) + print(power) + print(step) \ No newline at end of file diff --git a/auto_rx/autorx/sondehub.py b/auto_rx/autorx/sondehub.py index 66f67ff1..141c59d0 100644 --- a/auto_rx/autorx/sondehub.py +++ b/auto_rx/autorx/sondehub.py @@ -18,16 +18,10 @@ import os import requests import time +from queue import Queue from threading import Thread from email.utils import formatdate -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue - class SondehubUploader(object): """ Sondehub Uploader Class. @@ -61,6 +55,7 @@ def __init__( """ self.upload_rate = upload_rate + self.actual_upload_rate = upload_rate # Allow for the upload rate to be tweaked... self.upload_timeout = upload_timeout self.upload_retries = upload_retries self.user_callsign = user_callsign @@ -69,6 +64,8 @@ def __init__( self.contact_email = contact_email self.user_position_update_rate = user_position_update_rate + self.slower_uploads = False + if self.user_position is None: self.inhibit_upload = True else: @@ -80,20 +77,10 @@ def __init__( # Record of when we last uploaded a user station position to Sondehub. self.last_user_position_upload = 0 - try: - # Python 2 check. Python 2 doesnt have gzip.compress so this will throw an exception. - gzip.compress(b"\x00\x00") - - # Start queue processing thread. - self.input_processing_running = True - self.input_process_thread = Thread(target=self.process_queue) - self.input_process_thread.start() - - except: - logging.error( - "Detected Python 2.7, which does not support gzip.compress. Sondehub DB uploading will be disabled." - ) - self.input_processing_running = False + # Start queue processing thread. + self.input_processing_running = True + self.input_process_thread = Thread(target=self.process_queue) + self.input_process_thread.start() def update_station_position(self, lat, lon, alt): """ Update the internal station position record. Used when determining the station position by GPSD """ @@ -175,6 +162,10 @@ def reformat_data(self, telemetry): if "dfmcode" in telemetry: _output["dfmcode"] = telemetry["dfmcode"] + # We are handling DFM packets. We need a few more of these in an upload + # for our packets to pass the Sondehub z-check. + self.slower_uploads = True + elif telemetry["type"].startswith("M10") or telemetry["type"].startswith("M20"): _output["manufacturer"] = "Meteomodem" _output["type"] = telemetry["type"] @@ -213,10 +204,27 @@ def reformat_data(self, telemetry): _output["type"] = telemetry["subtype"] _output["serial"] = telemetry["id"].split("-")[1] + elif telemetry["type"] == "IMS100": + _output["manufacturer"] = "Meisei" + _output["type"] = "iMS-100" + _output["serial"] = telemetry["id"].split("-")[1] + + elif telemetry["type"] == "RS11G": + _output["manufacturer"] = "Meisei" + _output["type"] = "RS-11G" + _output["serial"] = telemetry["id"].split("-")[1] + elif telemetry["type"] == "MRZ": _output["manufacturer"] = "Meteo-Radiy" _output["type"] = "MRZ" _output["serial"] = telemetry["id"][4:] + if "subtype" in telemetry: + _output["subtype"] = telemetry["subtype"] + + elif telemetry["type"] == "MTS01": + _output["manufacturer"] = "Meteosis" + _output["type"] = "MTS01" + _output["serial"] = telemetry["id"].split("-")[1] else: self.log_error("Unknown Radiosonde Type %s" % telemetry["type"]) @@ -271,6 +279,13 @@ def reformat_data(self, telemetry): if "bt" in telemetry: _output["burst_timer"] = telemetry["bt"] + # Time / Position reference information (e.g. GPS or something else) + if "ref_position" in telemetry: + _output["ref_position"] = telemetry["ref_position"] + + if "ref_datetime" in telemetry: + _output["ref_datetime"] = telemetry["ref_datetime"] + # Handle the additional SNR and frequency estimation if we have it if "snr" in telemetry: _output["snr"] = telemetry["snr"] @@ -310,8 +325,13 @@ def process_queue(self): ) > self.user_position_update_rate * 3600: self.station_position_upload() + # If we are encounting DFM packets we need to upload at a slower rate so + # that we have enough uploaded packets to pass z-check. + if self.slower_uploads: + self.actual_upload_rate = min(30,int(self.upload_rate*1.5)) + # Sleep while waiting for some new data. - for i in range(self.upload_rate): + for i in range(self.actual_upload_rate): time.sleep(1) if self.input_processing_running == False: break @@ -385,7 +405,7 @@ def upload_telemetry(self, telem_list): _retries += 1 continue - elif _req.status_code == 201: + elif (_req.status_code == 201) or (_req.status_code == 202): self.log_debug( "Sondehub reported issue when adding packets to DB. Status Code: %d %s." % (_req.status_code, _req.text) diff --git a/auto_rx/autorx/static/css/c3.min.css b/auto_rx/autorx/static/css/c3.min.css old mode 100755 new mode 100644 index 86778ebe..f18940a5 --- a/auto_rx/autorx/static/css/c3.min.css +++ b/auto_rx/autorx/static/css/c3.min.css @@ -1 +1 @@ -.c3 svg{font:10px sans-serif;-webkit-tap-highlight-color:transparent}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc rect{stroke:#fff;stroke-width:1}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:grey;font-size:2em}.c3-line{stroke-width:1px}.c3-circle{fill:currentColor}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:1;fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-region text{fill-opacity:1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-title{font:14px sans-serif}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #ccc}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#fff}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip .value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:#fff}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max{fill:#777}.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}.c3-chart-arc.c3-target g path{opacity:1}.c3-chart-arc.c3-target.c3-focused g path{opacity:1}.c3-drag-zoom.enabled{pointer-events:all!important;visibility:visible}.c3-drag-zoom.disabled{pointer-events:none!important;visibility:hidden}.c3-drag-zoom .extent{fill-opacity:.1} \ No newline at end of file +.c3 svg{font:10px sans-serif;-webkit-tap-highlight-color:transparent}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc rect{stroke:#fff;stroke-width:1}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:grey;font-size:2em}.c3-line{stroke-width:1px}.c3-circle{fill:currentColor}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:1;fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-region text{fill-opacity:1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-title{font:14px sans-serif}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #ccc}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#fff}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip .value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:#fff}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max{fill:#777}.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}.c3-chart-arc.c3-target g path{opacity:1}.c3-chart-arc.c3-target.c3-focused g path{opacity:1}.c3-drag-zoom.enabled{pointer-events:all!important;visibility:visible}.c3-drag-zoom.disabled{pointer-events:none!important;visibility:hidden}.c3-drag-zoom .extent{fill-opacity:.1} diff --git a/auto_rx/autorx/static/css/main.css b/auto_rx/autorx/static/css/main.css index 915b7d90..899579ac 100644 --- a/auto_rx/autorx/static/css/main.css +++ b/auto_rx/autorx/static/css/main.css @@ -52,12 +52,11 @@ a { height: 100px; width: 100%; position: absolute; - z-index: 10; + z-index: 100; text-align: center; bottom: 0; left: 0; - pointer-events: none; - background: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.9); } .settings { @@ -78,7 +77,7 @@ a { } .settings form { - margin-left: 60px; + margin-left: 35px; color: white; } @@ -121,7 +120,7 @@ a { .settings h2 { font-size: 1.2em; color: white; - text-align: center; +/* text-align: center; */ } .settings p { @@ -131,23 +130,35 @@ a { button { border: none; - color: white; - padding: 12px 24px; - text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin: 4px 2px; transition-duration: 0.4s; cursor: pointer; - background-color: white; - color: black; - border: 2px solid #2196F3; + font-weight: normal; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + padding: 5px 15px; + line-height: 1.42857143; + border-radius: 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; + height: 31px; } button:hover { - background-color: #2196F3; - color: white; + color: #fff; + background-color: #286090; + border-color: #204d74; } button:disabled, @@ -164,7 +175,8 @@ button[disabled]{ } .sidenav table { - font-size: 20px; + font-size: 1.2rem; + font-size: calc(var(--vh, 1vh) * 2); color: white; text-align: center; table-layout: fixed; @@ -177,6 +189,7 @@ button[disabled]{ #main { transition: margin-left .5s; padding: 16px; + padding-top: 5px; flex: auto; display: flex; flex-direction: column; @@ -247,8 +260,8 @@ table { .switch { position: relative; display: inline-block; - width: 60px; - height: 34px; + width: 56px; + height: 28px; } .switch input { @@ -272,10 +285,10 @@ table { .slider:before { position: absolute; content: ""; - height: 26px; - width: 26px; + height: 22px; + width: 22px; left: 4px; - bottom: 4px; + bottom: 3px; background-color: white; -webkit-transition: .4s; transition: .4s; @@ -507,3 +520,7 @@ select[disabled]{ background: #04AA6D; /* Green background */ cursor: pointer; /* Cursor on hover */ } + +button.tabulator-page { + color: #000; +} diff --git a/auto_rx/autorx/static/js/c3.min.js b/auto_rx/autorx/static/js/c3.min.js old mode 100755 new mode 100644 index dc306698..09758a16 --- a/auto_rx/autorx/static/js/c3.min.js +++ b/auto_rx/autorx/static/js/c3.min.js @@ -1,2 +1,2 @@ /* @license C3.js v0.7.20 | (c) C3 Team and other contributors | http://c3js.org/ */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).c3=e()}(this,function(){"use strict";function l(t){var e=this;e.d3=window.d3?window.d3:"undefined"!=typeof require?require("d3"):void 0,e.api=t,e.config=e.getDefaultConfig(),e.data={},e.cache={},e.axes={}}function n(t){this.internal=new l(this),this.internal.loadConfig(t),this.internal.beforeInit(t),this.internal.init(),this.internal.afterInit(t),function e(i,n,r){Object.keys(i).forEach(function(t){n[t]=i[t].bind(r),0/g,">"):t}function c(t){var e=function(t){void 0===t&&(t=window.navigator.userAgent);var e=t.indexOf("MSIE ");return 0e.getTotalLength())break;i--}while(0=this.numberOfItems)throw"INDEX_SIZE_ERR"},window.SVGPathSegList.prototype.getItem=function(t){return this._checkPathSynchronizedToList(),this._checkValidIndex(t),this._list[t]},window.SVGPathSegList.prototype.insertItemBefore=function(t,e){return this._checkPathSynchronizedToList(),e>this.numberOfItems&&(e=this.numberOfItems),t._owningPathSegList&&(t=t.clone()),this._list.splice(e,0,t),(t._owningPathSegList=this)._writeListToPath(),t},window.SVGPathSegList.prototype.replaceItem=function(t,e){return this._checkPathSynchronizedToList(),t._owningPathSegList&&(t=t.clone()),this._checkValidIndex(e),((this._list[e]=t)._owningPathSegList=this)._writeListToPath(),t},window.SVGPathSegList.prototype.removeItem=function(t){this._checkPathSynchronizedToList(),this._checkValidIndex(t);var e=this._list[t];return this._list.splice(t,1),this._writeListToPath(),e},window.SVGPathSegList.prototype.appendItem=function(t){return this._checkPathSynchronizedToList(),t._owningPathSegList&&(t=t.clone()),this._list.push(t),(t._owningPathSegList=this)._writeListToPath(),t},window.SVGPathSegList._pathSegArrayAsString=function(t){var e="",i=!0;return t.forEach(function(t){i?(i=!1,e+=t._asPathString()):e+=" "+t._asPathString()}),e},window.SVGPathSegList.prototype._parsePath=function(t){if(!t||0==t.length)return[];function e(){this.pathSegList=[]}var n=this;e.prototype.appendSegment=function(t){this.pathSegList.push(t)};function i(t){this._string=t,this._currentIndex=0,this._endIndex=this._string.length,this._previousCommand=window.SVGPathSeg.PATHSEG_UNKNOWN,this._skipOptionalSpaces()}i.prototype._isCurrentSpace=function(){var t=this._string[this._currentIndex];return t<=" "&&(" "==t||"\n"==t||"\t"==t||"\r"==t||"\f"==t)},i.prototype._skipOptionalSpaces=function(){for(;this._currentIndex=this._endIndex||this._string.charAt(this._currentIndex)<"0"||"9"=this._endIndex||this._string.charAt(this._currentIndex)<"0"||"9"=this._endIndex)){var t=!1,e=this._string.charAt(this._currentIndex++);if("0"==e)t=!1;else{if("1"!=e)return;t=!0}return this._skipOptionalSpacesOrDelimiter(),t}},i.prototype.parseSegment=function(){var t=this._string[this._currentIndex],e=this._pathSegTypeFromChar(t);if(e==window.SVGPathSeg.PATHSEG_UNKNOWN){if(this._previousCommand==window.SVGPathSeg.PATHSEG_UNKNOWN)return null;if((e=this._nextCommandHelper(t,this._previousCommand))==window.SVGPathSeg.PATHSEG_UNKNOWN)return null}else this._currentIndex++;switch(this._previousCommand=e){case window.SVGPathSeg.PATHSEG_MOVETO_REL:return new window.SVGPathSegMovetoRel(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_MOVETO_ABS:return new window.SVGPathSegMovetoAbs(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_REL:return new window.SVGPathSegLinetoRel(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_ABS:return new window.SVGPathSegLinetoAbs(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL:return new window.SVGPathSegLinetoHorizontalRel(n,this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS:return new window.SVGPathSegLinetoHorizontalAbs(n,this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL:return new window.SVGPathSegLinetoVerticalRel(n,this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS:return new window.SVGPathSegLinetoVerticalAbs(n,this._parseNumber());case window.SVGPathSeg.PATHSEG_CLOSEPATH:return this._skipOptionalSpaces(),new window.SVGPathSegClosePath(n);case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL:var i={x1:this._parseNumber(),y1:this._parseNumber(),x2:this._parseNumber(),y2:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoCubicRel(n,i.x,i.y,i.x1,i.y1,i.x2,i.y2);case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS:i={x1:this._parseNumber(),y1:this._parseNumber(),x2:this._parseNumber(),y2:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoCubicAbs(n,i.x,i.y,i.x1,i.y1,i.x2,i.y2);case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL:i={x2:this._parseNumber(),y2:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoCubicSmoothRel(n,i.x,i.y,i.x2,i.y2);case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS:i={x2:this._parseNumber(),y2:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoCubicSmoothAbs(n,i.x,i.y,i.x2,i.y2);case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL:i={x1:this._parseNumber(),y1:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoQuadraticRel(n,i.x,i.y,i.x1,i.y1);case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS:i={x1:this._parseNumber(),y1:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoQuadraticAbs(n,i.x,i.y,i.x1,i.y1);case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL:return new window.SVGPathSegCurvetoQuadraticSmoothRel(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS:return new window.SVGPathSegCurvetoQuadraticSmoothAbs(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_ARC_REL:i={x1:this._parseNumber(),y1:this._parseNumber(),arcAngle:this._parseNumber(),arcLarge:this._parseArcFlag(),arcSweep:this._parseArcFlag(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegArcRel(n,i.x,i.y,i.x1,i.y1,i.arcAngle,i.arcLarge,i.arcSweep);case window.SVGPathSeg.PATHSEG_ARC_ABS:i={x1:this._parseNumber(),y1:this._parseNumber(),arcAngle:this._parseNumber(),arcLarge:this._parseArcFlag(),arcSweep:this._parseArcFlag(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegArcAbs(n,i.x,i.y,i.x1,i.y1,i.arcAngle,i.arcLarge,i.arcSweep);default:throw"Unknown path seg type."}};var r=new e,a=new i(t);if(!a.initialCommandIsMoveTo())return[];for(;a.hasMoreData();){var o=a.parseSegment();if(!o)return[];r.appendSegment(o)}return r.pathSegList}),String.prototype.padEnd||(String.prototype.padEnd=function(t,e){return t>>=0,e=String(void 0!==e?e:" "),this.length>t?String(this):((t-=this.length)>e.length&&(e+=e.repeat(t/e.length)),String(this)+e.slice(0,t))}),"function"!=typeof Object.assign&&Object.defineProperty(Object,"assign",{value:function(t,e){if(null==t)throw new TypeError("Cannot convert undefined or null to object");for(var i=Object(t),n=1;n'":;\[\]\/|~`{}\\])/g,"\\$1")},l.prototype.selectorTarget=function(t,e){return(e||"")+"."+Y.target+this.getTargetSelectorSuffix(t)},l.prototype.selectorTargets=function(t,e){var i=this;return(t=t||[]).length?t.map(function(t){return i.selectorTarget(t,e)}):null},l.prototype.selectorLegend=function(t){return"."+Y.legendItem+this.getTargetSelectorSuffix(t)},l.prototype.selectorLegends=function(t){var e=this;return t&&t.length?t.map(function(t){return e.selectorLegend(t)}):null},l.prototype.getClipPath=function(t){return"url("+(c(9)?"":document.URL.split("#")[0])+"#"+t+")"},l.prototype.appendClip=function(t,e){return t.append("clipPath").attr("id",e).append("rect")},l.prototype.getAxisClipX=function(t){var e=Math.max(30,this.margin.left);return t?-(1+e):-(e-1)},l.prototype.getAxisClipY=function(t){return t?-20:-this.margin.top},l.prototype.getXAxisClipX=function(){return this.getAxisClipX(!this.config.axis_rotated)},l.prototype.getXAxisClipY=function(){return this.getAxisClipY(!this.config.axis_rotated)},l.prototype.getYAxisClipX=function(){return this.config.axis_y_inner?-1:this.getAxisClipX(this.config.axis_rotated)},l.prototype.getYAxisClipY=function(){return this.getAxisClipY(this.config.axis_rotated)},l.prototype.getAxisClipWidth=function(t){var e=Math.max(30,this.margin.left),i=Math.max(30,this.margin.right);return t?this.width+2+e+i:this.margin.left+20},l.prototype.getAxisClipHeight=function(t){return(t?this.margin.bottom:this.margin.top+this.height)+20},l.prototype.getXAxisClipWidth=function(){return this.getAxisClipWidth(!this.config.axis_rotated)},l.prototype.getXAxisClipHeight=function(){return this.getAxisClipHeight(!this.config.axis_rotated)},l.prototype.getYAxisClipWidth=function(){return this.getAxisClipWidth(this.config.axis_rotated)+(this.config.axis_y_inner?20:0)},l.prototype.getYAxisClipHeight=function(){return this.getAxisClipHeight(this.config.axis_rotated)},l.prototype.generateColor=function(){var t=this.config,e=this.d3,n=t.data_colors,r=L(t.color_pattern)?t.color_pattern:e.schemeCategory10,a=t.data_color,o=[];return function(t){var e,i=t.id||t.data&&t.data.id||t;return n[i]instanceof Function?e=n[i](t):n[i]?e=n[i]:(o.indexOf(i)<0&&o.push(i),e=r[o.indexOf(i)%r.length],n[i]=e),a instanceof Function?a(e,t):e}},l.prototype.generateLevelColor=function(){var t=this.config,n=t.color_pattern,e=t.color_threshold,r="value"===e.unit,a=e.values&&e.values.length?e.values:[],o=e.max||100;return L(e)&&L(n)?function(t){for(var e=n[n.length-1],i=0;is&&(o=o.filter(function(t){return(""+t).indexOf(".")<0}));return o},l.prototype.getGridFilterToRemove=function(t){return t?function(e){var i=!1;return[].concat(t).forEach(function(t){("value"in t&&e.value===t.value||"class"in t&&e.class===t.class)&&(i=!0)}),i}:function(){return!0}},l.prototype.removeGridLines=function(t,e){function i(t){return!r(t)}var n=this.config,r=this.getGridFilterToRemove(t),a=e?Y.xgridLines:Y.ygridLines,o=e?Y.xgridLine:Y.ygridLine;this.main.select("."+a).selectAll("."+o).filter(r).transition().duration(n.transition_duration).style("opacity",0).remove(),e?n.grid_x_lines=n.grid_x_lines.filter(i):n.grid_y_lines=n.grid_y_lines.filter(i)},l.prototype.initEventRect=function(){var t=this,e=t.config;t.main.select("."+Y.chart).append("g").attr("class",Y.eventRects).style("fill-opacity",0),t.eventRect=t.main.select("."+Y.eventRects).append("rect").attr("class",Y.eventRect),e.zoom_enabled&&t.zoom&&(t.eventRect.call(t.zoom).on("dblclick.zoom",null),e.zoom_initialRange&&t.eventRect.transition().duration(0).call(t.zoom.transform,t.zoomTransform(e.zoom_initialRange)))},l.prototype.redrawEventRect=function(){var s=this,c=s.d3,d=s.config;function l(){s.svg.select("."+Y.eventRect).style("cursor",null),s.hideXGridFocus(),s.hideTooltip(),s.unexpandCircles(),s.unexpandBars()}function u(t,e){return e&&(s.isBarType(e.id)||s.dist(e,t)i.bar_width_max?i.bar_width_max:n},l.prototype.getBars=function(t,e){return(e?this.main.selectAll("."+Y.bars+this.getTargetSelectorSuffix(e)):this.main).selectAll("."+Y.bar+(C(t)?"-"+t:""))},l.prototype.expandBars=function(t,e,i){i&&this.unexpandBars(),this.getBars(t,e).classed(Y.EXPANDED,!0)},l.prototype.unexpandBars=function(t){this.getBars(t).classed(Y.EXPANDED,!1)},l.prototype.generateDrawBar=function(t,e){var a=this.config,o=this.generateGetBarPoints(t,e);return function(t,e){var i=o(t,e),n=a.axis_rotated?1:0,r=a.axis_rotated?0:1;return"M "+i[0][n]+","+i[0][r]+" L"+i[1][n]+","+i[1][r]+" L"+i[2][n]+","+i[2][r]+" L"+i[3][n]+","+i[3][r]+" z"}},l.prototype.generateGetBarPoints=function(t,e){var o=this,i=e?o.subXAxis:o.xAxis,n=t.__max__+1,s=o.getBarW(i,n),c=o.getShapeX(s,n,t,!!e),d=o.getShapeY(!!e),l=o.getShapeOffset(o.isBarType,t,!!e),u=s*(o.config.bar_space/2),h=e?o.getSubYScale:o.getYScale;return function(t,e){var i=h.call(o,t.id)(0),n=l(t,e)||i,r=c(t),a=d(t);return o.config.axis_rotated&&(0r.width?o=r.width-a.width:o<0&&(o=4)),o},l.prototype.getYForText=function(t,e,i){var n,r=this,a=d(i);return r.config.axis_rotated?n=(t[0][0]+t[2][0]+.6*a.height)/2:(n=t[2][1],e.value<0||0===e.value&&!r.hasPositiveValue?(n+=a.height,r.isBarType(e)&&r.isSafari()?n-=3:!r.isBarType(e)&&r.isChrome()&&(n+=3)):n+=r.isBarType(e)?-3:-6),null!==e.value||r.config.axis_rotated||(nthis.height&&(n=this.height-4)),n},l.prototype.initTitle=function(){this.title=this.svg.append("text").text(this.config.title_text).attr("class",this.CLASS.title)},l.prototype.redrawTitle=function(){var t=this;t.title.attr("x",t.xForTitle.bind(t)).attr("y",t.yForTitle.bind(t))},l.prototype.xForTitle=function(){var t=this,e=t.config,i=e.title_position||"left",n=0<=i.indexOf("right")?t.currentWidth-t.getTextRect(t.title.node().textContent,t.CLASS.title,t.title.node()).width-e.title_padding.right:0<=i.indexOf("center")?Math.max((t.currentWidth-t.getTextRect(t.title.node().textContent,t.CLASS.title,t.title.node()).width)/2,0):e.title_padding.left;return n},l.prototype.yForTitle=function(){var t=this;return t.config.title_padding.top+t.getTextRect(t.title.node().textContent,t.CLASS.title,t.title.node()).height},l.prototype.getTitlePadding=function(){return this.yForTitle()+this.config.title_padding.bottom},l.prototype.drawColorScale=function(){var t,e,i,n,r,a,o=this,s=o.d3,c=o.config,d=o.data.targets[0],l=isNaN(c.stanford_scaleWidth)?20:c.stanford_scaleWidth;if(l<0)throw Error("Colorscale's barheight and barwidth must be greater than 0.");a=o.height-c.stanford_padding.bottom-c.stanford_padding.top,e=s.range(c.stanford_padding.bottom,a,5),r=s.scaleSequential(d.colors).domain([e[e.length-1],e[0]]),o.colorScale&&o.colorScale.remove(),o.colorScale=o.svg.append("g").attr("width",50).attr("height",a).attr("class",Y.colorScale),o.colorScale.append("g").attr("transform","translate(0, "+c.stanford_padding.top+")").selectAll("bars").data(e).enter().append("rect").attr("y",function(t,e){return 5*e}).attr("x",0).attr("width",l).attr("height",5).attr("fill",function(t){return r(t)}),n=s.scaleLog().domain([d.minEpochs,d.maxEpochs]).range([e[0]+c.stanford_padding.top+e[e.length-1]+5-1,e[0]+c.stanford_padding.top]),i=s.axisRight(n),"pow10"===c.stanford_scaleFormat?i.tickValues([1,10,100,1e3,1e4,1e5,1e6,1e7]):h(c.stanford_scaleFormat)?i.tickFormat(c.stanford_scaleFormat):i.tickFormat(s.format("d")),h(c.stanford_scaleValues)&&i.tickValues(c.stanford_scaleValues(d.minEpochs,d.maxEpochs)),t=o.colorScale.append("g").attr("class","legend axis").attr("transform","translate("+l+",0)").call(i),"pow10"===c.stanford_scaleFormat&&t.selectAll(".tick text").text(null).filter(x).text(10).append("tspan").attr("dy","-.7em").text(function(t){return Math.round(Math.log(t)/Math.LN10)}),o.colorScale.attr("transform","translate("+(o.currentWidth-o.xForColorScale())+", 0)")},l.prototype.xForColorScale=function(){return this.config.stanford_padding.right+d(this.colorScale.node()).width},l.prototype.getColorScalePadding=function(){return this.xForColorScale()+this.config.stanford_padding.left+20},l.prototype.isStanfordGraphType=function(){return"stanford"===this.config.data_type},l.prototype.initStanfordData=function(){var t,e,i,n=this.d3,r=this.config,a=this.data.targets[0];if(a.values.sort(v),t=a.values.map(function(t){return t.epochs}),i=isNaN(r.stanford_scaleMin)?n.min(t):r.stanford_scaleMin,(e=isNaN(r.stanford_scaleMax)?n.max(t):r.stanford_scaleMax)"+(e?_(e):"x")+""+t.x+"\n "+(i?_(i):"y")+""+t.value+"\n "},l.prototype.countEpochsInRegion=function(i){var t=this.data.targets[0],e=t.values.reduce(function(t,e){return t+Number(e.epochs)},0),n=t.values.reduce(function(t,e){return S(e,i)?t+Number(e.epochs):t},0);return{value:n,percentage:0!==n?(n/e*100).toFixed(1):0}};var y=function(t){for(var e,i,n=0,r=0,a=t.length-1;re.epochs?1:0};return l.prototype.initStanfordElements=function(){var t=this;t.stanfordElements=t.main.select("."+Y.chart).append("g").attr("class",Y.stanfordElements),t.stanfordElements.append("g").attr("class",Y.stanfordLines),t.stanfordElements.append("g").attr("class",Y.stanfordTexts),t.stanfordElements.append("g").attr("class",Y.stanfordRegions)},l.prototype.updateStanfordElements=function(t){var e,i,n,r,a=this,o=a.main,s=a.config,c=a.xvCustom.bind(a),d=a.yvCustom.bind(a),l=a.countEpochsInRegion.bind(a),u=o.select("."+Y.stanfordLines).style("shape-rendering","geometricprecision").selectAll("."+Y.stanfordLine).data(s.stanford_lines),h=u.enter().append("g").attr("class",function(t){return Y.stanfordLine+(t.class?" "+t.class:"")});h.append("line").attr("x1",function(t){return s.axis_rotated?d(t,"value_y1"):c(t,"value_x1")}).attr("x2",function(t){return s.axis_rotated?d(t,"value_y2"):c(t,"value_x2")}).attr("y1",function(t){return s.axis_rotated?c(t,"value_x1"):d(t,"value_y1")}).attr("y2",function(t){return s.axis_rotated?c(t,"value_x2"):d(t,"value_y2")}).style("opacity",0),a.stanfordLines=h.merge(u),a.stanfordLines.select("line").transition().duration(t).attr("x1",function(t){return s.axis_rotated?d(t,"value_y1"):c(t,"value_x1")}).attr("x2",function(t){return s.axis_rotated?d(t,"value_y2"):c(t,"value_x2")}).attr("y1",function(t){return s.axis_rotated?c(t,"value_x1"):d(t,"value_y1")}).attr("y2",function(t){return s.axis_rotated?c(t,"value_x2"):d(t,"value_y2")}).style("opacity",1),u.exit().transition().duration(t).style("opacity",0).remove(),(r=(n=o.select("."+Y.stanfordTexts).selectAll("."+Y.stanfordText).data(s.stanford_texts)).enter().append("g").attr("class",function(t){return Y.stanfordText+(t.class?" "+t.class:"")})).append("text").attr("x",function(t){return s.axis_rotated?d(t,"y"):c(t,"x")}).attr("y",function(t){return s.axis_rotated?c(t,"x"):d(t,"y")}).style("opacity",0),a.stanfordTexts=r.merge(n),a.stanfordTexts.select("text").transition().duration(t).attr("x",function(t){return s.axis_rotated?d(t,"y"):c(t,"x")}).attr("y",function(t){return s.axis_rotated?c(t,"x"):d(t,"y")}).text(function(t){return t.content}).style("opacity",1),n.exit().transition().duration(t).style("opacity",0).remove(),(i=(e=o.select("."+Y.stanfordRegions).selectAll("."+Y.stanfordRegion).data(s.stanford_regions)).enter().append("g").attr("class",function(t){return Y.stanfordRegion+(t.class?" "+t.class:"")})).append("polygon").attr("points",function(t){return t.points.map(function(t){return[s.axis_rotated?d(t,"y"):c(t,"x"),s.axis_rotated?c(t,"x"):d(t,"y")].join(",")}).join(" ")}).style("opacity",0),i.append("text").attr("x",function(t){return a.getCentroid(t.points).x}).attr("y",function(t){return a.getCentroid(t.points).y}).style("opacity",0),a.stanfordRegions=i.merge(e),a.stanfordRegions.select("polygon").transition().duration(t).attr("points",function(t){return t.points.map(function(t){return[s.axis_rotated?d(t,"y"):c(t,"x"),s.axis_rotated?c(t,"x"):d(t,"y")].join(",")}).join(" ")}).style("opacity",function(t){return t.opacity?t.opacity:.2}),a.stanfordRegions.select("text").transition().duration(t).attr("x",function(t){return s.axis_rotated?d(a.getCentroid(t.points),"y"):c(a.getCentroid(t.points),"x")}).attr("y",function(t){return s.axis_rotated?c(a.getCentroid(t.points),"x"):d(a.getCentroid(t.points),"y")}).text(function(t){var e,i,n;return t.text?(a.isStanfordGraphType()&&(e=(n=l(t.points)).value,i=n.percentage),t.text(e,i)):""}).attr("text-anchor","middle").attr("dominant-baseline","middle").style("opacity",1),e.exit().transition().duration(t).style("opacity",0).remove()},l.prototype.initTooltip=function(){var t,e=this,i=e.config;if(e.tooltip=e.selectChart.style("position","relative").append("div").attr("class",Y.tooltipContainer).style("position","absolute").style("pointer-events","none").style("display","none"),i.tooltip_init_show){if(e.isTimeSeries()&&g(i.tooltip_init_x)){for(i.tooltip_init_x=e.parseDate(i.tooltip_init_x),t=0;t"+o),d=l.getStanfordPointColor(t[a]),c=_(u.data_epochs),s=t[a].epochs;else if(r||(o=_(h?h(t[a].x,t[a].index):t[a].x),r=""+(o||0===o?"":"")),void 0!==(s=_(p(t[a].value,t[a].ratio,t[a].id,t[a].index,t)))){if(null===t[a].name)continue;c=_(g(t[a].name,t[a].ratio,t[a].id,t[a].index)),d=l.levelColor?l.levelColor(t[a].value):n(t[a].id)}void 0!==s&&(r+="",r+="",r+="",r+="")}return r+"
"+o+"
"+c+""+s+"
"},l.prototype.tooltipPosition=function(t,e,i,n){var r,a,o,s,c,d=this,l=d.config,u=d.d3,h=d.hasArcType(),g=u.mouse(n);return h?(a=(d.width-(d.isLegendRight?d.getLegendWidth():0))/2+g[0],s=(d.hasType("gauge")?d.height:d.height/2)+g[1]+20):(r=d.getSvgLeft(!0),s=l.axis_rotated?(o=(a=r+g[0]+100)+e,c=d.currentWidth-d.getCurrentPaddingRight(),d.x(t[0].x)+20):(o=(a=r+d.getCurrentPaddingLeft(!0)+d.x(t[0].x)+20)+e,c=r+d.currentWidth-d.getCurrentPaddingRight(),g[1]+15),cd.currentHeight&&(s-=i+30)),s<0&&(s=0),{top:s,left:a}},l.prototype.showTooltip=function(t,e){var i,n,r,a=this,o=a.config,s=a.hasArcType(),c=t.filter(function(t){return t&&C(t.value)}),d=o.tooltip_position||l.prototype.tooltipPosition;0!==c.length&&o.tooltip_show?(a.tooltip.html(o.tooltip_contents.call(a,t,a.axis.getXAxisTickFormat(),a.getYFormat(s),a.color)).style("display","block"),i=a.tooltip.property("offsetWidth"),n=a.tooltip.property("offsetHeight"),r=d.call(this,c,i,n,e),a.tooltip.style("top",r.top+"px").style("left",r.left+"px")):a.hideTooltip()},l.prototype.hideTooltip=function(){this.tooltip.style("display","none")},l.prototype.setTargetType=function(t,e){var i=this,n=i.config;i.mapToTargetIds(t).forEach(function(t){i.withoutFadeIn[t]=e===n.data_types[t],n.data_types[t]=e}),t||(n.data_type=e)},l.prototype.hasType=function(i,t){var n=this.config.data_types,r=!1;return(t=t||this.data.targets)&&t.length?t.forEach(function(t){var e=n[t.id];(e&&0<=e.indexOf(i)||!e&&"line"===i)&&(r=!0)}):Object.keys(n).length?Object.keys(n).forEach(function(t){n[t]===i&&(r=!0)}):r=this.config.data_type===i,r},l.prototype.hasArcType=function(t){return this.hasType("pie",t)||this.hasType("donut",t)||this.hasType("gauge",t)},l.prototype.isLineType=function(t){var e=this.config,i=g(t)?t:t.id;return!e.data_types[i]||0<=["line","spline","area","area-spline","step","area-step"].indexOf(e.data_types[i])},l.prototype.isStepType=function(t){var e=g(t)?t:t.id;return 0<=["step","area-step"].indexOf(this.config.data_types[e])},l.prototype.isSplineType=function(t){var e=g(t)?t:t.id;return 0<=["spline","area-spline"].indexOf(this.config.data_types[e])},l.prototype.isAreaType=function(t){var e=g(t)?t:t.id;return 0<=["area","area-spline","area-step"].indexOf(this.config.data_types[e])},l.prototype.isBarType=function(t){var e=g(t)?t:t.id;return"bar"===this.config.data_types[e]},l.prototype.isScatterType=function(t){var e=g(t)?t:t.id;return"scatter"===this.config.data_types[e]},l.prototype.isStanfordType=function(t){var e=g(t)?t:t.id;return"stanford"===this.config.data_types[e]},l.prototype.isPieType=function(t){var e=g(t)?t:t.id;return"pie"===this.config.data_types[e]},l.prototype.isGaugeType=function(t){var e=g(t)?t:t.id;return"gauge"===this.config.data_types[e]},l.prototype.isDonutType=function(t){var e=g(t)?t:t.id;return"donut"===this.config.data_types[e]},l.prototype.isArcType=function(t){return this.isPieType(t)||this.isDonutType(t)||this.isGaugeType(t)},l.prototype.lineData=function(t){return this.isLineType(t)?[t]:[]},l.prototype.arcData=function(t){return this.isArcType(t.data)?[t]:[]},l.prototype.barData=function(t){return this.isBarType(t)?t.values:[]},l.prototype.lineOrScatterOrStanfordData=function(t){return this.isLineType(t)||this.isScatterType(t)||this.isStanfordType(t)?t.values:[]},l.prototype.barOrLineData=function(t){return this.isBarType(t)||this.isLineType(t)?t.values:[]},l.prototype.isSafari=function(){var t=window.navigator.userAgent;return 0<=t.indexOf("Safari")&&t.indexOf("Chrome")<0},l.prototype.isChrome=function(){return 0<=window.navigator.userAgent.indexOf("Chrome")},l.prototype.initZoom=function(){var e,i=this,n=i.d3,r=i.config;return i.zoom=n.zoom().on("start",function(){var t;"scroll"===r.zoom_type&&((t=n.event.sourceEvent)&&"brush"===t.type||(e=t,r.zoom_onzoomstart.call(i.api,t)))}).on("zoom",function(){var t;"scroll"===r.zoom_type&&((t=n.event.sourceEvent)&&"brush"===t.type||(i.redrawForZoom(),r.zoom_onzoom.call(i.api,i.x.orgDomain())))}).on("end",function(){var t;"scroll"===r.zoom_type&&((t=n.event.sourceEvent)&&"brush"===t.type||t&&e.clientX===t.clientX&&e.clientY===t.clientY||r.zoom_onzoomend.call(i.api,i.x.orgDomain()))}),i.zoom.updateDomain=function(){return n.event&&n.event.transform&&(r.axis_rotated&&"scroll"===r.zoom_type&&"mousemove"===n.event.sourceEvent.type?i.x.domain(n.event.transform.rescaleY(i.subX).domain()):i.x.domain(n.event.transform.rescaleX(i.subX).domain())),this},i.zoom.updateExtent=function(){return this.scaleExtent([1,1/0]).translateExtent([[0,0],[i.width,i.height]]).extent([[0,0],[i.width,i.height]]),this},i.zoom.update=function(){return this.updateExtent().updateDomain()},i.zoom.updateExtent()},l.prototype.zoomTransform=function(t){var e=[this.x(t[0]),this.x(t[1])];return this.d3.zoomIdentity.scale(this.width/(e[1]-e[0])).translate(-e[0],0)},l.prototype.initDragZoom=function(){var e,t,i=this,n=i.d3,r=i.config,a=i.context=i.svg,o=i.margin.left+20.5,s=i.margin.top+.5;"drag"===r.zoom_type&&r.zoom_enabled&&(e=function(t){return t&&t.map(function(t){return i.x.invert(t)})},t=i.dragZoomBrush=n.brushX().on("start",function(){i.api.unzoom(),i.svg.select("."+Y.dragZoom).classed("disabled",!1),r.zoom_onzoomstart.call(i.api,n.event.sourceEvent)}).on("brush",function(){r.zoom_onzoom.call(i.api,e(n.event.selection))}).on("end",function(){var t;null!=n.event.selection&&(t=e(n.event.selection),r.zoom_disableDefaultBehavior||i.api.zoom(t),i.svg.select("."+Y.dragZoom).classed("disabled",!0),r.zoom_onzoomend.call(i.api,t))}),a.append("g").classed(Y.dragZoom,!0).attr("clip-path",i.clipPath).attr("transform","translate("+o+","+s+")").call(t))},l.prototype.getZoomDomain=function(){var t=this.config,e=this.d3;return[e.min([this.orgXDomain[0],t.zoom_x_min]),e.max([this.orgXDomain[1],t.zoom_x_max])]},l.prototype.redrawForZoom=function(){var t=this,e=t.d3,i=t.config,n=t.zoom,r=t.x;i.zoom_enabled&&0!==t.filterTargetsToShow(t.data.targets).length&&(n.update(),i.zoom_disableDefaultBehavior||(t.isCategorized()&&r.orgDomain()[0]===t.orgXDomain[0]&&r.domain([t.orgXDomain[0]-1e-10,r.orgDomain()[1]]),t.redraw({withTransition:!1,withY:i.zoom_rescale,withSubchart:!1,withEventRect:!1,withDimension:!1}),e.event.sourceEvent&&"mousemove"===e.event.sourceEvent.type&&(t.cancelClick=!0)))},i}); \ No newline at end of file +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).c3=e()}(this,function(){"use strict";function l(t){var e=this;e.d3=window.d3?window.d3:"undefined"!=typeof require?require("d3"):void 0,e.api=t,e.config=e.getDefaultConfig(),e.data={},e.cache={},e.axes={}}function n(t){this.internal=new l(this),this.internal.loadConfig(t),this.internal.beforeInit(t),this.internal.init(),this.internal.afterInit(t),function e(i,n,r){Object.keys(i).forEach(function(t){n[t]=i[t].bind(r),0/g,">"):t}function c(t){var e=function(t){void 0===t&&(t=window.navigator.userAgent);var e=t.indexOf("MSIE ");return 0e.getTotalLength())break;i--}while(0=this.numberOfItems)throw"INDEX_SIZE_ERR"},window.SVGPathSegList.prototype.getItem=function(t){return this._checkPathSynchronizedToList(),this._checkValidIndex(t),this._list[t]},window.SVGPathSegList.prototype.insertItemBefore=function(t,e){return this._checkPathSynchronizedToList(),e>this.numberOfItems&&(e=this.numberOfItems),t._owningPathSegList&&(t=t.clone()),this._list.splice(e,0,t),(t._owningPathSegList=this)._writeListToPath(),t},window.SVGPathSegList.prototype.replaceItem=function(t,e){return this._checkPathSynchronizedToList(),t._owningPathSegList&&(t=t.clone()),this._checkValidIndex(e),((this._list[e]=t)._owningPathSegList=this)._writeListToPath(),t},window.SVGPathSegList.prototype.removeItem=function(t){this._checkPathSynchronizedToList(),this._checkValidIndex(t);var e=this._list[t];return this._list.splice(t,1),this._writeListToPath(),e},window.SVGPathSegList.prototype.appendItem=function(t){return this._checkPathSynchronizedToList(),t._owningPathSegList&&(t=t.clone()),this._list.push(t),(t._owningPathSegList=this)._writeListToPath(),t},window.SVGPathSegList._pathSegArrayAsString=function(t){var e="",i=!0;return t.forEach(function(t){i?(i=!1,e+=t._asPathString()):e+=" "+t._asPathString()}),e},window.SVGPathSegList.prototype._parsePath=function(t){if(!t||0==t.length)return[];function e(){this.pathSegList=[]}var n=this;e.prototype.appendSegment=function(t){this.pathSegList.push(t)};function i(t){this._string=t,this._currentIndex=0,this._endIndex=this._string.length,this._previousCommand=window.SVGPathSeg.PATHSEG_UNKNOWN,this._skipOptionalSpaces()}i.prototype._isCurrentSpace=function(){var t=this._string[this._currentIndex];return t<=" "&&(" "==t||"\n"==t||"\t"==t||"\r"==t||"\f"==t)},i.prototype._skipOptionalSpaces=function(){for(;this._currentIndex=this._endIndex||this._string.charAt(this._currentIndex)<"0"||"9"=this._endIndex||this._string.charAt(this._currentIndex)<"0"||"9"=this._endIndex)){var t=!1,e=this._string.charAt(this._currentIndex++);if("0"==e)t=!1;else{if("1"!=e)return;t=!0}return this._skipOptionalSpacesOrDelimiter(),t}},i.prototype.parseSegment=function(){var t=this._string[this._currentIndex],e=this._pathSegTypeFromChar(t);if(e==window.SVGPathSeg.PATHSEG_UNKNOWN){if(this._previousCommand==window.SVGPathSeg.PATHSEG_UNKNOWN)return null;if((e=this._nextCommandHelper(t,this._previousCommand))==window.SVGPathSeg.PATHSEG_UNKNOWN)return null}else this._currentIndex++;switch(this._previousCommand=e){case window.SVGPathSeg.PATHSEG_MOVETO_REL:return new window.SVGPathSegMovetoRel(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_MOVETO_ABS:return new window.SVGPathSegMovetoAbs(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_REL:return new window.SVGPathSegLinetoRel(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_ABS:return new window.SVGPathSegLinetoAbs(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_REL:return new window.SVGPathSegLinetoHorizontalRel(n,this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_HORIZONTAL_ABS:return new window.SVGPathSegLinetoHorizontalAbs(n,this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_REL:return new window.SVGPathSegLinetoVerticalRel(n,this._parseNumber());case window.SVGPathSeg.PATHSEG_LINETO_VERTICAL_ABS:return new window.SVGPathSegLinetoVerticalAbs(n,this._parseNumber());case window.SVGPathSeg.PATHSEG_CLOSEPATH:return this._skipOptionalSpaces(),new window.SVGPathSegClosePath(n);case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL:var i={x1:this._parseNumber(),y1:this._parseNumber(),x2:this._parseNumber(),y2:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoCubicRel(n,i.x,i.y,i.x1,i.y1,i.x2,i.y2);case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_ABS:i={x1:this._parseNumber(),y1:this._parseNumber(),x2:this._parseNumber(),y2:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoCubicAbs(n,i.x,i.y,i.x1,i.y1,i.x2,i.y2);case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_REL:i={x2:this._parseNumber(),y2:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoCubicSmoothRel(n,i.x,i.y,i.x2,i.y2);case window.SVGPathSeg.PATHSEG_CURVETO_CUBIC_SMOOTH_ABS:i={x2:this._parseNumber(),y2:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoCubicSmoothAbs(n,i.x,i.y,i.x2,i.y2);case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_REL:i={x1:this._parseNumber(),y1:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoQuadraticRel(n,i.x,i.y,i.x1,i.y1);case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_ABS:i={x1:this._parseNumber(),y1:this._parseNumber(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegCurvetoQuadraticAbs(n,i.x,i.y,i.x1,i.y1);case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL:return new window.SVGPathSegCurvetoQuadraticSmoothRel(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS:return new window.SVGPathSegCurvetoQuadraticSmoothAbs(n,this._parseNumber(),this._parseNumber());case window.SVGPathSeg.PATHSEG_ARC_REL:i={x1:this._parseNumber(),y1:this._parseNumber(),arcAngle:this._parseNumber(),arcLarge:this._parseArcFlag(),arcSweep:this._parseArcFlag(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegArcRel(n,i.x,i.y,i.x1,i.y1,i.arcAngle,i.arcLarge,i.arcSweep);case window.SVGPathSeg.PATHSEG_ARC_ABS:i={x1:this._parseNumber(),y1:this._parseNumber(),arcAngle:this._parseNumber(),arcLarge:this._parseArcFlag(),arcSweep:this._parseArcFlag(),x:this._parseNumber(),y:this._parseNumber()};return new window.SVGPathSegArcAbs(n,i.x,i.y,i.x1,i.y1,i.arcAngle,i.arcLarge,i.arcSweep);default:throw"Unknown path seg type."}};var r=new e,a=new i(t);if(!a.initialCommandIsMoveTo())return[];for(;a.hasMoreData();){var o=a.parseSegment();if(!o)return[];r.appendSegment(o)}return r.pathSegList}),String.prototype.padEnd||(String.prototype.padEnd=function(t,e){return t>>=0,e=String(void 0!==e?e:" "),this.length>t?String(this):((t-=this.length)>e.length&&(e+=e.repeat(t/e.length)),String(this)+e.slice(0,t))}),"function"!=typeof Object.assign&&Object.defineProperty(Object,"assign",{value:function(t,e){if(null==t)throw new TypeError("Cannot convert undefined or null to object");for(var i=Object(t),n=1;n'":;\[\]\/|~`{}\\])/g,"\\$1")},l.prototype.selectorTarget=function(t,e){return(e||"")+"."+Y.target+this.getTargetSelectorSuffix(t)},l.prototype.selectorTargets=function(t,e){var i=this;return(t=t||[]).length?t.map(function(t){return i.selectorTarget(t,e)}):null},l.prototype.selectorLegend=function(t){return"."+Y.legendItem+this.getTargetSelectorSuffix(t)},l.prototype.selectorLegends=function(t){var e=this;return t&&t.length?t.map(function(t){return e.selectorLegend(t)}):null},l.prototype.getClipPath=function(t){return"url("+(c(9)?"":document.URL.split("#")[0])+"#"+t+")"},l.prototype.appendClip=function(t,e){return t.append("clipPath").attr("id",e).append("rect")},l.prototype.getAxisClipX=function(t){var e=Math.max(30,this.margin.left);return t?-(1+e):-(e-1)},l.prototype.getAxisClipY=function(t){return t?-20:-this.margin.top},l.prototype.getXAxisClipX=function(){return this.getAxisClipX(!this.config.axis_rotated)},l.prototype.getXAxisClipY=function(){return this.getAxisClipY(!this.config.axis_rotated)},l.prototype.getYAxisClipX=function(){return this.config.axis_y_inner?-1:this.getAxisClipX(this.config.axis_rotated)},l.prototype.getYAxisClipY=function(){return this.getAxisClipY(this.config.axis_rotated)},l.prototype.getAxisClipWidth=function(t){var e=Math.max(30,this.margin.left),i=Math.max(30,this.margin.right);return t?this.width+2+e+i:this.margin.left+20},l.prototype.getAxisClipHeight=function(t){return(t?this.margin.bottom:this.margin.top+this.height)+20},l.prototype.getXAxisClipWidth=function(){return this.getAxisClipWidth(!this.config.axis_rotated)},l.prototype.getXAxisClipHeight=function(){return this.getAxisClipHeight(!this.config.axis_rotated)},l.prototype.getYAxisClipWidth=function(){return this.getAxisClipWidth(this.config.axis_rotated)+(this.config.axis_y_inner?20:0)},l.prototype.getYAxisClipHeight=function(){return this.getAxisClipHeight(this.config.axis_rotated)},l.prototype.generateColor=function(){var t=this.config,e=this.d3,n=t.data_colors,r=L(t.color_pattern)?t.color_pattern:e.schemeCategory10,a=t.data_color,o=[];return function(t){var e,i=t.id||t.data&&t.data.id||t;return n[i]instanceof Function?e=n[i](t):n[i]?e=n[i]:(o.indexOf(i)<0&&o.push(i),e=r[o.indexOf(i)%r.length],n[i]=e),a instanceof Function?a(e,t):e}},l.prototype.generateLevelColor=function(){var t=this.config,n=t.color_pattern,e=t.color_threshold,r="value"===e.unit,a=e.values&&e.values.length?e.values:[],o=e.max||100;return L(e)&&L(n)?function(t){for(var e=n[n.length-1],i=0;is&&(o=o.filter(function(t){return(""+t).indexOf(".")<0}));return o},l.prototype.getGridFilterToRemove=function(t){return t?function(e){var i=!1;return[].concat(t).forEach(function(t){("value"in t&&e.value===t.value||"class"in t&&e.class===t.class)&&(i=!0)}),i}:function(){return!0}},l.prototype.removeGridLines=function(t,e){function i(t){return!r(t)}var n=this.config,r=this.getGridFilterToRemove(t),a=e?Y.xgridLines:Y.ygridLines,o=e?Y.xgridLine:Y.ygridLine;this.main.select("."+a).selectAll("."+o).filter(r).transition().duration(n.transition_duration).style("opacity",0).remove(),e?n.grid_x_lines=n.grid_x_lines.filter(i):n.grid_y_lines=n.grid_y_lines.filter(i)},l.prototype.initEventRect=function(){var t=this,e=t.config;t.main.select("."+Y.chart).append("g").attr("class",Y.eventRects).style("fill-opacity",0),t.eventRect=t.main.select("."+Y.eventRects).append("rect").attr("class",Y.eventRect),e.zoom_enabled&&t.zoom&&(t.eventRect.call(t.zoom).on("dblclick.zoom",null),e.zoom_initialRange&&t.eventRect.transition().duration(0).call(t.zoom.transform,t.zoomTransform(e.zoom_initialRange)))},l.prototype.redrawEventRect=function(){var s=this,c=s.d3,d=s.config;function l(){s.svg.select("."+Y.eventRect).style("cursor",null),s.hideXGridFocus(),s.hideTooltip(),s.unexpandCircles(),s.unexpandBars()}function u(t,e){return e&&(s.isBarType(e.id)||s.dist(e,t)i.bar_width_max?i.bar_width_max:n},l.prototype.getBars=function(t,e){return(e?this.main.selectAll("."+Y.bars+this.getTargetSelectorSuffix(e)):this.main).selectAll("."+Y.bar+(C(t)?"-"+t:""))},l.prototype.expandBars=function(t,e,i){i&&this.unexpandBars(),this.getBars(t,e).classed(Y.EXPANDED,!0)},l.prototype.unexpandBars=function(t){this.getBars(t).classed(Y.EXPANDED,!1)},l.prototype.generateDrawBar=function(t,e){var a=this.config,o=this.generateGetBarPoints(t,e);return function(t,e){var i=o(t,e),n=a.axis_rotated?1:0,r=a.axis_rotated?0:1;return"M "+i[0][n]+","+i[0][r]+" L"+i[1][n]+","+i[1][r]+" L"+i[2][n]+","+i[2][r]+" L"+i[3][n]+","+i[3][r]+" z"}},l.prototype.generateGetBarPoints=function(t,e){var o=this,i=e?o.subXAxis:o.xAxis,n=t.__max__+1,s=o.getBarW(i,n),c=o.getShapeX(s,n,t,!!e),d=o.getShapeY(!!e),l=o.getShapeOffset(o.isBarType,t,!!e),u=s*(o.config.bar_space/2),h=e?o.getSubYScale:o.getYScale;return function(t,e){var i=h.call(o,t.id)(0),n=l(t,e)||i,r=c(t),a=d(t);return o.config.axis_rotated&&(0r.width?o=r.width-a.width:o<0&&(o=4)),o},l.prototype.getYForText=function(t,e,i){var n,r=this,a=d(i);return r.config.axis_rotated?n=(t[0][0]+t[2][0]+.6*a.height)/2:(n=t[2][1],e.value<0||0===e.value&&!r.hasPositiveValue?(n+=a.height,r.isBarType(e)&&r.isSafari()?n-=3:!r.isBarType(e)&&r.isChrome()&&(n+=3)):n+=r.isBarType(e)?-3:-6),null!==e.value||r.config.axis_rotated||(nthis.height&&(n=this.height-4)),n},l.prototype.initTitle=function(){this.title=this.svg.append("text").text(this.config.title_text).attr("class",this.CLASS.title)},l.prototype.redrawTitle=function(){var t=this;t.title.attr("x",t.xForTitle.bind(t)).attr("y",t.yForTitle.bind(t))},l.prototype.xForTitle=function(){var t=this,e=t.config,i=e.title_position||"left",n=0<=i.indexOf("right")?t.currentWidth-t.getTextRect(t.title.node().textContent,t.CLASS.title,t.title.node()).width-e.title_padding.right:0<=i.indexOf("center")?Math.max((t.currentWidth-t.getTextRect(t.title.node().textContent,t.CLASS.title,t.title.node()).width)/2,0):e.title_padding.left;return n},l.prototype.yForTitle=function(){var t=this;return t.config.title_padding.top+t.getTextRect(t.title.node().textContent,t.CLASS.title,t.title.node()).height},l.prototype.getTitlePadding=function(){return this.yForTitle()+this.config.title_padding.bottom},l.prototype.drawColorScale=function(){var t,e,i,n,r,a,o=this,s=o.d3,c=o.config,d=o.data.targets[0],l=isNaN(c.stanford_scaleWidth)?20:c.stanford_scaleWidth;if(l<0)throw Error("Colorscale's barheight and barwidth must be greater than 0.");a=o.height-c.stanford_padding.bottom-c.stanford_padding.top,e=s.range(c.stanford_padding.bottom,a,5),r=s.scaleSequential(d.colors).domain([e[e.length-1],e[0]]),o.colorScale&&o.colorScale.remove(),o.colorScale=o.svg.append("g").attr("width",50).attr("height",a).attr("class",Y.colorScale),o.colorScale.append("g").attr("transform","translate(0, "+c.stanford_padding.top+")").selectAll("bars").data(e).enter().append("rect").attr("y",function(t,e){return 5*e}).attr("x",0).attr("width",l).attr("height",5).attr("fill",function(t){return r(t)}),n=s.scaleLog().domain([d.minEpochs,d.maxEpochs]).range([e[0]+c.stanford_padding.top+e[e.length-1]+5-1,e[0]+c.stanford_padding.top]),i=s.axisRight(n),"pow10"===c.stanford_scaleFormat?i.tickValues([1,10,100,1e3,1e4,1e5,1e6,1e7]):h(c.stanford_scaleFormat)?i.tickFormat(c.stanford_scaleFormat):i.tickFormat(s.format("d")),h(c.stanford_scaleValues)&&i.tickValues(c.stanford_scaleValues(d.minEpochs,d.maxEpochs)),t=o.colorScale.append("g").attr("class","legend axis").attr("transform","translate("+l+",0)").call(i),"pow10"===c.stanford_scaleFormat&&t.selectAll(".tick text").text(null).filter(x).text(10).append("tspan").attr("dy","-.7em").text(function(t){return Math.round(Math.log(t)/Math.LN10)}),o.colorScale.attr("transform","translate("+(o.currentWidth-o.xForColorScale())+", 0)")},l.prototype.xForColorScale=function(){return this.config.stanford_padding.right+d(this.colorScale.node()).width},l.prototype.getColorScalePadding=function(){return this.xForColorScale()+this.config.stanford_padding.left+20},l.prototype.isStanfordGraphType=function(){return"stanford"===this.config.data_type},l.prototype.initStanfordData=function(){var t,e,i,n=this.d3,r=this.config,a=this.data.targets[0];if(a.values.sort(v),t=a.values.map(function(t){return t.epochs}),i=isNaN(r.stanford_scaleMin)?n.min(t):r.stanford_scaleMin,(e=isNaN(r.stanford_scaleMax)?n.max(t):r.stanford_scaleMax)"+(e?_(e):"x")+""+t.x+"\n "+(i?_(i):"y")+""+t.value+"\n "},l.prototype.countEpochsInRegion=function(i){var t=this.data.targets[0],e=t.values.reduce(function(t,e){return t+Number(e.epochs)},0),n=t.values.reduce(function(t,e){return S(e,i)?t+Number(e.epochs):t},0);return{value:n,percentage:0!==n?(n/e*100).toFixed(1):0}};var y=function(t){for(var e,i,n=0,r=0,a=t.length-1;re.epochs?1:0};return l.prototype.initStanfordElements=function(){var t=this;t.stanfordElements=t.main.select("."+Y.chart).append("g").attr("class",Y.stanfordElements),t.stanfordElements.append("g").attr("class",Y.stanfordLines),t.stanfordElements.append("g").attr("class",Y.stanfordTexts),t.stanfordElements.append("g").attr("class",Y.stanfordRegions)},l.prototype.updateStanfordElements=function(t){var e,i,n,r,a=this,o=a.main,s=a.config,c=a.xvCustom.bind(a),d=a.yvCustom.bind(a),l=a.countEpochsInRegion.bind(a),u=o.select("."+Y.stanfordLines).style("shape-rendering","geometricprecision").selectAll("."+Y.stanfordLine).data(s.stanford_lines),h=u.enter().append("g").attr("class",function(t){return Y.stanfordLine+(t.class?" "+t.class:"")});h.append("line").attr("x1",function(t){return s.axis_rotated?d(t,"value_y1"):c(t,"value_x1")}).attr("x2",function(t){return s.axis_rotated?d(t,"value_y2"):c(t,"value_x2")}).attr("y1",function(t){return s.axis_rotated?c(t,"value_x1"):d(t,"value_y1")}).attr("y2",function(t){return s.axis_rotated?c(t,"value_x2"):d(t,"value_y2")}).style("opacity",0),a.stanfordLines=h.merge(u),a.stanfordLines.select("line").transition().duration(t).attr("x1",function(t){return s.axis_rotated?d(t,"value_y1"):c(t,"value_x1")}).attr("x2",function(t){return s.axis_rotated?d(t,"value_y2"):c(t,"value_x2")}).attr("y1",function(t){return s.axis_rotated?c(t,"value_x1"):d(t,"value_y1")}).attr("y2",function(t){return s.axis_rotated?c(t,"value_x2"):d(t,"value_y2")}).style("opacity",1),u.exit().transition().duration(t).style("opacity",0).remove(),(r=(n=o.select("."+Y.stanfordTexts).selectAll("."+Y.stanfordText).data(s.stanford_texts)).enter().append("g").attr("class",function(t){return Y.stanfordText+(t.class?" "+t.class:"")})).append("text").attr("x",function(t){return s.axis_rotated?d(t,"y"):c(t,"x")}).attr("y",function(t){return s.axis_rotated?c(t,"x"):d(t,"y")}).style("opacity",0),a.stanfordTexts=r.merge(n),a.stanfordTexts.select("text").transition().duration(t).attr("x",function(t){return s.axis_rotated?d(t,"y"):c(t,"x")}).attr("y",function(t){return s.axis_rotated?c(t,"x"):d(t,"y")}).text(function(t){return t.content}).style("opacity",1),n.exit().transition().duration(t).style("opacity",0).remove(),(i=(e=o.select("."+Y.stanfordRegions).selectAll("."+Y.stanfordRegion).data(s.stanford_regions)).enter().append("g").attr("class",function(t){return Y.stanfordRegion+(t.class?" "+t.class:"")})).append("polygon").attr("points",function(t){return t.points.map(function(t){return[s.axis_rotated?d(t,"y"):c(t,"x"),s.axis_rotated?c(t,"x"):d(t,"y")].join(",")}).join(" ")}).style("opacity",0),i.append("text").attr("x",function(t){return a.getCentroid(t.points).x}).attr("y",function(t){return a.getCentroid(t.points).y}).style("opacity",0),a.stanfordRegions=i.merge(e),a.stanfordRegions.select("polygon").transition().duration(t).attr("points",function(t){return t.points.map(function(t){return[s.axis_rotated?d(t,"y"):c(t,"x"),s.axis_rotated?c(t,"x"):d(t,"y")].join(",")}).join(" ")}).style("opacity",function(t){return t.opacity?t.opacity:.2}),a.stanfordRegions.select("text").transition().duration(t).attr("x",function(t){return s.axis_rotated?d(a.getCentroid(t.points),"y"):c(a.getCentroid(t.points),"x")}).attr("y",function(t){return s.axis_rotated?c(a.getCentroid(t.points),"x"):d(a.getCentroid(t.points),"y")}).text(function(t){var e,i,n;return t.text?(a.isStanfordGraphType()&&(e=(n=l(t.points)).value,i=n.percentage),t.text(e,i)):""}).attr("text-anchor","middle").attr("dominant-baseline","middle").style("opacity",1),e.exit().transition().duration(t).style("opacity",0).remove()},l.prototype.initTooltip=function(){var t,e=this,i=e.config;if(e.tooltip=e.selectChart.style("position","relative").append("div").attr("class",Y.tooltipContainer).style("position","absolute").style("pointer-events","none").style("display","none"),i.tooltip_init_show){if(e.isTimeSeries()&&g(i.tooltip_init_x)){for(i.tooltip_init_x=e.parseDate(i.tooltip_init_x),t=0;t"+o),d=l.getStanfordPointColor(t[a]),c=_(u.data_epochs),s=t[a].epochs;else if(r||(o=_(h?h(t[a].x,t[a].index):t[a].x),r=""+(o||0===o?"":"")),void 0!==(s=_(p(t[a].value,t[a].ratio,t[a].id,t[a].index,t)))){if(null===t[a].name)continue;c=_(g(t[a].name,t[a].ratio,t[a].id,t[a].index)),d=l.levelColor?l.levelColor(t[a].value):n(t[a].id)}void 0!==s&&(r+="",r+="",r+="",r+="")}return r+"
"+o+"
"+c+""+s+"
"},l.prototype.tooltipPosition=function(t,e,i,n){var r,a,o,s,c,d=this,l=d.config,u=d.d3,h=d.hasArcType(),g=u.mouse(n);return h?(a=(d.width-(d.isLegendRight?d.getLegendWidth():0))/2+g[0],s=(d.hasType("gauge")?d.height:d.height/2)+g[1]+20):(r=d.getSvgLeft(!0),s=l.axis_rotated?(o=(a=r+g[0]+100)+e,c=d.currentWidth-d.getCurrentPaddingRight(),d.x(t[0].x)+20):(o=(a=r+d.getCurrentPaddingLeft(!0)+d.x(t[0].x)+20)+e,c=r+d.currentWidth-d.getCurrentPaddingRight(),g[1]+15),cd.currentHeight&&(s-=i+30)),s<0&&(s=0),{top:s,left:a}},l.prototype.showTooltip=function(t,e){var i,n,r,a=this,o=a.config,s=a.hasArcType(),c=t.filter(function(t){return t&&C(t.value)}),d=o.tooltip_position||l.prototype.tooltipPosition;0!==c.length&&o.tooltip_show?(a.tooltip.html(o.tooltip_contents.call(a,t,a.axis.getXAxisTickFormat(),a.getYFormat(s),a.color)).style("display","block"),i=a.tooltip.property("offsetWidth"),n=a.tooltip.property("offsetHeight"),r=d.call(this,c,i,n,e),a.tooltip.style("top",r.top+"px").style("left",r.left+"px")):a.hideTooltip()},l.prototype.hideTooltip=function(){this.tooltip.style("display","none")},l.prototype.setTargetType=function(t,e){var i=this,n=i.config;i.mapToTargetIds(t).forEach(function(t){i.withoutFadeIn[t]=e===n.data_types[t],n.data_types[t]=e}),t||(n.data_type=e)},l.prototype.hasType=function(i,t){var n=this.config.data_types,r=!1;return(t=t||this.data.targets)&&t.length?t.forEach(function(t){var e=n[t.id];(e&&0<=e.indexOf(i)||!e&&"line"===i)&&(r=!0)}):Object.keys(n).length?Object.keys(n).forEach(function(t){n[t]===i&&(r=!0)}):r=this.config.data_type===i,r},l.prototype.hasArcType=function(t){return this.hasType("pie",t)||this.hasType("donut",t)||this.hasType("gauge",t)},l.prototype.isLineType=function(t){var e=this.config,i=g(t)?t:t.id;return!e.data_types[i]||0<=["line","spline","area","area-spline","step","area-step"].indexOf(e.data_types[i])},l.prototype.isStepType=function(t){var e=g(t)?t:t.id;return 0<=["step","area-step"].indexOf(this.config.data_types[e])},l.prototype.isSplineType=function(t){var e=g(t)?t:t.id;return 0<=["spline","area-spline"].indexOf(this.config.data_types[e])},l.prototype.isAreaType=function(t){var e=g(t)?t:t.id;return 0<=["area","area-spline","area-step"].indexOf(this.config.data_types[e])},l.prototype.isBarType=function(t){var e=g(t)?t:t.id;return"bar"===this.config.data_types[e]},l.prototype.isScatterType=function(t){var e=g(t)?t:t.id;return"scatter"===this.config.data_types[e]},l.prototype.isStanfordType=function(t){var e=g(t)?t:t.id;return"stanford"===this.config.data_types[e]},l.prototype.isPieType=function(t){var e=g(t)?t:t.id;return"pie"===this.config.data_types[e]},l.prototype.isGaugeType=function(t){var e=g(t)?t:t.id;return"gauge"===this.config.data_types[e]},l.prototype.isDonutType=function(t){var e=g(t)?t:t.id;return"donut"===this.config.data_types[e]},l.prototype.isArcType=function(t){return this.isPieType(t)||this.isDonutType(t)||this.isGaugeType(t)},l.prototype.lineData=function(t){return this.isLineType(t)?[t]:[]},l.prototype.arcData=function(t){return this.isArcType(t.data)?[t]:[]},l.prototype.barData=function(t){return this.isBarType(t)?t.values:[]},l.prototype.lineOrScatterOrStanfordData=function(t){return this.isLineType(t)||this.isScatterType(t)||this.isStanfordType(t)?t.values:[]},l.prototype.barOrLineData=function(t){return this.isBarType(t)||this.isLineType(t)?t.values:[]},l.prototype.isSafari=function(){var t=window.navigator.userAgent;return 0<=t.indexOf("Safari")&&t.indexOf("Chrome")<0},l.prototype.isChrome=function(){return 0<=window.navigator.userAgent.indexOf("Chrome")},l.prototype.initZoom=function(){var e,i=this,n=i.d3,r=i.config;return i.zoom=n.zoom().on("start",function(){var t;"scroll"===r.zoom_type&&((t=n.event.sourceEvent)&&"brush"===t.type||(e=t,r.zoom_onzoomstart.call(i.api,t)))}).on("zoom",function(){var t;"scroll"===r.zoom_type&&((t=n.event.sourceEvent)&&"brush"===t.type||(i.redrawForZoom(),r.zoom_onzoom.call(i.api,i.x.orgDomain())))}).on("end",function(){var t;"scroll"===r.zoom_type&&((t=n.event.sourceEvent)&&"brush"===t.type||t&&e.clientX===t.clientX&&e.clientY===t.clientY||r.zoom_onzoomend.call(i.api,i.x.orgDomain()))}),i.zoom.updateDomain=function(){return n.event&&n.event.transform&&(r.axis_rotated&&"scroll"===r.zoom_type&&"mousemove"===n.event.sourceEvent.type?i.x.domain(n.event.transform.rescaleY(i.subX).domain()):i.x.domain(n.event.transform.rescaleX(i.subX).domain())),this},i.zoom.updateExtent=function(){return this.scaleExtent([1,1/0]).translateExtent([[0,0],[i.width,i.height]]).extent([[0,0],[i.width,i.height]]),this},i.zoom.update=function(){return this.updateExtent().updateDomain()},i.zoom.updateExtent()},l.prototype.zoomTransform=function(t){var e=[this.x(t[0]),this.x(t[1])];return this.d3.zoomIdentity.scale(this.width/(e[1]-e[0])).translate(-e[0],0)},l.prototype.initDragZoom=function(){var e,t,i=this,n=i.d3,r=i.config,a=i.context=i.svg,o=i.margin.left+20.5,s=i.margin.top+.5;"drag"===r.zoom_type&&r.zoom_enabled&&(e=function(t){return t&&t.map(function(t){return i.x.invert(t)})},t=i.dragZoomBrush=n.brushX().on("start",function(){i.api.unzoom(),i.svg.select("."+Y.dragZoom).classed("disabled",!1),r.zoom_onzoomstart.call(i.api,n.event.sourceEvent)}).on("brush",function(){r.zoom_onzoom.call(i.api,e(n.event.selection))}).on("end",function(){var t;null!=n.event.selection&&(t=e(n.event.selection),r.zoom_disableDefaultBehavior||i.api.zoom(t),i.svg.select("."+Y.dragZoom).classed("disabled",!0),r.zoom_onzoomend.call(i.api,t))}),a.append("g").classed(Y.dragZoom,!0).attr("clip-path",i.clipPath).attr("transform","translate("+o+","+s+")").call(t))},l.prototype.getZoomDomain=function(){var t=this.config,e=this.d3;return[e.min([this.orgXDomain[0],t.zoom_x_min]),e.max([this.orgXDomain[1],t.zoom_x_max])]},l.prototype.redrawForZoom=function(){var t=this,e=t.d3,i=t.config,n=t.zoom,r=t.x;i.zoom_enabled&&0!==t.filterTargetsToShow(t.data.targets).length&&(n.update(),i.zoom_disableDefaultBehavior||(t.isCategorized()&&r.orgDomain()[0]===t.orgXDomain[0]&&r.domain([t.orgXDomain[0]-1e-10,r.orgDomain()[1]]),t.redraw({withTransition:!1,withY:i.zoom_rescale,withSubchart:!1,withEventRect:!1,withDimension:!1}),e.event.sourceEvent&&"mousemove"===e.event.sourceEvent.type&&(t.cancelClick=!0)))},i}); diff --git a/auto_rx/autorx/templates/historical.html b/auto_rx/autorx/templates/historical.html index b27e607b..c11f518b 100644 --- a/auto_rx/autorx/templates/historical.html +++ b/auto_rx/autorx/templates/historical.html @@ -100,10 +100,11 @@ {title:"Date", field:"datetime", width:160, resizable:false, formatter:function(cell, formatterParams, onRendered){ if (getCookie('UTC') == 'false') { var temp_time = new Date(cell.getValue()); - if (temp_time.toLocaleString("en-AU") == "Invalid Date") { + var temp_converted = temp_time.toLocaleString(window.navigator.language,{hourCycle:'h23', year:"numeric", month:"2-digit", day:'2-digit', hour:'2-digit',minute:'2-digit', second:'2-digit'}); + if (temp_converted == "Invalid Date") { return; } else { - return temp_time.toLocaleString("en-AU"); + return temp_converted; } } else { return cell.getValue(); @@ -698,6 +699,8 @@ } } }); + } else { + enableMenu(); } if (i < (selectedrows.length-1)) { i++; diff --git a/auto_rx/autorx/templates/index.html b/auto_rx/autorx/templates/index.html index 8727116c..83c9016d 100644 --- a/auto_rx/autorx/templates/index.html +++ b/auto_rx/autorx/templates/index.html @@ -5,7 +5,7 @@ - + @@ -14,7 +14,7 @@ - + @@ -29,23 +29,23 @@ - + - +
- +
@@ -1413,12 +1434,12 @@

Log

- +
-

Settings

-
-

Table Options

+

Settings

+
+

Table Options


@@ -1459,14 +1480,14 @@

Table Options



-
+

Show Table

 
+


@@ -1513,7 +1534,7 @@

Show UTC Time

Set Pagination Size

 
- @@ -1537,7 +1558,7 @@

Follow Sonde


-
+

Show Imperial Units

 
@@ -1557,7 +1578,7 @@

Show Software Version


-
+

Live KML

 
@@ -1569,32 +1590,32 @@

Reset Page

 
-
+


Controls

 
-
+


Historical View

 
-
+



-
-
+
+
- +
Radiosonde Auto-RX @@ -1615,15 +1636,15 @@

Scan Results:

- +
diff --git a/auto_rx/autorx/utils.py b/auto_rx/autorx/utils.py index 8e6b2f09..8e502faa 100644 --- a/auto_rx/autorx/utils.py +++ b/auto_rx/autorx/utils.py @@ -22,31 +22,27 @@ from dateutil.parser import parse from datetime import datetime, timedelta from math import radians, degrees, sin, cos, atan2, sqrt, pi +from queue import Queue from . import __version__ as auto_rx_version -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue - # List of binaries we check for on startup REQUIRED_RS_UTILS = [ "dft_detect", "dfm09mod", "m10mod", - "imet1rs_dft", "rs41mod", "rs92mod", "fsk_demod", - "mk2mod", + "mk2a1680mod", "lms6Xmod", "meisei100mod", "imet54mod", "mp3h1mod", "m20mod", + "imet4iq", + "mts01mod", + "iq_dec" ] @@ -146,7 +142,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)-") + _re = re.compile("^(DFM|M10|M20|IMET|IMET5|IMET54|MRZ|LMS6|IMS100|RS11G|MTS01)-") # If we have a match, return the trailing part of the serial, re-adding # any - separators if they exist. @@ -186,8 +182,14 @@ def short_type_lookup(type_name): return "Intermet Systems iMet-5x" elif type_name == "MEISEI": return "Meisei iMS-100/RS-11" + elif type_name == "IMS100": + return "Meisei iMS-100" + elif type_name == "RS11G": + return "Meisei RS-11G" elif type_name == "MRZ": return "Meteo-Radiy MRZ" + elif type_name == "MTS01": + return "Meteosis MTS01" else: return "Unknown" @@ -220,8 +222,14 @@ def short_short_type_lookup(type_name): return "iMet-5x" elif type_name == "MEISEI": return "iMS-100" + elif type_name == "IMS100": + return "iMS-100" + elif type_name == "RS11G": + return "RS-11G" elif type_name == "MRZ": return "MRZ" + elif type_name == "MTS01": + return "MTS01" else: return "Unknown" @@ -277,7 +285,7 @@ def generate_aprs_id(sonde_data): _id_hex = hex(_id_suffix).upper() _object_name = "LMS6" + _id_hex[-5:] - elif "MEISEI" in sonde_data["type"]: + 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 _meisei_id = int(sonde_data["id"].split("-")[-1]) _id_suffix = hex(_meisei_id).upper().split("0X")[1] @@ -297,6 +305,18 @@ def generate_aprs_id(sonde_data): _id_hex = _id_hex[-6:] _object_name = "MRZ" + _id_hex.upper() + elif "MTS01" in sonde_data["type"]: + # Split out just the serial number part of the ID, and cast it to an int + # This acts as another check that we have been provided with a numeric serial. + _mts_id = int(sonde_data["id"].split("-")[-1]) + + # Convert to upper-case hex, and take the last 6 nibbles. + _id_suffix = hex(_mts_id).upper()[-6:] + + # Create the object name + _object_name = "MTS" + _id_suffix + + # New Sonde types will be added in here. else: # Unknown sonde type, don't know how to handle this yet. @@ -723,8 +743,7 @@ def is_not_linux(): # Second check for the existence of '-Microsoft' in the uname release field. # This is a good check that we are running in WSL. - # Note the use of indexing instead of the named field, for Python 2 & 3 compatability. - if "Microsoft" in platform.uname()[2]: + if "Microsoft" in platform.uname().release: return True # Else, we're probably in native Linux! diff --git a/auto_rx/autorx/web.py b/auto_rx/autorx/web.py index 9640012e..4457d4e7 100644 --- a/auto_rx/autorx/web.py +++ b/auto_rx/autorx/web.py @@ -21,6 +21,7 @@ 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 queue import Queue from threading import Thread import flask from flask import request, abort, make_response, send_file @@ -35,13 +36,6 @@ ) sys.exit(1) -try: - # Python 2 - from Queue import Queue -except ImportError: - # Python 3 - from queue import Queue - # Inhibit Flask warning message about running a development server... (we know!) cli = sys.modules["flask.cli"] @@ -342,7 +336,7 @@ def flask_export_selected_log_files(serialb64): _zip, mimetype="application/zip", as_attachment=True, - attachment_filename=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}.zip", ) ) @@ -373,7 +367,7 @@ def flask_export_all_log_files(): _zip, mimetype="application/zip", as_attachment=True, - attachment_filename=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}.zip", ) ) @@ -562,7 +556,12 @@ def refresh_client(arg1): def flask_thread(host="0.0.0.0", port=5000): """ Flask Server Thread""" - socketio.run(app, host=host, port=port) + try: + socketio.run(app, host=host, port=port, allow_unsafe_werkzeug=True) + except TypeError: + # Catch old flask-socketio version. + logging.debug("Web - Not using allow_unsafe_werkzeug argument.") + socketio.run(app, host=host, port=port) def start_flask(host="0.0.0.0", port=5000): @@ -631,7 +630,7 @@ def __init__(self, max_age=120): """ Initialise a WebExporter object. Args: - max_age: Store telemetry data up to X hours old + max_age: Store telemetry data up to X minutes old """ self.max_age = max_age * 60 diff --git a/auto_rx/build.sh b/auto_rx/build.sh index a8e2015b..0c84746a 100755 --- a/auto_rx/build.sh +++ b/auto_rx/build.sh @@ -1,85 +1,42 @@ #!/bin/bash # # Auto Sonde Decoder build script. -# -# TODO: Convert this to a makefile! Any takers? - -# Auto_RX version number - needs to match the contents of autorx/__init__.py -# This can probably be done automatically. -#AUTO_RX_VERSION="\"1.4.1-beta8\"" +# Get the auto-rx version. AUTO_RX_VERSION="\"$(python3 -m autorx.version 2>/dev/null || python -m autorx.version)\"" echo "Building for radiosonde_auto_rx version: $AUTO_RX_VERSION" -VERS_FLAG="-DVER_JSN_STR=$AUTO_RX_VERSION" - - -# Build rs_detect. -echo "Building dft_detect" -cd ../scan/ -gcc dft_detect.c -lm -o dft_detect -DNOC34C50 -w -Ofast - -# New demodulators -cd ../demod/mod/ -echo "Building shared demod libraries." -gcc -c demod_mod.c -w -Ofast -gcc -c bch_ecc_mod.c -w -O3 -echo "Building RS41 demod." -gcc rs41mod.c demod_mod.o bch_ecc_mod.o -lm -O3 -o rs41mod -w $VERS_FLAG -echo "Building DFM demod." -gcc dfm09mod.c demod_mod.o -lm -O3 -o dfm09mod -w $VERS_FLAG -echo "Building RS92 demod." -gcc rs92mod.c demod_mod.o bch_ecc_mod.o -lm -O3 -o rs92mod -w $VERS_FLAG -echo "Building LMS6-400 demod." -gcc lms6mod.c demod_mod.o bch_ecc_mod.o -lm -O3 -o lms6mod -w $VERS_FLAG -gcc lms6Xmod.c demod_mod.o bch_ecc_mod.o -lm -O3 -o lms6Xmod -w $VERS_FLAG -echo "Building Meisei demod." -gcc meisei100mod.c demod_mod.o bch_ecc_mod.o -lm -O3 -o meisei100mod -w $VERS_FLAG -echo "Building M10 demod." -gcc m10mod.c demod_mod.o -lm -O3 -o m10mod -w $VERS_FLAG -echo "Building M20 demod." -gcc m20mod.c demod_mod.o -lm -O3 -o m20mod -w $VERS_FLAG -echo "Building iMet-54 demod." -gcc imet54mod.c demod_mod.o -lm -O3 -o imet54mod -w $VERS_FLAG -echo "Building MRZ demod." -gcc mp3h1mod.c demod_mod.o -lm -O3 -o mp3h1mod -w $VERS_FLAG - -# Build LMS6-1680 Decoder -echo "Building LMS6-1680 demod." -cd ../../mk2a/ -gcc mk2a1680mod.c -Ofast -lm -o mk2mod $VERS_FLAG - -echo "Building iMet-4 demod." -cd ../imet/ -# Note -O3 flag removed from this demodulator until Bus Error bug can be resolved. -gcc imet1rs_dft.c -lm -Ofast -o imet1rs_dft $VERS_FLAG +cd $(dirname $0) -echo "Building fsk-demod utils from codec2" -cd ../utils/ -# This produces a static build of fsk_demod -gcc fsk_demod.c fsk.c modem_stats.c kiss_fftr.c kiss_fft.c -lm -O3 -o fsk_demod +#cd ../utils/ # Build tsrc - this is only required for the test/test_demod.py script, so is not included in the standard build. #gcc tsrc.c -o tsrc -lm -lsamplerate # If running under OSX and using MacPorts, you may need to uncomment the following line to be able to find libsamplerate. #gcc tsrc.c -o tsrc -lm -lsamplerate -I/opt/local/include -L/opt/local/lib +# ... and for homebrew users. +#gcc tsrc.c -o tsrc -lm -lsamplerate -I/opt/homebrew/include -L/opt/homebrew/lib +# Clean before build to ensure the auto_rx version is updated. +make -C .. clean all # Copy all necessary files into this directory. echo "Copying files into auto_rx directory." cd ../auto_rx/ -cp ../scan/dft_detect . -cp ../utils/fsk_demod . -cp ../imet/imet1rs_dft . -cp ../mk2a/mk2mod . -cp ../demod/mod/rs41mod . -cp ../demod/mod/dfm09mod . -cp ../demod/mod/m10mod . -cp ../demod/mod/m20mod . -cp ../demod/mod/rs92mod . -cp ../demod/mod/lms6Xmod . -cp ../demod/mod/meisei100mod . -cp ../demod/mod/imet54mod . -cp ../demod/mod/mp3h1mod . +mv ../scan/dft_detect . +mv ../utils/fsk_demod . +mv ../imet/imet4iq . +mv ../mk2a/mk2a1680mod . +mv ../demod/mod/rs41mod . +mv ../demod/mod/dfm09mod . +mv ../demod/mod/m10mod . +mv ../demod/mod/m20mod . +mv ../demod/mod/rs92mod . +mv ../demod/mod/lms6Xmod . +mv ../demod/mod/meisei100mod . +mv ../demod/mod/imet54mod . +mv ../demod/mod/mp3h1mod . +mv ../demod/mod/mts01mod . +mv ../demod/mod/iq_dec . echo "Done!" diff --git a/auto_rx/clean.sh b/auto_rx/clean.sh index c9507503..6ac1bdf8 100755 --- a/auto_rx/clean.sh +++ b/auto_rx/clean.sh @@ -3,67 +3,28 @@ # Auto Sonde Decoder clean script. # -# TODO: Convert this to a makefile! Any takers? - -# rs_detect. -echo "Cleaning dft_detect" -cd ../scan/ -rm dft_detect - -echo "Cleaning RS92/RS41/DFM/LMS6/iMS Demodulators" - - -# New demodulators -cd ../demod/mod/ - -rm *.o -rm rs41mod -rm dfm09mod -rm rs92mod -rm lms6mod -rm lms6Xmod -rm meisei100mod -rm m10mod -rm m20mod -rm mXXmod -rm mp3h1mod -rm imet54mod - -# LMS6-1680 Decoder -echo "Cleaning LMS6-1680 Demodulator." -cd ../../mk2a/ - -rm mk2mod - -echo "Cleaning iMet Demodulator." -cd ../imet/ - -rm imet1rs_dft - - -echo "Cleaning fsk_demod" -cd ../utils/ - -rm fsk_demod +# Clean all binaries +echo "Cleaning all binaries." +make -C .. clean echo "Removing binaries in the auto_rx directory." cd ../auto_rx/ rm dft_detect rm fsk_demod -rm imet1rs_dft -rm mk2a_lms1680 -rm mk2mod +rm imet4iq +rm mk2a1680mod rm rs41mod rm rs92mod rm dfm09mod rm m10mod -rm mXXmod rm m20mod rm lms6Xmod rm meisei100mod rm mp3h1mod rm imet54mod +rm mts01mod +rm iq_dec echo "Done!" diff --git a/auto_rx/requirements.txt b/auto_rx/requirements.txt index 48fde231..c9ebba6d 100644 --- a/auto_rx/requirements.txt +++ b/auto_rx/requirements.txt @@ -1,7 +1,7 @@ crcmod python-dateutil -flask==2.2.2 -flask-socketio==5.2.0 +flask +flask-socketio numpy requests semver diff --git a/auto_rx/station.cfg.example b/auto_rx/station.cfg.example index 7e350c8f..c5dbca5f 100644 --- a/auto_rx/station.cfg.example +++ b/auto_rx/station.cfg.example @@ -9,20 +9,58 @@ # - [search_params] -> min_freq, max_freq - Set these suitable for your location! # -################### -# RTLSDR SETTINGS # -################### +################ +# SDR SETTINGS # +################ [sdr] -# Number of RTLSDRs to use. +# +# SDR Type +# +# RTLSDR - Use one or more RTLSDRs +# +# 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. +# +sdr_type = RTLSDR + + +# +# Number of SDRs or SDR Connection Threads to use +# +# If SDR type is set to RTLSDR above, then this number is the number of individual RTLSDRs +# that will be used, eith each RTLSDR allocated a scan or decode task. # If more than one RTLSDR is in use, multiple [sdr_X] sections must be populated below +# +# If SDR type is either KA9Q or SpyServer, this defines the maximum number of parallel +# decoding/scan tasks. On a RPi 4, ~5 tasks are possible. +# sdr_quantity = 1 -# Individual SDR Settings. + +# +# 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. +# +sdr_hostname = localhost +sdr_port = 5555 + +# +# Individual RTLSDR Settings +# +# Provide details of your RTLSDRs here, e.g. device numbers, gain settings, and PPM offsets. +# If using a Network SDR, do not modify any of these settings. +# [sdr_1] # Device Index / Serial # If using a single RTLSDR, set this value to 0 # If using multiple SDRs, you MUST allocate each SDR a unique serial number using rtl_eeprom +# The serial number must NOT be 00000000 or 00000001, as this causes big confusion to the rtl utilities. # i.e. to set the serial number of a (single) connected RTLSDR: rtl_eeprom -s 00000002 # Then set the device_idx below to 00000002, and repeat for the other [sdr_n] sections below # @@ -68,6 +106,9 @@ bias = False # In these areas I suggest using the only_scan feature below instead of using the peak-detect search. # You may also need to apply a small offset to the frequency for decoding reliability (i.e. 1676.025 MHz) as # the sondes are often off-frequency. For now, check in something like GQRX to get an idea of what offset is required. +# +# Note - when using either a KA9Q or SpyServer as the SDR backend, the frequency scan limits are set by the +# Server. min_freq = 400.05 max_freq = 403.0 @@ -89,6 +130,11 @@ never_scan = [] # This is useful when you know the regular frequency of a local sonde, but still want to allow detections on other frequencies. always_scan = [] +# always_decode - Always-running decoders. Only possible in a multi-SDR (or network-based SDR) system. +# List must be in the form: [[401.5,"RS41"], [402.5,"DFM"], [400.5,"M10"], [400.5,"IMET"]] +# Valid sonde types: RS41, RS92, DFM, M10, M20, IMET, IMET5, MK2LMS, LMS6, MEISEI, MRZ, MTS01 +always_decode = [] + #################### # STATION LOCATION # @@ -97,6 +143,10 @@ always_scan = [] # Lat/Lon in decimal degrees, altitude in metres. # Note: You do not need to specify your home station accurately if you don't want to! # Feel free to use a position somewhere near your general area, that doesn't identify your home location. +# +# If this is a mobile or portable station, it is recommended to leave this at 0.0, and disable +# 'upload_listener_position' in the section below. Mobile station positions should be uploaded using +# ChaseMapper. [location] station_lat = 0.0 station_lon = 0.0 @@ -105,8 +155,9 @@ station_alt = 0.0 # Station Position from GPSD # If your station is likely to move, then you may wish to have your station position updated from GPSD. # NOTE: This feature is intended to make life slightly easier when using an auto_rx station in a portable -# capacity, in particular when using a rotator. For the web interface to start up correctly, a lat/lon still -# needs to be entered above. +# capacity, in particular when using a rotator. It is *not* intended for uploading positions of mobile +# chase-cars. +# For the web interface to start up correctly, a lat/lon still needs to be entered above. # If you are operating a stationary auto_rx station, please just set a fixed position above rather than using GPSD. # # If you are doing mobile balloon chasing, please use chasemapper ( https://github.com/projecthorus/chasemapper ) @@ -128,10 +179,14 @@ gpsd_port = 2947 [habitat] # Uploader callsign. PLEASE CHANGE THIS TO SOMETHING UNIQUE. -uploader_callsign = CHANGEME_AUTO_RX +# If using ChaseMapper to upload mobile station positions, ensure this callsign +# is set identically to that set in ChaseMapper. +uploader_callsign = CHANGEME # Upload listener position to Sondehub? (So you show up on the map) # Note that if GPSD is used, this is overriden and enabled. +# If this is a mobile or portable station, it is recommended to set this to False. +# Mobile stations should use ChaseMapper or the SondeHub Tracker to upload positions. upload_listener_position = True # Uploader Antenna Description. @@ -344,6 +399,12 @@ to = someone@example.com # - Sonde Serial Number (i.e. M1234567) subject = Sonde launch detected on : +# Custom nearby landing subject field. The following fields can be included: +# - Sonde Frequency, i.e. 401.520 MHz +# - Sonde Type (RS94/RS41) +# - Sonde Serial Number (i.e. M1234567) +nearby_landing_subject = Nearby Radiosonde Landing Detected - + ################### # ROTATOR CONTROL # @@ -380,6 +441,17 @@ rotator_home_elevation = 0.0 # If enabled, a log file will be written to ./log/ for each detected radiosonde. per_sonde_log = True +# Enable logging of system-level logs to disk. +# This is the equivalent of starting auto_rx with the --systemlog option, but only takes effect after the config file is read. +# These logs will end up in the log directory. +save_system_log = False + +# Enable logging of debug messages. +# This is the equivalent of starting auto_rx with the -v option, but only takes effect after the config file is read. +# This setting, in combination with save_system_log (above), can help provide detailed information when debugging +# auto_rx operational issues. +enable_debug_logging = False + ########################### # WEB INTERFACE SETTINNGS # @@ -434,6 +506,7 @@ save_decode_iq = False # Saving raw data is currently only supported for: RS41, LMS6-1680, LMS6-400, M10, M20, IMET-4 save_raw_hex = False + ##################### # ADVANCED SETTINGS # ##################### @@ -474,6 +547,11 @@ payload_id_valid = 3 sdr_fm_path = rtl_fm sdr_power_path = rtl_power +# Paths to SpyServer client (https://github.com/miweber67/spyserver_client) utilities, for experimental SpyServer Client support +# At the moment we assume these are in the auto_rx directory. +ss_iq_path = ./ss_iq +ss_power_path = ./ss_power + ################################ # DEMODULATOR / DECODER TWEAKS # @@ -495,6 +573,8 @@ dfm_experimental = True m10_experimental = True lms6-400_experimental = True imet54_experimental = True +meisei_experimental = True + # LMS6-1680 Decoder options: # False = Less CPU, possibly worse weak-signal performance. # True = Higher CPU (Pushing a RPi 3 to its limits), possibly better weak-signal performance. @@ -504,6 +584,8 @@ 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 diff --git a/auto_rx/station.cfg.example.network b/auto_rx/station.cfg.example.network new file mode 100644 index 00000000..a0062617 --- /dev/null +++ b/auto_rx/station.cfg.example.network @@ -0,0 +1,621 @@ +# +# Radiosonde Auto RX v2 Station Configuration File +# +# Copy this file to station.cfg and modify as required. +# +# If you only change a few settings, the important ones to change are: +# - [habitat] -> uploader_callsign - Your station callsign! +# - [location] -> station_lat, station_lon, station_alt - Your station location! +# - [search_params] -> min_freq, max_freq - Set these suitable for your location! +# + + +################ +# SDR SETTINGS # +################ +[sdr] + +# +# SDR Type +# +# RTLSDR - Use one or more RTLSDRs +# +# 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. +# +sdr_type = SpyServer + + +# +# Number of SDRs or SDR Connection Threads to use +# +# If SDR type is set to RTLSDR above, then this number is the number of individual RTLSDRs +# that will be used, eith each RTLSDR allocated a scan or decode task. +# If more than one RTLSDR is in use, multiple [sdr_X] sections must be populated below +# +# If SDR type is either KA9Q or SpyServer, this defines the maximum number of parallel +# decoding/scan tasks. On a RPi 4, ~5 tasks are possible. +# +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. +# +sdr_hostname = localhost +sdr_port = 5555 + +# +# Individual RTLSDR Settings +# +# Provide details of your RTLSDRs here, e.g. device numbers, gain settings, and PPM offsets. +# If using a Network SDR, do not modify any of these settings. +# +[sdr_1] +# Device Index / Serial +# If using a single RTLSDR, set this value to 0 +# If using multiple SDRs, you MUST allocate each SDR a unique serial number using rtl_eeprom +# The serial number must NOT be 00000000 or 00000001, as this causes big confusion to the rtl utilities. +# i.e. to set the serial number of a (single) connected RTLSDR: rtl_eeprom -s 00000002 +# Then set the device_idx below to 00000002, and repeat for the other [sdr_n] sections below +# +# For the special case when auto_rx is used entirely with a JSON-via-UDP input, set the below to TCP001 +# This will bypass all RTLSDR checks and allow auto_rx to continue running. +device_idx = 0 + +# Frequency Correction (ppm offset) +# Refer here for a method of determining this correction using LTE signals: https://gist.github.com/darksidelemm/b517e6a9b821c50c170f1b9b7d65b824 +ppm = 0 + +# SDR Gain Setting +# Gain settings can generally range between 0dB and 40dB depending on the tuner in use. +# Run rtl_test to confirm what gain settings are available, or use a value of -1 to use hardware automatic gain control (AGC). +# Note that this is an overall gain value, not an individual mixer/tuner gain. This is a limitation of the rtl_power/rtl_fm utils. +gain = -1 + +# Bias Tee - Enable the bias tee in the RTLSDR v3 Dongles. +bias = False + +[sdr_2] +# As above, for the next SDR, if used. Note the warning about serial numbers. +device_idx = 00000002 +ppm = 0 +gain = -1 +bias = False + +# Add more SDR definitions here if needed. ([sdr_3], [sdr_4]) + + +############################## +# RADIOSONDE SEARCH SETTINGS # +############################## +[search_params] +# Minimum and maximum search frequencies, in MHz. +# Australia: Use 400.05 - 403 MHz +# New Zealand: Use 400.05 - 404 MHz +# Europe: Use 400.05 - 406 MHz +# US: +# Some areas have transitioned to the 400.05 - 406 MHz band, so try 400.05 - 406 +# Some areas are still using ~1680 MHz sondes, which use channels of 1676, 1678, 1680 and 1682 MHz (Thanks Tory!) +# Example only_scan for these sondes: [1676.0, 1678.0, 1680.0, 1682.0] +# In these areas I suggest using the only_scan feature below instead of using the peak-detect search. +# You may also need to apply a small offset to the frequency for decoding reliability (i.e. 1676.025 MHz) as +# the sondes are often off-frequency. For now, check in something like GQRX to get an idea of what offset is required. +# +# Note - when using either a KA9Q or SpyServer as the SDR backend, the frequency scan limits are set by the +# server and these settings are ignored. +# Refer https://github.com/projecthorus/radiosonde_auto_rx/wiki/Network-SDR-Decoding-Instructions + +min_freq = 400.05 +max_freq = 403.0 + +# After rx_timeout seconds of no valid data from a decoder, stop the decoder and go back to scanning (if a SDR is available) +rx_timeout = 180 + +# Frequency Lists - These must be provided as JSON-compatible lists of floats (in MHz), i.e. [400.50, 401.520, 403.200] + +# only_scan - Add values to this list to *only* scan on these frequencies. +# This is for when you only want to monitor a small set of launch frequencies. +only_scan = [] + +# never_scan - Any values added to this list will be removed from the list of detected peaks. +# This is used to remove known spurs or other interferers from the scan list, potentially speeding up detection of a sonde. +never_scan = [] + +# always_scan - Any values in this list will be added to the start every scan run. +# This is useful when you know the regular frequency of a local sonde, but still want to allow detections on other frequencies. +always_scan = [] + +# always_decode - Always-running decoders. Only possible in a multi-SDR (or network-based SDR) system. +# List must be in the form: [[401.5,"RS41"], [402.5,"DFM"], [400.5,"M10"], [400.5,"IMET"]] +# Valid sonde types: RS41, RS92, DFM, M10, M20, IMET, IMET5, MK2LMS, LMS6, MEISEI, MRZ, MTS01 +always_decode = [] + +#################### +# STATION LOCATION # +#################### +# Used by the Sondehub Uploader, APRS Uploader, and by Rotator Control +# Lat/Lon in decimal degrees, altitude in metres. +# Note: You do not need to specify your home station accurately if you don't want to! +# Feel free to use a position somewhere near your general area, that doesn't identify your home location. +# +# If this is a mobile or portable station, it is recommended to leave this at 0.0, and disable +# 'upload_listener_position' in the section below. Mobile station positions should be uploaded using +# ChaseMapper. +[location] +station_lat = 0.0 +station_lon = 0.0 +station_alt = 0.0 + +# Station Position from GPSD +# If your station is likely to move, then you may wish to have your station position updated from GPSD. +# NOTE: This feature is intended to make life slightly easier when using an auto_rx station in a portable +# capacity, in particular when using a rotator. It is *not* intended for uploading positions of mobile +# chase-cars. +# For the web interface to start up correctly, a lat/lon still needs to be entered above. +# If you are operating a stationary auto_rx station, please just set a fixed position above rather than using GPSD. +# +# If you are doing mobile balloon chasing, please use chasemapper ( https://github.com/projecthorus/chasemapper ) +# which is far beter suited than auto_rx by itself... +gpsd_enabled = False +gpsd_host = localhost +gpsd_port = 2947 + + +################################################### +# SONDEHUB / HABITAT (deprecated) UPLOAD SETTINGS # +################################################### +# +# Settings relating to uploads to the SondeHub v2 Database and tracker, +# available at https://tracker.sondehub.org/ +# +# Note that uploads to the Habitat (amateur HAB) database are now disabled, +# and any references to Habitat in this config are for legacy reasons only. +[habitat] + +# Uploader callsign. PLEASE CHANGE THIS TO SOMETHING UNIQUE. +# If using ChaseMapper to upload mobile station positions, ensure this callsign +# is set identically to that set in ChaseMapper. +uploader_callsign = CHANGEME + +# Upload listener position to Sondehub? (So you show up on the map) +# Note that if GPSD is used, this is overriden and enabled. +# If this is a mobile or portable station, it is recommended to set this to False. +# Mobile stations should use ChaseMapper or the SondeHub Tracker to upload positions. +upload_listener_position = True + +# Uploader Antenna Description. +# If upload_listener_position is enabled, this information will show up in your station description on the habhub map. +uploader_antenna = 1/4 wave monopole + +[sondehub] +# Enable uploading to Sondehub v2 - please leave this enabled! +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. +sondehub_upload_rate = 15 + +# An optional contact e-mail address. +# This e-mail address will *only* be available to the Sondehub admins, and will *only* +# be used to contact you if there is an obvious issue with your station. +# We recommend that you join the radiosonde_auto_rx mailing list to receive updates: +# https://groups.google.com/forum/#!forum/radiosonde_auto_rx +sondehub_contact_email = none@none.com + + + +######################## +# APRS UPLOAD SETTINGS # +######################## +# Settings for uploading to radiosondy.info +# +# IMPORTANT APRS NOTE +# +# As of auto_rx version 1.5.10, we are limiting APRS output to only radiosondy.info, +# and only on the non-forwarding port. +# This decision was not made lightly, and is a result of the considerable amount of +# non-amateur traffic that radiosonde flights are causing within the APRS-IS network. +# Until some form of common format can be agreed to amongst the developers of *all* +# radiosonde tracking software to enable radiosonde telemetry to be de-duped, +# I have decided to help reduce the impact on the wider APRS-IS network by restricting +# the allowed servers and ports. +# If you are using another APRS-IS server that *does not* forward to the wider APRS-IS +# network and want it allowed, then please raise an issue at +# https://github.com/projecthorus/radiosonde_auto_rx/issues +# +# You are of course free to fork and modify this codebase as you wish, but please be aware +# that this goes against the wishes of the radiosonde_auto_rx developers to not be part +# of the bigger problem of APRS-IS congestion. +# As of 2022-03-01, radiosonde traffic has been filtered from aprs.fi, so even if you do +# modify the code, you still won't see sondes on that map. +# APRS-IS is a *shared resource*, intended for the use of all amateur radio operators, and +# for many years we have been treating it as a playground to dump large amounts of non-amateur +# traffic into, so we can see weather balloons on a map. +# Instead of congesting this shared resource with this non-amateur traffic, we should instead +# be moving to using databases and sites specialised for this purpose, for example sondehub.org + +[aprs] +# Enable APRS upload (you will also need to change some options below!) +aprs_enabled = False + +# APRS-IS Login Information +# The aprs_user field can have an SSID on the end if desired, i.e. N0CALL-4 +aprs_user = N0CALL +# APRS-IS Passcode. You can generate one for your callsign here: https://apps.magicbug.co.uk/passcode/ +aprs_pass = 00000 + +# APRS Upload Rate - Upload a packet every X seconds. +# This has a lower limit of 30 seconds, to avoid flooding radiosondy.info +# Please be respectful, and do not attempt to upload faster than this. +upload_rate = 30 + +# APRS-IS server to upload to. +# Currently we only support uploading to radiosondy.info +# When using port 14580, packets are not forwarded to the wider APRS-IS network, and hence +# are help reduce the huge amount of non-amateur traffic that ends up in APRS-IS from +# radiosondes. +aprs_server = radiosondy.info + +# APRS-IS Port Number to upload to. +# +# Port 14590 - Packets stay within radiosondy.info and do not congest the wider APRS-IS +# network. +# +aprs_port = 14580 + +# APRS Station Location Beaconing +# If enabled, you will show up on APRS using the aprs_user callsign set above. +# This also requires your station location to be set in the above [location] section. +station_beacon_enabled = False + +# Station beaconing rate, in minutes. +station_beacon_rate = 30 + +# Station beacon comment. +# will be replaced with the current auto_rx version number +station_beacon_comment = radiosonde_auto_rx SondeGate v + +# Station Beacon Icon +# The APRS icon to use, as defined in http://www.aprs.org/symbols.html +# Note that the two characters that define the icon need to be concatenated. Examples: +# Antenna Tower = /r +# House with Yagi = /y +# Satellite Dish = /` (This is required if you want to show up on radiosondy.info's station list.) +station_beacon_icon = /` + +# Custom Object name / Callsign to be used when uploading telemetry to APRS-IS (Max 9 chars) +# WARNING: THIS SHOULD BE LEFT AT THE DEFAULT OF UNLESS YOU HAVE A VERY GOOD REASON +# TO CHANGE IT! +# +# Using means the uploads from multiple stations remain consistent, and we don't end up with +# lots of duplicate sondes on APRS-IS. If you enable the station location beaconing (below), maps +# like aprs.fi and radiosondy.info will show your station as the receiver. +# +# If used, this field should be either a callsign with a -11 or -12 suffix (i.e. N0CALL-12), +# or , which will be replaced with the radiosondes serial number. +# +# WARNING - If running multiple RTLSDRs, setting this to a fixed callsign will result in odd behaviour on the online tracker. +# DO NOT SET THIS TO ANYTHING OTHER THAN IF YOU ARE USING MORE THAN ONE SDR! +aprs_object_id = + +# Confirmation of the above setting. +# Set to True to confirm use of the custom object ID set above. +# Please read the notes above before setting this to True. +aprs_use_custom_object_id = False + +# The APRS-IS beacon comment. The following fields can be included: +# - Sonde Frequency, e.g. 401.520 MHz +# - Sonde Type (e.g. RS41-SG, DFM09, etc....) +# - Sonde Serial Number (i.e. M1234567) +# - Sonde Vertical Velocity (i.e. -5.1m/s) +# - Sonde reported temperature. If no temp data available, this will report -273 degC. +# - Sonde reported humidity. +# - Sonde reported pressure +# - Battery Voltage (e.g. 3.1V) +# The default comment (below) is compatible with radiosondy.info's parsers, and should only be modified +# if absolutely necessary. +aprs_custom_comment = Clb= t= h= p= Type= ser= Radiosonde + + +########################### +# CHASEMAPPER DATA OUTPUT # +########################### +# Settings for pushing data into Chasemapper and/or OziPlotter +# Oziplotter receives data via a basic CSV format, via UDP. +# Chasemapper can receive data in either the basic CSV format, or in the more descriptive 'payload summary' JSON format. +[oziplotter] +# How often to output data (seconds) +ozi_update_rate = 5 + +# Enable the 'OziMux' basic CSV output +# Note - this cannot be enabled in a multi-SDR configuration. +ozi_enabled = False + +# OziMux UDP Broadcast output port. +# Set to 8942 to send packets directly into OziPlotter, or set to 55681 to send via OziMux +ozi_port = 8942 + +# Payload summary UDP output, which is the preferred input if using ChaseMapper. +# Using this output allows multiple sondes to be plotted in Chasemapper. +# As of 2019-05, this is enabled by default. +payload_summary_enabled = True +payload_summary_port = 55673 + + +####################### +# EMAIL NOTIFICATIONS # +####################### +# Sends an email notification to the specified address when a new Sonde is detected +[email] +email_enabled = False + +# Send an e-mail for each new radiosonde observed. +launch_notifications = True + +# Send e-mails when a radiosonde is detected descending near your station location +landing_notifications = True + +# Range threshold for Landing notifications (km from your station location) +landing_range_threshold = 30 + +# Altitude threshold for Landing Notifications (metres) +# Notifications will only occur for sondes which have been observed descending (>2m/s descent rate) below this altitude +# for at least 10 observations +landing_altitude_threshold = 1000 + +# Enable sending critical error notifications (one that cause auto_rx to completely crash out) via email. +# WARNING: If an error is persistent and you are running as a systemd service, this could result +# in many e-mails being sent! +error_notifications = False + +# Server details. Note that usually Port 465 is used for SSL auth, and 587 is used for TLS. +smtp_server = localhost +smtp_port = 25 + +# Authentication type, Valid options are: None, TLS, and SSL +smtp_authentication = None + +# If using authentication, the following fields need to be populated: +smtp_login = None +smtp_password = None + +# 'Source' e-mail +from = sonde@localhost + +# Destination emails. You can send to multiple addresses by separating each address with a semicolon, +# i.e. test@test.com;test2@test2.com +to = someone@example.com + +# Custom subject field. The following fields can be included: +# - Sonde Frequency, i.e. 401.520 MHz +# - Sonde Type (RS94/RS41) +# - Sonde Serial Number (i.e. M1234567) +subject = Sonde launch detected on : + +# Custom nearby landing subject field. The following fields can be included: +# - Sonde Frequency, i.e. 401.520 MHz +# - Sonde Type (RS94/RS41) +# - Sonde Serial Number (i.e. M1234567) +nearby_landing_subject = Nearby Radiosonde Landing Detected - + +################### +# ROTATOR CONTROL # +################### +# auto_rx can communicate with an instance of rotctld, on either the local machine or elsewhere on the network. +# This is, admittedly, a bit of a waste of an az/el rotator setup when a vertical antenna usually works fine, +# but it's still fun :-) +[rotator] +# WARNING - This should not be enabled in a multi-SDR configuration. +# Your station location (defined in the [location] section above) must also be defined. +rotator_enabled = False +# How often to update the rotator position. (Seconds) +update_rate = 30 +# Only move the rotator if the new position is more than X degrees in azimuth or elevation from the current position. +rotation_threshold = 5.0 +# Hostname / Port of the rotctld instance. +rotator_hostname = 127.0.0.1 +rotator_port = 4533 +# Rotator Homing. +# If enabled, turn to a 'home' location when scanning for sondes. +# This could be used to point an antenna at a known radiosonde launch location. +rotator_homing_enabled = False +# Wait until x minutes after no sonde data received before moving to the home location. +rotator_homing_delay = 10 +# Rotator home azimuth/elevation, in degrees true. +rotator_home_azimuth = 0.0 +rotator_home_elevation = 0.0 + + +########### +# LOGGING # +########### +[logging] +# If enabled, a log file will be written to ./log/ for each detected radiosonde. +per_sonde_log = True + +# Enable logging of system-level logs to disk. +# This is the equivalent of starting auto_rx with the --systemlog option, but only takes effect after the config file is read. +# These logs will end up in the log directory. +save_system_log = False + +# Enable logging of debug messages. +# This is the equivalent of starting auto_rx with the -v option, but only takes effect after the config file is read. +# This setting, in combination with save_system_log (above), can help provide detailed information when debugging +# auto_rx operational issues. +enable_debug_logging = False + +########################### +# WEB INTERFACE SETTINNGS # +########################### +[web] +# Server Host - Can be set to :: to listen on IPv6 +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 +# Archive Age - How long to keep a sonde telemetry in memory for the web client to access, in minutes +# Note: The higher this number, the more data the client will need to load in on startup +archive_age = 120 + +# Enable control over the scanner, and starting/stopping decoders from the web interface. +# A password must also be set (below). +# USERS ENABLE THIS AT THEIR OWN RISK!! +web_control = False + +# Password for controls on the web interface. The main interface will still be publicly available. +# Note that as the web interface is HTTP-only, this password will be sent over the internet in the clear, +# so don't re-use passwords! +web_password = none + +# KML refresh rate +kml_refresh_rate = 10 + + +################## +# DEBUG SETTINGS # +################## +[debugging] +# WARNING - Enabling these settings can result in lots of SD-card IO, potentially +# reducing the life of the card. These should only be enabled to collect data for +# debugging purposes. + +# Save the audio that a detection pass is run over to: detect_.wav +# This file is over-written with every new detection. +save_detection_audio = False + +# Save the audio from the output from a sonde decode chain to decode_.wav +# This file is over-written with each new sonde decoded for a particular SDR. +# This only works for the 'legacy' FM-based demodulators. +save_decode_audio = False + +# Save the decimated IQ data from an experimental sonde decode chain to decode_IQ_.bin +# This will be in complex signed 16-bit int format, and may be either 48 kHz or 96 kHz. +# Note: This will use a LOT of disk space. +save_decode_iq = False + +# Save raw hexadecimal radiosonde frame data. This is useful to provide data for telemetry analysis. +# Raw hex data is saved to the logging directory with a filename of format YYYYMMDD-HHMMSS__.raw +# Saving raw data is currently only supported for: RS41, LMS6-1680, LMS6-400, M10, M20, IMET-4 +save_raw_hex = False + + +##################### +# ADVANCED SETTINGS # +##################### +# These control low-level settings within various modules. +# Playing with them may result in odd behaviour. +[advanced] +# Scanner - Receive bin width (Hz) +search_step = 800 +# Scanner - Minimum SNR for a peak to be detected. The lower the number, the more peaks detected. +snr_threshold = 10 +# Scanner - Maximum number of peaks to search through during a scan pass. +# Increase this if you have lots of spurious signals, though this will increase scan times. +max_peaks = 10 +# Scanner - Minimum distance between detected peaks (Hz) +min_distance = 1000 +# Scanner - Scan Dwell Time - How long to observe the specified spectrum for. +scan_dwell_time = 20 +# Scanner - Detection Dwell time - How long to wait for a sonde detection on each peak. +detect_dwell_time = 5 +# Scanner - Delay between scans. We should delay a short amount between scans to allow for decoders and other actions to jump in. +scan_delay = 10 +# Quantize search results to x Hz steps. Useful as most sondes are on 10 kHz frequency steps. +quantization = 10000 +# Decoder Spacing Limit - Only start a new decoder if it is separated from an existing decoder by at least +# this value (Hz). This helps avoid issues where a drifting radiosonde is detected on two adjacent channels. +# If you regularly encounter radiosondes on adjacent (10kHz) channels, then set this value to 5000. +decoder_spacing_limit = 15000 +# Temporary Block Time (minutes) - How long to block encrypted or otherwise non-decodable sondes for. +temporary_block_time = 120 +# Upload when (seconds_since_utc_epoch%upload_rate) == 0. Otherwise just delay upload_rate seconds between uploads. +# Setting this to True with multple uploaders should give a higher chance of all uploaders uploading the same frame, +# however the upload_rate should not be set too low, else there may be a chance of missing upload slots. +synchronous_upload = True +# Only accept a payload ID as valid until it has been seen N times. +# This helps avoid corrupted callsigns getting onto the map. +payload_id_valid = 3 +# Paths to the rtl_fm and rtl_power utilities. If these are on your system path, then you don't need to change these. +sdr_fm_path = rtl_fm +sdr_power_path = rtl_power + +# Paths to SpyServer client (https://github.com/miweber67/spyserver_client) utilities, for experimental SpyServer Client support +# At the moment we assume these are in the auto_rx directory. +ss_iq_path = ./ss_iq +ss_power_path = ./ss_power + + +################################ +# DEMODULATOR / DECODER TWEAKS # +################################ + +# +# Alternate Decode Chains +# +# NOTE: This section will be deprecated soon, with the fsk_demod chains used permanently. +# +# These (not so) experimental demod chains use David Rowe's fsk_demod modem. +# They have much better drift handling performance, and so may be better suited for +# 1680 MHz sondes, but use considerably more CPU. Recommend use of a Pi 3 or better. +# The experimental decoders can be enabled/disabled independently for each radiosonde type. +# It is recommended that these are always left enabled. +rs41_experimental = True +rs92_experimental = True +dfm_experimental = True +m10_experimental = True +lms6-400_experimental = True +imet54_experimental = True +meisei_experimental = True + +# LMS6-1680 Decoder options: +# False = Less CPU, possibly worse weak-signal performance. +# True = Higher CPU (Pushing a RPi 3 to its limits), possibly better weak-signal performance. +lms6-1680_experimental = False + +# MRZ sondes have not yet been tested with the fsk_demod flowgraph in the wild. +# 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 +# This flag sets the use of IQ detection when scanning for 1680 MHz sondes, which allows RS92-NGP sondes +# to be detected reliably. +# Set this to True if you are sure that only RS92-NGPs are flying in your area. +ngp_tweak = False + + +###################### +# POSITION FILTERING # +###################### +# These are used to discard positions which are clearly bad, such as where the payload has jumped halfway around the world, +# or has suddenly ended up in orbit. +# +# Users can also decide to temporary block sondes which are outside of a radius range. +# +[filtering] +# Discard positions with an altitude greater than 50000 metres. +max_altitude = 50000 + +# Discard positions from sondes which are outside the following min/max radius limits. +# This requires the ground-station location to be set. +max_radius_km = 1000 +min_radius_km = 0 + +# Temporarily block the frequency of sondes which report a position outside of the above limits. +# WARNING: This may cause sondes which sometimes report glitchy positions (RS92, DFM) to be blocked. +# (True or False) +radius_temporary_block = False + +# Reported Time Threshold +# Discard positions if the sonde's reported time is more than X hours from the system time. +# This helps catch glitches around 00Z UTC from iMet & LMS6 sondes, and bad CRC checks from +# DFM sondes. +sonde_time_threshold = 3 \ No newline at end of file diff --git a/auto_rx/test/bit_to_samples.py b/auto_rx/test/bit_to_samples.py index 891fab85..972bdaaf 100644 --- a/auto_rx/test/bit_to_samples.py +++ b/auto_rx/test/bit_to_samples.py @@ -4,27 +4,17 @@ # import sys -# Check if we are running in Python 2 or 3 -PY3 = sys.version_info[0] == 3 _sample_rate = int(sys.argv[1]) _symbol_rate = int(sys.argv[2]) _bit_length = _sample_rate // _symbol_rate -if PY3: - _zero = b'\x00'*_bit_length - _one = b'\xFF'*_bit_length +_zero = b'\x00'*_bit_length +_one = b'\xFF'*_bit_length - _in = sys.stdin.read - _out = sys.stdout.buffer.write - -else: - _zero = '\x00'*_bit_length - _one = '\xFF'*_bit_length - - _in = sys.stdin.read - _out = sys.stdout.write +_in = sys.stdin.read +_out = sys.stdout.buffer.write while True: _char = _in(1) diff --git a/auto_rx/test/generate_lowsnr.py b/auto_rx/test/generate_lowsnr.py index 23ff41a2..4540af0c 100644 --- a/auto_rx/test/generate_lowsnr.py +++ b/auto_rx/test/generate_lowsnr.py @@ -45,7 +45,9 @@ ['imet54_96k_float.bin', 4800, -10.0, 96000], # 4800 baud GMSK ['rsngp_96k_float.bin', 2400, -100.0, 96000], # RS92-NGP - wider bandwidth. ['lms6-400_96k_float.bin', 4800, -100, 96000], # LMS6, 400 MHz variant. Continuous signal. - ['mrz_96k_float.bin', 2400, -100, 96000] # MRZ Continuous signal. + ['mrz_96k_float.bin', 2400, -100, 96000], # MRZ Continuous signal. + ['m20_96k_float.bin', 9600, -15, 96000], # M20, kind of continuous signal? residual carrier when not transmitting + ['mts01_96k_float.bin', 1200, -20, 96000] ] diff --git a/auto_rx/test/plot_per.py b/auto_rx/test/plot_per.py index eef6f8ee..66097a45 100644 --- a/auto_rx/test/plot_per.py +++ b/auto_rx/test/plot_per.py @@ -29,15 +29,30 @@ # } +# soft-decision decoding, with a +0.25 Fs signal centre. +# sonde_types = { +# 'RS41': {'csv':'rs41_fsk_demod_soft.txt', 'packets': 118, 'color': 'C0'}, +# 'RS92': {'csv':'rs92_fsk_demod_soft.txt', 'packets': 120, 'color': 'C1'}, +# 'RS92-NGP': {'csv':'rs92ngp_fsk_demod_soft.txt', 'packets': 120, 'color': 'C2'}, +# 'DFM09': {'csv':'dfm_fsk_demod_soft.txt', 'packets': 96, 'color': 'C3'}, +# 'M10': {'csv':'m10_fsk_demod_soft.txt', 'packets': 120, 'color': 'C4'}, +# 'LMS6-400': {'csv':'lms6-400_fsk_demod_soft.txt', 'packets': 120, 'color': 'C5'}, +# 'MRZ': {'csv':'mrz_fsk_demod_soft.txt', 'packets': 105, 'color': 'C6'}, +# 'iMet-54': {'csv':'imet54_fsk_demod_soft.txt', 'packets': 240, 'color': 'C7'}, +# } + +# soft-decision decoding, with a 0Hz signal centre. sonde_types = { - 'RS41': {'csv':'rs41_fsk_demod_soft.txt', 'packets': 118, 'color': 'C0'}, - 'RS92': {'csv':'rs92_fsk_demod_soft.txt', 'packets': 120, 'color': 'C1'}, - 'RS92-NGP': {'csv':'rs92ngp_fsk_demod_soft.txt', 'packets': 120, 'color': 'C2'}, - 'DFM09': {'csv':'dfm_fsk_demod_soft.txt', 'packets': 96, 'color': 'C3'}, - 'M10': {'csv':'m10_fsk_demod_soft.txt', 'packets': 120, 'color': 'C4'}, - 'LMS6-400': {'csv':'lms6-400_fsk_demod_soft.txt', 'packets': 120, 'color': 'C5'}, - 'MRZ': {'csv':'mrz_fsk_demod_soft.txt', 'packets': 105, 'color': 'C6'}, - 'iMet-54': {'csv':'imet54_fsk_demod_soft.txt', 'packets': 240, 'color': 'C7'}, + 'RS41': {'csv':'rs41_fsk_demod_soft_centre.txt', 'packets': 118, 'color': 'C0'}, + 'RS92': {'csv':'rs92_fsk_demod_soft_centre.txt', 'packets': 120, 'color': 'C1'}, + 'RS92-NGP': {'csv':'rs92ngp_fsk_demod_soft_centre.txt', 'packets': 120, 'color': 'C2'}, + 'DFM09': {'csv':'dfm_fsk_demod_soft_centre.txt', 'packets': 96, 'color': 'C3'}, + 'M10': {'csv':'m10_fsk_demod_soft_centre.txt', 'packets': 120, 'color': 'C4'}, + 'LMS6-400': {'csv':'lms6-400_fsk_demod_soft_centre.txt', 'packets': 120, 'color': 'C5'}, + 'MRZ': {'csv':'mrz_fsk_demod_soft_centre.txt', 'packets': 105, 'color': 'C6'}, + 'iMet-54': {'csv':'imet54_fsk_demod_soft_centre.txt', 'packets': 240, 'color': 'C7'}, + 'M20': {'csv':'m20_fsk_demod_soft_centre.txt', 'packets': 120, 'color': 'C8'}, + 'iMet-4': {'csv':'imet4_iq.txt', 'packets': 218, 'color': 'C9'}, } @@ -85,5 +100,5 @@ def read_csv(filename): plt.grid() plt.ylabel("Packet Error Rate") plt.xlabel("Eb/No (dB)") -plt.title("auto_rx Decode Chain Performance - fsk_demod") +plt.title("auto_rx Decode Chain Performance - fsk_demod soft-decision 0 Hz") plt.show() \ No newline at end of file diff --git a/auto_rx/test/test_demod.py b/auto_rx/test/test_demod.py index a28ab001..e51d16f5 100644 --- a/auto_rx/test/test_demod.py +++ b/auto_rx/test/test_demod.py @@ -207,6 +207,16 @@ "post_process" : " | grep frame | wc -l", 'files' : "./generated/rs41*" }, + 'rs41_fsk_demod_soft_centre': { + # Keep signal centred. + 'demod' : "| csdr convert_f_s16 | ./tsrc - - 0.500 | ../fsk_demod --cs16 -b -10000 -u 10000 -s --stats=5 2 48000 4800 - - 2>stats.txt |", + + # Decode using rs41ecc + 'decode': "../rs41mod --ecc --ptu --crc --softin -i --json 2>/dev/null", + # Count the number of telemetry lines. + "post_process" : " | grep frame | wc -l", + 'files' : "./generated/rs41*" + }, # RS92 Decoding 'rs92_fsk_demod_soft': { # Shift up to ~24 khz, and then pass into fsk_demod. @@ -218,6 +228,16 @@ "post_process" : " | grep M2513116 | wc -l", 'files' : "./generated/rs92*" }, + 'rs92_fsk_demod_soft_centre': { + # Shift up to ~24 khz, and then pass into fsk_demod. + 'demod' : "| csdr convert_f_s16 | ./tsrc - - 0.500 | ../fsk_demod --cs16 -b -10000 -u 10000 -s --stats=5 2 48000 4800 - - 2>stats.txt |", + + # Decode using rs41ecc + 'decode': "../rs92mod -vx -v --crc --ecc --vel --softin -i 2>/dev/null", + # Count the number of telemetry lines. + "post_process" : " | grep M2513116 | wc -l", + 'files' : "./generated/rs92*" + }, # RS92-NGP Decoding 'rs92ngp_fsk_demod_soft': { # Shift up to ~24 khz, and then pass into fsk_demod. @@ -229,6 +249,16 @@ "post_process" : " | grep P3213708 | wc -l", 'files' : "./generated/rsngp*" }, + 'rs92ngp_fsk_demod_soft_centre': { + # Shift up to ~24 khz, and then pass into fsk_demod. + 'demod' : "| csdr convert_f_s16 | ../fsk_demod --cs16 -b -20000 -u 20000 -s --stats=5 2 96000 4800 - - 2>stats.txt |", + + # Decode using rs41ecc + 'decode': "../rs92mod -vx -v --crc --ecc --vel --ngp --softin -i 2>/dev/null", + # Count the number of telemetry lines. + "post_process" : " | grep P3213708 | wc -l", + 'files' : "./generated/rsngp*" + }, 'm10_fsk_demod_soft': { # Shift up to ~24 khz, and then pass into fsk_demod. 'demod' : "| csdr shift_addition_cc 0.125 2>/dev/null | csdr convert_f_s16 | ../tsrc - - 0.50083333333 -c | ../fsk_demod --cs16 -b 1 -p 5 -u 23000 -s --stats=5 2 48080 9616 - - 2>stats.txt |", @@ -237,6 +267,24 @@ "post_process" : "| grep aprsid | wc -l", 'files' : "./generated/m10*" }, + 'm10_fsk_demod_soft_centre': { + # Shift up to ~24 khz, and then pass into fsk_demod. + 'demod' : "| csdr convert_f_s16 | ../tsrc - - 0.50083333333 -c | ../fsk_demod --cs16 -b -10000 -p 5 -u 10000 -s --stats=5 2 48080 9616 - - 2>stats.txt |", + 'decode': "../m10mod --json --softin -i -vvv 2>/dev/null", + # Count the number of telemetry lines. + "post_process" : "| grep aprsid | wc -l", + 'files' : "./generated/m10*" + }, + 'm20_fsk_demod_soft_centre': { + # Shift up to ~24 khz, and then pass into fsk_demod. + 'demod' : "| csdr convert_f_s16 | ./tsrc - - 0.500 | ../fsk_demod --cs16 -p 5 -b -10000 -u 10000 -s --stats=5 2 48000 9600 - - 2>stats.txt |", + + # Decode using rs41ecc + 'decode': "../m20mod --json --ptu -vvv --softin -i 2>/dev/null", + # Count the number of telemetry lines. + "post_process" : " | grep rawid | wc -l", + 'files' : "./generated/m20*" + }, 'dfm_fsk_demod_soft': { # cat ./generated/dfm09_96k_float_15.0dB.bin | csdr shift_addition_cc 0.25000 2>/dev/null | csdr convert_f_s16 | #./tsrc - - 1.041666 | ../fsk_demod --cs16 -b 1 -u 45000 2 100000 2500 - - 2>/dev/null | @@ -249,7 +297,18 @@ #"post_process" : "| grep -o '\[OK\]' | wc -l", # No ECC 'files' : "./generated/dfm*.bin" }, + 'dfm_fsk_demod_soft_centre': { + # cat ./generated/dfm09_96k_float_15.0dB.bin | csdr shift_addition_cc 0.25000 2>/dev/null | csdr convert_f_s16 | + #./tsrc - - 1.041666 | ../fsk_demod --cs16 -b 1 -u 45000 2 100000 2500 - - 2>/dev/null | + #python ./bit_to_samples.py 50000 2500 | sox -t raw -r 50k -e unsigned-integer -b 8 -c 1 - -r 50000 -b 8 -t wav - 2>/dev/null| + #../dfm09ecc -vv --json --dist --auto + 'demod': '| csdr convert_f_s16 | ../tsrc - - 0.5208| ../fsk_demod --cs16 -b -10000 -u 10000 -s --stats=5 2 50000 2500 - - 2>stats.txt |',#' python ./bit_to_samples.py 50000 2500 | sox -t raw -r 50k -e unsigned-integer -b 8 -c 1 - -r 50000 -b 8 -t wav - 2>/dev/null| ', + 'decode': '../dfm09mod -vv --json --dist --auto --softin -i 2>/dev/null', + "post_process" : " | grep frame | wc -l", # ECC + #"post_process" : "| grep -o '\[OK\]' | wc -l", # No ECC + 'files' : "./generated/dfm*.bin" + }, # LMS6-400 Decoding 'lms6-400_fsk_demod_soft': { # Shift up to ~24 khz, and then pass into fsk_demod. @@ -261,7 +320,16 @@ "post_process" : "| grep frame | wc -l", 'files' : "./generated/lms6-400*", }, + 'lms6-400_fsk_demod_soft_centre': { + # Shift up to ~24 khz, and then pass into fsk_demod. + 'demod' : "| csdr convert_f_s16 | ../tsrc - - 0.500 | ../fsk_demod --cs16 -b -10000 -u 10000 -s --stats=5 2 48000 4800 - - 2>stats.txt |", + # Decode using rs41ecc + 'decode': "../lms6Xmod --json --softin -i 2>/dev/null", + # Count the number of telemetry lines. + "post_process" : "| grep frame | wc -l", + 'files' : "./generated/lms6-400*", + }, # iMet-54 Decoding 'imet54_fsk_demod_soft': { # Shift up to ~24 khz, and then pass into fsk_demod. @@ -273,7 +341,16 @@ "post_process" : "| grep frame | wc -l", 'files' : "./generated/imet54*", }, + 'imet54_fsk_demod_soft_centre': { + # Shift up to ~24 khz, and then pass into fsk_demod. + 'demod' : "| csdr convert_f_s16 | ../tsrc - - 0.500 | ../fsk_demod --cs16 -b -10000 -u 10000 -s --stats=5 2 48000 4800 - - 2>stats.txt |", + # Decode using rs41ecc + 'decode': "../imet54mod --ecc --json --softin -i 2>/dev/null", + # Count the number of telemetry lines. + "post_process" : "| grep frame | wc -l", + 'files' : "./generated/imet54*", + }, # MRZ Sonde decoding - Soft Input 'mrz_fsk_demod_soft': { 'demod': '| csdr shift_addition_cc 0.125000 2>/dev/null | csdr convert_f_s16 | ../tsrc - - 0.50| ../fsk_demod --cs16 -s -b 1250 -u 23750 --stats=5 2 48000 2400 - - 2>stats.txt |', @@ -281,6 +358,30 @@ "post_process" : " | grep -F [OK] | wc -l", # ECC 'files' : "./generated/mrz*.bin" }, + 'mrz_fsk_demod_soft_centre': { + 'demod': '| csdr convert_f_s16 | ../tsrc - - 0.500 | ../fsk_demod --cs16 -s -b -10000 -u 10000 --stats=5 2 48000 2400 - - 2>stats.txt |', + 'decode': '../mp3h1mod -vv --softin --json --auto 2>/dev/null', + "post_process" : " | grep -F [OK] | wc -l", # ECC + 'files' : "./generated/mrz*.bin" + }, + 'imet4_iq': { + 'demod': '| csdr convert_f_s16 | ../tsrc - - 0.50|', + 'decode': '../imet4iq --iq 0.0 --lpIQ --dc - 48000 16 --json 2> /dev/null', + "post_process" : "| grep -F [OK] | wc -l", # ECC + 'files' : "./generated/imet4*.bin" + }, + 'mts01_fsk_demod_soft_centre': { + # cat ./generated/dfm09_96k_float_15.0dB.bin | csdr shift_addition_cc 0.25000 2>/dev/null | csdr convert_f_s16 | + #./tsrc - - 1.041666 | ../fsk_demod --cs16 -b 1 -u 45000 2 100000 2500 - - 2>/dev/null | + #python ./bit_to_samples.py 50000 2500 | sox -t raw -r 50k -e unsigned-integer -b 8 -c 1 - -r 50000 -b 8 -t wav - 2>/dev/null| + #../dfm09ecc -vv --json --dist --auto + + 'demod': '| csdr convert_f_s16 | ../tsrc - - 0.500|', # ../fsk_demod --cs16 -b -10000 -u 10000 -s --stats=5 2 48000 1200 - - 2>stats.txt |',#' python ./bit_to_samples.py 50000 2500 | sox -t raw -r 50k -e unsigned-integer -b 8 -c 1 - -r 50000 -b 8 -t wav - 2>/dev/null| ', + 'decode': '../mts01mod --json --IQ 0.0 --lpIQ --dc - 48000 16 2>/dev/null', + "post_process" : " | grep frame | wc -l", # ECC + #"post_process" : "| grep -o '\[OK\]' | wc -l", # No ECC + 'files' : "./generated/mts01*.bin" + }, } @@ -615,7 +716,7 @@ processing_type['dft_detect_iq'] = { 'demod': _demod_command, - 'decode': "../dft_detect -t 5 --iq --bw 32 --dc - 48000 16 2>/dev/null", + 'decode': "../dft_detect -t 5 --iq --bw 20 --dc - 48000 16 2>/dev/null", # Grep out the line containing the detected sonde type. "post_process" : " | grep \:", 'files' : "./generated/*.bin" @@ -709,7 +810,7 @@ def run_analysis(mode, file_mask=None, shift=0.0, verbose=False, log_output = No _runtime = time.time() - _start - _result = "%s, %s, %.1f" % (os.path.basename(_file), _output.strip(), _runtime) + _result = "%s, %s, %.3f" % (os.path.basename(_file), _output.strip(), _runtime) print(_result) if log_output is not None: @@ -726,7 +827,7 @@ def run_analysis(mode, file_mask=None, shift=0.0, verbose=False, log_output = No if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("-m", "--mode", type=str, default="rs41_fsk_demod", help="Operation mode.") + parser.add_argument("-m", "--mode", type=str, default="rs41_fsk_demod_soft", help="Operation mode.") parser.add_argument("-f", "--files", type=str, default=None, help="Glob-path to files to run over.") parser.add_argument("-v", "--verbose", action='store_true', default=False, help="Show additional debug info.") parser.add_argument("-d", "--dry-run", action='store_true', default=False, help="Show additional debug info.") @@ -743,7 +844,8 @@ def run_analysis(mode, file_mask=None, shift=0.0, verbose=False, log_output = No sys.exit(1) - batch_modes = ['dfm_fsk_demod_soft', 'rs41_fsk_demod_soft', 'm10_fsk_demod_soft', 'rs92_fsk_demod_soft', 'rs92ngp_fsk_demod_soft', 'lms6-400_fsk_demod_soft', 'imet4_rtlfm', 'mrz_fsk_demod_soft', 'imet54_fsk_demod_soft'] + #batch_modes = ['dfm_fsk_demod_soft', 'rs41_fsk_demod_soft', 'm10_fsk_demod_soft', 'rs92_fsk_demod_soft', 'rs92ngp_fsk_demod_soft', 'lms6-400_fsk_demod_soft', 'imet4_rtlfm', 'mrz_fsk_demod_soft', 'imet54_fsk_demod_soft'] + batch_modes = ['dfm_fsk_demod_soft_centre', 'rs41_fsk_demod_soft_centre', 'm10_fsk_demod_soft_centre', 'rs92_fsk_demod_soft_centre', 'rs92ngp_fsk_demod_soft_centre', 'lms6-400_fsk_demod_soft_centre', 'imet4_iq', 'mrz_fsk_demod_soft_centre', 'imet54_fsk_demod_soft_centre', 'm20_fsk_demod_soft_centre'] if args.batch: for _mode in batch_modes: diff --git a/auto_rx/utils/listener_nmea_crlf.py b/auto_rx/utils/listener_nmea_crlf.py index 7438ab9c..7284f6f1 100644 --- a/auto_rx/utils/listener_nmea_crlf.py +++ b/auto_rx/utils/listener_nmea_crlf.py @@ -13,12 +13,9 @@ from threading import Thread from dateutil.parser import parse from datetime import datetime, timedelta +from io import StringIO import time -try: - from StringIO import StringIO ## for Python 2 -except ImportError: - from io import StringIO ## for Python 3 MAX_JSON_LEN = 32768 diff --git a/auto_rx/utils/log_to_kml.py b/auto_rx/utils/log_to_kml.py index 7c4df18e..d5003796 100644 --- a/auto_rx/utils/log_to_kml.py +++ b/auto_rx/utils/log_to_kml.py @@ -17,7 +17,7 @@ import glob import os import fastkml -from dateutil.parser import * +from dateutil.parser import parse from shapely.geometry import Point, LineString def read_telemetry_csv(filename, diff --git a/auto_rx/utils/receiver_stats.py b/auto_rx/utils/receiver_stats.py index cedc4dfd..cc992f91 100644 --- a/auto_rx/utils/receiver_stats.py +++ b/auto_rx/utils/receiver_stats.py @@ -77,8 +77,8 @@ def position_info(listener, balloon): eb = sin(angle_at_centre) * tb elevation = atan2(ea, eb) - # Use pythagorean theorem to find unknown side. - distance = sqrt((ea ** 2) + (eb ** 2)) + # Use cosine rule to find unknown side. + distance = sqrt((ta ** 2) + (tb ** 2) - 2 * tb * ta * cos(angle_at_centre)) # Give a bearing in range 0 <= b < 2pi if bearing < 0: diff --git a/demod/mod/Makefile b/demod/mod/Makefile index bfe2e96c..1cddb31c 100644 --- a/demod/mod/Makefile +++ b/demod/mod/Makefile @@ -1,14 +1,6 @@ -# Makefile for demod programs - -# Auto_RX version number - needs to match the contents of autorx/__init__.py -# This can probably be done automatically. -#AUTO_RX_VERSION="\"1.4.1-beta8\"" -AUTO_RX_VERSION := $(shell PYTHONPATH=../../auto_rx python -m autorx.version) - -CFLAGS = -O3 -Wall -Wno-unused-variable -DVER_JSN_STR=\"$(AUTO_RX_VERSION)\" LDLIBS = -lm -PROGRAMS := rs41mod dfm09mod rs92mod lms6mod lms6Xmod meisei100mod m10mod m20mod imet54mod +PROGRAMS := rs41mod dfm09mod rs92mod lms6mod lms6Xmod meisei100mod m10mod m20mod imet54mod mp3h1mod mts01mod iq_dec all: $(PROGRAMS) @@ -32,5 +24,15 @@ imet54mod: imet54mod.o demod_mod.o mp3h1mod: mp3h1mod.o demod_mod.o +mts01mod: mts01mod.o demod_mod.o + +bch_ecc_mod.o: bch_ecc_mod.h + +demod_mod.o: CFLAGS += -Ofast +demod_mod.o: demod_mod.h + +iq_dec: CFLAGS += -Ofast +iq_dec: iq_dec.o + clean: - $(RM) $(PROGRAMS) $(PROGRAMS:=.o) demod_mod.o bch_ecc_mod.o + $(RM) $(PROGRAMS) $(PROGRAMS:=.o) demod_mod.o bch_ecc_mod.o \ No newline at end of file diff --git a/demod/mod/demod_mod.c b/demod/mod/demod_mod.c index d6d45336..38d8f5c7 100644 --- a/demod/mod/demod_mod.c +++ b/demod/mod/demod_mod.c @@ -125,14 +125,14 @@ static int dft_window(dft_t *dft, int w) { dft->win[n] = 1.0; break; case 1: // Hann - dft->win[n] = 0.5 * ( 1.0 - cos(2*M_PI*n/(float)(dft->N2-1)) ); + dft->win[n] = 0.5 * ( 1.0 - cos(_2PI*n/(float)(dft->N2-1)) ); break ; case 2: // Hamming - dft->win[n] = 25/46.0 - (1.0 - 25/46.0)*cos(2*M_PI*n / (float)(dft->N2-1)); + dft->win[n] = 25/46.0 - (1.0 - 25/46.0)*cos(_2PI*n / (float)(dft->N2-1)); break ; case 3: // Blackmann dft->win[n] = 7938/18608.0 - - 9240/18608.0*cos(2*M_PI*n / (float)(dft->N2-1)) + - 9240/18608.0*cos(_2PI*n / (float)(dft->N2-1)) + 1430/18608.0*cos(4*M_PI*n / (float)(dft->N2-1)); break ; } @@ -310,7 +310,7 @@ static int findstr(char *buff, char *str, int pos) { return i; } -float read_wav_header(pcm_t *pcm, FILE *fp) { +int read_wav_header(pcm_t *pcm, FILE *fp) { char txt[4+1] = "\0\0\0\0"; unsigned char dat[4]; int byte, p=0; @@ -465,7 +465,7 @@ static int f32read_cblock(dsp_t *dsp) { int n; int len; float x, y; - ui8_t s[4*2*dsp->decM]; //uin8,int16,flot32 + ui8_t s[4*2*dsp->decM]; //uin8,int16,float32 ui8_t *u = (ui8_t*)s; short *b = (short*)s; float *f = (float*)s; @@ -567,7 +567,7 @@ static int lowpass_init(float f, int taps, float **pws) { ws = (float*)calloc( 2*taps+1, sizeof(float)); if (ws == NULL) return -1; for (n = 0; n < taps; n++) { - w[n] = 7938/18608.0 - 9240/18608.0*cos(2*M_PI*n/(taps-1)) + 1430/18608.0*cos(4*M_PI*n/(taps-1)); // Blackmann + w[n] = 7938/18608.0 - 9240/18608.0*cos(_2PI*n/(taps-1)) + 1430/18608.0*cos(4*M_PI*n/(taps-1)); // Blackmann h[n] = 2*f*sinc(2*f*(n-(taps-1)/2)); ws[n] = w[n]*h[n]; norm += ws[n]; // 1-norm @@ -600,7 +600,7 @@ static int lowpass_update(float f, int taps, float *ws) { w = (double*)calloc( taps+1, sizeof(double)); if (w == NULL) return -1; for (n = 0; n < taps; n++) { - w[n] = 7938/18608.0 - 9240/18608.0*cos(2*M_PI*n/(taps-1)) + 1430/18608.0*cos(4*M_PI*n/(taps-1)); // Blackmann + w[n] = 7938/18608.0 - 9240/18608.0*cos(_2PI*n/(taps-1)) + 1430/18608.0*cos(4*M_PI*n/(taps-1)); // Blackmann h[n] = 2*f*sinc(2*f*(n-(taps-1)/2)); ws[n] = w[n]*h[n]; norm += ws[n]; // 1-norm @@ -618,40 +618,104 @@ static int lowpass_update(float f, int taps, float *ws) { } static float complex lowpass0(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { - ui32_t n; + ui32_t n; // sample: oldest_sample double complex w = 0; for (n = 0; n < taps; n++) { - w += buffer[(sample+n+1)%taps]*ws[taps-1-n]; + w += buffer[(sample+n)%taps]*ws[taps-1-n]; } return (float complex)w; } -static float complex lowpass(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { - ui32_t n; - ui32_t s = sample % taps; +static float complex lowpass1a(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { double complex w = 0; + ui32_t n; + ui32_t S = taps-1 + (sample % taps); + for (n = 0; n < taps; n++) { + w += buffer[n]*ws[S-n]; // ws[taps+s-n] = ws[(taps+sample-n)%taps] + } + return (float complex)w; +// symmetry: ws[n] == ws[taps-1-n] +} +//static __attribute__((optimize("-ffast-math"))) float complex lowpass() +static float complex lowpass(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { + float complex w = 0; + int n; // -Ofast + int S = taps - (sample % taps); for (n = 0; n < taps; n++) { - w += buffer[n]*ws[taps+s-n]; // ws[taps+s-n] = ws[(taps+sample-n)%taps] + w += buffer[n]*ws[S+n]; // ws[taps+s-n] = ws[(taps+sample-n)%taps] + } + return w; +// symmetry: ws[n] == ws[taps-1-n] +} +static float complex lowpass2(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { + float complex w = 0; + int n; // -Ofast + int s = sample % taps; + int S1 = s; + int S1N = S1-taps; + int n0 = taps-s; + for (n = 0; n < n0; n++) { + w += buffer[S1+n]*ws[n]; + } + for (n = n0; n < taps; n++) { + w += buffer[S1N+n]*ws[n]; + } + return w; +// symmetry: ws[n] == ws[taps-1-n] +} +static float complex lowpass0_sym(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { + ui32_t n; + double complex w = buffer[(sample+(taps-1)/2) % taps]*ws[(taps-1)/2]; // (N+1)/2 = (N-1)/2 + 1 + for (n = 0; n < (taps-1)/2; n++) { + w += (buffer[(sample+n)%taps]+buffer[(sample+taps-n-1)%taps])*ws[n]; } return (float complex)w; +} +static float complex lowpass2_sym(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { + float complex w = 0; + int n; + int s = sample % taps; // lpIQ + int SW = (taps-1)/2; + int B1 = s + SW; + int n1 = SW - s; + int n0 = 0; + + if (s > SW) { + B1 -= taps; + n1 = -n1 - 1; + n0 = B1+n1+1; + } + + w = buffer[B1]*ws[SW]; + + for (n = 1; n < n1+1; n++) { + w += (buffer[B1 + n] + buffer[B1 - n]) * ws[SW+n]; + } + + for (n = 0; n < SW-n1; n++) { + w += (buffer[s + n] + buffer[s-1 - n]) * ws[SW+SW-n]; + } + + return w; // symmetry: ws[n] == ws[taps-1-n] } + static float re_lowpass0(float buffer[], ui32_t sample, ui32_t taps, float *ws) { ui32_t n; double w = 0; for (n = 0; n < taps; n++) { - w += buffer[(sample+n+1)%taps]*ws[taps-1-n]; + w += buffer[(sample+n)%taps]*ws[taps-1-n]; } return (float)w; } static float re_lowpass(float buffer[], ui32_t sample, ui32_t taps, float *ws) { - ui32_t n; - ui32_t s = sample % taps; - double w = 0; + float w = 0; + int n; + int S = taps - (sample % taps); for (n = 0; n < taps; n++) { - w += buffer[n]*ws[taps+s-n]; // ws[taps+s-n] = ws[(taps+sample-n)%taps] + w += buffer[n]*ws[S+n]; // ws[taps+s-n] = ws[(taps+sample-n)%taps] } - return (float)w; + return w; } @@ -668,40 +732,39 @@ int f32buf_sample(dsp_t *dsp, int inv) { if (dsp->opt_iq) { if (dsp->opt_iq == 5) { - ui32_t s_reset = dsp->dectaps*dsp->lut_len; int j; if ( f32read_cblock(dsp) < dsp->decM ) return EOF; for (j = 0; j < dsp->decM; j++) { if (dsp->opt_nolut) { double _s_base = (double)(dsp->sample_in*dsp->decM+j); // dsp->sample_dec double f0 = dsp->xlt_fq*_s_base - dsp->Df*_s_base/(double)dsp->sr_base; - z = dsp->decMbuf[j] * cexp(f0*2*M_PI*I); + z = dsp->decMbuf[j] * cexp(f0*_2PI*I); } else { - z = dsp->decMbuf[j] * dsp->ex[dsp->sample_dec % dsp->lut_len]; + z = dsp->decMbuf[j] * dsp->ex[dsp->sample_decM]; } + dsp->sample_decM += 1; if (dsp->sample_decM >= dsp->lut_len) dsp->sample_decM = 0; - dsp->decXbuffer[dsp->sample_dec % dsp->dectaps] = z; - dsp->sample_dec += 1; - if (dsp->sample_dec == s_reset) dsp->sample_dec = 0; + dsp->decXbuffer[dsp->sample_decX] = z; + dsp->sample_decX += 1; if (dsp->sample_decX >= dsp->dectaps) dsp->sample_decX = 0; } if (dsp->decM > 1) { - z = lowpass(dsp->decXbuffer, dsp->sample_dec, dsp->dectaps, ws_dec); + z = lowpass(dsp->decXbuffer, dsp->sample_decX, dsp->dectaps, ws_dec); // oldest sample: dsp->sample_decX } } else if ( f32read_csample(dsp, &z) == EOF ) return EOF; if (dsp->opt_dc && !dsp->opt_nolut) { - z *= cexp(-t*2*M_PI*dsp->Df*I); + z *= cexp(-t*_2PI*dsp->Df*I); } // IF-lowpass if (dsp->opt_lp & LP_IQ) { dsp->lpIQ_buf[dsp->sample_in % dsp->lpIQtaps] = z; - z = lowpass(dsp->lpIQ_buf, dsp->sample_in, dsp->lpIQtaps, dsp->ws_lpIQ); + z = lowpass(dsp->lpIQ_buf, dsp->sample_in+1, dsp->lpIQtaps, dsp->ws_lpIQ); } @@ -709,7 +772,7 @@ int f32buf_sample(dsp_t *dsp, int inv) { w = z * conj(z0); s_fm = gain * carg(w)/M_PI; - dsp->rot_iqbuf[dsp->sample_in % dsp->N_IQBUF] = z; + dsp->rot_iqbuf[dsp->sample_in % dsp->N_IQBUF] = z; // sample_in & (N-1) , N = (1<opt_iq >= 2) @@ -717,8 +780,8 @@ int f32buf_sample(dsp_t *dsp, int inv) { if (dsp->opt_iq >= 2) { double xbit = 0.0; //float complex xi = cexp(+I*M_PI*dsp->h/dsp->sps); - double f1 = -dsp->h*dsp->sr/(2.0*dsp->sps); - double f2 = -f1; + //double f1 = -dsp->h*dsp->sr/(2.0*dsp->sps); + //double f2 = -f1; float complex X0 = 0; float complex X = 0; @@ -730,13 +793,13 @@ int f32buf_sample(dsp_t *dsp, int inv) { z0 = dsp->rot_iqbuf[(dsp->sample_in-n + dsp->N_IQBUF) % dsp->N_IQBUF]; // f1 - X0 = z0 * cexp(-tn*2*M_PI*f1*I); // alt - X = z * cexp(-t *2*M_PI*f1*I); // neu + X0 = z0 * cexp(-tn*dsp->iw1); // alt + X = z * cexp(-t *dsp->iw1); // neu dsp->F1sum += X - X0; // f2 - X0 = z0 * cexp(-tn*2*M_PI*f2*I); // alt - X = z * cexp(-t *2*M_PI*f2*I); // neu + X0 = z0 * cexp(-tn*dsp->iw2); // alt + X = z * cexp(-t *dsp->iw2); // neu dsp->F2sum += X - X0; xbit = cabs(dsp->F2sum) - cabs(dsp->F1sum); @@ -746,8 +809,8 @@ int f32buf_sample(dsp_t *dsp, int inv) { else if (0 && dsp->opt_iq == 4) { double xbit = 0.0; //float complex xi = cexp(+I*M_PI*dsp->h/dsp->sps); - double f1 = -dsp->h*dsp->sr/(2*dsp->sps); - double f2 = -f1; + //double f1 = -dsp->h*dsp->sr/(2*dsp->sps); + //double f2 = -f1; float complex X1 = 0; float complex X2 = 0; @@ -758,8 +821,8 @@ int f32buf_sample(dsp_t *dsp, int inv) { n--; t = -n / (double)dsp->sr; z = dsp->rot_iqbuf[(dsp->sample_in - n + dsp->N_IQBUF) % dsp->N_IQBUF]; // +1 - X1 += z*cexp(-t*2*M_PI*f1*I); - X2 += z*cexp(-t*2*M_PI*f2*I); + X1 += z*cexp(-t*dsp->iw1); + X2 += z*cexp(-t*dsp->iw2); } xbit = cabs(X2) - cabs(X1); @@ -779,14 +842,14 @@ int f32buf_sample(dsp_t *dsp, int inv) { // FM-lowpass if (dsp->opt_lp & LP_FM) { dsp->lpFM_buf[dsp->sample_in % dsp->lpFMtaps] = s_fm; - s_fm = re_lowpass(dsp->lpFM_buf, dsp->sample_in, dsp->lpFMtaps, dsp->ws_lpFM); + s_fm = re_lowpass(dsp->lpFM_buf, dsp->sample_in+1, dsp->lpFMtaps, dsp->ws_lpFM); if (dsp->opt_iq < 2) s = s_fm; } dsp->fm_buffer[dsp->sample_in % dsp->M] = s_fm; if (inv) s = -s; - dsp->bufs[dsp->sample_in % dsp->M] = s; + dsp->bufs[dsp->sample_in % dsp->M] = s; // sample_in & (M-1) , M = (1<bufs[(dsp->sample_in ) % dsp->M]; @@ -1145,7 +1208,7 @@ int init_buffers(dsp_t *dsp) { int i, pos; float b0, b1, b2, b, t; float normMatch; - double sigma = sqrt(log(2)) / (2*M_PI*dsp->BT); + double sigma = sqrt(log(2)) / (_2PI*dsp->BT); int p2 = 1; int K, L, M; @@ -1226,7 +1289,7 @@ int init_buffers(dsp_t *dsp) { if (dsp->ex == NULL) return -1; for (n = 0; n < dsp->lut_len; n++) { t = f0*(double)n; - dsp->ex[n] = cexp(t*2*M_PI*I); + dsp->ex[n] = cexp(t*_2PI*I); } } @@ -1313,7 +1376,7 @@ int init_buffers(dsp_t *dsp) { dsp->K = K; dsp->L = L; - dsp->M = M; + dsp->M = M; // = (1<Nvar = L; // wenn Nvar fuer xnorm, dann Nvar=rshd.L @@ -1390,13 +1453,21 @@ int init_buffers(dsp_t *dsp) { { if (dsp->nch < 2) return -1; - dsp->N_IQBUF = dsp->DFT.N; + dsp->N_IQBUF = dsp->DFT.N; // = (1<rot_iqbuf = calloc(dsp->N_IQBUF+1, sizeof(float complex)); if (dsp->rot_iqbuf == NULL) return -1; } dsp->fm_buffer = (float *)calloc( M+1, sizeof(float)); if (dsp->fm_buffer == NULL) return -1; // dsp->bufs[] + if (dsp->opt_iq) + { + double f1 = -dsp->h*dsp->sr/(2.0*dsp->sps); + double f2 = -f1; + dsp->iw1 = _2PI*I*f1; + dsp->iw2 = _2PI*I*f2; + } + return K; } @@ -1488,8 +1559,8 @@ int find_header(dsp_t *dsp, float thres, int hdmax, int bitofs, int opt_dc) { double diffDf = dsp->dDf*0.6; //0.4 if (1 && dsp->opt_iq >= 2) { // update rot_iqbuf, F1sum, F2sum - double f1 = -dsp->h*dsp->sr/(2*dsp->sps); - double f2 = -f1; + //double f1 = -dsp->h*dsp->sr/(2*dsp->sps); + //double f2 = -f1; float complex X1 = 0; float complex X2 = 0; float complex _z = 0; @@ -1498,12 +1569,12 @@ int find_header(dsp_t *dsp, float thres, int hdmax, int bitofs, int opt_dc) { { // update rot_iqbuf double _tn = (dsp->sample_in - _n) / (double)dsp->sr; - dsp->rot_iqbuf[(dsp->sample_in - _n + dsp->N_IQBUF) % dsp->N_IQBUF] *= cexp(-_tn*2*M_PI*diffDf*I); + dsp->rot_iqbuf[(dsp->sample_in - _n + dsp->N_IQBUF) % dsp->N_IQBUF] *= cexp(-_tn*_2PI*diffDf*I); // //update/reset F1sum, F2sum _z = dsp->rot_iqbuf[(dsp->sample_in - _n + dsp->N_IQBUF) % dsp->N_IQBUF]; - X1 += _z*cexp(-_tn*2*M_PI*f1*I); - X2 += _z*cexp(-_tn*2*M_PI*f2*I); + X1 += _z*cexp(-_tn*dsp->iw1); + X2 += _z*cexp(-_tn*dsp->iw2); _n--; } dsp->F1sum = X1; @@ -1549,7 +1620,7 @@ int find_header(dsp_t *dsp, float thres, int hdmax, int bitofs, int opt_dc) { #else // external FSK demod: read float32 soft symbols -float read_wav_header(pcm_t *pcm, FILE *fp) {} +int read_wav_header(pcm_t *pcm, FILE *fp) {} int f32buf_sample(dsp_t *dsp, int inv) {} int read_slbit(dsp_t *dsp, int *bit, int inv, int ofs, int pos, float l, int spike) {} int read_softbit(dsp_t *dsp, hsbit_t *shb, int inv, int ofs, int pos, float l, int spike) {} diff --git a/demod/mod/demod_mod.h b/demod/mod/demod_mod.h index 8225156e..aa2cc731 100644 --- a/demod/mod/demod_mod.h +++ b/demod/mod/demod_mod.h @@ -6,6 +6,8 @@ #ifndef M_PI #define M_PI (3.1415926535897932384626433832795) #endif +#define _2PI (6.2831853071795864769252867665590) + #define LP_IQ 1 #define LP_FM 2 @@ -17,6 +19,7 @@ typedef unsigned char ui8_t; typedef unsigned short ui16_t; typedef unsigned int ui32_t; +typedef unsigned long long ui64_t; typedef char i8_t; typedef short i16_t; typedef int i32_t; @@ -82,6 +85,10 @@ typedef struct { float complex *rot_iqbuf; float complex F1sum; float complex F2sum; + // + double complex iw1; + double complex iw2; + // char *rawbits; @@ -116,8 +123,9 @@ typedef struct { int decM; ui32_t sr_base; ui32_t dectaps; - ui32_t sample_dec; + ui32_t sample_decX; ui32_t lut_len; + ui32_t sample_decM; float complex *decXbuffer; float complex *decMbuf; float complex *ex; // exp_lut @@ -168,7 +176,7 @@ typedef struct { } hdb_t; -float read_wav_header(pcm_t *, FILE *); +int read_wav_header(pcm_t *, FILE *); int f32buf_sample(dsp_t *, int); int read_slbit(dsp_t *, int*, int, int, int, float, int); int read_softbit(dsp_t *, hsbit_t *, int, int, int, float, int); diff --git a/demod/mod/dfm09mod.c b/demod/mod/dfm09mod.c index 5054c59c..ea5e52e9 100644 --- a/demod/mod/dfm09mod.c +++ b/demod/mod/dfm09mod.c @@ -38,6 +38,7 @@ enum dfmtyp_keys_t { UNDEF, UNKNW, DFM06, + DFM06P, PS15, DFM09, DFM09P, @@ -49,6 +50,7 @@ static char *DFM_types[] = { [UNDEF] = "", [UNKNW] = "DFMxX", [DFM06] = "DFM06", + [DFM06P] = "DFM06P", [PS15] = "PS15", [DFM09] = "DFM09", [DFM09P] = "DFM09P", @@ -63,6 +65,7 @@ typedef struct { i8_t ecc; // Hamming ECC i8_t sat; // GPS sat data i8_t ptu; // PTU: temperature + i8_t aux; // decode xdata i8_t inv; i8_t aut; i8_t jsn; // JSON output (auto_rx) @@ -87,28 +90,35 @@ typedef struct { typedef struct { ui32_t prn; // SVs used (PRN) float dMSL; // Alt_MSL - Alt_ellipsoid = -N = - geoid_height = ellipsoid - geoid - ui8_t nSV; // numSVs used + ui8_t nSV; // numSVs used + ui8_t nPRN; // numSVs in in PRN list } gpsdat_t; #define BITFRAME_LEN 280 +#define XDATA_LEN 26 // (2+4*6) typedef struct { int frnr; int sonde_typ; ui32_t SN6; ui32_t SN; + char SN_out[10]; int week; int tow; ui32_t sec_gps; int jahr; int monat; int tag; int std; int min; float sek; double lat; double lon; double alt; double dir; double horiV; double vertV; - //float T; + double lat2; double lon2; double alt2; + double dir2; double horiV2; double vertV2; + float T; float Rf; float _frmcnt; float meas24[9]; - float status[2]; + float status[3]; ui32_t val24[9]; ui8_t cfgchk24[9]; + i8_t posmode; + ui8_t xdata[XDATA_LEN]; // 2+4*6 int cfgchk; char sonde_id[16]; // "ID__:xxxxxxxx\0\0" hsbit_t frame[BITFRAME_LEN+4]; // char frame_bits[BITFRAME_LEN+4]; @@ -117,7 +127,7 @@ typedef struct { pcksts_t pck[9]; option_t option; int ptu_out; - char sensortyp0xC; + char sensortyp; char *dfmtyp; int jsn_freq; // freq/kHz (SDR) gpsdat_t gps; @@ -341,6 +351,7 @@ static int dat_out(gpx_t *gpx, ui8_t *dat_bits, int ec) { int frnr = 0; int msek = 0; int lat = 0, lon = 0, alt = 0; + int mode = 2; int nib; int dvv; // signed/unsigned 16bit @@ -369,47 +380,136 @@ static int dat_out(gpx_t *gpx, ui8_t *dat_bits, int ec) { if (fr_id == 0) { //start = 0x1000; + mode = bits2val(dat_bits+16, 8); + if (mode > 1 && mode < 5) gpx->posmode = mode; + else gpx->posmode = -1;//2 frnr = bits2val(dat_bits+24, 8); gpx->frnr = frnr; } - if (fr_id == 1) { - // 00..31: GPS-Sats in solution (bitmap) - gpx->gps.prn = bits2val(dat_bits, 32); // SV/PRN used - msek = bits2val(dat_bits+32, 16); // UTC (= GPS - 18sec ab 1.1.2017) - gpx->sek = msek/1000.0; - } + if (gpx->posmode <= 2) + { + if (fr_id == 0) { + } + if (fr_id == 1) { + // 00..31: GPS-Sats in solution (bitmap) + gpx->gps.prn = bits2val(dat_bits, 32); // SV/PRN bitmask + gpx->gps.nPRN = 0; for (int j = 0; j < 32; j++) { if ((gpx->gps.prn >> j)&1) gpx->gps.nPRN += 1; } + msek = bits2val(dat_bits+32, 16); // UTC (= GPS - 18sec ab 1.1.2017) + gpx->sek = msek/1000.0; + } - if (fr_id == 2) { - lat = bits2val(dat_bits, 32); - gpx->lat = lat/1e7; - dvv = (short)bits2val(dat_bits+32, 16); // (short)? zusammen mit dir sollte unsigned sein - gpx->horiV = dvv/1e2; - } + if (fr_id == 2) { + lat = bits2val(dat_bits, 32); + gpx->lat = lat/1e7; + dvv = (short)bits2val(dat_bits+32, 16); // (short)? zusammen mit dir sollte unsigned sein + gpx->horiV = dvv/1e2; + } - if (fr_id == 3) { - lon = bits2val(dat_bits, 32); - gpx->lon = lon/1e7; - dvv = bits2val(dat_bits+32, 16) & 0xFFFF; // unsigned - gpx->dir = dvv/1e2; - } + if (fr_id == 3) { + lon = bits2val(dat_bits, 32); + gpx->lon = lon/1e7; + dvv = bits2val(dat_bits+32, 16) & 0xFFFF; // unsigned + gpx->dir = dvv/1e2; + } - if (fr_id == 4) { - alt = bits2val(dat_bits, 32); - gpx->alt = alt/1e2; - dvv = (short)bits2val(dat_bits+32, 16); // signed - gpx->vertV = dvv/1e2; - } + if (fr_id == 4) { + alt = bits2val(dat_bits, 32); + gpx->alt = alt/1e2; // GPS/Ellipsoid + dvv = (short)bits2val(dat_bits+32, 16); // signed + gpx->vertV = dvv/1e2; + } + + if (fr_id == 5) { + short dMSL = bits2val(dat_bits, 16); + gpx->gps.dMSL = dMSL/1e2; + } - if (fr_id == 5) { - short dMSL = bits2val(dat_bits, 16); - gpx->gps.dMSL = dMSL/1e2; + if (fr_id == 6) { // sat data + } + + if (fr_id == 7) { // sat data + } } + else if (gpx->posmode == 3) // cf. dfm-ts20170801.c + { + if (fr_id == 0) { + msek = bits2val(dat_bits, 16); + gpx->sek = msek/1000.0; + dvv = (short)bits2val(dat_bits+32, 16); + gpx->horiV = dvv/1e2; + } + if (fr_id == 1) { + lat = bits2val(dat_bits, 32); + gpx->lat = lat/1e7; + dvv = bits2val(dat_bits+32, 16) & 0xFFFF; // unsigned + gpx->dir = dvv/1e2; + } + + if (fr_id == 2) { + lon = bits2val(dat_bits, 32); + gpx->lon = lon/1e7; + dvv = (short)bits2val(dat_bits+32, 16); // signed + gpx->vertV = dvv/1e2; + } + + if (fr_id == 3) { + alt = bits2val(dat_bits, 32); + gpx->alt = alt/1e2; // mode>2: alt/MSL + } + + if (fr_id == 5) { + lat = bits2val(dat_bits, 32); + gpx->lat2 = lat/1e7; + dvv = (short)bits2val(dat_bits+32, 16); // (short)? zusammen mit dir sollte unsigned sein + gpx->horiV2 = dvv/1e2; + } - if (fr_id == 6) { // sat data + if (fr_id == 6) { + lon = bits2val(dat_bits, 32); + gpx->lon2 = lon/1e7; + dvv = bits2val(dat_bits+32, 16) & 0xFFFF; // unsigned + gpx->dir2 = dvv/1e2; + } + + if (fr_id == 7) { + alt = bits2val(dat_bits, 32); + gpx->alt2 = alt/1e2; + dvv = (short)bits2val(dat_bits+32, 16); // signed + gpx->vertV2 = dvv/1e2; + } } + else if (gpx->posmode == 4) // XDATA: cf. DF9DQ https://github.com/einergehtnochrein/ra-firmware/tree/master/src/dfm + { + if (fr_id == 0) { + msek = bits2val(dat_bits, 16); + gpx->sek = msek/1000.0; + dvv = (short)bits2val(dat_bits+32, 16); + gpx->horiV = dvv/1e2; + } + if (fr_id == 1) { + lat = bits2val(dat_bits, 32); + gpx->lat = lat/1e7; + dvv = bits2val(dat_bits+32, 16) & 0xFFFF; // unsigned + gpx->dir = dvv/1e2; + } + + if (fr_id == 2) { + lon = bits2val(dat_bits, 32); + gpx->lon = lon/1e7; + dvv = (short)bits2val(dat_bits+32, 16); // signed + gpx->vertV = dvv/1e2; + } - if (fr_id == 7) { // sat data + if (fr_id == 3) { + alt = bits2val(dat_bits, 32); + gpx->alt = alt/1e2; // mode>2: alt/MSL + for (int j = 0; j < 2; j++) gpx->xdata[j] = bits2val(dat_bits+32+8*j, 8); + } + if (fr_id > 3 && fr_id < 8) { + int ofs = fr_id - 4; + for (int j = 0; j < 6; j++) gpx->xdata[2+6*ofs+j] = bits2val(dat_bits+8*j, 8); + } } if (fr_id == 8) { @@ -469,7 +569,8 @@ static float get_Temp(gpx_t *gpx) { // meas[0..4] float f = gpx->meas24[0], f1 = gpx->meas24[3], f2 = gpx->meas24[4]; - if (gpx->sensortyp0xC == 'P') { // 0xC: "P+" DFM-09P , "T-" DFM-17TU ; 0xD: "P-" DFM-17P ? + if (gpx->sensortyp == 'P') // 0xC: "P+" DFM-09P , "T-" DFM-17TU ; 0xD: "P-" DFM-17P ? + { // 0x8: "P-" (gpx->sonde_id[3] == '8') DFM-6/9P ? f = gpx->meas24[0+1]; f1 = gpx->meas24[3+2]; f2 = gpx->meas24[4+2]; @@ -501,7 +602,7 @@ static float get_Temp2(gpx_t *gpx) { // meas[0..4] float f = gpx->meas24[0], f1 = gpx->meas24[3], f2 = gpx->meas24[4]; - if (gpx->ptu_out >= 0xC && gpx->meas24[6] < 220e3) { + if (gpx->ptu_out >= 0xC && gpx->meas24[6] < 220e3 || gpx->sonde_id[3] == '8') { f = gpx->meas24[0+1]; f1 = gpx->meas24[3+2]; f2 = gpx->meas24[4+2]; @@ -557,7 +658,7 @@ static float get_Temp4(gpx_t *gpx) { // meas[0..4] // [ 30.0 , 0.82845 , 3.7 ] // [ 35.0 , 0.68991 , 3.6 ] // [ 40.0 , 0.57742 , 3.5 ] -// -> Steinhart–Hart coefficients (polyfit): +// -> Steinhart-Hart coefficients (polyfit): float p0 = 1.09698417e-03, p1 = 2.39564629e-04, p2 = 2.48821437e-06, @@ -566,7 +667,7 @@ static float get_Temp4(gpx_t *gpx) { // meas[0..4] float f = gpx->meas24[0], f1 = gpx->meas24[3], f2 = gpx->meas24[4]; - if (gpx->ptu_out >= 0xC && gpx->meas24[6] < 220e3) { + if (gpx->ptu_out >= 0xC && gpx->meas24[6] < 220e3 || gpx->sonde_id[3] == '8') { f = gpx->meas24[0+1]; f1 = gpx->meas24[3+2]; f2 = gpx->meas24[4+2]; @@ -588,6 +689,9 @@ static int reset_cfgchk(gpx_t *gpx) { for (j = 0; j < 9; j++) gpx->cfgchk24[j] = 0; gpx->cfgchk = 0; gpx->ptu_out = 0; + //gpx->gps.dMSL = 0; + *gpx->SN_out = '\0'; + gpx->T = -273.15f; return 0; } @@ -632,12 +736,15 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { sn2_ch = bits2val(conf_bits, 8); sn_ch = ((sn2_ch>>4) & 0xF); // sn_ch == config_id - if ( (gpx->snc.nul_ch & 0x58) == 0x58 ) { // 0x5A, 0x5B - SN6 = bits2val(conf_bits+4, 4*6); // DFM-06: Kanal 6 + if ( (gpx->snc.nul_ch & 0x58) == 0x58 ) { // 0x5A, 0x5B or 0x7A, 0x7B + SN6 = bits2val(conf_bits+4, 4*6); // DFM-06: Kanal 6 DFM-06P: Kanal 8 (DFM-6/9P) if (SN6 == gpx->SN6 && SN6 != 0) { // nur Nibble-Werte 0..9 - gpx->sonde_typ = SNbit | 6; + gpx->sonde_typ = SNbit | sn_ch; //6 or 8 gpx->ptu_out = 6; // <-> DFM-06 - sprintf(gpx->sonde_id, "IDx%1X:%6X", gpx->sonde_typ & 0xF, gpx->SN6); + // (test SN6 for BCD (binary coded decimal) ?) + //sprintf(gpx->sonde_id, "IDx%1X:%6X", gpx->sonde_typ & 0xF, gpx->SN6); + sprintf(gpx->sonde_id, "IDx%1X:%6X", sn_ch & 0xF, gpx->SN6); + sprintf(gpx->SN_out, "%6X", gpx->SN6); } else { // reset gpx->sonde_typ = 0; @@ -676,8 +783,9 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { if (sn_ch == 0xD) gpx->ptu_out = sn_ch; // <-> DFM-17P(?) // PS-15 ? (sn2_ch & 0xF) == 0x0 : gpx->ptu_out = 0 // <-> PS-15 - if ( (gpx->sonde_typ & 0xF) > 6) { + if ( gpx->SN6 == 0 || (gpx->sonde_typ & 0xF) >= 0xA) { sprintf(gpx->sonde_id, "IDx%1X:%6u", gpx->sonde_typ & 0xF, gpx->SN); + sprintf(gpx->SN_out, "%6u", gpx->SN); } } else { // reset @@ -693,7 +801,7 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { } - if (conf_id >= 0 && conf_id <= 8) { + if (conf_id >= 0 && conf_id <= 8 && ec == 0) { gpx->cfgchk24[conf_id] = 1; val = bits2val(conf_bits+4, 4*6); gpx->val24[conf_id] = val; @@ -711,20 +819,25 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { if (gpx->ptu_out >= 0x8) gpx->cfgchk *= gpx->cfgchk24[8]; } - gpx->sensortyp0xC = 'T'; + gpx->sensortyp = 'T'; gpx->Rf = 220e3; if (gpx->cfgchk) { // 0xC: "P+" DFM-09P , "T-" DFM-17TU ; 0xD: "P-" DFM-17P ? - if (gpx->ptu_out >= 0xD || (gpx->ptu_out >= 0xC && gpx->meas24[6] < 220e3)) { // gpx->meas24[6] < 220e3 <=> gpx->meas24[0] > 1e6 ? - gpx->sensortyp0xC = 'P'; // gpx->meas24[0] > 1e6 ? + if (gpx->ptu_out >= 0xD || (gpx->ptu_out >= 0xC && gpx->meas24[6] < 220e3)) { // gpx->meas24[6] < 220e3 <=> gpx->meas24[0] > 2e5 ? + 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 == 6 && (gpx->sonde_typ & 0xF) == 8) { + gpx->sensortyp = 'P'; } - if ( ((gpx->ptu_out == 0xB || gpx->ptu_out == 0xC) && gpx->sensortyp0xC == 'T') || gpx->ptu_out >= 0xD) gpx->Rf = 332e3; // DFM-17 ? // STM32-status: Bat, MCU-Temp if (gpx->ptu_out >= 0xA) { // DFM>=09(P) (STM32) ui8_t ofs = 0; - if (gpx->sensortyp0xC == 'P') ofs = 2; + if (gpx->sensortyp == 'P') ofs = 2; // + // c0xxxx0 inner 16 bit if (conf_id == 0x5+ofs) { // voltage val = bits2val(conf_bits+8, 4*4); gpx->status[0] = val/1000.0; @@ -733,10 +846,15 @@ 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 + val = bits2val(conf_bits+8, 4*4); + gpx->status[2] = val/1.0; // sec counter + } } else { gpx->status[0] = 0; gpx->status[1] = 0; + gpx->status[2] = 0; } } @@ -753,14 +871,15 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { case 0x6: gpx->dfmtyp = DFM_types[DFM06]; break; case 0x7: - case 0x8: gpx->dfmtyp = DFM_types[PS15]; + 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]; break; case 0xB: gpx->dfmtyp = DFM_types[DFM17]; break; - case 0xC: if (gpx->sensortyp0xC == 'P') gpx->dfmtyp = DFM_types[DFM09P]; - else /*'T'*/ gpx->dfmtyp = DFM_types[DFM17]; + case 0xC: if (gpx->sensortyp == 'P') gpx->dfmtyp = DFM_types[DFM09P]; + else /*'T'*/ gpx->dfmtyp = DFM_types[DFM17]; break; case 0xD: gpx->dfmtyp = DFM_types[DFM17P]; break; @@ -774,9 +893,11 @@ static int conf_out(gpx_t *gpx, ui8_t *conf_bits, int ec) { static void print_gpx(gpx_t *gpx) { int i, j; int contgps = 0; + int contaux = 0; int output = 0; int jsonout = 0; int start = 0; + int repeat_gps = 0; if (gpx->frnr > 0) start = 0x1000; @@ -793,7 +914,10 @@ static void print_gpx(gpx_t *gpx) { jsonout = output; - contgps = ((output & 0x11F) == 0x11F); // 0,1,2,3,8 + contgps = ((output & 0x11F) == 0x11F); // 0,1,2,3,4,8 (incl. xdata ID=0x01) + if (gpx->posmode == 4) { // xdata + contaux = ((output & 0xF8) == 0xF8); // 3,4,5,6,7 + } if (gpx->option.dst && !contgps) { output = 0; @@ -823,6 +947,18 @@ static void print_gpx(gpx_t *gpx) { gpx->prev_cntsec_diff = cntsec_diff; gpx->prev_manpol = gpx->option.inv; } + + gpx->T = -273.15f; + if (gpx->cfgchk && gpx->ptu_out) + { + gpx->T = get_Temp(gpx); + if (gpx->T < -270.0f && gpx->dfmtyp != DFM_types[UNDEF]) { + if ((gpx->sonde_typ & 0xF) == 0x8 || (gpx->sonde_typ & 0xF) == 0xC) + { + gpx->dfmtyp = DFM_types[UNKNW]; + } + } + } } if (output & 0xF000) { @@ -854,21 +990,22 @@ static void print_gpx(gpx_t *gpx) { if (gpx->cfgchk) { if (gpx->option.ptu && gpx->ptu_out) { - float t = get_Temp(gpx); - if (t > -270.0) { - printf(" T=%.1fC ", t); // 0xC:P+ DFM-09P , 0xC:T- DFM-17TU , 0xD:P- DFM-17P ? - if (gpx->option.vbs == 3) printf(" (0x%X:%c%c) ", gpx->sonde_typ & 0xF, gpx->sensortyp0xC, gpx->option.inv?'-':'+'); + //float t = get_Temp(gpx); + if (gpx->T > -270.0f) { + printf(" T=%.1fC ", gpx->T); // 0xC:P+ DFM-09P , 0xC:T- DFM-17TU , 0xD:P- DFM-17P ? + if (gpx->option.vbs == 3) printf(" (0x%X:%c%c) ", gpx->sonde_typ & 0xF, gpx->sensortyp, gpx->option.inv?'-':'+'); } if (gpx->option.dbg) { float t2 = get_Temp2(gpx); float t4 = get_Temp4(gpx); - if (t2 > -270.0) printf(" T2=%.1fC ", t2); - if (t4 > -270.0) printf(" T4=%.1fC ", t4); + if (t2 > -270.0f) printf(" T2=%.1fC ", t2); + if (t4 > -270.0f) printf(" T4=%.1fC ", t4); } } if (gpx->option.vbs == 3 && gpx->ptu_out >= 0xA) { if (gpx->status[0]> 0.0) printf(" U: %.2fV ", gpx->status[0]); if (gpx->status[1]> 0.0) printf(" Ti: %.1fK ", gpx->status[1]); + if (gpx->status[2]> 0.0) printf(" sec: %.0f ", gpx->status[2]); } } if (gpx->option.dbg) { @@ -877,7 +1014,7 @@ static void print_gpx(gpx_t *gpx) { printf(" f2:%.1f", gpx->meas24[2]); printf(" f3:%.1f", gpx->meas24[3]); printf(" f4:%.1f", gpx->meas24[4]); - if (gpx->ptu_out >= 0xA /*0xC*/) { + if (gpx->ptu_out >= 0xA /*0xC*/ || gpx->sonde_id[3] == '8') { printf(" f5:%.1f", gpx->meas24[5]); printf(" f6:%.1f", gpx->meas24[6]); } @@ -894,12 +1031,52 @@ static void print_gpx(gpx_t *gpx) { } printf("\n"); - if (gpx->option.sat) { + if (gpx->posmode > 2) { + //printf(" "); + //printf("(mode:%d) ", gpx->posmode); + if (gpx->posmode == 3 && repeat_gps) { + printf(" "); + printf("(mode:%d) ", gpx->posmode); + printf(" lat: %.5f ", gpx->lat2); + printf(" lon: %.5f ", gpx->lon2); + printf(" alt: %.1f ", gpx->alt2); + printf(" vH: %5.2f ", gpx->horiV2); + printf(" D: %5.1f ", gpx->dir2); + printf(" vV: %5.2f ", gpx->vertV2); + printf("\n"); + } + if (gpx->posmode == 4 && gpx->option.aux) { + printf(" "); + //printf("(mode:%d) ", gpx->posmode); + printf("XDATA:"); + for (j = 0; j < 2; j++) printf(" %02X", gpx->xdata[j]); + for (j = 2; j < XDATA_LEN; j++) printf(" %02X", gpx->xdata[j]); + printf("\n"); + if (gpx->xdata[0] == 0x01) + { // ECC Ozonesonde 01 .. .. (MSB) + ui8_t InstrumentNum = gpx->xdata[1]; + ui16_t Icell = gpx->xdata[2+1] | (gpx->xdata[2]<<8); // MSB + i16_t Tpump = gpx->xdata[4+1] | (gpx->xdata[4]<<8); // MSB + ui8_t Ipump = gpx->xdata[6]; + ui8_t Vbat = gpx->xdata[7]; + printf(" "); + printf(" ID=0x01 ECC "); + printf(" Icell:%.3fuA ", Icell/1000.0); + printf(" Tpump:%.2fC ", Tpump/100.0); + printf(" Ipump:%dmA ", Ipump); + printf(" Vbat:%.1fV ", Vbat/10.0); + printf("\n"); + } + } + } + + if (gpx->option.sat && gpx->posmode <= 2) { printf(" "); printf(" dMSL: %+.2f", gpx->gps.dMSL); // MSL = alt + gps.dMSL printf(" sats: %d", gpx->gps.nSV); printf(" ("); for (j = 0; j < 32; j++) { if ((gpx->gps.prn >> j)&1) printf(" %02d", j+1); } + printf(" nPRN: %d", gpx->gps.nPRN); printf(" )"); printf("\n"); } @@ -908,29 +1085,33 @@ static void print_gpx(gpx_t *gpx) { if (gpx->option.jsn && jsonout && gpx->sek < 60.0) { char *ver_jsn = NULL; - char json_sonde_id[] = "DFM-xxxxxxxx\0\0"; + char json_sonde_id[] = "DFM-xxxxxxxx\0\0"; // default (dfmXtyp==0) ui8_t dfmXtyp = (gpx->sonde_typ & 0xF); - switch ( dfmXtyp ) { - case 0: sprintf(json_sonde_id, "DFM-xxxxxxxx"); break; //json_sonde_id[0] = '\0'; - case 6: sprintf(json_sonde_id, "DFM-%6X", gpx->SN6); break; // DFM-06 - case 0xA: sprintf(json_sonde_id, "DFM-%6u", gpx->SN); break; // DFM-09 - // 0x7:PS-15?, 0xB:DFM-17? 0xC:DFM-09P?DFM-17TU? 0xD:DFM-17P? - default : sprintf(json_sonde_id, "DFM-%6u", gpx->SN); - } + if (*gpx->SN_out) strncpy(json_sonde_id+4, gpx->SN_out, 9); // JSON frame counter: gpx->sec_gps , seconds since GPS (ignoring leap seconds, DFM=UTC) + int _sats = gpx->gps.nSV; + if (_sats == 0 /*&& sonde_type == 6*/) _sats = gpx->gps.nPRN; // Print JSON blob // valid sonde_ID? printf("{ \"type\": \"%s\"", "DFM"); printf(", \"frame\": %u, ", gpx->sec_gps); // gpx->frnr printf("\"id\": \"%s\", \"datetime\": \"%04d-%02d-%02dT%02d:%02d:%06.3fZ\", \"lat\": %.5f, \"lon\": %.5f, \"alt\": %.5f, \"vel_h\": %.5f, \"heading\": %.5f, \"vel_v\": %.5f, \"sats\": %d", - json_sonde_id, gpx->jahr, gpx->monat, gpx->tag, gpx->std, gpx->min, gpx->sek, gpx->lat, gpx->lon, gpx->alt, gpx->horiV, gpx->dir, gpx->vertV, gpx->gps.nSV); + json_sonde_id, gpx->jahr, gpx->monat, gpx->tag, gpx->std, gpx->min, gpx->sek, gpx->lat, gpx->lon, gpx->alt, gpx->horiV, gpx->dir, gpx->vertV, _sats); if (gpx->ptu_out >= 0xA && gpx->status[0] > 0) { // DFM>=09(P): Battery (STM32) printf(", \"batt\": %.2f", gpx->status[0]); } if (gpx->ptu_out) { // get temperature - float t = get_Temp(gpx); // ecc-valid temperature? - if (t > -270.0) printf(", \"temp\": %.1f", t); + //float t = get_Temp(gpx); // ecc-valid temperature? + if (gpx->T > -270.0f) printf(", \"temp\": %.1f", gpx->T); + } + if (gpx->posmode == 4 && contaux && gpx->xdata[0]) { + char xdata_str[2*XDATA_LEN+1]; + memset(xdata_str, 0, 2*XDATA_LEN+1); + for (j = 0; j < XDATA_LEN; j++) { + sprintf(xdata_str+2*j, "%02X", gpx->xdata[j]); + } + printf(", \"aux\": \"%s\"", xdata_str); } //if (dfmXtyp > 0) printf(", \"subtype\": \"0x%1X\"", dfmXtyp); if (dfmXtyp > 0) { @@ -941,6 +1122,15 @@ static void print_gpx(gpx_t *gpx) { if (gpx->jsn_freq > 0) { printf(", \"freq\": %d", gpx->jsn_freq); } + + // Reference time/position + printf(", \"ref_datetime\": \"%s\"", "UTC" ); // {"GPS", "UTC"} GPS-UTC=leap_sec + if (gpx->posmode <= 2) { // mode 2 + printf(", \"ref_position\": \"%s\"", "GPS" ); // {"GPS", "MSL"} GPS=ellipsoid , MSL=geoid + printf(", \"diff_GPS_MSL\": %.2f", -gpx->gps.dMSL ); // MSL = GPS + gps.dMSL + } + else printf(", \"ref_position\": \"%s\"", "MSL" ); // mode 3,4 + #ifdef VER_JSN_STR ver_jsn = VER_JSN_STR; #endif @@ -1034,6 +1224,7 @@ static int print_frame(gpx_t *gpx) { if (gpx->option.ecc && gpx->option.vbs) { if (gpx->option.vbs > 1) printf(" (%1X,%1X,%1X) ", cnt_biterr(ret0), cnt_biterr(ret1), cnt_biterr(ret2)); printf(" (%d) ", cnt_biterr(ret0)+cnt_biterr(ret1)+cnt_biterr(ret2)); + if (gpx->option.vbs > 1) printf(" <%.1f>", gpx->_frmcnt); } printf("\n"); @@ -1075,7 +1266,6 @@ static int print_frame(gpx_t *gpx) { int main(int argc, char **argv) { - int option_verbose = 0; // ausfuehrliche Anzeige int option_raw = 0; // rohe Frames int option_inv = 0; // invertiert Signal int option_ecc = 0; @@ -1098,6 +1288,8 @@ int main(int argc, char **argv) { int rawhex = 0; int cfreq = -1; + float baudrate = -1; + FILE *fp = NULL; char *fpname = NULL; @@ -1153,10 +1345,11 @@ int main(int argc, char **argv) { return 0; } else if ( (strcmp(*argv, "-v") == 0) || (strcmp(*argv, "--verbose") == 0) ) { - option_verbose = 1; + gpx.option.vbs = 1; } - else if ( (strcmp(*argv, "-vv" ) == 0) ) { option_verbose = 2; } - else if ( (strcmp(*argv, "-vvv") == 0) ) { option_verbose = 3; } + else if (strcmp(*argv, "-vv" ) == 0) { gpx.option.vbs = 2; } + else if (strcmp(*argv, "-vvv") == 0) { gpx.option.vbs = 3; } + else if (strcmp(*argv, "-vx" ) == 0) { gpx.option.aux = 1; } else if ( (strcmp(*argv, "-r") == 0) || (strcmp(*argv, "--raw") == 0) ) { option_raw = 1; } @@ -1236,6 +1429,14 @@ int main(int argc, char **argv) { else if (strcmp(*argv, "--min") == 0) { option_min = 1; } + else if ( (strcmp(*argv, "--br") == 0) ) { + ++argv; + if (*argv) { + baudrate = atof(*argv); + if (baudrate < 2200 || baudrate > 2800) baudrate = BAUD_RATE; // default: 2500 + } + else return -1; + } else if (strcmp(*argv, "--dbg") == 0) { gpx.option.dbg = 1; } else if (strcmp(*argv, "--sat") == 0) { gpx.option.sat = 1; } else if (strcmp(*argv, "--rawhex") == 0) { rawhex = 1; } // raw hex input @@ -1303,7 +1504,6 @@ int main(int argc, char **argv) { for (k = 0; k < 9; k++) gpx.pck[k].ec = -1; // init ecc-status gpx.option.inv = option_inv; - gpx.option.vbs = option_verbose; gpx.option.raw = option_raw; gpx.option.ptu = option_ptu; gpx.option.ecc = option_ecc; @@ -1311,6 +1511,8 @@ int main(int argc, char **argv) { gpx.option.dst = option_dist; gpx.option.jsn = option_json; + if (gpx.option.aux && gpx.option.vbs < 1) gpx.option.vbs = 1; + if (cfreq > 0) gpx.jsn_freq = (cfreq+500)/1000; headerlen = strlen(dfm_rawheader); @@ -1381,6 +1583,11 @@ int main(int argc, char **argv) { fprintf(stderr, "note: sample rate low\n"); } + if (baudrate > 0) { + dsp.br = (float)baudrate; + dsp.sps = (float)dsp.sr/dsp.br; + fprintf(stderr, "sps corr: %.4f\n", dsp.sps); + } k = init_buffers(&dsp); if ( k < 0 ) { @@ -1526,7 +1733,7 @@ int main(int argc, char **argv) { int pos = 0; float _frmcnt = -1.0f; - memset(buffer_rawhex, BUFLEN+1, 0); + memset(buffer_rawhex, 0, BUFLEN+1); while ( (ch=fgetc(fp)) != EOF) { diff --git a/demod/mod/imet54mod.c b/demod/mod/imet54mod.c index cd38dc52..abfd361b 100644 --- a/demod/mod/imet54mod.c +++ b/demod/mod/imet54mod.c @@ -56,9 +56,11 @@ typedef struct { #define BITS (10) -#define FRAME_LEN (220) +#define STDFRMLEN (220) // 108 byte +#define FRAME_LEN (220) //(std=220, 108 byte) (full=440=2*std, 216 byte) #define BITFRAME_LEN (FRAME_LEN*BITS) - +#define FRMBYTE_STD (108) //(FRAME_LEN-FRAMESTART)/2 = 108 +// FRAME_FULL = 2*FRAME_STD = 216 ? typedef struct { int out; @@ -85,7 +87,7 @@ typedef struct { // shorter header correlation, such that, in mixed signal/noise, // signal samples have more weight: header = 0x00 0xAA 0x24 0x24 // (in particular for soft bit input!) -static char imet54_header[] = //"0000000001""0101010101""0000000001""0101010101" +static char imet54_header[] = //"0000000001""0101010101""0000000001""0101010101" // 20x 0x00AA //"0000000001""0101010101""0000000001""0101010101" //"0000000001""0101010101""0000000001""0101010101" //"0000000001""0101010101""0000000001""0101010101" @@ -227,8 +229,9 @@ static ui8_t hamming(int opt_ecc, ui8_t *cwb, ui8_t *sym) { static int crc32ok(ui8_t *bytes, int len) { ui32_t poly0 = 0x0EDB; ui32_t poly1 = 0x8260; - //[105 , 7, 0x8EDB, 0x8260], + //[105 , 7, 0x8EDB, 0x8260] // CRC32 802-3 (Ethernet) reversed reciprocal //[104 , 0, 0x48EB, 0x1ACA] + //[102 , 0, 0x1DB7, 0x04C1] // CRC32 802-3 (Ethernet) normal int n = 104; int b = 0; ui32_t c0 = 0x48EB; @@ -242,7 +245,7 @@ static int crc32ok(ui8_t *bytes, int len) { ui32_t crc0 = 0; ui32_t crc1 = 0; - if (len < 108) return 0; + if (len < 108) return 0; // FRMBYTE_STD=108 while (n >= 0) { @@ -453,7 +456,7 @@ static int reset_gpx(gpx_t *gpx) { /* ------------------------------------------------------------------------------------ */ -static int print_position(gpx_t *gpx, int len, int ecc_frm, int ecc_gps) { +static int print_position(gpx_t *gpx, int len, int ecc_frm, int ecc_gps, int ecc_std) { int prnGPS = 0, prnPTU = 0, @@ -462,7 +465,8 @@ static int print_position(gpx_t *gpx, int len, int ecc_frm, int ecc_gps) { int tp_err = 0; int pos_ok = 0, frm_ok = 0, - crc_ok = 0; + crc_ok = 0, + std_ok = 0; int rs_type = 54; crc_ok = crc32ok(gpx->frame, len); @@ -515,8 +519,17 @@ static int print_position(gpx_t *gpx, int len, int ecc_frm, int ecc_gps) { if (gpx->RH > -0.5f) fprintf(stdout, " RH=%.0f%% ", gpx->RH); } - if (gpx->option.vbs) { - if ( crc_ok ) fprintf(stdout, " [OK]"); else fprintf(stdout, " [NO]"); + if ( crc_ok ) fprintf(stdout, " [OK]"); // std frame: frame[104..105]==0x4000 ? + else { + if (gpx->frame[pos_F8] == 0xF8) fprintf(stdout, " [NO]"); + else if ( ecc_std == 0 ) { // full frame: pos_F8_full==pos_F8_std+11 ? + fprintf(stdout, " [ok]"); + std_ok = 1; + } + else { + fprintf(stdout, " [no]"); + std_ok = 0; + } } // (imet54:GPS+PTU) status: 003E , (imet50:GPS); 0030 @@ -534,10 +547,9 @@ static int print_position(gpx_t *gpx, int len, int ecc_frm, int ecc_gps) { } // prnGPS,prnTPU - if (gpx->option.jsn && frm_ok && crc_ok && (gpx->status&0x30)==0x30) { + if (gpx->option.jsn && frm_ok && (crc_ok || std_ok) && (gpx->status&0x30)==0x30) { char *ver_jsn = NULL; - //char *subtype = (rs_type == 54) ? "IMET54" : "IMET50"; - char *subtype = (rs_type == 54) ? "iMet-54" : "iMet-50"; + char *subtype = (rs_type == 54) ? "IMET54" : "IMET50"; unsigned long count_day = (unsigned long)(gpx->std*3600 + gpx->min*60 + gpx->sek+0.5); // (gpx->timems/1e3+0.5) has gaps fprintf(stdout, "{ \"type\": \"%s\"", "IMET5"); fprintf(stdout, ", \"frame\": %lu", count_day); @@ -553,8 +565,13 @@ static int print_position(gpx_t *gpx, int len, int ecc_frm, int ecc_gps) { } fprintf(stdout, ", \"subtype\": \"%s\"", subtype); // "IMET54"/"IMET50" if (gpx->jsn_freq > 0) { - fprintf(stdout, ", \"freq\": %d", gpx->jsn_freq); + fprintf(stdout, ", \"freq\": %d", gpx->jsn_freq ); } + + // Reference time/position + 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 @@ -568,7 +585,7 @@ static int print_position(gpx_t *gpx, int len, int ecc_frm, int ecc_gps) { static void print_frame(gpx_t *gpx, int len, int b2B) { int i, j; - int ecc_frm = 0, ecc_gps = 0; + int ecc_frm = 0, ecc_gps = 0, ecc_std = 0; ui8_t bits8n1[BITFRAME_LEN+10]; // (RAW)BITFRAME_LEN ui8_t bits[BITFRAME_LEN]; // 8/10 (RAW)BITFRAME_LEN ui8_t nib[FRAME_LEN]; @@ -596,12 +613,15 @@ static void print_frame(gpx_t *gpx, int len, int b2B) { ecc_frm = 0; ecc_gps = 0; + ecc_std = 0; for (j = 0; j < len/8; j++) { // alt. only GPS block ecc_frm += ec[j]; if (ec[j] > 0x10) ecc_frm = -1; if (j < pos_GPSalt+4+8) ecc_gps = ecc_frm; + if (j < 2*FRMBYTE_STD) ecc_std = ecc_frm; if (ecc_frm < 0) break; } + if (j < 2*FRMBYTE_STD) ecc_std = -1; } else { ecc_frm = -2; // TODO: parse ecc-info from raw file @@ -610,6 +630,8 @@ static void print_frame(gpx_t *gpx, int len, int b2B) { if (gpx->option.raw) { + int crc_ok = crc32ok(gpx->frame, len); + for (i = 0; i < len/16; i++) { fprintf(stdout, "%02X", gpx->frame[i]); if (gpx->option.raw > 1) @@ -618,7 +640,13 @@ static void print_frame(gpx_t *gpx, int len, int b2B) { if (gpx->option.raw == 4 && i % 4 == 3) fprintf(stdout, " "); } } - if ( crc32ok(gpx->frame, len) ) fprintf(stdout, " [OK]"); else fprintf(stdout, " [NO]"); + + if ( crc_ok ) fprintf(stdout, " [OK]"); // std frame: frame[104..105]==0x4000 ? + else { + if (gpx->frame[pos_F8] == 0xF8) fprintf(stdout, " [NO]"); // full frame: pos_F8_full==pos_F8_std+11 ? + else if ( ecc_std == 0 ) fprintf(stdout, " [ok]"); + else fprintf(stdout, " [no]"); + } if (gpx->option.ecc && ecc_frm != 0) { fprintf(stdout, " # (%d)", ecc_frm); fprintf(stdout, " [%d]", ecc_gps); @@ -626,12 +654,12 @@ static void print_frame(gpx_t *gpx, int len, int b2B) { fprintf(stdout, "\n"); if (gpx->option.slt /*&& gpx->option.jsn*/) { - print_position(gpx, len/16, ecc_frm, ecc_gps); + print_position(gpx, len/16, ecc_frm, ecc_gps, ecc_std); } } else { - print_position(gpx, len/16, ecc_frm, ecc_gps); + print_position(gpx, len/16, ecc_frm, ecc_gps, ecc_std); } } @@ -646,6 +674,7 @@ int main(int argc, char *argv[]) { int option_iqdc = 0; int option_lp = 0; int option_dc = 0; + int option_noLUT = 0; int option_softin = 0; int option_pcmraw = 0; int wavloaded = 0; @@ -762,16 +791,18 @@ int main(int argc, char *argv[]) { 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, "--lpIQ") == 0) { option_lp |= LP_IQ; } // IQ/IF lowpass else if (strcmp(*argv, "--lpbw") == 0) { // IQ lowpass BW / kHz double bw = 0.0; ++argv; if (*argv) bw = atof(*argv); else return -1; if (bw > 4.6 && bw < 24.0) lpIQ_bw = bw*1e3; - option_lp = 1; + option_lp |= LP_IQ; } + else if (strcmp(*argv, "--lpFM") == 0) { option_lp |= LP_FM; } // FM lowpass else if (strcmp(*argv, "--dc") == 0) { option_dc = 1; } + else if (strcmp(*argv, "--noLUT") == 0) { option_noLUT = 1; } else if (strcmp(*argv, "--min") == 0) { option_min = 1; } @@ -815,6 +846,13 @@ int main(int argc, char *argv[]) { } if (!wavloaded) fp = stdin; + if (option_iq == 5 && option_dc) option_lp |= LP_FM; + + // LUT faster for decM, however frequency correction after decimation + // LUT recommonded if decM > 2 + // + if (option_noLUT && option_iq == 5) dsp.opt_nolut = 1; else dsp.opt_nolut = 0; + if (gpx.option.raw && gpx.option.jsn) gpx.option.slt = 1; diff --git a/demod/mod/iq_dec.c b/demod/mod/iq_dec.c new file mode 100644 index 00000000..935536d6 --- /dev/null +++ b/demod/mod/iq_dec.c @@ -0,0 +1,1157 @@ + +/* + * compile: + * + * gcc -Ofast iq_dec.c -lm -o iq_dec + * + * + * usage: + * + * ./iq_dec [--bo ] [--iq ] [iq_baseband.wav] # =8,16,32 bit output + * ./iq_dec [--bo ] [--iq ] - [iq_baseband.raw] + * + * ./iq_dec [--bo ] [--wav] [--FM] [--iq ] iq_baseband.wav + * ./iq_dec [--bo ] [--wav] [--decFM] [--iq ] - [iq_baseband.raw] + * --iq : center at fq=freq/sr (default: 0.0) + * --wav : output wav header + * --FM/decFM : FM demodulation + * --bo : output bits per sample b=8,16,32 (u8, s16, f32 (default)) + * + * + * author: zilog80 + */ + + +/* ------------------------------------------------------------------------------------ */ + +#include +#include +#include + + +#define FM_GAIN (0.8) + +/* ------------------------------------------------------------------------------------ */ + + +#include +#include + +#ifndef M_PI + #define M_PI (3.1415926535897932384626433832795) +#endif +#define _2PI (6.2831853071795864769252867665590) + +#define LP_IQ 1 +#define LP_FM 2 +#define LP_IQFM 4 + + +#ifndef INTTYPES +#define INTTYPES +typedef unsigned char ui8_t; +typedef unsigned short ui16_t; +typedef unsigned int ui32_t; +typedef unsigned long long ui64_t; +typedef char i8_t; +typedef short i16_t; +typedef int i32_t; +#endif + + +typedef struct { + FILE *fp; + // + int sr; // sample_rate + int bps; // bits/sample + int nch; // channels + int ch; // select channel + // + int bps_out; + // + ui32_t sample_in; + ui32_t sample_out; + // + + // IQ-data + //int opt_iq; // always IQ input + int opt_iqdc; // in f32read_cblock() anyway + + + double V_noise; + double V_signal; + double SNRdB; + + // decimate + int exlut; + int opt_nolut; // default: exlut + int opt_IFmin; + int decM; + int decFM; + ui32_t sr_base; + ui32_t dectaps; + ui32_t sample_decX; + ui32_t lut_len; + ui32_t sample_decM; + float complex *decXbuffer; + float complex *decMbuf; + float complex *ex; // exp_lut + double xlt_fq; + + int opt_fm; + int opt_lp; + + // IF: lowpass + int lpIQ_bw; + int lpIQtaps; // ui32_t + float lpIQ_fbw; + float *ws_lpIQ; + float complex *lpIQ_buf; + + // FM: lowpass + int lpFM_bw; + int lpFMtaps; // ui32_t + float *ws_lpFM; + float *lpFM_buf; + float *fm_buffer; + +} dsp_t; + + +typedef struct { + int sr; // sample_rate + int sr_out; + int bps; // bits_sample bits/sample + int bps_out; + int nch; // channels + int sel_ch; // select wav channel +} pcm_t; + + + +/* ------------------------------------------------------------------------------------ */ + +static 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; +} + +static int read_wav_header(pcm_t *pcm, FILE *fp) { + char txt[4+1] = "\0\0\0\0"; + unsigned char dat[4]; + int byte, p=0; + int sample_rate = 0, bits_sample = 0, channels = 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 (pcm->sel_ch < 0 || pcm->sel_ch >= channels) pcm->sel_ch = 0; // default channel: 0 + + if (bits_sample != 8 && bits_sample != 16 && bits_sample != 32) return -1; + + if (sample_rate == 900001) sample_rate -= 1; + + pcm->sr = sample_rate; + pcm->bps = bits_sample; + pcm->nch = channels; + + return 0; +} + +static float write_wav_header(pcm_t *pcm) { + FILE *fp = stdout; + ui32_t sr = pcm->sr_out; + ui32_t bps = pcm->bps_out; + ui32_t data = 0; + + fwrite("RIFF", 1, 4, fp); + data = 0; // bytes-8=headersize-8+datasize + fwrite(&data, 1, 4, fp); + fwrite("WAVE", 1, 4, fp); + + fwrite("fmt ", 1, 4, fp); + data = 16; if (bps == 32) data += 2; + fwrite(&data, 1, 4, fp); + + if (bps == 32) data = 3; // IEEE float + else data = 1; // PCM + fwrite(&data, 1, 2, fp); + + data = pcm->nch; // channels + fwrite(&data, 1, 2, fp); + + data = sr; + fwrite(&data, 1, 4, fp); + + data = sr*bps/8; + fwrite(&data, 1, 4, fp); + + data = (bps+7)/8; + fwrite(&data, 1, 2, fp); + + data = bps; + fwrite(&data, 1, 2, fp); + + if (bps == 32) { + data = 0; // size of extension: 0 + fwrite(&data, 1, 2, fp); + } + + fwrite("data", 1, 4, fp); + data = 0xFFFFFFFF; // datasize unknown + fwrite(&data, 1, 4, fp); + + return 0; +} + + +static int f32read_sample(dsp_t *dsp, float *s) { + int i; + unsigned int word = 0; + short *b = (short*)&word; + float *f = (float*)&word; + + for (i = 0; i < dsp->nch; i++) { + + if (fread( &word, dsp->bps/8, 1, dsp->fp) != 1) return EOF; + + if (i == dsp->ch) { // 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 (dsp->bps == 32) { + *s = *f; + } + else { + if (dsp->bps == 8) { *b -= 128; } + *s = *b/128.0; + if (dsp->bps == 16) { *s /= 256.0; } + } + } + } + + return 0; +} + +typedef struct { + double sumIQx; + double sumIQy; + float avgIQx; + float avgIQy; + float complex avgIQ; + ui32_t cnt; + ui32_t maxcnt; + ui32_t maxlim; +} iq_dc_t; +static iq_dc_t IQdc; + +static int f32read_csample(dsp_t *dsp, float complex *z) { + + float x, y; + + if (dsp->bps == 32) { //float32 + float f[2]; + if (fread( f, dsp->bps/8, 2, dsp->fp) != 2) return EOF; + x = f[0]; + y = f[1]; + } + else if (dsp->bps == 16) { //int16 + short b[2]; + if (fread( b, dsp->bps/8, 2, dsp->fp) != 2) return EOF; + x = b[0]/32768.0; + y = b[1]/32768.0; + } + else { // dsp->bps == 8 //uint8 + ui8_t u[2]; + if (fread( u, dsp->bps/8, 2, dsp->fp) != 2) return EOF; + x = (u[0]-128)/128.0; + y = (u[1]-128)/128.0; + } + + *z = x + I*y; + + // IQ-dc removal optional + if (dsp->opt_iqdc) { + *z -= IQdc.avgIQ; + + IQdc.sumIQx += x; + IQdc.sumIQy += y; + IQdc.cnt += 1; + if (IQdc.cnt == IQdc.maxcnt) { + IQdc.avgIQx = IQdc.sumIQx/(float)IQdc.maxcnt; + IQdc.avgIQy = IQdc.sumIQy/(float)IQdc.maxcnt; + IQdc.avgIQ = IQdc.avgIQx + I*IQdc.avgIQy; + IQdc.sumIQx = 0; IQdc.sumIQy = 0; IQdc.cnt = 0; + if (IQdc.maxcnt < IQdc.maxlim) IQdc.maxcnt *= 2; + } + } + + return 0; +} + +static int f32read_cblock(dsp_t *dsp) { + + int n; + int len; + float x, y; + ui8_t s[4*2*dsp->decM]; //uin8,int16,float32 + ui8_t *u = (ui8_t*)s; + short *b = (short*)s; + float *f = (float*)s; + + + len = fread( s, dsp->bps/8, 2*dsp->decM, dsp->fp) / 2; + + //for (n = 0; n < len; n++) dsp->decMbuf[n] = (u[2*n]-128)/128.0 + I*(u[2*n+1]-128)/128.0; + // u8: 0..255, 128 -> 0V + for (n = 0; n < len; n++) { + if (dsp->bps == 8) { //uint8 + x = (u[2*n ]-128)/128.0; + y = (u[2*n+1]-128)/128.0; + } + else if (dsp->bps == 16) { //int16 + x = b[2*n ]/32768.0; + y = b[2*n+1]/32768.0; + } + else { // dsp->bps == 32 //float32 + x = f[2*n]; + y = f[2*n+1]; + } + + // baseband: IQ-dc removal mandatory + dsp->decMbuf[n] = (x-IQdc.avgIQx) + I*(y-IQdc.avgIQy); + + IQdc.sumIQx += x; + IQdc.sumIQy += y; + IQdc.cnt += 1; + if (IQdc.cnt == IQdc.maxcnt) { + IQdc.avgIQx = IQdc.sumIQx/(float)IQdc.maxcnt; + IQdc.avgIQy = IQdc.sumIQy/(float)IQdc.maxcnt; + IQdc.avgIQ = IQdc.avgIQx + I*IQdc.avgIQy; + IQdc.sumIQx = 0; IQdc.sumIQy = 0; IQdc.cnt = 0; + if (IQdc.maxcnt < IQdc.maxlim) IQdc.maxcnt *= 2; + } + } + + return len; +} + +// decimate lowpass +static float *ws_dec; + +static double sinc(double x) { + double y; + if (x == 0) y = 1; + else y = sin(M_PI*x)/(M_PI*x); + return y; +} + +static int lowpass_init(float f, int taps, float **pws) { + double *h, *w; + double norm = 0; + int n; + float *ws = NULL; + + if (taps % 2 == 0) taps++; // odd/symmetric + + if ( taps < 1 ) taps = 1; + + h = (double*)calloc( taps+1, sizeof(double)); if (h == NULL) return -1; + w = (double*)calloc( taps+1, sizeof(double)); if (w == NULL) return -1; + ws = (float*)calloc( 2*taps+1, sizeof(float)); if (ws == NULL) return -1; + + for (n = 0; n < taps; n++) { + w[n] = 7938/18608.0 - 9240/18608.0*cos(_2PI*n/(taps-1)) + 1430/18608.0*cos(4*M_PI*n/(taps-1)); // Blackmann + h[n] = 2*f*sinc(2*f*(n-(taps-1)/2)); + ws[n] = w[n]*h[n]; + norm += ws[n]; // 1-norm + } + for (n = 0; n < taps; n++) { + ws[n] /= norm; // 1-norm + } + + for (n = 0; n < taps; n++) ws[taps+n] = ws[n]; // duplicate/unwrap + + *pws = ws; + + free(h); h = NULL; + free(w); w = NULL; + + return taps; +} + + +static int lowpass_update(float f, int taps, float *ws) { + double *h, *w; + double norm = 0; + int n; + + if (taps % 2 == 0) taps++; // odd/symmetric + + if ( taps < 1 ) taps = 1; + + h = (double*)calloc( taps+1, sizeof(double)); if (h == NULL) return -1; + w = (double*)calloc( taps+1, sizeof(double)); if (w == NULL) return -1; + + for (n = 0; n < taps; n++) { + w[n] = 7938/18608.0 - 9240/18608.0*cos(_2PI*n/(taps-1)) + 1430/18608.0*cos(4*M_PI*n/(taps-1)); // Blackmann + h[n] = 2*f*sinc(2*f*(n-(taps-1)/2)); + ws[n] = w[n]*h[n]; + norm += ws[n]; // 1-norm + } + for (n = 0; n < taps; n++) { + ws[n] /= norm; // 1-norm + } + + for (n = 0; n < taps; n++) ws[taps+n] = ws[n]; + + free(h); h = NULL; + free(w); w = NULL; + + return taps; +} + +static float complex lowpass0(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { + ui32_t n; + double complex w = 0; + for (n = 0; n < taps; n++) { + w += buffer[(sample+n)%taps]*ws[taps-1-n]; + } + return (float complex)w; +} +//static __attribute__((optimize("-ffast-math"))) float complex lowpass() +static float complex lowpass(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { + float complex w = 0; + int n; // -Ofast + int S = taps - (sample % taps); + for (n = 0; n < taps; n++) { + w += buffer[n]*ws[S+n]; // ws[taps+s-n] = ws[(taps+sample-n)%taps] + } + return w; +// symmetry: ws[n] == ws[taps-1-n] +} +static float complex lowpass2(float complex buffer[], ui32_t sample, ui32_t taps, float *ws) { + float complex w = 0; + int n; + int s = sample % taps; + int S1 = s; + int S1N = S1-taps; + int n0 = taps-s; + for (n = 0; n < n0; n++) { + w += buffer[S1+n]*ws[n]; + } + for (n = n0; n < taps; n++) { + w += buffer[S1N+n]*ws[n]; + } + return w; +// symmetry: ws[n] == ws[taps-1-n] +} + +static float re_lowpass0(float buffer[], ui32_t sample, ui32_t taps, float *ws) { + ui32_t n; + double w = 0; + for (n = 0; n < taps; n++) { + w += buffer[(sample+n)%taps]*ws[taps-1-n]; + } + return (float)w; +} +static float re_lowpass(float buffer[], ui32_t sample, ui32_t taps, float *ws) { + float w = 0; + int n; + int S = taps - (sample % taps); + for (n = 0; n < taps; n++) { + w += buffer[n]*ws[S+n]; // ws[taps+s-n] = ws[(taps+sample-n)%taps] + } + return w; +} + + +static int ifblock(dsp_t *dsp, float complex *z_out) { + + float complex z; + int j; + + if ( f32read_cblock(dsp) < dsp->decM ) return EOF; + + for (j = 0; j < dsp->decM; j++) { + if (dsp->opt_nolut) { + double _s_base = (double)(dsp->sample_in*dsp->decM+j); // dsp->sample_dec + double f0 = dsp->xlt_fq*_s_base; + z = dsp->decMbuf[j] * cexp(f0*_2PI*I); + } + else if (dsp->exlut) { + z = dsp->decMbuf[j] * dsp->ex[dsp->sample_decM]; + } + else { + z = dsp->decMbuf[j]; + } + dsp->sample_decM += 1; if (dsp->sample_decM >= dsp->lut_len) dsp->sample_decM = 0; + + dsp->decXbuffer[dsp->sample_decX] = z; + dsp->sample_decX += 1; if (dsp->sample_decX >= dsp->dectaps) dsp->sample_decX = 0; + } + if (dsp->decM > 1) + { + z = lowpass(dsp->decXbuffer, dsp->sample_decX, dsp->dectaps, ws_dec); + } + + *z_out = z; + + dsp->sample_in += 1; + + return 0; +} + +static int if_fm(dsp_t *dsp, float complex *z_out, float *s) { + + static float complex z0; + float complex z, w; + float s_fm = 0.0f; + float gain = FM_GAIN; + ui32_t _sample = dsp->sample_in * dsp->decFM; + int m; + int j; + + for (m = 0; m < dsp->decFM; m++) + { + + if ( f32read_cblock(dsp) < dsp->decM ) return EOF; + + for (j = 0; j < dsp->decM; j++) { + if (dsp->opt_nolut) { + double _s_base = (double)(_sample*dsp->decM+j); // dsp->sample_dec + double f0 = dsp->xlt_fq*_s_base; + z = dsp->decMbuf[j] * cexp(f0*_2PI*I); + } + else if (dsp->exlut) { + z = dsp->decMbuf[j] * dsp->ex[dsp->sample_decM]; + } + else { + z = dsp->decMbuf[j]; + } + dsp->sample_decM += 1; if (dsp->sample_decM >= dsp->lut_len) dsp->sample_decM = 0; + + dsp->decXbuffer[dsp->sample_decX] = z; + dsp->sample_decX += 1; if (dsp->sample_decX >= dsp->dectaps) dsp->sample_decX = 0; + } + if (dsp->decM > 1) + { + z = lowpass(dsp->decXbuffer, dsp->sample_decX, dsp->dectaps, ws_dec); + } + + // IF-lowpass + if (dsp->opt_lp & LP_IQ) { + dsp->lpIQ_buf[_sample % dsp->lpIQtaps] = z; + z = lowpass(dsp->lpIQ_buf, _sample+1, dsp->lpIQtaps, dsp->ws_lpIQ); + } + + if (dsp->opt_fm) { + w = z * conj(z0); + s_fm = gain * carg(w)/M_PI; + z0 = z; + + // FM-lowpass + if (dsp->opt_lp & LP_FM) { + dsp->lpFM_buf[_sample % dsp->lpFMtaps] = s_fm; + if (m+1 == dsp->decFM) { + s_fm = re_lowpass(dsp->lpFM_buf, _sample+1, dsp->lpFMtaps, dsp->ws_lpFM); + } + } + } + + *z_out = z; + + _sample += 1; + + } + + *s = s_fm; + + dsp->sample_in += 1; + + return 0; +} + + +/* -------------------------------------------------------------------------- */ + +#define IF_SAMPLE_RATE 48000 +#define IF_SAMPLE_RATE_MIN 32000 + +static int IF_min = IF_SAMPLE_RATE; + +#define IF_TRANSITION_BW (4e3) // 4kHz transition width +#define FM_TRANSITION_BW (2e3) // 2kHz transition width + + +static int init_buffers(dsp_t *dsp) { + + int K = 0; + int n, k; + + + // decimate + int IF_sr = IF_min; // designated IF sample rate + int decM = 1; // decimate M:1 + int sr_base = dsp->sr; + float f_lp; // dec_lowpass: lowpass_bandwidth/2 + float t_bw; // dec_lowpass: transition_bandwidth + int taps; // dec_lowpass: taps + + //if (dsp->opt_IFmin) IF_sr = IF_SAMPLE_RATE_MIN; + if (IF_sr > sr_base) IF_sr = sr_base; + if (IF_sr < sr_base) { + while (sr_base % IF_sr) IF_sr += 1; + decM = sr_base / IF_sr; + } + + f_lp = (IF_sr+20e3)/(4.0*sr_base); // for IF=48k + t_bw = (IF_sr-20e3)/*/2.0*/; + if (dsp->opt_IFmin) { + t_bw = (IF_sr-12e3); + } + if (t_bw < 0) t_bw = 10e3; + t_bw /= sr_base; + taps = 4.0/t_bw; if (taps%2==0) taps++; + + taps = lowpass_init(f_lp, taps, &ws_dec); // decimate lowpass + if (taps < 0) return -1; + dsp->dectaps = (ui32_t)taps; + + dsp->sr_base = sr_base; + dsp->sr = IF_sr; // sr_base/decM + dsp->decM = decM; + + fprintf(stderr, "IF: %d\n", IF_sr); + fprintf(stderr, "dec: %d\n", decM); + + + if (dsp->exlut && !dsp->opt_nolut) + { + // look up table, exp-rotation + int W = 2*8; // 16 Hz window + int d = 1; // 1..W , groesster Teiler d <= W von sr_base + int freq = (int)( dsp->xlt_fq * (double)dsp->sr_base + 0.5); + int freq0 = freq; // init + double f0 = freq0 / (double)dsp->sr_base; // init + + for (d = W; d > 0; d--) { // groesster Teiler d <= W von sr + if (dsp->sr_base % d == 0) break; + } + if (d == 0) d = 1; // d >= 1 ? + + for (k = 0; k < W/2; k++) { + if ((freq+k) % d == 0) { + freq0 = freq + k; + break; + } + if ((freq-k) % d == 0) { + freq0 = freq - k; + break; + } + } + + dsp->lut_len = dsp->sr_base / d; + f0 = freq0 / (double)dsp->sr_base; + + dsp->ex = calloc(dsp->lut_len+1, sizeof(float complex)); + if (dsp->ex == NULL) return -1; + for (n = 0; n < dsp->lut_len; n++) { + double t = f0*(double)n; + dsp->ex[n] = cexp(t*_2PI*I); + } + } + + dsp->decXbuffer = calloc( dsp->dectaps+1, sizeof(float complex)); + if (dsp->decXbuffer == NULL) return -1; + + dsp->decMbuf = calloc( dsp->decM+1, sizeof(float complex)); + if (dsp->decMbuf == NULL) return -1; + + + // IF lowpass + if (dsp->opt_lp & LP_IQ) + { + float f_lp; // lowpass_bw + int taps; // lowpass taps: 4*sr/transition_bw + + f_lp = 24e3/(float)dsp->sr/2.0; // default + if (dsp->lpIQ_bw) f_lp = dsp->lpIQ_bw/(float)dsp->sr/2.0; + taps = 4*dsp->sr/IF_TRANSITION_BW; if (taps%2==0) taps++; + taps = lowpass_init(f_lp, taps, &dsp->ws_lpIQ); if (taps < 0) return -1; + + dsp->lpIQ_fbw = f_lp; + dsp->lpIQtaps = taps; + dsp->lpIQ_buf = calloc( dsp->lpIQtaps+3, sizeof(float complex)); + if (dsp->lpIQ_buf == NULL) return -1; + + } + + // FM lowpass + if (dsp->opt_lp & LP_FM) + { + float f_lp; // lowpass_bw + int taps; // lowpass taps: 4*sr/transition_bw + + f_lp = 10e3/(float)dsp->sr; // default + if (dsp->lpFM_bw > 0) f_lp = dsp->lpFM_bw/(float)dsp->sr; + taps = 4*dsp->sr/FM_TRANSITION_BW; if (taps%2==0) taps++; + taps = lowpass_init(f_lp, taps, &dsp->ws_lpFM); if (taps < 0) return -1; + + dsp->lpFMtaps = taps; + dsp->lpFM_buf = calloc( dsp->lpFMtaps+3, sizeof(float complex)); + if (dsp->lpFM_buf == NULL) return -1; + } + + + memset(&IQdc, 0, sizeof(IQdc)); + IQdc.maxlim = dsp->sr; + IQdc.maxcnt = IQdc.maxlim/32; // 32,16,8,4,2,1 + if (dsp->decM > 1) { + IQdc.maxlim *= dsp->decM; + IQdc.maxcnt *= dsp->decM; + } + + + if (dsp->nch < 2) return -1; + + return K; +} + +static int free_buffers(dsp_t *dsp) { + + // decimate + if (dsp->decXbuffer) { free(dsp->decXbuffer); dsp->decXbuffer = NULL; } + if (dsp->decMbuf) { free(dsp->decMbuf); dsp->decMbuf = NULL; } + if (dsp->exlut && !dsp->opt_nolut) { + if (dsp->ex) { free(dsp->ex); dsp->ex = NULL; } + } + + if (ws_dec) { free(ws_dec); ws_dec = NULL; } + + + // IF lowpass + if (dsp->opt_lp & LP_IQ) + { + if (dsp->ws_lpIQ) { free(dsp->ws_lpIQ); dsp->ws_lpIQ = NULL; } + if (dsp->lpIQ_buf) { free(dsp->lpIQ_buf); dsp->lpIQ_buf = NULL; } + } + // FM lowpass + if (dsp->opt_lp & LP_FM) + { + if (dsp->ws_lpFM) { free(dsp->ws_lpFM); dsp->ws_lpFM = NULL; } + if (dsp->lpFM_buf) { free(dsp->lpFM_buf); dsp->lpFM_buf = NULL; } + } + + return 0; +} + +/* ------------------------------------------------------------------------------------ */ + +#include + +static int write_cpx_blk(dsp_t *dsp, float complex *z, int len) { + int j, l; + short b[2*len]; + ui8_t u[2*len]; + float xy[2*len]; + int bps = dsp->bps_out; + int fd = 1; // STDOUT_FILENO + + for (j = 0; j < len; j++) { + xy[2*j ] = creal(z[j]); + xy[2*j+1] = cimag(z[j]); + } + + if (bps == 32) { + l = write(fd, xy, 2*len*bps/8); + } + else { + for (j = 0; j < 2*len; j++) xy[j] *= 128.0; // 127.0 + if (bps == 8) { + for (j = 0; j < 2*len; j++) { + xy[j] += 128.0; // x *= scale8b; + u[j] = (ui8_t)(xy[j]); //b = (int)(x+0.5); + } + l = write(fd, u, 2*len*bps/8); + } + else { // bps == 16 + for (j = 0; j < 2*len; j++) { + xy[j] *= 256.0; + b[j] = (short)xy[j]; //b = (int)(x+0.5); + } + l = write(fd, b, 2*len*bps/8); + } + } + + return l*8/(2*bps); +} + +// fwrite return items: size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); +static int fwrite_cpx_blk(dsp_t *dsp, float complex *z, int len) { + int j, l; + short b[2*len]; + ui8_t u[2*len]; + float xy[2*len]; + int bps = dsp->bps_out; + FILE *fo = stdout; + + for (j = 0; j < len; j++) { + xy[2*j ] = creal(z[j]); + xy[2*j+1] = cimag(z[j]); + } + + if (bps == 32) { + l = fwrite(xy, 2*bps/8, len, fo); + } + else { + for (j = 0; j < 2*len; j++) xy[j] *= 128.0; // 127.0 + if (bps == 8) { + for (j = 0; j < 2*len; j++) { + xy[j] += 128.0; // x *= scale8b; + u[j] = (ui8_t)(xy[j]); //b = (int)(x+0.5); + } + l = fwrite(u, 2*bps/8, len, fo); + } + else { // bps == 16 + for (j = 0; j < 2*len; j++) { + xy[j] *= 256.0; + b[j] = (short)xy[j]; //b = (int)(x+0.5); + } + l = fwrite(b, 2*bps/8, len, fo); + } + } + + return l; +} + +static int fwrite_fm(dsp_t *dsp, float s) { + int bps = dsp->bps_out; + FILE *fpo = stdout; + ui8_t u = 0; + i16_t b = 0; + ui32_t *w = (ui32_t*)&s; + + if (bps == 8) { + s *= 127.0; + s += 128.0; + u = (ui8_t)s; + w = (ui32_t*)&u; + } + else if (bps == 16) { + s *= 127.0*256.0; + b = (i16_t)s; + w = (ui32_t*)&b; + } + fwrite( w, bps/8, 1, fpo); + + return 0; +} + +static int fwrite_fm_blk(dsp_t *dsp, float *s, int len) { + int j, l; + short b[len]; + ui8_t u[len]; + float x[len]; + int bps = dsp->bps_out; + FILE *fo = stdout; + + for (j = 0; j < len; j++) { + x[j] = s[j]; + } + + if (bps == 32) { + l = fwrite(x, bps/8, len, fo); + } + else { + for (j = 0; j < len; j++) x[j] *= 128.0; // 127.0 + if (bps == 8) { + for (j = 0; j < len; j++) { + x[j] += 128.0; // x *= scale8b; + u[j] = (ui8_t)(x[j]); //b = (int)(x+0.5); + } + l = fwrite(u, bps/8, len, fo); + } + else { // bps == 16 + for (j = 0; j < len; j++) { + x[j] *= 256.0; + b[j] = (short)x[j]; //b = (int)(x+0.5); + } + l = fwrite(b, bps/8, len, fo); + } + } + + return l; +} + + +/* ------------------------------------------------------------------------------------ */ + + +#define ZLEN 64 + +int main(int argc, char *argv[]) { + + //int option_inv = 0; // invertiert Signal + int option_min = 0; + //int option_iq = 5; + int option_iqdc = 0; + int option_lp = 0; + int option_dc = 0; + int option_noLUT = 0; + int option_pcmraw = 0; + int option_wav = 0; + int option_fm = 0; + int option_decFM = 0; + + int wavloaded = 0; + + FILE *fp; + char *fpname = NULL; + + int k; + + int bitQ; + + int bps_out = 32; + float lpIQ_bw = 10e3; + + + pcm_t pcm = {0}; + dsp_t dsp = {0}; //memset(&dsp, 0, sizeof(dsp)); + + + 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, " --iq0,2,3 (IQ data)\n"); + return 0; + } + else if (strcmp(*argv, "--iqdc") == 0) { option_iqdc = 1; } // iq-dc removal + 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) + dsp.exlut = 1; + //option_iq = 5; + } + else if (strcmp(*argv, "--IFbw") == 0) { // min IF bandwidth / kHz + int ifbw = 0; + ++argv; + if (*argv) ifbw = atoi(*argv); + else return -1; + if (ifbw*1000 >= IF_SAMPLE_RATE_MIN) IF_min = ifbw*1000; + // ?option_lp |= LP_IQ; + } + else if (strcmp(*argv, "--lpIQ") == 0) { option_lp |= LP_IQ; } // IQ/IF lowpass + else if (strcmp(*argv, "--lpbw") == 0) { // IQ lowpass BW / kHz + double bw = 0.0; + ++argv; + if (*argv) bw = atof(*argv); + else return -1; + if (bw > 1.0f) lpIQ_bw = bw*1e3; + option_lp |= LP_IQ; + } + else if (strcmp(*argv, "--FM") == 0) { option_fm = 1; } + else if (strcmp(*argv, "--lpFM") == 0) { + option_lp |= LP_FM; // FM lowpass + option_fm = 1; + } + else if (strcmp(*argv, "--decFM") == 0) { // FM decimation + option_decFM = 4; + option_lp |= LP_FM; // FM lowpass + option_fm = 1; + } + else if (strcmp(*argv, "--dc") == 0) { option_dc = 1; } + else if (strcmp(*argv, "--noLUT") == 0) { option_noLUT = 1; } + else if (strcmp(*argv, "--min") == 0) { + option_min = 1; + } + else if (strcmp(*argv, "--wav") == 0) { + option_wav = 1; + } + else if (strcmp(*argv, "-") == 0) { + int sample_rate = 0, bits_sample = 0, channels = 0; + ++argv; + if (*argv) sample_rate = atoi(*argv); else return -1; + ++argv; + if (*argv) bits_sample = atoi(*argv); else return -1; + channels = 2; + if (sample_rate < 1 || (bits_sample != 8 && bits_sample != 16 && bits_sample != 32)) { + fprintf(stderr, "- \n"); + return -1; + } + pcm.sr = sample_rate; + pcm.bps = bits_sample; + pcm.nch = channels; + option_pcmraw = 1; + } + else if (strcmp(*argv, "--bo") == 0) { + ++argv; + if (*argv) bps_out = atoi(*argv); else return -1; + if ((bps_out != 8 && bps_out != 16 && bps_out != 32)) { + bps_out = 0; + } + } + else { + fp = fopen(*argv, "rb"); + if (fp == NULL) { + fprintf(stderr, "error: open %s\n", *argv); + return -1; + } + wavloaded = 1; + } + ++argv; + } + if (!wavloaded) fp = stdin; + + if (/*option_iq == 5 &&*/ option_dc) option_lp |= LP_FM; + + // LUT faster for decM, however frequency correction after decimation + // LUT recommonded if decM > 2 + // + if (option_noLUT /*&& option_iq == 5*/) dsp.opt_nolut = 1; else dsp.opt_nolut = 0; + + + pcm.sel_ch = 0; + if (option_pcmraw == 0) { + k = read_wav_header(&pcm, fp); + if ( k < 0 ) { + fclose(fp); + fprintf(stderr, "error: wav header\n"); + return -1; + } + } + + + // init dsp + // + dsp.fp = fp; + dsp.sr = pcm.sr; + dsp.bps = pcm.bps; + dsp.nch = pcm.nch; + dsp.ch = pcm.sel_ch; + //dsp.opt_iq = option_iq; + dsp.opt_iqdc = option_iqdc; // in f32read_cblock() anyway + dsp.opt_lp = option_lp; + dsp.lpIQ_bw = lpIQ_bw; // 10e3 // IF lowpass bandwidth + dsp.lpFM_bw = 6e3; // FM audio lowpass + dsp.opt_IFmin = option_min; + dsp.bps_out = bps_out; + + if (option_fm) dsp.opt_fm = 1; + + k = init_buffers(&dsp); + if ( k < 0 ) { + fprintf(stderr, "error: init buffers\n"); + return -1; + } + // base: dsp.sr_base + // if : dsp.sr + + dsp.decFM = 1; + if (option_decFM) { + int fm_sr = dsp.sr; + while (fm_sr % 2 == 0 && fm_sr/2 >= 48000) { + fm_sr /= 2; + dsp.decFM *= 2; + } + // if (dsp.decFM > 1) option_lp |= LP_FM; // set above + dsp.opt_fm = 1; + } + + pcm.sr_out = dsp.sr; + pcm.bps_out = dsp.bps_out; + if (option_fm) { + pcm.nch = 1; + pcm.sr_out = dsp.sr / dsp.decFM; + } + if (option_wav) write_wav_header( &pcm ); + + + int len = ZLEN; + int l, n = 0; + + float complex z_vec[ZLEN]; // init ? + float s_vec[ZLEN]; + + bitQ = 0; + while ( bitQ != EOF ) + { + bitQ = if_fm(&dsp, z_vec+n, s_vec+n); + n++; + if (n == len || bitQ == EOF) { + if (bitQ == EOF) n--; + if (dsp.opt_fm) { + l = fwrite_fm_blk(&dsp, s_vec, n); + } + else { + l = fwrite_cpx_blk(&dsp, z_vec, n); + } + n = 0; + } + } + + + free_buffers(&dsp); + + fclose(fp); + + return 0; +} + diff --git a/demod/mod/lms6Xmod.c b/demod/mod/lms6Xmod.c index 01633443..f16f7b4a 100644 --- a/demod/mod/lms6Xmod.c +++ b/demod/mod/lms6Xmod.c @@ -778,12 +778,18 @@ static void print_frame(gpx_t *gpx, int crc_err, int len) { if (gpx->jsn_freq > 0) { printf(", \"freq\": %d", gpx->jsn_freq); } + + // Reference time/position + printf(", \"ref_datetime\": \"%s\"", "GPS" ); // {"GPS", "UTC"} GPS-UTC=leap_sec + printf(", \"ref_position\": \"%s\"", "GPS" ); // {"GPS", "MSL"} GPS=ellipsoid , MSL=geoid + #ifdef VER_JSN_STR ver_jsn = VER_JSN_STR; #endif if (ver_jsn && *ver_jsn != '\0') printf(", \"version\": \"%s\"", ver_jsn); printf(" }\n"); printf("\n"); + fflush(stdout); } } @@ -994,6 +1000,7 @@ int main(int argc, char **argv) { int option_iqdc = 0; int option_lp = 0; int option_dc = 0; + int option_noLUT = 0; int option_softin = 0; int option_pcmraw = 0; int wavloaded = 0; @@ -1128,16 +1135,18 @@ int main(int argc, char **argv) { 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, "--lpIQ") == 0) { option_lp |= LP_IQ; } // IQ/IF lowpass else if (strcmp(*argv, "--lpbw") == 0) { // IQ lowpass BW / kHz double bw = 0.0; ++argv; if (*argv) bw = atof(*argv); else return -1; if (bw > 4.6 && bw < 24.0) lpIQ_bw = bw*1e3; - option_lp = 1; + option_lp |= LP_IQ; } + else if (strcmp(*argv, "--lpFM") == 0) { option_lp |= LP_FM; } // FM lowpass else if (strcmp(*argv, "--dc") == 0) { option_dc = 1; } + else if (strcmp(*argv, "--noLUT") == 0) { option_noLUT = 1; } else if (strcmp(*argv, "--min") == 0) { option_min = 1; } @@ -1181,6 +1190,13 @@ int main(int argc, char **argv) { } if (!wavloaded) fp = stdin; + if (option_iq == 5 && option_dc) option_lp |= LP_FM; + + // LUT faster for decM, however frequency correction after decimation + // LUT recommonded if decM > 2 + // + if (option_noLUT && option_iq == 5) dsp.opt_nolut = 1; else dsp.opt_nolut = 0; + if (gpx->option.raw == 4) gpx->option.ecc = 1; @@ -1260,7 +1276,7 @@ int main(int argc, char **argv) { dsp.opt_iq = option_iq; dsp.opt_iqdc = option_iqdc; dsp.opt_lp = option_lp; - dsp.lpIQ_bw = lpIQ_bw; // 16e3; // IF lowpass bandwidth // soft decoding? + dsp.lpIQ_bw = lpIQ_bw; //16e3; // IF lowpass bandwidth // soft decoding? dsp.lpFM_bw = 6e3; // FM audio lowpass dsp.opt_dc = option_dc; dsp.opt_IFmin = option_min; diff --git a/demod/mod/m10mod.c b/demod/mod/m10mod.c index d8b8e835..e80af154 100644 --- a/demod/mod/m10mod.c +++ b/demod/mod/m10mod.c @@ -657,7 +657,7 @@ static float get_Temp(gpx_t *gpx) { // [ 30.0 , 4.448 ] // [ 35.0 , 3.704 ] // [ 40.0 , 3.100 ] -// -> Steinhart–Hart coefficients (polyfit): +// -> Steinhart-Hart coefficients (polyfit): float p0 = 1.07303516e-03, p1 = 2.41296733e-04, p2 = 2.26744154e-06, @@ -753,7 +753,7 @@ static float get_Tntc2(gpx_t *gpx) { // float R25 = 2.2e3; // float b = 3650.0; // B/Kelvin // float T25 = 25.0 + 273.15; // T0=25C, R0=R25=5k -// -> Steinhart–Hart coefficients (polyfit): +// -> Steinhart-Hart coefficients (polyfit): float p0 = 4.42606809e-03, p1 = -6.58184309e-04, p2 = 8.95735557e-05, @@ -1026,6 +1026,12 @@ static int print_pos(gpx_t *gpx, int csOK) { if (gpx->jsn_freq > 0) { fprintf(stdout, ", \"freq\": %d", gpx->jsn_freq); } + + // Reference time/position (M10 time ref UTC only for json) + fprintf(stdout, ", \"ref_datetime\": \"%s\"", "UTC" ); // {"GPS", "UTC"} GPS-UTC=leap_sec + fprintf(stdout, ", \"ref_position\": \"%s\"", "GPS" ); // {"GPS", "MSL"} GPS=ellipsoid , MSL=geoid + fprintf(stdout, ", \"gpsutc_leapsec\": %d", gpx->utc_ofs); // GPS-UTC offset, utc_s = gpx->gpssec - gpx->utc_ofs; + #ifdef VER_JSN_STR ver_jsn = VER_JSN_STR; #endif @@ -1143,6 +1149,7 @@ int main(int argc, char **argv) { int option_iqdc = 0; int option_lp = 0; int option_dc = 0; + int option_noLUT = 0; int option_chk = 0; int option_softin = 0; int option_pcmraw = 0; @@ -1170,6 +1177,8 @@ int main(int argc, char **argv) { float thres = 0.76; float _mv = 0.0; + float lpIQ_bw = 24e3; + int symlen = 2; int bitofs = 0; // 0 .. +2 int shift = 0; @@ -1254,8 +1263,18 @@ int main(int argc, char **argv) { 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, "--lpIQ") == 0) { option_lp |= LP_IQ; } // IQ/IF lowpass + else if (strcmp(*argv, "--lpbw") == 0) { // IQ lowpass BW / kHz + double bw = 0.0; + ++argv; + if (*argv) bw = atof(*argv); + else return -1; + if (bw > 4.6 && bw < 48.0) lpIQ_bw = bw*1e3; + option_lp |= LP_IQ; + } + else if (strcmp(*argv, "--lpFM") == 0) { option_lp |= LP_FM; } // FM lowpass else if (strcmp(*argv, "--dc") == 0) { option_dc = 1; } + else if (strcmp(*argv, "--noLUT") == 0) { option_noLUT = 1; } else if (strcmp(*argv, "--min") == 0) { option_min = 1; } @@ -1296,6 +1315,13 @@ int main(int argc, char **argv) { } if (!wavloaded) fp = stdin; + if (option_iq == 5 && option_dc) option_lp |= LP_FM; + + // LUT faster for decM, however frequency correction after decimation + // LUT recommonded if decM > 2 + // + if (option_noLUT && option_iq == 5) dsp.opt_nolut = 1; else dsp.opt_nolut = 0; + if (gpx.option.raw && gpx.option.jsn) gpx.option.slt = 1; @@ -1356,7 +1382,7 @@ int main(int argc, char **argv) { dsp.opt_iq = option_iq; dsp.opt_iqdc = option_iqdc; dsp.opt_lp = option_lp; - dsp.lpIQ_bw = 24e3; // IF lowpass bandwidth + dsp.lpIQ_bw = lpIQ_bw; //24e3; // IF lowpass bandwidth dsp.lpFM_bw = 10e3; // FM audio lowpass dsp.opt_dc = option_dc; dsp.opt_IFmin = option_min; @@ -1496,7 +1522,7 @@ int main(int argc, char **argv) { while (1 > 0) { - memset(buffer_rawhex, 2*(FRAME_LEN+AUX_LEN)+12, 0); + memset(buffer_rawhex, 0, 2*(FRAME_LEN+AUX_LEN)+12); pbuf = fgets(buffer_rawhex, 2*(FRAME_LEN+AUX_LEN)+12, fp); if (pbuf == NULL) break; buffer_rawhex[2*(FRAME_LEN+AUX_LEN)] = '\0'; diff --git a/demod/mod/m20mod.c b/demod/mod/m20mod.c index ad9b9868..072a7ebe 100644 --- a/demod/mod/m20mod.c +++ b/demod/mod/m20mod.c @@ -751,12 +751,12 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { 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"%.1f"col_TXT" D: "col_GPSvel"%.1f"col_TXT" vV: "col_GPSvel"%.1f"col_TXT" ", gpx->vH, gpx->vD, gpx->vV); + 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 >= 2 && (bcOK || csOK)) { // SN + if (gpx->option.vbs >= 1 && (bcOK || csOK)) { // SN fprintf(stdout, " SN: "col_SN"%s"col_TXT, gpx->SN); } - if (gpx->option.vbs >= 2) { + 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); @@ -769,7 +769,9 @@ static int print_pos(gpx_t *gpx, int bcOK, int 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->TH > -273.0f) fprintf(stdout, " TH:%.1fC", gpx->TH); + 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); @@ -789,12 +791,12 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { fprintf(stdout, " lon: %.5f ", gpx->lon); fprintf(stdout, " alt: %.2f ", gpx->alt); if (!err2) { - fprintf(stdout, " vH: %.1f D: %.1f vV: %.1f ", gpx->vH, gpx->vD, gpx->vV); + fprintf(stdout, " vH: %4.1f D: %5.1f vV: %3.1f ", gpx->vH, gpx->vD, gpx->vV); } - if (gpx->option.vbs >= 2 && (bcOK || csOK)) { // SN + if (gpx->option.vbs >= 1 && (bcOK || csOK)) { // SN fprintf(stdout, " SN: %s", gpx->SN); } - if (gpx->option.vbs >= 2) { + if (gpx->option.vbs >= 1) { fprintf(stdout, " # "); //if (bcOK) fprintf(stdout, " (ok)"); else fprintf(stdout, " (no)"); if (bcOK > 0) fprintf(stdout, " (ok)"); @@ -807,7 +809,9 @@ static int print_pos(gpx_t *gpx, int bcOK, int 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->TH > -273.0f) fprintf(stdout, " TH:%.1fC", gpx->TH); + 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); @@ -842,6 +846,11 @@ static int print_pos(gpx_t *gpx, int bcOK, int csOK) { if (gpx->jsn_freq > 0) { fprintf(stdout, ", \"freq\": %d", gpx->jsn_freq); } + + // Reference time/position + fprintf(stdout, ", \"ref_datetime\": \"%s\"", "GPS" ); // {"GPS", "UTC"} GPS-UTC=leap_sec + fprintf(stdout, ", \"ref_position\": \"%s\"", "GPS" ); // {"GPS", "MSL"} GPS=ellipsoid , MSL=geoid + #ifdef VER_JSN_STR ver_jsn = VER_JSN_STR; #endif @@ -1318,6 +1327,7 @@ int main(int argc, char **argv) { header_found = 0; // bis Ende der Sekunde vorspulen; allerdings Doppel-Frame alle 10 sek + // M20 only single frame ... AUX ? if (gpx.option.vbs < 3) { // && (regulare frame) // print_frame-return? while ( bitpos < 5*BITFRAME_LEN ) { if (option_softin) { @@ -1350,7 +1360,7 @@ int main(int argc, char **argv) { while (1 > 0) { - memset(buffer_rawhex, 2*(FRAME_LEN+AUX_LEN)+12, 0); + memset(buffer_rawhex, 0, 2*(FRAME_LEN+AUX_LEN)+12); pbuf = fgets(buffer_rawhex, 2*(FRAME_LEN+AUX_LEN)+12, fp); if (pbuf == NULL) break; buffer_rawhex[2*(FRAME_LEN+AUX_LEN)] = '\0'; diff --git a/demod/mod/meisei100mod.c b/demod/mod/meisei100mod.c index 0e190ceb..19eba26d 100644 --- a/demod/mod/meisei100mod.c +++ b/demod/mod/meisei100mod.c @@ -23,7 +23,7 @@ PCM-FM, 1200 baud biphase-S 1200 bit pro Sekunde: zwei Frames, die wiederum in zwei Subframes unterteilt werden koennen, d.h. 4 mal 300 bit. -Variante 1 (RS-11G ?) +Variante 1 (RS-11G)