From 24b6835331f3ed9b4f98377f2cde9800a3e1fa90 Mon Sep 17 00:00:00 2001 From: Mark Jessop Date: Fri, 10 Nov 2023 08:42:53 +1030 Subject: [PATCH 1/3] Some intial tests of KA9Q-Radio support (no spectrum yet) --- auto_rx/auto_rx.py | 9 +- auto_rx/autorx/config.py | 4 +- auto_rx/autorx/ka9q.py | 129 ++++++++++++++++++++++++++++ auto_rx/autorx/scan.py | 8 +- auto_rx/autorx/sdr_wrappers.py | 107 ++++++++++++++++++++--- auto_rx/station.cfg.example | 10 +-- auto_rx/station.cfg.example.network | 10 +-- 7 files changed, 250 insertions(+), 27 deletions(-) create mode 100644 auto_rx/autorx/ka9q.py diff --git a/auto_rx/auto_rx.py b/auto_rx/auto_rx.py index bcd03c40..99481433 100644 --- a/auto_rx/auto_rx.py +++ b/auto_rx/auto_rx.py @@ -445,7 +445,8 @@ def clean_task_list(): else: # Shutdown the SDR, if required for the particular SDR type. - shutdown_sdr(config["sdr_type"], _task_sdr) + if _key != 'SCAN': + shutdown_sdr(config["sdr_type"], _task_sdr, sdr_hostname=config["sdr_hostname"], frequency=_key) # Release its associated SDR. autorx.sdr_list[_task_sdr]["in_use"] = False autorx.sdr_list[_task_sdr]["task"] = None @@ -505,6 +506,12 @@ def stop_all(): for _task in autorx.task_list.keys(): try: autorx.task_list[_task]["task"].stop() + + # Release the SDR channel if necessary + _task_sdr = autorx.task_list[_task]["device_idx"] + if _task != 'SCAN': + shutdown_sdr(config["sdr_type"], _task_sdr, sdr_hostname=config["sdr_hostname"], frequency=_task) + except Exception as e: logging.error("Error stopping task - %s" % str(e)) diff --git a/auto_rx/autorx/config.py b/auto_rx/autorx/config.py index 07503a81..bfc45a89 100644 --- a/auto_rx/autorx/config.py +++ b/auto_rx/autorx/config.py @@ -867,7 +867,7 @@ def read_auto_rx_config(filename, no_sdr_test=False): return None for _n in range(1, auto_rx_config["sdr_quantity"] + 1): - _sdr_name = f"KA9Q{_n:02d}" + _sdr_name = f"KA9Q-{_n:02d}" auto_rx_config["sdr_settings"][_sdr_name] = { "ppm": 0, "gain": 0, @@ -876,8 +876,6 @@ def read_auto_rx_config(filename, no_sdr_test=False): "task": None, } - logging.critical("Config - KA9Q SDR Support not implemented yet - exiting.") - return None else: logging.critical(f"Config - Unknown SDR Type {auto_rx_config['sdr_type']} - exiting.") diff --git a/auto_rx/autorx/ka9q.py b/auto_rx/autorx/ka9q.py new file mode 100644 index 00000000..f4ca66f1 --- /dev/null +++ b/auto_rx/autorx/ka9q.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# +# radiosonde_auto_rx - SDR Abstraction - KA9Q-Radio +# +# Copyright (C) 2022 Mark Jessop +# Released under GNU GPL v3 or later +# + +import logging +import os.path +import platform +import subprocess +from .utils import timeout_cmd + + +def ka9q_setup_channel( + sdr_hostname, + frequency, + sample_rate +): + # tune --samprate 48000 --frequency 404m09 --mode iq --ssrc 404090000 --radio sonde.local + _cmd = ( + f"{timeout_cmd()} 5 " # Add a timeout, because connections to non-existing servers block for ages + f"tune " + f"--samprate {int(sample_rate)} " + f"--mode iq " + f"--frequency {int(frequency)} " + f"--ssrc {int(frequency)} " + f"--radio {sdr_hostname}" + ) + + logging.debug(f"KA9Q - Starting channel at {frequency} Hz, with command: {_cmd}") + + try: + _output = subprocess.check_output( + _cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + + if e.returncode == 124: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed while opening channel with a timeout. Is the server running?" + ) + elif e.returncode == 127: + logging.critical( + f"KA9Q ({sdr_hostname}) - Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed." + ) + else: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed while opening channel with return code {e.returncode}." + ) + # Look at the error output in a bit more details. + #_output = e.output.decode("ascii") + + # TODO - see if we can look in the output for any error messages. + return False + + return True + + +def ka9q_close_channel( + sdr_hostname, + frequency +): + + _cmd = ( + f"{timeout_cmd()} 5 " # Add a timeout, because connections to non-existing servers block for ages + f"tune " + f"--samprate 48000 " + f"--mode iq " + f"--frequency 0 " + f"--ssrc {int(frequency)} " + f"--radio {sdr_hostname}" + ) + + logging.debug(f"KA9Q - Closing channel at {frequency} Hz, with command: {_cmd}") + + try: + _output = subprocess.check_output( + _cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + + if e.returncode == 124: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed while closing channel with a timeout. Is the server running?" + ) + elif e.returncode == 127: + logging.critical( + f"KA9Q ({sdr_hostname}) - Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed." + ) + else: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed while closing chanel with return code {e.returncode}." + ) + # Look at the error output in a bit more details. + #_output = e.output.decode("ascii") + + # TODO - see if we can look in the output for any error messages. + return False + + return True + + +def ka9q_get_iq_cmd( + sdr_hostname, + frequency, + sample_rate +): + + # We need to setup a channel before we can use it! + _setup_success = ka9q_setup_channel(sdr_hostname, frequency, sample_rate) + + if not _setup_success: + logging.critical(f"KA9Q ({sdr_hostname}) - Could not setup rx channel! Decoder will likely timeout.") + + # Get the 'PCM' version of the server name, where as assume -pcm is added to the first part of the hostname. + _pcm_host = sdr_hostname.split('.')[0] + "-pcm." + ".".join(sdr_hostname.split(".")[1:]) + + # pcmcat -2 -s 404090000 sonde-pcm.local + _cmd = ( + f"pcmcat -2 " + f"-s {int(frequency)} " + f"{_pcm_host} |" + ) + + return _cmd diff --git a/auto_rx/autorx/scan.py b/auto_rx/autorx/scan.py index cc2d791e..c1085dd5 100644 --- a/auto_rx/autorx/scan.py +++ b/auto_rx/autorx/scan.py @@ -26,7 +26,7 @@ peak_decimation, timeout_cmd ) -from .sdr_wrappers import test_sdr, reset_sdr, get_sdr_name, get_sdr_iq_cmd, get_sdr_fm_cmd, get_power_spectrum +from .sdr_wrappers import test_sdr, reset_sdr, get_sdr_name, get_sdr_iq_cmd, get_sdr_fm_cmd, get_power_spectrum, shutdown_sdr try: @@ -434,6 +434,10 @@ def detect_sonde( ret_output = subprocess.check_output(rx_test_command, shell=True, stderr=FNULL) FNULL.close() ret_output = ret_output.decode("utf8") + + # Release the SDR channel if necessary + shutdown_sdr(sdr_type, rtl_device_idx, sdr_hostname, frequency) + except subprocess.CalledProcessError as e: # dft_detect returns a code of 1 if no sonde is detected. # logging.debug("Scanner - dfm_detect return code: %s" % e.returncode) @@ -452,7 +456,7 @@ def detect_sonde( except Exception as e: # Something broke when running the detection function. logging.error( - f"Scanner ({_sdr_name}) - Error when running dft_detect - {sdr(e)}" + f"Scanner ({_sdr_name}) - Error when running dft_detect - {str(e)}" ) return (None, 0.0) diff --git a/auto_rx/autorx/sdr_wrappers.py b/auto_rx/autorx/sdr_wrappers.py index 6fc2afe0..e874b939 100644 --- a/auto_rx/autorx/sdr_wrappers.py +++ b/auto_rx/autorx/sdr_wrappers.py @@ -12,6 +12,7 @@ import numpy as np from .utils import rtlsdr_test, reset_rtlsdr_by_serial, reset_all_rtlsdrs, timeout_cmd +from .ka9q import * def test_sdr( @@ -51,13 +52,86 @@ def test_sdr( elif sdr_type == "KA9Q": - # To be implemented - _ok = False + # Test that a KA9Q server is working by attempting to start up a new narrowband channel on it. + + # Check for presence of KA9Q-radio binaries that we need + # if not os.path.isfile('tune'): + # logging.critical("Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed.") + # return False + # if not os.path.isfile('pcmcat'): + # logging.critical("Could not find KA9Q-Radio 'pcmcat' binary! This may need to be compiled and installed.") + # return False + # TBD - whatever we need for spectrum use. + # if not os.path.isfile('TBD'): + # logging.critical("Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed.") + # return False + + + # Try and configure a channel at check_freq Hz + # tune --samprate 48000 --frequency 404m09 --mode iq --ssrc 404090000 --radio sonde.local + _cmd = ( + f"{timeout_cmd()} 5 " # Add a timeout, because connections to non-existing servers block for ages + f"tune " + f"--samprate 48000 --mode iq " + f"--frequency {int(check_freq)} " + f"--ssrc {int(check_freq)} " + f"--radio {sdr_hostname}" + ) - if not _ok: - logging.error(f"KA9Q Server {sdr_hostname}:{sdr_port} non-functional.") + logging.debug(f"KA9Q - Testing using command: {_cmd}") - return _ok + try: + _output = subprocess.check_output( + _cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + + if e.returncode == 124: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed with a timeout. Is the server running?" + ) + elif e.returncode == 127: + logging.critical( + f"KA9Q ({sdr_hostname}) - Could not find KA9Q-Radio 'tune' binary! This may need to be compiled and installed." + ) + else: + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call failed with return code {e.returncode}." + ) + # Look at the error output in a bit more details. + #_output = e.output.decode("ascii") + + # TODO - see if we can look in the output for any error messages. + return False + + # Now close the channel we just opened by setting the frequency to 0 Hz. + _cmd = ( + f"{timeout_cmd()} 5 " # Add a timeout, because connections to non-existing servers block for ages + f"tune " + f"--samprate 48000 --mode iq " + f"--frequency 0 " + f"--ssrc {int(check_freq)} " + f"--radio {sdr_hostname}" + ) + + logging.debug(f"KA9Q - Closing testing channel using command: {_cmd}") + try: + _output = subprocess.check_output( + _cmd, shell=True, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + # Something went wrong... + logging.critical( + f"KA9Q ({sdr_hostname}) - tune call (closing channel) failed with return code {e.returncode}." + ) + # Look at the error output in a bit more details. + #_output = e.output.decode("ascii") + + # TODO - see if we can look in the output for any error messages. + return False + + return True elif sdr_type == "SpyServer": # Test connectivity to a SpyServer by trying to grab some samples. @@ -156,7 +230,7 @@ def get_sdr_name( return f"RTLSDR {rtl_device_idx}" elif sdr_type == "KA9Q": - return f"KA9Q {sdr_hostname}:{sdr_port}" + return f"KA9Q {sdr_hostname}" elif sdr_type == "SpyServer": return f"SpyServer {sdr_hostname}:{sdr_port}" @@ -167,7 +241,9 @@ def get_sdr_name( def shutdown_sdr( sdr_type: str, - sdr_id: str + sdr_id: str, + sdr_hostname = "", + frequency: int = None ): """ Function to trigger shutdown/cleanup of some SDR types. @@ -178,8 +254,8 @@ def shutdown_sdr( """ if sdr_type == "KA9Q": - # TODO - KA9Q Server channel cleanup. - logging.debug(f"TODO - Cleanup for SDR type {sdr_type}") + logging.debug(f"KA9Q - Closing Channel for {sdr_hostname} @ {frequency} Hz.") + ka9q_close_channel(sdr_hostname, frequency) pass else: logging.debug(f"No shutdown action required for SDR type {sdr_type}") @@ -278,6 +354,14 @@ def get_sdr_iq_cmd( _cmd += _dc_remove return _cmd + + if sdr_type == "KA9Q": + _cmd = ka9q_get_iq_cmd(sdr_hostname, frequency, sample_rate) + + if dc_block: + _cmd += _dc_remove + + return _cmd else: logging.critical(f"IQ Source - Unsupported SDR type {sdr_type}") @@ -614,8 +698,9 @@ def get_power_spectrum( else: # Unsupported SDR Type - logging.critical(f"Get PSD - Unsupported SDR Type: {sdr_type}") - return (None, None, None) + logging.debug(f"Get PSD - Unsupported SDR Type: {sdr_type}") + return (np.array([0,1,2]),np.array([0,1,2]),1) + #return (None, None, None) if __name__ == "__main__": diff --git a/auto_rx/station.cfg.example b/auto_rx/station.cfg.example index edbde7f9..1588252a 100644 --- a/auto_rx/station.cfg.example +++ b/auto_rx/station.cfg.example @@ -21,8 +21,8 @@ # # EXPERIMENTAL / NOT IMPLEMENTED options: # SpyServer - Use an Airspy SpyServer -# KA9Q - Use a KA9Q SDR Server (Not yet implemented) -# WARNING: These are still under development and may not work. +# KA9Q - Use a KA9Q-Radio Server +# WARNING: These are still under development and may not work correctly. # sdr_type = RTLSDR @@ -43,9 +43,9 @@ sdr_quantity = 1 # # Network SDR Connection Details # -# If using either a KA9Q or SpyServer network server, the hostname and port -# of the server needs to be defined below. Usually this will be running on the -# same machine as auto_rx, so the defaults are usually fine. +# If using a spyserver, the hostname and port need to be defined below. +# Is using KA9Q-Radio, the hostname of the 'radio' server (e.g. sonde.local) needs to be +# defined, and the port number is unused. # sdr_hostname = localhost sdr_port = 5555 diff --git a/auto_rx/station.cfg.example.network b/auto_rx/station.cfg.example.network index 4cd46182..b94e0152 100644 --- a/auto_rx/station.cfg.example.network +++ b/auto_rx/station.cfg.example.network @@ -22,8 +22,8 @@ # # EXPERIMENTAL / NOT IMPLEMENTED options: # SpyServer - Use an Airspy SpyServer -# KA9Q - Use a KA9Q SDR Server (Not yet implemented) -# WARNING: These are still under development and may not work. +# KA9Q - Use a KA9Q-Radio Server +# WARNING: These are still under development and may not work correctly. # sdr_type = SpyServer @@ -44,9 +44,9 @@ sdr_quantity = 5 # # Network SDR Connection Details # -# If using either a KA9Q or SpyServer network server, the hostname and port -# of the server needs to be defined below. Usually this will be running on the -# same machine as auto_rx, so the defaults are usually fine. +# If using a spyserver, the hostname and port need to be defined below. +# Is using KA9Q-Radio, the hostname of the 'radio' server (e.g. sonde.local) needs to be +# defined, and the port number is unused. # sdr_hostname = localhost sdr_port = 5555 From 1d90d0c4b62051e3f3523377e2eda830dd79182a Mon Sep 17 00:00:00 2001 From: Mark Jessop Date: Fri, 10 Nov 2023 08:43:27 +1030 Subject: [PATCH 2/3] bump testing version --- auto_rx/autorx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auto_rx/autorx/__init__.py b/auto_rx/autorx/__init__.py index 07c5bdf5..9dc1d8b1 100644 --- a/auto_rx/autorx/__init__.py +++ b/auto_rx/autorx/__init__.py @@ -12,7 +12,7 @@ # MINOR - New sonde type support, other fairly big changes that may result in telemetry or config file incompatability issus. # PATCH - Small changes, or minor feature additions. -__version__ = "1.7.2-beta1" +__version__ = "1.7.2-beta2" # Global Variables From 2807232f23f0ec910993d739b9f520f08c5a437f Mon Sep 17 00:00:00 2001 From: Mark Jessop Date: Mon, 1 Jan 2024 10:25:38 +1030 Subject: [PATCH 3/3] Add Temp Block button on web controls --- auto_rx/autorx/__init__.py | 2 +- auto_rx/autorx/decode.py | 6 ++++- auto_rx/autorx/static/js/autorxapi.js | 38 +++++++++++++++++++++++++++ auto_rx/autorx/templates/index.html | 3 +++ auto_rx/autorx/web.py | 13 +++++++-- 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/auto_rx/autorx/__init__.py b/auto_rx/autorx/__init__.py index 9dc1d8b1..206edf55 100644 --- a/auto_rx/autorx/__init__.py +++ b/auto_rx/autorx/__init__.py @@ -12,7 +12,7 @@ # MINOR - New sonde type support, other fairly big changes that may result in telemetry or config file incompatability issus. # PATCH - Small changes, or minor feature additions. -__version__ = "1.7.2-beta2" +__version__ = "1.7.2-beta3" # Global Variables diff --git a/auto_rx/autorx/decode.py b/auto_rx/autorx/decode.py index 75679a28..ed4e0b71 100644 --- a/auto_rx/autorx/decode.py +++ b/auto_rx/autorx/decode.py @@ -1845,8 +1845,12 @@ def log_critical(self, line): f"Decoder ({_sdr_name}) {self.sonde_type} {self.sonde_freq/1e6:.3f} - {line}" ) - def stop(self, nowait=False): + def stop(self, nowait=False, temporary_lockout=False): """ Kill the currently running decoder subprocess """ + + if temporary_lockout: + self.exit_state = "TempBlock" + self.decoder_running = False if self.decoder is not None and (not nowait): diff --git a/auto_rx/autorx/static/js/autorxapi.js b/auto_rx/autorx/static/js/autorxapi.js index 340cfcff..7cf7c141 100644 --- a/auto_rx/autorx/static/js/autorxapi.js +++ b/auto_rx/autorx/static/js/autorxapi.js @@ -62,6 +62,7 @@ function disable_web_controls(){ $("#verify-password").prop('disabled', true); $("#start-decoder").prop('disabled', true); $("#stop-decoder").prop('disabled', true); + $("#stop-decoder-lockout").prop('disabled', true); $("#enable-scanner").prop('disabled', true); $("#disable-scanner").prop('disabled', true); $("#frequency-input").prop('disabled', true); @@ -75,6 +76,7 @@ function pause_web_controls() { $("#verify-password").prop('disabled', true); $("#start-decoder").prop('disabled', true); $("#stop-decoder").prop('disabled', true); + $("#stop-decoder-lockout").prop('disabled', true); $("#enable-scanner").prop('disabled', true); $("#disable-scanner").prop('disabled', true); $("#frequency-input").prop('disabled', true); @@ -86,6 +88,7 @@ function resume_web_controls() { $("#verify-password").prop('disabled', false); $("#start-decoder").prop('disabled', false); $("#stop-decoder").prop('disabled', false); + $("#stop-decoder-lockout").prop('disabled', false); $("#enable-scanner").prop('disabled', false); $("#disable-scanner").prop('disabled', false); $("#frequency-input").prop('disabled', false); @@ -235,6 +238,41 @@ function stop_decoder(){ }); } +function stop_decoder_lockout(){ + // Stop the decoder on the requested frequency, and lockout frequency + + // Re-verify the password. This will occur async, so wont stop the main request from going ahead, + // but will at least present an error for the user. + verify_password(); + + // Grab the password + _api_password = getCookie("password"); + + // Grab the selected frequency + _decoder = $('#stop-frequency-select').val(); + + // Do the request + $.post( + "stop_decoder", + {password: _api_password, freq: _decoder, lockout: 1}, + function(data){ + //console.log(data); + pause_web_controls(); + setTimeout(resume_web_controls,10000); + // Need to figure out where to put this data.. + } + ).fail(function(xhr, status, error){ + console.log(error); + // Otherwise, we probably got a 403 error (forbidden) which indicates the password was bad. + if(error == "FORBIDDEN"){ + $("#password-header").html("

Incorrect Password

"); + } else if (error == "NOT FOUND"){ + // Scanner isn't running. Don't do anything. + alert("Decoder on supplied frequency not running!"); + } + }); +} + function start_decoder(){ // Start a decoder on the requested frequency diff --git a/auto_rx/autorx/templates/index.html b/auto_rx/autorx/templates/index.html index 345c555c..2e43c1ad 100644 --- a/auto_rx/autorx/templates/index.html +++ b/auto_rx/autorx/templates/index.html @@ -1684,6 +1684,9 @@

Decoder Control

+
+ +

Scanner Control

Scanner

diff --git a/auto_rx/autorx/web.py b/auto_rx/autorx/web.py index 1455a3c0..67be9dab 100644 --- a/auto_rx/autorx/web.py +++ b/auto_rx/autorx/web.py @@ -464,7 +464,11 @@ def flask_start_decoder(): def flask_stop_decoder(): """ Request that a decoder process be halted. Example: - curl -d "freq=403250000" -X POST http://localhost:5000/stop_decoder + + curl -d "freq=403250000&password=foobar" -X POST http://localhost:5000/stop_decoder + + Stop decoder and lockout for temporary_block_time + curl -d "freq=403250000&password=foobar&lockout=1" -X POST http://localhost:5000/stop_decoder """ if request.method == "POST" and autorx.config.global_config["web_control"]: @@ -476,10 +480,15 @@ def flask_stop_decoder(): ): _freq = float(request.form["freq"]) + _lockout = False + if "lockout" in request.form: + if int(request.form["lockout"]) == 1: + _lockout = True + logging.info("Web - Got decoder stop request: %f" % (_freq)) if _freq in autorx.task_list: - autorx.task_list[_freq]["task"].stop(nowait=True) + autorx.task_list[_freq]["task"].stop(nowait=True, temporary_lockout=_lockout) return "OK" else: # If we aren't running a decoder, 404.