├── .gitattributes ├── .gitignore ├── CHANGELOG.rst ├── MANIFEST.in ├── README.md ├── bin ├── srt_controller.py └── srt_runner.py ├── config-vsrt ├── calibration.json ├── config.yaml ├── schema.yaml └── sky_coords.csv ├── config ├── .gitignore ├── config.yaml ├── schema.yaml └── sky_coords.csv ├── docs ├── 2020_Report.pdf ├── command_files.md ├── config_directory.md ├── diagrams │ ├── SRT-BlockDiagram_ConceptA.drawio │ ├── SRT-BlockDiagram_ConceptA.pdf │ ├── SRT-BlockDiagram_ConceptB.drawio │ ├── SRT-BlockDiagram_ConceptB.pdf │ ├── SRT-BlockDiagram_Current.drawio │ ├── SRT-BlockDiagram_Current.pdf │ ├── SRT-BlockDiagram_Final.drawio │ └── SRT-BlockDiagram_Final.pdf ├── images │ ├── monitor_page.png │ ├── radio_calibrate.png │ ├── radio_process.png │ ├── radio_save_raw.png │ ├── radio_save_spec.png │ ├── radio_save_spec_fits.png │ ├── srt_image.jpg │ └── system_page.png ├── port_usage.md ├── radio_processing.md └── save_files.md ├── examples ├── example_cmd_file.txt ├── galactic_rotation_curve.py └── move_cmds.txt ├── license ├── news ├── TEMPLATE.rst └── update-conda-recipe.rst ├── radio ├── .gitignore ├── radio_calibrate │ └── radio_calibrate.grc ├── radio_process │ ├── radio_process.grc │ └── radioonly.grc ├── radio_save_raw │ └── radio_save_raw.grc ├── radio_save_spec │ └── radio_save_spec.grc └── radio_save_spec_fits │ └── radio_save_spec_fits.grc ├── recipe ├── .condarc └── meta.yaml ├── rever.xsh ├── scripts ├── test_ephemeris.py ├── test_motor.py └── test_yaml.py ├── setup.cfg ├── setup.py ├── srt ├── __init__.py ├── _version.py ├── config_loader.py ├── daemon │ ├── __init__.py │ ├── daemon.py │ ├── radio_control │ │ ├── __init__.py │ │ ├── radio_calibrate │ │ │ ├── __init__.py │ │ │ ├── radio_calibrate.py │ │ │ └── save_calibration.py │ │ ├── radio_process │ │ │ ├── __init__.py │ │ │ ├── add_clock_tags.py │ │ │ └── radio_process.py │ │ ├── radio_save_raw │ │ │ ├── __init__.py │ │ │ └── radio_save_raw.py │ │ ├── radio_save_spec_fits │ │ │ ├── __init__.py │ │ │ ├── radio_save_spec_fits.py │ │ │ └── save_fits_file.py │ │ ├── radio_save_spec_rad │ │ │ ├── __init__.py │ │ │ ├── radio_save_spec.py │ │ │ └── save_rad_file.py │ │ └── radio_task_starter.py │ ├── rotor_control │ │ ├── __init__.py │ │ ├── motors.py │ │ └── rotors.py │ └── utilities │ │ ├── __init__.py │ │ ├── functions.py │ │ └── object_tracker.py ├── dashboard │ ├── __init__.py │ ├── app.py │ ├── assets │ │ ├── __init__.py │ │ ├── favicon.ico │ │ ├── layout_styles.css │ │ ├── resizing_script.js │ │ ├── responsive-sidebar.css │ │ └── styles.css │ ├── images │ │ ├── MIT_HO_logo_landscape.png │ │ └── MIT_HO_logo_square_transparent.png │ ├── layouts │ │ ├── __init__.py │ │ ├── graphs.py │ │ ├── monitor_page.py │ │ ├── navbar.py │ │ ├── sidebar.py │ │ └── system_page.py │ └── messaging │ │ ├── __init__.py │ │ ├── command_dispatcher.py │ │ ├── raw_radio_fetcher.py │ │ ├── spectrum_fetcher.py │ │ └── status_fetcher.py └── postprocessing │ ├── __init__.py │ └── readrad.py └── versioneer.py /.gitattributes: -------------------------------------------------------------------------------- 1 | srt/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # PyCharm project settings 107 | .idea/ 108 | 109 | # Rever 110 | rever/ 111 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | srt-py Change Log 3 | ================= 4 | 5 | .. current developments 6 | 7 | vv1.1.1 8 | ==================== 9 | 10 | **Added:** 11 | 12 | * Dashboard view mode in srt_runner.py 13 | * Visability on dashboard showing beamwdith. 14 | 15 | 16 | 17 | vv1.1.0 18 | ==================== 19 | 20 | **Added:** 21 | 22 | * Added npoint scan (sinc) interpolation to daemon/utilities/functions.py 23 | * Added npoint scan image to dashboard/layouts/graphs.py 24 | * VLSR calculation added to object_tracker 25 | * VLSR distributed through daemon 26 | * VLSR shown on display 27 | 28 | **Changed:** 29 | 30 | * Changed radio_process.grc in the radio directory 31 | * Generated radio_process.py from the grc file and placed in the daemon directory 32 | * Adjusted behavior of npoint scan to have single center point during scan. 33 | * Moved readrad.py to postprocessing folder within main srt folder 34 | * Added baudrate as an option to config/schema file. 35 | 36 | **Fixed:** 37 | 38 | * Time alignment issues with spectrum and pointing direction. 39 | * Typo in gnuradio grc files and derived python vslr to vlsr. 40 | * Dashboard error from not finding image. MHO images now install with setup and listed in Manifest.in. 41 | * Added shebang to start of scripts. 42 | 43 | 44 | 45 | v1.0.0 46 | ==================== 47 | 48 | **Added:** 49 | 50 | * Added npoint scan (sinc) interpolation to daemon/utilities/functions.py 51 | * Added npoint scan image to dashboard/layouts/graphs.py 52 | 53 | **Changed:** 54 | 55 | * Changed radio_process.grc in the radio directory 56 | * Generated radio_process.py from the grc file and placed in the daemon directory 57 | * Adjusted behavior of npoint scan to have single center point during scan. 58 | * Moved readrad.py to postprocessing folder within main srt folder 59 | 60 | **Fixed:** 61 | 62 | * Time alignment issues with spectrum and pointing direction. 63 | 64 | 65 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include srt/_version.py 3 | include srt/dashboard/images/MIT_HO_logo_square_transparent.png 4 | include srt/dashboard/images/MIT_HO_logo_landscape.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SRT-Py 2 | 3 | Small Radio Telescope Control Code for Python 4 | 5 | ## Description 6 | 7 | A Python port of Haystack Observatory's Small Radio Telescope control software. 8 | 9 | ## Features 10 | 11 | * A New Dash-based Webpage UI for Operating the SRT Remotely 12 | * GNU Radio Flowgraphs Controlling the Signal Processing 13 | * Capable of Saving Raw I/Q Samples or Spectrum Data 14 | 15 | ### Prerequisites 16 | 17 | This software is written in pure Python, and so depends on having an installed version of Python 3.6 or newer (although previous versions of Python 3 could be made compatible with only minor tweaks). For your system, installation instructions for just Python can be found [here](https://www.python.org/downloads/), but it is recommended to instead install [Anaconda](https://docs.anaconda.com/anaconda/install/) or [Miniconda](https://docs.conda.io/en/latest/miniconda.html) Python in order to take advantage of their dependency management. 18 | 19 | ## Installation 20 | 21 | ### Building the Conda Package Locally 22 | 23 | After downloading the srt-py source repository, open up a command prompt or terminal with conda installed and navigate to the folder containing the srt-py directory. Additionally, ensure that you have conda-build and conda-verify installed 24 | 25 | ``` 26 | conda install conda-build conda-verify 27 | ``` 28 | 29 | Build and install the conda package 30 | 31 | ``` 32 | conda-build srt-py 33 | conda install -c file://${CONDA_PREFIX}/conda-bld/ srt-py 34 | ``` 35 | 36 | ### Building the Pip Package Locally (Not Recommended due to Dependency Issues) 37 | 38 | After downloading the srt-py source repository, open up a command prompt or terminal with conda installed and navigate to the srt-py directory. Additionally, it will be necessary to manually install some dependencies, which are specified below, that are not available through PyPI. If these are not installed, then the following steps will throw an error with the missing dependencies. 39 | 40 | Build and install the pip package 41 | 42 | ``` 43 | pip install . 44 | ``` 45 | 46 | ## Getting Started 47 | 48 | Before using the software, you must create a "config" directory, which should follow the convention set by the example config directory in this repository. Be sure to change the settings to best match your hardware, celestial objects of interest, and use case. Please refer to the docs for more information. 49 | 50 | Once installed, you can start the SRT Daemon and SRT Dashboard by running by executing (for the default runtime options): 51 | 52 | ``` 53 | srt_runner.py --config_dir=PATH_TO_CONFIG_DIR 54 | ``` 55 | 56 | If instead running without installing, you can start the SRT Daemon and SRT Dashboard by running by executing while in the srt-py directory: 57 | 58 | ``` 59 | conda develop . 60 | python bin/srt_runner.py --config_dir=PATH_TO_CONFIG_DIR 61 | ``` 62 | 63 | For all commands in following sections which assume installation, modifying those commands analogously will also allow you to run them from source directly, which make custom modifications and debugging much easier. 64 | 65 | Note: This requires manually making sure you have all the dependencies listed in Required Libraries 66 | 67 | ### OS-Specific Setup 68 | 69 | #### Windows 70 | 71 | On Windows, using a certain radio typically requires that one install a Windows specific driver in addition to the steps above. For RTL-SDRs, like the one used for local testing during the development of this software, instructions for installing the driver using [Zagid](https://github.com/pbatard/libwdi/releases) can be found [here](https://www.rtl-sdr.com/rtl-sdr-quick-start-guide/), where only following steps 8-11 should be necessary. 72 | 73 | ## Example Usage 74 | 75 | Determining whether the dashboard will automatically open up is determined by the "RUN_HEADLESS" parameter in its YAML config file. When the software runs with its dashboard, operating via command line still remains available. 76 | 77 | ### Operating via the Dashboard 78 | 79 | The dashboard is accessed through a web browser, on a port specified in the YAML config file. 80 | 81 | Both of the two pages of the dashboard have a collapsible sidebar on the left side, which contains two different sections. One section gives a brief overview of the current status of the SRT, including whether it's running a command and its position. The other contains the links for switching between the Monitor and System pages. 82 | 83 | #### Monitoring Page UI 84 | 85 | ![Monitor Page](docs/images/monitor_page.png) 86 | 87 | The bar at the top of the dashboard manages sending commands to the SRT, which are organized into categories: 88 | 89 | - Antenna 90 | - Stow 91 | - Set AzEl 92 | - Set Offsets 93 | - Radio 94 | - Set Frequency 95 | - Set Bandwidth 96 | - Routine 97 | - Start Recording 98 | - Stop Recording 99 | - Calibrate 100 | - Upload CMD File 101 | - Power 102 | - Start Daemon 103 | - Shutdown 104 | 105 | Additionally, there are four different interactive graphs displayed on this screen. 106 | The 'Power vs Time' graph displays the received power over a certain range of time into the past. 107 | The first of the two spectrum graphs, 'Raw Spectrum', shows the processed and integrated radio FFT data, whose values don't necessarily have any real world units and have a shape that is influenced by the band-pass filter. The other, 'Calibrated Spectrum' shows the values after dividing out the calibration values taken when the 'Calibrate' command was last run on a test source of known temperature (such as a clump of trees or a noise diode). 108 | Finally, there is the Azimuth-Elevation graph, which shows the current position of all objects specified to be tracked in the sky_coords.csv configuration file, as well as the reachable limits of the motor and the horizon. Clicking on a point allows you to send a command to track that object, perform an n-point scan about the object, or repeatedly move the antenna across it. 109 | 110 | #### System Page UI 111 | 112 | ![System Page](docs/images/system_page.png) 113 | 114 | The System Page contains many displays of information not necessary for actively controlling the SRT. In case of a serious problem occuring when operating the SRT, there is a section for Emergency Contact Info. There is similarly a 'Message Logs' scrolling area for logs sent from the SRT, in order to assist in debugging or just determining what it has done recently. In the middle is a more verbose status blurb about the status of the SRT's command queue, including the number of commands queued up and what the SRT is currently trying to run. Finally, there is also a list of the files and folders in the SRT's specified recording save directory, which users can directly download files from via the dashboard if the "DASHBOARD_DOWNLOADS" setting in the configuration YAML is set to Yes. 115 | 116 | ### Running Headless / Command Line Usage 117 | 118 | The script 'srt_controller.py' allows for running individual commands, entire command files, and viewing the current SRT status JSON. More information on the available commands can be found in the documentation. 119 | 120 | #### Sending a Command 121 | 122 | ``` 123 | srt_controller.py command record test.fits 124 | ``` 125 | 126 | ``` 127 | srt_controller.py command stow 128 | ``` 129 | 130 | #### Sending a Command file 131 | 132 | ``` 133 | srt_controller.py command_file examples/example_cmd_file.txt 134 | ``` 135 | 136 | #### Viewing Status 137 | 138 | ``` 139 | srt_controller.py status 140 | ``` 141 | 142 | ``` 143 | srt_controller.py status --status_parameter=motor_azel 144 | ``` 145 | 146 | ## Required Libraries 147 | 148 | - python >=3.6 149 | - numpy 150 | - scipy 151 | - rtl-sdr 152 | - soapysdr 153 | - soapysdr-module-rtlsdr 154 | - gnuradio-core 155 | - gnuradio-zeromq 156 | - gnuradio-osmosdr 157 | - digital_rf 158 | - pyzmq 159 | - pyserial 160 | - astropy 161 | - yamale 162 | - dash 163 | - dash-bootstrap-components 164 | - plotly 165 | - pandas 166 | - waitress 167 | 168 | ## Accommodating Different Hardware 169 | 170 | This software was most heavily tested with a RTL-SDR and a SPID Azimuth-Elevation mount, but the current software should be able to support any SDR that is supported by [SoapySDR](https://github.com/pothosware/SoapySDR/wiki) as well as the H180 Mount used by previous versions of the SRT. 171 | 172 | ### Adding Other Radios 173 | 174 | Since the radio sample acquisition and processing is done via GNU Radio Companion flowgraph scripts, any receiver that has a GNU Radio block library supporting it (which is most SDRs at this point in time) can be operated by switching out the source block in the [radio_process](radio/radio_process/radio_process.grc) script in the radio/ directory. If there is not an existing GNU Radio block supporting your particular radio, consider writing your own [embedded Python block](https://wiki.gnuradio.org/index.php/Embedded_Python_Block) or [GNU Radio library](https://wiki.gnuradio.org/index.php/OutOfTreeModules) to add the feature. 175 | Assuming now that a new source block exists, the process for propagating the change to the code is as follows. First, after opening GNU Radio Companion and swapping out the source block in [radio_process.grc](radio/radio_process/radio_process.grc), generate its Python file equivalent(s). In the srt/daemon/radio_control folder, there should be a corresponding folder to the name of the GRC file, where the new Python file(s) should be placed to replace the existing ones. Make sure during this process that all Python imports still work, since sometimes GNU Radio scripts with embedded Python blocks need their relative imports to be modified after generation. 176 | Finally, refer to the earlier documentation for rebuilding the srt-py conda package and installing from source. 177 | 178 | Overall, this process gives a decent outline for how to make any changes needed to any of the radio processing scripts using GNU Radio, such as modifying the radio scripts that save into a particular file type ([digital_rf](radio/radio_save_raw/radio_save_raw.grc), [fits](radio/radio_save_spec_fits/radio_save_spec_fits.grc), or [rad](radio/radio_save_spec/radio_save_spec.grc)) or run [calibration](radio/radio_calibrate/radio_calibrate.grc). 179 | 180 | Note: Adding a new radio script to be run by the daemon also involves adding a new RadioTask in [radio_task_starter.py](srt/daemon/radio_control/radio_task_starter.py) 181 | 182 | ### Adding Other Antenna Dish Motors 183 | 184 | For adding support for new antenna motors, there are several key files where the in-code documentation should be refered to in order to add to a motor. In 'srt/daemon/rotor_control/rotors.py' is the 'Rotor' class which creates a 'Motor' (from 'srt/daemon/rotor_control/motors.py') based on the string motor_type passed to it. 185 | Adding a new antenna motor therefore requires: 186 | - Adding a new motor class to [motors.py](srt/daemon/rotor_control/motors.py) 187 | - Making the string name for that motor create that motor in [rotors.py](srt/daemon/rotor_control/rotors.py) 188 | - Adding the string name as an valid option in the [YAML schema](config/schema.yaml) MOTOR_TYPE so the new type will be considered valid, such as: 189 | ``` 190 | MOTOR_TYPE: enum('ALFASPID', 'H180MOUNT', 'PUSHROD', 'NONE') 191 | ``` 192 | - Changing the MOTOR_TYPE in your own configuration YAML to the new motor type 193 | 194 | ## Further Documentation 195 | 196 | More documentation into the specific features of the Small Radio Telescope software are included in the docs directory. 197 | 198 | ## More Information 199 | 200 | Since many of the more subtle features of this software are based on the previous incarnations of the Small Radio Telescope code, more information about the SRT can be found throughout the many memos created about the previous version by the designer of the SRT. These can be found on the [Haystack Observatory](https://www.haystack.mit.edu/haystack-public-outreach/srt-the-small-radio-telescope-for-education/) website. 201 | 202 | Important Sections: 203 | - [SRT Wiki](https://wikis.mit.edu/confluence/display/SRT/SRT+Wiki+Home) 204 | - [SRT Memos](https://www.haystack.mit.edu/haystack-memo-series/srt-memos/) 205 | - [SRT 2013 Paper](https://www.haystack.mit.edu/wp-content/uploads/2020/07/srt_2013_HigginsonRollinsPaper.pdf) 206 | 207 | ## License 208 | BSD 3-Clause License 209 | 210 | ## Acknowledgments 211 | 212 | This work was supported by the National Science Foundation and Haystack Observatory. Additionally, this work wouldn't have been possible without John Swoboda, Ryan Volz, and Alan Rogers and their help and guidance throughout its development. 213 | -------------------------------------------------------------------------------- /bin/srt_controller.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """srt_controller.py 3 | 4 | Sends Instructions to the SRT via the Command Line 5 | 6 | """ 7 | 8 | import zmq 9 | import argparse 10 | import json 11 | from pathlib import Path 12 | from time import sleep 13 | 14 | 15 | def status(args): 16 | """Displays the Status of All or A Specific Part of the SRT Status JSON 17 | 18 | Parameters 19 | ---------- 20 | args 21 | argparse Arguments to Function 22 | 23 | Returns 24 | ------- 25 | None 26 | """ 27 | context = zmq.Context() 28 | socket = context.socket(zmq.SUB) 29 | socket.connect(f"tcp://{args.host}:%s" % args.port) 30 | socket.subscribe("") 31 | poller = zmq.Poller() 32 | poller.register(socket, zmq.POLLIN) 33 | socks = dict(poller.poll(1000)) 34 | if socket in socks and socks[socket] == zmq.POLLIN: 35 | rec = socket.recv() 36 | dump = json.loads(rec) 37 | if args.status_parameter in dump: 38 | dump = dump[args.status_parameter] 39 | print(json.dumps(dump, sort_keys=True, indent=4)) 40 | else: 41 | print("SRT Daemon Not Online") 42 | socket.close() 43 | 44 | 45 | def command(args): 46 | """Sends a Command to the SRT 47 | 48 | Parameters 49 | ---------- 50 | args 51 | argparse Arguments to Function 52 | 53 | Returns 54 | ------- 55 | None 56 | """ 57 | context = zmq.Context() 58 | socket = context.socket(zmq.PUSH) 59 | socket.connect(f"tcp://{args.host}:%s" % args.port) 60 | srt_command = "" 61 | for part in args.command: 62 | srt_command += part + " " 63 | print(f"Sending Command '{srt_command}'") 64 | socket.send_string(srt_command) 65 | sleep(0.1) 66 | socket.close() 67 | 68 | 69 | def command_file(args): 70 | """Sends All Commands in a Command File to the SRT 71 | 72 | Parameters 73 | ---------- 74 | args 75 | argparse Arguments to Function 76 | 77 | Returns 78 | ------- 79 | None 80 | """ 81 | context = zmq.Context() 82 | socket = context.socket(zmq.PUSH) 83 | socket.connect(f"tcp://{args.host}:%s" % args.port) 84 | with open(Path(args.command_file).expanduser(), "r") as cmd_file: 85 | for cmd_line in cmd_file: 86 | cmd_line = cmd_line.strip() 87 | socket.send_string(cmd_line) 88 | sleep(0.1) 89 | socket.close() 90 | 91 | 92 | if __name__ == "__main__": 93 | parser = argparse.ArgumentParser(prog="SRT Control Utility") 94 | sp = parser.add_subparsers() 95 | 96 | # Add the Status Parser 97 | sp_status = sp.add_parser( 98 | "status", help="Gets the Current Status of the SRT") 99 | sp_status.add_argument( 100 | "--status_parameter", 101 | metavar="status_parameter", 102 | type=str, 103 | help="The Key for a Piece of the SRT JSON Status", 104 | default="", 105 | ) 106 | sp_status.add_argument( 107 | "--host", 108 | metavar="host", 109 | type=str, 110 | help="The Host of the SRT Status Publisher", 111 | default="localhost", 112 | ) 113 | sp_status.add_argument( 114 | "--port", 115 | metavar="port", 116 | type=int, 117 | help="The Port of the SRT Status Publisher", 118 | default=5555, 119 | ) 120 | sp_status.set_defaults( 121 | func=status, 122 | ) 123 | 124 | sp_command = sp.add_parser("command", help="Sends a SRT Command") 125 | sp_command.add_argument( 126 | "command", 127 | nargs="*", 128 | metavar="command", 129 | type=str, 130 | help="The SRT Command to Execute", 131 | ) 132 | sp_command.add_argument( 133 | "--host", 134 | metavar="host", 135 | type=str, 136 | help="The Host of the SRT Command Queue", 137 | default="localhost", 138 | ) 139 | sp_command.add_argument( 140 | "--port", 141 | metavar="port", 142 | type=int, 143 | help="The Port of the SRT Command Queue", 144 | default=5556, 145 | ) 146 | sp_command.set_defaults(func=command) 147 | 148 | sp_command_file = sp.add_parser( 149 | "command_file", help="Sends a Command File to the SRT" 150 | ) 151 | sp_command_file.add_argument( 152 | "command_file", 153 | metavar="command_file", 154 | type=str, 155 | help="The SRT Command File to Execute", 156 | ) 157 | sp_command_file.add_argument( 158 | "--host", 159 | metavar="host", 160 | type=str, 161 | help="The Host of the SRT Command Queue", 162 | default="localhost", 163 | ) 164 | sp_command_file.add_argument( 165 | "--port", 166 | metavar="port", 167 | type=int, 168 | help="The Port of the SRT Command Queue", 169 | default=5556, 170 | ) 171 | sp_command_file.set_defaults(func=command_file) 172 | 173 | args = parser.parse_args() 174 | args.func(args) 175 | -------------------------------------------------------------------------------- /bin/srt_runner.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """srt_runner.py 3 | 4 | Starts the SRT Daemon and/or Dashboard 5 | 6 | """ 7 | 8 | import argparse 9 | from pathlib import Path 10 | from multiprocessing import Process 11 | 12 | from waitress import serve 13 | from srt import config_loader 14 | 15 | 16 | def run_srt_daemon(configuration_dir, configuration_dict): 17 | from srt.daemon import daemon as srt_d 18 | 19 | daemon = srt_d.SmallRadioTelescopeDaemon(configuration_dir, configuration_dict) 20 | daemon.srt_daemon_main() 21 | 22 | 23 | def run_srt_dashboard(configuration_dir, configuration_dict): 24 | from srt.dashboard import app as srt_app 25 | 26 | app_server, _ = srt_app.generate_app(configuration_dir, configuration_dict) 27 | serve( 28 | app_server, 29 | host=configuration_dict["DASHBOARD_HOST"], 30 | port=configuration_dict["DASHBOARD_PORT"], 31 | ) 32 | 33 | 34 | if __name__ == "__main__": 35 | # Create the parser 36 | my_parser = argparse.ArgumentParser(description="Runs the SRT Control Application") 37 | 38 | # Add the arguments 39 | my_parser.add_argument( 40 | "--config_dir", 41 | metavar="config_dir", 42 | type=str, 43 | help="The Path to the SRT Config Directory", 44 | default="~/.srt-config", 45 | ) 46 | my_parser.add_argument( 47 | "--config_file_name", 48 | metavar="config_file_name", 49 | type=str, 50 | help="The filename of the Config File to Load", 51 | default="config.yaml", 52 | ) 53 | 54 | my_parser.add_argument( 55 | "--dash_only", 56 | dest="dash_only", 57 | action="store_true", 58 | help="Load up the dashboard only", 59 | 60 | ) 61 | # Execute the parse_args() method 62 | args = my_parser.parse_args() 63 | 64 | # Create Path Objects 65 | config_dir_path = Path(args.config_dir) 66 | sky_coords_path = Path(config_dir_path, "sky_coords.csv") 67 | schema_path = Path(config_dir_path, "schema.yaml") 68 | config_path = Path(config_dir_path, args.config_file_name) 69 | 70 | dash_only = args.dash_only 71 | 72 | if not config_dir_path.is_dir(): 73 | print("Configuration Directory Does Not Exist") 74 | print("Please Refer to the Documentation for Creating a Config Directory") 75 | elif not sky_coords_path.is_file(): 76 | print("Sky Coordinates CSV Not Found") 77 | print("Please Refer to the Documentation for Creating a sky_coords.csv File") 78 | elif not schema_path.is_file(): 79 | print("YAML Configuration Schema File Not Found") 80 | print("Please Put a Copy of the Sample YAML Schema in your Config Directory") 81 | elif not config_path.is_file(): 82 | print("YAML Configuration File Not Found") 83 | print("Please Refer to the Documentation for Creating a config.yaml File") 84 | elif not config_loader.validate_yaml_schema(config_path, schema_path): 85 | print("YAML Configuration File Invalid") 86 | print("YAML did not validate against its schema file") 87 | elif dash_only: 88 | config_dict = config_loader.load_yaml(config_path) 89 | dashboard_process = Process( 90 | target=run_srt_dashboard, 91 | args=(args.config_dir, config_dict), 92 | name="SRT-Dashboard", 93 | ) 94 | dashboard_process.start() 95 | dashboard_process.join() 96 | else: 97 | config_dict = config_loader.load_yaml(config_path) 98 | 99 | daemon_process = Process( 100 | target=run_srt_daemon, 101 | args=(args.config_dir, config_dict), 102 | name="SRT-Daemon", 103 | ) 104 | daemon_process.start() 105 | 106 | if not config_dict["RUN_HEADLESS"]: 107 | dashboard_process = Process( 108 | target=run_srt_dashboard, 109 | args=(args.config_dir, config_dict), 110 | name="SRT-Dashboard", 111 | ) 112 | dashboard_process.start() 113 | dashboard_process.join() 114 | else: 115 | daemon_process.join() 116 | -------------------------------------------------------------------------------- /config-vsrt/calibration.json: -------------------------------------------------------------------------------- 1 | { 2 | "cal_pwr": 0.0029448782745724114, 3 | "cal_values": [ 4 | 0.4783603013532807, 5 | 0.45515598908895016, 6 | 0.44963990909856383, 7 | 0.44730654679185317, 8 | 0.44404472298008374, 9 | 0.44047663023500033, 10 | 0.4387396680551553, 11 | 0.4408437375828613, 12 | 0.4479965712329665, 13 | 0.4604827106352571, 14 | 0.47782285213302556, 15 | 0.4990414889162228, 16 | 0.5229416749294503, 17 | 0.5483339481318505, 18 | 0.5741979284388419, 19 | 0.5997745137945647, 20 | 0.6245974823885859, 21 | 0.6484783955439463, 22 | 0.6714599857093394, 23 | 0.6937521914969471, 24 | 0.7156627222294271, 25 | 0.7375312192119, 26 | 0.7596732212481659, 27 | 0.7823375272807023, 28 | 0.8056783480359886, 29 | 0.8297419199616626, 30 | 0.8544660188990402, 31 | 0.8796900265702664, 32 | 0.9051728035783794, 33 | 0.9306155394422276, 34 | 0.9556869062934554, 35 | 0.9800481675681109, 36 | 1.0033763223301624, 37 | 1.0253838451475872, 38 | 1.0458340669248676, 39 | 1.064551699798588, 40 | 1.0814284154213036, 41 | 1.096423725178961, 42 | 1.1095616755525242, 43 | 1.1209240599500052, 44 | 1.1306409632544536, 45 | 1.13887950350025, 46 | 1.1458316256496572, 47 | 1.1517017457552907, 48 | 1.1566949509656923, 49 | 1.1610063427025261, 50 | 1.1648119771769174, 51 | 1.1682617185476507, 52 | 1.1714741833231213, 53 | 1.1745338266874434, 54 | 1.177490107326721, 55 | 1.1803585706270703, 56 | 1.1831236130742093, 57 | 1.1857426342614126, 58 | 1.1881512470119437, 59 | 1.1902691997283061, 60 | 1.1920066664562412, 61 | 1.1932705770115315, 62 | 1.1939706892379431, 63 | 1.1940251451818842, 64 | 1.1933652998160622, 65 | 1.1919396620790466, 66 | 1.1897168407871634, 67 | 1.1866874400281167, 68 | 1.1828648978931335, 69 | 1.178285307135589, 70 | 1.1730062952286433, 71 | 1.1671050733789385, 72 | 1.1606757887647743, 73 | 1.1538263313730348, 74 | 1.1466747564001283, 75 | 1.1393454856210399, 76 | 1.1319654470139153, 77 | 1.124660302035224, 78 | 1.117550895187504, 79 | 1.110750041911202, 80 | 1.104359749409945, 81 | 1.098468941831292, 82 | 1.0931517372833695, 83 | 1.0884663004144568, 84 | 1.0844542715662964, 85 | 1.0811407525669816, 86 | 1.078534810661228, 87 | 1.0766304463529712, 88 | 1.0754079583849914, 89 | 1.074835629891303, 90 | 1.0748716539860297, 91 | 1.0754662146301543, 92 | 1.0765636393675162, 93 | 1.0781045441716681, 94 | 1.08002789684598, 95 | 1.082272933761358, 96 | 1.0847808747486134, 97 | 1.0874963922124135, 98 | 1.0903688025217912, 99 | 1.093352959990045, 100 | 1.0964098458412923, 101 | 1.0995068560666756, 102 | 1.102617802643226, 103 | 1.105722651922202, 104 | 1.1088070318530843, 105 | 1.1118615459218488, 106 | 1.1148809361418939, 107 | 1.1178631401031274, 108 | 1.1208082879819377, 109 | 1.1237176846226902, 110 | 1.1265928194522343, 111 | 1.1294344432589383, 112 | 1.1322417459689782, 113 | 1.1350116637235304, 114 | 1.137738337057143, 115 | 1.140412735063778, 116 | 1.143022453375816, 117 | 1.1455516868260984, 118 | 1.1479813710500901, 119 | 1.1502894812266342, 120 | 1.15245147083403, 121 | 1.154440828861603, 122 | 1.156229730476481, 123 | 1.1577897537727315, 124 | 1.1590926339573642, 125 | 1.1601110261479983, 126 | 1.1608192488262932, 127 | 1.1611939818315329, 128 | 1.1612148954821, 129 | 1.160865190845804, 130 | 1.1601320351901843, 131 | 1.1590068810641219, 132 | 1.157485662117661, 133 | 1.1555688634814454, 134 | 1.1532614691285732, 135 | 1.150572792967857, 136 | 1.147516204321595, 137 | 1.1441087617958527, 138 | 1.1403707722533825, 139 | 1.1363252935714967, 140 | 1.1319976010608725, 141 | 1.1274146378171643, 142 | 1.1226044688857377, 143 | 1.1175957579795996, 144 | 1.1124172836669957, 145 | 1.1070975095273492, 146 | 1.1016642198715554, 147 | 1.096144229360491, 148 | 1.0905631713706434, 149 | 1.0849453663908837, 150 | 1.0793137682334493, 151 | 1.0736899825447175, 152 | 1.068094349137396, 153 | 1.0625460771511892, 154 | 1.0570634200811126, 155 | 1.051663876366472, 156 | 1.046364400559091, 157 | 1.04118161010942, 158 | 1.036131973518331, 159 | 1.0312319669669898, 160 | 1.026498188496243, 161 | 1.0219474212742181, 162 | 1.0175966403577321, 163 | 1.0134629604926544, 164 | 1.0095635257698292, 165 | 1.0059153452073628, 166 | 1.0025350814152434, 167 | 0.9994388022660975, 168 | 0.9966417078078235, 169 | 0.9941578463870653, 170 | 0.9919998350056565, 171 | 0.9901785992305735, 172 | 0.9887031474777995, 173 | 0.9875803931823108, 174 | 0.9868150362770658, 175 | 0.9864095125975096, 176 | 0.9863640164054237, 177 | 0.9866765973220476, 178 | 0.9873433287411398, 179 | 0.988358540449387, 180 | 0.9897151039245325, 181 | 0.9914047548321064, 182 | 0.9934184338228822, 183 | 0.9957466240607965, 184 | 0.9983796621825565, 185 | 1.0013079987746833, 186 | 1.004522385082089, 187 | 1.008013964617007, 188 | 1.0117742516452437, 189 | 1.0157949831516455, 190 | 1.0200678367253964, 191 | 1.0245840136848503, 192 | 1.0293336944396296, 193 | 1.0343053812575966, 194 | 1.0394851519018704, 195 | 1.0448558556158143, 196 | 1.050396290218027, 197 | 1.0560804051620296, 198 | 1.0618765798616958, 199 | 1.0677470289519315, 200 | 1.073647386066871, 201 | 1.079526514871847, 202 | 1.0853265902817568, 203 | 1.0909834839611925, 204 | 1.09642747640548, 205 | 1.1015843033855544, 206 | 1.1063765277179067, 207 | 1.1107252087956898, 208 | 1.1145518228664097, 209 | 1.1177803676142597, 210 | 1.1203395662871432, 211 | 1.1221650706136992, 212 | 1.1232015493265246, 213 | 1.1234045415266312, 214 | 1.122741952580357, 215 | 1.121195075773722, 216 | 1.1187590363768127, 217 | 1.115442576560435, 218 | 1.111267129799866, 219 | 1.1062651715256417, 220 | 1.1004778777714082, 221 | 1.0939521736414914, 222 | 1.0867373062363015, 223 | 1.0788811289692941, 224 | 1.0704263322909164, 225 | 1.0614068953533533, 226 | 1.0518450593178588, 227 | 1.0417491310043465, 228 | 1.0311124107239347, 229 | 1.019913496195811, 230 | 1.0081181426068329, 231 | 0.9956827554423613, 232 | 0.9825594589045182, 233 | 0.9687025221014907, 234 | 0.9540757454268658, 235 | 0.9386602223543744, 236 | 0.9224617133817637, 237 | 0.9055167211358246, 238 | 0.8878962639923537, 239 | 0.8697063409830268, 240 | 0.8510841957052238, 241 | 0.8321897540899561, 242 | 0.8131920592925436, 243 | 0.7942511742882018, 244 | 0.7754968711226167, 245 | 0.757006443023346, 246 | 0.7387850898557319, 247 | 0.7207533974850265, 248 | 0.7027472274716954, 249 | 0.6845355015160489, 250 | 0.6658603844318398, 251 | 0.6465015014051133, 252 | 0.6263600536896656, 253 | 0.6055486299622853, 254 | 0.5844563096748554, 255 | 0.563733902276243, 256 | 0.5441076944216595, 257 | 0.5258778649226199, 258 | 0.5078846135551519, 259 | 0.48562452604166007 260 | ] 261 | } -------------------------------------------------------------------------------- /config-vsrt/config.yaml: -------------------------------------------------------------------------------- 1 | SOFTWARE: Very Small Radio Telescope 2 | EMERGENCY_CONTACT: 3 | name: John Doe 4 | email: test@example.com 5 | phone_number: 555-555-5555 6 | AZLIMITS: 7 | lower_bound: 38.0 8 | upper_bound: 355.0 9 | ELLIMITS: 10 | lower_bound: 0.0 11 | upper_bound: 89.0 12 | STOW_LOCATION: 13 | azimuth: 38.0 14 | elevation: 0.0 15 | CAL_LOCATION: 16 | azimuth: 120.0 17 | elevation: 7.0 18 | HORIZON_POINTS: 19 | - azimuth: 0 20 | elevation: 0 21 | - azimuth: 120 22 | elevation: 5 23 | - azimuth: 240 24 | elevation: 5 25 | - azimuth: 360 26 | elevation: 0 27 | MOTOR_TYPE: NONE 28 | MOTOR_PORT: /dev/ttyUSB0 29 | MOTOR_BAUDRATE: 600 30 | RADIO_CF: 1420000000 31 | RADIO_SF: 2400000 32 | RADIO_FREQ_CORR: -50000 33 | RADIO_NUM_BINS: 256 34 | RADIO_INTEG_CYCLES: 100000 35 | RADIO_AUTOSTART: Yes 36 | NUM_BEAMSWITCHES: 25 37 | BEAMWIDTH: 7.0 38 | TSYS: 171 39 | TCAL: 290 40 | SAVE_DIRECTORY: ~/Desktop/SRT-Saves 41 | RUN_HEADLESS: No 42 | DASHBOARD_PORT: 8080 43 | DASHBOARD_HOST: 0.0.0.0 44 | DASHBOARD_DOWNLOADS: Yes 45 | DASHBOARD_REFRESH_MS: 3000 -------------------------------------------------------------------------------- /config-vsrt/schema.yaml: -------------------------------------------------------------------------------- 1 | SOFTWARE: str() 2 | EMERGENCY_CONTACT: include('contact_info') 3 | AZLIMITS: include('limit') 4 | ELLIMITS: include('limit') 5 | STOW_LOCATION: include('az_el_point') 6 | CAL_LOCATION: include('az_el_point') 7 | HORIZON_POINTS: list(include('az_el_point'), min=0) 8 | MOTOR_TYPE: enum('ALFASPID', 'H180MOUNT', 'PUSHROD', 'NONE') 9 | MOTOR_BAUDRATE: int() 10 | MOTOR_PORT: str() 11 | RADIO_CF: int() 12 | RADIO_SF: int() 13 | RADIO_FREQ_CORR: int() 14 | RADIO_NUM_BINS: int() 15 | RADIO_INTEG_CYCLES: int() 16 | RADIO_AUTOSTART: bool() 17 | NUM_BEAMSWITCHES: int() 18 | BEAMWIDTH: num() 19 | TSYS: num() 20 | TCAL: num() 21 | SAVE_DIRECTORY: str() 22 | RUN_HEADLESS: bool() 23 | DASHBOARD_PORT: int() 24 | DASHBOARD_HOST: ip() 25 | DASHBOARD_DOWNLOADS: bool() 26 | DASHBOARD_REFRESH_MS: int() 27 | --- 28 | location: 29 | latitude: num() 30 | longitude: num() 31 | name: str() 32 | --- 33 | limit: 34 | lower_bound: num() 35 | upper_bound: num() 36 | --- 37 | az_el_point: 38 | azimuth: num() 39 | elevation: num() 40 | --- 41 | contact_info: 42 | name: str() 43 | email: str() 44 | phone_number: str() 45 | -------------------------------------------------------------------------------- /config-vsrt/sky_coords.csv: -------------------------------------------------------------------------------- 1 | coordinate_system,coordinate_a,coordinate_b,name 2 | fk4,05 31 30,21 58 00,Crab 3 | fk4,05 32 48,-5 27 00,Orion 4 | fk4,05 42 00,-1 00 00,S8 5 | fk4,23 21 12,58 44 00,Cass 6 | fk4,17 42 54,-28 50 00,SgrA 7 | fk4,06 29 12,04 57 00,Rosett 8 | fk4,18 17 30,-16 18 00,M17 9 | fk4,20 27 00,41 00 00,CygEMN 10 | fk4,21 12 00,48 00 00,G90 11 | fk4,05 40 00,29 00 00,G180 12 | fk4,12 48 00,28 00 00,GNpole 13 | fk4,00 39 00,40 30 00,Androm 14 | fk4,05 14 12,18 44 00,AC1 15 | fk4,03 29 00,54 00 00,PULSAR 16 | fk4,08 30 00,-45 00 00,PS 17 | galactic,10,1,RC_CLOUD 18 | galactic,00,0,G00 19 | galactic,10,0,G10 20 | galactic,20,0,G20 21 | galactic,30,0,G30 22 | galactic,40,0,G40 23 | galactic,50,0,G50 24 | galactic,60,0,G60 25 | galactic,70,0,G70 26 | galactic,80,0,G80 27 | galactic,90,0,G90 28 | galactic,100,0,G100 29 | galactic,110,0,G110 30 | galactic,120,0,G120 31 | galactic,130,0,G130 32 | galactic,140,0,G140 33 | galactic,150,0,G150 34 | galactic,160,0,G160 35 | galactic,170,0,G170 36 | galactic,180,0,G180 37 | galactic,190,0,G190 38 | galactic,200,0,G200 39 | galactic,210,0,G210 40 | galactic,220,0,G220 41 | galactic,230,0,G230 42 | galactic,240,0,G240 43 | galactic,250,0,G250 44 | galactic,260,0,G260 45 | galactic,270,0,G270 46 | galactic,280,0,G280 47 | galactic,290,0,G290 48 | galactic,300,0,G300 49 | galactic,310,0,G310 50 | galactic,320,0,G320 51 | galactic,330,0,G330 52 | galactic,340,0,G340 53 | galactic,350,0,G350 -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | calibration.json -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | SOFTWARE: Small Radio Telescope 2 | STATION: 3 | latitude: 42.5 4 | longitude: -71.5 5 | name: Haystack 6 | EMERGENCY_CONTACT: 7 | name: John Doe 8 | email: test@example.com 9 | phone_number: 555-555-5555 10 | AZLIMITS: 11 | lower_bound: 38.0 12 | upper_bound: 355.0 13 | ELLIMITS: 14 | lower_bound: 0.0 15 | upper_bound: 89.0 16 | STOW_LOCATION: 17 | azimuth: 38.0 18 | elevation: 0.0 19 | CAL_LOCATION: 20 | azimuth: 120.0 21 | elevation: 7.0 22 | HORIZON_POINTS: 23 | - azimuth: 0 24 | elevation: 0 25 | - azimuth: 120 26 | elevation: 5 27 | - azimuth: 240 28 | elevation: 5 29 | - azimuth: 360 30 | elevation: 0 31 | MOTOR_TYPE: NONE 32 | MOTOR_PORT: /dev/ttyUSB0 33 | MOTOR_BAUDRATE: 600 34 | RADIO_CF: 1420000000 35 | RADIO_SF: 2400000 36 | RADIO_FREQ_CORR: -50000 37 | RADIO_NUM_BINS: 256 38 | RADIO_INTEG_CYCLES: 100000 39 | RADIO_AUTOSTART: Yes 40 | NUM_BEAMSWITCHES: 25 41 | BEAMWIDTH: 7.0 42 | TSYS: 171 43 | TCAL: 290 44 | SAVE_DIRECTORY: ~/Desktop/SRT-Saves 45 | RUN_HEADLESS: No 46 | DASHBOARD_PORT: 8080 47 | DASHBOARD_HOST: 0.0.0.0 48 | DASHBOARD_DOWNLOADS: Yes 49 | DASHBOARD_REFRESH_MS: 3000 50 | -------------------------------------------------------------------------------- /config/schema.yaml: -------------------------------------------------------------------------------- 1 | SOFTWARE: str() 2 | STATION: include('location') 3 | EMERGENCY_CONTACT: include('contact_info') 4 | AZLIMITS: include('limit') 5 | ELLIMITS: include('limit') 6 | STOW_LOCATION: include('az_el_point') 7 | CAL_LOCATION: include('az_el_point') 8 | HORIZON_POINTS: list(include('az_el_point'), min=0) 9 | MOTOR_TYPE: enum('ALFASPID', 'H180MOUNT', 'PUSHROD', 'NONE') 10 | MOTOR_BAUDRATE: int() 11 | MOTOR_PORT: str() 12 | RADIO_CF: int() 13 | RADIO_SF: int() 14 | RADIO_FREQ_CORR: int() 15 | RADIO_NUM_BINS: int() 16 | RADIO_INTEG_CYCLES: int() 17 | RADIO_AUTOSTART: bool() 18 | NUM_BEAMSWITCHES: int() 19 | BEAMWIDTH: num() 20 | TSYS: num() 21 | TCAL: num() 22 | SAVE_DIRECTORY: str() 23 | RUN_HEADLESS: bool() 24 | DASHBOARD_PORT: int() 25 | DASHBOARD_HOST: ip() 26 | DASHBOARD_DOWNLOADS: bool() 27 | DASHBOARD_REFRESH_MS: int() 28 | --- 29 | location: 30 | latitude: num() 31 | longitude: num() 32 | name: str() 33 | --- 34 | limit: 35 | lower_bound: num() 36 | upper_bound: num() 37 | --- 38 | az_el_point: 39 | azimuth: num() 40 | elevation: num() 41 | --- 42 | contact_info: 43 | name: str() 44 | email: str() 45 | phone_number: str() 46 | -------------------------------------------------------------------------------- /config/sky_coords.csv: -------------------------------------------------------------------------------- 1 | coordinate_system,coordinate_a,coordinate_b,name 2 | fk4,05 31 30,21 58 00,Crab 3 | fk4,05 32 48,-5 27 00,Orion 4 | fk4,05 42 00,-1 00 00,S8 5 | fk4,23 21 12,58 44 00,Cass 6 | fk4,17 42 54,-28 50 00,SgrA 7 | fk4,06 29 12,04 57 00,Rosett 8 | fk4,18 17 30,-16 18 00,M17 9 | fk4,20 27 00,41 00 00,CygEMN 10 | fk4,21 12 00,48 00 00,G90 11 | fk4,05 40 00,29 00 00,G180 12 | fk4,12 48 00,28 00 00,GNpole 13 | fk4,00 39 00,40 30 00,Androm 14 | fk4,05 14 12,18 44 00,AC1 15 | fk4,03 29 00,54 00 00,PULSAR 16 | fk4,08 30 00,-45 00 00,PS 17 | galactic,10,1,RC_CLOUD 18 | galactic,00,0,G00 19 | galactic,10,0,G10 20 | galactic,20,0,G20 21 | galactic,30,0,G30 22 | galactic,40,0,G40 23 | galactic,50,0,G50 24 | galactic,60,0,G60 25 | galactic,70,0,G70 26 | galactic,80,0,G80 27 | galactic,90,0,G90 28 | galactic,100,0,G100 29 | galactic,110,0,G110 30 | galactic,120,0,G120 31 | galactic,130,0,G130 32 | galactic,140,0,G140 33 | galactic,150,0,G150 34 | galactic,160,0,G160 35 | galactic,170,0,G170 36 | galactic,180,0,G180 37 | galactic,190,0,G190 38 | galactic,200,0,G200 39 | galactic,210,0,G210 40 | galactic,220,0,G220 41 | galactic,230,0,G230 42 | galactic,240,0,G240 43 | galactic,250,0,G250 44 | galactic,260,0,G260 45 | galactic,270,0,G270 46 | galactic,280,0,G280 47 | galactic,290,0,G290 48 | galactic,300,0,G300 49 | galactic,310,0,G310 50 | galactic,320,0,G320 51 | galactic,330,0,G330 52 | galactic,340,0,G340 53 | galactic,350,0,G350 -------------------------------------------------------------------------------- /docs/2020_Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/2020_Report.pdf -------------------------------------------------------------------------------- /docs/command_files.md: -------------------------------------------------------------------------------- 1 | ## Small Radio Telescope Docs 2 | #### Commands / Command File Syntax 3 | 4 | Note: The SRT 2020 software uses a command syntax heavily influenced by the command file syntax of the previous generation's software, which is well documented in [SRT Memo 17](https://www.haystack.mit.edu/wp-content/uploads/2020/07/memo_SRT_017.pdf). Additionally, the old syntax preceded every command with a ':' character, which is still valid for backwards compatibility. For the most part, old SRT commands and command files should still be valid. 5 | 6 | The SRT software accepts commands in order to change settings at runtime as well as control the running operations. All commands are funneled into a command queue, which will execute them in order of being received. Parameters for a command should be separated by spaces. Most commands are not case sensitive and do not care about excess whitespace. 7 | 8 | | Command | Parameters | Notes |Info | 9 | |--------------|------------|-------|--------------------------------------------| 10 | | * | Any text | 1 | Makes Line a Comment | 11 | | stow | None | | Sends the Antenna to Stow | 12 | | cal | None | | Sends the Antenna to Calibration Position | 13 | | calibrate | None | 2 | Saves Current Spec as Calibration Data | 14 | | quit | None | 3 | Stows and Gracefully Closes Daemon | 15 | | record | [filename] | 4 | Starts Saving into File of Name 'filename' | 16 | | roff | None | | Ends Current Recording if Applicable | 17 | | azel | [az] [el] | | Points at Azimuth 'az', Elevation 'el' | 18 | | offset | [az] [el] | | Offsets from Current Object by 'az', 'el' | 19 | | freq | [cf] | | Sets Center Frequency in MHz to 'cf' | 20 | | samp | [sf] | | Sets Sampling Frequency in MHz to 'sf' | 21 | | wait | [time] | | Stops Execution and Waits for 'time' Secs. | 22 | | [time] | None | | Waits for 'time' Seconds | 23 | | LST:hh:mm:ss | None | | Waits Until Next Time hh:mm:ss in UTC | 24 | | Y:D:H:M:S | None | | Waits Until Year:DayOfYear:Hour:Minute:Sec | 25 | | [name] | [n] or [b] | 5 | Points Antenna at Object named 'name' | 26 | 27 | Additional Notes: 28 | 1. Only is considered a comment if the line starts with '\*'. 29 | 2. Calibration takes samples at its current location (which should typically be 'cal') and uses those to determine the shape of the band-pass filter and eliminate it from the calibrated spectra. Calibration should therefore by done against a non-source object of known noise temperature. 30 | 3. Unlike the previous SRT software, 'quit' both stows and quits (ends the daemon process). After this is done, it will be necessary to restart the daemon process from command line or via the Dashboard 'Start Daemon' button. 31 | 4. Currently, three different file types are supported, and the type used is determined by the file extension of the name given. FITS (Calibrated Spectra) is used when the file ends in '.fits', rad (Calibrated Spectra) is used when it ends in '.rad', and Digital RF (Raw I/Q Samples) is used when the name lacks a file ending. If no filename is provided, Digital RF will be used with an auto-generated name. In order to use rad or FITS with an autogenerated name, use "\*.rad" or "\*.fits" respectively. 32 | 5. The names used for pointing at objects are set by the 'sky_coords.csv' file, which is further documented in the config folder portion of the docs. By default, 'Sun' and 'Moon' are already loaded. 33 | 34 | ##### Building Command Files 35 | 36 | Building a command file simply involves putting commands, in order of execution, in a text file. Only one command can be on a line, as any additional text past the command will be ignored (excluding commands with parameters, which may look past the command statement for additional information). An example command file is available in the examples/ directory. 37 | -------------------------------------------------------------------------------- /docs/config_directory.md: -------------------------------------------------------------------------------- 1 | ## Small Radio Telescope Docs 2 | #### Config Directory Details 3 | 4 | The configuration folder for the SRT contains all of the important settings that allow the SRT to operate in different modes and with different default values. This is split across several different files, including: 5 | 6 | * 'config.yaml' - The main configuration file for the SRT, containing all of the key settings 7 | * 'sky_coords.csv' - A CSV file listing all celestial objects that the user would like to be able to track automatically 8 | 9 | As well as the below, which the user should not typically have to modify: 10 | 11 | * 'schema.yaml' - The schema for config.yaml, which lists the valid range of options in config.yaml 12 | * 'calibration.json' - A JSON containing the calibration data from the most recent time the calibrate command was running 13 | 14 | If the user wants to add configuration options within these files they must update schema.yaml and config.yaml and make sure they are in the same directory together when calling srt_runner.py. 15 | ##### config.yaml 16 | 17 | The config.yaml file contains the following settings: 18 | 19 | * STATION - The latitude, longitude, and name of the location of the SRT. 20 | ```YAML 21 | STATION: 22 | latitude: 42.5 23 | longitude: -71.5 24 | name: Haystack 25 | ``` 26 | 27 | * EMERGENCY_CONTACT - The emergency contact info that will show up on the SRT Dashboard in case anything happens with the SRT 28 | ```YAML 29 | EMERGENCY_CONTACT: 30 | name: John Doe 31 | email: test@example.com 32 | phone_number: 555-555-5555 33 | ``` 34 | 35 | * AZLIMITS - The range of movement of the SRT's motor controller along the azimuth, given clockwise in degrees 36 | ```YAML 37 | AZLIMITS: 38 | lower_bound: 38.0 39 | upper_bound: 355.0 40 | ``` 41 | 42 | * ELLIMITS - The range of movement of the SRT's motor controller in elevation, given in degrees 43 | ```YAML 44 | ELLIMITS: 45 | lower_bound: 0.0 46 | upper_bound: 89.0 47 | ``` 48 | 49 | * STOW_LOCATION - The azimuth and elevation that the 'stow' command should return the SRT to 50 | ```YAML 51 | STOW_LOCATION: 52 | azimuth: 38.0 53 | elevation: 0.0 54 | ``` 55 | 56 | * CAL_LOCATION - The azimuth and elevation for calibration of the SRT to be performed against. For example, for the SRT at Haystack Observatory, this points to a thick cluster of trees so that the sky cannot be seen and noise can be evaluated comparatively. 57 | ```YAML 58 | CAL_LOCATION: 59 | azimuth: 120.0 60 | elevation: 7.0 61 | ``` 62 | 63 | * MOTOR_TYPE - The type of motor being used. Several different types are currently allowed, include NONE (which works for either a fixed antenna or simulating on a system without an antenna), ALFASPID (which works with any ROT2 protocol supporting motor), H180MOUNT (which works with the H180 motor on some SRTs), and PUSHROD (which works with the old custom pushrod system of the SRT). Currently, since the SRT at Haystack uses a ALFASPID motor, that is the only one which has currently been extensively tested. Additionally, please refer to the in-code documentation in srt/daemon/rotor_control for more information on adding support for more motors. 64 | ```YAML 65 | MOTOR_TYPE: NONE 66 | ``` 67 | 68 | * MOTOR_PORT - The location of the motor on the host system. For example, on a Unix system this will probably be some device like '/dev/ttyUSB0', on Mac is will be something like '/dev/tty.usbserial-A602P777' and on Windows this will be a COM port like 'COM3'. This is not used if MOTOR_TYPE is NONE. 69 | ```YAML 70 | MOTOR_PORT: /dev/ttyUSB0 71 | ``` 72 | 73 | * MOTOR_BAUDRATE - The baudrate for the serial connection to the motor controller. The ALFASPID motor baudrate can vary depending on the specific model, the ROT2PROG is 600, while the MD-01/MD-02 default setting is 460800. This can be changed and see the instruction manual to learn how to set and check this value. The H180MOUNT is 2400 and the PUSHROD is 2000. This is not used if MOTOR_TYPE is NONE. 74 | ```YAML 75 | MOTOR_BAUDRATE: 600 76 | ``` 77 | 78 | * RADIO_CF - The default center frequency of the SRT in Hz. The center frequency of the SRT can be changed during run-time, but this is the default and initial value on startup. 79 | ```YAML 80 | RADIO_CF: 1420000000 81 | ``` 82 | 83 | * RADIO_SF - The sample frequency of the SRT in Hz. Since SDRs typically take both an I and Q sample at this rate, the sample frequency is conveniently also the effective bandwidth. This can be changed during run-time, but this is the default and initial value on startup. 84 | ```YAML 85 | RADIO_SF: 2000000 86 | ``` 87 | 88 | * RADIO_NUM_BINS - The number of bins that the FFT will output. More bins means a more precise spectrum, but at a higher computational cost. 89 | ```YAML 90 | RADIO_NUM_BINS: 4096 91 | ``` 92 | 93 | * RADIO_INTEG_CYCLES - The number of FFT output arrays to average over before saving or displaying the result. Higher values means a more accurate spectrum, but less frequently updating. Note that the integration time can be calculated using RADIO_NUM_BINS * RADIO_INTEG_CYCLES / RADIO_SF. 94 | ```YAML 95 | RADIO_INTEG_CYCLES: 1000 96 | ``` 97 | 98 | * RADIO_AUTOSTART - Whether to automatically start the GNU Radio script that performs the FFT and integration when the program starts. Keep this True for default behavior, but if a custom radio processing script is desired, make this false and run your own following the input and outputs used in radio/radio_processing to make sure all the data gets to the right places 99 | ```YAML 100 | RADIO_AUTOSTART: Yes 101 | ``` 102 | 103 | * BEAMWIDTH - The half-power beamwidth of the antenna being used, in degrees 104 | ```YAML 105 | BEAMWIDTH: 7.0 106 | ``` 107 | 108 | * TSYS - The estimated system temperature of the radio dish and its path to the receiver, in Kelvin 109 | ```YAML 110 | TSYS: 171 111 | ``` 112 | 113 | * TCAL - The temperature of the calibration source (~300K for a typical object on earth), in Kelvin 114 | ```YAML 115 | TCAL: 290 116 | ``` 117 | 118 | * SAVE_DIRECTORY - The folder to save any recordings the SRT will produce 119 | ```YAML 120 | SAVE_DIRECTORY: ~/Desktop/SRT-Saves 121 | ``` 122 | 123 | * DASHBOARD_REFRESH_MS - The number of milliseconds for dashboard refresh. 124 | ```YAML 125 | DASHBOARD_REFRESH_MS: 3000 126 | ``` 127 | ##### sky_coords.csv 128 | 129 | The sky_coords data file is organized into four columns, with a row for each entry. 130 | * The first column is the coordinate system of the celestial object, which supports any coordinate system name recognized by AstroPy, and has been tested with 'fk4' and 'galactic'. 131 | * The next two columns are the first and second coordinate of the object, such as ra and dec for fk4 and l and b for galactic. 132 | * The last column is the name of the object. 133 | 134 | All points listed here will show up as points on the Dashboard, and the SRT will be able to track their movements. Additionally, their names will become keywords in command files, so to point at a object given the name Orion, the command would just be 'Orion'. 135 | 136 | Below is an example excerpt of a sky_coords.csv file: 137 | ```CSV 138 | coordinate_system,coordinate_a,coordinate_b,name 139 | fk4,05 31 30,21 58 00,Crab 140 | fk4,05 32 48,-5 27 00,Orion 141 | fk4,05 42 00,-1 00 00,S8 142 | galactic,10,1,RC_CLOUD 143 | galactic,00,0,G00 144 | ``` 145 | -------------------------------------------------------------------------------- /docs/diagrams/SRT-BlockDiagram_ConceptA.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/diagrams/SRT-BlockDiagram_ConceptA.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/diagrams/SRT-BlockDiagram_ConceptA.pdf -------------------------------------------------------------------------------- /docs/diagrams/SRT-BlockDiagram_ConceptB.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/diagrams/SRT-BlockDiagram_ConceptB.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/diagrams/SRT-BlockDiagram_ConceptB.pdf -------------------------------------------------------------------------------- /docs/diagrams/SRT-BlockDiagram_Current.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/diagrams/SRT-BlockDiagram_Current.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/diagrams/SRT-BlockDiagram_Current.pdf -------------------------------------------------------------------------------- /docs/diagrams/SRT-BlockDiagram_Final.drawio: -------------------------------------------------------------------------------- 1 | 7V1bc9q6Fv41mWkfwtgSvj3mtpvunZ6dht3T9rycEViAW2NR2SShv35L+IItL8CADGSSdCbBy0KYtT6tu9QzfDV5/sDJdPyJ+TQ8Q4b/fIavzxBybVf8loR5SrBMIyWMeOCnJHNJ6AW/aUbMh80Cn8aVgQljYRJMq8QBiyI6SCo0wjl7qg4bsrD6qVMyojVCb0DCOvVr4Cfj7GtZxpJ+S4PROP9k08juTEg+OCPEY+KzpxIJ35zhK85Ykr6aPF/RUPIu50v6vj9W3C0ejNMoafKGn+cM3Tw89ak7/zC/G57/9ct7PM9meSThLPvC2cMm85wDnM0in8pJjDN8+TQOEtqbkoG8+yRELmjjZBKKK1O89Ek8XoyVF/UnzD+O8oQ+l0jZE3+gbEITPhdDsrvdjHkZetzs8mkpCsfJaOOSGCwvI5JM/KNi5iWHxIuMSVswDL0shjmoIcew2RbHcI1jtyxOBKUnVnaNeeJrJlUOxQlnP+kVCxkXlIhFYuTlMAhDhUTCYBSJy4FgHhX0S8m0QKzki+zGJPB9+TGgSKpCq0hlyKIkU0yoq0dK2KqKyapLybQAKSENQjLDH39d9P8hf97+f/49/Bh9vL748zyX/jpcU19oxuyS8WTMRiwi4c2SqjBxOeaOsWnGzB80SeYZN8ksYVVZr2RtzGZ8QNegzMpsA+EjmqwZZ6fj5HdZKyhOQ5IEj1UrALE9e+s9C6JkKWDTrgq4qy6v9EGzdy2Fd8E5mZeGTeWAeM3nONXPwZ5iBLYbL16kT7BEUsGT3TWABWDLDpNsaVVAZv+asfzGebyAyYUYYHanz8ub4tUo+7uYpc9VinjMdOqcrE1Fq8pA3rwnidA40WIEMrqKejpDeDgcosGgpsvEHd/u25atR6nYTseqgs6rqxWMALVSoFW7XsGnoVcEU/n8m7g4NzqGgXLKdzljByMvJ1w/Z5+RXs3LV/eUB4Ip0rQsiLvrKruuq0DmHUhX5Togh03hZWnWVarRQxZeq6s2jG9HV9mvXVf5hLpDUFfZA5f2h3p0lQo5o66pUGuaCo6EIE21neQFTwDJX7HJhES+uP95RsXUDcV+go7wCvzod48dpFgyDFgyF4BHe+iAAuUt0QHqhWvB0z4jXOLjK+M/KY/fELIZIabhdpwqRixIh+DCJ9IdRsEw0Z0eaIF1nuomWrjZ4nJa41p3f6NrQYvrE0sE7JFxJQZzFoaU183wu979RzGveHLj1pRfW74SN8lEiiL9LSj3s3gs/jww//2BTLkqejWmsOQ/0E4vfrIZSvT0R9P6M6sGHNDPJmS/VddSH4Z0BZmpa2+EQUTP8yeXd6VpseoQ610/AJj6b0DEW3qMTOeLAcZd0OeEz9+wI430qWEH1VOUNfbTyL+Q5QRpNUMSx0HqIhOe1MlrBEGfg0TGoUbHyq6+ZyPl62X4KS8q0Wf6Jmx3y7GrjGZREd8eJnjNlbW2TFtJ7FDKM6ftGeR2jSroavnu9IvXgtz6RGpUqvqcK6JlXQEq2l/RwZ5o7+Efce+a0AmL3lzQJiZQdaSKomVZk9mH9D+Rp8kKquC4mY7pROiQWHpTJByAdkxxmZbeF+d0kAQsahzabGXsdMhS0Q5FGa0kSQ9KR+iomsGJU0iSR0ic5vaqZK2Wtgu2V7vbFtxtaDP2zXi6bkXclrogt854rjYXnqeGW2ZB2WB7dJkMXI+v/ghCGs/jhE7adTTdAYULH33XkpZUy+q16skAqPQBOpWtZYyw05IqvqaPgZCGCC+mdBAMhce5CHCHwehUtSsyFDt5SO0KCqcLefxbCseGhPNAnqRkhA0U62v3YK/uVzX3lzgVD0n6i6mkfDMlJea1Ls+sazmXUPPpFwHXMGcJkdZaXHuaVqjtKssTQEAh7TIEuq0hQEPOCUYAHTDuB9Fojzzuyxa2ZaOqsIHEbOFwHUTYFtrsTB07LWsatbwssEgOmpe1GuRFjs62WhRmAc1Uh2WbBtUCWv57zgY0jgW3ZZyeyBTjdRBPw4VPWo+/enRRdczqjzJyE979thH+wX0F3KQ8AdqKQqfolyiQc7Euv8SynmBcyiZjyt/F76XCrZn8r7S/YHpR5mvT3T5qXtdVq7YuoPixAeRDil5H/ZKD+jl0SqBJ7umUhNRVG4TzwGmTjFB7q2v/pBWc0cxnCYq6zN9T6euQ8H1J/QXq8OXavWPC5xLTZ4pXOHVvidEGEKu1b0AgK7y9g0Tjdr1/48N/vgjCA/EDaRV7Ax5MZS4nL9WdnKfTVVqKXahpHLcVRsNJys3Kdce6WanmZZ2VKl6bil0r+byxXJUXkE+kXmUrjdo19du0XlW4T9lEZsNc5w7JRxAj+ccdO5G9AlDGroAqJ7PXde+WgTe9MkL66/bH58/kdjqboQt28y3fgqU5571tc67tKSlxvKbZFgAZVt6N1Bx3yzB769u1qOt3IbfSRX1sa9pjYKnV7+M37iK3JdFf0yGZhelOtVFUcwRFfJ3MpqcaR9uO3UGb0+6uWYw6jMuwf1UE9vUr3tybj75xHde6QMHmBQgbre1BBBeyfndyEUkvHcqOgQqHQEsH1Yk4kAh3OwZ2u7ZpLX53qxba3dWfRNjuGJa3/KnCyM6znwey+3j/xny4jvPt093DvewFfleEiMLBlHWd97srmAMWdjQoCawaEGAzOWqrkANLe3XzWzwl0e7m4yoMBJ9kDjffl16IOJ34pG2IFll71XUMNVB4qB1z8Ov+9gv++sN5DOzz299X3bjPfjc5aeGwcSN6FYFjPbYz7QoysI13sxxFl9XWQeLWway9RTC7gyUC8dokGXZQvDqvE6+20svZVTVUY7zah8Krg46AVw0ZE9Bxkm1pCScvwklqo9XJcarwA6Ju6AAeHT4TKGcNfYignK+EnPqcJIt2hNcudKNBfxsU7OmQ+jplW5L6/yhnnz7XRKGz/4CavkUdKAfp2Q4myxxkjbMA/1czu94i4kC5i5bqiyC7NfSTgnFJ0U1Yz0OnHUCtZR/3EpGlHjMEpImhjl8dfSCgfNpqycqVICihVCnOJi9FSk37sluTklVjySG3uGyxJRP0ZHfznjPzvPEwIAcWZWNneS/BQD5Ek8ysjsBkKR+hR8oS6hgmXisleQGnbbVIDoh71my/OVojgRoOqX2vTcMh29oQV+nL9IIY3LU6sOduthJuPc+tALdju+4m8K4uHWyLwXVu3kYMWseEoKVA0Fa93sYRuXpspnrMa8sQbLInsykEMy247QkAluuVMShPADDR4UDY9Dy70wNhsQcgV19qLNAUhFhpqGp6tKcuELZ2UFlPhMUz2QH5ZeqLsP61nEO1lwuNbPOIR5XB+GhwpvduhtIx7K39rUJ1mWcV05nNdRittZf7Zh9Ta3WVs3Lwrt6b2j2MD+y9gSej6UxDyoRevi3qBSQhNegeeRhQJXwHTscHG8LbSzpCaZadHaTd/KNtQkvg8P3NquVogZxnK6bGxrv60Z5qtYodGrorsV61GuJk2Fv9ZGjd+L0rYTBs610lfyfjxea+vPfnQro0EZvI737P2YiTSVyDtoYdIvslxpU4q9UdIuJy+X+XpJJY/gcw+OZf -------------------------------------------------------------------------------- /docs/diagrams/SRT-BlockDiagram_Final.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/diagrams/SRT-BlockDiagram_Final.pdf -------------------------------------------------------------------------------- /docs/images/monitor_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/images/monitor_page.png -------------------------------------------------------------------------------- /docs/images/radio_calibrate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/images/radio_calibrate.png -------------------------------------------------------------------------------- /docs/images/radio_process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/images/radio_process.png -------------------------------------------------------------------------------- /docs/images/radio_save_raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/images/radio_save_raw.png -------------------------------------------------------------------------------- /docs/images/radio_save_spec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/images/radio_save_spec.png -------------------------------------------------------------------------------- /docs/images/radio_save_spec_fits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/images/radio_save_spec_fits.png -------------------------------------------------------------------------------- /docs/images/srt_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/images/srt_image.jpg -------------------------------------------------------------------------------- /docs/images/system_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/docs/images/system_page.png -------------------------------------------------------------------------------- /docs/port_usage.md: -------------------------------------------------------------------------------- 1 | ## Small Radio Telescope Docs 2 | #### Internal Port Mapping 3 | 4 | The SRT control software primarily uses sockets in order to communicate between its various different components. 5 | This gives the user the ability to be able to remotely control or access data (depending on the purpose of the port), just by opening the specific port on the host system. 6 | Excluding the Dashboard Webpage and XMLRPC, all of the ports use the ZeroMQ socket library, and are capable of accepting from multiple simultaneous sources or multiple recipients depending on the purpose of the port. 7 | For instance, if the user were to open up port 5558, anyone could subscribe to the ZMQ publisher socket and receive a live stream copy of the raw I/Q samples from the SDR along with GNU Radio metadata data tags. Similarly, opening up port 5559 would let anyone subscribe for a copy of the raw I/Q samples that don't contain any GNU Radio metadata, which means the received bytes can just be cast into their appropriate complex float datatype without the need for GNU Radio to parse it. 8 | 9 | Quick descriptions of each port, its intended purpose in the system, and the original source and destination of the data are listed below: 10 | 11 | 12 | | Purpose | Port | From | To | 13 | |---------------------------|-------|-----------|-----------------| 14 | | Status Updates | 5555 | Daemon | Dashboard | 15 | | Command Queue | 5556 | Dashboard | Daemon | 16 | | GNU Radio XMLRPC | 5557 | Daemon | GNU Radio | 17 | | Raw I/Q Data w. Tags | 5558 | GNU Radio | Daemon | 18 | | Raw I/Q Data w/o Tags | 5559 | GNU Radio | Dashboard | 19 | | Raw Spec. w. Tags | 5560 | GNU Radio | User (Opt.) | 20 | | Raw Spec. w/o Tags | 5561 | GNU Radio | Dashboard | 21 | | Calibrated Spec. w. Tags | 5562 | GNU Radio | Daemon | 22 | | Calibrated Spec. w/o Tags | 5563 | GNU Radio | Dashboard | 23 | | Dashboard Webpage | 8080 | Dashboard | User | 24 | 25 | Note: User (Opt.) indicates a port where there is no intended destination unless the user optionally wishes to listen to that port with a GNU Radio ZMQ Sub Source. 26 | -------------------------------------------------------------------------------- /docs/radio_processing.md: -------------------------------------------------------------------------------- 1 | ## Small Radio Telescope Docs 2 | #### Radio Processing Details 3 | 4 | Since the SRT Software uses GNU Radio for all of its signal processing, there are several scripts which handle different functions. These scripts, and how they are interconnected, are listed below: 5 | 6 | ##### Radio Process (radio_process.grc) 7 | 8 | Radio Process handles the central portion of the signal processing, taking samples from their source, adding metadata tags to the stream, removing any DC offset, performing a Weighed Overlap Add ([See Here for More Info](https://astropeiler.de/sites/default/files/EUCARA2018_Dwingeloo_goes_SDR.pdf)), taking its FFT, averaging over some number of spectra, and re-scaling each bin based on its calibration value. Throughout this process, different ZeroMQ Publish blocks are placed to give any number of recipients access to the live data stream at specific points, such as at the raw sample level, the spectra, and the calibrated spectra. The radio process script also contains a '[XMLRPC](https://wiki.gnuradio.org/index.php/XMLRPC_Server)' block, which allows for other processes to change many of the variables in the script at any time. The Daemon uses this in order to send new metadata values whenever appropriate (i.e. motor changes position). The ports used by these are better documented in [port_usage](port_usage.md). 9 | 10 | The radio processing script can be automatically started when the daemon starts or have to be run independently, depending on the "RADIO_AUTOSTART" value in the YAML config. Additionally, this script can be entirely replaced by another GNU Radio script doing an entirely different approach to processing assuming it outputs its data on the same ports and has the same metadata variables. 11 | 12 | Note: For SDR sources that have external timing (i.e. GPS), please remove the 'Add Clock Tags' block, since it adds computer clock timing tags as the RTL-SDR used at Haystack does not give any sample timing itself. 13 | 14 | ![Radio Process](images/radio_process.png) 15 | 16 | ##### Radio Calibrate (radio_calibrate.grc) 17 | 18 | Radio Calibrate takes data from the tagged uncalibrated spectra stream, and does a high-order polynomial fit to create a smooth calibration curve. It saves the points from this curve and a scaling value from SDR units to Kelvin into a 'calibration.json' file. This Daemon only depends on the calibration outputting a JSON, so the source and process for determining calibration values can be completely re-written depending on your circumstances and approach. 19 | 20 | ![Radio Calibrate](images/radio_calibrate.png) 21 | 22 | ##### Radio Save Raw (radio_save_raw.grc) 23 | 24 | Radio Save Raw takes tagged raw samples and loads them into a Digital RF Sink block, along with their metadata. 25 | 26 | ![Radio Save Raw](images/radio_save_raw.png) 27 | 28 | ##### Radio Save Spec (radio_save_spec.grc) 29 | 30 | Radio Save Spec takes tagged calibrated spectra and stores them in a text file in the .rad format, which is better documented in [save_files](save_files.md). 31 | 32 | ![Radio Save Spec](images/radio_save_spec.png) 33 | 34 | ##### Radio Save Spec FITS (radio_save_spec_fits.grc) 35 | 36 | Radio Save Spec FITS takes tagged calibrated spectra and stores them in a binary FITS file using AstroPy, which is better documented in [save_files](save_files.md). 37 | 38 | ![Radio Save Spec FITS](images/radio_save_spec_fits.png) 39 | -------------------------------------------------------------------------------- /docs/save_files.md: -------------------------------------------------------------------------------- 1 | ## Small Radio Telescope Docs 2 | #### Save File Details 3 | 4 | The SRT Currently supports saving data into 3 different file types: 5 | - [Digital RF](https://github.com/MITHaystack/digital_rf) - Saves raw I/Q samples 6 | - [FITS](https://docs.astropy.org/en/stable/io/fits/) - Saves calibrated spectra 7 | - [rad](https://www.haystack.mit.edu/haystack-public-outreach/srt-the-small-radio-telescope-for-education/) - Saves calibrated spectra (see pswriter documentation for more info) 8 | 9 | Each format has different techniques for encoding metadata about the status of the SRT when it was taking the data, and so each has nearly the same data but accessible in a different manner. Examples for reading the main data and metadata for each file format follow 10 | 11 | ##### Digital RF - GNU Radio 12 | 13 | Since digital_rf is compatible with GNU Radio, it is possible to load a stream of raw samples directly into a GNU Radio Companion flowgraph once the gr_digital_rf library is installed. This process simply uses the 'Digital RF Channel Source' block with its channel set to the path to the folder containing the data. In addition, any metadata will appear in the stream as tags associated with the sample(s) that they were saved with. 14 | 15 | Form more information about using [GNU Radio Companion](https://wiki.gnuradio.org/index.php/Guided_Tutorial_GRC) or handling GNU Radio [tagged streams](https://wiki.gnuradio.org/index.php/Stream_Tags), see the GNU Radio docs. 16 | 17 | ##### Digital RF - Manual Usage 18 | 19 | Multiple detailed examples for reading Digital RF files can be found in the examples/ directory of the [digital_rf repository](https://github.com/MITHaystack/digital_rf/blob/master/python/examples/). 20 | 21 | ##### FITS 22 | 23 | The SRT software uses AstroPy for saving spectrum data, and so reading the data back can be performed easily with AstroPy as well. Notably, each spectra generated is saved into its own HDU, which in addition to the typical FITS metadata, each spectra has all of its SRT metadata saved in a JSON-encoded string in the header "METADATA". 24 | 25 | ```Python 26 | from astropy.io import fits 27 | import json 28 | hdul = fits.open(fits_filename) 29 | for hdu in hdul: 30 | metadata = json.loads(hdu.header["METADATA"]) 31 | data = hdu.data 32 | print(metadata) 33 | print(data) 34 | ``` 35 | 36 | ##### rad 37 | 38 | The below excerpt is a modification (namely, to have larger buffers and excluding doing anything with the loaded data for simplicity) on the original pswriter.c code which parses .rad files and generates .ps graphs from their content. More information of [PS Writer](https://www.haystack.mit.edu/wp-content/uploads/2020/07/srt_Pswriter_instructions.pdf) can be found on the Haystack Observatory [website](https://www.haystack.mit.edu/haystack-public-outreach/srt-the-small-radio-telescope-for-education/). 39 | 40 | ```c 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | 47 | int main(void) 48 | { 49 | char txt[80], fnam[80], datafile[80]; 50 | int i, j, jmax, k, np, j1, j2, n3, npoint, yr, da, hr, mn, sc, obsn1, obsn2; 51 | double xx, yy, dmax, ddmax, dmin, slope, dd, ddd, totpp, scale, sigma, freq, freqq, fstart, fstop, vstart, vstop, xoffset; 52 | double freqsep, x1, x2, y1, y2, wid, sx, sy, yoffset, x, y, xp, yp, av, avx, avy, avxx, avxy, psx1, psx2, psy1, psy2, yps; 53 | double restfreq; 54 | 55 | //Added variables for data input from file 56 | double aznow, elnow, tsys, tant, vlsr, glat, glon, bw, fbw, integ; 57 | int nfreq, nsam, bsw, intp; 58 | char soutrack[80*100], buf[80*100]; 59 | double pp[512*64*2]; 60 | FILE *file1 = NULL; 61 | FILE *dfile=NULL; 62 | 63 | //Ask user to enter name of data file to be read 64 | printf("Enter name of data file: "); 65 | scanf("%s", datafile); 66 | if ((dfile = fopen(datafile, "r")) == NULL) { 67 | printf("cannot read %s\n", datafile); 68 | return 0; 69 | } 70 | 71 | //Ask user to enter observation to be read 72 | printf("Enter observation number to read: "); 73 | scanf("%d", &obsn1); 74 | 75 | obsn2=-1; //Initialize obsn2 for first comparison 76 | while(obsn1!=obsn2) //Scan in data file until entered and scanned observation numbers match 77 | { 78 | //Scan in first two lines of data 79 | fscanf(dfile, "%[^\n]\n", buf); 80 | if (buf[0] == '*') 81 | { 82 | fscanf(dfile, "DATE %4d:%03d:%02d:%02d:%02d obsn %3d az %lf el %lf freq_MHz %lf Tsys %lf Tant %lf vlsr %lf glat %lf glon %lf source %s\n", 83 | &yr, &da, &hr, &mn, &sc, &obsn2, &aznow, &elnow, &freq, &tsys, &tant, &vlsr, &glat, &glon, soutrack); 84 | } 85 | else 86 | { 87 | sscanf(buf, "DATE %4d:%03d:%02d:%02d:%02d obsn %3d az %lf el %lf freq_MHz %lf Tsys %lf Tant %lf vlsr %lf glat %lf glon %lf source %s\n", 88 | &yr, &da, &hr, &mn, &sc, &obsn2, &aznow, &elnow, &freq, &tsys, &tant, &vlsr, &glat, &glon, soutrack); 89 | } 90 | fscanf(dfile, "Fstart %lf fstop %lf spacing %lf bw %lf fbw %lf MHz nfreq %d nsam %d npoint %d integ %lf sigma %lf bsw %d\n", 91 | &fstart, &fstop, &freqsep, &bw, &fbw, &nfreq, &nsam, &npoint, &integ, &sigma, &bsw); 92 | //Calculate a few things that are based on early data in the file and are needed to define later scanning from the file 93 | np = npoint; 94 | j1 = np * 0; 95 | j2 = np * 1; 96 | //Scan in spectrum data 97 | fscanf(dfile, "Spectrum %2d integration periods\n", &intp); 98 | for (j=0; j 200) 102 | { 103 | intp = 1; 104 | fseek(dfile, -9 * np, SEEK_CUR); 105 | fscanf(dfile, "Spectrum \n"); 106 | for (j=0; j 4 | 5 | **Changed:** 6 | 7 | * 8 | 9 | **Deprecated:** 10 | 11 | * 12 | 13 | **Removed:** 14 | 15 | * 16 | 17 | **Fixed:** 18 | 19 | * 20 | 21 | **Security:** 22 | 23 | * 24 | -------------------------------------------------------------------------------- /news/update-conda-recipe.rst: -------------------------------------------------------------------------------- 1 | **Added:** 2 | 3 | * 4 | 5 | **Changed:** 6 | 7 | * Updated the conda recipe so that it is "noarch: python", meaning it does not need to be rebuilt for different Python versions. 8 | 9 | **Deprecated:** 10 | 11 | * 12 | 13 | **Removed:** 14 | 15 | * 16 | 17 | **Fixed:** 18 | 19 | * 20 | 21 | **Security:** 22 | 23 | * 24 | -------------------------------------------------------------------------------- /radio/.gitignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | *.py 3 | -------------------------------------------------------------------------------- /radio/radio_calibrate/radio_calibrate.grc: -------------------------------------------------------------------------------- 1 | options: 2 | parameters: 3 | author: '' 4 | category: '[GRC Hier Blocks]' 5 | cmake_opt: '' 6 | comment: '' 7 | copyright: '' 8 | description: '' 9 | gen_cmake: 'On' 10 | gen_linking: dynamic 11 | generate_options: no_gui 12 | hier_block_src_path: '.:' 13 | id: radio_calibrate 14 | max_nouts: '0' 15 | output_language: python 16 | placement: (0,0) 17 | qt_qss_theme: '' 18 | realtime_scheduling: '' 19 | run: 'True' 20 | run_command: '{python} -u {filename}' 21 | run_options: run 22 | sizing_mode: fixed 23 | thread_safe_setters: '' 24 | title: radio_calibrate 25 | window_size: '' 26 | states: 27 | bus_sink: false 28 | bus_source: false 29 | bus_structure: null 30 | coordinate: [8, 8] 31 | rotation: 0 32 | state: enabled 33 | 34 | blocks: 35 | - name: directory_name 36 | id: parameter 37 | parameters: 38 | alias: '' 39 | comment: '' 40 | hide: none 41 | label: directory_name 42 | short_id: '' 43 | type: str 44 | value: '"."' 45 | states: 46 | bus_sink: false 47 | bus_source: false 48 | bus_structure: null 49 | coordinate: [8, 205] 50 | rotation: 0 51 | state: true 52 | - name: num_bins 53 | id: parameter 54 | parameters: 55 | alias: '' 56 | comment: '' 57 | hide: none 58 | label: num_bins 59 | short_id: '' 60 | type: intx 61 | value: '4096' 62 | states: 63 | bus_sink: false 64 | bus_source: false 65 | bus_structure: null 66 | coordinate: [9, 106] 67 | rotation: 0 68 | state: true 69 | - name: save_calibration 70 | id: epy_block 71 | parameters: 72 | _source_code: "\"\"\"\nEmbedded Python Blocks:\n\nEach time this file is saved,\ 73 | \ GRC will instantiate the first class it finds\nto get ports and parameters\ 74 | \ of your block. The arguments to __init__ will\nbe the parameters. All of\ 75 | \ them are required to have default values!\n\"\"\"\n\nimport numpy as np\n\ 76 | import numpy.polynomial.polynomial as poly\nimport json\n\nfrom gnuradio import\ 77 | \ gr\nimport pmt\n\nimport pathlib\n\n\nclass blk(gr.sync_block):\n \"\"\"\ 78 | Embedded Python Block - Calculate Calibration Values\"\"\"\n\n def __init__(\n\ 79 | \ self,\n directory=\".\",\n filename=\"calibration.json\"\ 80 | ,\n vec_length=4096,\n poly_smoothing_order=25,\n ): # only\ 81 | \ default arguments here\n \"\"\"arguments to this function show up as\ 82 | \ parameters in GRC\"\"\"\n gr.sync_block.__init__(\n self,\n\ 83 | \ name=\"Embedded Python Block\", # will show up in GRC\n \ 84 | \ in_sig=[(np.float32, vec_length)],\n out_sig=None,\n \ 85 | \ )\n # if an attribute with the same name as a parameter is found,\n\ 86 | \ # a callback is registered (properties work, too).\n self.directory\ 87 | \ = directory\n self.filename = filename\n self.vec_length = vec_length\n\ 88 | \ self.poly_smoothing_order = poly_smoothing_order\n self.past_input\ 89 | \ = np.zeros(vec_length)\n self.num_past_input = 0\n\n def work(self,\ 90 | \ input_items, output_items):\n \"\"\"Divide Input by Average, Determine\ 91 | \ Calibration Power, and Save\"\"\"\n for input_array in input_items[0]:\n\ 92 | \ self.past_input += input_array\n self.num_past_input\ 93 | \ += 1\n averaged_input = self.past_input / self.num_past_input\n\ 94 | \ relative_freq_values = np.linspace(-1, 1, self.vec_length)\n \ 95 | \ poly_fit = poly.Polynomial.fit(\n relative_freq_values,\ 96 | \ averaged_input, self.poly_smoothing_order,\n )\n smoothed_input\ 97 | \ = poly_fit(relative_freq_values)\n average_value = np.average(smoothed_input)\n\ 98 | \ rescaled_input = smoothed_input / average_value\n file_output\ 99 | \ = {\n \"cal_pwr\": average_value,\n \"cal_values\"\ 100 | : rescaled_input.tolist(),\n }\n with open(pathlib.Path(self.directory,\ 101 | \ self.filename), \"w\") as outfile:\n json.dump(file_output,\ 102 | \ outfile)\n return len(input_items[0])\n" 103 | affinity: '' 104 | alias: '' 105 | comment: '' 106 | directory: directory_name 107 | filename: '''calibration.json''' 108 | maxoutbuf: '0' 109 | minoutbuf: '0' 110 | poly_smoothing_order: '25' 111 | vec_length: num_bins 112 | states: 113 | _io_cache: ('Embedded Python Block', 'blk', [('directory', "'.'"), ('filename', 114 | "'calibration.json'"), ('vec_length', '4096'), ('poly_smoothing_order', '25')], 115 | [('0', 'float', 4096)], [], 'Embedded Python Block - Calculate Calibration Values', 116 | ['directory', 'filename', 'poly_smoothing_order', 'vec_length']) 117 | bus_sink: false 118 | bus_source: false 119 | bus_structure: null 120 | coordinate: [492, 134] 121 | rotation: 0 122 | state: true 123 | - name: zeromq_sub_source_0 124 | id: zeromq_sub_source 125 | parameters: 126 | address: tcp://127.0.0.1:5560 127 | affinity: '' 128 | alias: '' 129 | comment: '' 130 | hwm: '-1' 131 | maxoutbuf: '0' 132 | minoutbuf: '0' 133 | pass_tags: 'True' 134 | timeout: '100' 135 | type: float 136 | vlen: num_bins 137 | states: 138 | bus_sink: false 139 | bus_source: false 140 | bus_structure: null 141 | coordinate: [238, 134] 142 | rotation: 0 143 | state: true 144 | 145 | connections: 146 | - [zeromq_sub_source_0, '0', save_calibration, '0'] 147 | 148 | metadata: 149 | file_format: 1 150 | -------------------------------------------------------------------------------- /radio/radio_save_raw/radio_save_raw.grc: -------------------------------------------------------------------------------- 1 | options: 2 | parameters: 3 | author: '' 4 | category: '[GRC Hier Blocks]' 5 | cmake_opt: '' 6 | comment: '' 7 | copyright: '' 8 | description: '' 9 | gen_cmake: 'On' 10 | gen_linking: dynamic 11 | generate_options: no_gui 12 | hier_block_src_path: '.:' 13 | id: radio_save_raw 14 | max_nouts: '0' 15 | output_language: python 16 | placement: (0,0) 17 | qt_qss_theme: '' 18 | realtime_scheduling: '' 19 | run: 'True' 20 | run_command: '{python} -u {filename}' 21 | run_options: run 22 | sizing_mode: fixed 23 | thread_safe_setters: '' 24 | title: radio_save_raw 25 | window_size: '' 26 | states: 27 | bus_sink: false 28 | bus_source: false 29 | bus_structure: null 30 | coordinate: [8, 8] 31 | rotation: 0 32 | state: enabled 33 | 34 | blocks: 35 | - name: directory_name 36 | id: parameter 37 | parameters: 38 | alias: '' 39 | comment: '' 40 | hide: none 41 | label: directory_name 42 | short_id: '' 43 | type: str 44 | value: '"./rf_data"' 45 | states: 46 | bus_sink: false 47 | bus_source: false 48 | bus_structure: null 49 | coordinate: [11, 203] 50 | rotation: 0 51 | state: true 52 | - name: gr_digital_rf_digital_rf_channel_sink_0 53 | id: gr_digital_rf_digital_rf_channel_sink 54 | parameters: 55 | affinity: '' 56 | alias: '' 57 | center_freqs: '[]' 58 | checksum: 'False' 59 | comment: '' 60 | compression_level: '0' 61 | debug: 'False' 62 | dir: directory_name 63 | file_cadence_ms: '1000' 64 | ignore_tags: 'False' 65 | input: fc32 66 | is_continuous: 'True' 67 | marching_periods: 'True' 68 | metadata: '{}' 69 | min_chunksize: '0' 70 | sample_rate_denominator: '1' 71 | sample_rate_numerator: int(samp_rate) 72 | start: '''now''' 73 | stop_on_skipped: 'False' 74 | stop_on_time_tag: 'False' 75 | subdir_cadence_s: '3600' 76 | uuid: '' 77 | vlen: '1' 78 | states: 79 | bus_sink: false 80 | bus_source: false 81 | bus_structure: null 82 | coordinate: [659, 110] 83 | rotation: 0 84 | state: true 85 | - name: samp_rate 86 | id: parameter 87 | parameters: 88 | alias: '' 89 | comment: '' 90 | hide: none 91 | label: samp_rate 92 | short_id: '' 93 | type: intx 94 | value: '2400000' 95 | states: 96 | bus_sink: false 97 | bus_source: false 98 | bus_structure: null 99 | coordinate: [10, 107] 100 | rotation: 0 101 | state: true 102 | - name: zeromq_sub_source_0 103 | id: zeromq_sub_source 104 | parameters: 105 | address: tcp://127.0.0.1:5558 106 | affinity: '' 107 | alias: '' 108 | comment: '' 109 | hwm: '-1' 110 | maxoutbuf: '0' 111 | minoutbuf: '0' 112 | pass_tags: 'True' 113 | timeout: '100' 114 | type: complex 115 | vlen: '1' 116 | states: 117 | bus_sink: false 118 | bus_source: false 119 | bus_structure: null 120 | coordinate: [328, 119] 121 | rotation: 0 122 | state: true 123 | 124 | connections: 125 | - [zeromq_sub_source_0, '0', gr_digital_rf_digital_rf_channel_sink_0, '0'] 126 | 127 | metadata: 128 | file_format: 1 129 | -------------------------------------------------------------------------------- /radio/radio_save_spec/radio_save_spec.grc: -------------------------------------------------------------------------------- 1 | options: 2 | parameters: 3 | author: '' 4 | category: '[GRC Hier Blocks]' 5 | cmake_opt: '' 6 | comment: '' 7 | copyright: '' 8 | description: '' 9 | gen_cmake: 'On' 10 | gen_linking: dynamic 11 | generate_options: no_gui 12 | hier_block_src_path: '.:' 13 | id: radio_save_spec 14 | max_nouts: '0' 15 | output_language: python 16 | placement: (0,0) 17 | qt_qss_theme: '' 18 | realtime_scheduling: '' 19 | run: 'True' 20 | run_command: '{python} -u {filename}' 21 | run_options: run 22 | sizing_mode: fixed 23 | thread_safe_setters: '' 24 | title: radio_save_spec 25 | window_size: '' 26 | states: 27 | bus_sink: false 28 | bus_source: false 29 | bus_structure: null 30 | coordinate: [8, 8] 31 | rotation: 0 32 | state: enabled 33 | 34 | blocks: 35 | - name: directory_name 36 | id: parameter 37 | parameters: 38 | alias: '' 39 | comment: '' 40 | hide: none 41 | label: directory_name 42 | short_id: '' 43 | type: str 44 | value: '"."' 45 | states: 46 | bus_sink: false 47 | bus_source: false 48 | bus_structure: null 49 | coordinate: [123, 106] 50 | rotation: 0 51 | state: true 52 | - name: file_name 53 | id: parameter 54 | parameters: 55 | alias: '' 56 | comment: '' 57 | hide: none 58 | label: file_name 59 | short_id: '' 60 | type: str 61 | value: '"test.rad"' 62 | states: 63 | bus_sink: false 64 | bus_source: false 65 | bus_structure: null 66 | coordinate: [116, 203] 67 | rotation: 0 68 | state: true 69 | - name: num_bins 70 | id: parameter 71 | parameters: 72 | alias: '' 73 | comment: '' 74 | hide: none 75 | label: num_bins 76 | short_id: '' 77 | type: intx 78 | value: '4096' 79 | states: 80 | bus_sink: false 81 | bus_source: false 82 | bus_structure: null 83 | coordinate: [9, 204] 84 | rotation: 0 85 | state: true 86 | - name: samp_rate 87 | id: parameter 88 | parameters: 89 | alias: '' 90 | comment: '' 91 | hide: none 92 | label: samp_rate 93 | short_id: '' 94 | type: intx 95 | value: '2400000' 96 | states: 97 | bus_sink: false 98 | bus_source: false 99 | bus_structure: null 100 | coordinate: [10, 107] 101 | rotation: 0 102 | state: true 103 | - name: save_rad_file 104 | id: epy_block 105 | parameters: 106 | _source_code: "\"\"\"\nEmbedded Python Blocks:\n\nEach time this file is saved,\ 107 | \ GRC will instantiate the first class it finds\nto get ports and parameters\ 108 | \ of your block. The arguments to __init__ will\nbe the parameters. All of\ 109 | \ them are required to have default values!\n\"\"\"\n\nimport numpy as np\n\ 110 | from gnuradio import gr\nimport pmt\n\nimport pathlib\nfrom datetime import\ 111 | \ datetime, timezone\nfrom math import sqrt\n\n\n# String Formatting Constants\n\ 112 | header_format = (\n \"DATE %4d:%03d:%02d:%02d:%02d obsn %3d az %4.1f el %3.1f\ 113 | \ freq_MHz \"\n \"%10.4f Tsys %6.3f Tant %6.3f vlsr %7.2f glat %6.3f glon\ 114 | \ %6.3f source %s\\n\"\n)\nstart_format = (\n \"Fstart %8.3f fstop %8.3f\ 115 | \ spacing %8.6f bw %8.3f fbw %8.3f MHz nfreq \"\n \"%d nsam %d npoint %d\ 116 | \ integ %5.0f sigma %8.3f bsw %d\\n\"\n)\nintegration_format = \"Spectrum %6.0f\ 117 | \ integration periods\\n\"\nnumber_format = \"%8.3f \"\n\n\ndef parse_time(rx_time):\n\ 118 | \ time_since_epoch = rx_time[0] + rx_time[1]\n date = datetime.fromtimestamp(time_since_epoch,\ 119 | \ timezone.utc)\n new_year_day = datetime(year=date.year, month=1, day=1,\ 120 | \ tzinfo=timezone.utc)\n day_of_year = (date - new_year_day).days + 1\n \ 121 | \ return date.year, day_of_year, date.hour, date.minute, date.second\n\n\n\ 122 | def parse_metadata(metadata):\n motor_az = metadata[\"motor_az\"]\n motor_el\ 123 | \ = metadata[\"motor_el\"]\n samp_rate = metadata[\"samp_rate\"]\n num_integrations\ 124 | \ = metadata[\"num_integrations\"]\n freq = metadata[\"freq\"]\n num_bins\ 125 | \ = metadata[\"num_bins\"]\n tsys = metadata[\"tsys\"]\n tcal = metadata[\"\ 126 | tcal\"]\n cal_pwr = metadata[\"cal_pwr\"]\n vslr = metadata[\"vslr\"]\n\ 127 | \ glat = metadata[\"glat\"]\n glon = metadata[\"glon\"]\n soutrack\ 128 | \ = metadata[\"soutrack\"]\n bsw = metadata[\"bsw\"]\n return (\n \ 129 | \ motor_az,\n motor_el,\n freq / pow(10, 6),\n samp_rate\ 130 | \ / pow(10, 6),\n num_integrations,\n num_bins,\n tsys,\n\ 131 | \ tcal,\n cal_pwr,\n vslr,\n glat,\n glon,\n\ 132 | \ soutrack,\n bsw,\n )\n\n\nclass blk(\n gr.sync_block\n\ 133 | ): # other base classes are basic_block, decim_block, interp_block\n \"\"\ 134 | \"Embedded Python Block example - a simple multiply const\"\"\"\n\n def __init__(\n\ 135 | \ self, directory=\".\", filename=\"test.rad\", vec_length=4096\n \ 136 | \ ): # only default arguments here\n \"\"\"arguments to this function\ 137 | \ show up as parameters in GRC\"\"\"\n gr.sync_block.__init__(\n \ 138 | \ self,\n name=\"Embedded Python Block\", # will show up\ 139 | \ in GRC\n in_sig=[(np.float32, vec_length)],\n out_sig=None,\n\ 140 | \ )\n # if an attribute with the same name as a parameter is found,\n\ 141 | \ # a callback is registered (properties work, too).\n self.directory\ 142 | \ = directory\n self.filename = filename\n self.vec_length = vec_length\n\ 143 | \ self.obsn = 0\n\n def work(self, input_items, output_items):\n \ 144 | \ \"\"\"example: multiply with constant\"\"\"\n file = open(pathlib.Path(self.directory,\ 145 | \ self.filename), \"a+\")\n tags = self.get_tags_in_window(0, 0, len(input_items[0]))\n\ 146 | \ latest_data_dict = {\n pmt.to_python(tag.key): pmt.to_python(tag.value)\ 147 | \ for tag in tags\n }\n yr, da, hr, mn, sc = parse_time(latest_data_dict[\"\ 148 | rx_time\"])\n (\n aznow,\n elnow,\n \ 149 | \ freq,\n bw,\n integ,\n nfreq,\n \ 150 | \ tsys,\n tcal,\n calpwr,\n vlsr,\n \ 151 | \ glat,\n glon,\n soutrack,\n bsw,\n\ 152 | \ ) = parse_metadata(latest_data_dict[\"metadata\"])\n fbw = bw\ 153 | \ # Old SRT Software Had Relative Bandwidth Limits and An Unchanging Sample\ 154 | \ Rate\n f1 = 0 # Relative Lower Bound (Since Sample Rate Determines\ 155 | \ Bandwidth)\n f2 = 1 # Relative Upper Bound (Since Sample Rate Determines\ 156 | \ Bandwidth)\n istart = f1 * nfreq + 0.5\n istop = f2 * nfreq\ 157 | \ + 0.5\n efflofreq = freq - bw * 0.5\n freqsep = bw / nfreq\n\ 158 | \ nsam = nfreq # Old SRT Software Had a Specific Bundle of Samples Shuffled\ 159 | \ Out and Processed at a Time\n sigma = tsys / sqrt((nsam * integ / (2.0e6\ 160 | \ * bw)) * freqsep * 1e6)\n for input_array in input_items[0]:\n \ 161 | \ p = np.sum(input_array)\n a = len(input_array)\n \ 162 | \ pwr = (tsys + tcal) * p / (a * calpwr)\n ppwr = pwr - tsys\n\ 163 | \ header_line = header_format % (\n yr,\n \ 164 | \ da,\n hr,\n mn,\n sc,\n\ 165 | \ self.obsn,\n aznow,\n elnow,\n\ 166 | \ freq,\n tsys,\n ppwr,\n \ 167 | \ vlsr,\n glat,\n glon,\n \ 168 | \ soutrack,\n )\n start_line = start_format % (\n\ 169 | \ istart * bw / nfreq + efflofreq,\n istop * bw\ 170 | \ / nfreq + efflofreq,\n freqsep,\n bw,\n \ 171 | \ fbw,\n nfreq,\n nsam,\n \ 172 | \ istop - istart,\n integ * nsam / (2.0e6 * bw),\n \ 173 | \ sigma,\n bsw,\n )\n integration_line\ 174 | \ = integration_format % integ\n file.writelines([header_line, start_line,\ 175 | \ integration_line])\n for val in input_array:\n file.write(number_format\ 176 | \ % val)\n file.write(\"\\n\")\n self.obsn += 1\n\n \ 177 | \ file.close()\n return len(input_items[0])\n" 178 | affinity: '' 179 | alias: '' 180 | comment: '' 181 | directory: directory_name 182 | filename: file_name 183 | maxoutbuf: '0' 184 | minoutbuf: '0' 185 | vec_length: num_bins 186 | states: 187 | _io_cache: ('Embedded Python Block', 'blk', [('directory', "'.'"), ('filename', 188 | "'test.rad'"), ('vec_length', '4096')], [('0', 'float', 4096)], [], 'Embedded 189 | Python Block example - a simple multiply const', ['directory', 'filename', 'vec_length']) 190 | bus_sink: false 191 | bus_source: false 192 | bus_structure: null 193 | coordinate: [636, 127] 194 | rotation: 0 195 | state: true 196 | - name: zeromq_sub_source_0 197 | id: zeromq_sub_source 198 | parameters: 199 | address: tcp://127.0.0.1:5562 200 | affinity: '' 201 | alias: '' 202 | comment: '' 203 | hwm: '-1' 204 | maxoutbuf: '0' 205 | minoutbuf: '0' 206 | pass_tags: 'True' 207 | timeout: '100' 208 | type: float 209 | vlen: num_bins 210 | states: 211 | bus_sink: false 212 | bus_source: false 213 | bus_structure: null 214 | coordinate: [328, 119] 215 | rotation: 0 216 | state: true 217 | 218 | connections: 219 | - [zeromq_sub_source_0, '0', save_rad_file, '0'] 220 | 221 | metadata: 222 | file_format: 1 223 | -------------------------------------------------------------------------------- /radio/radio_save_spec_fits/radio_save_spec_fits.grc: -------------------------------------------------------------------------------- 1 | options: 2 | parameters: 3 | author: '' 4 | category: '[GRC Hier Blocks]' 5 | cmake_opt: '' 6 | comment: '' 7 | copyright: '' 8 | description: '' 9 | gen_cmake: 'On' 10 | gen_linking: dynamic 11 | generate_options: no_gui 12 | hier_block_src_path: '.:' 13 | id: radio_save_spec_fits 14 | max_nouts: '0' 15 | output_language: python 16 | placement: (0,0) 17 | qt_qss_theme: '' 18 | realtime_scheduling: '' 19 | run: 'True' 20 | run_command: '{python} -u {filename}' 21 | run_options: run 22 | sizing_mode: fixed 23 | thread_safe_setters: '' 24 | title: radio_save_spec_fits 25 | window_size: '' 26 | states: 27 | bus_sink: false 28 | bus_source: false 29 | bus_structure: null 30 | coordinate: [8, 8] 31 | rotation: 0 32 | state: enabled 33 | 34 | blocks: 35 | - name: directory_name 36 | id: parameter 37 | parameters: 38 | alias: '' 39 | comment: '' 40 | hide: none 41 | label: directory_name 42 | short_id: '' 43 | type: str 44 | value: '"."' 45 | states: 46 | bus_sink: false 47 | bus_source: false 48 | bus_structure: null 49 | coordinate: [123, 106] 50 | rotation: 0 51 | state: true 52 | - name: file_name 53 | id: parameter 54 | parameters: 55 | alias: '' 56 | comment: '' 57 | hide: none 58 | label: file_name 59 | short_id: '' 60 | type: str 61 | value: '"test.fits"' 62 | states: 63 | bus_sink: false 64 | bus_source: false 65 | bus_structure: null 66 | coordinate: [116, 203] 67 | rotation: 0 68 | state: true 69 | - name: num_bins 70 | id: parameter 71 | parameters: 72 | alias: '' 73 | comment: '' 74 | hide: none 75 | label: num_bins 76 | short_id: '' 77 | type: intx 78 | value: '4096' 79 | states: 80 | bus_sink: false 81 | bus_source: false 82 | bus_structure: null 83 | coordinate: [9, 204] 84 | rotation: 0 85 | state: true 86 | - name: samp_rate 87 | id: parameter 88 | parameters: 89 | alias: '' 90 | comment: '' 91 | hide: none 92 | label: samp_rate 93 | short_id: '' 94 | type: intx 95 | value: '2400000' 96 | states: 97 | bus_sink: false 98 | bus_source: false 99 | bus_structure: null 100 | coordinate: [10, 107] 101 | rotation: 0 102 | state: true 103 | - name: save_fits_file 104 | id: epy_block 105 | parameters: 106 | _source_code: "\"\"\"\nEmbedded Python Blocks:\n\nEach time this file is saved,\ 107 | \ GRC will instantiate the first class it finds\nto get ports and parameters\ 108 | \ of your block. The arguments to __init__ will\nbe the parameters. All of\ 109 | \ them are required to have default values!\n\"\"\"\n\nimport numpy as np\n\ 110 | from gnuradio import gr\nimport pmt\nimport json\n\nimport pathlib\nfrom datetime\ 111 | \ import datetime, timezone\nfrom astropy.io import fits\n\n\nclass blk(gr.sync_block):\n\ 112 | \ \"\"\"Embedded Python Block - Saving \"\"\"\n\n def __init__(\n \ 113 | \ self, directory=\".\", filename=\"test.fits\", vec_length=4096\n ):\ 114 | \ # only default arguments here\n \"\"\"arguments to this function show\ 115 | \ up as parameters in GRC\"\"\"\n gr.sync_block.__init__(\n \ 116 | \ self,\n name=\"Embedded Python Block\", # will show up in GRC\n\ 117 | \ in_sig=[(np.float32, vec_length)],\n out_sig=None,\n\ 118 | \ )\n # if an attribute with the same name as a parameter is found,\n\ 119 | \ # a callback is registered (properties work, too).\n self.directory\ 120 | \ = directory\n self.filename = filename\n self.vec_length = vec_length\n\ 121 | \n def work(self, input_items, output_items):\n \"\"\"Saving Spectrum\ 122 | \ Data to a FITS File\"\"\"\n file_path = pathlib.Path(self.directory,\ 123 | \ self.filename)\n for i, input_array in enumerate(input_items[0]):\n\ 124 | \ file = open(file_path, \"ab+\")\n tags = self.get_tags_in_window(0,\ 125 | \ 0, len(input_items[0]))\n tags_dict = {\n pmt.to_python(tag.key):\ 126 | \ pmt.to_python(tag.value) for tag in tags\n }\n time_since_epoch\ 127 | \ = tags_dict[\"rx_time\"][0] + tags_dict[\"rx_time\"][1]\n date\ 128 | \ = datetime.fromtimestamp(time_since_epoch, timezone.utc)\n metadata\ 129 | \ = tags_dict[\"metadata\"]\n samp_rate = metadata[\"samp_rate\"\ 130 | ]\n num_integrations = metadata[\"num_integrations\"]\n \ 131 | \ freq = metadata[\"freq\"]\n num_bins = metadata[\"num_bins\"\ 132 | ]\n soutrack = metadata[\"soutrack\"]\n\n hdr = fits.Header()\n\ 133 | \ hdr[\"BUNIT\"] = \"K\"\n hdr[\"CTYPE1\"] = \"Freq\"\n\ 134 | \ hdr[\"CRPIX1\"] = num_bins / float(2) # Reference pixel (center)\n\ 135 | \ hdr[\"CRVAL1\"] = freq # Center, USRP, frequency\n \ 136 | \ hdr[\"CDELT1\"] = samp_rate / (1 * num_bins) # Channel width\n \ 137 | \ hdr[\"CUNIT1\"] = \"Hz\"\n\n hdr[\"TELESCOP\"] = \"SmallRadioTelescope\"\ 138 | \n hdr[\"OBJECT\"] = soutrack\n hdr[\"OBSTIME\"] = (num_bins\ 139 | \ * num_integrations) / samp_rate\n\n hdr[\"DATE-OBS\"] = date.strftime(\"\ 140 | %Y-%m-%d\")\n hdr[\"UTC\"] = date.strftime(\"%H:%M:00%s\")\n \ 141 | \ hdr[\"METADATA\"] = json.dumps(metadata)\n\n fits.append(file,\ 142 | \ input_array, hdr)\n file.close()\n # p = np.sum(input_array)\n\ 143 | \ # a = len(input_array)\n # pwr = (tsys + tcal) * p /\ 144 | \ (a * calpwr)\n # ppwr = pwr - tsys\n return len(input_items[0])\n" 145 | affinity: '' 146 | alias: '' 147 | comment: '' 148 | directory: directory_name 149 | filename: file_name 150 | maxoutbuf: '0' 151 | minoutbuf: '0' 152 | vec_length: num_bins 153 | states: 154 | _io_cache: ('Embedded Python Block', 'blk', [('directory', "'.'"), ('filename', 155 | "'test.fits'"), ('vec_length', '4096')], [('0', 'float', 4096)], [], 'Embedded 156 | Python Block - Saving ', ['directory', 'filename', 'vec_length']) 157 | bus_sink: false 158 | bus_source: false 159 | bus_structure: null 160 | coordinate: [636, 127] 161 | rotation: 0 162 | state: true 163 | - name: zeromq_sub_source_0 164 | id: zeromq_sub_source 165 | parameters: 166 | address: tcp://127.0.0.1:5562 167 | affinity: '' 168 | alias: '' 169 | comment: '' 170 | hwm: '-1' 171 | maxoutbuf: '0' 172 | minoutbuf: '0' 173 | pass_tags: 'True' 174 | timeout: '100' 175 | type: float 176 | vlen: num_bins 177 | states: 178 | bus_sink: false 179 | bus_source: false 180 | bus_structure: null 181 | coordinate: [328, 119] 182 | rotation: 0 183 | state: true 184 | 185 | connections: 186 | - [zeromq_sub_source_0, '0', save_fits_file, '0'] 187 | 188 | metadata: 189 | file_format: 1 190 | -------------------------------------------------------------------------------- /recipe/.condarc: -------------------------------------------------------------------------------- 1 | channel_priority: strict 2 | channels: 3 | - conda-forge 4 | - defaults 5 | -------------------------------------------------------------------------------- /recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set version = "1.1.1" %} 2 | 3 | package: 4 | name: "srt-py" 5 | version: "{{ version }}" 6 | 7 | source: 8 | path: ../ 9 | 10 | build: 11 | noarch: python 12 | number: 0 13 | script: {{ PYTHON }} -m pip install . -vv 14 | 15 | requirements: 16 | host: 17 | - pip 18 | - python >=3.6 19 | - setuptools 20 | run: 21 | - astropy 22 | - dash 23 | - dash-bootstrap-components 24 | - dash-html-components 25 | - dash-core-components 26 | - digital_rf 27 | - gnuradio-core 28 | - gnuradio-zeromq 29 | - gnuradio-osmosdr 30 | - pandas 31 | - plotly 32 | - pyserial 33 | - python >=3.6 34 | - pyzmq 35 | - numpy 36 | - rtl-sdr 37 | - scipy 38 | - soapysdr 39 | - soapysdr-module-rtlsdr 40 | - waitress 41 | - yamale 42 | 43 | test: 44 | requires: 45 | - pytest 46 | imports: 47 | - srt 48 | 49 | about: 50 | home: https://github.com/MITHaystack/srt-py 51 | summary: Small Radio Telescope Control Code for Python 52 | license: MIT 53 | license_file: license 54 | -------------------------------------------------------------------------------- /rever.xsh: -------------------------------------------------------------------------------- 1 | $PROJECT = 'srt-py' 2 | $WEBSITE_URL = "https://github.com/MITHaystack/srt-py" 3 | $GITHUB_ORG = "MITHaystack" 4 | $GITHUB_REPO = "srt-py" 5 | $ACTIVITIES = [ 6 | 'version_bump', # Changes the version number in various source files (setup.py, __init__.py, etc) 7 | 'changelog', # Uses files in the news folder to create a changelog for release 8 | 'tag', # Creates a tag for the new version number 9 | # 'push_tag', # Pushes the tag up to the $TAG_REMOTE 10 | # 'pypi', # Sends the package to pypi 11 | # 'conda_forge', # Creates a PR into your package's feedstock 12 | # 'ghrelease' # Creates a Github release entry for the new tag 13 | ] 14 | 15 | 16 | $TAG_TEMPLATE = 'v$VERSION' 17 | $VERSION_BUMP_PATTERNS = [ # These note where/how to find the version numbers 18 | ('srt/__init__.py', r'__version__\s*=.*', "__version__ = '$VERSION'"), 19 | ('setup.py', r'version\s*=.*,', "version='$VERSION',"), 20 | ('recipe/meta.yaml', r'{%\s*set\s*version\s*=.*', '{% set version = "$VERSION" %}') 21 | ] 22 | $CHANGELOG_FILENAME = 'CHANGELOG.rst' # Filename for the changelog 23 | $CHANGELOG_TEMPLATE = 'TEMPLATE.rst' # Filename for the news template 24 | -------------------------------------------------------------------------------- /scripts/test_ephemeris.py: -------------------------------------------------------------------------------- 1 | """test_ephemeris.py 2 | 3 | Calculates and Displays All AzEl Coordinates Above the Horizon 4 | 5 | """ 6 | from srt.daemon.utilities.object_tracker import EphemerisTracker 7 | import matplotlib.pyplot as plt 8 | 9 | if __name__ == "__main__": 10 | eph = EphemerisTracker(42.5, -71.5) 11 | 12 | daz = [] 13 | dalt = [] 14 | dnames = [] 15 | for val in eph.get_all_azimuth_elevation(): 16 | print(val + str(eph.get_all_azimuth_elevation()[val])) 17 | if eph.get_all_azimuth_elevation()[val][1] > 0: 18 | daz.append(eph.get_all_azimuth_elevation()[val][0]) 19 | dalt.append(eph.get_all_azimuth_elevation()[val][1]) 20 | dnames.append(val) 21 | 22 | fig, ax = plt.subplots() 23 | ax.scatter(daz, dalt) 24 | plt.xlabel("azimuth (degs)") 25 | plt.ylabel("altitude (degs)") 26 | for i, txt in enumerate(dnames): 27 | ax.annotate(txt, (daz[i], dalt[i])) 28 | plt.show() 29 | -------------------------------------------------------------------------------- /scripts/test_motor.py: -------------------------------------------------------------------------------- 1 | """test_motor.py 2 | 3 | Moves a Motor Through 10 Random Points and Back to Stow 4 | 5 | """ 6 | from random import uniform 7 | from time import sleep 8 | 9 | from srt.daemon.rotor_control.rotors import Rotor, RotorType 10 | 11 | stow_position = (38, 0) # Taken from Starting Position of Haystack SRT 12 | az_limit = (38, 355) # Taken From Haystack srt.cat 13 | el_limit = (0, 89) # Taken From Haystack srt.cat 14 | 15 | if __name__ == "__main__": 16 | rotor = Rotor(RotorType.ROT2, "/dev/ttyUSB0", az_limit, el_limit) 17 | az, el = rotor.get_azimuth_elevation() 18 | print("Current AzEl: " + str((az, el))) 19 | 20 | num_points = 10 21 | test_points = [ 22 | (uniform(az_limit[0], az_limit[1]), uniform(el_limit[0], el_limit[1])) 23 | for _ in range(num_points) 24 | ] 25 | 26 | for point in test_points: 27 | rotor.set_azimuth_elevation(point[0], point[1]) 28 | while abs(az - point[0]) > 1 or abs(el - point[1]) > 1: 29 | sleep(1) 30 | az, el = rotor.get_azimuth_elevation() 31 | print("Current AzEl: " + str((az, el))) 32 | print("Point " + str(point) + " Movement Complete") 33 | sleep(5) 34 | 35 | print("Stowing Dish") 36 | rotor.set_azimuth_elevation(stow_position[0], stow_position[1]) 37 | while abs(az - stow_position[0]) > 1 or abs(el - stow_position[1]) > 1: 38 | sleep(1) 39 | az, el = rotor.get_azimuth_elevation() 40 | print("Current AzEl: " + str((az, el))) 41 | print("Stow Complete") 42 | -------------------------------------------------------------------------------- /scripts/test_yaml.py: -------------------------------------------------------------------------------- 1 | """test_yaml.py 2 | 3 | Validates and Prints the YAML Dictionary 4 | 5 | """ 6 | from srt.daemon.utilities.yaml_tools import validate_yaml_schema, load_yaml 7 | 8 | if __name__ == "__main__": 9 | validate_yaml_schema() 10 | print(load_yaml()) 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [versioneer] 2 | VCS = git 3 | style = pep440 4 | versionfile_source = srt/_version.py 5 | versionfile_build = srt/_version.py 6 | tag_prefix = 7 | parentdir_prefix = srt- 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | from pathlib import Path 3 | import versioneer 4 | 5 | 6 | scripts = ["bin/srt_controller.py", "bin/srt_runner.py"] 7 | 8 | with open("README.md", "r") as fh: 9 | long_description = fh.read() 10 | 11 | setuptools.setup( 12 | name="srt-py", 13 | version='v1.1.1', 14 | include_package_data=True, 15 | cmdclass=versioneer.get_cmdclass(), 16 | author="MIT Haystack", 17 | author_email="srt@mit.edu", 18 | description="Python implementation of the Small Radio Telescope.", 19 | long_description=long_description, 20 | long_description_content_type="text/markdown", 21 | url="https://github.mit.edu/SmallRadioTelescope/srt-py", 22 | packages=setuptools.find_packages(), 23 | scripts=scripts, 24 | package_data={"": ["*.js", "*.css", "*.ico", "*.png"]}, 25 | classifiers=[ 26 | "Programming Language :: Python :: 3", 27 | "License :: OSI Approved :: MIT License", 28 | "Operating System :: OS Independent", 29 | ], 30 | python_requires=">=3.6", 31 | ) 32 | -------------------------------------------------------------------------------- /srt/__init__.py: -------------------------------------------------------------------------------- 1 | from . import _version 2 | 3 | __version__ = 'v1.1.1' 4 | -------------------------------------------------------------------------------- /srt/config_loader.py: -------------------------------------------------------------------------------- 1 | """config_loader.py 2 | 3 | Module Containing Brief Functions for Validating and Parsing YAML 4 | 5 | """ 6 | import yamale 7 | import yaml 8 | 9 | 10 | def validate_yaml_schema(config_path, schema_path): 11 | """Validates YAML Config File Against Schema 12 | 13 | Parameters 14 | ---------- 15 | config_path : str 16 | Name / Path to the config.yaml File 17 | schema_path : str 18 | Name / Path to the schema.yaml File 19 | 20 | Returns 21 | ------- 22 | bool 23 | If the Yamale Validates Properly 24 | """ 25 | schema = yamale.make_schema(schema_path) 26 | data = yamale.make_data(config_path) 27 | return yamale.validate(schema, data) 28 | 29 | 30 | def load_yaml(config_path): 31 | """Parses 32 | 33 | Parameters 34 | ---------- 35 | config_path : str 36 | Name / Path to the config.yaml File 37 | 38 | Returns 39 | ------- 40 | config : dict 41 | Dictionary containing configuration info 42 | """ 43 | with open(config_path) as file: 44 | # The FullLoader parameter handles the conversion from YAML 45 | # scalar values to Python the dictionary format 46 | config = yaml.load(file, Loader=yaml.FullLoader) 47 | return config 48 | -------------------------------------------------------------------------------- /srt/daemon/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/daemon/__init__.py -------------------------------------------------------------------------------- /srt/daemon/radio_control/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/daemon/radio_control/__init__.py -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_calibrate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/daemon/radio_control/radio_calibrate/__init__.py -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_calibrate/radio_calibrate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: radio_calibrate 9 | # GNU Radio version: 3.8.1.0 10 | 11 | from gnuradio import gr 12 | from gnuradio.filter import firdes 13 | import sys 14 | import signal 15 | from argparse import ArgumentParser 16 | from gnuradio.eng_arg import eng_float, intx 17 | from gnuradio import eng_notation 18 | from gnuradio import zeromq 19 | from . import save_calibration 20 | 21 | 22 | class radio_calibrate(gr.top_block): 23 | def __init__(self, directory_name=".", num_bins=4096): 24 | gr.top_block.__init__(self, "radio_calibrate") 25 | 26 | ################################################## 27 | # Parameters 28 | ################################################## 29 | self.directory_name = directory_name 30 | self.num_bins = num_bins 31 | 32 | ################################################## 33 | # Blocks 34 | ################################################## 35 | self.zeromq_sub_source_0 = zeromq.sub_source( 36 | gr.sizeof_float, num_bins, "tcp://127.0.0.1:5560", 100, True, -1 37 | ) 38 | self.save_calibration = save_calibration.blk( 39 | directory=directory_name, 40 | filename="calibration.json", 41 | vec_length=num_bins, 42 | poly_smoothing_order=25, 43 | ) 44 | 45 | ################################################## 46 | # Connections 47 | ################################################## 48 | self.connect((self.zeromq_sub_source_0, 0), (self.save_calibration, 0)) 49 | 50 | def get_directory_name(self): 51 | return self.directory_name 52 | 53 | def set_directory_name(self, directory_name): 54 | self.directory_name = directory_name 55 | self.save_calibration.directory = self.directory_name 56 | 57 | def get_num_bins(self): 58 | return self.num_bins 59 | 60 | def set_num_bins(self, num_bins): 61 | self.num_bins = num_bins 62 | self.save_calibration.vec_length = self.num_bins 63 | 64 | 65 | def argument_parser(): 66 | parser = ArgumentParser() 67 | parser.add_argument( 68 | "--directory-name", 69 | dest="directory_name", 70 | type=str, 71 | default=".", 72 | help="Set . [default=%(default)r]", 73 | ) 74 | parser.add_argument( 75 | "--num-bins", 76 | dest="num_bins", 77 | type=intx, 78 | default=4096, 79 | help="Set num_bins [default=%(default)r]", 80 | ) 81 | return parser 82 | 83 | 84 | def main(top_block_cls=radio_calibrate, options=None): 85 | if options is None: 86 | options = argument_parser().parse_args() 87 | tb = top_block_cls(directory_name=options.directory_name, num_bins=options.num_bins) 88 | 89 | def sig_handler(sig=None, frame=None): 90 | tb.stop() 91 | tb.wait() 92 | 93 | sys.exit(0) 94 | 95 | signal.signal(signal.SIGINT, sig_handler) 96 | signal.signal(signal.SIGTERM, sig_handler) 97 | 98 | tb.start() 99 | 100 | tb.wait() 101 | 102 | 103 | if __name__ == "__main__": 104 | main() 105 | -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_calibrate/save_calibration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Embedded Python Blocks: 3 | 4 | Each time this file is saved, GRC will instantiate the first class it finds 5 | to get ports and parameters of your block. The arguments to __init__ will 6 | be the parameters. All of them are required to have default values! 7 | """ 8 | 9 | import numpy as np 10 | import numpy.polynomial.polynomial as poly 11 | import json 12 | 13 | from gnuradio import gr 14 | import pmt 15 | 16 | import pathlib 17 | 18 | 19 | class blk(gr.sync_block): 20 | """Embedded Python Block - Calculate Calibration Values""" 21 | 22 | def __init__( 23 | self, 24 | directory=".", 25 | filename="calibration.json", 26 | vec_length=4096, 27 | poly_smoothing_order=25, 28 | ): # only default arguments here 29 | """arguments to this function show up as parameters in GRC""" 30 | gr.sync_block.__init__( 31 | self, 32 | name="Embedded Python Block", # will show up in GRC 33 | in_sig=[(np.float32, vec_length)], 34 | out_sig=None, 35 | ) 36 | # if an attribute with the same name as a parameter is found, 37 | # a callback is registered (properties work, too). 38 | self.directory = directory 39 | self.filename = filename 40 | self.vec_length = vec_length 41 | self.poly_smoothing_order = poly_smoothing_order 42 | self.past_input = np.zeros(vec_length) 43 | self.num_past_input = 0 44 | 45 | def work(self, input_items, output_items): 46 | """Divide Input by Average, Determine Calibration Power, and Save""" 47 | for input_array in input_items[0]: 48 | self.past_input += input_array 49 | self.num_past_input += 1 50 | averaged_input = self.past_input / self.num_past_input 51 | relative_freq_values = np.linspace(-1, 1, self.vec_length) 52 | poly_fit = poly.Polynomial.fit( 53 | relative_freq_values, 54 | averaged_input, 55 | self.poly_smoothing_order, 56 | ) 57 | smoothed_input = poly_fit(relative_freq_values) 58 | average_value = np.average(smoothed_input) 59 | rescaled_input = smoothed_input / average_value 60 | file_output = { 61 | "cal_pwr": average_value, 62 | "cal_values": rescaled_input.tolist(), 63 | } 64 | with open(pathlib.Path(self.directory, self.filename), "w") as outfile: 65 | json.dump(file_output, outfile) 66 | return len(input_items[0]) 67 | -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_process/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/daemon/radio_control/radio_process/__init__.py -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_process/add_clock_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Embedded Python Blocks: 3 | 4 | Each time this file is saved, GRC will instantiate the first class it finds 5 | to get ports and parameters of your block. The arguments to __init__ will 6 | be the parameters. All of them are required to have default values! 7 | """ 8 | 9 | import numpy as np 10 | from gnuradio import gr 11 | import time 12 | import pmt 13 | 14 | 15 | def make_time_pair(t): 16 | return pmt.make_tuple( 17 | pmt.to_pmt(int(np.trunc(t))), pmt.to_pmt(t - int(np.trunc(t))) 18 | ) 19 | 20 | 21 | class clk(gr.sync_block): 22 | def __init__(self, nsamps=8192): 23 | gr.sync_block.__init__( 24 | self, name="Add Clock Tags", in_sig=[np.complex64], out_sig=[np.complex64] 25 | ) 26 | self.pmt_key = pmt.intern("rx_time") 27 | self.offset = 0 28 | self.nsamps = nsamps 29 | 30 | def work(self, input_items, output_items): 31 | nitems = len(input_items[0]) + self.nitems_written(0) 32 | if self.nitems_written(0) == 0: 33 | self.add_item_tag(0, 0, self.pmt_key, make_time_pair(time.time())) 34 | while (nitems - self.offset) > self.nsamps: 35 | self.offset += self.nsamps 36 | self.add_item_tag(0, self.offset, self.pmt_key, make_time_pair(time.time())) 37 | output_items[0][:] = input_items[0] # copy input to output 38 | return len(output_items[0]) 39 | -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_save_raw/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/daemon/radio_control/radio_save_raw/__init__.py -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_save_raw/radio_save_raw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: radio_save_raw 9 | # GNU Radio version: 3.8.1.0 10 | 11 | from gnuradio import gr 12 | from gnuradio.filter import firdes 13 | import sys 14 | import signal 15 | from argparse import ArgumentParser 16 | from gnuradio.eng_arg import eng_float, intx 17 | from gnuradio import eng_notation 18 | from gnuradio import zeromq 19 | import numpy as np 20 | import gr_digital_rf 21 | 22 | 23 | class radio_save_raw(gr.top_block): 24 | def __init__(self, directory_name="./rf_data", samp_rate=2400000): 25 | gr.top_block.__init__(self, "radio_save_raw") 26 | 27 | ################################################## 28 | # Parameters 29 | ################################################## 30 | self.directory_name = directory_name 31 | self.samp_rate = samp_rate 32 | 33 | ################################################## 34 | # Blocks 35 | ################################################## 36 | self.zeromq_sub_source_0 = zeromq.sub_source( 37 | gr.sizeof_gr_complex, 1, "tcp://127.0.0.1:5558", 100, True, -1 38 | ) 39 | self.gr_digital_rf_digital_rf_channel_sink_0 = ( 40 | gr_digital_rf.digital_rf_channel_sink( 41 | channel_dir=directory_name, 42 | dtype=np.complex64, 43 | subdir_cadence_secs=3600, 44 | file_cadence_millisecs=1000, 45 | sample_rate_numerator=int(samp_rate), 46 | sample_rate_denominator=1, 47 | start="now", 48 | ignore_tags=False, 49 | is_complex=True, 50 | num_subchannels=1, 51 | uuid_str=None, 52 | center_frequencies=[], 53 | metadata={}, 54 | is_continuous=True, 55 | compression_level=0, 56 | checksum=False, 57 | marching_periods=True, 58 | stop_on_skipped=False, 59 | stop_on_time_tag=False, 60 | debug=False, 61 | min_chunksize=None, 62 | ) 63 | ) 64 | 65 | ################################################## 66 | # Connections 67 | ################################################## 68 | self.connect( 69 | (self.zeromq_sub_source_0, 0), 70 | (self.gr_digital_rf_digital_rf_channel_sink_0, 0), 71 | ) 72 | 73 | def get_directory_name(self): 74 | return self.directory_name 75 | 76 | def set_directory_name(self, directory_name): 77 | self.directory_name = directory_name 78 | 79 | def get_samp_rate(self): 80 | return self.samp_rate 81 | 82 | def set_samp_rate(self, samp_rate): 83 | self.samp_rate = samp_rate 84 | 85 | 86 | def argument_parser(): 87 | parser = ArgumentParser() 88 | parser.add_argument( 89 | "--directory-name", 90 | dest="directory_name", 91 | type=str, 92 | default="./rf_data", 93 | help="Set ./rf_data [default=%(default)r]", 94 | ) 95 | parser.add_argument( 96 | "--samp-rate", 97 | dest="samp_rate", 98 | type=intx, 99 | default=2400000, 100 | help="Set samp_rate [default=%(default)r]", 101 | ) 102 | return parser 103 | 104 | 105 | def main(top_block_cls=radio_save_raw, options=None): 106 | if options is None: 107 | options = argument_parser().parse_args() 108 | tb = top_block_cls( 109 | directory_name=options.directory_name, samp_rate=options.samp_rate 110 | ) 111 | 112 | def sig_handler(sig=None, frame=None): 113 | tb.stop() 114 | tb.wait() 115 | 116 | sys.exit(0) 117 | 118 | signal.signal(signal.SIGINT, sig_handler) 119 | signal.signal(signal.SIGTERM, sig_handler) 120 | 121 | tb.start() 122 | 123 | tb.wait() 124 | 125 | 126 | if __name__ == "__main__": 127 | main() 128 | -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_save_spec_fits/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/daemon/radio_control/radio_save_spec_fits/__init__.py -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_save_spec_fits/radio_save_spec_fits.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: radio_save_spec_fits 9 | # GNU Radio version: 3.8.1.0 10 | 11 | from gnuradio import gr 12 | from gnuradio.filter import firdes 13 | import sys 14 | import signal 15 | from argparse import ArgumentParser 16 | from gnuradio.eng_arg import eng_float, intx 17 | from gnuradio import eng_notation 18 | from gnuradio import zeromq 19 | from . import save_fits_file 20 | 21 | 22 | class radio_save_spec_fits(gr.top_block): 23 | def __init__( 24 | self, 25 | directory_name=".", 26 | file_name="test.fits", 27 | num_bins=4096, 28 | samp_rate=2400000, 29 | ): 30 | gr.top_block.__init__(self, "radio_save_spec_fits") 31 | 32 | ################################################## 33 | # Parameters 34 | ################################################## 35 | self.directory_name = directory_name 36 | self.file_name = file_name 37 | self.num_bins = num_bins 38 | self.samp_rate = samp_rate 39 | 40 | ################################################## 41 | # Blocks 42 | ################################################## 43 | self.zeromq_sub_source_0 = zeromq.sub_source( 44 | gr.sizeof_float, num_bins, "tcp://127.0.0.1:5562", 100, True, -1 45 | ) 46 | self.save_fits_file = save_fits_file.blk( 47 | directory=directory_name, filename=file_name, vec_length=num_bins 48 | ) 49 | 50 | ################################################## 51 | # Connections 52 | ################################################## 53 | self.connect((self.zeromq_sub_source_0, 0), (self.save_fits_file, 0)) 54 | 55 | def get_directory_name(self): 56 | return self.directory_name 57 | 58 | def set_directory_name(self, directory_name): 59 | self.directory_name = directory_name 60 | self.save_fits_file.directory = self.directory_name 61 | 62 | def get_file_name(self): 63 | return self.file_name 64 | 65 | def set_file_name(self, file_name): 66 | self.file_name = file_name 67 | self.save_fits_file.filename = self.file_name 68 | 69 | def get_num_bins(self): 70 | return self.num_bins 71 | 72 | def set_num_bins(self, num_bins): 73 | self.num_bins = num_bins 74 | self.save_fits_file.vec_length = self.num_bins 75 | 76 | def get_samp_rate(self): 77 | return self.samp_rate 78 | 79 | def set_samp_rate(self, samp_rate): 80 | self.samp_rate = samp_rate 81 | 82 | 83 | def argument_parser(): 84 | parser = ArgumentParser() 85 | parser.add_argument( 86 | "--directory-name", 87 | dest="directory_name", 88 | type=str, 89 | default=".", 90 | help="Set . [default=%(default)r]", 91 | ) 92 | parser.add_argument( 93 | "--file-name", 94 | dest="file_name", 95 | type=str, 96 | default="test.fits", 97 | help="Set test.fits [default=%(default)r]", 98 | ) 99 | parser.add_argument( 100 | "--num-bins", 101 | dest="num_bins", 102 | type=intx, 103 | default=4096, 104 | help="Set num_bins [default=%(default)r]", 105 | ) 106 | parser.add_argument( 107 | "--samp-rate", 108 | dest="samp_rate", 109 | type=intx, 110 | default=2400000, 111 | help="Set samp_rate [default=%(default)r]", 112 | ) 113 | return parser 114 | 115 | 116 | def main(top_block_cls=radio_save_spec_fits, options=None): 117 | if options is None: 118 | options = argument_parser().parse_args() 119 | tb = top_block_cls( 120 | directory_name=options.directory_name, 121 | file_name=options.file_name, 122 | num_bins=options.num_bins, 123 | samp_rate=options.samp_rate, 124 | ) 125 | 126 | def sig_handler(sig=None, frame=None): 127 | tb.stop() 128 | tb.wait() 129 | 130 | sys.exit(0) 131 | 132 | signal.signal(signal.SIGINT, sig_handler) 133 | signal.signal(signal.SIGTERM, sig_handler) 134 | 135 | tb.start() 136 | 137 | tb.wait() 138 | 139 | 140 | if __name__ == "__main__": 141 | main() 142 | -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_save_spec_fits/save_fits_file.py: -------------------------------------------------------------------------------- 1 | """ 2 | Embedded Python Blocks: 3 | 4 | Each time this file is saved, GRC will instantiate the first class it finds 5 | to get ports and parameters of your block. The arguments to __init__ will 6 | be the parameters. All of them are required to have default values! 7 | """ 8 | 9 | import numpy as np 10 | from gnuradio import gr 11 | import pmt 12 | import json 13 | 14 | import pathlib 15 | from datetime import datetime, timezone 16 | from astropy.io import fits 17 | 18 | 19 | class blk(gr.sync_block): 20 | """Embedded Python Block - Saving""" 21 | 22 | def __init__( 23 | self, directory=".", filename="test.fits", vec_length=4096 24 | ): # only default arguments here 25 | """arguments to this function show up as parameters in GRC""" 26 | gr.sync_block.__init__( 27 | self, 28 | name="Embedded Python Block", # will show up in GRC 29 | in_sig=[(np.float32, vec_length)], 30 | out_sig=None, 31 | ) 32 | # if an attribute with the same name as a parameter is found, 33 | # a callback is registered (properties work, too). 34 | self.directory = directory 35 | self.filename = filename 36 | self.vec_length = vec_length 37 | 38 | def work(self, input_items, output_items): 39 | """Saving Spectrum Data to a FITS File""" 40 | file_path = pathlib.Path(self.directory, self.filename) 41 | for i, input_array in enumerate(input_items[0]): 42 | file = open(file_path, "ab+") 43 | tags = self.get_tags_in_window(0, 0, len(input_items[0])) 44 | tags_dict = { 45 | pmt.to_python(tag.key): pmt.to_python(tag.value) for tag in tags 46 | } 47 | time_since_epoch = tags_dict["rx_time"][0] + tags_dict["rx_time"][1] 48 | date = datetime.fromtimestamp(time_since_epoch, timezone.utc) 49 | metadata = tags_dict["metadata"] 50 | samp_rate = metadata["samp_rate"] 51 | num_integrations = metadata["num_integrations"] 52 | freq = metadata["freq"] 53 | num_bins = metadata["num_bins"] 54 | soutrack = metadata["soutrack"] 55 | 56 | hdr = fits.Header() 57 | hdr["BUNIT"] = "K" 58 | hdr["CTYPE1"] = "Freq" 59 | hdr["CRPIX1"] = num_bins / float(2) # Reference pixel (center) 60 | hdr["CRVAL1"] = freq # Center, USRP, frequency 61 | hdr["CDELT1"] = samp_rate / (1 * num_bins) # Channel width 62 | hdr["CUNIT1"] = "Hz" 63 | 64 | hdr["TELESCOP"] = "SmallRadioTelescope" 65 | hdr["OBJECT"] = soutrack 66 | hdr["OBSTIME"] = (num_bins * num_integrations) / samp_rate 67 | 68 | hdr["DATE-OBS"] = date.strftime("%Y-%m-%d") 69 | hdr["UTC"] = date.strftime("%H:%M:00%s") 70 | hdr["METADATA"] = json.dumps(metadata) 71 | 72 | fits.append(file, input_array, hdr) 73 | file.close() 74 | # p = np.sum(input_array) 75 | # a = len(input_array) 76 | # pwr = (tsys + tcal) * p / (a * calpwr) 77 | # ppwr = pwr - tsys 78 | return len(input_items[0]) 79 | -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_save_spec_rad/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/daemon/radio_control/radio_save_spec_rad/__init__.py -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_save_spec_rad/radio_save_spec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: radio_save_spec 9 | # GNU Radio version: 3.8.1.0 10 | 11 | from gnuradio import gr 12 | from gnuradio.filter import firdes 13 | import sys 14 | import signal 15 | from argparse import ArgumentParser 16 | from gnuradio.eng_arg import eng_float, intx 17 | from gnuradio import eng_notation 18 | from gnuradio import zeromq 19 | from . import save_rad_file 20 | 21 | 22 | class radio_save_spec(gr.top_block): 23 | def __init__( 24 | self, directory_name=".", file_name="test.rad", num_bins=4096, samp_rate=2400000 25 | ): 26 | gr.top_block.__init__(self, "radio_save_spec") 27 | 28 | ################################################## 29 | # Parameters 30 | ################################################## 31 | self.directory_name = directory_name 32 | self.file_name = file_name 33 | self.num_bins = num_bins 34 | self.samp_rate = samp_rate 35 | 36 | ################################################## 37 | # Blocks 38 | ################################################## 39 | self.zeromq_sub_source_0 = zeromq.sub_source( 40 | gr.sizeof_float, num_bins, "tcp://127.0.0.1:5562", 100, True, -1 41 | ) 42 | self.save_rad_file = save_rad_file.blk( 43 | directory=directory_name, filename=file_name, vec_length=num_bins 44 | ) 45 | 46 | ################################################## 47 | # Connections 48 | ################################################## 49 | self.connect((self.zeromq_sub_source_0, 0), (self.save_rad_file, 0)) 50 | 51 | def get_directory_name(self): 52 | return self.directory_name 53 | 54 | def set_directory_name(self, directory_name): 55 | self.directory_name = directory_name 56 | self.save_rad_file.directory = self.directory_name 57 | 58 | def get_file_name(self): 59 | return self.file_name 60 | 61 | def set_file_name(self, file_name): 62 | self.file_name = file_name 63 | self.save_rad_file.filename = self.file_name 64 | 65 | def get_num_bins(self): 66 | return self.num_bins 67 | 68 | def set_num_bins(self, num_bins): 69 | self.num_bins = num_bins 70 | self.save_rad_file.vec_length = self.num_bins 71 | 72 | def get_samp_rate(self): 73 | return self.samp_rate 74 | 75 | def set_samp_rate(self, samp_rate): 76 | self.samp_rate = samp_rate 77 | 78 | 79 | def argument_parser(): 80 | parser = ArgumentParser() 81 | parser.add_argument( 82 | "--directory-name", 83 | dest="directory_name", 84 | type=str, 85 | default=".", 86 | help="Set . [default=%(default)r]", 87 | ) 88 | parser.add_argument( 89 | "--file-name", 90 | dest="file_name", 91 | type=str, 92 | default="test.rad", 93 | help="Set test.rad [default=%(default)r]", 94 | ) 95 | parser.add_argument( 96 | "--num-bins", 97 | dest="num_bins", 98 | type=intx, 99 | default=4096, 100 | help="Set num_bins [default=%(default)r]", 101 | ) 102 | parser.add_argument( 103 | "--samp-rate", 104 | dest="samp_rate", 105 | type=intx, 106 | default=2400000, 107 | help="Set samp_rate [default=%(default)r]", 108 | ) 109 | return parser 110 | 111 | 112 | def main(top_block_cls=radio_save_spec, options=None): 113 | if options is None: 114 | options = argument_parser().parse_args() 115 | tb = top_block_cls( 116 | directory_name=options.directory_name, 117 | file_name=options.file_name, 118 | num_bins=options.num_bins, 119 | samp_rate=options.samp_rate, 120 | ) 121 | 122 | def sig_handler(sig=None, frame=None): 123 | tb.stop() 124 | tb.wait() 125 | 126 | sys.exit(0) 127 | 128 | signal.signal(signal.SIGINT, sig_handler) 129 | signal.signal(signal.SIGTERM, sig_handler) 130 | 131 | tb.start() 132 | 133 | tb.wait() 134 | 135 | 136 | if __name__ == "__main__": 137 | main() 138 | -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_save_spec_rad/save_rad_file.py: -------------------------------------------------------------------------------- 1 | """ 2 | Embedded Python Blocks: 3 | 4 | Each time this file is saved, GRC will instantiate the first class it finds 5 | to get ports and parameters of your block. The arguments to __init__ will 6 | be the parameters. All of them are required to have default values! 7 | """ 8 | 9 | import numpy as np 10 | from gnuradio import gr 11 | import pmt 12 | 13 | import pathlib 14 | from datetime import datetime, timezone 15 | from math import sqrt 16 | 17 | 18 | # String Formatting Constants 19 | header_format = ( 20 | "DATE %4d:%03d:%02d:%02d:%02d obsn %3d az %4.1f el %3.1f freq_MHz " 21 | "%10.4f Tsys %6.3f Tant %6.3f vlsr %7.2f glat %6.3f glon %6.3f source %s\n" 22 | ) 23 | start_format = ( 24 | "Fstart %8.3f fstop %8.3f spacing %8.6f bw %8.3f fbw %8.3f MHz nfreq " 25 | "%d nsam %d npoint %d integ %5.0f sigma %8.3f bsw %d\n" 26 | ) 27 | integration_format = "Spectrum %6.0f integration periods\n" 28 | number_format = "%8.3f " 29 | 30 | 31 | def parse_time(rx_time): 32 | time_since_epoch = rx_time[0] + rx_time[1] 33 | date = datetime.fromtimestamp(time_since_epoch, timezone.utc) 34 | new_year_day = datetime(year=date.year, month=1, day=1, tzinfo=timezone.utc) 35 | day_of_year = (date - new_year_day).days + 1 36 | return date.year, day_of_year, date.hour, date.minute, date.second 37 | 38 | 39 | def parse_metadata(metadata): 40 | motor_az = metadata["motor_az"] 41 | motor_el = metadata["motor_el"] 42 | samp_rate = metadata["samp_rate"] 43 | num_integrations = metadata["num_integrations"] 44 | freq = metadata["freq"] 45 | num_bins = metadata["num_bins"] 46 | tsys = metadata["tsys"] 47 | tcal = metadata["tcal"] 48 | cal_pwr = metadata["cal_pwr"] 49 | vlsr = metadata["vlsr"] 50 | glat = metadata["glat"] 51 | glon = metadata["glon"] 52 | soutrack = metadata["soutrack"] 53 | bsw = metadata["bsw"] 54 | return ( 55 | motor_az, 56 | motor_el, 57 | freq / pow(10, 6), 58 | samp_rate / pow(10, 6), 59 | num_integrations, 60 | num_bins, 61 | tsys, 62 | tcal, 63 | cal_pwr, 64 | vlsr, 65 | glat, 66 | glon, 67 | soutrack, 68 | bsw, 69 | ) 70 | 71 | 72 | class blk( 73 | gr.sync_block 74 | ): # other base classes are basic_block, decim_block, interp_block 75 | """Embedded Python Block example - a simple multiply const""" 76 | 77 | def __init__( 78 | self, directory=".", filename="test.rad", vec_length=4096 79 | ): # only default arguments here 80 | """arguments to this function show up as parameters in GRC""" 81 | gr.sync_block.__init__( 82 | self, 83 | name="Embedded Python Block", # will show up in GRC 84 | in_sig=[(np.float32, vec_length)], 85 | out_sig=None, 86 | ) 87 | # if an attribute with the same name as a parameter is found, 88 | # a callback is registered (properties work, too). 89 | self.directory = directory 90 | self.filename = filename 91 | self.vec_length = vec_length 92 | self.obsn = 0 93 | 94 | def work(self, input_items, output_items): 95 | """Saves the data to a rad file. 96 | 97 | Takes the input item, (float,vec) along with the output items [none] and runs the rad file save. 98 | 99 | Parameters 100 | ---------- 101 | input_items : list 102 | 103 | 104 | """ 105 | file = open(pathlib.Path(self.directory, self.filename), "a+") 106 | tags = self.get_tags_in_window(0, 0, len(input_items[0])) 107 | latest_data_dict = { 108 | pmt.to_python(tag.key): pmt.to_python(tag.value) for tag in tags 109 | } 110 | yr, da, hr, mn, sc = parse_time(latest_data_dict["rx_time"]) 111 | ( 112 | aznow, 113 | elnow, 114 | freq, 115 | bw, 116 | integ, 117 | nfreq, 118 | tsys, 119 | tcal, 120 | calpwr, 121 | vlsr, 122 | glat, 123 | glon, 124 | soutrack, 125 | bsw, 126 | ) = parse_metadata(latest_data_dict["metadata"]) 127 | fbw = bw # Old SRT Software Had Relative Bandwidth Limits and An Unchanging Sample Rate 128 | f1 = 0 # Relative Lower Bound (Since Sample Rate Determines Bandwidth) 129 | f2 = 1 # Relative Upper Bound (Since Sample Rate Determines Bandwidth) 130 | istart = f1 * nfreq + 0.5 131 | istop = f2 * nfreq + 0.5 132 | efflofreq = freq - bw * 0.5 133 | freqsep = bw / nfreq 134 | nsam = nfreq # Old SRT Software Had a Specific Bundle of Samples Shuffled Out and Processed at a Time 135 | sigma = tsys / sqrt((nsam * integ / (2.0e6 * bw)) * freqsep * 1e6) 136 | for input_array in input_items[0]: 137 | p = np.sum(input_array) 138 | a = len(input_array) 139 | pwr = (tsys + tcal) * p / (a * calpwr) 140 | ppwr = pwr - tsys 141 | header_line = header_format % ( 142 | yr, 143 | da, 144 | hr, 145 | mn, 146 | sc, 147 | self.obsn, 148 | aznow, 149 | elnow, 150 | freq, 151 | tsys, 152 | ppwr, 153 | vlsr, 154 | glat, 155 | glon, 156 | soutrack, 157 | ) 158 | start_line = start_format % ( 159 | istart * bw / nfreq + efflofreq, 160 | istop * bw / nfreq + efflofreq, 161 | freqsep, 162 | bw, 163 | fbw, 164 | nfreq, 165 | nsam, 166 | istop - istart, 167 | integ * nsam / (2.0e6 * bw), 168 | sigma, 169 | bsw, 170 | ) 171 | integration_line = integration_format % integ 172 | file.writelines([header_line, start_line, integration_line]) 173 | for val in input_array: 174 | file.write(number_format % val) 175 | file.write("\n") 176 | self.obsn += 1 177 | 178 | file.close() 179 | return len(input_items[0]) 180 | -------------------------------------------------------------------------------- /srt/daemon/radio_control/radio_task_starter.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | from pathlib import Path 3 | from os.path import expanduser 4 | from argparse import Namespace 5 | import time 6 | 7 | from .radio_process import radio_process 8 | from .radio_calibrate import radio_calibrate 9 | from .radio_save_raw import radio_save_raw 10 | from .radio_save_spec_rad import radio_save_spec 11 | from .radio_save_spec_fits import radio_save_spec_fits 12 | 13 | 14 | class RadioTask(multiprocessing.Process): 15 | """ 16 | Multiprocessing Wrapper Process Superclass for Calling Unmodified GNU Radio Companion Scripts 17 | """ 18 | 19 | def __init__(self, main_method, **kwargs): 20 | super().__init__( 21 | target=main_method, 22 | kwargs={"options": Namespace(**kwargs)}, 23 | daemon=True, 24 | ) 25 | 26 | 27 | class RadioProcessTask(RadioTask): 28 | """ 29 | Multiprocessing Wrapper Process for Starting the Processing of Radio Signals 30 | """ 31 | 32 | def __init__(self, num_bins, num_integrations): 33 | super().__init__( 34 | radio_process.main, num_bins=num_bins, num_integrations=num_integrations 35 | ) 36 | 37 | 38 | class RadioSaveRawTask(RadioTask): 39 | """ 40 | Multiprocessing Wrapper Process for Saving Raw I/Q Samples 41 | """ 42 | 43 | def __init__(self, samp_rate, root_save_directory, directory): 44 | if directory is None: 45 | directory = time.strftime("SRT_RAW_SAVE-%Y_%m_%d_%H_%M_%S") 46 | path = str(Path(expanduser(root_save_directory), directory).absolute()) 47 | super().__init__(radio_save_raw.main, directory_name=path, samp_rate=samp_rate) 48 | 49 | 50 | class RadioSaveSpecRadTask(RadioTask): 51 | """ 52 | Multiprocessing Wrapper Process for Saving Spectrum Data in .rad Files 53 | """ 54 | 55 | def __init__(self, samp_rate, num_bins, root_save_directory, file_name): 56 | if file_name is None: 57 | file_name = time.strftime("%y-%j-%H_%M_%S.rad") 58 | path = str(Path(expanduser(root_save_directory)).absolute()) 59 | super().__init__( 60 | radio_save_spec.main, 61 | directory_name=path, 62 | samp_rate=samp_rate, 63 | num_bins=num_bins, 64 | file_name=file_name, 65 | ) 66 | 67 | 68 | class RadioSaveSpecFitsTask(RadioTask): 69 | """ 70 | Multiprocessing Wrapper Process for Saving Spectrum Data in .fits Files 71 | """ 72 | 73 | def __init__(self, samp_rate, num_bins, root_save_directory, file_name): 74 | if file_name is None: 75 | file_name = time.strftime("SRT_SPEC_SAVE-%Y_%m_%d_%H_%M_%S.fits") 76 | path = str(Path(expanduser(root_save_directory)).absolute()) 77 | super().__init__( 78 | radio_save_spec_fits.main, 79 | directory_name=path, 80 | samp_rate=samp_rate, 81 | num_bins=num_bins, 82 | file_name=file_name, 83 | ) 84 | 85 | 86 | class RadioCalibrateTask(RadioTask): 87 | """ 88 | Multiprocessing Wrapper Process for Generating a New calibration.json 89 | """ 90 | 91 | def __init__(self, num_bins, config_directory): 92 | path = str(Path(expanduser(config_directory)).absolute()) 93 | super().__init__( 94 | radio_calibrate.main, 95 | directory_name=path, 96 | num_bins=num_bins, 97 | ) 98 | -------------------------------------------------------------------------------- /srt/daemon/rotor_control/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/daemon/rotor_control/__init__.py -------------------------------------------------------------------------------- /srt/daemon/rotor_control/rotors.py: -------------------------------------------------------------------------------- 1 | """rotors.py 2 | 3 | Module for Managing Different Motor Objects 4 | 5 | """ 6 | from enum import Enum 7 | 8 | from .motors import NoMotor, Rot2Motor, H180Motor, PushRodMotor 9 | 10 | 11 | def angle_within_range(angle, limits): 12 | lower_limit, upper_limit = limits 13 | if lower_limit <= upper_limit: 14 | return lower_limit <= angle <= upper_limit 15 | else: 16 | return not lower_limit < angle < upper_limit 17 | 18 | 19 | class RotorType(Enum): 20 | """ 21 | Enum Class for the Different Types of 22 | """ 23 | 24 | NONE = "NONE" 25 | ROT2 = "ALFASPID" 26 | H180 = "H180MOUNT" 27 | PUSH_ROD = "PUSHROD" 28 | 29 | 30 | class Rotor: 31 | """ 32 | Class for Controlling Any Rotor Motor Through a Common Interface 33 | 34 | See Also 35 | -------- 36 | motors.py 37 | """ 38 | 39 | def __init__(self, motor_type, port, baudrate, az_limits, el_limits): 40 | """Initializes the Rotor with its Motor Object 41 | 42 | Parameters 43 | ---------- 44 | motor_type : RotorType 45 | String enum Identifying the Type of Motor 46 | port : str 47 | Serial Port Identifier String for Communicating with the Motor 48 | az_limits : (float, float) 49 | Tuple of Lower and Upper Azimuth Limits 50 | el_limits : (float, float) 51 | Tuple of Lower and Upper Elevation Limits 52 | """ 53 | if motor_type == RotorType.NONE or motor_type == RotorType.NONE.value: 54 | self.motor = NoMotor(port, baudrate, az_limits, el_limits) 55 | elif motor_type == RotorType.ROT2 or motor_type == RotorType.ROT2.value: 56 | self.motor = Rot2Motor(port, baudrate, az_limits, el_limits) 57 | elif motor_type == RotorType.H180 or motor_type == RotorType.H180.value: 58 | self.motor = H180Motor(port, baudrate, az_limits, el_limits) 59 | elif motor_type == RotorType.PUSH_ROD == RotorType.PUSH_ROD.value: 60 | self.motor = PushRodMotor(port, baudrate, az_limits, el_limits) 61 | else: 62 | raise ValueError("Not a known motor type") 63 | 64 | self.az_limits = az_limits 65 | self.el_limits = el_limits 66 | 67 | def get_azimuth_elevation(self): 68 | """Latest Known Azimuth and Elevation 69 | 70 | Returns 71 | ------- 72 | (float, float) 73 | Azimuth and Elevation Coordinate as a Tuple of Floats 74 | """ 75 | return self.motor.status() 76 | 77 | def set_azimuth_elevation(self, az, el): 78 | """Sets the Azimuth and Elevation of the Motor 79 | 80 | Parameters 81 | ---------- 82 | az : float 83 | Azimuth Coordinate to Point At 84 | el : float 85 | Elevation Coordinate to Point At 86 | 87 | Returns 88 | ------- 89 | None 90 | """ 91 | if self.angles_within_bounds(az, el): 92 | self.motor.point(az, el) 93 | else: 94 | raise ValueError("Angle Not Within Bounds") 95 | 96 | def angles_within_bounds(self, az, el): 97 | return angle_within_range(az, self.az_limits) and angle_within_range( 98 | el, self.el_limits 99 | ) 100 | -------------------------------------------------------------------------------- /srt/daemon/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/daemon/utilities/__init__.py -------------------------------------------------------------------------------- /srt/daemon/utilities/functions.py: -------------------------------------------------------------------------------- 1 | """functions.py 2 | 3 | Extra Functions Condensed for Ease-of-Use 4 | 5 | """ 6 | import zmq 7 | import numpy as np 8 | 9 | 10 | def angle_within_range(actual_angle, desired_angle, bounds=0.5): 11 | """Determines if Angles are Within a Threshold of One Another 12 | 13 | Parameters 14 | ---------- 15 | actual_angle : float 16 | Value of the Actual Current Angle 17 | desired_angle : float 18 | Value of the Desired Angel 19 | bounds : float 20 | Maximum Difference Between Actual and Desired Tolerated 21 | 22 | Returns 23 | ------- 24 | bool 25 | Whether Angles Were Within Threshold 26 | """ 27 | return abs(actual_angle - desired_angle) < bounds 28 | 29 | 30 | def azel_within_range(actual_azel, desired_azel, bounds=(0.5, 0.5)): 31 | """Determines if AzEls are Within a Threshold of One Another 32 | 33 | Parameters 34 | ---------- 35 | actual_azel : (float, float) 36 | Value of the Actual Current Azimuth and Elevation 37 | desired_azel : (float, float) 38 | Value of the Desired Azimuth and Elevation 39 | bounds : (float, float) 40 | Maximum Difference Between Actual and Desired Tolerated 41 | Returns 42 | ------- 43 | bool 44 | Whether Angles Were Within Threshold 45 | """ 46 | actual_az, actual_el = actual_azel 47 | desired_az, desired_el = desired_azel 48 | bounds_az, bounds_el = bounds 49 | return angle_within_range(actual_az, desired_az, bounds_az) and angle_within_range( 50 | actual_el, desired_el, bounds_el 51 | ) 52 | 53 | 54 | def get_spectrum(port=5561): 55 | """Quickly opens a zmq socket and gets a spectrum 56 | 57 | Parameters 58 | ---------- 59 | port : int 60 | Number that the spectrum data is broadcast on. 61 | 62 | Returns 63 | ------- 64 | var : array_like 65 | Spectrum array as numpy array 66 | 67 | """ 68 | context = zmq.Context() 69 | socket = context.socket(zmq.SUB) 70 | socket.connect("tcp://localhost:%s" % port) 71 | socket.subscribe("") 72 | try: 73 | rec = socket.recv() 74 | var = np.frombuffer(rec, dtype="float32") 75 | except: 76 | return None 77 | 78 | return var 79 | 80 | 81 | def sinc_interp2d(x, y, values, dx, dy, xout, yout): 82 | """Perform a sinc interpolation 83 | 84 | Parameters 85 | ---------- 86 | x : array_like 87 | A 1-d array of x values. 88 | y : array_like 89 | A 1-d array of y values. 90 | values : array_like 91 | A 1-d array of values that will be interpolated. 92 | dx : float 93 | Sampling rate along the x axis. 94 | dy : float 95 | Sampling rate along the y axis. 96 | xout : array_like 97 | 2-d array for x axis sampling. 98 | yout : array_like 99 | 2-d array for y axis sampling. 100 | 101 | Returns 102 | ------- 103 | val_out : array_like 104 | 2-d array for of the values at the new sampling sampling. 105 | """ 106 | 107 | val_out = np.zeros_like(xout) 108 | 109 | for x_c, y_c, v_c in zip(x, y, values): 110 | x_1 = (xout - x_c) / dx 111 | y_1 = (yout - y_c) / dy 112 | val_out += float(v_c) * np.sinc(x_1) * np.sinc(y_1) 113 | 114 | return val_out 115 | 116 | 117 | def npoint_interp(az, el, val, d_az, d_el, nout=100): 118 | """Interpolate the result of the npoint scan on to a grid. 119 | 120 | Parameters 121 | ---------- 122 | az : array_like 123 | A 1-d array of az values. 124 | el : array_like 125 | A 1-d array of el values. 126 | val : array_like 127 | A 1-d array of values that will be interpolated. 128 | d_az : float 129 | Sampling rate along the az axis. 130 | d_el : float 131 | Sampling rate along the el axis. 132 | nout : int 133 | Number of samples per axis. 134 | 135 | Returns 136 | ------- 137 | azarr : array_like 138 | 2-d array for az axis sampling. 139 | elarr : array_like 140 | 2-d array for el axis sampling. 141 | val_out : array_like 142 | 2-d array for of the values at the new sampling sampling. 143 | """ 144 | 145 | azmin = np.nanmin(az) 146 | azmax = np.nanmax(az) 147 | azvec = np.linspace(azmin, azmax, nout) 148 | 149 | elmin = np.nanmin(el) 150 | elmax = np.nanmax(el) 151 | elvec = np.linspace(elmin, elmax, nout) 152 | azarr, elarr = np.meshgrid(azvec, elvec, indexing="xy") 153 | 154 | val_out = sinc_interp2d( 155 | az, el, val, d_az, d_el, azarr.astype(float), elarr.astype(float) 156 | ) 157 | 158 | return azarr, elarr, val_out 159 | -------------------------------------------------------------------------------- /srt/daemon/utilities/object_tracker.py: -------------------------------------------------------------------------------- 1 | """object_tracker.py 2 | 3 | Module for Tracking and Caching the Azimuth-Elevation Coords of Celestial Objects 4 | 5 | """ 6 | from astropy.coordinates import SkyCoord, EarthLocation, get_sun, get_moon 7 | from astropy.coordinates import ICRS, Galactic, FK4, CIRS, AltAz 8 | from astropy.utils.iers.iers import conf 9 | from astropy.table import Table 10 | from astropy.time import Time 11 | import astropy.units as u 12 | 13 | import numpy as np 14 | from pathlib import Path 15 | from copy import deepcopy 16 | 17 | 18 | root_folder = Path(__file__).parent.parent.parent.parent 19 | 20 | 21 | class EphemerisTracker: 22 | """ 23 | Enables Calculating the AzEl Coordinates of the Bodies Specified in sky_coords.csv 24 | """ 25 | 26 | def __init__( 27 | self, 28 | observer_lat, 29 | observer_lon, 30 | observer_elevation=0, 31 | config_file="config/sky_coords.csv", 32 | refresh_time=10, 33 | auto_download=True, 34 | ): 35 | """Initializer for EphemerisTracker 36 | 37 | - Reads CSV File for Objects to Track 38 | - Converts to Common Coordinate System 39 | - Populates AzEl Dictionary with Current Values 40 | 41 | Parameters 42 | ---------- 43 | observer_lat : float 44 | Observer's Location Latitude in degrees 45 | observer_lon : float 46 | Observer's Location Longitude in degrees 47 | observer_elevation : float 48 | Observer's Location Elevation in meters 49 | config_file : str 50 | Location of the List File for Bodies Being Tracked 51 | refresh_time : float 52 | Maximum Amount of Time Cache is Valid 53 | auto_download : bool 54 | Whether AstroPy is Permitted to Use Internet to Increase Accuracy 55 | """ 56 | 57 | table = Table.read(Path(root_folder, config_file), format="ascii.csv") 58 | 59 | self.sky_coord_names = {} 60 | sky_coords_ra = np.zeros(len(table)) 61 | sky_coords_dec = np.zeros(len(table)) 62 | 63 | for index, row in enumerate(table): 64 | coordinate_system = row["coordinate_system"] 65 | coordinate_a = row["coordinate_a"] 66 | coordinate_b = row["coordinate_b"] 67 | name = row["name"] 68 | unit = ( 69 | u.deg 70 | if coordinate_system == Galactic.__name__.lower() 71 | else (u.hourangle, u.deg) 72 | ) 73 | sky_coord = SkyCoord( 74 | coordinate_a, coordinate_b, frame=coordinate_system, unit=unit 75 | ) 76 | sky_coord_transformed = sky_coord.transform_to(CIRS) 77 | sky_coords_ra[index] = sky_coord_transformed.ra.degree 78 | sky_coords_dec[index] = sky_coord_transformed.dec.degree 79 | self.sky_coord_names[name] = index 80 | 81 | self.sky_coords = SkyCoord( 82 | ra=sky_coords_ra * u.deg, dec=sky_coords_dec * u.deg, frame=CIRS 83 | ) 84 | self.location = EarthLocation.from_geodetic( 85 | lat=observer_lat * u.deg, 86 | lon=observer_lon * u.deg, 87 | height=observer_elevation * u.m, 88 | ) 89 | self.latest_time = None 90 | self.refresh_time = refresh_time * u.second 91 | 92 | self.az_el_dict = {} 93 | self.vlsr_dict = {} 94 | # self.time_interval_dict = {} 95 | self.time_interval_dict = self.inital_azeltime() 96 | 97 | self.update_all_az_el() 98 | 99 | # self.update_azeltime() 100 | conf.auto_download = auto_download 101 | 102 | def calculate_az_el(self, name, time, alt_az_frame): 103 | """Calculates Azimuth and Elevation of the Specified Object at the Specified Time 104 | 105 | Parameters 106 | ---------- 107 | name : str 108 | Name of the Object being Tracked 109 | time : Time 110 | Current Time (only necessary for Sun/Moon Ephemeris) 111 | alt_az_frame : AltAz 112 | AltAz Frame Object 113 | 114 | Returns 115 | ------- 116 | (float, float) 117 | (az, el) Tuple 118 | """ 119 | if name == "Sun": 120 | alt_az = get_sun(time).transform_to(alt_az_frame) 121 | elif name == "Moon": 122 | alt_az = get_moon(time, self.location).transform_to(alt_az_frame) 123 | else: 124 | alt_az = self.sky_coords[self.sky_coord_names[name]].transform_to( 125 | alt_az_frame 126 | ) 127 | return alt_az.az.degree, alt_az.alt.degree 128 | 129 | def calculate_vlsr(self, name, time, frame): 130 | """Calculates the velocity in the local standard of rest. 131 | 132 | Parameters 133 | ---------- 134 | name : str 135 | Name of the Object being Tracked 136 | time : Time 137 | Current Time (only necessary for Sun/Moon Ephemeris) 138 | alt_az_frame : AltAz 139 | AltAz Frame Object 140 | 141 | 142 | Returns 143 | ------- 144 | float 145 | vlsr in km/s. 146 | """ 147 | if name == "Sun": 148 | tframe = get_sun(time).transform_to(frame) 149 | vlsr = tframe.radial_velocity_correction(obstime=time) 150 | elif name == "Moon": 151 | tframe = get_moon(time).transform_to(frame) 152 | vlsr = tframe.radial_velocity_correction(obstime=time) 153 | else: 154 | tframe = self.sky_coord_names[name].transform_to(frame) 155 | vlsr = tframe.radial_velocity_correction(obstime=time) 156 | 157 | return vlsr.to(u.km / u.s).value 158 | 159 | def calculate_vlsr_azel(self, az_el, time=None): 160 | """Takes an AzEl tuple and derives the vlsr from Location 161 | 162 | Parameters 163 | ---------- 164 | az_el : (float, float) 165 | Azimuth and Elevation 166 | time : AstroPy Time Obj 167 | Time of Conversion 168 | 169 | Returns 170 | ------- 171 | float 172 | vlsr in km/s. 173 | """ 174 | 175 | if time is None: 176 | time = Time.now() 177 | 178 | az, el = az_el 179 | start_frame = AltAz( 180 | obstime=time, location=self.location, alt=el * u.deg, az=az * u.deg 181 | ) 182 | end_frame = Galactic() 183 | result = start_frame.transform_to(end_frame) 184 | sk1 = SkyCoord(result) 185 | f1 = AltAz(obstime=time, location=self.location) 186 | vlsr = sk1.transform_to(f1).radial_velocity_correction(obstime=time) 187 | 188 | return vlsr.to(u.km/u.s).value 189 | 190 | def convert_to_gal_coord(self, az_el, time=None): 191 | """Converts an AzEl Tuple into a Galactic Tuple from Location 192 | 193 | Parameters 194 | ---------- 195 | az_el : (float, float) 196 | Azimuth and Elevation to Convert 197 | time : AstroPy Time Obj 198 | Time of Conversion 199 | 200 | Returns 201 | ------- 202 | (float, float) 203 | Galactic Latitude and Longitude 204 | """ 205 | if time is None: 206 | time = Time.now() 207 | az, el = az_el 208 | start_frame = AltAz( 209 | obstime=time, location=self.location, alt=el * u.deg, az=az * u.deg 210 | ) 211 | end_frame = Galactic() 212 | result = start_frame.transform_to(end_frame) 213 | g_lat = float(result.b.degree) 214 | g_lng = float(result.l.degree) 215 | return g_lat, g_lng 216 | 217 | def update_all_az_el(self): 218 | """Updates Every Entry in the AzEl Dictionary Cache, if the Cache is Outdated 219 | 220 | Returns 221 | ------- 222 | None 223 | """ 224 | if ( 225 | self.latest_time is not None 226 | and Time.now() < self.latest_time + self.refresh_time 227 | ): 228 | return 229 | time = Time.now() 230 | frame = AltAz(obstime=time, location=self.location) 231 | transformed = self.sky_coords.transform_to(frame) 232 | for name in self.sky_coord_names: 233 | index = self.sky_coord_names[name] 234 | self.az_el_dict[name] = ( 235 | transformed.az[index].degree, 236 | transformed.alt[index].degree, 237 | ) 238 | vlsr = transformed[index].radial_velocity_correction(obstime=time) 239 | self.vlsr_dict[name] = vlsr.to(u.km / u.s).value 240 | self.az_el_dict["Sun"] = self.calculate_az_el("Sun", time, frame) 241 | self.vlsr_dict["Sun"] = self.calculate_vlsr("Sun", time, frame) 242 | self.az_el_dict["Moon"] = self.calculate_az_el("Moon", time, frame) 243 | self.vlsr_dict["Moon"] = self.calculate_vlsr("Moon", time, frame) 244 | 245 | for time_passed in range(0, 61, 5): 246 | 247 | timenew = time + time_passed 248 | frame = AltAz(obstime=timenew, location=self.location) 249 | transformed = self.sky_coords.transform_to(frame) 250 | 251 | for name in self.sky_coord_names: 252 | index = self.sky_coord_names[name] 253 | self.time_interval_dict[time_passed][name] = ( 254 | transformed.az[index].degree, 255 | transformed.alt[index].degree, 256 | ) 257 | self.time_interval_dict[time_passed]["Sun"] = self.calculate_az_el( 258 | "Sun", time, frame) 259 | self.time_interval_dict[time_passed]["Moon"] = self.calculate_az_el( 260 | "Moon", time, frame) 261 | 262 | self.latest_time = time 263 | 264 | def get_all_azimuth_elevation(self): 265 | """Returns Dictionary Mapping the Objects to their Current AzEl Coordinates 266 | 267 | Returns 268 | ------- 269 | self.az_el_dict : {str: (float, float)} 270 | """ 271 | return self.az_el_dict 272 | 273 | def get_all_azel_time(self): 274 | """Returns Dictionary Mapping the Time Offset to a dictionary of updated azel coordinates 275 | 276 | Returns 277 | ------- 278 | self.time_interval_dict : {int: {str: (float, float)}}s 279 | """ 280 | # return 281 | return self.time_interval_dict 282 | 283 | def get_azimuth_elevation(self, name, time_offset): 284 | """Returns Individual Object AzEl at Specified Time Offset 285 | 286 | Parameters 287 | ---------- 288 | name : str 289 | Object Name 290 | time_offset : Time 291 | Any Offset from the Current Time 292 | Returns 293 | ------- 294 | (float, float) 295 | (az, el) Tuple 296 | """ 297 | if time_offset == 0: 298 | return self.get_all_azimuth_elevation()[name] 299 | else: 300 | time = Time.now() + time_offset 301 | return self.calculate_az_el( 302 | name, time, AltAz(obstime=time, location=self.location) 303 | ) 304 | 305 | def get_all_vlsr(self): 306 | return self.vlsr_dict 307 | 308 | def get_vlsr(self, name, time_offset=0): 309 | 310 | if time_offset == 0: 311 | return self.get_all_vlsr()[name] 312 | else: 313 | time = Time.now() + time_offset 314 | frame = AltAz(obstime=time, location=self.location) 315 | return self.calculate_vlsr(name, time, frame) 316 | 317 | def inital_azeltime(self): 318 | new_dict = {} 319 | for time_passed in range(0, 61, 5): 320 | # new_time_dict = deepcopy(self.az_el_dict) 321 | new_time_dict = {} 322 | new_dict[time_passed] = new_time_dict 323 | return new_dict 324 | 325 | def update_azeltime(self): 326 | # if ( 327 | # self.latest_time is not None 328 | # and Time.now() < self.latest_time + self.refresh_time 329 | # ): 330 | # return 331 | 332 | for time_passed in range(0, 61, 5): 333 | 334 | time = Time.now() + time_passed 335 | frame = AltAz(obstime=time, location=self.location) 336 | transformed = self.sky_coords.transform_to(frame) 337 | 338 | for name in self.sky_coord_names: 339 | index = self.sky_coord_names[name] 340 | self.time_interval_dict[time_passed][name] = ( 341 | transformed.az[index].degree, 342 | transformed.alt[index].degree, 343 | ) 344 | self.latest_time = Time.now() 345 | return 346 | -------------------------------------------------------------------------------- /srt/dashboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/dashboard/__init__.py -------------------------------------------------------------------------------- /srt/dashboard/app.py: -------------------------------------------------------------------------------- 1 | """app.py 2 | 3 | Dash Small Radio Telescope Web App Dashboard 4 | 5 | """ 6 | 7 | import dash 8 | 9 | try: 10 | from dash import dcc 11 | except: 12 | import dash_core_components as dcc 13 | 14 | try: 15 | from dash import html 16 | except: 17 | import dash_html_components as html 18 | 19 | import dash_bootstrap_components as dbc 20 | from dash.dependencies import Input, Output, State, ClientsideFunction 21 | 22 | import flask 23 | import plotly.io as pio 24 | import numpy as np 25 | from time import time 26 | from pathlib import Path 27 | import base64 28 | 29 | from .layouts import monitor_page, system_page # , figure_page 30 | from .layouts.sidebar import generate_sidebar 31 | from .messaging.status_fetcher import StatusThread 32 | from .messaging.command_dispatcher import CommandThread 33 | from .messaging.spectrum_fetcher import SpectrumThread 34 | 35 | 36 | def generate_app(config_dir, config_dict): 37 | """Generates App and Server Objects for Hosting Dashboard 38 | 39 | Parameters 40 | ---------- 41 | config_dir : str 42 | Path to the Configuration Directory 43 | config_dict : dict 44 | Configuration Directory (Output of YAML Parser) 45 | 46 | Returns 47 | ------- 48 | (server, app) 49 | """ 50 | config_dict["CONFIG_DIR"] = config_dir 51 | software = config_dict["SOFTWARE"] 52 | 53 | # Set Up Flash and Dash Objects 54 | server = flask.Flask(__name__) 55 | app = dash.Dash( 56 | __name__, 57 | server=server, 58 | external_stylesheets=[dbc.themes.BOOTSTRAP], 59 | meta_tags=[ 60 | {"name": "viewport", "content": "width=device-width, initial-scale=1"} 61 | ], 62 | ) 63 | app.title = software 64 | 65 | # Start Listening for Radio and Status Data 66 | status_thread = StatusThread(port=5555) 67 | status_thread.start() 68 | 69 | command_thread = CommandThread(port=5556) 70 | command_thread.start() 71 | 72 | raw_spectrum_thread = SpectrumThread(port=5561) 73 | raw_spectrum_thread.start() 74 | 75 | cal_spectrum_thread = SpectrumThread(port=5563) 76 | cal_spectrum_thread.start() 77 | 78 | # Dictionary of Pages and matching URL prefixes 79 | pages = { 80 | "Monitor Page": "monitor-page", 81 | "System Page": "system-page", 82 | # "Figure Page": "figure-page" 83 | } 84 | if "DASHBOARD_REFRESH_MS" in config_dict.keys(): 85 | refresh_time = config_dict["DASHBOARD_REFRESH_MS"] # ms 86 | else: 87 | refresh_time = 1000 88 | pio.templates.default = "seaborn" # Style Choice for Graphs 89 | curfold = Path(__file__).parent.absolute() 90 | # Generate Sidebar Objects 91 | side_title = software 92 | image_filename = curfold.joinpath( 93 | "images", "MIT_HO_logo_landscape.png" 94 | ) # replace with your own image 95 | # Check if file is there and if not put in a single pixel image. 96 | if image_filename.exists(): 97 | encoded_image = base64.b64encode(open(image_filename, "rb").read()) 98 | else: 99 | encoded_image = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" 100 | 101 | side_content = { 102 | "Status": dcc.Markdown(id="sidebar-status"), 103 | "Pages": html.Div( 104 | [ 105 | html.H4("Pages"), 106 | dbc.Nav( 107 | [ 108 | dbc.NavLink( 109 | page_name, 110 | href=f"/{pages[page_name]}", 111 | id=f"{pages[page_name]}-link", 112 | ) 113 | for page_name in pages 114 | ], 115 | vertical=True, 116 | pills=True, 117 | ), 118 | ] 119 | ), 120 | "Image": html.Div( 121 | [ 122 | html.A( 123 | [ 124 | html.Img( 125 | src="data:image/png;base64,{}".format( 126 | encoded_image.decode() 127 | ), 128 | style={"height": "100%", "width": "100%"}, 129 | ) 130 | ], 131 | href="https://www.haystack.mit.edu/", 132 | ) 133 | ] 134 | ), 135 | } 136 | sidebar = generate_sidebar(side_title, side_content) 137 | 138 | # Build Dashboard Framework 139 | content = html.Div(id="page-content") 140 | layout = html.Div( 141 | [ 142 | dcc.Location(id="url"), 143 | sidebar, 144 | content, 145 | dcc.Interval(id="interval-component", 146 | interval=refresh_time, n_intervals=0), 147 | html.Div(id="output-clientside"), 148 | ], 149 | id="mainContainer", 150 | style={ 151 | "height": "100vh", 152 | "min_height": "100vh", 153 | "width": "100%", 154 | "display": "inline-block", 155 | }, 156 | ) 157 | 158 | app.layout = layout # Set App Layout to Dashboard Framework 159 | app.validation_layout = html.Div( 160 | [ 161 | layout, 162 | monitor_page.generate_layout(config_dict["SOFTWARE"]), 163 | system_page.generate_layout(), 164 | # figure_page.generate_layout() 165 | ] 166 | ) # Necessary for Allowing Other Files to Create Callbacks 167 | 168 | # Create Resizing JS Script Callback 169 | app.clientside_callback( 170 | ClientsideFunction(namespace="clientside", function_name="resize"), 171 | Output("output-clientside", "children"), 172 | [Input("page-content", "children")], 173 | ) 174 | # Create Callbacks for Monitoring Page Objects 175 | monitor_page.register_callbacks( 176 | app, 177 | config_dict, 178 | status_thread, 179 | command_thread, 180 | raw_spectrum_thread, 181 | cal_spectrum_thread, 182 | software 183 | ) 184 | # Create Callbacks for System Page Objects 185 | system_page.register_callbacks(app, config_dict, status_thread) 186 | 187 | # # Create Callbacks for figure page callbacks 188 | # figure_page.register_callbacks(app,config_dict, status_thread) 189 | # Activates Downloadable Saves - Caution 190 | if config_dict["DASHBOARD_DOWNLOADS"]: 191 | 192 | @server.route("/download/") 193 | def download(path): 194 | """Serve a file from the upload directory.""" 195 | return flask.send_from_directory( 196 | Path(config_dict["SAVE_DIRECTORY"]).expanduser(), 197 | path, 198 | as_attachment=True, 199 | ) 200 | 201 | @app.callback( 202 | [Output(f"{pages[page_name]}-link", "active") for page_name in pages], 203 | [Input("url", "pathname")], 204 | ) 205 | def toggle_active_links(pathname): 206 | """Sets the Page Links to Highlight to Current Page 207 | 208 | Parameters 209 | ---------- 210 | pathname : str 211 | Current Page Pathname 212 | 213 | Returns 214 | ------- 215 | list 216 | Sparse Bool List Which is True Only on the Proper Page Link 217 | """ 218 | if pathname == "/": 219 | # Treat page 1 as the homepage / index 220 | return tuple([i == 0 for i, _ in enumerate(pages)]) 221 | return [pathname == f"/{pages[page_name]}" for page_name in pages] 222 | 223 | @app.callback( 224 | Output("sidebar", "className"), 225 | [Input("sidebar-toggle", "n_clicks")], 226 | [State("sidebar", "className")], 227 | ) 228 | def toggle_classname(n, classname): 229 | """Changes Sidebar's className When it is Collapsed 230 | 231 | Notes 232 | ----- 233 | As per the Dash example this is based on, changing the sidebar's className 234 | changes the CSS that applying to it, allowing for hiding the sidebar 235 | 236 | Parameters 237 | ---------- 238 | n 239 | Num Clicks on Button 240 | classname : str 241 | Current Classname 242 | 243 | Returns 244 | ------- 245 | 246 | """ 247 | if n and classname == "": 248 | return "collapsed" 249 | return "" 250 | 251 | @app.callback( 252 | Output("sidebar-status", "children"), 253 | [Input("interval-component", "n_intervals")], 254 | ) 255 | def update_status_display(n): 256 | """Updates the Status Part of the Sidebar 257 | 258 | Parameters 259 | ---------- 260 | n : int 261 | Number of Intervals that Have Occurred (Unused) 262 | 263 | Returns 264 | ------- 265 | str 266 | Content for the Sidebar, Formatted as Markdown 267 | """ 268 | status = status_thread.get_status() 269 | if status is None: 270 | lat = lon = np.nan 271 | az = el = np.nan 272 | az_offset = el_offset = np.nan 273 | cf = np.nan 274 | bandwidth = np.nan 275 | status_string = "SRT Not Connected" 276 | vlsr = np.nan 277 | else: 278 | lat = status["location"]["latitude"] 279 | lon = status["location"]["longitude"] 280 | az = status["motor_azel"][0] 281 | el = status["motor_azel"][1] 282 | az_offset = status["motor_offsets"][0] 283 | el_offset = status["motor_offsets"][1] 284 | cf = status["center_frequency"] 285 | bandwidth = status["bandwidth"] 286 | vlsr = status["vlsr"] 287 | time_dif = time() - status["time"] 288 | if time_dif > 5: 289 | status_string = "SRT Daemon Not Available" 290 | elif status["queue_size"] == 0 and status["queued_item"] == "None": 291 | status_string = "SRT Inactive" 292 | else: 293 | status_string = "SRT In Use!" 294 | 295 | if config_dict["SOFTWARE"] == "Very Small Radio Telescope": 296 | status_string = f""" 297 | #### {status_string} 298 | - Location Lat, Long: {lat:.1f}, {lon:.1f} deg 299 | - Motor Az, El: {az:.1f}, {el:.1f} deg 300 | - Center Frequency: {cf / pow(10, 6)} MHz 301 | - Bandwidth: {bandwidth / pow(10, 6)} MHz 302 | - VLSR: {vlsr:.1f} km/s 303 | """ 304 | else: 305 | status_string = f""" 306 | #### {status_string} 307 | - Location Lat, Long: {lat:.1f}, {lon:.1f} deg 308 | - Motor Az, El: {az:.1f}, {el:.1f} deg 309 | - Motor Offsets: {az_offset:.1f}, {el_offset:.1f} deg 310 | - Center Frequency: {cf / pow(10, 6)} MHz 311 | - Bandwidth: {bandwidth / pow(10, 6)} MHz 312 | - VLSR: {vlsr:.1f} km/s 313 | """ 314 | 315 | return status_string 316 | 317 | @app.callback(Output("page-content", "children"), [Input("url", "pathname")]) 318 | def render_page_content(pathname): 319 | """Renders the Correct Content of the Page Portion 320 | 321 | Parameters 322 | ---------- 323 | pathname : str 324 | URL Path Requested 325 | 326 | Returns 327 | ------- 328 | Content of page-content 329 | """ 330 | 331 | if pathname in ["/", f"/{pages['Monitor Page']}"]: 332 | return monitor_page.generate_layout(config_dict["SOFTWARE"]) 333 | elif pathname == f"/{pages['System Page']}": 334 | return system_page.generate_layout() 335 | # elif pathname == f"/{pages['Figure Page']}": 336 | # return figure_page.generate_layout() 337 | # If the user tries to reach a different page, return a 404 message 338 | return dbc.Jumbotron( 339 | [ 340 | html.H1("404: Not found", className="text-danger"), 341 | html.Hr(), 342 | html.P(f"The pathname {pathname} was not recognised..."), 343 | ] 344 | ) 345 | 346 | return server, app 347 | -------------------------------------------------------------------------------- /srt/dashboard/assets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/dashboard/assets/__init__.py -------------------------------------------------------------------------------- /srt/dashboard/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/dashboard/assets/favicon.ico -------------------------------------------------------------------------------- /srt/dashboard/assets/resizing_script.js: -------------------------------------------------------------------------------- 1 | if (!window.dash_clientside) { 2 | window.dash_clientside = {}; 3 | } 4 | window.dash_clientside.clientside = { 5 | resize: function(value) { 6 | console.log("resizing..."); // for testing 7 | setTimeout(function() { 8 | window.dispatchEvent(new Event("resize")); 9 | console.log("fired resize"); 10 | }, 500); 11 | return null; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /srt/dashboard/assets/responsive-sidebar.css: -------------------------------------------------------------------------------- 1 | #sidebar { 2 | text-align: center; 3 | padding: 2rem 1rem; 4 | background-color: #f8f9fa; 5 | overflow-y: auto; 6 | overflow-x: hidden; 7 | } 8 | 9 | #sidebar h2 { 10 | text-align: left; 11 | margin-bottom: 0; 12 | } 13 | 14 | /* Hide the blurb on a small screen */ 15 | #blurb { 16 | display: none; 17 | } 18 | 19 | #sidebar-toggle { 20 | display: none; 21 | } 22 | 23 | #collapse *:first-child { 24 | margin-top: 1rem; 25 | } 26 | 27 | /* add the three horizontal bars icon for the toggle */ 28 | .navbar-toggler-icon { 29 | background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); 30 | } 31 | 32 | #page-content { 33 | padding: 0rem 0rem; 34 | } 35 | 36 | @media (min-width: 48em) { 37 | #sidebar { 38 | position: fixed; 39 | top: 0; 40 | left: 0; 41 | bottom: 0; 42 | width: 30rem; 43 | text-align: left; 44 | transition: margin 0.3s ease-in-out, padding 0.3s ease-in-out; 45 | } 46 | 47 | #sidebar-toggle { 48 | display: inline-block; 49 | position: relative; 50 | top: 0; 51 | transition: top 0.3s ease-in-out; 52 | } 53 | 54 | /* add negative margin to sidebar to achieve the collapse */ 55 | #sidebar.collapsed { 56 | margin-left: -25.5rem; 57 | padding-right: 0.5rem; 58 | overflow: hidden; 59 | } 60 | 61 | /* move the sidebar toggle up to the top left corner */ 62 | #sidebar.collapsed #sidebar-toggle { 63 | top: -2rem; 64 | } 65 | 66 | /* also adjust margin of page content */ 67 | #sidebar.collapsed ~ #page-content { 68 | margin-left: 6.5rem; 69 | } 70 | 71 | /* move all contents of navbar other than header (containing toggle) further 72 | off-screen */ 73 | #sidebar.collapsed > *:not(:first-child) { 74 | margin-left: -6rem; 75 | margin-right: 6rem; 76 | } 77 | 78 | /* reveal the blurb on a large screen */ 79 | #blurb { 80 | display: block; 81 | } 82 | 83 | /* Hide the toggle on a large screen */ 84 | #navbar-toggle { 85 | display: none; 86 | } 87 | 88 | #collapse { 89 | display: block; 90 | } 91 | 92 | /* set margins of the main content so that it doesn't overlap the sidebar */ 93 | #page-content { 94 | margin-left: 30rem; 95 | margin-right: 0rem; 96 | transition: margin-left 0.3s ease-in-out; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /srt/dashboard/assets/styles.css: -------------------------------------------------------------------------------- 1 | .js-plotly-plot .plotly .modebar { 2 | padding-top: 5%; 3 | margin-right: 3.5%; 4 | } 5 | 6 | body { 7 | background-color: #f2f2f2; 8 | margin: 0%; 9 | } 10 | 11 | .two.columns { 12 | width: 16.25%; 13 | } 14 | 15 | .column, 16 | .columns { 17 | margin-left: 0.5%; 18 | } 19 | 20 | .pretty_container { 21 | border-radius: 5px; 22 | background-color: #f9f9f9; 23 | margin: 5px; 24 | padding: 5px; 25 | position: relative; 26 | box-shadow: 2px 2px 2px lightgrey; 27 | } 28 | 29 | .bare_container { 30 | margin: 0 0 0 0; 31 | padding: 0 0 0 0; 32 | } 33 | 34 | .dcc_control { 35 | margin: 0; 36 | padding: 5px; 37 | width: calc(100%-40px); 38 | } 39 | 40 | .control_label { 41 | margin: 0; 42 | padding: 10px; 43 | padding-bottom: 0px; 44 | margin-bottom: 0px; 45 | width: calc(100%-40px); 46 | } 47 | 48 | .rc-slider { 49 | margin-left: 0px; 50 | padding-left: 0px; 51 | } 52 | 53 | .flex-display { 54 | display: flex; 55 | } 56 | 57 | .container-display { 58 | display: flex; 59 | } 60 | 61 | #individual_graph, 62 | #aggregate_graph { 63 | width: calc(100% - 30px); 64 | position: absolute; 65 | } 66 | 67 | #header { 68 | align-items: center; 69 | } 70 | 71 | .mini_container { 72 | border-radius: 5px; 73 | background-color: #f9f9f9; 74 | margin: 5px; 75 | padding: 5px; 76 | position: relative; 77 | box-shadow: 2px 2px 2px lightgrey; 78 | } 79 | 80 | #right-column { 81 | display: flex; 82 | flex-direction: column; 83 | } 84 | 85 | #tripleContainer { 86 | display: flex; 87 | flex: 3; 88 | } 89 | 90 | #mainContainer { 91 | display: flex; 92 | flex-direction: column; 93 | } 94 | 95 | -------------------------------------------------------------------------------- /srt/dashboard/images/MIT_HO_logo_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/dashboard/images/MIT_HO_logo_landscape.png -------------------------------------------------------------------------------- /srt/dashboard/images/MIT_HO_logo_square_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/dashboard/images/MIT_HO_logo_square_transparent.png -------------------------------------------------------------------------------- /srt/dashboard/layouts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/dashboard/layouts/__init__.py -------------------------------------------------------------------------------- /srt/dashboard/layouts/navbar.py: -------------------------------------------------------------------------------- 1 | """navbar.py 2 | 3 | Contains Functions for Interacting with a Navbar 4 | 5 | """ 6 | 7 | import dash_bootstrap_components as dbc 8 | 9 | 10 | def generate_navbar(dropdowns, title="Commands"): 11 | """Generates the Navbar 12 | 13 | Parameters 14 | ---------- 15 | dropdowns : dict 16 | Dictionary of Buttons for Each Dropdown Menu 17 | title : str 18 | Title of the Navbar 19 | 20 | Returns 21 | ------- 22 | NavbarSimple 23 | """ 24 | navbar = dbc.NavbarSimple( 25 | [ 26 | dbc.DropdownMenu( 27 | children=dropdowns[drop_down], 28 | in_navbar=True, 29 | label=drop_down, 30 | style={"display": "flex", "flexWrap": "wrap"}, 31 | className="m-1", 32 | ) 33 | for drop_down in dropdowns 34 | ], 35 | brand=title, 36 | brand_style={"font-size": "large"}, 37 | color="secondary", 38 | dark=True, 39 | ) 40 | return navbar 41 | -------------------------------------------------------------------------------- /srt/dashboard/layouts/sidebar.py: -------------------------------------------------------------------------------- 1 | """sidebar.py 2 | 3 | Functions to Generate Sidebar 4 | 5 | """ 6 | try: 7 | from dash import html 8 | except: 9 | import dash_html_components as html 10 | 11 | import dash_bootstrap_components as dbc 12 | 13 | 14 | def generate_sidebar(title, sections): 15 | """Generates the Sidebar for the SRT Dashboard 16 | 17 | Parameters 18 | ---------- 19 | title : str 20 | Title String for the Sidebar 21 | sections : dict 22 | Dictionary of Children to Put In Sidebar 23 | 24 | See Also 25 | -------- 26 | 27 | 28 | Returns 29 | ------- 30 | Sidebar 31 | """ 32 | # we use the Row and Col components to construct the sidebar header 33 | # it consists of a title, and a toggle, the latter is hidden on large screens 34 | sidebar_header = dbc.Row( 35 | [ 36 | dbc.Col( 37 | html.H3(title, className="display-7"), 38 | ), 39 | dbc.Col( 40 | [ 41 | html.Button( 42 | # use the Bootstrap navbar-toggler classes to style 43 | html.Span(className="navbar-toggler-icon"), 44 | className="navbar-toggler", 45 | # the navbar-toggler classes don't set color 46 | style={ 47 | "color": "rgba(0,0,0,.5)", 48 | "border-color": "rgba(0,0,0,.1)", 49 | }, 50 | id="navbar-toggle", 51 | ), 52 | html.Button( 53 | # use the Bootstrap navbar-toggler classes to style 54 | html.Span(className="navbar-toggler-icon"), 55 | className="navbar-toggler", 56 | # the navbar-toggler classes don't set color 57 | style={ 58 | "color": "rgba(0,0,0,.5)", 59 | "border-color": "rgba(0,0,0,.1)", 60 | }, 61 | id="sidebar-toggle", 62 | ), 63 | ], 64 | # the column containing the toggle will be only as wide as the 65 | # toggle, resulting in the toggle being right aligned 66 | width="auto", 67 | # vertically align the toggle in the center 68 | align="center", 69 | ), 70 | ] 71 | ) 72 | contents_list = [] 73 | for section in sections: 74 | contents_list.append(html.Div([html.Hr()])) 75 | contents_list.append(sections[section]) 76 | # use the Collapse component to animate hiding / revealing links 77 | sidebar = html.Div( 78 | [ 79 | sidebar_header, 80 | dbc.Collapse( 81 | contents_list, 82 | id="collapse", 83 | ), 84 | ], 85 | id="sidebar", 86 | ) 87 | return sidebar 88 | -------------------------------------------------------------------------------- /srt/dashboard/layouts/system_page.py: -------------------------------------------------------------------------------- 1 | """system_page.py 2 | 3 | Function for Generating System Page and Creating Callback 4 | 5 | """ 6 | 7 | try: 8 | from dash import dcc 9 | except: 10 | import dash_core_components as dcc 11 | 12 | try: 13 | from dash import html 14 | except: 15 | import dash_html_components as html 16 | 17 | from dash.dependencies import Input, Output, State 18 | 19 | from urllib.parse import quote as urlquote 20 | from datetime import datetime 21 | from pathlib import Path 22 | 23 | 24 | def generate_layout(): 25 | """Generates the Basic Layout for the System Page 26 | 27 | Returns 28 | ------- 29 | System Page Layout 30 | """ 31 | layout = html.Div( 32 | [ 33 | html.Div( 34 | [ 35 | html.Div( 36 | [], 37 | className="one-third column", 38 | ), 39 | html.Div( 40 | [ 41 | html.H4( 42 | "SRT System Page", 43 | style={"margin-bottom": "0px", "text-align": "center"}, 44 | ), 45 | ], 46 | className="one-third column", 47 | id="title", 48 | ), 49 | html.Div([], className="one-third column", id="button"), 50 | ], 51 | id="header", 52 | className="row flex-display", 53 | style={"margin-bottom": "25px"}, 54 | ), 55 | html.Div( 56 | [ 57 | html.Div( 58 | [ 59 | html.H4( 60 | "Emergency Contact Info", 61 | id="text-contact", 62 | style={"text-align": "center"}, 63 | ), 64 | dcc.Markdown(id="emergency-contact-info"), 65 | ], 66 | className="pretty_container four columns", 67 | ), 68 | html.Div( 69 | [ 70 | html.H4( 71 | id="text-queue-status", style={"text-align": "center"} 72 | ), 73 | dcc.Markdown(id="command-display"), 74 | ], 75 | className="pretty_container four columns", 76 | ), 77 | html.Div( 78 | [ 79 | html.H4( 80 | "Recordings", 81 | id="text-recordings", 82 | style={"text-align": "center"}, 83 | ), 84 | html.Div( 85 | id="recordings-list", 86 | style={ 87 | "height": 150, 88 | "overflow": "hidden", 89 | "overflow-y": "scroll", 90 | }, 91 | ), 92 | ], 93 | className="pretty_container four columns", 94 | ), 95 | ], 96 | className="flex-display", 97 | style={"justify-content": "center"}, 98 | ), 99 | html.Div( 100 | [ 101 | html.Div( 102 | [ 103 | html.H4( 104 | "Message Logs", 105 | style={"text-align": "center"}, 106 | ), 107 | html.Div( 108 | id="message-logs", 109 | style={ 110 | "height": 200, 111 | "overflow": "hidden", 112 | "overflow-y": "scroll", 113 | }, 114 | ), 115 | ], 116 | className="pretty_container twelve columns", 117 | ), 118 | ], 119 | className="flex-display", 120 | style={"justify-content": "center", "margin": "5px"}, 121 | ), 122 | ] 123 | ) 124 | return layout 125 | 126 | 127 | def register_callbacks(app, config, status_thread): 128 | """Registers the Callbacks for the System Page 129 | 130 | Parameters 131 | ---------- 132 | app : Dash Object 133 | Dash Object to Set Up Callbacks to 134 | config : dict 135 | Contains All Settings for Dashboard / Daemon 136 | status_thread : Thread 137 | Thread for Getting Status from Daemon 138 | 139 | Returns 140 | ------- 141 | None 142 | """ 143 | 144 | @app.callback( 145 | Output("emergency-contact-info", "children"), 146 | [Input("interval-component", "n_intervals")], 147 | ) 148 | def update_contact_info(n): 149 | status = status_thread.get_status() 150 | if status is None or "emergency_contact" not in status: 151 | return "" 152 | status = status["emergency_contact"] 153 | status_string = f""" 154 | - Name: {status["name"]} 155 | - Email: {status["email"]} 156 | - Phone Number: {status["phone_number"]} 157 | """ 158 | return status_string 159 | 160 | @app.callback( 161 | Output("message-logs", "children"), 162 | [Input("interval-component", "n_intervals")], 163 | ) 164 | def update_message_logs(n): 165 | status = status_thread.get_status() 166 | if status is None or "error_logs" not in status: 167 | return "" 168 | status = status["error_logs"] 169 | children = [ 170 | html.P( 171 | f"{datetime.fromtimestamp(log_time).strftime('%Y-%m-%d %H:%M:%S')}: {log_txt}" 172 | ) 173 | for log_time, log_txt in status 174 | ] 175 | return html.Div(children=children) 176 | 177 | @app.callback( 178 | Output("text-queue-status", "children"), 179 | [Input("interval-component", "n_intervals")], 180 | ) 181 | def update_command_queue_display(n): 182 | status = status_thread.get_status() 183 | if status is None or ( 184 | status["queue_size"] == 0 and status["queued_item"] == "None" 185 | ): 186 | return "SRT Inactive" 187 | return "SRT in Use!" 188 | 189 | @app.callback( 190 | Output("command-display", "children"), 191 | [Input("interval-component", "n_intervals")], 192 | ) 193 | def update_command_display(n): 194 | status = status_thread.get_status() 195 | if status is None: 196 | return "" 197 | current_cmd = status["queued_item"] 198 | queue_size = status["queue_size"] 199 | status_string = f""" 200 | ##### Command Queue Status 201 | - Running Command: {current_cmd} 202 | - {queue_size} More Commands Waiting in the Queue 203 | """ 204 | return status_string 205 | 206 | @app.callback( 207 | Output("recordings-list", "children"), 208 | [Input("interval-component", "n_intervals")], 209 | ) 210 | def update_output(n): 211 | """Save uploaded files and regenerate the file list.""" 212 | 213 | files = [ 214 | file.name 215 | for file in Path(config["SAVE_DIRECTORY"]).expanduser().glob("*") 216 | if file.is_file() 217 | ] 218 | folders = [ 219 | file.name 220 | for file in Path(config["SAVE_DIRECTORY"]).expanduser().glob("*") 221 | if file.is_dir() 222 | ] 223 | if len(files) == 0: 224 | return [html.Li("No files yet!")] 225 | else: 226 | if config["DASHBOARD_DOWNLOADS"]: 227 | return [ 228 | html.Li(html.A(filename, href=f"/download/{urlquote(filename)}")) 229 | for filename in files 230 | ] + [html.Li(html.A(foldername)) for foldername in folders] 231 | else: 232 | return [html.Li(html.A(filename)) for filename in (files + folders)] 233 | -------------------------------------------------------------------------------- /srt/dashboard/messaging/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/dashboard/messaging/__init__.py -------------------------------------------------------------------------------- /srt/dashboard/messaging/command_dispatcher.py: -------------------------------------------------------------------------------- 1 | """command_dispatcher.py 2 | 3 | Thread Which Handles Sending Commands to the Daemon 4 | 5 | """ 6 | 7 | import zmq 8 | from threading import Thread 9 | from queue import Queue 10 | 11 | 12 | class CommandThread(Thread): 13 | """ 14 | Thread Which Handles Sending Commands to the Daemon 15 | """ 16 | 17 | def __init__(self, group=None, target=None, name=None, port=5556): 18 | """Initializer for the Command Thread 19 | 20 | Parameters 21 | ---------- 22 | group : NoneType 23 | The ThreadGroup the Thread Belongs to (Currently Unimplemented in Python 3.8) 24 | target : callable 25 | Function that the Thread Should Run (Leave This Be For Command Sending) 26 | name : str 27 | Name of the Thread 28 | port : int 29 | Port to Access for Sending Commands 30 | """ 31 | super().__init__(group=group, target=target, name=name, daemon=True) 32 | self.queue = Queue() 33 | self.port = port 34 | 35 | def run(self): 36 | """Grabs Commands from the Queue and Sends the to the Daemon 37 | 38 | Returns 39 | ------- 40 | None 41 | """ 42 | context = zmq.Context() 43 | socket = context.socket(zmq.PUSH) 44 | socket.connect("tcp://localhost:%s" % self.port) 45 | while self.is_alive(): 46 | command = self.queue.get() 47 | socket.send_string(command) 48 | 49 | def add_to_queue(self, cmd): 50 | """Adds a New Item to the Queue 51 | 52 | Parameters 53 | ---------- 54 | cmd : str 55 | New Command to Add to the Queue 56 | 57 | Returns 58 | ------- 59 | None 60 | """ 61 | self.queue.put(cmd) 62 | 63 | def get_queue_empty(self): 64 | """Returns if the Queue is Empty 65 | 66 | Returns 67 | ------- 68 | bool 69 | If the Queue is Empty 70 | """ 71 | return self.queue.empty() 72 | 73 | 74 | def test_cmd_send(): # TODO: Look into PyTest Fixtures - Having Multiple Threads in PyTest Causes Copious Warnings 75 | from time import sleep 76 | 77 | port = 5556 78 | num_dispatchers = 10 79 | num_in_queue = 5 80 | 81 | context = zmq.Context() 82 | socket = context.socket(zmq.PULL) 83 | socket.bind("tcp://*:%s" % port) 84 | 85 | def receive_func(): 86 | while True: 87 | received.append(socket.recv_string()) 88 | 89 | threads = [CommandThread(port=port) for _ in range(num_dispatchers)] 90 | for i, thread in enumerate(threads): 91 | for _ in range(num_in_queue): 92 | thread.add_to_queue(str(i)) 93 | for thread in threads: 94 | thread.start() 95 | 96 | received = [] 97 | receive_thread = Thread(target=receive_func, daemon=True) 98 | receive_thread.start() 99 | sleep(1) 100 | print(received) 101 | assert len(received) == num_in_queue * num_dispatchers 102 | -------------------------------------------------------------------------------- /srt/dashboard/messaging/raw_radio_fetcher.py: -------------------------------------------------------------------------------- 1 | """raw_radio_fetcher.py 2 | 3 | Thread Which Handles Receiving Raw I/Q Samples 4 | 5 | """ 6 | 7 | import zmq 8 | import numpy as np 9 | from threading import Thread 10 | from time import sleep 11 | 12 | 13 | class RadioThread(Thread): 14 | """ 15 | Thread for Fetching Raw I/Q Samples from GNU Radio via ZMQ PUB/SUB 16 | """ 17 | 18 | def __init__( 19 | self, group=None, target=None, name=None, port=5559, cache_size=4_000_000 20 | ): 21 | """Initializer for the RadioThread 22 | 23 | Parameters 24 | ---------- 25 | group : NoneType 26 | The ThreadGroup the Thread Belongs to (Currently Unimplemented in Python 3.8) 27 | target : callable 28 | Function that the Thread Should Run (Leave This Be For Command Sending) 29 | name : str 30 | Name of the Thread 31 | port : int 32 | Port of the Raw I/Q Sample ZMQ PUB/SUB Socket 33 | cache_size : int 34 | Number of Samples to Keep In Cache 35 | """ 36 | super().__init__(group=group, target=target, name=name, daemon=True) 37 | self.history = np.zeros(cache_size, dtype="complex64") 38 | self.history_index = 0 39 | self.port = port 40 | 41 | def run(self): 42 | """Grabs Samples From ZMQ, Converts them to Numpy, and Stores 43 | 44 | Returns 45 | ------- 46 | None 47 | """ 48 | context = zmq.Context() 49 | socket = context.socket(zmq.SUB) 50 | socket.connect("tcp://localhost:%s" % self.port) 51 | socket.subscribe("") 52 | while True: 53 | rec = socket.recv() 54 | print(len(rec)) 55 | var = np.frombuffer(rec, dtype="complex64") 56 | new_history_index = self.history_index + len(var) 57 | self.history.put( 58 | range(self.history_index, new_history_index), var, mode="wrap" 59 | ) 60 | self.history_index = new_history_index % len(self.history) 61 | 62 | def get_sample_history(self, num_samples=-1): 63 | """Get a Copy of the Sample History 64 | 65 | Parameters 66 | ---------- 67 | num_samples : int 68 | Number of Samples to Return in an Array ; If -1, then Return All 69 | 70 | Returns 71 | ------- 72 | A Portion of the Sample History : (num_samples) ndarray 73 | """ 74 | if 0 < num_samples < len(self.history): 75 | return self.history.take( 76 | range(self.history_index, self.history_index + num_samples), mode="wrap" 77 | ) 78 | return self.history.take( 79 | range(self.history_index, self.history_index + len(self.history)), 80 | mode="wrap", 81 | ) 82 | 83 | 84 | if __name__ == "__main__": 85 | import matplotlib.pyplot as plt 86 | 87 | thread = RadioThread() 88 | thread.start() 89 | sleep(1) 90 | powerSpectrum, freqenciesFound, time, imageAxis = plt.specgram( 91 | thread.get_sample_history(), Fc=100000000, Fs=2000000 92 | ) 93 | plt.xlabel("Time") 94 | plt.ylabel("Frequency") 95 | plt.show() 96 | -------------------------------------------------------------------------------- /srt/dashboard/messaging/spectrum_fetcher.py: -------------------------------------------------------------------------------- 1 | """spectrum_fetcher.py 2 | 3 | Thread Which Handles Receiving Spectrum Data 4 | 5 | """ 6 | 7 | import zmq 8 | import numpy as np 9 | from threading import Thread 10 | from time import sleep 11 | import time 12 | 13 | 14 | class SpectrumThread(Thread): 15 | """ 16 | Thread for Fetching Spectrum Data from GNU Radio via ZMQ PUB/SUB 17 | """ 18 | 19 | def __init__( 20 | self, group=None, target=None, name=None, port=5560, history_length=1000 21 | ): 22 | """Initializer for the SpectrumThread 23 | 24 | Parameters 25 | ---------- 26 | group : NoneType 27 | The ThreadGroup the Thread Belongs to (Currently Unimplemented in Python 3.8) 28 | target : callable 29 | Function that the Thread Should Run (Leave This Be For Command Sending) 30 | name : str 31 | Name of the Thread 32 | port : int 33 | Port of the Spectrum Data ZMQ PUB/SUB Socket 34 | history_length : int 35 | Max Length of Spectrum Data History List 36 | """ 37 | super().__init__(group=group, target=target, name=name, daemon=True) 38 | self.history_length = history_length 39 | self.spectrum = None 40 | self.history = [] 41 | self.port = port 42 | 43 | def run(self): 44 | """Grabs Samples From ZMQ, Converts them to Numpy, and Stores 45 | 46 | Returns 47 | ------- 48 | None 49 | """ 50 | context = zmq.Context() 51 | socket = context.socket(zmq.SUB) 52 | socket.connect("tcp://localhost:%s" % self.port) 53 | socket.subscribe("") 54 | while True: 55 | rec = socket.recv() 56 | var = np.frombuffer(rec, dtype="float32") 57 | if len(self.history) >= self.history_length: 58 | self.history.pop() 59 | self.history.insert(0, (time.time(), var)) 60 | self.spectrum = var 61 | 62 | def get_spectrum(self): 63 | """Return Most Recently Received Spectrum 64 | 65 | Returns 66 | ------- 67 | self.spectrum : (N) ndarray 68 | """ 69 | return self.spectrum 70 | 71 | def get_history(self): 72 | """Return Entire History List 73 | 74 | Returns 75 | ------- 76 | [(int, ndarary)] 77 | Time and Numpy Spectrum Pairs History 78 | """ 79 | return self.history.copy() 80 | 81 | 82 | if __name__ == "__main__": 83 | import matplotlib.pyplot as plt 84 | 85 | thread = SpectrumThread() 86 | thread.start() 87 | sleep(1) 88 | print(thread.get_spectrum()) 89 | data = thread.get_spectrum() 90 | plt.hist(range(len(data)), range(len(data)), weights=data) 91 | plt.xlabel("Frequency") 92 | plt.ylabel("Power") 93 | plt.show() 94 | -------------------------------------------------------------------------------- /srt/dashboard/messaging/status_fetcher.py: -------------------------------------------------------------------------------- 1 | """status_fetcher.py 2 | 3 | Thread Which Handles Receiving Status Data 4 | 5 | """ 6 | 7 | import zmq 8 | from threading import Thread 9 | from time import sleep 10 | import json 11 | 12 | 13 | class StatusThread(Thread): 14 | """ 15 | Thread Which Handles Receiving Status Data 16 | """ 17 | 18 | def __init__(self, group=None, target=None, name=None, port=5550): # 5555 19 | """Initializer for StatusThread 20 | 21 | Parameters 22 | ---------- 23 | group : NoneType 24 | The ThreadGroup the Thread Belongs to (Currently Unimplemented in Python 3.8) 25 | target : callable 26 | Function that the Thread Should Run (Leave This Be For Command Sending) 27 | name : str 28 | Name of the Thread 29 | port : int 30 | Port of the Status Data ZMQ PUB/SUB Socket 31 | """ 32 | super().__init__(group=group, target=target, name=name, daemon=True) 33 | self.status = None 34 | self.port = port 35 | 36 | def run(self): 37 | """Grabs Most Recent Status From ZMQ and Stores 38 | 39 | Returns 40 | ------- 41 | 42 | """ 43 | context = zmq.Context() 44 | socket = context.socket(zmq.SUB) 45 | socket.connect("tcp://localhost:%s" % self.port) 46 | socket.subscribe("") 47 | while True: 48 | rec = socket.recv() 49 | dump = json.loads(rec) 50 | self.status = dump 51 | 52 | def get_status(self): 53 | """Return Most Recent Status Dictionary 54 | 55 | Returns 56 | ------- 57 | dict 58 | Status Dictionary 59 | """ 60 | return self.status 61 | 62 | 63 | if __name__ == "__main__": 64 | thread = StatusThread() 65 | thread.start() 66 | sleep(1) 67 | print(thread.get_status()) 68 | -------------------------------------------------------------------------------- /srt/postprocessing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITHaystack/srt-py/e188efae514dcccc1fb3a2cc7da3323f8e8f1b4d/srt/postprocessing/__init__.py -------------------------------------------------------------------------------- /srt/postprocessing/readrad.py: -------------------------------------------------------------------------------- 1 | """ 2 | readrad.py 3 | 4 | """ 5 | from datetime import datetime 6 | import numpy as np 7 | 8 | 9 | def read_radfile(filename): 10 | """Read in a rad file.py 11 | 12 | Parameters 13 | ---------- 14 | filename : str 15 | Input filename. 16 | 17 | Returns 18 | ------- 19 | outdict : dict 20 | Holds the info from each entry in the file 21 | """ 22 | 23 | with open(filename) as fp: 24 | lines = fp.read().splitlines() 25 | 26 | outdict = {} 27 | for iline in lines: 28 | # Rad files have 4 types of lines 29 | linesp = list(filter(None, iline.split(" "))) 30 | 31 | # This has time stamp. 32 | if linesp[0] == "DATE": 33 | cur_dict = { 34 | linesp[i]: float(linesp[i + 1]) 35 | if is_number(linesp[i + 1]) 36 | else linesp[i + 1] 37 | for i in range(0, len(linesp), 2) 38 | } 39 | cur_dict["DATE"] = datetime.strptime(cur_dict["DATE"], "%Y:%j:%H:%M:%S") 40 | # This has receiver info 41 | elif linesp[0] == "Fstart": 42 | linesp.remove("MHz") 43 | temp_dict = { 44 | linesp[i]: float(linesp[i + 1]) 45 | if is_number(linesp[i + 1]) 46 | else linesp[i + 1] 47 | for i in range(0, len(linesp), 2) 48 | } 49 | cur_dict.update(temp_dict) 50 | elif linesp[0] == "Spectrum": 51 | cur_dict["integrations"] = float(linesp[1]) 52 | elif is_number(linesp[0]): 53 | cur_dict["spectrum"] = np.array(linesp).astype(float) 54 | tstp = cur_dict["DATE"].timestamp() 55 | del cur_dict["DATE"] 56 | outdict[int(tstp)] = cur_dict 57 | return outdict 58 | 59 | 60 | def is_number(s): 61 | """Checks if a string can be translated to a float. 62 | 63 | Parameters 64 | ---------- 65 | s : string 66 | Input string. 67 | 68 | Returns 69 | ------- 70 | boolean 71 | Answer to such a deep question. 72 | """ 73 | try: 74 | float(s) 75 | return True 76 | except ValueError: 77 | pass 78 | 79 | try: 80 | import unicodedata 81 | 82 | unicodedata.numeric(s) 83 | return True 84 | except (TypeError, ValueError): 85 | pass 86 | 87 | return False 88 | --------------------------------------------------------------------------------