├── .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 | 
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 | 
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 | 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
--------------------------------------------------------------------------------
/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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------