Merge branch 'develop' into packet
This commit is contained in:
		
							
								
								
									
										97
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										97
									
								
								README.md
									
									
									
									
									
								
							| @@ -9,38 +9,42 @@ OpenWebRX is a multi-user SDR receiver software with a web interface. | |||||||
|  |  | ||||||
| It has the following features: | It has the following features: | ||||||
|  |  | ||||||
| - <a href="https://github.com/simonyiszk/csdr">csdr</a> based demodulators (AM/FM/SSB/CW/BPSK31), | - [csdr](https://github.com/simonyiszk/csdr) based demodulators (AM/FM/SSB/CW/BPSK31), | ||||||
| - filter passband can be set from GUI, | - filter passband can be set from GUI, | ||||||
| - waterfall display can be shifted back in time, | - it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas | ||||||
| - it extensively uses HTML5 features like WebSocket, Web Audio API, and <canvas>, | - it works in Google Chrome, Chromium and Mozilla Firefox | ||||||
| - it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28), | - currently supports RTL-SDR, HackRF, SDRplay, AirSpy | ||||||
| - currently supports RTL-SDR, HackRF, SDRplay, AirSpy and many other devices, see the <a href="https://github.com/simonyiszk/openwebrx/wiki/">OpenWebRX Wiki</a>, | - Multiple SDR devices can be used simultaneously | ||||||
| - it has a 3D waterfall display: | - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF) | ||||||
|  | - [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN) | ||||||
|  | - [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9) | ||||||
|  |  | ||||||
|  | **News (2019-07-21 by DD5JFK)** | ||||||
|  | - Latest Features: | ||||||
|  |   - More WSJT-X modes have been added, including the new FT4 mode | ||||||
|  |   - I started adding a bandplan feature, the first thing visible is the "dial" indicator that brings you right to the dial frequency for digital modes | ||||||
|  |   - fixed some bugs in the websocket communication which broke the map | ||||||
|  |  | ||||||
| **News (2015-08-18)** | **News (2019-07-13 by DD5JFK)** | ||||||
| - My BSc. thesis written on OpenWebRX is <a href="https://sdr.hu/static/bsc-thesis.pdf">available here.</a> | - Latest Features: | ||||||
| - Several bugs were fixed to improve reliability and stability. |   - FT8 Integration (using wsjt-x demodulators) | ||||||
| - OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.) |   - New Map Feature that shows both decoded grid squares from FT8 and Locations decoded from YSF digital voice | ||||||
| - OpenWebRX now uses <a href="https://github.com/simonyiszk/csdr#sdrjs">sdr.js</a> (*libcsdr* compiled to JavaScript) for some client-side DSP tasks.  |   - New Feature report that will show what functionality is available | ||||||
| - Receivers can now be listed on <a href="http://sdr.hu/">SDR.hu</a>. | - There's a new Raspbian SD Card image available (see below) | ||||||
| - License for OpenWebRX is now Affero GPL v3.  |  | ||||||
|  |  | ||||||
| **News (2016-02-14)** | **News (2019-06-30 by DD5JFK)** | ||||||
| - The DDC in *csdr* has been manually optimized for ARM NEON, so it runs around 3 times faster on the Raspberry Pi 2 than before.  | - I have done some major rework on the openwebrx core, and I am planning to continue adding more features in the near future. Please check this place for updates. | ||||||
| - Also we use *ncat* instead of *rtl_mus*, and it is 3 times faster in some cases. | - My work has not been accepted into the upstream repository, so you will need to chose between my fork and the official version. | ||||||
| - OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb` | - I have enabled the issue tracker on this project, so feel free to file bugs or suggest enhancements there! | ||||||
| - UI improvements were made, thanks to John Seamons and Gnoxter. | - This version sports the following new and amazing features: | ||||||
|  |   - Support of multiple SDR devices simultaneously | ||||||
|  |   - Support for multiple profiles per SDR that allow the user to listen to different frequencies | ||||||
|  |   - Support for digital voice decoding | ||||||
|  |   - Feature detection that will disable functionality when dependencies are not available (if you're missing the digital buttons, this is probably why) | ||||||
|  | - Raspbian SD Card Images and Docker builds available (see below) | ||||||
|  | - I am currently working on the feature set for a stable release, but you are more than welcome to test development versions! | ||||||
|  |  | ||||||
| **News (2017-04-04)** | > When upgrading OpenWebRX, please make sure that you also upgrade *csdr* and *digiham*! | ||||||
| - *ncat* has been replaced with a custom implementation called *nmux* due to a bug that caused regular crashes on some machines. The *nmux* tool is part of the *csdr* package. |  | ||||||
| - Most consumer SDR devices are supported via <a href="https://github.com/rxseger/rx_tools">rx_tools</a>, see the <a href="https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX">OpenWebRX Wiki</a> on that. |  | ||||||
|  |  | ||||||
| **News (2017-07-12)** |  | ||||||
| - OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display. |  | ||||||
|  |  | ||||||
| > When upgrading OpenWebRX, please make sure that you also upgrade *csdr*! |  | ||||||
|  |  | ||||||
| ## OpenWebRX servers on SDR.hu | ## OpenWebRX servers on SDR.hu | ||||||
|  |  | ||||||
| @@ -50,22 +54,49 @@ It has the following features: | |||||||
|  |  | ||||||
| ## Setup | ## Setup | ||||||
|  |  | ||||||
| OpenWebRX currently requires Linux and python 2.7 to run.  | ### Raspberry Pi SD Card Images | ||||||
|  |  | ||||||
|  | Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-07-13-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. | ||||||
|  |  | ||||||
|  | This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply. | ||||||
|  |  | ||||||
|  | Please note: I have not updated this to include the Raspberry Pi 4 yet. (It seems to be impossible to build Rasbpian Buster images on x86 hardware right now. Stay tuned!) | ||||||
|  |  | ||||||
|  | Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which should make it available as http://openwebrx:8073/ on most networks. This may vary depending on your specific setup. | ||||||
|  |  | ||||||
|  | For Digital voice, the minimum requirement right now seems to be a Rasbperry Pi 3B+. I would like to work on optimizing this for lower specs, but at this point I am not sure how much can be done.  | ||||||
|  |  | ||||||
|  | ### Docker Images | ||||||
|  |  | ||||||
|  | For those familiar with docker, I am providing [recent builds and Releases for both x86 and arm processors on the Docker hub](https://hub.docker.com/r/jketterl/openwebrx). You can find a short introduction there. | ||||||
|  |  | ||||||
|  | ### Manual Installation | ||||||
|  |  | ||||||
|  | OpenWebRX currently requires Linux and python 3 to run.  | ||||||
|  |  | ||||||
| First you will need to install the dependencies: | First you will need to install the dependencies: | ||||||
|  |  | ||||||
| - <a href="https://github.com/simonyiszk/csdr">libcsdr</a> | - [csdr](https://github.com/simonyiszk/csdr) | ||||||
| - <a href="http://sdr.osmocom.org/trac/wiki/rtl-sdr">rtl-sdr</a> | - [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr) | ||||||
|  |  | ||||||
|  | Optional Dependencies if you want to be able to listen do digital voice: | ||||||
|  |  | ||||||
|  | - [digiham](https://github.com/jketterl/digiham) | ||||||
|  | - [dsd](https://github.com/f4exb/dsdcc) | ||||||
|  |  | ||||||
|  | Optional Dependency if you want to decode WSJT-X modes: | ||||||
|  |  | ||||||
|  | - [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) | ||||||
|  |  | ||||||
| After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: | After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: | ||||||
|  |  | ||||||
| 	python openwebrx.py | 	./openwebrx.py | ||||||
| 	 | 	 | ||||||
| You can now open the GUI at <a href="http://localhost:8073">http://localhost:8073</a>. | You can now open the GUI at <a href="http://localhost:8073">http://localhost:8073</a>. | ||||||
|  |  | ||||||
| Please note that the server is also listening on the following ports (on localhost only): | Please note that the server is also listening on the following ports (on localhost only): | ||||||
|  |  | ||||||
| - port 4951 for the multi-user I/Q server. | - ports 4950 to 4960 for the multi-user I/Q servers. | ||||||
|  |  | ||||||
| Now the next step is to customize the parameters of your server in `config_webrx.py`. | Now the next step is to customize the parameters of your server in `config_webrx.py`. | ||||||
|  |  | ||||||
| @@ -86,8 +117,6 @@ If you have any problems installing OpenWebRX, you should check out the <a href= | |||||||
|  |  | ||||||
| Sometimes the actual error message is not at the end of the terminal output, you may have to look at the whole output to find it. | Sometimes the actual error message is not at the end of the terminal output, you may have to look at the whole output to find it. | ||||||
|  |  | ||||||
| If you want to run OpenWebRX on a remote server instead of *localhost*, do not forget to set *server_hostname* in `config_webrx.py`. |  | ||||||
|  |  | ||||||
| ## Licensing | ## Licensing | ||||||
|  |  | ||||||
| OpenWebRX is available under Affero GPL v3 license (<a href="https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)">summary</a>). | OpenWebRX is available under Affero GPL v3 license (<a href="https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)">summary</a>). | ||||||
|   | |||||||
							
								
								
									
										189
									
								
								bands.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								bands.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | |||||||
|  | [ | ||||||
|  |   { | ||||||
|  |     "name": "160m", | ||||||
|  |     "lower_bound": 1810000, | ||||||
|  |     "upper_bound": 2000000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": 1838000, | ||||||
|  |       "ft8": 1840000, | ||||||
|  |       "wspr": 1836600, | ||||||
|  |       "jt65": 1838000, | ||||||
|  |       "jt9": 1839000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "80m", | ||||||
|  |     "lower_bound": 3500000, | ||||||
|  |     "upper_bound": 3800000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": 3580000, | ||||||
|  |       "ft8": 3573000, | ||||||
|  |       "wspr": 3592600, | ||||||
|  |       "jt65": 3570000, | ||||||
|  |       "jt9": 3572000, | ||||||
|  |       "ft4": [3568000, 3568000] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "60m", | ||||||
|  |     "lower_bound": 5351500, | ||||||
|  |     "upper_bound": 5366500, | ||||||
|  |     "frequencies": { | ||||||
|  |       "ft8": 5357000, | ||||||
|  |       "wspr": 5287200 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "40m", | ||||||
|  |     "lower_bound": 7000000, | ||||||
|  |     "upper_bound": 7200000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": 7040000, | ||||||
|  |       "ft8": 7074000, | ||||||
|  |       "wspr": 7038600, | ||||||
|  |       "jt65": 7076000, | ||||||
|  |       "jt9": 7078000, | ||||||
|  |       "ft4": 7047500 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "30m", | ||||||
|  |     "lower_bound": 10100000, | ||||||
|  |     "upper_bound": 10150000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": 10141000, | ||||||
|  |       "ft8": 10136000, | ||||||
|  |       "wspr": 10138700, | ||||||
|  |       "jt65": 10138000, | ||||||
|  |       "jt9": 10140000, | ||||||
|  |       "ft4": 10140000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "20m", | ||||||
|  |     "lower_bound": 14000000, | ||||||
|  |     "upper_bound": 14350000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": 14070000, | ||||||
|  |       "ft8": 14074000, | ||||||
|  |       "wspr": 14095600, | ||||||
|  |       "jt65": 14076000, | ||||||
|  |       "jt9": 14078000, | ||||||
|  |       "ft4": 14080000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "17m", | ||||||
|  |     "lower_bound": 18068000, | ||||||
|  |     "upper_bound": 18168000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": 18098000, | ||||||
|  |       "ft8": 18100000, | ||||||
|  |       "wspr": 18104600, | ||||||
|  |       "jt65": 18102000, | ||||||
|  |       "jt9": 18104000, | ||||||
|  |       "ft4": 18104000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "15m", | ||||||
|  |     "lower_bound": 21000000, | ||||||
|  |     "upper_bound": 21450000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": 21070000, | ||||||
|  |       "ft8": 21074000, | ||||||
|  |       "wspr": 21094600, | ||||||
|  |       "jt65": 21076000, | ||||||
|  |       "jt9": 21078000, | ||||||
|  |       "ft4": 21140000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "12m", | ||||||
|  |     "lower_bound": 24890000, | ||||||
|  |     "upper_bound": 24990000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": 24920000, | ||||||
|  |       "ft8": 24915000, | ||||||
|  |       "wspr": 24924600, | ||||||
|  |       "jt65": 24917000, | ||||||
|  |       "jt9": 24919000, | ||||||
|  |       "ft4": 24919000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "10m", | ||||||
|  |     "lower_bound": 28000000, | ||||||
|  |     "upper_bound": 29700000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": [28070000, 28120000], | ||||||
|  |       "ft8": 28074000, | ||||||
|  |       "wspr": 28124600, | ||||||
|  |       "jt65": 28076000, | ||||||
|  |       "jt9": 28078000, | ||||||
|  |       "ft4": 28180000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "6m", | ||||||
|  |     "lower_bound": 50030000, | ||||||
|  |     "upper_bound": 51000000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "psk31": 50305000, | ||||||
|  |       "ft8": 50313000, | ||||||
|  |       "wspr": 50293000, | ||||||
|  |       "jt65": 50310000, | ||||||
|  |       "jt9": 50312000, | ||||||
|  |       "ft4": 50318000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "4m", | ||||||
|  |     "lower_bound": 70150000, | ||||||
|  |     "upper_bound": 70200000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "wspr": 70091000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "2m", | ||||||
|  |     "lower_bound": 144000000, | ||||||
|  |     "upper_bound": 146000000, | ||||||
|  |     "frequencies": { | ||||||
|  |       "wspr": 144489000, | ||||||
|  |       "ft8": 144174000, | ||||||
|  |       "ft4": 144170000, | ||||||
|  |       "jt65": 144120000 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "70cm", | ||||||
|  |     "lower_bound": 430000000, | ||||||
|  |     "upper_bound": 440000000 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "23cm", | ||||||
|  |     "lower_bound": 1240000000, | ||||||
|  |     "upper_bound": 1300000000 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "13cm", | ||||||
|  |     "lower_bound": 2320000000, | ||||||
|  |     "upper_bound": 2450000000 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "9cm", | ||||||
|  |     "lower_bound": 3400000000, | ||||||
|  |     "upper_bound": 3475000000 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "6cm", | ||||||
|  |     "lower_bound": 5650000000, | ||||||
|  |     "upper_bound": 5850000000 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "name": "3cm", | ||||||
|  |     "lower_bound": 10000000000, | ||||||
|  |     "upper_bound": 10500000000 | ||||||
|  |   } | ||||||
|  | ] | ||||||
							
								
								
									
										132
									
								
								config_webrx.py
									
									
									
									
									
								
							
							
						
						
									
										132
									
								
								config_webrx.py
									
									
									
									
									
								
							| @@ -35,21 +35,21 @@ config_webrx: configuration options for OpenWebRX | |||||||
| #       https://github.com/simonyiszk/openwebrx/wiki | #       https://github.com/simonyiszk/openwebrx/wiki | ||||||
|  |  | ||||||
| # ==== Server settings ==== | # ==== Server settings ==== | ||||||
| web_port=8073 | web_port = 8073 | ||||||
| max_clients=20 | max_clients = 20 | ||||||
|  |  | ||||||
| # ==== Web GUI configuration ==== | # ==== Web GUI configuration ==== | ||||||
| receiver_name="[Callsign]" | receiver_name = "[Callsign]" | ||||||
| receiver_location="Budapest, Hungary" | receiver_location = "Budapest, Hungary" | ||||||
| receiver_qra="JN97ML" | receiver_qra = "JN97ML" | ||||||
| receiver_asl=200 | receiver_asl = 200 | ||||||
| receiver_ant="Longwire" | receiver_ant = "Longwire" | ||||||
| receiver_device="RTL-SDR" | receiver_device = "RTL-SDR" | ||||||
| receiver_admin="example@example.com" | receiver_admin = "example@example.com" | ||||||
| receiver_gps=(47.000000,19.000000) | receiver_gps = (47.000000, 19.000000) | ||||||
| photo_height=350 | photo_height = 350 | ||||||
| photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory" | photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory" | ||||||
| photo_desc=""" | photo_desc = """ | ||||||
| You can add your own background photo and receiver information.<br /> | You can add your own background photo and receiver information.<br /> | ||||||
| Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/> | Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/> | ||||||
| Device: %[RX_DEVICE]<br /> | Device: %[RX_DEVICE]<br /> | ||||||
| @@ -64,18 +64,20 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a> | |||||||
| sdrhu_key = "" | sdrhu_key = "" | ||||||
| # 3. Set this setting to True to enable listing: | # 3. Set this setting to True to enable listing: | ||||||
| sdrhu_public_listing = False | sdrhu_public_listing = False | ||||||
| server_hostname="localhost" | server_hostname = "localhost" | ||||||
|  |  | ||||||
| # ==== DSP/RX settings ==== | # ==== DSP/RX settings ==== | ||||||
| fft_fps=9 | fft_fps = 9 | ||||||
| fft_size=4096 #Should be power of 2 | fft_size = 4096  # Should be power of 2 | ||||||
| fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. | fft_voverlap_factor = ( | ||||||
|  |     0.3 | ||||||
|  | )  # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. | ||||||
|  |  | ||||||
| audio_compression="adpcm" #valid values: "adpcm", "none" | audio_compression = "adpcm"  # valid values: "adpcm", "none" | ||||||
| fft_compression="adpcm" #valid values: "adpcm", "none" | fft_compression = "adpcm"  # valid values: "adpcm", "none" | ||||||
|  |  | ||||||
| digimodes_enable=True #Decoding digimodes come with higher CPU usage.  | digimodes_enable = True  # Decoding digimodes come with higher CPU usage. | ||||||
| digimodes_fft_size=1024 | digimodes_fft_size = 1024 | ||||||
|  |  | ||||||
| # determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes | # determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes | ||||||
| # if you're running on a Raspi (up to 3B+) you'll want to leave this on 1 | # if you're running on a Raspi (up to 3B+) you'll want to leave this on 1 | ||||||
| @@ -116,7 +118,7 @@ sdrs = { | |||||||
|                 "rf_gain": 30, |                 "rf_gain": 30, | ||||||
|                 "samp_rate": 2400000, |                 "samp_rate": 2400000, | ||||||
|                 "start_freq": 439275000, |                 "start_freq": 439275000, | ||||||
|                 "start_mod": "nfm" |                 "start_mod": "nfm", | ||||||
|             }, |             }, | ||||||
|             "2m": { |             "2m": { | ||||||
|                 "name": "2m komplett", |                 "name": "2m komplett", | ||||||
| @@ -124,9 +126,9 @@ sdrs = { | |||||||
|                 "rf_gain": 30, |                 "rf_gain": 30, | ||||||
|                 "samp_rate": 2400000, |                 "samp_rate": 2400000, | ||||||
|                 "start_freq": 145725000, |                 "start_freq": 145725000, | ||||||
|                 "start_mod": "nfm" |                 "start_mod": "nfm", | ||||||
|             } |             }, | ||||||
|         } |         }, | ||||||
|     }, |     }, | ||||||
|     "sdrplay": { |     "sdrplay": { | ||||||
|         "name": "SDRPlay RSP2", |         "name": "SDRPlay RSP2", | ||||||
| @@ -134,39 +136,39 @@ sdrs = { | |||||||
|         "ppm": 0, |         "ppm": 0, | ||||||
|         "profiles": { |         "profiles": { | ||||||
|             "20m": { |             "20m": { | ||||||
|                 "name":"20m", |                 "name": "20m", | ||||||
|                 "center_freq": 14150000, |                 "center_freq": 14150000, | ||||||
|                 "rf_gain": 4, |                 "rf_gain": 4, | ||||||
|                 "samp_rate": 500000, |                 "samp_rate": 500000, | ||||||
|                 "start_freq": 14070000, |                 "start_freq": 14070000, | ||||||
|                 "start_mod": "usb", |                 "start_mod": "usb", | ||||||
|                 "antenna": "Antenna A" |                 "antenna": "Antenna A", | ||||||
|             }, |             }, | ||||||
|             "30m": { |             "30m": { | ||||||
|                 "name":"30m", |                 "name": "30m", | ||||||
|                 "center_freq": 10125000, |                 "center_freq": 10125000, | ||||||
|                 "rf_gain": 4, |                 "rf_gain": 4, | ||||||
|                 "samp_rate": 250000, |                 "samp_rate": 250000, | ||||||
|                 "start_freq": 10142000, |                 "start_freq": 10142000, | ||||||
|                 "start_mod": "usb" |                 "start_mod": "usb", | ||||||
|             }, |             }, | ||||||
|             "40m": { |             "40m": { | ||||||
|                 "name":"40m", |                 "name": "40m", | ||||||
|                 "center_freq": 7100000, |                 "center_freq": 7100000, | ||||||
|                 "rf_gain": 4, |                 "rf_gain": 4, | ||||||
|                 "samp_rate": 500000, |                 "samp_rate": 500000, | ||||||
|                 "start_freq": 7070000, |                 "start_freq": 7070000, | ||||||
|                 "start_mod": "usb", |                 "start_mod": "usb", | ||||||
|                 "antenna": "Antenna A" |                 "antenna": "Antenna A", | ||||||
|             }, |             }, | ||||||
|             "80m": { |             "80m": { | ||||||
|                 "name":"80m", |                 "name": "80m", | ||||||
|                 "center_freq": 3650000, |                 "center_freq": 3650000, | ||||||
|                 "rf_gain": 4, |                 "rf_gain": 4, | ||||||
|                 "samp_rate": 500000, |                 "samp_rate": 500000, | ||||||
|                 "start_freq": 3570000, |                 "start_freq": 3570000, | ||||||
|                 "start_mod": "usb", |                 "start_mod": "usb", | ||||||
|                 "antenna": "Antenna A" |                 "antenna": "Antenna A", | ||||||
|             }, |             }, | ||||||
|             "49m": { |             "49m": { | ||||||
|                 "name": "49m Broadcast", |                 "name": "49m Broadcast", | ||||||
| @@ -175,42 +177,43 @@ sdrs = { | |||||||
|                 "samp_rate": 500000, |                 "samp_rate": 500000, | ||||||
|                 "start_freq": 6070000, |                 "start_freq": 6070000, | ||||||
|                 "start_mod": "am", |                 "start_mod": "am", | ||||||
|                 "antenna": "Antenna A" |                 "antenna": "Antenna A", | ||||||
|             } |             }, | ||||||
|         } |         }, | ||||||
|     }, |     }, | ||||||
|     # this one is just here to test feature detection |     # this one is just here to test feature detection | ||||||
|     "test": { |     "test": {"type": "test"}, | ||||||
|         "type": "test" |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| # ==== Misc settings ==== | # ==== Misc settings ==== | ||||||
|  |  | ||||||
| client_audio_buffer_size = 5 | client_audio_buffer_size = 5 | ||||||
| #increasing client_audio_buffer_size will: | # increasing client_audio_buffer_size will: | ||||||
| # - also increase the latency | # - also increase the latency | ||||||
| # - decrease the chance of audio underruns | # - decrease the chance of audio underruns | ||||||
|  |  | ||||||
| iq_port_range = [4950, 4960] #TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. | iq_port_range = [ | ||||||
|  |     4950, | ||||||
|  |     4960, | ||||||
|  | ]  # TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. | ||||||
|  |  | ||||||
| # ==== Color themes ==== | # ==== Color themes ==== | ||||||
|  |  | ||||||
| #A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels | # A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels | ||||||
|  |  | ||||||
| ### default theme by teejez: | ### default theme by teejez: | ||||||
| waterfall_colors = [0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff] | waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF] | ||||||
| waterfall_min_level = -88 #in dB | waterfall_min_level = -88  # in dB | ||||||
| waterfall_max_level = -20 | waterfall_max_level = -20 | ||||||
| waterfall_auto_level_margin = (5, 40) | waterfall_auto_level_margin = (5, 40) | ||||||
| ### old theme by HA7ILM: | ### old theme by HA7ILM: | ||||||
| #waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff,  0xfff775ff, 0xff8a8aff, 0xb20000ff]" | # waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff,  0xfff775ff, 0xff8a8aff, 0xb20000ff]" | ||||||
| #waterfall_min_level = -115 #in dB | # waterfall_min_level = -115 #in dB | ||||||
| #waterfall_max_level = 0 | # waterfall_max_level = 0 | ||||||
| #waterfall_auto_level_margin = (20, 30) | # waterfall_auto_level_margin = (20, 30) | ||||||
| ##For the old colors, you might also want to set [fft_voverlap_factor] to 0. | ##For the old colors, you might also want to set [fft_voverlap_factor] to 0. | ||||||
|  |  | ||||||
| #Note: When the auto waterfall level button is clicked, the following happens: | # Note: When the auto waterfall level button is clicked, the following happens: | ||||||
| #   [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]] | #   [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]] | ||||||
| #   [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] | #   [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] | ||||||
| # | # | ||||||
| @@ -219,14 +222,35 @@ waterfall_auto_level_margin = (5, 40) | |||||||
| #                                                      current_max_power_level __| | #                                                      current_max_power_level __| | ||||||
|  |  | ||||||
| # 3D view settings | # 3D view settings | ||||||
| mathbox_waterfall_frequency_resolution = 128 #bins | mathbox_waterfall_frequency_resolution = 128  # bins | ||||||
| mathbox_waterfall_history_length = 10 #seconds | mathbox_waterfall_history_length = 10  # seconds | ||||||
| mathbox_waterfall_colors = [0x000000ff,0x2e6893ff,0x69a5d0ff,0x214b69ff,0x9dc4e0ff,0xfff775ff,0xff8a8aff,0xb20000ff] | mathbox_waterfall_colors = [ | ||||||
|  |     0x000000FF, | ||||||
|  |     0x2E6893FF, | ||||||
|  |     0x69A5D0FF, | ||||||
|  |     0x214B69FF, | ||||||
|  |     0x9DC4E0FF, | ||||||
|  |     0xFFF775FF, | ||||||
|  |     0xFF8A8AFF, | ||||||
|  |     0xB20000FF, | ||||||
|  | ] | ||||||
|  |  | ||||||
| # === Experimental settings === | # === Experimental settings === | ||||||
| #Warning! The settings below are very experimental. | # Warning! The settings below are very experimental. | ||||||
| csdr_dynamic_bufsize = False  # This allows you to change the buffering mode of csdr. | csdr_dynamic_bufsize = False  # This allows you to change the buffering mode of csdr. | ||||||
| csdr_print_bufsizes = False  # This prints the buffer sizes used for csdr processes. | csdr_print_bufsizes = False  # This prints the buffer sizes used for csdr processes. | ||||||
| csdr_through = False  # Setting this True will print out how much data is going into the DSP chains. | csdr_through = False  # Setting this True will print out how much data is going into the DSP chains. | ||||||
|  |  | ||||||
| nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux. | nmux_memory = 50  # in megabytes. This sets the approximate size of the circular buffer used by nmux. | ||||||
|  |  | ||||||
|  | google_maps_api_key = "" | ||||||
|  |  | ||||||
|  | # how long should positions be visible on the map? | ||||||
|  | # they will start fading out after half of that | ||||||
|  | # in seconds; default: 2 hours | ||||||
|  | map_position_retention_time = 2 * 60 * 60 | ||||||
|  |  | ||||||
|  | temporary_directory = "/tmp" | ||||||
|  |  | ||||||
|  | services_enabled = False | ||||||
|  | services_decoders = ["ft8", "ft4", "wspr"] | ||||||
|   | |||||||
							
								
								
									
										488
									
								
								csdr.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										488
									
								
								csdr.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -21,32 +21,55 @@ OpenWebRX csdr plugin: do the signal processing with csdr | |||||||
| """ | """ | ||||||
|  |  | ||||||
| import subprocess | import subprocess | ||||||
| import time |  | ||||||
| import os | import os | ||||||
| import signal | import signal | ||||||
| import threading | import threading | ||||||
| from functools import partial | from functools import partial | ||||||
|  | from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class output(object): | class output(object): | ||||||
|     def add_output(self, type, read_fn): |     def send_output(self, t, read_fn): | ||||||
|         pass |         if not self.supports_type(t): | ||||||
|     def reset(self): |             # TODO rewrite the output mechanism in a way that avoids producing unnecessary data | ||||||
|  |             logger.warning("dumping output of type %s since it is not supported.", t) | ||||||
|  |             threading.Thread(target=self.pump(read_fn, lambda x: None)).start() | ||||||
|  |             return | ||||||
|  |         self.receive_output(t, read_fn) | ||||||
|  |  | ||||||
|  |     def receive_output(self, t, read_fn): | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |     def pump(self, read, write): | ||||||
|  |         def copy(): | ||||||
|  |             run = True | ||||||
|  |             while run: | ||||||
|  |                 data = read() | ||||||
|  |                 if data is None or (isinstance(data, bytes) and len(data) == 0): | ||||||
|  |                     run = False | ||||||
|  |                 else: | ||||||
|  |                     write(data) | ||||||
|  |  | ||||||
|  |         return copy | ||||||
|  |  | ||||||
|  |     def supports_type(self, t): | ||||||
|  |         return True | ||||||
|  |  | ||||||
|  |  | ||||||
| class dsp(object): | class dsp(object): | ||||||
|  |  | ||||||
|     def __init__(self, output): |     def __init__(self, output): | ||||||
|         self.samp_rate = 250000 |         self.samp_rate = 250000 | ||||||
|         self.output_rate = 11025 #this is default, and cannot be set at the moment |         self.output_rate = 11025  # this is default, and cannot be set at the moment | ||||||
|         self.fft_size = 1024 |         self.fft_size = 1024 | ||||||
|         self.fft_fps = 5 |         self.fft_fps = 5 | ||||||
|         self.offset_freq = 0 |         self.offset_freq = 0 | ||||||
|         self.low_cut = -4000 |         self.low_cut = -4000 | ||||||
|         self.high_cut = 4000 |         self.high_cut = 4000 | ||||||
|         self.bpf_transition_bw = 320 #Hz, and this is a constant |         self.bpf_transition_bw = 320  # Hz, and this is a constant | ||||||
|         self.ddc_transition_bw_rate = 0.15  # of the IF sample rate |         self.ddc_transition_bw_rate = 0.15  # of the IF sample rate | ||||||
|         self.running = False |         self.running = False | ||||||
|         self.secondary_processes_running = False |         self.secondary_processes_running = False | ||||||
| @@ -67,101 +90,161 @@ class dsp(object): | |||||||
|         self.secondary_fft_size = 1024 |         self.secondary_fft_size = 1024 | ||||||
|         self.secondary_process_fft = None |         self.secondary_process_fft = None | ||||||
|         self.secondary_process_demod = None |         self.secondary_process_demod = None | ||||||
|         self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "meta_pipe", "iqtee_pipe", |         self.pipe_names = [ | ||||||
|                          "iqtee2_pipe", "dmr_control_pipe"] |             "bpf_pipe", | ||||||
|         self.secondary_pipe_names=["secondary_shift_pipe"] |             "shift_pipe", | ||||||
|  |             "squelch_pipe", | ||||||
|  |             "smeter_pipe", | ||||||
|  |             "meta_pipe", | ||||||
|  |             "iqtee_pipe", | ||||||
|  |             "iqtee2_pipe", | ||||||
|  |             "dmr_control_pipe", | ||||||
|  |         ] | ||||||
|  |         self.secondary_pipe_names = ["secondary_shift_pipe"] | ||||||
|         self.secondary_offset_freq = 1000 |         self.secondary_offset_freq = 1000 | ||||||
|         self.unvoiced_quality = 1 |         self.unvoiced_quality = 1 | ||||||
|         self.modification_lock = threading.Lock() |         self.modification_lock = threading.Lock() | ||||||
|         self.output = output |         self.output = output | ||||||
|  |         self.temporary_directory = "/tmp" | ||||||
|  |  | ||||||
|     def chain(self,which): |     def set_temporary_directory(self, what): | ||||||
|         chain ="nc -v 127.0.0.1 {nc_port} | " |         self.temporary_directory = what | ||||||
|         if self.csdr_dynamic_bufsize: chain += "csdr setbuf {start_bufsize} | " |  | ||||||
|         if self.csdr_through: chain +="csdr through | " |     def chain(self, which): | ||||||
|  |         chain = ["nc -v 127.0.0.1 {nc_port}"] | ||||||
|  |         if self.csdr_dynamic_bufsize: | ||||||
|  |             chain += ["csdr setbuf {start_bufsize}"] | ||||||
|  |         if self.csdr_through: | ||||||
|  |             chain += ["csdr through"] | ||||||
|         if which == "fft": |         if which == "fft": | ||||||
|             chain += "csdr fft_cc {fft_size} {fft_block_size} | " + \ |             chain += [ | ||||||
|                 ("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \ |                 "csdr fft_cc {fft_size} {fft_block_size}", | ||||||
|                 "csdr fft_exchange_sides_ff {fft_size}" |                 "csdr logpower_cf -70" | ||||||
|             if self.fft_compression=="adpcm": |                 if self.fft_averages == 0 | ||||||
|                 chain += " | csdr compress_fft_adpcm_f_u8 {fft_size}" |                 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages}", | ||||||
|  |                 "csdr fft_exchange_sides_ff {fft_size}", | ||||||
|  |             ] | ||||||
|  |             if self.fft_compression == "adpcm": | ||||||
|  |                 chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] | ||||||
|             return chain |             return chain | ||||||
|         chain += "csdr shift_addition_cc --fifo {shift_pipe} | " |         chain += [ | ||||||
|         chain += "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | " |             "csdr shift_addition_cc --fifo {shift_pipe}", | ||||||
|         chain += "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every} | " |             "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING", | ||||||
|  |             "csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING", | ||||||
|  |         ] | ||||||
|  |         if self.output.supports_type("smeter"): | ||||||
|  |             chain += [ | ||||||
|  |                 "csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}" | ||||||
|  |             ] | ||||||
|         if self.secondary_demodulator: |         if self.secondary_demodulator: | ||||||
|             chain += "csdr tee {iqtee_pipe} | " |             if self.output.supports_type("secondary_fft"): | ||||||
|             chain += "csdr tee {iqtee2_pipe} | " |                 chain += ["csdr tee {iqtee_pipe}"] | ||||||
|  |             chain += ["csdr tee {iqtee2_pipe}"] | ||||||
|  |             # early exit if we don't want audio | ||||||
|  |             if not self.output.supports_type("audio"): | ||||||
|  |                 return chain | ||||||
|         # safe some cpu cycles... no need to decimate if decimation factor is 1 |         # safe some cpu cycles... no need to decimate if decimation factor is 1 | ||||||
|         last_decimation_block = "csdr fractional_decimator_ff {last_decimation} | " if self.last_decimation != 1.0 else "" |         last_decimation_block = ( | ||||||
|  |             ["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else [] | ||||||
|  |         ) | ||||||
|         if which == "nfm": |         if which == "nfm": | ||||||
|             chain += "csdr fmdemod_quadri_cf | csdr limit_ff | " |             chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"] | ||||||
|             chain += last_decimation_block |             chain += last_decimation_block | ||||||
|             chain += "csdr deemphasis_nfm_ff {output_rate} | csdr convert_f_s16" |             chain += ["csdr deemphasis_nfm_ff {output_rate}", "csdr convert_f_s16"] | ||||||
|         elif self.isDigitalVoice(which): |         elif self.isDigitalVoice(which): | ||||||
|             chain += "csdr fmdemod_quadri_cf | dc_block | " |             chain += ["csdr fmdemod_quadri_cf", "dc_block "] | ||||||
|             chain += last_decimation_block |             chain += last_decimation_block | ||||||
|             # dsd modes |             # dsd modes | ||||||
|             if which in [ "dstar", "nxdn" ]: |             if which in ["dstar", "nxdn"]: | ||||||
|                 chain += "csdr limit_ff | csdr convert_f_s16 | " |                 chain += ["csdr limit_ff", "csdr convert_f_s16"] | ||||||
|                 if which == "dstar": |                 if which == "dstar": | ||||||
|                     chain += "dsd -fd" |                     chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "] | ||||||
|                 elif which == "nxdn": |                 elif which == "nxdn": | ||||||
|                     chain += "dsd -fi" |                     chain += ["dsd -fi -i - -o - -u {unvoiced_quality} -g -1 "] | ||||||
|                 chain += " -i - -o - -u {unvoiced_quality} -g -1 | CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f | " |                 chain += ["CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f"] | ||||||
|                 max_gain = 5 |                 max_gain = 5 | ||||||
|             # digiham modes |             # digiham modes | ||||||
|             else: |             else: | ||||||
|                 chain += "rrc_filter | gfsk_demodulator | " |                 chain += ["rrc_filter", "gfsk_demodulator"] | ||||||
|                 if which == "dmr": |                 if which == "dmr": | ||||||
|                     chain += "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe} | mbe_synthesizer -f -u {unvoiced_quality} | " |                     chain += [ | ||||||
|  |                         "dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}", | ||||||
|  |                         "mbe_synthesizer -f -u {unvoiced_quality}", | ||||||
|  |                     ] | ||||||
|                 elif which == "ysf": |                 elif which == "ysf": | ||||||
|                     chain += "ysf_decoder --fifo {meta_pipe} | mbe_synthesizer -y -f -u {unvoiced_quality} | " |                     chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y -f -u {unvoiced_quality}"] | ||||||
|                 max_gain = 0.0005 |                 max_gain = 0.0005 | ||||||
|             chain += "digitalvoice_filter -f | " |             chain += [ | ||||||
|             chain += "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain} | ".format(max_gain=max_gain) |                 "digitalvoice_filter -f", | ||||||
|             chain += "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " |                 "CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain), | ||||||
|  |                 "sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ", | ||||||
|  |             ] | ||||||
|         elif which == "packet": |         elif which == "packet": | ||||||
|             chain += "csdr fmdemod_quadri_cf | " |             chain += ["csdr fmdemod_quadri_cf"] | ||||||
|             chain += last_decimation_block |             chain += last_decimation_block | ||||||
|             chain += "csdr convert_f_s16 | " |             chain += [ | ||||||
|             chain += "direwolf -r {audio_rate} - 1>&2" |                 "csdr convert_f_s16", | ||||||
|  |                 "direwolf -r {audio_rate} - 1>&2" | ||||||
|  |             ] | ||||||
|         elif which == "am": |         elif which == "am": | ||||||
|             chain += "csdr amdemod_cf | csdr fastdcblock_ff | " |             chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"] | ||||||
|             chain += last_decimation_block |             chain += last_decimation_block | ||||||
|             chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" |             chain += ["csdr agc_ff", "csdr limit_ff", "csdr convert_f_s16"] | ||||||
|         elif which == "ssb": |         elif which == "ssb": | ||||||
|             chain += "csdr realpart_cf | " |             chain += ["csdr realpart_cf"] | ||||||
|             chain += last_decimation_block |             chain += last_decimation_block | ||||||
|             chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" |             chain += ["csdr agc_ff", "csdr limit_ff"] | ||||||
|  |             # fixed sample rate necessary for the wsjt-x tools. fix with sox... | ||||||
|  |             if self.isWsjtMode() and self.get_audio_rate() != self.get_output_rate(): | ||||||
|  |                 chain += [ | ||||||
|  |                     "sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - " | ||||||
|  |                 ] | ||||||
|  |             else: | ||||||
|  |                 chain += ["csdr convert_f_s16"] | ||||||
|  |  | ||||||
|         if self.audio_compression=="adpcm": |         if self.audio_compression == "adpcm": | ||||||
|             chain += " | csdr encode_ima_adpcm_i16_u8" |             chain += ["csdr encode_ima_adpcm_i16_u8"] | ||||||
|         return chain |         return chain | ||||||
|  |  | ||||||
|     def secondary_chain(self, which): |     def secondary_chain(self, which): | ||||||
|         secondary_chain_base="cat {input_pipe} | " |         secondary_chain_base = "cat {input_pipe} | " | ||||||
|         if which == "fft": |         if which == "fft": | ||||||
|             return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "") |             return ( | ||||||
|  |                 secondary_chain_base | ||||||
|  |                 + "csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " | ||||||
|  |                 + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression == "adpcm" else "") | ||||||
|  |             ) | ||||||
|         elif which == "bpsk31": |         elif which == "bpsk31": | ||||||
|             return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ |             return ( | ||||||
|                     "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " + \ |                 secondary_chain_base | ||||||
|                     "csdr simple_agc_cc 0.001 0.5 | " + \ |                 + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " | ||||||
|                     "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ |                 + "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff} | " | ||||||
|                     "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ |                 + "csdr simple_agc_cc 0.001 0.5 | " | ||||||
|                     "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" |                 + "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " | ||||||
|  |                 + "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " | ||||||
|  |                 + "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" | ||||||
|  |             ) | ||||||
|  |         elif self.isWsjtMode(which): | ||||||
|  |             chain = secondary_chain_base + "csdr realpart_cf | " | ||||||
|  |             if self.last_decimation != 1.0: | ||||||
|  |                 chain += "csdr fractional_decimator_ff {last_decimation} | " | ||||||
|  |             chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16" | ||||||
|  |             return chain | ||||||
|  |  | ||||||
|     def set_secondary_demodulator(self, what): |     def set_secondary_demodulator(self, what): | ||||||
|         if self.get_secondary_demodulator() == what: |         if self.get_secondary_demodulator() == what: | ||||||
|             return |             return | ||||||
|         self.secondary_demodulator = what |         self.secondary_demodulator = what | ||||||
|  |         self.calculate_decimation() | ||||||
|         self.restart() |         self.restart() | ||||||
|  |  | ||||||
|     def secondary_fft_block_size(self): |     def secondary_fft_block_size(self): | ||||||
|         return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here |         return (self.samp_rate / self.decimation) / ( | ||||||
|  |             self.fft_fps * 2 | ||||||
|  |         )  # *2 is there because we do FFT on real signal here | ||||||
|  |  | ||||||
|     def secondary_decimation(self): |     def secondary_decimation(self): | ||||||
|         return 1 #currently unused |         return 1  # currently unused | ||||||
|  |  | ||||||
|     def secondary_bpf_cutoff(self): |     def secondary_bpf_cutoff(self): | ||||||
|         if self.secondary_demodulator == "bpsk31": |         if self.secondary_demodulator == "bpsk31": | ||||||
| @@ -175,7 +258,7 @@ class dsp(object): | |||||||
|  |  | ||||||
|     def secondary_samples_per_bits(self): |     def secondary_samples_per_bits(self): | ||||||
|         if self.secondary_demodulator == "bpsk31": |         if self.secondary_demodulator == "bpsk31": | ||||||
|             return int(round(self.if_samp_rate()/31.25))&~3 |             return int(round(self.if_samp_rate() / 31.25)) & ~3 | ||||||
|         return 0 |         return 0 | ||||||
|  |  | ||||||
|     def secondary_bw(self): |     def secondary_bw(self): | ||||||
| @@ -183,55 +266,82 @@ class dsp(object): | |||||||
|             return 31.25 |             return 31.25 | ||||||
|  |  | ||||||
|     def start_secondary_demodulator(self): |     def start_secondary_demodulator(self): | ||||||
|         if not self.secondary_demodulator: return |         if not self.secondary_demodulator: | ||||||
|         logger.debug("[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()) |             return | ||||||
|         secondary_command_fft=self.secondary_chain("fft") |         logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate()) | ||||||
|         secondary_command_demod=self.secondary_chain(self.secondary_demodulator) |         secondary_command_demod = self.secondary_chain(self.secondary_demodulator) | ||||||
|         self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) |         self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod) | ||||||
|  |  | ||||||
|         secondary_command_fft=secondary_command_fft.format( |         secondary_command_demod = secondary_command_demod.format( | ||||||
|             input_pipe=self.iqtee_pipe, |  | ||||||
|             secondary_fft_input_size=self.secondary_fft_size, |  | ||||||
|             secondary_fft_size=self.secondary_fft_size, |  | ||||||
|             secondary_fft_block_size=self.secondary_fft_block_size(), |  | ||||||
|             ) |  | ||||||
|         secondary_command_demod=secondary_command_demod.format( |  | ||||||
|             input_pipe=self.iqtee2_pipe, |             input_pipe=self.iqtee2_pipe, | ||||||
|             secondary_shift_pipe=self.secondary_shift_pipe, |             secondary_shift_pipe=self.secondary_shift_pipe, | ||||||
|             secondary_decimation=self.secondary_decimation(), |             secondary_decimation=self.secondary_decimation(), | ||||||
|             secondary_samples_per_bits=self.secondary_samples_per_bits(), |             secondary_samples_per_bits=self.secondary_samples_per_bits(), | ||||||
|             secondary_bpf_cutoff=self.secondary_bpf_cutoff(), |             secondary_bpf_cutoff=self.secondary_bpf_cutoff(), | ||||||
|             secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), |             secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), | ||||||
|             if_samp_rate=self.if_samp_rate() |             if_samp_rate=self.if_samp_rate(), | ||||||
|  |             last_decimation=self.last_decimation, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft) |         logger.debug("secondary command (demod) = %s", secondary_command_demod) | ||||||
|         logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (demod) = %s", secondary_command_demod) |         my_env = os.environ.copy() | ||||||
|         my_env=os.environ.copy() |         # if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; | ||||||
|         #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; |         if self.csdr_print_bufsizes: | ||||||
|         if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; |             my_env["CSDR_PRINT_BUFSIZES"] = "1" | ||||||
|         self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) |         if self.output.supports_type("secondary_fft"): | ||||||
|         logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)") |             secondary_command_fft = self.secondary_chain("fft") | ||||||
|         self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes |             secondary_command_fft = secondary_command_fft.format( | ||||||
|         logger.debug("[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)") #TODO digimodes |                 input_pipe=self.iqtee_pipe, | ||||||
|  |                 secondary_fft_input_size=self.secondary_fft_size, | ||||||
|  |                 secondary_fft_size=self.secondary_fft_size, | ||||||
|  |                 secondary_fft_block_size=self.secondary_fft_block_size(), | ||||||
|  |             ) | ||||||
|  |             logger.debug("secondary command (fft) = %s", secondary_command_fft) | ||||||
|  |  | ||||||
|  |             self.secondary_process_fft = subprocess.Popen( | ||||||
|  |                 secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env | ||||||
|  |             ) | ||||||
|  |             self.output.send_output( | ||||||
|  |                 "secondary_fft", | ||||||
|  |                 partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())), | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         self.secondary_process_demod = subprocess.Popen( | ||||||
|  |             secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env | ||||||
|  |         ) | ||||||
|         self.secondary_processes_running = True |         self.secondary_processes_running = True | ||||||
|  |  | ||||||
|         self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read()))) |         if self.isWsjtMode(): | ||||||
|         self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) |             smd = self.get_secondary_demodulator() | ||||||
|  |             if smd == "ft8": | ||||||
|  |                 chopper = Ft8Chopper(self.secondary_process_demod.stdout) | ||||||
|  |             elif smd == "wspr": | ||||||
|  |                 chopper = WsprChopper(self.secondary_process_demod.stdout) | ||||||
|  |             elif smd == "jt65": | ||||||
|  |                 chopper = Jt65Chopper(self.secondary_process_demod.stdout) | ||||||
|  |             elif smd == "jt9": | ||||||
|  |                 chopper = Jt9Chopper(self.secondary_process_demod.stdout) | ||||||
|  |             elif smd == "ft4": | ||||||
|  |                 chopper = Ft4Chopper(self.secondary_process_demod.stdout) | ||||||
|  |             chopper.start() | ||||||
|  |             self.output.send_output("wsjt_demod", chopper.read) | ||||||
|  |         else: | ||||||
|  |             self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) | ||||||
|  |  | ||||||
|         #open control pipes for csdr and send initialization data |         # open control pipes for csdr and send initialization data | ||||||
|         if self.secondary_shift_pipe != None: #TODO digimodes |         if self.secondary_shift_pipe != None:  # TODO digimodes | ||||||
|             self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes |             self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w")  # TODO digimodes | ||||||
|             self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes |             self.set_secondary_offset_freq(self.secondary_offset_freq)  # TODO digimodes | ||||||
|  |  | ||||||
|     def set_secondary_offset_freq(self, value): |     def set_secondary_offset_freq(self, value): | ||||||
|         self.secondary_offset_freq=value |         self.secondary_offset_freq = value | ||||||
|         if self.secondary_processes_running: |         if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"): | ||||||
|             self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate())) |             self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())) | ||||||
|             self.secondary_shift_pipe_file.flush() |             self.secondary_shift_pipe_file.flush() | ||||||
|  |  | ||||||
|     def stop_secondary_demodulator(self): |     def stop_secondary_demodulator(self): | ||||||
|         if self.secondary_processes_running == False: return |         if self.secondary_processes_running == False: | ||||||
|  |             return | ||||||
|         self.try_delete_pipes(self.secondary_pipe_names) |         self.try_delete_pipes(self.secondary_pipe_names) | ||||||
|         if self.secondary_process_fft: |         if self.secondary_process_fft: | ||||||
|             try: |             try: | ||||||
| @@ -250,42 +360,47 @@ class dsp(object): | |||||||
|     def get_secondary_demodulator(self): |     def get_secondary_demodulator(self): | ||||||
|         return self.secondary_demodulator |         return self.secondary_demodulator | ||||||
|  |  | ||||||
|     def set_secondary_fft_size(self,secondary_fft_size): |     def set_secondary_fft_size(self, secondary_fft_size): | ||||||
|         #to change this, restart is required |         # to change this, restart is required | ||||||
|         self.secondary_fft_size=secondary_fft_size |         self.secondary_fft_size = secondary_fft_size | ||||||
|  |  | ||||||
|     def set_audio_compression(self,what): |     def set_audio_compression(self, what): | ||||||
|         self.audio_compression = what |         self.audio_compression = what | ||||||
|  |  | ||||||
|     def set_fft_compression(self,what): |     def set_fft_compression(self, what): | ||||||
|         self.fft_compression = what |         self.fft_compression = what | ||||||
|  |  | ||||||
|     def get_fft_bytes_to_read(self): |     def get_fft_bytes_to_read(self): | ||||||
|         if self.fft_compression=="none": return self.fft_size*4 |         if self.fft_compression == "none": | ||||||
|         if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2) |             return self.fft_size * 4 | ||||||
|  |         if self.fft_compression == "adpcm": | ||||||
|  |             return (self.fft_size / 2) + (10 / 2) | ||||||
|  |  | ||||||
|     def get_secondary_fft_bytes_to_read(self): |     def get_secondary_fft_bytes_to_read(self): | ||||||
|         if self.fft_compression=="none": return self.secondary_fft_size*4 |         if self.fft_compression == "none": | ||||||
|         if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) |             return self.secondary_fft_size * 4 | ||||||
|  |         if self.fft_compression == "adpcm": | ||||||
|  |             return (self.secondary_fft_size / 2) + (10 / 2) | ||||||
|  |  | ||||||
|     def set_samp_rate(self,samp_rate): |     def set_samp_rate(self, samp_rate): | ||||||
|         self.samp_rate=samp_rate |         self.samp_rate = samp_rate | ||||||
|         self.calculate_decimation() |         self.calculate_decimation() | ||||||
|         if self.running: self.restart() |         if self.running: | ||||||
|  |             self.restart() | ||||||
|  |  | ||||||
|     def calculate_decimation(self): |     def calculate_decimation(self): | ||||||
|         (self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate()) |         (self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate()) | ||||||
|  |  | ||||||
|     def get_decimation(self, input_rate, output_rate): |     def get_decimation(self, input_rate, output_rate): | ||||||
|         decimation=1 |         decimation = 1 | ||||||
|         while input_rate /  (decimation+1) >= output_rate: |         while input_rate / (decimation + 1) >= output_rate: | ||||||
|             decimation += 1 |             decimation += 1 | ||||||
|         fraction = float(input_rate / decimation) / output_rate |         fraction = float(input_rate / decimation) / output_rate | ||||||
|         intermediate_rate = input_rate / decimation |         intermediate_rate = input_rate / decimation | ||||||
|         return (decimation, fraction, intermediate_rate) |         return (decimation, fraction, intermediate_rate) | ||||||
|  |  | ||||||
|     def if_samp_rate(self): |     def if_samp_rate(self): | ||||||
|         return self.samp_rate/self.decimation |         return self.samp_rate / self.decimation | ||||||
|  |  | ||||||
|     def get_name(self): |     def get_name(self): | ||||||
|         return self.name |         return self.name | ||||||
| @@ -296,61 +411,73 @@ class dsp(object): | |||||||
|     def get_audio_rate(self): |     def get_audio_rate(self): | ||||||
|         if self.isDigitalVoice() or self.isPacket(): |         if self.isDigitalVoice() or self.isPacket(): | ||||||
|             return 48000 |             return 48000 | ||||||
|  |         elif self.isWsjtMode(): | ||||||
|  |             return 12000 | ||||||
|         return self.get_output_rate() |         return self.get_output_rate() | ||||||
|  |  | ||||||
|     def isDigitalVoice(self, demodulator = None): |     def isDigitalVoice(self, demodulator=None): | ||||||
|         if demodulator is None: |         if demodulator is None: | ||||||
|             demodulator = self.get_demodulator() |             demodulator = self.get_demodulator() | ||||||
|         return demodulator in ["dmr", "dstar", "nxdn", "ysf"] |         return demodulator in ["dmr", "dstar", "nxdn", "ysf"] | ||||||
|  |  | ||||||
|  |     def isWsjtMode(self, demodulator=None): | ||||||
|  |         if demodulator is None: | ||||||
|  |             demodulator = self.get_secondary_demodulator() | ||||||
|  |         return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] | ||||||
|  |  | ||||||
|     def isPacket(self, demodulator = None): |     def isPacket(self, demodulator = None): | ||||||
|         if demodulator is None: |         if demodulator is None: | ||||||
|             demodulator = self.get_demodulator() |             demodulator = self.get_demodulator() | ||||||
|         return demodulator == "packet" |         return demodulator == "packet" | ||||||
|  |  | ||||||
|     def set_output_rate(self,output_rate): |     def set_output_rate(self, output_rate): | ||||||
|         self.output_rate=output_rate |         self.output_rate = output_rate | ||||||
|         self.calculate_decimation() |         self.calculate_decimation() | ||||||
|  |  | ||||||
|     def set_demodulator(self,demodulator): |     def set_demodulator(self, demodulator): | ||||||
|         if (self.demodulator == demodulator): return |         if self.demodulator == demodulator: | ||||||
|         self.demodulator=demodulator |             return | ||||||
|  |         self.demodulator = demodulator | ||||||
|         self.calculate_decimation() |         self.calculate_decimation() | ||||||
|         self.restart() |         self.restart() | ||||||
|  |  | ||||||
|     def get_demodulator(self): |     def get_demodulator(self): | ||||||
|         return self.demodulator |         return self.demodulator | ||||||
|  |  | ||||||
|     def set_fft_size(self,fft_size): |     def set_fft_size(self, fft_size): | ||||||
|         self.fft_size=fft_size |         self.fft_size = fft_size | ||||||
|         self.restart() |         self.restart() | ||||||
|  |  | ||||||
|     def set_fft_fps(self,fft_fps): |     def set_fft_fps(self, fft_fps): | ||||||
|         self.fft_fps=fft_fps |         self.fft_fps = fft_fps | ||||||
|         self.restart() |         self.restart() | ||||||
|  |  | ||||||
|     def set_fft_averages(self,fft_averages): |     def set_fft_averages(self, fft_averages): | ||||||
|         self.fft_averages=fft_averages |         self.fft_averages = fft_averages | ||||||
|         self.restart() |         self.restart() | ||||||
|  |  | ||||||
|     def fft_block_size(self): |     def fft_block_size(self): | ||||||
|         if self.fft_averages == 0: return self.samp_rate/self.fft_fps |         if self.fft_averages == 0: | ||||||
|         else: return self.samp_rate/self.fft_fps/self.fft_averages |             return self.samp_rate / self.fft_fps | ||||||
|  |         else: | ||||||
|  |             return self.samp_rate / self.fft_fps / self.fft_averages | ||||||
|  |  | ||||||
|     def set_offset_freq(self,offset_freq): |     def set_offset_freq(self, offset_freq): | ||||||
|         self.offset_freq=offset_freq |         self.offset_freq = offset_freq | ||||||
|         if self.running: |         if self.running: | ||||||
|             self.modification_lock.acquire() |             self.modification_lock.acquire() | ||||||
|             self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate)) |             self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate)) | ||||||
|             self.shift_pipe_file.flush() |             self.shift_pipe_file.flush() | ||||||
|             self.modification_lock.release() |             self.modification_lock.release() | ||||||
|  |  | ||||||
|     def set_bpf(self,low_cut,high_cut): |     def set_bpf(self, low_cut, high_cut): | ||||||
|         self.low_cut=low_cut |         self.low_cut = low_cut | ||||||
|         self.high_cut=high_cut |         self.high_cut = high_cut | ||||||
|         if self.running: |         if self.running: | ||||||
|             self.modification_lock.acquire() |             self.modification_lock.acquire() | ||||||
|             self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) ) |             self.bpf_pipe_file.write( | ||||||
|  |                 "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate()) | ||||||
|  |             ) | ||||||
|             self.bpf_pipe_file.flush() |             self.bpf_pipe_file.flush() | ||||||
|             self.modification_lock.release() |             self.modification_lock.release() | ||||||
|  |  | ||||||
| @@ -358,12 +485,12 @@ class dsp(object): | |||||||
|         return [self.low_cut, self.high_cut] |         return [self.low_cut, self.high_cut] | ||||||
|  |  | ||||||
|     def set_squelch_level(self, squelch_level): |     def set_squelch_level(self, squelch_level): | ||||||
|         self.squelch_level=squelch_level |         self.squelch_level = squelch_level | ||||||
|         #no squelch required on digital voice modes |         # no squelch required on digital voice modes | ||||||
|         actual_squelch = 0 if self.isDigitalVoice() or self.isPacket() else self.squelch_level |         actual_squelch = 0 if self.isDigitalVoice() or self.isPacket() else self.squelch_level | ||||||
|         if self.running: |         if self.running: | ||||||
|             self.modification_lock.acquire() |             self.modification_lock.acquire() | ||||||
|             self.squelch_pipe_file.write("%g\n"%(float(actual_squelch))) |             self.squelch_pipe_file.write("%g\n" % (float(actual_squelch))) | ||||||
|             self.squelch_pipe_file.flush() |             self.squelch_pipe_file.flush() | ||||||
|             self.modification_lock.release() |             self.modification_lock.release() | ||||||
|  |  | ||||||
| @@ -379,7 +506,7 @@ class dsp(object): | |||||||
|             self.dmr_control_pipe_file.write("{0}\n".format(filter)) |             self.dmr_control_pipe_file.write("{0}\n".format(filter)) | ||||||
|             self.dmr_control_pipe_file.flush() |             self.dmr_control_pipe_file.flush() | ||||||
|  |  | ||||||
|     def mkfifo(self,path): |     def mkfifo(self, path): | ||||||
|         try: |         try: | ||||||
|             os.unlink(path) |             os.unlink(path) | ||||||
|         except: |         except: | ||||||
| @@ -387,64 +514,94 @@ class dsp(object): | |||||||
|         os.mkfifo(path) |         os.mkfifo(path) | ||||||
|  |  | ||||||
|     def ddc_transition_bw(self): |     def ddc_transition_bw(self): | ||||||
|         return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate)) |         return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate)) | ||||||
|  |  | ||||||
|     def try_create_pipes(self, pipe_names, command_base): |     def try_create_pipes(self, pipe_names, command_base): | ||||||
|         for pipe_name in pipe_names: |         for pipe_name in pipe_names: | ||||||
|             if "{"+pipe_name+"}" in command_base: |             if "{" + pipe_name + "}" in command_base: | ||||||
|                 setattr(self, pipe_name, self.pipe_base_path+pipe_name) |                 setattr(self, pipe_name, self.pipe_base_path + pipe_name) | ||||||
|                 self.mkfifo(getattr(self, pipe_name)) |                 self.mkfifo(getattr(self, pipe_name)) | ||||||
|             else: |             else: | ||||||
|                 setattr(self, pipe_name, None) |                 setattr(self, pipe_name, None) | ||||||
|  |  | ||||||
|     def try_delete_pipes(self, pipe_names): |     def try_delete_pipes(self, pipe_names): | ||||||
|         for pipe_name in pipe_names: |         for pipe_name in pipe_names: | ||||||
|             pipe_path = getattr(self,pipe_name,None) |             pipe_path = getattr(self, pipe_name, None) | ||||||
|             if pipe_path: |             if pipe_path: | ||||||
|                 try: os.unlink(pipe_path) |                 try: | ||||||
|  |                     os.unlink(pipe_path) | ||||||
|  |                 except FileNotFoundError: | ||||||
|  |                     # it seems like we keep calling this twice. no idea why, but we don't need the resulting error. | ||||||
|  |                     pass | ||||||
|                 except Exception: |                 except Exception: | ||||||
|                     logger.exception("try_delete_pipes()") |                     logger.exception("try_delete_pipes()") | ||||||
|  |  | ||||||
|     def start(self): |     def start(self): | ||||||
|         self.modification_lock.acquire() |         self.modification_lock.acquire() | ||||||
|         if (self.running): |         if self.running: | ||||||
|             self.modification_lock.release() |             self.modification_lock.release() | ||||||
|             return |             return | ||||||
|         self.running = True |         self.running = True | ||||||
|  |  | ||||||
|         command_base=self.chain(self.demodulator) |         command_base = " | ".join(self.chain(self.demodulator)) | ||||||
|  |  | ||||||
|         #create control pipes for csdr |         # create control pipes for csdr | ||||||
|         self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) |         self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self)) | ||||||
|  |  | ||||||
|         self.try_create_pipes(self.pipe_names, command_base) |         self.try_create_pipes(self.pipe_names, command_base) | ||||||
|  |  | ||||||
|         #run the command |         # run the command | ||||||
|         command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, |         command = command_base.format( | ||||||
|             last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, |             bpf_pipe=self.bpf_pipe, | ||||||
|             bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), |             shift_pipe=self.shift_pipe, | ||||||
|             flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, |             decimation=self.decimation, | ||||||
|             squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, meta_pipe=self.meta_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe, |             last_decimation=self.last_decimation, | ||||||
|             output_rate = self.get_output_rate(), smeter_report_every = int(self.if_samp_rate()/6000), |             fft_size=self.fft_size, | ||||||
|             unvoiced_quality = self.get_unvoiced_quality(), audio_rate = self.get_audio_rate(), |             fft_block_size=self.fft_block_size(), | ||||||
|             dmr_control_pipe = self.dmr_control_pipe) |             fft_averages=self.fft_averages, | ||||||
|  |             bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(), | ||||||
|  |             ddc_transition_bw=self.ddc_transition_bw(), | ||||||
|  |             flowcontrol=int(self.samp_rate * 2), | ||||||
|  |             start_bufsize=self.base_bufsize * self.decimation, | ||||||
|  |             nc_port=self.nc_port, | ||||||
|  |             squelch_pipe=self.squelch_pipe, | ||||||
|  |             smeter_pipe=self.smeter_pipe, | ||||||
|  |             meta_pipe=self.meta_pipe, | ||||||
|  |             iqtee_pipe=self.iqtee_pipe, | ||||||
|  |             iqtee2_pipe=self.iqtee2_pipe, | ||||||
|  |             output_rate=self.get_output_rate(), | ||||||
|  |             smeter_report_every=int(self.if_samp_rate() / 6000), | ||||||
|  |             unvoiced_quality=self.get_unvoiced_quality(), | ||||||
|  |             dmr_control_pipe=self.dmr_control_pipe, | ||||||
|  |             audio_rate=self.get_audio_rate(), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         logger.debug("[openwebrx-dsp-plugin:csdr] Command = %s", command) |         logger.debug("Command = %s", command) | ||||||
|         my_env=os.environ.copy() |         my_env = os.environ.copy() | ||||||
|         if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; |         if self.csdr_dynamic_bufsize: | ||||||
|         if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; |             my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1" | ||||||
|         self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) |         if self.csdr_print_bufsizes: | ||||||
|  |             my_env["CSDR_PRINT_BUFSIZES"] = "1" | ||||||
|  |  | ||||||
|  |         out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL | ||||||
|  |         self.process = subprocess.Popen(command, stdout=out, shell=True, preexec_fn=os.setpgrp, env=my_env) | ||||||
|  |  | ||||||
|         def watch_thread(): |         def watch_thread(): | ||||||
|             rc = self.process.wait() |             rc = self.process.wait() | ||||||
|             logger.debug("dsp thread ended with rc=%d", rc) |             logger.debug("dsp thread ended with rc=%d", rc) | ||||||
|             if (rc == 0 and self.running and not self.modification_lock.locked()): |             if rc == 0 and self.running and not self.modification_lock.locked(): | ||||||
|                 logger.debug("restarting since rc = 0, self.running = true, and no modification") |                 logger.debug("restarting since rc = 0, self.running = true, and no modification") | ||||||
|                 self.restart() |                 self.restart() | ||||||
|  |  | ||||||
|         threading.Thread(target = watch_thread).start() |         threading.Thread(target=watch_thread).start() | ||||||
|  |  | ||||||
|         self.output.add_output("audio", partial(self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256)) |         if self.output.supports_type("audio"): | ||||||
|  |             self.output.send_output( | ||||||
|  |                 "audio", | ||||||
|  |                 partial( | ||||||
|  |                     self.process.stdout.read, int(self.get_fft_bytes_to_read()) if self.demodulator == "fft" else 256 | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         # open control pipes for csdr |         # open control pipes for csdr | ||||||
|         if self.bpf_pipe: |         if self.bpf_pipe: | ||||||
| @@ -465,24 +622,28 @@ class dsp(object): | |||||||
|         if self.bpf_pipe: |         if self.bpf_pipe: | ||||||
|             self.set_bpf(self.low_cut, self.high_cut) |             self.set_bpf(self.low_cut, self.high_cut) | ||||||
|         if self.smeter_pipe: |         if self.smeter_pipe: | ||||||
|             self.smeter_pipe_file=open(self.smeter_pipe,"r") |             self.smeter_pipe_file = open(self.smeter_pipe, "r") | ||||||
|  |  | ||||||
|             def read_smeter(): |             def read_smeter(): | ||||||
|                 raw = self.smeter_pipe_file.readline() |                 raw = self.smeter_pipe_file.readline() | ||||||
|                 if len(raw) == 0: |                 if len(raw) == 0: | ||||||
|                     return None |                     return None | ||||||
|                 else: |                 else: | ||||||
|                     return float(raw.rstrip("\n")) |                     return float(raw.rstrip("\n")) | ||||||
|             self.output.add_output("smeter", read_smeter) |  | ||||||
|  |             self.output.send_output("smeter", read_smeter) | ||||||
|         if self.meta_pipe != None: |         if self.meta_pipe != None: | ||||||
|             # TODO make digiham output unicode and then change this here |             # TODO make digiham output unicode and then change this here | ||||||
|             self.meta_pipe_file=open(self.meta_pipe, "r", encoding="cp437") |             self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437") | ||||||
|  |  | ||||||
|             def read_meta(): |             def read_meta(): | ||||||
|                 raw = self.meta_pipe_file.readline() |                 raw = self.meta_pipe_file.readline() | ||||||
|                 if len(raw) == 0: |                 if len(raw) == 0: | ||||||
|                     return None |                     return None | ||||||
|                 else: |                 else: | ||||||
|                     return raw.rstrip("\n") |                     return raw.rstrip("\n") | ||||||
|             self.output.add_output("meta", read_meta) |  | ||||||
|  |             self.output.send_output("meta", read_meta) | ||||||
|  |  | ||||||
|         if self.dmr_control_pipe: |         if self.dmr_control_pipe: | ||||||
|             self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w") |             self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w") | ||||||
| @@ -503,10 +664,11 @@ class dsp(object): | |||||||
|         self.modification_lock.release() |         self.modification_lock.release() | ||||||
|  |  | ||||||
|     def restart(self): |     def restart(self): | ||||||
|         if not self.running: return |         if not self.running: | ||||||
|  |             return | ||||||
|         self.stop() |         self.stop() | ||||||
|         self.start() |         self.start() | ||||||
|  |  | ||||||
|     def __del__(self): |     def __del__(self): | ||||||
|         self.stop() |         self.stop() | ||||||
|         del(self.process) |         del self.process | ||||||
|   | |||||||
| @@ -14,8 +14,8 @@ function cmakebuild() { | |||||||
|  |  | ||||||
| cd /tmp | cd /tmp | ||||||
|  |  | ||||||
| STATIC_PACKAGES="libusb fftw" | STATIC_PACKAGES="libusb fftw udev" | ||||||
| BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++ libusb-dev fftw-dev" | BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev" | ||||||
|  |  | ||||||
| apk add --no-cache $STATIC_PACKAGES | apk add --no-cache $STATIC_PACKAGES | ||||||
| apk add --no-cache --virtual .build-deps $BUILD_PACKAGES | apk add --no-cache --virtual .build-deps $BUILD_PACKAGES | ||||||
|   | |||||||
| @@ -14,8 +14,8 @@ function cmakebuild() { | |||||||
|  |  | ||||||
| cd /tmp | cd /tmp | ||||||
|  |  | ||||||
| STATIC_PACKAGES="libusb" | STATIC_PACKAGES="libusb udev" | ||||||
| BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++ libusb-dev" | BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev" | ||||||
|  |  | ||||||
| apk add --no-cache $STATIC_PACKAGES | apk add --no-cache $STATIC_PACKAGES | ||||||
| apk add --no-cache --virtual .build-deps $BUILD_PACKAGES | apk add --no-cache --virtual .build-deps $BUILD_PACKAGES | ||||||
|   | |||||||
| @@ -14,8 +14,10 @@ function cmakebuild() { | |||||||
|  |  | ||||||
| cd /tmp | cd /tmp | ||||||
|  |  | ||||||
| BUILD_PACKAGES="git cmake make patch wget sudo udev gcc g++" | STATIC_PACKAGES="udev" | ||||||
|  | BUILD_PACKAGES="git cmake make patch wget sudo gcc g++" | ||||||
|  |  | ||||||
|  | apk add --no-cache $STATIC_PACKAGES | ||||||
| apk add --no-cache --virtual .build-deps $BUILD_PACKAGES | apk add --no-cache --virtual .build-deps $BUILD_PACKAGES | ||||||
|  |  | ||||||
| git clone https://github.com/pothosware/SoapySDR | git clone https://github.com/pothosware/SoapySDR | ||||||
|   | |||||||
| @@ -14,8 +14,8 @@ function cmakebuild() { | |||||||
|  |  | ||||||
| cd /tmp | cd /tmp | ||||||
|  |  | ||||||
| STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack" | STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport" | ||||||
| BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers" | BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev asciidoctor asciidoc" | ||||||
|  |  | ||||||
| apk add --no-cache $STATIC_PACKAGES | apk add --no-cache $STATIC_PACKAGES | ||||||
| apk add --no-cache --virtual .build-deps $BUILD_PACKAGES | apk add --no-cache --virtual .build-deps $BUILD_PACKAGES | ||||||
| @@ -23,7 +23,7 @@ apk add --no-cache --virtual .build-deps $BUILD_PACKAGES | |||||||
| git clone https://git.code.sf.net/p/itpp/git itpp | git clone https://git.code.sf.net/p/itpp/git itpp | ||||||
| cmakebuild itpp | cmakebuild itpp | ||||||
|  |  | ||||||
| git clone https://github.com/simonyiszk/csdr.git | git clone https://github.com/jketterl/csdr.git -b 48khz_filter | ||||||
| cd csdr | cd csdr | ||||||
| patch -Np1 <<'EOF' | patch -Np1 <<'EOF' | ||||||
| --- a/csdr.c | --- a/csdr.c | ||||||
| @@ -68,6 +68,10 @@ rm -rf csdr | |||||||
|  |  | ||||||
| git clone https://github.com/szechyjs/mbelib.git | git clone https://github.com/szechyjs/mbelib.git | ||||||
| cmakebuild mbelib | cmakebuild mbelib | ||||||
|  | if [ -d "/usr/local/lib64" ]; then | ||||||
|  |     # no idea why it's put into there now. alpine does not handle it correctly, so move it. | ||||||
|  |     mv /usr/local/lib64/libmbe* /usr/local/lib | ||||||
|  | fi | ||||||
|  |  | ||||||
| git clone https://github.com/jketterl/digiham.git | git clone https://github.com/jketterl/digiham.git | ||||||
| cmakebuild digiham | cmakebuild digiham | ||||||
| @@ -75,4 +79,10 @@ cmakebuild digiham | |||||||
| git clone https://github.com/f4exb/dsd.git | git clone https://github.com/f4exb/dsd.git | ||||||
| cmakebuild dsd | cmakebuild dsd | ||||||
|  |  | ||||||
|  | WSJT_DIR=wsjtx-2.0.1 | ||||||
|  | WSJT_TGZ=${WSJT_DIR}.tgz | ||||||
|  | wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ | ||||||
|  | tar xvfz $WSJT_TGZ | ||||||
|  | cmakebuild $WSJT_DIR | ||||||
|  |  | ||||||
| apk del .build-deps | apk del .build-deps | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								htdocs/css/features.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								htdocs/css/features.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | @import url("openwebrx-header.css"); | ||||||
|  | @import url("openwebrx-globals.css"); | ||||||
|  |  | ||||||
|  | /* expandable photo not implemented on features page */ | ||||||
|  | #webrx-top-photo-clip { | ||||||
|  |     max-height: 67px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | h1 { | ||||||
|  |     text-align: center; | ||||||
|  |     margin: 50px 0; | ||||||
|  | } | ||||||
							
								
								
									
										57
									
								
								htdocs/css/map.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								htdocs/css/map.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | @import url("openwebrx-header.css"); | ||||||
|  | @import url("openwebrx-globals.css"); | ||||||
|  |  | ||||||
|  | /* expandable photo not implemented on map page */ | ||||||
|  | #webrx-top-photo-clip { | ||||||
|  |     max-height: 67px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-top-container { | ||||||
|  |     flex: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .openwebrx-map { | ||||||
|  |     flex: 1 1 auto; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | h3 { | ||||||
|  |     margin: 10px 0; | ||||||
|  |     text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ul { | ||||||
|  |     margin-block-start: 5px; | ||||||
|  |     margin-block-end: 5px; | ||||||
|  |     padding-inline-start: 25px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .openwebrx-map-legend { | ||||||
|  |     background-color: #fff; | ||||||
|  |     padding: 10px; | ||||||
|  |     margin: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .openwebrx-map-legend ul { | ||||||
|  |     list-style-type: none; | ||||||
|  |     padding: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .openwebrx-map-legend li.square .illustration { | ||||||
|  |     display: inline-block; | ||||||
|  |     width: 30px; | ||||||
|  |     height: 20px; | ||||||
|  |     margin-right: 10px; | ||||||
|  |     border-width: 2px; | ||||||
|  |     border-style: solid; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .openwebrx-map-legend select { | ||||||
|  |     background-color: #FFF; | ||||||
|  |     border-color: #DDD; | ||||||
|  |     padding: 5px; | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								htdocs/css/openwebrx-globals.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								htdocs/css/openwebrx-globals.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | html, body | ||||||
|  | { | ||||||
|  |     margin: 0; | ||||||
|  |     padding: 0; | ||||||
|  |     height: 100%; | ||||||
|  |     font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										203
									
								
								htdocs/css/openwebrx-header.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								htdocs/css/openwebrx-header.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | |||||||
|  | #webrx-top-container | ||||||
|  | { | ||||||
|  |     position: relative; | ||||||
|  |     z-index:1000; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-top-photo | ||||||
|  | { | ||||||
|  |     width: 100%; | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-top-photo-clip | ||||||
|  | { | ||||||
|  |     min-height: 67px; | ||||||
|  |     max-height: 350px; | ||||||
|  |     overflow: hidden; | ||||||
|  |     position: relative; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .webrx-top-bar-parts | ||||||
|  | { | ||||||
|  |     height:67px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-top-bar | ||||||
|  | { | ||||||
|  |     background: rgba(128, 128, 128, 0.15); | ||||||
|  |     margin:0; | ||||||
|  |     padding:0; | ||||||
|  |     user-select: none; | ||||||
|  |     -webkit-touch-callout: none; | ||||||
|  |     -webkit-user-select: none; | ||||||
|  |     -khtml-user-select: none; | ||||||
|  |     -moz-user-select: none; | ||||||
|  |     -ms-user-select: none; | ||||||
|  |     overflow: hidden; | ||||||
|  |     position: absolute; | ||||||
|  |     left: 0; | ||||||
|  |     top: 0; | ||||||
|  |     right: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-top-logo | ||||||
|  | { | ||||||
|  |     padding: 12px; | ||||||
|  |     float: left; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-ha5kfu-top-logo | ||||||
|  | { | ||||||
|  |     float: right; | ||||||
|  |     padding: 15px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-avatar-background | ||||||
|  | { | ||||||
|  |     cursor:pointer; | ||||||
|  |     background-image: url(../gfx/openwebrx-avatar-background.png); | ||||||
|  |     background-origin: content-box; | ||||||
|  |     background-repeat: no-repeat; | ||||||
|  |     float: left; | ||||||
|  |     width: 54px; | ||||||
|  |     height: 54px; | ||||||
|  |     padding: 7px; | ||||||
|  |     box-sizing: content-box; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-avatar | ||||||
|  | { | ||||||
|  |     cursor:pointer; | ||||||
|  |     width: 46px; | ||||||
|  |     height: 46px; | ||||||
|  |     padding: 4px; | ||||||
|  |     border-radius: 8px; | ||||||
|  |     box-sizing: content-box; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-texts { | ||||||
|  |     float: left; | ||||||
|  |     padding: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-texts div { | ||||||
|  |     padding: 3px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-title | ||||||
|  | { | ||||||
|  |     white-space:nowrap; | ||||||
|  |     overflow: hidden; | ||||||
|  |     cursor:pointer; | ||||||
|  |     font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; | ||||||
|  |     color: #909090; | ||||||
|  |     font-size: 11pt; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-desc | ||||||
|  | { | ||||||
|  |     white-space:nowrap; | ||||||
|  |     overflow: hidden; | ||||||
|  |     cursor:pointer; | ||||||
|  |     font-size: 10pt; | ||||||
|  |     color: #909090; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-desc a | ||||||
|  | { | ||||||
|  |     color: #909090; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #openwebrx-rx-details-arrow | ||||||
|  | { | ||||||
|  |     cursor:pointer; | ||||||
|  |     position: absolute; | ||||||
|  |     left: 470px; | ||||||
|  |     top: 51px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #openwebrx-rx-details-arrow a | ||||||
|  | { | ||||||
|  |     margin: 0; | ||||||
|  |     padding: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #openwebrx-rx-details-arrow-down | ||||||
|  | { | ||||||
|  |     display:none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #openwebrx-main-buttons ul | ||||||
|  | { | ||||||
|  |     display: table; | ||||||
|  |     margin:0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #openwebrx-main-buttons ul li | ||||||
|  | { | ||||||
|  |     display: table-cell; | ||||||
|  |     padding-left: 5px; | ||||||
|  |     padding-right: 5px; | ||||||
|  |     cursor:pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #openwebrx-main-buttons a { | ||||||
|  |     color: inherit; | ||||||
|  |     text-decoration: inherit; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #openwebrx-main-buttons li:hover | ||||||
|  | { | ||||||
|  |     background-color: rgba(255, 255, 255, 0.3); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #openwebrx-main-buttons li:active | ||||||
|  | { | ||||||
|  |     background-color: rgba(255, 255, 255, 0.55); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #openwebrx-main-buttons | ||||||
|  | { | ||||||
|  |     float: right; | ||||||
|  |     margin:0; | ||||||
|  |     color: white; | ||||||
|  |     text-shadow: 0px 0px 4px #000000; | ||||||
|  |     text-align: center; | ||||||
|  |     font-size: 9pt; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-photo-title | ||||||
|  | { | ||||||
|  |     position: absolute; | ||||||
|  |     left: 15px; | ||||||
|  |     top: 78px; | ||||||
|  |     color: White; | ||||||
|  |     font-size: 16pt; | ||||||
|  |     text-shadow: 1px 1px 4px #444; | ||||||
|  |     opacity: 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-photo-desc | ||||||
|  | { | ||||||
|  |     position: absolute; | ||||||
|  |     left: 15px; | ||||||
|  |     top: 109px; | ||||||
|  |     color: White; | ||||||
|  |     font-size: 10pt; | ||||||
|  |     font-weight: bold; | ||||||
|  |     text-shadow: 0px 0px 6px #444; | ||||||
|  |     opacity: 1; | ||||||
|  |     line-height: 1.5em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #webrx-rx-photo-desc a | ||||||
|  | { | ||||||
|  |     color: #5ca8ff; | ||||||
|  |     text-shadow: none; | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -18,13 +18,10 @@ | |||||||
|     along with this program.  If not, see <http://www.gnu.org/licenses/>. |     along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
| 
 | 
 | ||||||
| */ | */ | ||||||
|  | @import url("openwebrx-header.css"); | ||||||
|  | @import url("openwebrx-globals.css"); | ||||||
| 
 | 
 | ||||||
| html, body | html, body { | ||||||
| { |  | ||||||
| 	margin: 0; |  | ||||||
| 	padding: 0; |  | ||||||
| 	height: 100%; |  | ||||||
| 	font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; |  | ||||||
| 	overflow: hidden; | 	overflow: hidden; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -147,182 +144,16 @@ input[type=range]:focus::-ms-fill-upper | |||||||
| 	background: #B6B6B6; | 	background: #B6B6B6; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #webrx-top-container |  | ||||||
| { |  | ||||||
| 	position: relative; |  | ||||||
| 	z-index:1000; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .webrx-top-bar-parts |  | ||||||
| { |  | ||||||
| 	height:67px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-top-bar |  | ||||||
| { |  | ||||||
| 	background: rgba(128, 128, 128, 0.15); |  | ||||||
| 	margin:0; |  | ||||||
| 	padding:0; |  | ||||||
| 	user-select: none; |  | ||||||
|     -webkit-touch-callout: none; |  | ||||||
|     -webkit-user-select: none; |  | ||||||
|     -khtml-user-select: none; |  | ||||||
|     -moz-user-select: none; |  | ||||||
|     -ms-user-select: none; |  | ||||||
|     overflow: hidden; |  | ||||||
|     position: absolute; |  | ||||||
|     left: 0; |  | ||||||
|     top: 0; |  | ||||||
|     right: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-top-logo |  | ||||||
| { |  | ||||||
|     padding: 12px; |  | ||||||
|     float: left; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-ha5kfu-top-logo |  | ||||||
| { |  | ||||||
|     float: right; |  | ||||||
|     padding: 15px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-top-photo |  | ||||||
| { |  | ||||||
| 	width: 100%; |  | ||||||
| 	display: block; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-rx-avatar-background |  | ||||||
| { |  | ||||||
| 	cursor:pointer; |  | ||||||
|     background-image: url(gfx/openwebrx-avatar-background.png); |  | ||||||
|     background-origin: content-box; |  | ||||||
|     background-repeat: no-repeat; |  | ||||||
|     float: left; |  | ||||||
|     width: 54px; |  | ||||||
|     height: 54px; |  | ||||||
|     padding: 7px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-rx-avatar |  | ||||||
| { |  | ||||||
| 	cursor:pointer; |  | ||||||
| 	width: 46px; |  | ||||||
| 	height: 46px; |  | ||||||
| 	padding: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-top-photo-clip |  | ||||||
| { |  | ||||||
|     min-height: 67px; |  | ||||||
| 	max-height: 350px; |  | ||||||
| 	overflow: hidden; |  | ||||||
| 	position: relative; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-page-container | #webrx-page-container | ||||||
| { | { | ||||||
|    min-height:100%; |    min-height:100%; | ||||||
|    position:relative; |    position:relative; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #webrx-rx-photo-title |  | ||||||
| { |  | ||||||
| 	position: absolute; |  | ||||||
| 	left: 15px; |  | ||||||
| 	top: 78px; |  | ||||||
| 	color: White; |  | ||||||
| 	font-size: 16pt; |  | ||||||
| 	text-shadow: 1px 1px 4px #444; |  | ||||||
| 	opacity: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-rx-photo-desc |  | ||||||
| { |  | ||||||
| 	position: absolute; |  | ||||||
| 	left: 15px; |  | ||||||
| 	top: 109px; |  | ||||||
| 	color: White; |  | ||||||
| 	font-size: 10pt; |  | ||||||
| 	font-weight: bold; |  | ||||||
| 	text-shadow: 0px 0px 6px #444; |  | ||||||
| 	opacity: 1; |  | ||||||
| 	line-height: 1.5em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-rx-photo-desc a |  | ||||||
| { |  | ||||||
| 	color: #5ca8ff; |  | ||||||
| 	text-shadow: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-rx-texts { |  | ||||||
|     float: left; |  | ||||||
|     padding: 10px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-rx-texts div { |  | ||||||
|     padding: 3px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-rx-title |  | ||||||
| { |  | ||||||
| 	white-space:nowrap; |  | ||||||
| 	overflow: hidden; |  | ||||||
| 	cursor:pointer; |  | ||||||
| 	font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; |  | ||||||
| 	color: #909090; |  | ||||||
| 	font-size: 11pt; |  | ||||||
| 	font-weight: bold; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-rx-desc |  | ||||||
| { |  | ||||||
| 	white-space:nowrap; |  | ||||||
| 	overflow: hidden; |  | ||||||
| 	cursor:pointer; |  | ||||||
| 	font-size: 10pt; |  | ||||||
| 	color: #909090; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #webrx-rx-desc a |  | ||||||
| { |  | ||||||
| 	color: #909090; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #openwebrx-rx-details-arrow |  | ||||||
| { |  | ||||||
| 	cursor:pointer; |  | ||||||
| 	position: absolute; |  | ||||||
| 	left: 470px; |  | ||||||
| 	top: 51px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #openwebrx-rx-details-arrow a |  | ||||||
| { |  | ||||||
| 	margin: 0; |  | ||||||
| 	padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #openwebrx-rx-details-arrow-down |  | ||||||
| { |  | ||||||
| 	display:none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /*canvas#waterfall-canvas |  | ||||||
| { |  | ||||||
| 	border-style: none; |  | ||||||
| 	border-width: 1px; |  | ||||||
| 	height: 150px; |  | ||||||
| 	width: 100%; |  | ||||||
| }*/ |  | ||||||
| 
 |  | ||||||
| #openwebrx-scale-container | #openwebrx-scale-container | ||||||
| { | { | ||||||
| 	height: 47px; | 	height: 47px; | ||||||
| 	background-image: url("gfx/openwebrx-scale-background.png"); | 	background-image: url("../gfx/openwebrx-scale-background.png"); | ||||||
| 	background-repeat: repeat-x; | 	background-repeat: repeat-x; | ||||||
| 	overflow: hidden; | 	overflow: hidden; | ||||||
| 	z-index:1000; | 	z-index:1000; | ||||||
| @@ -331,14 +162,14 @@ input[type=range]:focus::-ms-fill-upper | |||||||
| 
 | 
 | ||||||
| #webrx-canvas-container | #webrx-canvas-container | ||||||
| { | { | ||||||
| 	/*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/ | 	/*background-image:url('../gfx/openwebrx-blank-background-1.jpg');*/ | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 	height: 2000px; | 	height: 2000px; | ||||||
| 	overflow-y: scroll; | 	overflow-y: scroll; | ||||||
| 	overflow-x: hidden; | 	overflow-x: hidden; | ||||||
| 	/*background-color: #646464;*/ | 	/*background-color: #646464;*/ | ||||||
| 	/*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/ | 	/*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/ | ||||||
| 	background-image: url('gfx/openwebrx-background-cool-blue.png'); | 	background-image: url('../gfx/openwebrx-background-cool-blue.png'); | ||||||
| 	background-repeat: no-repeat; | 	background-repeat: no-repeat; | ||||||
| 	background-color: #1e5f7f; | 	background-color: #1e5f7f; | ||||||
| 	cursor: crosshair; | 	cursor: crosshair; | ||||||
| @@ -428,15 +259,15 @@ input[type=range]:focus::-ms-fill-upper | |||||||
| /* removed non-free fonts like that: */ | /* removed non-free fonts like that: */ | ||||||
| /*@font-face { | /*@font-face { | ||||||
|     font-family: 'unibody_8_pro_regregular'; |     font-family: 'unibody_8_pro_regregular'; | ||||||
|     src: url('gfx/unibody8pro-regular-webfont.eot'); |     src: url('../gfx/unibody8pro-regular-webfont.eot'); | ||||||
|     src: url('gfx/unibody8pro-regular-webfont.ttf'); |     src: url('../gfx/unibody8pro-regular-webfont.ttf'); | ||||||
|     font-weight: normal; |     font-weight: normal; | ||||||
|     font-style: normal; |     font-style: normal; | ||||||
| }*/ | }*/ | ||||||
| 
 | 
 | ||||||
| @font-face { | @font-face { | ||||||
|     font-family: 'expletus-sans-medium'; |     font-family: 'expletus-sans-medium'; | ||||||
|     src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf'); |     src: url('../gfx/font-expletus-sans/ExpletusSans-Medium.ttf'); | ||||||
|     font-weight: normal; |     font-weight: normal; | ||||||
|     font-style: normal; |     font-style: normal; | ||||||
| } | } | ||||||
| @@ -533,6 +364,20 @@ input[type=range]:focus::-ms-fill-upper | |||||||
| 	text-align: center; | 	text-align: center; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .openwebrx-dial-button svg { | ||||||
|  |     width: 19px; | ||||||
|  |     height: 19px; | ||||||
|  |     vertical-align: bottom; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .openwebrx-dial-button #ph_dial { | ||||||
|  |     fill: #888; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .openwebrx-dial-button.available #ph_dial { | ||||||
|  |     fill: #FFF; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .openwebrx-square-button img | .openwebrx-square-button img | ||||||
| { | { | ||||||
| 	height: 27px; | 	height: 27px; | ||||||
| @@ -637,47 +482,6 @@ img.openwebrx-mirror-img | |||||||
| 	height: 20px; | 	height: 20px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #openwebrx-main-buttons img |  | ||||||
| { |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #openwebrx-main-buttons ul |  | ||||||
| { |  | ||||||
| 	display: table; |  | ||||||
| 	margin:0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| #openwebrx-main-buttons ul li |  | ||||||
| { |  | ||||||
| 	display: table-cell; |  | ||||||
| 	padding-left: 5px; |  | ||||||
| 	padding-right: 5px; |  | ||||||
| 	cursor:pointer; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #openwebrx-main-buttons li:hover |  | ||||||
| { |  | ||||||
| 	background-color: rgba(255, 255, 255, 0.3); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #openwebrx-main-buttons li:active |  | ||||||
| { |  | ||||||
| 	background-color: rgba(255, 255, 255, 0.55); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| #openwebrx-main-buttons |  | ||||||
| { |  | ||||||
|     float: right; |  | ||||||
| 	margin:0; |  | ||||||
| 	color: white; |  | ||||||
| 	text-shadow: 0px 0px 4px #000000; |  | ||||||
| 	text-align: center; |  | ||||||
| 	font-size: 9pt; |  | ||||||
| 	font-weight: bold; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #openwebrx-panel-receiver | #openwebrx-panel-receiver | ||||||
| { | { | ||||||
| 	width:110px; | 	width:110px; | ||||||
| @@ -812,7 +616,7 @@ img.openwebrx-mirror-img | |||||||
| 
 | 
 | ||||||
| #openwebrx-secondary-demod-listbox | #openwebrx-secondary-demod-listbox | ||||||
| { | { | ||||||
| 	width: 201px; | 	width: 174px; | ||||||
| 	height: 27px; | 	height: 27px; | ||||||
| 	padding-left:3px; | 	padding-left:3px; | ||||||
| } | } | ||||||
| @@ -951,7 +755,7 @@ img.openwebrx-mirror-img | |||||||
| .openwebrx-meta-slot.muted:before { | .openwebrx-meta-slot.muted:before { | ||||||
|     display: block; |     display: block; | ||||||
|     content: ""; |     content: ""; | ||||||
|     background-image: url("gfx/openwebrx-mute.png"); |     background-image: url("../gfx/openwebrx-mute.png"); | ||||||
|     width:100%; |     width:100%; | ||||||
|     height:133px; |     height:133px; | ||||||
|     background-position: center; |     background-position: center; | ||||||
| @@ -993,11 +797,11 @@ img.openwebrx-mirror-img | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .openwebrx-meta-slot.active .openwebrx-meta-user-image { | .openwebrx-meta-slot.active .openwebrx-meta-user-image { | ||||||
|     background-image: url("gfx/openwebrx-directcall.png"); |     background-image: url("../gfx/openwebrx-directcall.png"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .openwebrx-meta-slot.active .openwebrx-meta-user-image.group { | .openwebrx-meta-slot.active .openwebrx-meta-user-image.group { | ||||||
|     background-image: url("gfx/openwebrx-groupcall.png"); |     background-image: url("../gfx/openwebrx-groupcall.png"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .openwebrx-dmr-timeslot-panel * { | .openwebrx-dmr-timeslot-panel * { | ||||||
| @@ -1005,7 +809,7 @@ img.openwebrx-mirror-img | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .openwebrx-maps-pin { | .openwebrx-maps-pin { | ||||||
|     background-image: url("gfx/google_maps_pin.svg"); |     background-image: url("../gfx/google_maps_pin.svg"); | ||||||
|     background-position: center; |     background-position: center; | ||||||
|     background-repeat: no-repeat; |     background-repeat: no-repeat; | ||||||
|     width: 15px; |     width: 15px; | ||||||
| @@ -1013,3 +817,62 @@ img.openwebrx-mirror-img | |||||||
|     background-size: contain; |     background-size: contain; | ||||||
|     display: inline-block; |     display: inline-block; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #openwebrx-panel-wsjt-message { | ||||||
|  |     height: 180px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #openwebrx-panel-wsjt-message tbody  { | ||||||
|  |     display: block; | ||||||
|  |     overflow: auto; | ||||||
|  |     height: 150px; | ||||||
|  |     width: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #openwebrx-panel-wsjt-message thead tr { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #openwebrx-panel-wsjt-message th, | ||||||
|  | #openwebrx-panel-wsjt-message td { | ||||||
|  |     width: 50px; | ||||||
|  |     text-align: left; | ||||||
|  |     padding: 1px 3px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #openwebrx-panel-wsjt-message .message { | ||||||
|  |     width: 380px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #openwebrx-panel-wsjt-message .decimal { | ||||||
|  |     text-align: right; | ||||||
|  |     width: 35px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #openwebrx-panel-wsjt-message .decimal.freq { | ||||||
|  |     width: 70px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-content-container, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel | ||||||
|  | { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container, | ||||||
|  | #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container | ||||||
|  | { | ||||||
|  |     height: 200px; | ||||||
|  |     margin: -10px; | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								htdocs/features.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								htdocs/features.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | <HTML><HEAD> | ||||||
|  |     <TITLE>OpenWebRX Feature report</TITLE> | ||||||
|  |     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||||||
|  |     <link rel="stylesheet" href="static/css/features.css"> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script> | ||||||
|  |     <script src="static/lib/jquery-3.2.1.min.js"></script> | ||||||
|  |     <script src="static/features.js"></script> | ||||||
|  | </HEAD><BODY> | ||||||
|  |     ${header} | ||||||
|  |     <div class="container"> | ||||||
|  |         <h1>OpenWebRX Feature Report</h1> | ||||||
|  |         <table class="features table"> | ||||||
|  |             <tr> | ||||||
|  |                 <th>Feature</th> | ||||||
|  |                 <th>Requirement</th> | ||||||
|  |                 <th>Description</th> | ||||||
|  |                 <th>Available</th> | ||||||
|  |             </tr> | ||||||
|  |         </table> | ||||||
|  |     </div> | ||||||
|  | </BODY></HTML> | ||||||
							
								
								
									
										24
									
								
								htdocs/features.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								htdocs/features.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | $(function(){ | ||||||
|  |     var converter = new showdown.Converter(); | ||||||
|  |     $.ajax('/api/features').done(function(data){ | ||||||
|  |         var $table = $('table.features'); | ||||||
|  |         $.each(data, function(name, details) { | ||||||
|  |             var requirements = $.map(details.requirements, function(r, name){ | ||||||
|  |                 return '<tr>' + | ||||||
|  |                            '<td></td>' + | ||||||
|  |                            '<td>' + name + '</td>' + | ||||||
|  |                            '<td>' + converter.makeHtml(r.description) + '</td>' + | ||||||
|  |                            '<td>' + (r.available ? 'YES' : 'NO') + '</td>' + | ||||||
|  |                        '</tr>'; | ||||||
|  |             }); | ||||||
|  |             $table.append( | ||||||
|  |                 '<tr>' + | ||||||
|  |                     '<td colspan=2>' + name + '</td>' + | ||||||
|  |                     '<td>' + converter.makeHtml(details.description) + '</td>' + | ||||||
|  |                     '<td>' + (details.available ? 'YES' : 'NO') + '</td>' + | ||||||
|  |                 '</tr>' + | ||||||
|  |                 requirements.join("") | ||||||
|  |             ); | ||||||
|  |         }) | ||||||
|  |     }); | ||||||
|  | }); | ||||||
							
								
								
									
										
											BIN
										
									
								
								htdocs/gfx/openwebrx-panel-map.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								htdocs/gfx/openwebrx-panel-map.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.0 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 69 KiB | 
| @@ -1,85 +0,0 @@ | |||||||
| <html> |  | ||||||
| <!-- |  | ||||||
|  |  | ||||||
| 	This file is part of OpenWebRX,  |  | ||||||
| 	an open-source SDR receiver software with a web UI. |  | ||||||
| 	Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> |  | ||||||
|  |  | ||||||
|     This program is free software: you can redistribute it and/or modify |  | ||||||
|     it under the terms of the GNU Affero General Public License as |  | ||||||
|     published by the Free Software Foundation, either version 3 of the |  | ||||||
|     License, or (at your option) any later version. |  | ||||||
|  |  | ||||||
|     This program is distributed in the hope that it will be useful, |  | ||||||
|     but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|     GNU Affero General Public License for more details. |  | ||||||
|  |  | ||||||
|     You should have received a copy of the GNU Affero General Public License |  | ||||||
|     along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
|  |  | ||||||
| --> |  | ||||||
| <head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |  | ||||||
| <style> |  | ||||||
| html, body |  | ||||||
| { |  | ||||||
| 	font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; |  | ||||||
| 	width: 100%; |  | ||||||
| 	text-align: center; |  | ||||||
| 	margin: 0; |  | ||||||
| 	padding: 0; |  | ||||||
| } |  | ||||||
| img.logo |  | ||||||
| {  |  | ||||||
| 	margin-top: 120px; |  | ||||||
| } |  | ||||||
| div.frame |  | ||||||
| { |  | ||||||
| 	text-align: left; |  | ||||||
| 	margin:0px auto; |  | ||||||
| 	width: 800px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| div.panel |  | ||||||
| { |  | ||||||
| 	text-align: center; |  | ||||||
| 	background-color:#777777;  |  | ||||||
| 	border-radius: 15px; |  | ||||||
| 	padding: 12px; |  | ||||||
| 	font-weight: bold; |  | ||||||
| 	color: White; |  | ||||||
| 	font-size: 13pt; |  | ||||||
| 	/*text-shadow: 1px 1px 4px #444;*/ |  | ||||||
| 	font-family: sans; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| div.alt |  | ||||||
| { |  | ||||||
| 	font-size: 10pt; |  | ||||||
| 	padding-top: 10px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| body div a |  | ||||||
| { |  | ||||||
| 	color: #5ca8ff; |  | ||||||
| 	text-shadow: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| span.browser |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| </head> |  | ||||||
| <body> |  | ||||||
|  |  | ||||||
| <div class="frame"> |  | ||||||
| 	<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/> |  | ||||||
| 	<div class="panel"> |  | ||||||
| 		Sorry, the receiver is inactive due to internal error. |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								htdocs/include/header.include.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								htdocs/include/header.include.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | <div id="webrx-top-container"> | ||||||
|  |     <div id="webrx-top-photo-clip"> | ||||||
|  |         <img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/> | ||||||
|  |         <div id="webrx-top-bar" class="webrx-top-bar-parts"> | ||||||
|  |             <a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a> | ||||||
|  |             <a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a> | ||||||
|  |             <div id="webrx-rx-avatar-background"> | ||||||
|  |                 <img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png"/> | ||||||
|  |             </div> | ||||||
|  |             <div id="webrx-rx-texts"> | ||||||
|  |                 <div id="webrx-rx-title" class="openwebrx-photo-trigger"></div> | ||||||
|  |                 <div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div> | ||||||
|  |             </div> | ||||||
|  |             <div id="openwebrx-rx-details-arrow"> | ||||||
|  |                 <a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a> | ||||||
|  |                 <a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a> | ||||||
|  |             </div> | ||||||
|  |             <section id="openwebrx-main-buttons"> | ||||||
|  |                 <ul> | ||||||
|  |                     <li data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" /><br/>Status</li> | ||||||
|  |                     <li data-toggle-panel="openwebrx-panel-log"><img  src="static/gfx/openwebrx-panel-log.png" /><br/>Log</li> | ||||||
|  |                     <li data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li> | ||||||
|  |                     <li><a href="/map" target="_blank"><img src="static/gfx/openwebrx-panel-map.png" /><br/>Map</a></li> | ||||||
|  |                 </ul> | ||||||
|  |             </section> | ||||||
|  |         </div> | ||||||
|  |         <div id="webrx-rx-photo-title"></div> | ||||||
|  |         <div id="webrx-rx-photo-desc"></div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -25,43 +25,15 @@ | |||||||
|         <script src="static/sdr.js"></script> |         <script src="static/sdr.js"></script> | ||||||
| 		<script src="static/mathbox-bundle.min.js"></script> | 		<script src="static/mathbox-bundle.min.js"></script> | ||||||
|         <script src="static/openwebrx.js"></script> |         <script src="static/openwebrx.js"></script> | ||||||
|         <script src="static/jquery-3.2.1.min.js"></script> |         <script src="static/lib/jquery-3.2.1.min.js"></script> | ||||||
|         <script src="static/jquery.nanoscroller.js"></script> |         <script src="static/lib/jquery.nanoscroller.js"></script> | ||||||
|         <link rel="stylesheet" type="text/css" href="static/nanoscroller.css" /> |         <link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" /> | ||||||
|         <link rel="stylesheet" type="text/css" href="static/openwebrx.css" /> |         <link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" /> | ||||||
|         <meta charset="utf-8"> |         <meta charset="utf-8"> | ||||||
|     </head> |     </head> | ||||||
|     <body onload="openwebrx_init();"> |     <body onload="openwebrx_init();"> | ||||||
| <div id="webrx-page-container"> | <div id="webrx-page-container"> | ||||||
|     <div id="webrx-top-container"> |     ${header} | ||||||
|         <div id="webrx-top-photo-clip"> |  | ||||||
|             <img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/> |  | ||||||
|             <div id="webrx-top-bar" class="webrx-top-bar-parts"> |  | ||||||
|                 <a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a> |  | ||||||
|                 <a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a> |  | ||||||
|                 <div id="webrx-rx-avatar-background"> |  | ||||||
|                     <img id="webrx-rx-avatar" src="static/gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/> |  | ||||||
|                 </div> |  | ||||||
|                 <div id="webrx-rx-texts"> |  | ||||||
|                     <div id="webrx-rx-title" onclick="toggle_rx_photo();"></div> |  | ||||||
|                     <div id="webrx-rx-desc" onclick="toggle_rx_photo();"></div> |  | ||||||
|                 </div> |  | ||||||
|                 <div id="openwebrx-rx-details-arrow"> |  | ||||||
|                     <a id="openwebrx-rx-details-arrow-up" onclick="toggle_rx_photo();"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a> |  | ||||||
|                     <a id="openwebrx-rx-details-arrow-down" onclick="toggle_rx_photo();"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a> |  | ||||||
|                 </div> |  | ||||||
|                 <section id="openwebrx-main-buttons"> |  | ||||||
|                     <ul> |  | ||||||
|                         <li onmouseup="toggle_panel('openwebrx-panel-status');"><img src="static/gfx/openwebrx-panel-status.png" /><br/>Status</li> |  | ||||||
|                         <li onmouseup="toggle_panel('openwebrx-panel-log');"><img  src="static/gfx/openwebrx-panel-log.png" /><br/>Log</li> |  | ||||||
|                         <li onmouseup="toggle_panel('openwebrx-panel-receiver');"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li> |  | ||||||
|                     </ul> |  | ||||||
|                 </section> |  | ||||||
|             </div> |  | ||||||
|             <div id="webrx-rx-photo-title"></div> |  | ||||||
|             <div id="webrx-rx-photo-desc"></div> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div id="webrx-main-container"> |     <div id="webrx-main-container"> | ||||||
|             <div id="openwebrx-scale-container"> |             <div id="openwebrx-scale-container"> | ||||||
|                 <canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas> |                 <canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas> | ||||||
| @@ -111,7 +83,19 @@ | |||||||
|                         <select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();"> |                         <select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();"> | ||||||
|                             <option value="none"></option> |                             <option value="none"></option> | ||||||
|                             <option value="bpsk31">BPSK31</option> |                             <option value="bpsk31">BPSK31</option> | ||||||
|  |                             <option value="ft8" data-feature="wsjt-x">FT8</option> | ||||||
|  |                             <option value="wspr" data-feature="wsjt-x">WSPR</option> | ||||||
|  |                             <option value="jt65" data-feature="wsjt-x">JT65</option> | ||||||
|  |                             <option value="jt9" data-feature="wsjt-x">JT9</option> | ||||||
|  |                             <option value="ft4" data-feature="wsjt-x">FT4</option> | ||||||
|                         </select> |                         </select> | ||||||
|  |                         <div id="openwebrx-secondary-demod-dial-button" class="openwebrx-button openwebrx-dial-button" onclick="dial_button_click();"> | ||||||
|  |                             <svg version="1.1" id="Layer_1" x="0px" y="0px" width="246px" height="246px" viewBox="0 0 246 246" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |                                 <g id="ph_dial_1_" transform="matrix(1, 0, 0, 1, -45.398312, -50.931698)"> | ||||||
|  |                                     <path id="ph_dial" d="M238.875,190.125c3.853,7.148,34.267,4.219,50.242,2.145c0.891-5.977,1.508-12.043,1.508-18.27 c0-67.723-54.901-122.625-122.625-122.625c-67.723,0-122.625,54.902-122.625,122.625c0,67.723,54.902,122.625,122.625,122.625 c51.06,0,94.797-31.227,113.25-75.609c-13.969-9.668-41.625-18.891-41.625-18.891c-5.25,0-10.5-3-12.75-8.25 S233.625,180.375,238.875,190.125z M220.465,175.313c0,28.478-23.086,51.563-51.563,51.563c-28.478,0-51.563-23.086-51.563-51.563 c0-28.477,23.086-51.563,51.563-51.563C197.379,123.75,220.465,146.836,220.465,175.313z M185.25,64.125 c10.563,0,19.125,8.563,19.125,19.125s-8.563,19.125-19.125,19.125c-10.562,0-19.125-8.563-19.125-19.125 S174.688,64.125,185.25,64.125z M142.875,69C153.438,69,162,77.563,162,88.125s-8.563,19.125-19.125,19.125 c-10.562,0-19.125-8.563-19.125-19.125S132.313,69,142.875,69z M106.5,91.875c10.563,0,19.125,8.563,19.125,19.125 s-8.563,19.125-19.125,19.125c-10.562,0-19.125-8.562-19.125-19.125S95.938,91.875,106.5,91.875z M81.375,126.75 c10.563,0,19.125,8.563,19.125,19.125S91.938,165,81.375,165c-10.563,0-19.125-8.563-19.125-19.125S70.813,126.75,81.375,126.75z M58.125,188.625c0-10.559,8.563-19.125,19.125-19.125c10.563,0,19.125,8.566,19.125,19.125S87.813,207.75,77.25,207.75 C66.687,207.75,58.125,199.184,58.125,188.625z M75.75,229.875c0-10.559,8.563-19.125,19.125-19.125 c10.563,0,19.125,8.566,19.125,19.125S105.438,249,94.875,249C84.312,249,75.75,240.434,75.75,229.875z M126.375,276 c-10.563,0-19.125-8.566-19.125-19.125s8.563-19.125,19.125-19.125c10.563,0,19.125,8.566,19.125,19.125S136.938,276,126.375,276z M168,288c-10.563,0-19.125-8.566-19.125-19.125S157.438,249.75,168,249.75c10.563,0,19.125,8.566,19.125,19.125 S178.563,288,168,288z M210.375,276c-10.563,0-19.125-8.566-19.125-19.125s8.563-19.125,19.125-19.125 c10.563,0,19.125,8.566,19.125,19.125S220.938,276,210.375,276z M243.375,210.75c10.563,0,19.125,8.566,19.125,19.125 S253.938,249,243.375,249c-10.563,0-19.125-8.566-19.125-19.125S232.813,210.75,243.375,210.75z"/> | ||||||
|  |                                 </g> | ||||||
|  |                             </svg> | ||||||
|  |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                     <div class="openwebrx-panel-line"> |                     <div class="openwebrx-panel-line"> | ||||||
|                         <div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div> |                         <div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div> | ||||||
| @@ -160,7 +144,7 @@ | |||||||
|                     <span style="font-size: 15pt; font-weight: bold;">Under construction</span> |                     <span style="font-size: 15pt; font-weight: bold;">Under construction</span> | ||||||
|                     <br />We're working on the code right now, so the application might fail. |                     <br />We're working on the code right now, so the application might fail. | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="openwebrx-panel" id="openwebrx-panel-digimodes" data-panel-name="digimodes" data-panel-pos="left" data-panel-order="2" data-panel-size="619,210"> |                 <div class="openwebrx-panel" id="openwebrx-panel-digimodes" data-panel-name="digimodes" data-panel-pos="left" data-panel-order="3" data-panel-size="619,210"> | ||||||
|                     <div id="openwebrx-digimode-canvas-container"> |                     <div id="openwebrx-digimode-canvas-container"> | ||||||
|                         <div id="openwebrx-digimode-select-channel"></div> |                         <div id="openwebrx-digimode-select-channel"></div> | ||||||
|                     </div> |                     </div> | ||||||
| @@ -171,6 +155,16 @@ | |||||||
|                         </div> |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <table class="openwebrx-panel" id="openwebrx-panel-wsjt-message" data-panel-name="wsjt-message" data-panel-pos="left" data-panel-order="2" data-panel-size="619,200"> | ||||||
|  |                     <thead><tr> | ||||||
|  |                         <th>UTC</th> | ||||||
|  |                         <th class="decimal">dB</th> | ||||||
|  |                         <th class="decimal">DT</th> | ||||||
|  |                         <th class="decimal freq">Freq</th> | ||||||
|  |                         <th class="message">Message</th> | ||||||
|  |                     </tr></thead> | ||||||
|  |                     <tbody></tbody> | ||||||
|  |                 </table> | ||||||
|                 <div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" data-panel-name="metadata-ysf" data-panel-pos="left" data-panel-order="2" data-panel-size="145,220"> |                 <div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" data-panel-name="metadata-ysf" data-panel-pos="left" data-panel-order="2" data-panel-size="145,220"> | ||||||
|                     <div class="openwebrx-meta-frame"> |                     <div class="openwebrx-meta-frame"> | ||||||
|                         <div class="openwebrx-meta-slot"> |                         <div class="openwebrx-meta-slot"> | ||||||
|   | |||||||
							
								
								
									
										58
									
								
								htdocs/lib/chroma.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								htdocs/lib/chroma.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										143
									
								
								htdocs/lib/nite-overlay.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								htdocs/lib/nite-overlay.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | |||||||
|  | /* Nite v1.7 | ||||||
|  |  * A tiny library to create a night overlay over the map | ||||||
|  |  * Author: Rossen Georgiev @ https://github.com/rossengeorgiev | ||||||
|  |  * Requires: GMaps API 3 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | var nite = { | ||||||
|  |     map: null, | ||||||
|  |     date: null, | ||||||
|  |     sun_position: null, | ||||||
|  |     earth_radius_meters: 6371008, | ||||||
|  |     marker_twilight_civil: null, | ||||||
|  |     marker_twilight_nautical: null, | ||||||
|  |     marker_twilight_astronomical: null, | ||||||
|  |     marker_night: null, | ||||||
|  |  | ||||||
|  |     init: function(map) { | ||||||
|  |         if(typeof google === 'undefined' | ||||||
|  |            || typeof google.maps === 'undefined') throw "Nite Overlay: no google.maps detected"; | ||||||
|  |  | ||||||
|  |         this.map = map; | ||||||
|  |         this.sun_position = this.calculatePositionOfSun(); | ||||||
|  |  | ||||||
|  |         this.marker_twilight_civil = new google.maps.Circle({ | ||||||
|  |             map: this.map, | ||||||
|  |             center: this.getShadowPosition(), | ||||||
|  |             radius: this.getShadowRadiusFromAngle(0.566666), | ||||||
|  |             fillColor: "#000", | ||||||
|  |             fillOpacity: 0.1, | ||||||
|  |             strokeOpacity: 0, | ||||||
|  |             clickable: false, | ||||||
|  |             editable: false | ||||||
|  |         }); | ||||||
|  |         this.marker_twilight_nautical = new google.maps.Circle({ | ||||||
|  |             map: this.map, | ||||||
|  |             center: this.getShadowPosition(), | ||||||
|  |             radius: this.getShadowRadiusFromAngle(6), | ||||||
|  |             fillColor: "#000", | ||||||
|  |             fillOpacity: 0.1, | ||||||
|  |             strokeOpacity: 0, | ||||||
|  |             clickable: false, | ||||||
|  |             editable: false | ||||||
|  |         }); | ||||||
|  |         this.marker_twilight_astronomical = new google.maps.Circle({ | ||||||
|  |             map: this.map, | ||||||
|  |             center: this.getShadowPosition(), | ||||||
|  |             radius: this.getShadowRadiusFromAngle(12), | ||||||
|  |             fillColor: "#000", | ||||||
|  |             fillOpacity: 0.1, | ||||||
|  |             strokeOpacity: 0, | ||||||
|  |             clickable: false, | ||||||
|  |             editable: false | ||||||
|  |         }); | ||||||
|  |         this.marker_night = new google.maps.Circle({ | ||||||
|  |             map: this.map, | ||||||
|  |             center: this.getShadowPosition(), | ||||||
|  |             radius: this.getShadowRadiusFromAngle(18), | ||||||
|  |             fillColor: "#000", | ||||||
|  |             fillOpacity: 0.1, | ||||||
|  |             strokeOpacity: 0, | ||||||
|  |             clickable: false, | ||||||
|  |             editable: false | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     getShadowRadiusFromAngle: function(angle) { | ||||||
|  |         var shadow_radius =  this.earth_radius_meters * Math.PI * 0.5; | ||||||
|  |         var twilight_dist = ((this.earth_radius_meters * 2 * Math.PI) / 360) * angle; | ||||||
|  |         return shadow_radius - twilight_dist; | ||||||
|  |     }, | ||||||
|  |     getSunPosition: function() { | ||||||
|  |         return this.sun_position; | ||||||
|  |     }, | ||||||
|  |     getShadowPosition: function() { | ||||||
|  |         return (this.sun_position) ? new google.maps.LatLng(-this.sun_position.lat(), this.sun_position.lng() + 180) : null; | ||||||
|  |     }, | ||||||
|  |     refresh: function() { | ||||||
|  |         if(!this.isVisible()) return; | ||||||
|  |         this.sun_position = this.calculatePositionOfSun(this.date); | ||||||
|  |         var shadow_position = this.getShadowPosition(); | ||||||
|  |         this.marker_twilight_civil.setCenter(shadow_position); | ||||||
|  |         this.marker_twilight_nautical.setCenter(shadow_position); | ||||||
|  |         this.marker_twilight_astronomical.setCenter(shadow_position); | ||||||
|  |         this.marker_night.setCenter(shadow_position); | ||||||
|  |     }, | ||||||
|  |     jday: function(date) { | ||||||
|  |         return (date.getTime() / 86400000.0) + 2440587.5; | ||||||
|  |     }, | ||||||
|  |     calculatePositionOfSun: function(date) { | ||||||
|  |         date = (date instanceof Date) ? date : new Date(); | ||||||
|  |  | ||||||
|  |         var rad = 0.017453292519943295; | ||||||
|  |  | ||||||
|  |         // based on NOAA solar calculations | ||||||
|  |         var ms_past_midnight = ((date.getUTCHours() * 60 + date.getUTCMinutes()) * 60 + date.getUTCSeconds()) * 1000 + date.getUTCMilliseconds(); | ||||||
|  |         var jc = (this.jday(date) - 2451545)/36525; | ||||||
|  |         var mean_long_sun = (280.46646+jc*(36000.76983+jc*0.0003032)) % 360; | ||||||
|  |         var mean_anom_sun = 357.52911+jc*(35999.05029-0.0001537*jc); | ||||||
|  |         var sun_eq = Math.sin(rad*mean_anom_sun)*(1.914602-jc*(0.004817+0.000014*jc))+Math.sin(rad*2*mean_anom_sun)*(0.019993-0.000101*jc)+Math.sin(rad*3*mean_anom_sun)*0.000289; | ||||||
|  |         var sun_true_long = mean_long_sun + sun_eq; | ||||||
|  |         var sun_app_long = sun_true_long - 0.00569 - 0.00478*Math.sin(rad*125.04-1934.136*jc); | ||||||
|  |         var mean_obliq_ecliptic = 23+(26+((21.448-jc*(46.815+jc*(0.00059-jc*0.001813))))/60)/60; | ||||||
|  |         var obliq_corr = mean_obliq_ecliptic + 0.00256*Math.cos(rad*125.04-1934.136*jc); | ||||||
|  |  | ||||||
|  |         var lat = Math.asin(Math.sin(rad*obliq_corr)*Math.sin(rad*sun_app_long)) / rad; | ||||||
|  |  | ||||||
|  |         var eccent = 0.016708634-jc*(0.000042037+0.0000001267*jc); | ||||||
|  |         var y = Math.tan(rad*(obliq_corr/2))*Math.tan(rad*(obliq_corr/2)); | ||||||
|  |         var rq_of_time = 4*((y*Math.sin(2*rad*mean_long_sun)-2*eccent*Math.sin(rad*mean_anom_sun)+4*eccent*y*Math.sin(rad*mean_anom_sun)*Math.cos(2*rad*mean_long_sun)-0.5*y*y*Math.sin(4*rad*mean_long_sun)-1.25*eccent*eccent*Math.sin(2*rad*mean_anom_sun))/rad); | ||||||
|  |         var true_solar_time_in_deg = ((ms_past_midnight+rq_of_time*60000) % 86400000) / 240000; | ||||||
|  |  | ||||||
|  |         var lng = -((true_solar_time_in_deg < 0) ? true_solar_time_in_deg + 180 : true_solar_time_in_deg - 180); | ||||||
|  |  | ||||||
|  |         return new google.maps.LatLng(lat, lng); | ||||||
|  |     }, | ||||||
|  |     setDate: function(date) { | ||||||
|  |         this.date = date; | ||||||
|  |         this.refresh(); | ||||||
|  |     }, | ||||||
|  |     setMap: function(map) { | ||||||
|  |         this.map = map; | ||||||
|  |         this.marker_twilight_civil.setMap(this.map); | ||||||
|  |         this.marker_twilight_nautical.setMap(this.map); | ||||||
|  |         this.marker_twilight_astronomical.setMap(this.map); | ||||||
|  |         this.marker_night.setMap(this.map); | ||||||
|  |     }, | ||||||
|  |     show: function() { | ||||||
|  |         this.marker_twilight_civil.setVisible(true); | ||||||
|  |         this.marker_twilight_nautical.setVisible(true); | ||||||
|  |         this.marker_twilight_astronomical.setVisible(true); | ||||||
|  |         this.marker_night.setVisible(true); | ||||||
|  |         this.refresh(); | ||||||
|  |     }, | ||||||
|  |     hide: function() { | ||||||
|  |         this.marker_twilight_civil.setVisible(false); | ||||||
|  |         this.marker_twilight_nautical.setVisible(false); | ||||||
|  |         this.marker_twilight_astronomical.setVisible(false); | ||||||
|  |         this.marker_night.setVisible(false); | ||||||
|  |     }, | ||||||
|  |     isVisible: function() { | ||||||
|  |         return this.marker_night.getVisible(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								htdocs/map.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								htdocs/map.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | <!DOCTYPE HTML> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |     <title>OpenWebRX Map</title> | ||||||
|  |     <script src="static/lib/jquery-3.2.1.min.js"></script> | ||||||
|  |     <script src="static/lib/chroma.min.js"></script> | ||||||
|  |     <script src="static/map.js"></script> | ||||||
|  |     <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> | ||||||
|  |     <link rel="stylesheet" type="text/css" href="static/css/map.css" /> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     ${header} | ||||||
|  |     <div class="openwebrx-map"></div> | ||||||
|  |     <div class="openwebrx-map-legend"> | ||||||
|  |         <h3>Colors</h3> | ||||||
|  |         <select id="openwebrx-map-colormode"> | ||||||
|  |             <option value="byband" selected="selected">By Band</option> | ||||||
|  |             <option value="bymode">By Mode</option> | ||||||
|  |         </select> | ||||||
|  |         <div class="content"></div> | ||||||
|  |     </div> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										340
									
								
								htdocs/map.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								htdocs/map.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,340 @@ | |||||||
|  | (function(){ | ||||||
|  |     var protocol = 'ws'; | ||||||
|  |     if (window.location.toString().startsWith('https://')) { | ||||||
|  |         protocol = 'wss'; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){ | ||||||
|  |         var s = v.split('='); | ||||||
|  |         var r = {}; | ||||||
|  |         r[s[0]] = s.slice(1).join('='); | ||||||
|  |         return r; | ||||||
|  |     }).reduce(function(a, b){ | ||||||
|  |         return a.assign(b); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     var expectedCallsign; | ||||||
|  |     if (query.callsign) expectedCallsign = query.callsign; | ||||||
|  |     var expectedLocator; | ||||||
|  |     if (query.locator) expectedLocator = query.locator; | ||||||
|  |  | ||||||
|  |     var ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/"; | ||||||
|  |     if (!("WebSocket" in window)) return; | ||||||
|  |  | ||||||
|  |     var map; | ||||||
|  |     var markers = {}; | ||||||
|  |     var rectangles = {}; | ||||||
|  |     var updateQueue = []; | ||||||
|  |  | ||||||
|  |     // reasonable default; will be overriden by server | ||||||
|  |     var retention_time = 2 * 60 * 60 * 1000; | ||||||
|  |     var strokeOpacity = 0.8; | ||||||
|  |     var fillOpacity = 0.35; | ||||||
|  |  | ||||||
|  |     var colorKeys = {}; | ||||||
|  |     var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl'); | ||||||
|  |     var getColor = function(id){ | ||||||
|  |         if (!id) return "#000000"; | ||||||
|  |         if (!colorKeys[id]) { | ||||||
|  |             var keys = Object.keys(colorKeys); | ||||||
|  |             keys.push(id); | ||||||
|  |             keys.sort(); | ||||||
|  |             var colors = colorScale.colors(keys.length); | ||||||
|  |             colorKeys = {}; | ||||||
|  |             keys.forEach(function(key, index) { | ||||||
|  |                 colorKeys[key] = colors[index]; | ||||||
|  |             }); | ||||||
|  |             reColor(); | ||||||
|  |             updateLegend(); | ||||||
|  |         } | ||||||
|  |         return colorKeys[id]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // when the color palette changes, update all grid squares with new color | ||||||
|  |     var reColor = function() { | ||||||
|  |         $.each(rectangles, function(_, r) { | ||||||
|  |             var color = getColor(colorAccessor(r)); | ||||||
|  |             r.setOptions({ | ||||||
|  |                 strokeColor: color, | ||||||
|  |                 fillColor: color | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var colorMode = 'byband'; | ||||||
|  |     var colorAccessor = function(r) { | ||||||
|  |         switch (colorMode) { | ||||||
|  |             case 'byband': | ||||||
|  |                 return r.band; | ||||||
|  |             case 'bymode': | ||||||
|  |                 return r.mode; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     $(function(){ | ||||||
|  |         $('#openwebrx-map-colormode').on('change', function(){ | ||||||
|  |             colorMode = $(this).val(); | ||||||
|  |             colorKeys = {}; | ||||||
|  |             reColor(); | ||||||
|  |             updateLegend(); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     var updateLegend = function() { | ||||||
|  |         var lis = $.map(colorKeys, function(value, key) { | ||||||
|  |             return '<li class="square"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>'; | ||||||
|  |         }); | ||||||
|  |         $(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var processUpdates = function(updates) { | ||||||
|  |         if (!map) { | ||||||
|  |             updateQueue = updateQueue.concat(updates); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         updates.forEach(function(update){ | ||||||
|  |  | ||||||
|  |             switch (update.location.type) { | ||||||
|  |                 case 'latlon': | ||||||
|  |                     var pos = new google.maps.LatLng(update.location.lat, update.location.lon); | ||||||
|  |                     var marker; | ||||||
|  |                     if (markers[update.callsign]) { | ||||||
|  |                         marker = markers[update.callsign]; | ||||||
|  |                     } else { | ||||||
|  |                         marker = new google.maps.Marker(); | ||||||
|  |                         marker.addListener('click', function(){ | ||||||
|  |                             showMarkerInfoWindow(update.callsign, pos); | ||||||
|  |                         }); | ||||||
|  |                         markers[update.callsign] = marker; | ||||||
|  |                     } | ||||||
|  |                     marker.setOptions($.extend({ | ||||||
|  |                         position: pos, | ||||||
|  |                         map: map, | ||||||
|  |                         title: update.callsign | ||||||
|  |                     }, getMarkerOpacityOptions(update.lastseen) )); | ||||||
|  |                     marker.lastseen = update.lastseen; | ||||||
|  |                     marker.mode = update.mode; | ||||||
|  |                     marker.band = update.band; | ||||||
|  |  | ||||||
|  |                     // TODO the trim should happen on the server side | ||||||
|  |                     if (expectedCallsign && expectedCallsign == update.callsign.trim()) { | ||||||
|  |                         map.panTo(pos); | ||||||
|  |                         showMarkerInfoWindow(update.callsign, pos); | ||||||
|  |                         delete(expectedCallsign); | ||||||
|  |                     } | ||||||
|  |                 break; | ||||||
|  |                 case 'locator': | ||||||
|  |                     var loc = update.location.locator; | ||||||
|  |                     var lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]); | ||||||
|  |                     var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2; | ||||||
|  |                     var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1}); | ||||||
|  |                     var rectangle; | ||||||
|  |                     // the accessor is designed to work on the rectangle... but it should work on the update object, too | ||||||
|  |                     var color = getColor(colorAccessor(update)); | ||||||
|  |                     if (rectangles[update.callsign]) { | ||||||
|  |                         rectangle = rectangles[update.callsign]; | ||||||
|  |                     } else { | ||||||
|  |                         rectangle = new google.maps.Rectangle(); | ||||||
|  |                         rectangle.addListener('click', function(){ | ||||||
|  |                             showLocatorInfoWindow(this.locator, this.center); | ||||||
|  |                         }); | ||||||
|  |                         rectangles[update.callsign] = rectangle; | ||||||
|  |                     } | ||||||
|  |                     rectangle.setOptions($.extend({ | ||||||
|  |                         strokeColor: color, | ||||||
|  |                         strokeWeight: 2, | ||||||
|  |                         fillColor: color, | ||||||
|  |                         map: map, | ||||||
|  |                         bounds:{ | ||||||
|  |                             north: lat, | ||||||
|  |                             south: lat + 1, | ||||||
|  |                             west: lon, | ||||||
|  |                             east: lon + 2 | ||||||
|  |                         } | ||||||
|  |                     }, getRectangleOpacityOptions(update.lastseen) )); | ||||||
|  |                     rectangle.lastseen = update.lastseen; | ||||||
|  |                     rectangle.locator = update.location.locator; | ||||||
|  |                     rectangle.mode = update.mode; | ||||||
|  |                     rectangle.band = update.band; | ||||||
|  |                     rectangle.center = center; | ||||||
|  |  | ||||||
|  |                     if (expectedLocator && expectedLocator == update.location.locator) { | ||||||
|  |                         map.panTo(center); | ||||||
|  |                         showLocatorInfoWindow(expectedLocator, center); | ||||||
|  |                         delete(expectedLocator); | ||||||
|  |                     } | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     var clearMap = function(){ | ||||||
|  |         var reset = function(callsign, item) { item.setMap(); }; | ||||||
|  |         $.each(markers, reset); | ||||||
|  |         $.each(rectangles, reset); | ||||||
|  |         markers = {}; | ||||||
|  |         rectangles = {}; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     var reconnect_timeout = false; | ||||||
|  |  | ||||||
|  |     var connect = function(){ | ||||||
|  |         var ws = new WebSocket(ws_url); | ||||||
|  |         ws.onopen = function(){ | ||||||
|  |             ws.send("SERVER DE CLIENT client=map.js type=map"); | ||||||
|  |             reconnect_timeout = false | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         ws.onmessage = function(e){ | ||||||
|  |             if (typeof e.data != 'string') { | ||||||
|  |                 console.error("unsupported binary data on websocket; ignoring"); | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             if (e.data.substr(0, 16) == "CLIENT DE SERVER") { | ||||||
|  |                 console.log("Server acknowledged WebSocket connection."); | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             try { | ||||||
|  |                 var json = JSON.parse(e.data); | ||||||
|  |                 switch (json.type) { | ||||||
|  |                     case "config": | ||||||
|  |                         var config = json.value; | ||||||
|  |                         if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ | ||||||
|  |                             map = new google.maps.Map($('.openwebrx-map')[0], { | ||||||
|  |                                 center: { | ||||||
|  |                                     lat: config.receiver_gps[0], | ||||||
|  |                                     lng: config.receiver_gps[1] | ||||||
|  |                                 }, | ||||||
|  |                                 zoom: 5 | ||||||
|  |                             }); | ||||||
|  |                             processUpdates(updateQueue); | ||||||
|  |                             updateQueue = []; | ||||||
|  |                             $.getScript("/static/lib/nite-overlay.js").done(function(){ | ||||||
|  |                                 nite.init(map); | ||||||
|  |                                 setInterval(function() { nite.refresh() }, 10000); // every 10s | ||||||
|  |                             }); | ||||||
|  |                             map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]); | ||||||
|  |                         }); | ||||||
|  |                         retention_time = config.map_position_retention_time * 1000; | ||||||
|  |                     break; | ||||||
|  |                     case "update": | ||||||
|  |                         processUpdates(json.value); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } catch (e) { | ||||||
|  |                 // don't lose exception | ||||||
|  |                 console.error(e); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |         ws.onclose = function(){ | ||||||
|  |             clearMap(); | ||||||
|  |             if (reconnect_timeout) { | ||||||
|  |                 // max value: roundabout 8 and a half minutes | ||||||
|  |                 reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); | ||||||
|  |             } else { | ||||||
|  |                 // initial value: 1s | ||||||
|  |                 reconnect_timeout = 1000; | ||||||
|  |             } | ||||||
|  |             setTimeout(connect, reconnect_timeout); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript | ||||||
|  |             ws.onclose = function () {}; | ||||||
|  |             ws.close(); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         /* | ||||||
|  |         ws.onerror = function(){ | ||||||
|  |             console.info("websocket error"); | ||||||
|  |         }; | ||||||
|  |         */ | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     connect(); | ||||||
|  |  | ||||||
|  |     var infowindow; | ||||||
|  |     var showLocatorInfoWindow = function(locator, pos) { | ||||||
|  |         if (!infowindow) infowindow = new google.maps.InfoWindow(); | ||||||
|  |         var inLocator = $.map(rectangles, function(r, callsign) { | ||||||
|  |             return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band} | ||||||
|  |         }).filter(function(d) { | ||||||
|  |             return d.locator == locator; | ||||||
|  |         }).sort(function(a, b){ | ||||||
|  |             return b.lastseen - a.lastseen; | ||||||
|  |         }); | ||||||
|  |         infowindow.setContent( | ||||||
|  |             '<h3>Locator: ' + locator + '</h3>' + | ||||||
|  |             '<div>Active Callsigns:</div>' + | ||||||
|  |             '<ul>' + | ||||||
|  |                 inLocator.map(function(i){ | ||||||
|  |                     var timestring = moment(i.lastseen).fromNow(); | ||||||
|  |                     var message = i.callsign + ' (' + timestring + ' using ' + i.mode; | ||||||
|  |                     if (i.band) message += ' on ' + i.band; | ||||||
|  |                     message += ')'; | ||||||
|  |                     return '<li>' + message + '</li>' | ||||||
|  |                 }).join("") + | ||||||
|  |             '</ul>' | ||||||
|  |         ); | ||||||
|  |         infowindow.setPosition(pos); | ||||||
|  |         infowindow.open(map); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     var showMarkerInfoWindow = function(callsign, pos) { | ||||||
|  |         if (!infowindow) infowindow = new google.maps.InfoWindow(); | ||||||
|  |         var marker = markers[callsign]; | ||||||
|  |         var timestring = moment(marker.lastseen).fromNow(); | ||||||
|  |         infowindow.setContent( | ||||||
|  |             '<h3>' + callsign + '</h3>' + | ||||||
|  |             '<div>' + timestring + ' using ' + marker.mode + '</div>' | ||||||
|  |         ); | ||||||
|  |         infowindow.open(map, marker); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var getScale = function(lastseen) { | ||||||
|  |         var age = new Date().getTime() - lastseen; | ||||||
|  |         var scale = 1; | ||||||
|  |         if (age >= retention_time / 2) { | ||||||
|  |             scale = (retention_time - age) / (retention_time / 2); | ||||||
|  |         } | ||||||
|  |         return Math.max(0, Math.min(1, scale)); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     var getRectangleOpacityOptions = function(lastseen) { | ||||||
|  |         var scale = getScale(lastseen); | ||||||
|  |         return { | ||||||
|  |             strokeOpacity: strokeOpacity * scale, | ||||||
|  |             fillOpacity: fillOpacity * scale | ||||||
|  |         }; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     var getMarkerOpacityOptions = function(lastseen) { | ||||||
|  |         var scale = getScale(lastseen); | ||||||
|  |         return { | ||||||
|  |             opacity: scale | ||||||
|  |         }; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // fade out / remove positions after time | ||||||
|  |     setInterval(function(){ | ||||||
|  |         var now = new Date().getTime(); | ||||||
|  |         $.each(rectangles, function(callsign, m) { | ||||||
|  |             var age = now - m.lastseen; | ||||||
|  |             if (age > retention_time) { | ||||||
|  |                 delete rectangles[callsign]; | ||||||
|  |                 m.setMap(); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             m.setOptions(getRectangleOpacityOptions(m.lastseen)); | ||||||
|  |         }); | ||||||
|  |         $.each(markers, function(callsign, m) { | ||||||
|  |             var age = now - m.lastseen; | ||||||
|  |             if (age > retention_time) { | ||||||
|  |                 delete markers[callsign]; | ||||||
|  |                 m.setMap(); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             m.setOptions(getMarkerOpacityOptions(m.lastseen)); | ||||||
|  |         }); | ||||||
|  |     }, 1000); | ||||||
|  |  | ||||||
|  | })(); | ||||||
| @@ -86,6 +86,7 @@ function init_rx_photo() | |||||||
| 	window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000); | 	window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000); | ||||||
| 	window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500); | 	window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500); | ||||||
| 	window.setTimeout(function() { close_rx_photo() },2500); | 	window.setTimeout(function() { close_rx_photo() },2500); | ||||||
|  | 	$('#webrx-top-container .openwebrx-photo-trigger').click(toggle_rx_photo); | ||||||
| } | } | ||||||
|  |  | ||||||
| dont_toggle_rx_photo_flag=0; | dont_toggle_rx_photo_flag=0; | ||||||
| @@ -1250,6 +1251,13 @@ function on_ws_recv(evt) | |||||||
| 					case "metadata": | 					case "metadata": | ||||||
| 						update_metadata(json.value); | 						update_metadata(json.value); | ||||||
| 					break; | 					break; | ||||||
|  | 					case "wsjt_message": | ||||||
|  | 					    update_wsjt_panel(json.value); | ||||||
|  | 					    break; | ||||||
|  | 					case "dial_frequencies": | ||||||
|  | 					    dial_frequencies = json.value; | ||||||
|  | 					    update_dial_button(); | ||||||
|  | 					    break; | ||||||
|                     default: |                     default: | ||||||
|                         console.warn('received message of unknown type: ' + json.type); |                         console.warn('received message of unknown type: ' + json.type); | ||||||
| 		        } | 		        } | ||||||
| @@ -1315,6 +1323,29 @@ function on_ws_recv(evt) | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var dial_frequencies = []; | ||||||
|  |  | ||||||
|  | function find_dial_frequencies() { | ||||||
|  |     var sdm = $("#openwebrx-secondary-demod-listbox")[0].value; | ||||||
|  |     return dial_frequencies.filter(function(d){ | ||||||
|  |         return d.mode == sdm; | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function update_dial_button() { | ||||||
|  |     var available = find_dial_frequencies(); | ||||||
|  |     $("#openwebrx-secondary-demod-dial-button")[available.length ? "addClass" : "removeClass"]("available"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function dial_button_click() { | ||||||
|  |     var available = find_dial_frequencies(); | ||||||
|  |     if (!available.length) return; | ||||||
|  |     var frequency = available[0].frequency; | ||||||
|  |     console.info(frequency); | ||||||
|  |     demodulator_set_offset_frequency(0, frequency - center_freq); | ||||||
|  |     $("#webrx-actual-freq").html(format_frequency("{x} MHz", frequency, 1e6, 4)); | ||||||
|  | } | ||||||
|  |  | ||||||
| function update_metadata(meta) { | function update_metadata(meta) { | ||||||
|     if (meta.protocol) switch (meta.protocol) { |     if (meta.protocol) switch (meta.protocol) { | ||||||
|         case 'DMR': |         case 'DMR': | ||||||
| @@ -1356,8 +1387,8 @@ function update_metadata(meta) { | |||||||
|             if (meta.mode && meta.mode != "") { |             if (meta.mode && meta.mode != "") { | ||||||
|                 mode = "Mode: " + meta.mode; |                 mode = "Mode: " + meta.mode; | ||||||
|                 source = meta.source || ""; |                 source = meta.source || ""; | ||||||
|                 if (meta.lat && meta.lon) { |                 if (meta.lat && meta.lon && meta.source) { | ||||||
|                     source = "<a class=\"openwebrx-maps-pin\" href=\"https://www.google.com/maps/search/?api=1&query=" + meta.lat + "," + meta.lon + "\" target=\"_blank\"></a>" + source; |                     source = "<a class=\"openwebrx-maps-pin\" href=\"/map?callsign=" + meta.source + "\" target=\"_blank\"></a>" + source; | ||||||
|                 } |                 } | ||||||
|                 up = meta.up ? "Up: " + meta.up : ""; |                 up = meta.up ? "Up: " + meta.up : ""; | ||||||
|                 down = meta.down ? "Down: " + meta.down : ""; |                 down = meta.down ? "Down: " + meta.down : ""; | ||||||
| @@ -1377,6 +1408,56 @@ function update_metadata(meta) { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function html_escape(input) { | ||||||
|  |     return $('<div/>').text(input).html() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function update_wsjt_panel(msg) { | ||||||
|  |     var $b = $('#openwebrx-panel-wsjt-message tbody'); | ||||||
|  |     var t = new Date(msg['timestamp']); | ||||||
|  |     var pad = function(i) { return ('' + i).padStart(2, "0"); } | ||||||
|  |     var linkedmsg = msg['msg']; | ||||||
|  |     if (['FT8', 'JT65', 'JT9', 'FT4'].indexOf(msg['mode']) >= 0) { | ||||||
|  |         var matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); | ||||||
|  |         if (matches && matches[2] != 'RR73') { | ||||||
|  |             linkedmsg = html_escape(matches[1]) + '<a href="/map?locator=' + matches[2] + '" target="_blank">' + matches[2] + '</a>'; | ||||||
|  |         } else { | ||||||
|  |             linkedmsg = html_escape(linkedmsg); | ||||||
|  |         } | ||||||
|  |     } else if (msg['mode'] == 'WSPR') { | ||||||
|  |         var matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); | ||||||
|  |         if (matches) { | ||||||
|  |             linkedmsg = html_escape(matches[1]) + '<a href="/map?locator=' + matches[2] + '" target="_blank">' + matches[2] + '</a>' + html_escape(matches[3]); | ||||||
|  |         } else { | ||||||
|  |             linkedmsg = html_escape(linkedmsg); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     $b.append($( | ||||||
|  |         '<tr data-timestamp="' + msg['timestamp'] + '">' + | ||||||
|  |             '<td>' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '</td>' + | ||||||
|  |             '<td class="decimal">' + msg['db'] + '</td>' + | ||||||
|  |             '<td class="decimal">' + msg['dt'] + '</td>' + | ||||||
|  |             '<td class="decimal freq">' + msg['freq'] + '</td>' + | ||||||
|  |             '<td class="message">' + linkedmsg + '</td>' + | ||||||
|  |         '</tr>' | ||||||
|  |     )); | ||||||
|  |     $b.scrollTop($b[0].scrollHeight); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var wsjt_removal_interval; | ||||||
|  |  | ||||||
|  | // remove old wsjt messages in fixed intervals | ||||||
|  | function init_wsjt_removal_timer() { | ||||||
|  |     if (wsjt_removal_interval) clearInterval(wsjt_removal_interval); | ||||||
|  |     wsjt_removal_interval = setInterval(function(){ | ||||||
|  |         var $elements = $('#openwebrx-panel-wsjt-message tbody tr'); | ||||||
|  |         // limit to 1000 entries in the list since browsers get laggy at some point | ||||||
|  |         var toRemove = $elements.length - 1000; | ||||||
|  |         if (toRemove <= 0) return; | ||||||
|  |         $elements.slice(0, toRemove).remove(); | ||||||
|  |     }, 15000); | ||||||
|  | } | ||||||
|  |  | ||||||
| function hide_digitalvoice_panels() { | function hide_digitalvoice_panels() { | ||||||
|     $(".openwebrx-meta-panel").each(function(_, p){ |     $(".openwebrx-meta-panel").each(function(_, p){ | ||||||
|         toggle_panel(p.id, false); |         toggle_panel(p.id, false); | ||||||
| @@ -1436,8 +1517,9 @@ function waterfall_dequeue() | |||||||
|  |  | ||||||
| function on_ws_opened() | function on_ws_opened() | ||||||
| { | { | ||||||
| 	ws.send("SERVER DE CLIENT openwebrx.js"); | 	ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); | ||||||
| 	divlog("WebSocket opened to "+ws_url); | 	divlog("WebSocket opened to "+ws_url); | ||||||
|  | 	reconnect_timeout = false; | ||||||
| } | } | ||||||
|  |  | ||||||
| var was_error=0; | var was_error=0; | ||||||
| @@ -1818,6 +1900,8 @@ function audio_init() | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | var reconnect_timeout = false; | ||||||
|  |  | ||||||
| function on_ws_closed() | function on_ws_closed() | ||||||
| { | { | ||||||
| 	try | 	try | ||||||
| @@ -1826,9 +1910,16 @@ function on_ws_closed() | |||||||
| 	} | 	} | ||||||
| 	catch (dont_care) {} | 	catch (dont_care) {} | ||||||
| 	audio_initialized = 0; | 	audio_initialized = 0; | ||||||
| 	divlog("WebSocket has closed unexpectedly. Attempting to reconnect in 5 seconds...", 1); | 	if (reconnect_timeout) { | ||||||
|  | 	    // max value: roundabout 8 and a half minutes | ||||||
|  |     	reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); | ||||||
|  | 	} else { | ||||||
|  | 	    // initial value: 1s | ||||||
|  | 	    reconnect_timeout = 1000; | ||||||
|  | 	} | ||||||
|  | 	divlog("WebSocket has closed unexpectedly. Attempting to reconnect in " + reconnect_timeout / 1000 + " seconds...", 1); | ||||||
|  |  | ||||||
| 	setTimeout(open_websocket, 5000); | 	setTimeout(open_websocket, reconnect_timeout); | ||||||
| } | } | ||||||
|  |  | ||||||
| function on_ws_error(event) | function on_ws_error(event) | ||||||
| @@ -2332,6 +2423,13 @@ function openwebrx_resize() | |||||||
| 	check_top_bar_congestion(); | 	check_top_bar_congestion(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function init_header() | ||||||
|  | { | ||||||
|  |     $('#openwebrx-main-buttons li[data-toggle-panel]').click(function() { | ||||||
|  |         toggle_panel($(this).data('toggle-panel')); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
| function openwebrx_init() | function openwebrx_init() | ||||||
| { | { | ||||||
| 	if(ios||is_chrome) e("openwebrx-big-grey").style.display="table-cell"; | 	if(ios||is_chrome) e("openwebrx-big-grey").style.display="table-cell"; | ||||||
| @@ -2344,6 +2442,7 @@ function openwebrx_init() | |||||||
| 	window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); | 	window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); | ||||||
| 	window.addEventListener("resize",openwebrx_resize); | 	window.addEventListener("resize",openwebrx_resize); | ||||||
| 	check_top_bar_congestion(); | 	check_top_bar_congestion(); | ||||||
|  | 	init_header(); | ||||||
|  |  | ||||||
| 	//Synchronise volume with slider | 	//Synchronise volume with slider | ||||||
| 	updateVolume(); | 	updateVolume(); | ||||||
| @@ -2351,7 +2450,9 @@ function openwebrx_init() | |||||||
| } | } | ||||||
|  |  | ||||||
| function digimodes_init() { | function digimodes_init() { | ||||||
|     hide_digitalvoice_panels(); |     $(".openwebrx-meta-panel").each(function(_, p){ | ||||||
|  |         p.openwebrxHidden = true; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     // initialze DMR timeslot muting |     // initialze DMR timeslot muting | ||||||
|     $('.openwebrx-dmr-timeslot-panel').click(function(e) { |     $('.openwebrx-dmr-timeslot-panel').click(function(e) { | ||||||
| @@ -2638,12 +2739,19 @@ function demodulator_digital_replace(subtype) | |||||||
|     { |     { | ||||||
|     case "bpsk31": |     case "bpsk31": | ||||||
|     case "rtty": |     case "rtty": | ||||||
|  |     case "ft8": | ||||||
|  |     case "wspr": | ||||||
|  |     case "jt65": | ||||||
|  |     case "jt9": | ||||||
|  |     case "ft4": | ||||||
|         secondary_demod_start(subtype); |         secondary_demod_start(subtype); | ||||||
|         demodulator_analog_replace('usb', true); |         demodulator_analog_replace('usb', true); | ||||||
|         demodulator_buttons_update(); |         demodulator_buttons_update(); | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|  |     $('#openwebrx-panel-digimodes').attr('data-mode', subtype); | ||||||
|     toggle_panel("openwebrx-panel-digimodes", true); |     toggle_panel("openwebrx-panel-digimodes", true); | ||||||
|  |     toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(subtype) >= 0); | ||||||
| } | } | ||||||
|  |  | ||||||
| function secondary_demod_create_canvas() | function secondary_demod_create_canvas() | ||||||
| @@ -2698,6 +2806,7 @@ function secondary_demod_swap_canvases() | |||||||
| function secondary_demod_init() | function secondary_demod_init() | ||||||
| { | { | ||||||
|     $("#openwebrx-panel-digimodes")[0].openwebrxHidden = true; |     $("#openwebrx-panel-digimodes")[0].openwebrxHidden = true; | ||||||
|  |     $("#openwebrx-panel-wsjt-message")[0].openwebrxHidden = true; | ||||||
|     secondary_demod_canvas_container = $("#openwebrx-digimode-canvas-container")[0]; |     secondary_demod_canvas_container = $("#openwebrx-digimode-canvas-container")[0]; | ||||||
|     $(secondary_demod_canvas_container) |     $(secondary_demod_canvas_container) | ||||||
|         .mousemove(secondary_demod_canvas_container_mousemove) |         .mousemove(secondary_demod_canvas_container_mousemove) | ||||||
| @@ -2705,6 +2814,7 @@ function secondary_demod_init() | |||||||
|         .mousedown(secondary_demod_canvas_container_mousedown) |         .mousedown(secondary_demod_canvas_container_mousedown) | ||||||
|         .mouseenter(secondary_demod_canvas_container_mousein) |         .mouseenter(secondary_demod_canvas_container_mousein) | ||||||
|         .mouseleave(secondary_demod_canvas_container_mouseout); |         .mouseleave(secondary_demod_canvas_container_mouseout); | ||||||
|  |     init_wsjt_removal_timer(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function secondary_demod_start(subtype)  | function secondary_demod_start(subtype)  | ||||||
| @@ -2762,6 +2872,7 @@ function secondary_demod_close_window() | |||||||
| { | { | ||||||
|     secondary_demod_stop(); |     secondary_demod_stop(); | ||||||
|     toggle_panel("openwebrx-panel-digimodes", false); |     toggle_panel("openwebrx-panel-digimodes", false); | ||||||
|  |     toggle_panel("openwebrx-panel-wsjt-message", false); | ||||||
| } | } | ||||||
|  |  | ||||||
| secondary_demod_fft_offset_db=30; //need to calculate that later | secondary_demod_fft_offset_db=30; //need to calculate that later | ||||||
| @@ -2802,19 +2913,23 @@ function secondary_demod_waterfall_dequeue() | |||||||
| secondary_demod_listbox_updating = false; | secondary_demod_listbox_updating = false; | ||||||
| function secondary_demod_listbox_changed() | function secondary_demod_listbox_changed() | ||||||
| { | { | ||||||
|     if(secondary_demod_listbox_updating) return; |     if (secondary_demod_listbox_updating) return; | ||||||
|     switch ($("#openwebrx-secondary-demod-listbox")[0].value) |     var sdm = $("#openwebrx-secondary-demod-listbox")[0].value; | ||||||
|     { |     switch (sdm) { | ||||||
|         case "none": |         case "none": | ||||||
|             demodulator_analog_replace_last(); |             demodulator_analog_replace_last(); | ||||||
|             break; |             break; | ||||||
|         case "bpsk31": |         case "bpsk31": | ||||||
|             demodulator_digital_replace('bpsk31'); |  | ||||||
|             break; |  | ||||||
|         case "rtty": |         case "rtty": | ||||||
|             demodulator_digital_replace('rtty'); |         case "ft8": | ||||||
|  |         case "wspr": | ||||||
|  |         case "jt65": | ||||||
|  |         case "jt9": | ||||||
|  |         case "ft4": | ||||||
|  |             demodulator_digital_replace(sdm); | ||||||
|             break; |             break; | ||||||
|     } |     } | ||||||
|  |     update_dial_button(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function secondary_demod_listbox_update() | function secondary_demod_listbox_update() | ||||||
|   | |||||||
| @@ -1,94 +0,0 @@ | |||||||
| <html> |  | ||||||
| <!-- |  | ||||||
|  |  | ||||||
| 	This file is part of OpenWebRX,  |  | ||||||
| 	an open-source SDR receiver software with a web UI. |  | ||||||
| 	Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> |  | ||||||
|  |  | ||||||
|     This program is free software: you can redistribute it and/or modify |  | ||||||
|     it under the terms of the GNU Affero General Public License as |  | ||||||
|     published by the Free Software Foundation, either version 3 of the |  | ||||||
|     License, or (at your option) any later version. |  | ||||||
|  |  | ||||||
|     This program is distributed in the hope that it will be useful, |  | ||||||
|     but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|     GNU Affero General Public License for more details. |  | ||||||
|  |  | ||||||
|     You should have received a copy of the GNU Affero General Public License |  | ||||||
|     along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
|  |  | ||||||
| --> |  | ||||||
| <head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |  | ||||||
| <style> |  | ||||||
| html, body |  | ||||||
| { |  | ||||||
| 	font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; |  | ||||||
| 	width: 100%; |  | ||||||
| 	text-align: center; |  | ||||||
| 	margin: 0; |  | ||||||
| 	padding: 0; |  | ||||||
| } |  | ||||||
| img.logo |  | ||||||
| {  |  | ||||||
| 	margin-top: 120px; |  | ||||||
| } |  | ||||||
| div.frame |  | ||||||
| { |  | ||||||
| 	text-align: left; |  | ||||||
| 	margin:0px auto; |  | ||||||
| 	width: 800px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| div.panel |  | ||||||
| { |  | ||||||
| 	text-align: center; |  | ||||||
| 	background-color:#777777;  |  | ||||||
| 	border-radius: 15px; |  | ||||||
| 	padding: 12px; |  | ||||||
| 	font-weight: bold; |  | ||||||
| 	color: White; |  | ||||||
| 	font-size: 13pt; |  | ||||||
| 	/*text-shadow: 1px 1px 4px #444;*/ |  | ||||||
| 	font-family: sans; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| div.alt |  | ||||||
| { |  | ||||||
| 	font-size: 10pt; |  | ||||||
| 	padding-top: 10px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| body div a |  | ||||||
| { |  | ||||||
| 	color: #5ca8ff; |  | ||||||
| 	text-shadow: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| span.browser |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| <script> |  | ||||||
| var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});} |  | ||||||
| var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); } |  | ||||||
| window.addEventListener("load",function(){rs=document.getElementById("reconnect-secs"); rt=document.getElementById("reconnect-text"); cnt=29;window.setInterval(function(){if(cnt<=-1) window.location.href=window.location.href.split("retry.")[0]; else if(cnt==0) {rt.innerHTML="Reconnecting..."; cnt--;} else rs.innerHTML=(cnt--).toString();},1000);},false); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| </head> |  | ||||||
| <body> |  | ||||||
|  |  | ||||||
| <div class="frame"> |  | ||||||
| 	<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/> |  | ||||||
| 	<div class="panel"> |  | ||||||
| 		There are no client slots left on this server. |  | ||||||
| 		<div class="alt"> |  | ||||||
| 			Please wait until a client disconnects.<br /><span id="reconnect-text">We will try to reconnect in <span id="reconnect-secs">30</span> seconds...</span>  |  | ||||||
| 		</div> |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
|  |  | ||||||
| @@ -1,95 +0,0 @@ | |||||||
| <html> |  | ||||||
| <!-- |  | ||||||
|  |  | ||||||
| 	This file is part of OpenWebRX,  |  | ||||||
| 	an open-source SDR receiver software with a web UI. |  | ||||||
| 	Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu> |  | ||||||
|  |  | ||||||
|     This program is free software: you can redistribute it and/or modify |  | ||||||
|     it under the terms of the GNU Affero General Public License as |  | ||||||
|     published by the Free Software Foundation, either version 3 of the |  | ||||||
|     License, or (at your option) any later version. |  | ||||||
|  |  | ||||||
|     This program is distributed in the hope that it will be useful, |  | ||||||
|     but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|     GNU Affero General Public License for more details. |  | ||||||
|  |  | ||||||
|     You should have received a copy of the GNU Affero General Public License |  | ||||||
|     along with this program.  If not, see <http://www.gnu.org/licenses/>. |  | ||||||
|  |  | ||||||
| --> |  | ||||||
| <head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |  | ||||||
| <style> |  | ||||||
| html, body |  | ||||||
| { |  | ||||||
| 	font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; |  | ||||||
| 	width: 100%; |  | ||||||
| 	text-align: center; |  | ||||||
| 	margin: 0; |  | ||||||
| 	padding: 0; |  | ||||||
| } |  | ||||||
| img.logo |  | ||||||
| {  |  | ||||||
| 	margin-top: 120px; |  | ||||||
| } |  | ||||||
| div.frame |  | ||||||
| { |  | ||||||
| 	text-align: left; |  | ||||||
| 	margin:0px auto; |  | ||||||
| 	width: 800px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| div.panel |  | ||||||
| { |  | ||||||
| 	text-align: center; |  | ||||||
| 	background-color:#777777;  |  | ||||||
| 	border-radius: 15px; |  | ||||||
| 	padding: 12px; |  | ||||||
| 	font-weight: bold; |  | ||||||
| 	color: White; |  | ||||||
| 	font-size: 13pt; |  | ||||||
| 	/*text-shadow: 1px 1px 4px #444;*/ |  | ||||||
| 	font-family: sans; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| div.alt |  | ||||||
| { |  | ||||||
| 	font-size: 10pt; |  | ||||||
| 	padding-top: 10px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| body div a |  | ||||||
| { |  | ||||||
| 	color: #5ca8ff; |  | ||||||
| 	text-shadow: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| span.browser |  | ||||||
| { |  | ||||||
| } |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| <script> |  | ||||||
| var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});} |  | ||||||
| var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); } |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| </head> |  | ||||||
| <body> |  | ||||||
|  |  | ||||||
| <div class="frame"> |  | ||||||
| 	<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/> |  | ||||||
| 	<div class="panel"> |  | ||||||
| 		Only the latest <span class="browser">Google Chrome</span> browser is supported at the moment.<br/> |  | ||||||
| 		Please <a href="http://chrome.google.com/">download and install Google Chrome.</a><br /> |  | ||||||
| 		<div class="alt"> |  | ||||||
| 			Alternatively, you may proceed to OpenWebRX, but it's not supposed to work as expected. <br /> |  | ||||||
| 			<a href="/?unsupported">Click here</a> if you still want to try OpenWebRX.</a> |  | ||||||
| 		</div> |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								openwebrx.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										22
									
								
								openwebrx.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -1,3 +1,5 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  |  | ||||||
| from http.server import HTTPServer | from http.server import HTTPServer | ||||||
| from owrx.http import RequestHandler | from owrx.http import RequestHandler | ||||||
| from owrx.config import PropertyManager | from owrx.config import PropertyManager | ||||||
| @@ -5,9 +7,11 @@ from owrx.feature import  FeatureDetector | |||||||
| from owrx.source import SdrService, ClientRegistry | from owrx.source import SdrService, ClientRegistry | ||||||
| from socketserver import ThreadingMixIn | from socketserver import ThreadingMixIn | ||||||
| from owrx.sdrhu import SdrHuUpdater | from owrx.sdrhu import SdrHuUpdater | ||||||
|  | from owrx.service import ServiceManager | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
| logging.basicConfig(level = logging.DEBUG, format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s") |  | ||||||
|  | logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") | ||||||
|  |  | ||||||
|  |  | ||||||
| class ThreadedHttpServer(ThreadingMixIn, HTTPServer): | class ThreadedHttpServer(ThreadingMixIn, HTTPServer): | ||||||
| @@ -15,21 +19,25 @@ class ThreadedHttpServer(ThreadingMixIn, HTTPServer): | |||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|     print(""" |     print( | ||||||
|  |         """ | ||||||
|  |  | ||||||
| OpenWebRX - Open Source SDR Web App for Everyone!  | for license see LICENSE file in the package | OpenWebRX - Open Source SDR Web App for Everyone!  | for license see LICENSE file in the package | ||||||
| _________________________________________________________________________________________________ | _________________________________________________________________________________________________ | ||||||
|  |  | ||||||
| Author contact info:    Andras Retzler, HA7ILM <randras@sdr.hu> | Author contact info:    Andras Retzler, HA7ILM <randras@sdr.hu> | ||||||
|  |  | ||||||
|     """) |     """ | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") |     pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") | ||||||
|  |  | ||||||
|     featureDetector = FeatureDetector() |     featureDetector = FeatureDetector() | ||||||
|     if not featureDetector.is_available("core"): |     if not featureDetector.is_available("core"): | ||||||
|         print("you are missing required dependencies to run openwebrx. " |         print( | ||||||
|               "please check that the following core requirements are installed:") |             "you are missing required dependencies to run openwebrx. " | ||||||
|  |             "please check that the following core requirements are installed:" | ||||||
|  |         ) | ||||||
|         print(", ".join(featureDetector.get_requirements("core"))) |         print(", ".join(featureDetector.get_requirements("core"))) | ||||||
|         return |         return | ||||||
|  |  | ||||||
| @@ -40,7 +48,9 @@ Author contact info:    Andras Retzler, HA7ILM <randras@sdr.hu> | |||||||
|         updater = SdrHuUpdater() |         updater = SdrHuUpdater() | ||||||
|         updater.start() |         updater.start() | ||||||
|  |  | ||||||
|     server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler) |     ServiceManager.getSharedInstance().start() | ||||||
|  |  | ||||||
|  |     server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler) | ||||||
|     server.serve_forever() |     server.serve_forever() | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								owrx/bands.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								owrx/bands.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | import json | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Band(object): | ||||||
|  |     def __init__(self, dict): | ||||||
|  |         self.name = dict["name"] | ||||||
|  |         self.lower_bound = dict["lower_bound"] | ||||||
|  |         self.upper_bound = dict["upper_bound"] | ||||||
|  |         self.frequencies = [] | ||||||
|  |         if "frequencies" in dict: | ||||||
|  |             for (mode, freqs) in dict["frequencies"].items(): | ||||||
|  |                 if not isinstance(freqs, list): | ||||||
|  |                     freqs = [freqs] | ||||||
|  |                 for f in freqs: | ||||||
|  |                     if not self.inBand(f): | ||||||
|  |                         logger.warning( | ||||||
|  |                             "Frequency for {mode} on {band} is not within band limits: {frequency}".format( | ||||||
|  |                                 mode=mode, frequency=f, band=self.name | ||||||
|  |                             ) | ||||||
|  |                         ) | ||||||
|  |                     else: | ||||||
|  |                         self.frequencies.append({"mode": mode, "frequency": f}) | ||||||
|  |  | ||||||
|  |     def inBand(self, freq): | ||||||
|  |         return self.lower_bound <= freq <= self.upper_bound | ||||||
|  |  | ||||||
|  |     def getName(self): | ||||||
|  |         return self.name | ||||||
|  |  | ||||||
|  |     def getDialFrequencies(self, range): | ||||||
|  |         (low, hi) = range | ||||||
|  |         return [e for e in self.frequencies if low <= e["frequency"] <= hi] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Bandplan(object): | ||||||
|  |     sharedInstance = None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def getSharedInstance(): | ||||||
|  |         if Bandplan.sharedInstance is None: | ||||||
|  |             Bandplan.sharedInstance = Bandplan() | ||||||
|  |         return Bandplan.sharedInstance | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         f = open("bands.json", "r") | ||||||
|  |         bands_json = json.load(f) | ||||||
|  |         f.close() | ||||||
|  |         self.bands = [Band(d) for d in bands_json] | ||||||
|  |  | ||||||
|  |     def findBands(self, freq): | ||||||
|  |         return [band for band in self.bands if band.inBand(freq)] | ||||||
|  |  | ||||||
|  |     def findBand(self, freq): | ||||||
|  |         bands = self.findBands(freq) | ||||||
|  |         if bands: | ||||||
|  |             return bands[0] | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |     def collectDialFrequencies(self, range): | ||||||
|  |         return [e for b in self.bands for e in b.getDialFrequencies(range)] | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -15,7 +16,7 @@ class Subscription(object): | |||||||
|  |  | ||||||
|  |  | ||||||
| class Property(object): | class Property(object): | ||||||
|     def __init__(self, value = None): |     def __init__(self, value=None): | ||||||
|         self.value = value |         self.value = value | ||||||
|         self.subscribers = [] |         self.subscribers = [] | ||||||
|  |  | ||||||
| @@ -23,7 +24,7 @@ class Property(object): | |||||||
|         return self.value |         return self.value | ||||||
|  |  | ||||||
|     def setValue(self, value): |     def setValue(self, value): | ||||||
|         if (self.value == value): |         if self.value == value: | ||||||
|             return self |             return self | ||||||
|         self.value = value |         self.value = value | ||||||
|         for c in self.subscribers: |         for c in self.subscribers: | ||||||
| @@ -36,7 +37,8 @@ class Property(object): | |||||||
|     def wire(self, callback): |     def wire(self, callback): | ||||||
|         sub = Subscription(self, callback) |         sub = Subscription(self, callback) | ||||||
|         self.subscribers.append(sub) |         self.subscribers.append(sub) | ||||||
|         if not self.value is None: sub.call(self.value) |         if not self.value is None: | ||||||
|  |             sub.call(self.value) | ||||||
|         return sub |         return sub | ||||||
|  |  | ||||||
|     def unwire(self, sub): |     def unwire(self, sub): | ||||||
| @@ -47,8 +49,10 @@ class Property(object): | |||||||
|             pass |             pass | ||||||
|         return self |         return self | ||||||
|  |  | ||||||
|  |  | ||||||
| class PropertyManager(object): | class PropertyManager(object): | ||||||
|     sharedInstance = None |     sharedInstance = None | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def getSharedInstance(): |     def getSharedInstance(): | ||||||
|         if PropertyManager.sharedInstance is None: |         if PropertyManager.sharedInstance is None: | ||||||
| @@ -56,9 +60,11 @@ class PropertyManager(object): | |||||||
|         return PropertyManager.sharedInstance |         return PropertyManager.sharedInstance | ||||||
|  |  | ||||||
|     def collect(self, *props): |     def collect(self, *props): | ||||||
|         return PropertyManager({name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}) |         return PropertyManager( | ||||||
|  |             {name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props} | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def __init__(self, properties = None): |     def __init__(self, properties=None): | ||||||
|         self.properties = {} |         self.properties = {} | ||||||
|         self.subscribers = [] |         self.subscribers = [] | ||||||
|         if properties is not None: |         if properties is not None: | ||||||
| @@ -67,12 +73,14 @@ class PropertyManager(object): | |||||||
|  |  | ||||||
|     def add(self, name, prop): |     def add(self, name, prop): | ||||||
|         self.properties[name] = prop |         self.properties[name] = prop | ||||||
|  |  | ||||||
|         def fireCallbacks(value): |         def fireCallbacks(value): | ||||||
|             for c in self.subscribers: |             for c in self.subscribers: | ||||||
|                 try: |                 try: | ||||||
|                     c.call(name, value) |                     c.call(name, value) | ||||||
|                 except Exception as e: |                 except Exception as e: | ||||||
|                     logger.exception(e) |                     logger.exception(e) | ||||||
|  |  | ||||||
|         prop.wire(fireCallbacks) |         prop.wire(fireCallbacks) | ||||||
|         return self |         return self | ||||||
|  |  | ||||||
| @@ -88,7 +96,7 @@ class PropertyManager(object): | |||||||
|         self.getProperty(name).setValue(value) |         self.getProperty(name).setValue(value) | ||||||
|  |  | ||||||
|     def __dict__(self): |     def __dict__(self): | ||||||
|         return {k:v.getValue() for k, v in self.properties.items()} |         return {k: v.getValue() for k, v in self.properties.items()} | ||||||
|  |  | ||||||
|     def hasProperty(self, name): |     def hasProperty(self, name): | ||||||
|         return name in self.properties |         return name in self.properties | ||||||
|   | |||||||
| @@ -1,20 +1,59 @@ | |||||||
| from owrx.config import PropertyManager | from owrx.config import PropertyManager | ||||||
| from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry | from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry | ||||||
| from owrx.feature import FeatureDetector | from owrx.feature import FeatureDetector | ||||||
|  | from owrx.version import openwebrx_version | ||||||
|  | from owrx.bands import Bandplan | ||||||
| import json | import json | ||||||
|  | from owrx.map import Map | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| class OpenWebRxClient(object): |  | ||||||
|     config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level", | class Client(object): | ||||||
|                    "waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps", |  | ||||||
|                    "audio_compression", "fft_compression", "max_clients", "start_mod", |  | ||||||
|                    "client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors", |  | ||||||
|                    "mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"] |  | ||||||
|     def __init__(self, conn): |     def __init__(self, conn): | ||||||
|         self.conn = conn |         self.conn = conn | ||||||
|  |  | ||||||
|  |     def protected_send(self, data): | ||||||
|  |         try: | ||||||
|  |             self.conn.send(data) | ||||||
|  |         # these exception happen when the socket is closed | ||||||
|  |         except OSError: | ||||||
|  |             self.close() | ||||||
|  |         except ValueError: | ||||||
|  |             self.close() | ||||||
|  |  | ||||||
|  |     def close(self): | ||||||
|  |         self.conn.close() | ||||||
|  |         logger.debug("connection closed") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OpenWebRxReceiverClient(Client): | ||||||
|  |     config_keys = [ | ||||||
|  |         "waterfall_colors", | ||||||
|  |         "waterfall_min_level", | ||||||
|  |         "waterfall_max_level", | ||||||
|  |         "waterfall_auto_level_margin", | ||||||
|  |         "lfo_offset", | ||||||
|  |         "samp_rate", | ||||||
|  |         "fft_size", | ||||||
|  |         "fft_fps", | ||||||
|  |         "audio_compression", | ||||||
|  |         "fft_compression", | ||||||
|  |         "max_clients", | ||||||
|  |         "start_mod", | ||||||
|  |         "client_audio_buffer_size", | ||||||
|  |         "start_freq", | ||||||
|  |         "center_freq", | ||||||
|  |         "mathbox_waterfall_colors", | ||||||
|  |         "mathbox_waterfall_history_length", | ||||||
|  |         "mathbox_waterfall_frequency_resolution", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     def __init__(self, conn): | ||||||
|  |         super().__init__(conn) | ||||||
|  |  | ||||||
|         self.dsp = None |         self.dsp = None | ||||||
|         self.sdr = None |         self.sdr = None | ||||||
|         self.configSub = None |         self.configSub = None | ||||||
| @@ -26,12 +65,23 @@ class OpenWebRxClient(object): | |||||||
|         self.setSdr() |         self.setSdr() | ||||||
|  |  | ||||||
|         # send receiver info |         # send receiver info | ||||||
|         receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl",  "receiver_gps", |         receiver_keys = [ | ||||||
|                          "photo_title", "photo_desc"] |             "receiver_name", | ||||||
|  |             "receiver_location", | ||||||
|  |             "receiver_qra", | ||||||
|  |             "receiver_asl", | ||||||
|  |             "receiver_gps", | ||||||
|  |             "photo_title", | ||||||
|  |             "photo_desc", | ||||||
|  |         ] | ||||||
|         receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys) |         receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys) | ||||||
|         self.write_receiver_details(receiver_details) |         self.write_receiver_details(receiver_details) | ||||||
|  |  | ||||||
|         profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()] |         profiles = [ | ||||||
|  |             {"name": s.getName() + " " + p["name"], "id": sid + "|" + pid} | ||||||
|  |             for (sid, s) in SdrService.getSources().items() | ||||||
|  |             for (pid, p) in s.getProfiles().items() | ||||||
|  |         ] | ||||||
|         self.write_profiles(profiles) |         self.write_profiles(profiles) | ||||||
|  |  | ||||||
|         features = FeatureDetector().feature_availability() |         features = FeatureDetector().feature_availability() | ||||||
| @@ -39,9 +89,9 @@ class OpenWebRxClient(object): | |||||||
|  |  | ||||||
|         CpuUsageThread.getSharedInstance().add_client(self) |         CpuUsageThread.getSharedInstance().add_client(self) | ||||||
|  |  | ||||||
|     def setSdr(self, id = None): |     def setSdr(self, id=None): | ||||||
|         next = SdrService.getSource(id) |         next = SdrService.getSource(id) | ||||||
|         if (next == self.sdr): |         if next == self.sdr: | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         self.stopDsp() |         self.stopDsp() | ||||||
| @@ -53,14 +103,23 @@ class OpenWebRxClient(object): | |||||||
|         self.sdr = next |         self.sdr = next | ||||||
|  |  | ||||||
|         # send initial config |         # send initial config | ||||||
|         configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance()) |         configProps = ( | ||||||
|  |             self.sdr.getProps() | ||||||
|  |             .collect(*OpenWebRxReceiverClient.config_keys) | ||||||
|  |             .defaults(PropertyManager.getSharedInstance()) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         def sendConfig(key, value): |         def sendConfig(key, value): | ||||||
|             config = dict((key, configProps[key]) for key in OpenWebRxClient.config_keys) |             config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys) | ||||||
|             # TODO mathematical properties? hmmmm |             # TODO mathematical properties? hmmmm | ||||||
|             config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] |             config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] | ||||||
|             self.write_config(config) |             self.write_config(config) | ||||||
|  |  | ||||||
|  |             cf = configProps["center_freq"] | ||||||
|  |             srh = configProps["samp_rate"] / 2 | ||||||
|  |             frequencyRange = (cf - srh, cf + srh) | ||||||
|  |             self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange)) | ||||||
|  |  | ||||||
|         self.configSub = configProps.wire(sendConfig) |         self.configSub = configProps.wire(sendConfig) | ||||||
|         sendConfig(None, None) |         sendConfig(None, None) | ||||||
|  |  | ||||||
| @@ -78,8 +137,7 @@ class OpenWebRxClient(object): | |||||||
|         if self.configSub is not None: |         if self.configSub is not None: | ||||||
|             self.configSub.cancel() |             self.configSub.cancel() | ||||||
|             self.configSub = None |             self.configSub = None | ||||||
|         self.conn.close() |         super().close() | ||||||
|         logger.debug("connection closed") |  | ||||||
|  |  | ||||||
|     def stopDsp(self): |     def stopDsp(self): | ||||||
|         if self.dsp is not None: |         if self.dsp is not None: | ||||||
| @@ -90,8 +148,11 @@ class OpenWebRxClient(object): | |||||||
|  |  | ||||||
|     def setParams(self, params): |     def setParams(self, params): | ||||||
|         # only the keys in the protected property manager can be overridden from the web |         # only the keys in the protected property manager can be overridden from the web | ||||||
|         protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain") \ |         protected = ( | ||||||
|  |             self.sdr.getProps() | ||||||
|  |             .collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain") | ||||||
|             .defaults(PropertyManager.getSharedInstance()) |             .defaults(PropertyManager.getSharedInstance()) | ||||||
|  |         ) | ||||||
|         for key, value in params.items(): |         for key, value in params.items(): | ||||||
|             protected[key] = value |             protected[key] = value | ||||||
|  |  | ||||||
| @@ -99,41 +160,71 @@ class OpenWebRxClient(object): | |||||||
|         for key, value in params.items(): |         for key, value in params.items(): | ||||||
|             self.dsp.setProperty(key, value) |             self.dsp.setProperty(key, value) | ||||||
|  |  | ||||||
|     def protected_send(self, data): |  | ||||||
|         try: |  | ||||||
|             self.conn.send(data) |  | ||||||
|         # these exception happen when the socket is closed |  | ||||||
|         except OSError: |  | ||||||
|             self.close() |  | ||||||
|         except ValueError: |  | ||||||
|             self.close() |  | ||||||
|  |  | ||||||
|     def write_spectrum_data(self, data): |     def write_spectrum_data(self, data): | ||||||
|         self.protected_send(bytes([0x01]) + data) |         self.protected_send(bytes([0x01]) + data) | ||||||
|  |  | ||||||
|     def write_dsp_data(self, data): |     def write_dsp_data(self, data): | ||||||
|         self.protected_send(bytes([0x02]) + data) |         self.protected_send(bytes([0x02]) + data) | ||||||
|  |  | ||||||
|     def write_s_meter_level(self, level): |     def write_s_meter_level(self, level): | ||||||
|         self.protected_send({"type":"smeter","value":level}) |         self.protected_send({"type": "smeter", "value": level}) | ||||||
|  |  | ||||||
|     def write_cpu_usage(self, usage): |     def write_cpu_usage(self, usage): | ||||||
|         self.protected_send({"type":"cpuusage","value":usage}) |         self.protected_send({"type": "cpuusage", "value": usage}) | ||||||
|  |  | ||||||
|     def write_clients(self, clients): |     def write_clients(self, clients): | ||||||
|         self.protected_send({"type":"clients","value":clients}) |         self.protected_send({"type": "clients", "value": clients}) | ||||||
|  |  | ||||||
|     def write_secondary_fft(self, data): |     def write_secondary_fft(self, data): | ||||||
|         self.protected_send(bytes([0x03]) + data) |         self.protected_send(bytes([0x03]) + data) | ||||||
|  |  | ||||||
|     def write_secondary_demod(self, data): |     def write_secondary_demod(self, data): | ||||||
|         self.protected_send(bytes([0x04]) + data) |         self.protected_send(bytes([0x04]) + data) | ||||||
|  |  | ||||||
|     def write_secondary_dsp_config(self, cfg): |     def write_secondary_dsp_config(self, cfg): | ||||||
|         self.protected_send({"type":"secondary_config", "value":cfg}) |         self.protected_send({"type": "secondary_config", "value": cfg}) | ||||||
|  |  | ||||||
|     def write_config(self, cfg): |     def write_config(self, cfg): | ||||||
|         self.protected_send({"type":"config","value":cfg}) |         self.protected_send({"type": "config", "value": cfg}) | ||||||
|  |  | ||||||
|     def write_receiver_details(self, details): |     def write_receiver_details(self, details): | ||||||
|         self.protected_send({"type":"receiver_details","value":details}) |         self.protected_send({"type": "receiver_details", "value": details}) | ||||||
|  |  | ||||||
|     def write_profiles(self, profiles): |     def write_profiles(self, profiles): | ||||||
|         self.protected_send({"type":"profiles","value":profiles}) |         self.protected_send({"type": "profiles", "value": profiles}) | ||||||
|  |  | ||||||
|     def write_features(self, features): |     def write_features(self, features): | ||||||
|         self.protected_send({"type":"features","value":features}) |         self.protected_send({"type": "features", "value": features}) | ||||||
|  |  | ||||||
|     def write_metadata(self, metadata): |     def write_metadata(self, metadata): | ||||||
|         self.protected_send({"type":"metadata","value":metadata}) |         self.protected_send({"type": "metadata", "value": metadata}) | ||||||
|  |  | ||||||
|  |     def write_wsjt_message(self, message): | ||||||
|  |         self.protected_send({"type": "wsjt_message", "value": message}) | ||||||
|  |  | ||||||
|  |     def write_dial_frequendies(self, frequencies): | ||||||
|  |         self.protected_send({"type": "dial_frequencies", "value": frequencies}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MapConnection(Client): | ||||||
|  |     def __init__(self, conn): | ||||||
|  |         super().__init__(conn) | ||||||
|  |  | ||||||
|  |         pm = PropertyManager.getSharedInstance() | ||||||
|  |         self.write_config(pm.collect("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__()) | ||||||
|  |  | ||||||
|  |         Map.getSharedInstance().addClient(self) | ||||||
|  |  | ||||||
|  |     def close(self): | ||||||
|  |         Map.getSharedInstance().removeClient(self) | ||||||
|  |         super().close() | ||||||
|  |  | ||||||
|  |     def write_config(self, cfg): | ||||||
|  |         self.protected_send({"type": "config", "value": cfg}) | ||||||
|  |  | ||||||
|  |     def write_update(self, update): | ||||||
|  |         self.protected_send({"type": "update", "value": update}) | ||||||
|  |  | ||||||
|  |  | ||||||
| class WebSocketMessageHandler(object): | class WebSocketMessageHandler(object): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
| @@ -142,12 +233,21 @@ class WebSocketMessageHandler(object): | |||||||
|         self.dsp = None |         self.dsp = None | ||||||
|  |  | ||||||
|     def handleTextMessage(self, conn, message): |     def handleTextMessage(self, conn, message): | ||||||
|         if (message[:16] == "SERVER DE CLIENT"): |         if message[:16] == "SERVER DE CLIENT": | ||||||
|             # maybe put some more info in there? nothing to store yet. |             meta = message[17:].split(" ") | ||||||
|             self.handshake = "completed" |             self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)} | ||||||
|  |  | ||||||
|  |             conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version)) | ||||||
|             logger.debug("client connection intitialized") |             logger.debug("client connection intitialized") | ||||||
|  |  | ||||||
|             self.client = OpenWebRxClient(conn) |             if "type" in self.handshake: | ||||||
|  |                 if self.handshake["type"] == "receiver": | ||||||
|  |                     self.client = OpenWebRxReceiverClient(conn) | ||||||
|  |                 if self.handshake["type"] == "map": | ||||||
|  |                     self.client = MapConnection(conn) | ||||||
|  |             # backwards compatibility | ||||||
|  |             else: | ||||||
|  |                 self.client = OpenWebRxReceiverClient(conn) | ||||||
|  |  | ||||||
|             return |             return | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,20 +1,27 @@ | |||||||
| import os | import os | ||||||
| import mimetypes | import mimetypes | ||||||
|  | import json | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  | from string import Template | ||||||
| from owrx.websocket import WebSocketConnection | from owrx.websocket import WebSocketConnection | ||||||
| from owrx.config import PropertyManager | from owrx.config import PropertyManager | ||||||
| from owrx.source import ClientRegistry | from owrx.source import ClientRegistry | ||||||
| from owrx.connection import WebSocketMessageHandler | from owrx.connection import WebSocketMessageHandler | ||||||
| from owrx.version import openwebrx_version | from owrx.version import openwebrx_version | ||||||
|  | from owrx.feature import FeatureDetector | ||||||
|  | from owrx.metrics import Metrics | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Controller(object): | class Controller(object): | ||||||
|     def __init__(self, handler, matches): |     def __init__(self, handler, request): | ||||||
|         self.handler = handler |         self.handler = handler | ||||||
|         self.matches = matches |         self.request = request | ||||||
|     def send_response(self, content, code = 200, content_type = "text/html", last_modified: datetime = None, max_age = None): |  | ||||||
|  |     def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None): | ||||||
|         self.handler.send_response(code) |         self.handler.send_response(code) | ||||||
|         if content_type is not None: |         if content_type is not None: | ||||||
|             self.handler.send_header("Content-Type", content_type) |             self.handler.send_header("Content-Type", content_type) | ||||||
| @@ -23,15 +30,10 @@ class Controller(object): | |||||||
|         if max_age is not None: |         if max_age is not None: | ||||||
|             self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age)) |             self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age)) | ||||||
|         self.handler.end_headers() |         self.handler.end_headers() | ||||||
|         if (type(content) == str): |         if type(content) == str: | ||||||
|             content = content.encode() |             content = content.encode() | ||||||
|         self.handler.wfile.write(content) |         self.handler.wfile.write(content) | ||||||
|     def render_template(self, template, **variables): |  | ||||||
|         f = open('htdocs/' + template) |  | ||||||
|         data = f.read() |  | ||||||
|         f.close() |  | ||||||
|  |  | ||||||
|         self.send_response(data) |  | ||||||
|  |  | ||||||
| class StatusController(Controller): | class StatusController(Controller): | ||||||
|     def handle_request(self): |     def handle_request(self): | ||||||
| @@ -47,41 +49,90 @@ class StatusController(Controller): | |||||||
|             "asl": pm["receiver_asl"], |             "asl": pm["receiver_asl"], | ||||||
|             "loc": pm["receiver_location"], |             "loc": pm["receiver_location"], | ||||||
|             "sw_version": openwebrx_version, |             "sw_version": openwebrx_version, | ||||||
|             "avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png") |             "avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"), | ||||||
|         } |         } | ||||||
|         self.send_response("\n".join(["{key}={value}".format(key = key, value = value) for key, value in vars.items()])) |         self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()])) | ||||||
|  |  | ||||||
|  |  | ||||||
| class AssetsController(Controller): | class AssetsController(Controller): | ||||||
|     def serve_file(self, file, content_type = None): |     def serve_file(self, file, content_type=None): | ||||||
|         try: |         try: | ||||||
|             modified = datetime.fromtimestamp(os.path.getmtime('htdocs/' + file)) |             modified = datetime.fromtimestamp(os.path.getmtime("htdocs/" + file)) | ||||||
|  |  | ||||||
|             if "If-Modified-Since" in self.handler.headers: |             if "If-Modified-Since" in self.handler.headers: | ||||||
|                 client_modified = datetime.strptime(self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z") |                 client_modified = datetime.strptime( | ||||||
|  |                     self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z" | ||||||
|  |                 ) | ||||||
|                 if modified <= client_modified: |                 if modified <= client_modified: | ||||||
|                     self.send_response("", code = 304) |                     self.send_response("", code=304) | ||||||
|                     return |                     return | ||||||
|  |  | ||||||
|             f = open('htdocs/' + file, 'rb') |             f = open("htdocs/" + file, "rb") | ||||||
|             data = f.read() |             data = f.read() | ||||||
|             f.close() |             f.close() | ||||||
|  |  | ||||||
|             if content_type is None: |             if content_type is None: | ||||||
|                 (content_type, encoding) = mimetypes.MimeTypes().guess_type(file) |                 (content_type, encoding) = mimetypes.MimeTypes().guess_type(file) | ||||||
|             self.send_response(data, content_type = content_type, last_modified = modified, max_age = 3600) |             self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600) | ||||||
|         except FileNotFoundError: |         except FileNotFoundError: | ||||||
|             self.send_response("file not found", code = 404) |             self.send_response("file not found", code=404) | ||||||
|  |  | ||||||
|     def handle_request(self): |     def handle_request(self): | ||||||
|         filename = self.matches.group(1) |         filename = self.request.matches.group(1) | ||||||
|         self.serve_file(filename) |         self.serve_file(filename) | ||||||
|  |  | ||||||
| class IndexController(AssetsController): |  | ||||||
|  | class TemplateController(Controller): | ||||||
|  |     def render_template(self, file, **vars): | ||||||
|  |         f = open("htdocs/" + file, "r") | ||||||
|  |         template = Template(f.read()) | ||||||
|  |         f.close() | ||||||
|  |  | ||||||
|  |         return template.safe_substitute(**vars) | ||||||
|  |  | ||||||
|  |     def serve_template(self, file, **vars): | ||||||
|  |         self.send_response(self.render_template(file, **vars), content_type="text/html") | ||||||
|  |  | ||||||
|  |     def default_variables(self): | ||||||
|  |         return {} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class WebpageController(TemplateController): | ||||||
|  |     def template_variables(self): | ||||||
|  |         header = self.render_template("include/header.include.html") | ||||||
|  |         return {"header": header} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class IndexController(WebpageController): | ||||||
|     def handle_request(self): |     def handle_request(self): | ||||||
|         self.serve_file("index.html", content_type = "text/html") |         self.serve_template("index.html", **self.template_variables()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MapController(WebpageController): | ||||||
|  |     def handle_request(self): | ||||||
|  |         # TODO check if we have a google maps api key first? | ||||||
|  |         self.serve_template("map.html", **self.template_variables()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FeatureController(WebpageController): | ||||||
|  |     def handle_request(self): | ||||||
|  |         self.serve_template("features.html", **self.template_variables()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ApiController(Controller): | ||||||
|  |     def handle_request(self): | ||||||
|  |         data = json.dumps(FeatureDetector().feature_report()) | ||||||
|  |         self.send_response(data, content_type="application/json") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MetricsController(Controller): | ||||||
|  |     def handle_request(self): | ||||||
|  |         data = json.dumps(Metrics.getSharedInstance().getMetrics()) | ||||||
|  |         self.send_response(data, content_type="application/json") | ||||||
|  |  | ||||||
|  |  | ||||||
| class WebSocketController(Controller): | class WebSocketController(Controller): | ||||||
|     def handle_request(self): |     def handle_request(self): | ||||||
|         conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) |         conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) | ||||||
|         conn.send("CLIENT DE SERVER openwebrx.py") |  | ||||||
|         # enter read loop |         # enter read loop | ||||||
|         conn.read_loop() |         conn.read_loop() | ||||||
|   | |||||||
							
								
								
									
										135
									
								
								owrx/feature.py
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								owrx/feature.py
									
									
									
									
									
								
							| @@ -4,29 +4,52 @@ from functools import reduce | |||||||
| from operator import and_ | from operator import and_ | ||||||
| import re | import re | ||||||
| from distutils.version import LooseVersion | from distutils.version import LooseVersion | ||||||
|  | import inspect | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class UnknownFeatureException(Exception): | class UnknownFeatureException(Exception): | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class FeatureDetector(object): | class FeatureDetector(object): | ||||||
|     features = { |     features = { | ||||||
|         "core": [ "csdr", "nmux", "nc" ], |         "core": ["csdr", "nmux", "nc"], | ||||||
|         "rtl_sdr": [ "rtl_sdr" ], |         "rtl_sdr": ["rtl_sdr"], | ||||||
|         "sdrplay": [ "rx_tools" ], |         "sdrplay": ["rx_tools"], | ||||||
|         "hackrf": [ "hackrf_transfer" ], |         "hackrf": ["hackrf_transfer"], | ||||||
|         "airspy": [ "airspy_rx" ], |         "airspy": ["airspy_rx"], | ||||||
|         "digital_voice_digiham": [ "digiham", "sox" ], |         "digital_voice_digiham": ["digiham", "sox"], | ||||||
|         "digital_voice_dsd": [ "dsd", "sox", "digiham" ], |         "digital_voice_dsd": ["dsd", "sox", "digiham"], | ||||||
|         "packet": [ "direwolf" ] |         "wsjt-x": ["wsjtx", "sox"], | ||||||
|  |         "packet": [ "direwolf" ], | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     def feature_availability(self): |     def feature_availability(self): | ||||||
|         return {name: self.is_available(name) for name in FeatureDetector.features} |         return {name: self.is_available(name) for name in FeatureDetector.features} | ||||||
|  |  | ||||||
|  |     def feature_report(self): | ||||||
|  |         def requirement_details(name): | ||||||
|  |             available = self.has_requirement(name) | ||||||
|  |             return { | ||||||
|  |                 "available": available, | ||||||
|  |                 # as of now, features are always enabled as soon as they are available. this may change in the future. | ||||||
|  |                 "enabled": available, | ||||||
|  |                 "description": self.get_requirement_description(name), | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |         def feature_details(name): | ||||||
|  |             return { | ||||||
|  |                 "description": "", | ||||||
|  |                 "available": self.is_available(name), | ||||||
|  |                 "requirements": {name: requirement_details(name) for name in self.get_requirements(name)}, | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |         return {name: feature_details(name) for name in FeatureDetector.features} | ||||||
|  |  | ||||||
|     def is_available(self, feature): |     def is_available(self, feature): | ||||||
|         return self.has_requirements(self.get_requirements(feature)) |         return self.has_requirements(self.get_requirements(feature)) | ||||||
|  |  | ||||||
| @@ -34,38 +57,77 @@ class FeatureDetector(object): | |||||||
|         try: |         try: | ||||||
|             return FeatureDetector.features[feature] |             return FeatureDetector.features[feature] | ||||||
|         except KeyError: |         except KeyError: | ||||||
|             raise UnknownFeatureException("Feature \"{0}\" is not known.".format(feature)) |             raise UnknownFeatureException('Feature "{0}" is not known.'.format(feature)) | ||||||
|  |  | ||||||
|     def has_requirements(self, requirements): |     def has_requirements(self, requirements): | ||||||
|         passed = True |         passed = True | ||||||
|         for requirement in requirements: |         for requirement in requirements: | ||||||
|  |             passed = passed and self.has_requirement(requirement) | ||||||
|  |         return passed | ||||||
|  |  | ||||||
|  |     def _get_requirement_method(self, requirement): | ||||||
|         methodname = "has_" + requirement |         methodname = "has_" + requirement | ||||||
|         if hasattr(self, methodname) and callable(getattr(self, methodname)): |         if hasattr(self, methodname) and callable(getattr(self, methodname)): | ||||||
|                 passed = passed and getattr(self, methodname)() |             return getattr(self, methodname) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     def has_requirement(self, requirement): | ||||||
|  |         method = self._get_requirement_method(requirement) | ||||||
|  |         if method is not None: | ||||||
|  |             return method() | ||||||
|         else: |         else: | ||||||
|             logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) |             logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) | ||||||
|         return passed |         return False | ||||||
|  |  | ||||||
|  |     def get_requirement_description(self, requirement): | ||||||
|  |         return inspect.getdoc(self._get_requirement_method(requirement)) | ||||||
|  |  | ||||||
|     def command_is_runnable(self, command): |     def command_is_runnable(self, command): | ||||||
|         return os.system("{0} 2>/dev/null >/dev/null".format(command)) != 32512 |         return os.system("{0} 2>/dev/null >/dev/null".format(command)) != 32512 | ||||||
|  |  | ||||||
|     def has_csdr(self): |     def has_csdr(self): | ||||||
|  |         """ | ||||||
|  |         OpenWebRX uses the demodulator and pipeline tools provided by the csdr project. Please check out [the project | ||||||
|  |         page on github](https://github.com/simonyiszk/csdr) for further details and installation instructions. | ||||||
|  |         """ | ||||||
|         return self.command_is_runnable("csdr") |         return self.command_is_runnable("csdr") | ||||||
|  |  | ||||||
|     def has_nmux(self): |     def has_nmux(self): | ||||||
|  |         """ | ||||||
|  |         Nmux is another tool provided by the csdr project. It is used for internal multiplexing of the IQ data streams. | ||||||
|  |         If you're missing nmux even though you have csdr installed, please update your csdr version. | ||||||
|  |         """ | ||||||
|         return self.command_is_runnable("nmux --help") |         return self.command_is_runnable("nmux --help") | ||||||
|  |  | ||||||
|     def has_nc(self): |     def has_nc(self): | ||||||
|         return self.command_is_runnable('nc --help') |         """ | ||||||
|  |         Nc is the client used to connect to the nmux multiplexer. It is provided by either the BSD netcat (recommended | ||||||
|  |         for better performance) or GNU netcat packages. Please check your distribution package manager for options. | ||||||
|  |         """ | ||||||
|  |         return self.command_is_runnable("nc --help") | ||||||
|  |  | ||||||
|     def has_rtl_sdr(self): |     def has_rtl_sdr(self): | ||||||
|  |         """ | ||||||
|  |         The rtl-sdr command is required to read I/Q data from an RTL SDR USB-Stick. It is available in most | ||||||
|  |         distribution package managers. | ||||||
|  |         """ | ||||||
|         return self.command_is_runnable("rtl_sdr --help") |         return self.command_is_runnable("rtl_sdr --help") | ||||||
|  |  | ||||||
|     def has_rx_tools(self): |     def has_rx_tools(self): | ||||||
|  |         """ | ||||||
|  |         The rx_tools package can be used to interface with SDR devices compatible with SoapySDR. It is currently used | ||||||
|  |         to connect to SDRPlay devices. Please check the following pages for more details: | ||||||
|  |  | ||||||
|  |         * [rx_tools GitHub page](https://github.com/rxseger/rx_tools) | ||||||
|  |         * [SoapySDR Project wiki](https://github.com/pothosware/SoapySDR/wiki) | ||||||
|  |         * [SDRPlay homepage](https://www.sdrplay.com/) | ||||||
|  |         """ | ||||||
|         return self.command_is_runnable("rx_sdr --help") |         return self.command_is_runnable("rx_sdr --help") | ||||||
|  |  | ||||||
|  |     def has_hackrf_transfer(self): | ||||||
|         """ |         """ | ||||||
|         To use a HackRF, compile the HackRF host tools from its "stdout" branch: |         To use a HackRF, compile the HackRF host tools from its "stdout" branch: | ||||||
|  |         ``` | ||||||
|          git clone https://github.com/mossmann/hackrf/ |          git clone https://github.com/mossmann/hackrf/ | ||||||
|          cd hackrf |          cd hackrf | ||||||
|          git fetch |          git fetch | ||||||
| @@ -76,8 +138,8 @@ class FeatureDetector(object): | |||||||
|          cmake .. -DINSTALL_UDEV_RULES=ON |          cmake .. -DINSTALL_UDEV_RULES=ON | ||||||
|          make |          make | ||||||
|          sudo make install |          sudo make install | ||||||
|  |         ``` | ||||||
|         """ |         """ | ||||||
|     def has_hackrf_transfer(self): |  | ||||||
|         # TODO i don't have a hackrf, so somebody doublecheck this. |         # TODO i don't have a hackrf, so somebody doublecheck this. | ||||||
|         # TODO also check if it has the stdout feature |         # TODO also check if it has the stdout feature | ||||||
|         return self.command_is_runnable("hackrf_transfer --help") |         return self.command_is_runnable("hackrf_transfer --help") | ||||||
| @@ -85,18 +147,19 @@ class FeatureDetector(object): | |||||||
|     def command_exists(self, command): |     def command_exists(self, command): | ||||||
|         return os.system("which {0}".format(command)) == 0 |         return os.system("which {0}".format(command)) == 0 | ||||||
|  |  | ||||||
|  |     def has_digiham(self): | ||||||
|         """ |         """ | ||||||
|     To use DMR and YSF, the digiham package is required. You can find the package and installation instructions here: |         To use digital voice modes, the digiham package is required. You can find the package and installation | ||||||
|     https://github.com/jketterl/digiham |         instructions [here](https://github.com/jketterl/digiham). | ||||||
|  |  | ||||||
|         Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work. |         Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work. | ||||||
|         If you have an older verison of digiham installed, please update it along with openwebrx. |         If you have an older verison of digiham installed, please update it along with openwebrx. | ||||||
|         As of now, we require version 0.2 of digiham. |         As of now, we require version 0.2 of digiham. | ||||||
|         """ |         """ | ||||||
|     def has_digiham(self): |  | ||||||
|         required_version = LooseVersion("0.2") |         required_version = LooseVersion("0.2") | ||||||
|  |  | ||||||
|         digiham_version_regex = re.compile('^digiham version (.*)$') |         digiham_version_regex = re.compile("^digiham version (.*)$") | ||||||
|  |  | ||||||
|         def check_digiham_version(command): |         def check_digiham_version(command): | ||||||
|             try: |             try: | ||||||
|                 process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) |                 process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) | ||||||
| @@ -105,22 +168,52 @@ class FeatureDetector(object): | |||||||
|                 return version >= required_version |                 return version >= required_version | ||||||
|             except FileNotFoundError: |             except FileNotFoundError: | ||||||
|                 return False |                 return False | ||||||
|         return reduce(and_, |  | ||||||
|  |         return reduce( | ||||||
|  |             and_, | ||||||
|             map( |             map( | ||||||
|                 check_digiham_version, |                 check_digiham_version, | ||||||
|                           ["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator", |                 [ | ||||||
|                            "digitalvoice_filter"] |                     "rrc_filter", | ||||||
|  |                     "ysf_decoder", | ||||||
|  |                     "dmr_decoder", | ||||||
|  |                     "mbe_synthesizer", | ||||||
|  |                     "gfsk_demodulator", | ||||||
|  |                     "digitalvoice_filter", | ||||||
|  |                 ], | ||||||
|             ), |             ), | ||||||
|                       True) |             True, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def has_dsd(self): |     def has_dsd(self): | ||||||
|  |         """ | ||||||
|  |         The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version | ||||||
|  |         modified by F4EXB that provides stdin/stdout support. You can find it [here](https://github.com/f4exb/dsd). | ||||||
|  |         """ | ||||||
|         return self.command_is_runnable("dsd") |         return self.command_is_runnable("dsd") | ||||||
|  |  | ||||||
|     def has_sox(self): |     def has_sox(self): | ||||||
|  |         """ | ||||||
|  |         The sox audio library is used to convert between the typical 8 kHz audio sampling rate used by digital modes and | ||||||
|  |         the audio sampling rate requested by the client. | ||||||
|  |  | ||||||
|  |         It is available for most distributions through the respective package manager. | ||||||
|  |         """ | ||||||
|         return self.command_is_runnable("sox") |         return self.command_is_runnable("sox") | ||||||
|  |  | ||||||
|     def has_direwolf(self): |     def has_direwolf(self): | ||||||
|         return self.command_is_runnable("direwolf --help") |         return self.command_is_runnable("direwolf --help") | ||||||
|  |  | ||||||
|     def has_airspy_rx(self): |     def has_airspy_rx(self): | ||||||
|  |         """ | ||||||
|  |         In order to use an Airspy Receiver, you need to install the airspy_rx receiver software. | ||||||
|  |         """ | ||||||
|         return self.command_is_runnable("airspy_rx --help 2> /dev/null") |         return self.command_is_runnable("airspy_rx --help 2> /dev/null") | ||||||
|  |  | ||||||
|  |     def has_wsjtx(self): | ||||||
|  |         """ | ||||||
|  |         To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the | ||||||
|  |         [WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions | ||||||
|  |         on how to build from source. | ||||||
|  |         """ | ||||||
|  |         return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								owrx/http.py
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								owrx/http.py
									
									
									
									
									
								
							| @@ -1,17 +1,37 @@ | |||||||
| from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController | from owrx.controllers import ( | ||||||
|  |     StatusController, | ||||||
|  |     IndexController, | ||||||
|  |     AssetsController, | ||||||
|  |     WebSocketController, | ||||||
|  |     MapController, | ||||||
|  |     FeatureController, | ||||||
|  |     ApiController, | ||||||
|  |     MetricsController, | ||||||
|  | ) | ||||||
| from http.server import BaseHTTPRequestHandler | from http.server import BaseHTTPRequestHandler | ||||||
| import re | import re | ||||||
|  | from urllib.parse import urlparse, parse_qs | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RequestHandler(BaseHTTPRequestHandler): | class RequestHandler(BaseHTTPRequestHandler): | ||||||
|     def __init__(self, request, client_address, server): |     def __init__(self, request, client_address, server): | ||||||
|         self.router = Router() |         self.router = Router() | ||||||
|         super().__init__(request, client_address, server) |         super().__init__(request, client_address, server) | ||||||
|  |  | ||||||
|     def do_GET(self): |     def do_GET(self): | ||||||
|         self.router.route(self) |         self.router.route(self) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Request(object): | ||||||
|  |     def __init__(self, query=None, matches=None): | ||||||
|  |         self.query = query | ||||||
|  |         self.matches = matches | ||||||
|  |  | ||||||
|  |  | ||||||
| class Router(object): | class Router(object): | ||||||
|     mappings = [ |     mappings = [ | ||||||
|         {"route": "/", "controller": IndexController}, |         {"route": "/", "controller": IndexController}, | ||||||
| @@ -20,8 +40,13 @@ class Router(object): | |||||||
|         {"route": "/ws/", "controller": WebSocketController}, |         {"route": "/ws/", "controller": WebSocketController}, | ||||||
|         {"regex": "(/favicon.ico)", "controller": AssetsController}, |         {"regex": "(/favicon.ico)", "controller": AssetsController}, | ||||||
|         # backwards compatibility for the sdr.hu portal |         # backwards compatibility for the sdr.hu portal | ||||||
|         {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController} |         {"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController}, | ||||||
|  |         {"route": "/map", "controller": MapController}, | ||||||
|  |         {"route": "/features", "controller": FeatureController}, | ||||||
|  |         {"route": "/api/features", "controller": ApiController}, | ||||||
|  |         {"route": "/metrics", "controller": MetricsController}, | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     def find_controller(self, path): |     def find_controller(self, path): | ||||||
|         for m in Router.mappings: |         for m in Router.mappings: | ||||||
|             if "route" in m: |             if "route" in m: | ||||||
| @@ -32,11 +57,17 @@ class Router(object): | |||||||
|                 matches = regex.match(path) |                 matches = regex.match(path) | ||||||
|                 if matches: |                 if matches: | ||||||
|                     return (m["controller"], matches) |                     return (m["controller"], matches) | ||||||
|  |  | ||||||
|     def route(self, handler): |     def route(self, handler): | ||||||
|         res = self.find_controller(handler.path) |         url = urlparse(handler.path) | ||||||
|  |         res = self.find_controller(url.path) | ||||||
|         if res is not None: |         if res is not None: | ||||||
|             (controller, matches) = res |             (controller, matches) = res | ||||||
|             logger.debug("path: {0}, controller: {1}, matches: {2}".format(handler.path, controller, matches)) |             query = parse_qs(url.query) | ||||||
|             controller(handler, matches).handle_request() |             logger.debug( | ||||||
|  |                 "path: {0}, controller: {1}, query: {2}, matches: {3}".format(handler.path, controller, query, matches) | ||||||
|  |             ) | ||||||
|  |             request = Request(query, matches) | ||||||
|  |             controller(handler, request).handle_request() | ||||||
|         else: |         else: | ||||||
|             handler.send_error(404, "Not Found", "The page you requested could not be found.") |             handler.send_error(404, "Not Found", "The page you requested could not be found.") | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								owrx/map.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								owrx/map.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | from datetime import datetime, timedelta | ||||||
|  | import threading, time | ||||||
|  | from owrx.config import PropertyManager | ||||||
|  | from owrx.bands import Band | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Location(object): | ||||||
|  |     def __dict__(self): | ||||||
|  |         return {} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Map(object): | ||||||
|  |     sharedInstance = None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def getSharedInstance(): | ||||||
|  |         if Map.sharedInstance is None: | ||||||
|  |             Map.sharedInstance = Map() | ||||||
|  |         return Map.sharedInstance | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         self.clients = [] | ||||||
|  |         self.positions = {} | ||||||
|  |  | ||||||
|  |         def removeLoop(): | ||||||
|  |             while True: | ||||||
|  |                 try: | ||||||
|  |                     self.removeOldPositions() | ||||||
|  |                 except Exception: | ||||||
|  |                     logger.exception("error while removing old map positions") | ||||||
|  |                 time.sleep(60) | ||||||
|  |  | ||||||
|  |         threading.Thread(target=removeLoop, daemon=True).start() | ||||||
|  |         super().__init__() | ||||||
|  |  | ||||||
|  |     def broadcast(self, update): | ||||||
|  |         for c in self.clients: | ||||||
|  |             c.write_update(update) | ||||||
|  |  | ||||||
|  |     def addClient(self, client): | ||||||
|  |         self.clients.append(client) | ||||||
|  |         client.write_update( | ||||||
|  |             [ | ||||||
|  |                 { | ||||||
|  |                     "callsign": callsign, | ||||||
|  |                     "location": record["location"].__dict__(), | ||||||
|  |                     "lastseen": record["updated"].timestamp() * 1000, | ||||||
|  |                     "mode": record["mode"], | ||||||
|  |                     "band": record["band"].getName() if record["band"] is not None else None, | ||||||
|  |                 } | ||||||
|  |                 for (callsign, record) in self.positions.items() | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def removeClient(self, client): | ||||||
|  |         try: | ||||||
|  |             self.clients.remove(client) | ||||||
|  |         except ValueError: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |     def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None): | ||||||
|  |         ts = datetime.now() | ||||||
|  |         self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band} | ||||||
|  |         self.broadcast( | ||||||
|  |             [ | ||||||
|  |                 { | ||||||
|  |                     "callsign": callsign, | ||||||
|  |                     "location": loc.__dict__(), | ||||||
|  |                     "lastseen": ts.timestamp() * 1000, | ||||||
|  |                     "mode": mode, | ||||||
|  |                     "band": band.getName() if band is not None else None, | ||||||
|  |                 } | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def removeLocation(self, callsign): | ||||||
|  |         self.positions.pop(callsign, None) | ||||||
|  |         # TODO broadcast removal to clients | ||||||
|  |  | ||||||
|  |     def removeOldPositions(self): | ||||||
|  |         pm = PropertyManager.getSharedInstance() | ||||||
|  |         retention = timedelta(seconds=pm["map_position_retention_time"]) | ||||||
|  |         cutoff = datetime.now() - retention | ||||||
|  |  | ||||||
|  |         to_be_removed = [callsign for (callsign, pos) in self.positions.items() if pos["updated"] < cutoff] | ||||||
|  |         for callsign in to_be_removed: | ||||||
|  |             self.removeLocation(callsign) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LatLngLocation(Location): | ||||||
|  |     def __init__(self, lat: float, lon: float): | ||||||
|  |         self.lat = lat | ||||||
|  |         self.lon = lon | ||||||
|  |  | ||||||
|  |     def __dict__(self): | ||||||
|  |         return {"type": "latlon", "lat": self.lat, "lon": self.lon} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocatorLocation(Location): | ||||||
|  |     def __init__(self, locator: str): | ||||||
|  |         self.locator = locator | ||||||
|  |  | ||||||
|  |     def __dict__(self): | ||||||
|  |         return {"type": "locator", "locator": self.locator} | ||||||
							
								
								
									
										45
									
								
								owrx/meta.py
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								owrx/meta.py
									
									
									
									
									
								
							| @@ -4,36 +4,43 @@ import json | |||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| import logging | import logging | ||||||
| import threading | import threading | ||||||
|  | from owrx.map import Map, LatLngLocation | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class DmrCache(object): | class DmrCache(object): | ||||||
|     sharedInstance = None |     sharedInstance = None | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def getSharedInstance(): |     def getSharedInstance(): | ||||||
|         if DmrCache.sharedInstance is None: |         if DmrCache.sharedInstance is None: | ||||||
|             DmrCache.sharedInstance = DmrCache() |             DmrCache.sharedInstance = DmrCache() | ||||||
|         return DmrCache.sharedInstance |         return DmrCache.sharedInstance | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.cache = {} |         self.cache = {} | ||||||
|         self.cacheTimeout = timedelta(seconds = 86400) |         self.cacheTimeout = timedelta(seconds=86400) | ||||||
|  |  | ||||||
|     def isValid(self, key): |     def isValid(self, key): | ||||||
|         if not key in self.cache: return False |         if not key in self.cache: | ||||||
|  |             return False | ||||||
|         entry = self.cache[key] |         entry = self.cache[key] | ||||||
|         return entry["timestamp"] + self.cacheTimeout > datetime.now() |         return entry["timestamp"] + self.cacheTimeout > datetime.now() | ||||||
|  |  | ||||||
|     def put(self, key, value): |     def put(self, key, value): | ||||||
|         self.cache[key] = { |         self.cache[key] = {"timestamp": datetime.now(), "data": value} | ||||||
|             "timestamp": datetime.now(), |  | ||||||
|             "data": value |  | ||||||
|         } |  | ||||||
|     def get(self, key): |     def get(self, key): | ||||||
|         if not self.isValid(key): return None |         if not self.isValid(key): | ||||||
|  |             return None | ||||||
|         return self.cache[key]["data"] |         return self.cache[key]["data"] | ||||||
|  |  | ||||||
|  |  | ||||||
| class DmrMetaEnricher(object): | class DmrMetaEnricher(object): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.threads = {} |         self.threads = {} | ||||||
|  |  | ||||||
|     def downloadRadioIdData(self, id): |     def downloadRadioIdData(self, id): | ||||||
|         cache = DmrCache.getSharedInstance() |         cache = DmrCache.getSharedInstance() | ||||||
|         try: |         try: | ||||||
| @@ -44,9 +51,12 @@ class DmrMetaEnricher(object): | |||||||
|         except json.JSONDecodeError: |         except json.JSONDecodeError: | ||||||
|             cache.put(id, None) |             cache.put(id, None) | ||||||
|         del self.threads[id] |         del self.threads[id] | ||||||
|  |  | ||||||
|     def enrich(self, meta): |     def enrich(self, meta): | ||||||
|         if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None |         if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: | ||||||
|         if not "source" in meta: return None |             return None | ||||||
|  |         if not "source" in meta: | ||||||
|  |             return None | ||||||
|         id = meta["source"] |         id = meta["source"] | ||||||
|         cache = DmrCache.getSharedInstance() |         cache = DmrCache.getSharedInstance() | ||||||
|         if not cache.isValid(id): |         if not cache.isValid(id): | ||||||
| @@ -60,10 +70,17 @@ class DmrMetaEnricher(object): | |||||||
|         return None |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class YsfMetaEnricher(object): | ||||||
|  |     def enrich(self, meta): | ||||||
|  |         if "source" in meta and "lat" in meta and "lon" in meta: | ||||||
|  |             # TODO parsing the float values should probably happen earlier | ||||||
|  |             loc = LatLngLocation(float(meta["lat"]), float(meta["lon"])) | ||||||
|  |             Map.getSharedInstance().updateLocation(meta["source"], loc, "YSF") | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
| class MetaParser(object): | class MetaParser(object): | ||||||
|     enrichers = { |     enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher()} | ||||||
|         "DMR": DmrMetaEnricher() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     def __init__(self, handler): |     def __init__(self, handler): | ||||||
|         self.handler = handler |         self.handler = handler | ||||||
| @@ -76,6 +93,6 @@ class MetaParser(object): | |||||||
|             protocol = meta["protocol"] |             protocol = meta["protocol"] | ||||||
|             if protocol in MetaParser.enrichers: |             if protocol in MetaParser.enrichers: | ||||||
|                 additional_data = MetaParser.enrichers[protocol].enrich(meta) |                 additional_data = MetaParser.enrichers[protocol].enrich(meta) | ||||||
|                 if additional_data is not None: meta["additional"] = additional_data |                 if additional_data is not None: | ||||||
|  |                     meta["additional"] = additional_data | ||||||
|         self.handler.write_metadata(meta) |         self.handler.write_metadata(meta) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								owrx/metrics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								owrx/metrics.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | class Metrics(object): | ||||||
|  |     sharedInstance = None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def getSharedInstance(): | ||||||
|  |         if Metrics.sharedInstance is None: | ||||||
|  |             Metrics.sharedInstance = Metrics() | ||||||
|  |         return Metrics.sharedInstance | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         self.metrics = {} | ||||||
|  |  | ||||||
|  |     def pushDecodes(self, band, mode, count=1): | ||||||
|  |         if band is None: | ||||||
|  |             band = "unknown" | ||||||
|  |         else: | ||||||
|  |             band = band.getName() | ||||||
|  |  | ||||||
|  |         if mode is None: | ||||||
|  |             mode = "unknown" | ||||||
|  |  | ||||||
|  |         if not band in self.metrics: | ||||||
|  |             self.metrics[band] = {} | ||||||
|  |         if not mode in self.metrics[band]: | ||||||
|  |             self.metrics[band][mode] = {"count": 0} | ||||||
|  |  | ||||||
|  |         self.metrics[band][mode]["count"] += count | ||||||
|  |  | ||||||
|  |     def getMetrics(self): | ||||||
|  |         return self.metrics | ||||||
| @@ -4,23 +4,26 @@ import time | |||||||
| from owrx.config import PropertyManager | from owrx.config import PropertyManager | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class SdrHuUpdater(threading.Thread): | class SdrHuUpdater(threading.Thread): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.doRun = True |         self.doRun = True | ||||||
|         super().__init__(daemon = True) |         super().__init__(daemon=True) | ||||||
|  |  | ||||||
|     def update(self): |     def update(self): | ||||||
|         pm = PropertyManager.getSharedInstance() |         pm = PropertyManager.getSharedInstance() | ||||||
|         cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}\" 2>&1".format(**pm.__dict__()) |         cmd = 'wget --timeout=15 -4qO- https://sdr.hu/update --post-data "url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}" 2>&1'.format( | ||||||
|  |             **pm.__dict__() | ||||||
|  |         ) | ||||||
|         logger.debug(cmd) |         logger.debug(cmd) | ||||||
|         returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() |         returned = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() | ||||||
|         returned=returned[0].decode('utf-8') |         returned = returned[0].decode("utf-8") | ||||||
|         if "UPDATE:" in returned: |         if "UPDATE:" in returned: | ||||||
|             retrytime_mins = 20 |             retrytime_mins = 20 | ||||||
|             value=returned.split("UPDATE:")[1].split("\n",1)[0] |             value = returned.split("UPDATE:")[1].split("\n", 1)[0] | ||||||
|             if value.startswith("SUCCESS"): |             if value.startswith("SUCCESS"): | ||||||
|                 logger.info("Update succeeded!") |                 logger.info("Update succeeded!") | ||||||
|             else: |             else: | ||||||
| @@ -33,4 +36,4 @@ class SdrHuUpdater(threading.Thread): | |||||||
|     def run(self): |     def run(self): | ||||||
|         while self.doRun: |         while self.doRun: | ||||||
|             retrytime_mins = self.update() |             retrytime_mins = self.update() | ||||||
|             time.sleep(60*retrytime_mins) |             time.sleep(60 * retrytime_mins) | ||||||
|   | |||||||
							
								
								
									
										118
									
								
								owrx/service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								owrx/service.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | |||||||
|  | import threading | ||||||
|  | from owrx.source import SdrService | ||||||
|  | from owrx.bands import Bandplan | ||||||
|  | from csdr import dsp, output | ||||||
|  | from owrx.wsjt import WsjtParser | ||||||
|  | from owrx.config import PropertyManager | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ServiceOutput(output): | ||||||
|  |     def __init__(self, frequency): | ||||||
|  |         self.frequency = frequency | ||||||
|  |  | ||||||
|  |     def receive_output(self, t, read_fn): | ||||||
|  |         parser = WsjtParser(WsjtHandler()) | ||||||
|  |         parser.setDialFrequency(self.frequency) | ||||||
|  |         target = self.pump(read_fn, parser.parse) | ||||||
|  |         threading.Thread(target=target).start() | ||||||
|  |  | ||||||
|  |     def supports_type(self, t): | ||||||
|  |         return t == "wsjt_demod" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ServiceHandler(object): | ||||||
|  |     def __init__(self, source): | ||||||
|  |         self.services = [] | ||||||
|  |         self.source = source | ||||||
|  |         self.startupTimer = None | ||||||
|  |         self.source.addClient(self) | ||||||
|  |         self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange) | ||||||
|  |         self.scheduleServiceStartup() | ||||||
|  |  | ||||||
|  |     def onSdrAvailable(self): | ||||||
|  |         self.scheduleServiceStartup() | ||||||
|  |  | ||||||
|  |     def onSdrUnavailable(self): | ||||||
|  |         self.stopServices() | ||||||
|  |  | ||||||
|  |     def isSupported(self, mode): | ||||||
|  |         return mode in PropertyManager.getSharedInstance()["services_decoders"] | ||||||
|  |  | ||||||
|  |     def stopServices(self): | ||||||
|  |         for service in self.services: | ||||||
|  |             service.stop() | ||||||
|  |         self.services = [] | ||||||
|  |  | ||||||
|  |     def startServices(self): | ||||||
|  |         for service in self.services: | ||||||
|  |             service.start() | ||||||
|  |  | ||||||
|  |     def onFrequencyChange(self, key, value): | ||||||
|  |         self.stopServices() | ||||||
|  |         if not self.source.isAvailable(): | ||||||
|  |             return | ||||||
|  |         self.scheduleServiceStartup() | ||||||
|  |  | ||||||
|  |     def scheduleServiceStartup(self): | ||||||
|  |         if self.startupTimer: | ||||||
|  |             self.startupTimer.cancel() | ||||||
|  |         self.startupTimer = threading.Timer(10, self.updateServices) | ||||||
|  |         self.startupTimer.start() | ||||||
|  |  | ||||||
|  |     def updateServices(self): | ||||||
|  |         logger.debug("re-scheduling services due to sdr changes") | ||||||
|  |         self.stopServices() | ||||||
|  |         cf = self.source.getProps()["center_freq"] | ||||||
|  |         srh = self.source.getProps()["samp_rate"] / 2 | ||||||
|  |         frequency_range = (cf - srh, cf + srh) | ||||||
|  |         self.services = [ | ||||||
|  |             self.setupService(dial["mode"], dial["frequency"]) | ||||||
|  |             for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range) | ||||||
|  |             if self.isSupported(dial["mode"]) | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |     def setupService(self, mode, frequency): | ||||||
|  |         logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) | ||||||
|  |         d = dsp(ServiceOutput(frequency)) | ||||||
|  |         d.nc_port = self.source.getPort() | ||||||
|  |         d.set_offset_freq(frequency - self.source.getProps()["center_freq"]) | ||||||
|  |         d.set_demodulator("usb") | ||||||
|  |         d.set_bpf(0, 3000) | ||||||
|  |         d.set_secondary_demodulator(mode) | ||||||
|  |         d.set_audio_compression("none") | ||||||
|  |         d.set_samp_rate(self.source.getProps()["samp_rate"]) | ||||||
|  |         d.start() | ||||||
|  |         return d | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class WsjtHandler(object): | ||||||
|  |     def write_wsjt_message(self, msg): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ServiceManager(object): | ||||||
|  |     sharedInstance = None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def getSharedInstance(): | ||||||
|  |         if ServiceManager.sharedInstance is None: | ||||||
|  |             ServiceManager.sharedInstance = ServiceManager() | ||||||
|  |         return ServiceManager.sharedInstance | ||||||
|  |  | ||||||
|  |     def start(self): | ||||||
|  |         if not PropertyManager.getSharedInstance()["services_enabled"]: | ||||||
|  |             return | ||||||
|  |         for source in SdrService.getSources().values(): | ||||||
|  |             ServiceHandler(source) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Service(object): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class WsjtService(Service): | ||||||
|  |     pass | ||||||
							
								
								
									
										237
									
								
								owrx/source.py
									
									
									
									
									
								
							
							
						
						
									
										237
									
								
								owrx/source.py
									
									
									
									
									
								
							| @@ -2,6 +2,7 @@ import subprocess | |||||||
| from owrx.config import PropertyManager | from owrx.config import PropertyManager | ||||||
| from owrx.feature import FeatureDetector, UnknownFeatureException | from owrx.feature import FeatureDetector, UnknownFeatureException | ||||||
| from owrx.meta import MetaParser | from owrx.meta import MetaParser | ||||||
|  | from owrx.wsjt import WsjtParser | ||||||
| import threading | import threading | ||||||
| import csdr | import csdr | ||||||
| import time | import time | ||||||
| @@ -13,10 +14,12 @@ import logging | |||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class SdrService(object): | class SdrService(object): | ||||||
|     sdrProps = None |     sdrProps = None | ||||||
|     sources = {} |     sources = {} | ||||||
|     lastPort = None |     lastPort = None | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def getNextPort(): |     def getNextPort(): | ||||||
|         pm = PropertyManager.getSharedInstance() |         pm = PropertyManager.getSharedInstance() | ||||||
| @@ -28,50 +31,70 @@ class SdrService(object): | |||||||
|             if SdrService.lastPort > end: |             if SdrService.lastPort > end: | ||||||
|                 raise IndexError("no more available ports to start more sdrs") |                 raise IndexError("no more available ports to start more sdrs") | ||||||
|         return SdrService.lastPort |         return SdrService.lastPort | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def loadProps(): |     def loadProps(): | ||||||
|         if SdrService.sdrProps is None: |         if SdrService.sdrProps is None: | ||||||
|             pm = PropertyManager.getSharedInstance() |             pm = PropertyManager.getSharedInstance() | ||||||
|             featureDetector = FeatureDetector() |             featureDetector = FeatureDetector() | ||||||
|  |  | ||||||
|             def loadIntoPropertyManager(dict: dict): |             def loadIntoPropertyManager(dict: dict): | ||||||
|                 propertyManager = PropertyManager() |                 propertyManager = PropertyManager() | ||||||
|                 for (name, value) in dict.items(): |                 for (name, value) in dict.items(): | ||||||
|                     propertyManager[name] = value |                     propertyManager[name] = value | ||||||
|                 return propertyManager |                 return propertyManager | ||||||
|  |  | ||||||
|             def sdrTypeAvailable(value): |             def sdrTypeAvailable(value): | ||||||
|                 try: |                 try: | ||||||
|                     if not featureDetector.is_available(value["type"]): |                     if not featureDetector.is_available(value["type"]): | ||||||
|                         logger.error("The RTL source type \"{0}\" is not available. please check requirements.".format(value["type"])) |                         logger.error( | ||||||
|  |                             'The RTL source type "{0}" is not available. please check requirements.'.format( | ||||||
|  |                                 value["type"] | ||||||
|  |                             ) | ||||||
|  |                         ) | ||||||
|                         return False |                         return False | ||||||
|                     return True |                     return True | ||||||
|                 except UnknownFeatureException: |                 except UnknownFeatureException: | ||||||
|                     logger.error("The RTL source type \"{0}\" is invalid. Please check your configuration".format(value["type"])) |                     logger.error( | ||||||
|  |                         'The RTL source type "{0}" is invalid. Please check your configuration'.format(value["type"]) | ||||||
|  |                     ) | ||||||
|                     return False |                     return False | ||||||
|  |  | ||||||
|             # transform all dictionary items into PropertyManager object, filtering out unavailable ones |             # transform all dictionary items into PropertyManager object, filtering out unavailable ones | ||||||
|             SdrService.sdrProps = { |             SdrService.sdrProps = { | ||||||
|                 name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value) |                 name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value) | ||||||
|             } |             } | ||||||
|             logger.info("SDR sources loaded. Availables SDRs: {0}".format(", ".join(map(lambda x: x["name"], SdrService.sdrProps.values())))) |             logger.info( | ||||||
|  |                 "SDR sources loaded. Availables SDRs: {0}".format( | ||||||
|  |                     ", ".join(map(lambda x: x["name"], SdrService.sdrProps.values())) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def getSource(id = None): |     def getSource(id=None): | ||||||
|         SdrService.loadProps() |         SdrService.loadProps() | ||||||
|         if id is None: |         if id is None: | ||||||
|             # TODO: configure default sdr in config? right now it will pick the first one off the list. |             # TODO: configure default sdr in config? right now it will pick the first one off the list. | ||||||
|             id = list(SdrService.sdrProps.keys())[0] |             id = list(SdrService.sdrProps.keys())[0] | ||||||
|         sources = SdrService.getSources() |         sources = SdrService.getSources() | ||||||
|         return sources[id] |         return sources[id] | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def getSources(): |     def getSources(): | ||||||
|         SdrService.loadProps() |         SdrService.loadProps() | ||||||
|         for id in SdrService.sdrProps.keys(): |         for id in SdrService.sdrProps.keys(): | ||||||
|             if not id in SdrService.sources: |             if not id in SdrService.sources: | ||||||
|                 props = SdrService.sdrProps[id] |                 props = SdrService.sdrProps[id] | ||||||
|                 className = ''.join(x for x in props["type"].title() if x.isalnum()) + "Source" |                 className = "".join(x for x in props["type"].title() if x.isalnum()) + "Source" | ||||||
|                 cls = getattr(sys.modules[__name__], className) |                 cls = getattr(sys.modules[__name__], className) | ||||||
|                 SdrService.sources[id] = cls(props, SdrService.getNextPort()) |                 SdrService.sources[id] = cls(props, SdrService.getNextPort()) | ||||||
|         return SdrService.sources |         return SdrService.sources | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SdrSourceException(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class SdrSource(object): | class SdrSource(object): | ||||||
|     def __init__(self, props, port): |     def __init__(self, props, port): | ||||||
|         self.props = props |         self.props = props | ||||||
| @@ -84,6 +107,7 @@ class SdrSource(object): | |||||||
|             logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value)) |             logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value)) | ||||||
|             self.stop() |             self.stop() | ||||||
|             self.start() |             self.start() | ||||||
|  |  | ||||||
|         self.rtlProps.wire(restart) |         self.rtlProps.wire(restart) | ||||||
|         self.port = port |         self.port = port | ||||||
|         self.monitor = None |         self.monitor = None | ||||||
| @@ -101,15 +125,16 @@ class SdrSource(object): | |||||||
|     def getFormatConversion(self): |     def getFormatConversion(self): | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|     def activateProfile(self, id = None): |     def activateProfile(self, profile_id=None): | ||||||
|         profiles = self.props["profiles"] |         profiles = self.props["profiles"] | ||||||
|         if id is None: |         if profile_id is None: | ||||||
|             id = list(profiles.keys())[0] |             profile_id = list(profiles.keys())[0] | ||||||
|         logger.debug("activating profile {0}".format(id)) |         logger.debug("activating profile {0}".format(profile_id)) | ||||||
|         profile = profiles[id] |         profile = profiles[profile_id] | ||||||
|         for (key, value) in profile.items(): |         for (key, value) in profile.items(): | ||||||
|             # skip the name, that would overwrite the source name. |             # skip the name, that would overwrite the source name. | ||||||
|             if key == "name": continue |             if key == "name": | ||||||
|  |                 continue | ||||||
|             self.props[key] = value |             self.props[key] = value | ||||||
|  |  | ||||||
|     def getProfiles(self): |     def getProfiles(self): | ||||||
| @@ -133,7 +158,9 @@ class SdrSource(object): | |||||||
|         props = self.rtlProps |         props = self.rtlProps | ||||||
|  |  | ||||||
|         start_sdr_command = self.getCommand().format( |         start_sdr_command = self.getCommand().format( | ||||||
|             **props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain").__dict__() |             **props.collect( | ||||||
|  |                 "samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain" | ||||||
|  |             ).__dict__() | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         format_conversion = self.getFormatConversion() |         format_conversion = self.getFormatConversion() | ||||||
| @@ -141,36 +168,54 @@ class SdrSource(object): | |||||||
|             start_sdr_command += " | " + format_conversion |             start_sdr_command += " | " + format_conversion | ||||||
|  |  | ||||||
|         nmux_bufcnt = nmux_bufsize = 0 |         nmux_bufcnt = nmux_bufsize = 0 | ||||||
|         while nmux_bufsize < props["samp_rate"]/4: nmux_bufsize += 4096 |         while nmux_bufsize < props["samp_rate"] / 4: | ||||||
|         while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: nmux_bufcnt += 1 |             nmux_bufsize += 4096 | ||||||
|  |         while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: | ||||||
|  |             nmux_bufcnt += 1 | ||||||
|         if nmux_bufcnt == 0 or nmux_bufsize == 0: |         if nmux_bufcnt == 0 or nmux_bufsize == 0: | ||||||
|             logger.error("Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py") |             logger.error( | ||||||
|  |                 "Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py" | ||||||
|  |             ) | ||||||
|             self.modificationLock.release() |             self.modificationLock.release() | ||||||
|             return |             return | ||||||
|         logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) |         logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) | ||||||
|         cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port) |         cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % ( | ||||||
|  |             nmux_bufsize, | ||||||
|  |             nmux_bufcnt, | ||||||
|  |             self.port, | ||||||
|  |         ) | ||||||
|         self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) |         self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) | ||||||
|         logger.info("Started rtl source: " + cmd) |         logger.info("Started rtl source: " + cmd) | ||||||
|  |  | ||||||
|  |         available = False | ||||||
|  |  | ||||||
|         def wait_for_process_to_end(): |         def wait_for_process_to_end(): | ||||||
|             rc = self.process.wait() |             rc = self.process.wait() | ||||||
|             logger.debug("shut down with RC={0}".format(rc)) |             logger.debug("shut down with RC={0}".format(rc)) | ||||||
|             self.monitor = None |             self.monitor = None | ||||||
|  |  | ||||||
|         self.monitor = threading.Thread(target = wait_for_process_to_end) |         self.monitor = threading.Thread(target=wait_for_process_to_end) | ||||||
|         self.monitor.start() |         self.monitor.start() | ||||||
|  |  | ||||||
|         while True: |         retries = 1000 | ||||||
|  |         while retries > 0: | ||||||
|  |             retries -= 1 | ||||||
|  |             if self.monitor is None: | ||||||
|  |                 break | ||||||
|             testsock = socket.socket() |             testsock = socket.socket() | ||||||
|             try: |             try: | ||||||
|                 testsock.connect(("127.0.0.1", self.getPort())) |                 testsock.connect(("127.0.0.1", self.getPort())) | ||||||
|                 testsock.close() |                 testsock.close() | ||||||
|  |                 available = True | ||||||
|                 break |                 break | ||||||
|             except: |             except: | ||||||
|                 time.sleep(0.1) |                 time.sleep(0.1) | ||||||
|  |  | ||||||
|         self.modificationLock.release() |         self.modificationLock.release() | ||||||
|  |  | ||||||
|  |         if not available: | ||||||
|  |             raise SdrSourceException("rtl source failed to start up") | ||||||
|  |  | ||||||
|         for c in self.clients: |         for c in self.clients: | ||||||
|             c.onSdrAvailable() |             c.onSdrAvailable() | ||||||
|  |  | ||||||
| @@ -200,6 +245,7 @@ class SdrSource(object): | |||||||
|     def addClient(self, c): |     def addClient(self, c): | ||||||
|         self.clients.append(c) |         self.clients.append(c) | ||||||
|         self.start() |         self.start() | ||||||
|  |  | ||||||
|     def removeClient(self, c): |     def removeClient(self, c): | ||||||
|         try: |         try: | ||||||
|             self.clients.remove(c) |             self.clients.remove(c) | ||||||
| @@ -235,6 +281,7 @@ class RtlSdrSource(SdrSource): | |||||||
|     def getFormatConversion(self): |     def getFormatConversion(self): | ||||||
|         return "csdr convert_u8_f" |         return "csdr convert_u8_f" | ||||||
|  |  | ||||||
|  |  | ||||||
| class HackrfSource(SdrSource): | class HackrfSource(SdrSource): | ||||||
|     def getCommand(self): |     def getCommand(self): | ||||||
|         return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-" |         return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-" | ||||||
| @@ -242,39 +289,54 @@ class HackrfSource(SdrSource): | |||||||
|     def getFormatConversion(self): |     def getFormatConversion(self): | ||||||
|         return "csdr convert_s8_f" |         return "csdr convert_s8_f" | ||||||
|  |  | ||||||
|  |  | ||||||
| class SdrplaySource(SdrSource): | class SdrplaySource(SdrSource): | ||||||
|     def getCommand(self): |     def getCommand(self): | ||||||
|         command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}" |         command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}" | ||||||
|         gainMap = { "rf_gain" : "RFGR", "if_gain" : "IFGR"} |         gainMap = {"rf_gain": "RFGR", "if_gain": "IFGR"} | ||||||
|         gains = [ "{0}={{{1}}}".format(gainMap[name], name) for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() if value is not None ] |         gains = [ | ||||||
|  |             "{0}={{{1}}}".format(gainMap[name], name) | ||||||
|  |             for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() | ||||||
|  |             if value is not None | ||||||
|  |         ] | ||||||
|         if gains: |         if gains: | ||||||
|             command += " -g {gains}".format(gains = ",".join(gains)) |             command += " -g {gains}".format(gains=",".join(gains)) | ||||||
|         if self.rtlProps["antenna"] is not None: |         if self.rtlProps["antenna"] is not None: | ||||||
|             command += " -a \"{antenna}\"" |             command += ' -a "{antenna}"' | ||||||
|         command += " -" |         command += " -" | ||||||
|         return command |         return command | ||||||
|  |  | ||||||
|     def sleepOnRestart(self): |     def sleepOnRestart(self): | ||||||
|         time.sleep(1) |         time.sleep(1) | ||||||
|  |  | ||||||
|  |  | ||||||
| class AirspySource(SdrSource): | class AirspySource(SdrSource): | ||||||
|     def getCommand(self): |     def getCommand(self): | ||||||
|         frequency = self.props['center_freq'] / 1e6 |         frequency = self.props["center_freq"] / 1e6 | ||||||
|         command = "airspy_rx" |         command = "airspy_rx" | ||||||
|         command += " -f{0}".format(frequency) |         command += " -f{0}".format(frequency) | ||||||
|         command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}" |         command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}" | ||||||
|         return command |         return command | ||||||
|  |  | ||||||
|     def getFormatConversion(self): |     def getFormatConversion(self): | ||||||
|         return "csdr convert_s16_f" |         return "csdr convert_s16_f" | ||||||
|  |  | ||||||
|  |  | ||||||
| class SpectrumThread(csdr.output): | class SpectrumThread(csdr.output): | ||||||
|     def __init__(self, sdrSource): |     def __init__(self, sdrSource): | ||||||
|         self.sdrSource = sdrSource |         self.sdrSource = sdrSource | ||||||
|         super().__init__() |         super().__init__() | ||||||
|  |  | ||||||
|         self.props = props = self.sdrSource.props.collect( |         self.props = props = self.sdrSource.props.collect( | ||||||
|             "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", |             "samp_rate", | ||||||
|             "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through" |             "fft_size", | ||||||
|  |             "fft_fps", | ||||||
|  |             "fft_voverlap_factor", | ||||||
|  |             "fft_compression", | ||||||
|  |             "csdr_dynamic_bufsize", | ||||||
|  |             "csdr_print_bufsizes", | ||||||
|  |             "csdr_through", | ||||||
|  |             "temporary_directory", | ||||||
|         ).defaults(PropertyManager.getSharedInstance()) |         ).defaults(PropertyManager.getSharedInstance()) | ||||||
|  |  | ||||||
|         self.dsp = dsp = csdr.dsp(self) |         self.dsp = dsp = csdr.dsp(self) | ||||||
| @@ -287,14 +349,19 @@ class SpectrumThread(csdr.output): | |||||||
|             fft_fps = props["fft_fps"] |             fft_fps = props["fft_fps"] | ||||||
|             fft_voverlap_factor = props["fft_voverlap_factor"] |             fft_voverlap_factor = props["fft_voverlap_factor"] | ||||||
|  |  | ||||||
|             dsp.set_fft_averages(int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) if fft_voverlap_factor>0 else 0) |             dsp.set_fft_averages( | ||||||
|  |                 int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) | ||||||
|  |                 if fft_voverlap_factor > 0 | ||||||
|  |                 else 0 | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         self.subscriptions = [ |         self.subscriptions = [ | ||||||
|             props.getProperty("samp_rate").wire(dsp.set_samp_rate), |             props.getProperty("samp_rate").wire(dsp.set_samp_rate), | ||||||
|             props.getProperty("fft_size").wire(dsp.set_fft_size), |             props.getProperty("fft_size").wire(dsp.set_fft_size), | ||||||
|             props.getProperty("fft_fps").wire(dsp.set_fft_fps), |             props.getProperty("fft_fps").wire(dsp.set_fft_fps), | ||||||
|             props.getProperty("fft_compression").wire(dsp.set_fft_compression), |             props.getProperty("fft_compression").wire(dsp.set_fft_compression), | ||||||
|             props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages) |             props.getProperty("temporary_directory").wire(dsp.set_temporary_directory), | ||||||
|  |             props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages), | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|         set_fft_averages(None, None) |         set_fft_averages(None, None) | ||||||
| @@ -309,25 +376,15 @@ class SpectrumThread(csdr.output): | |||||||
|         if self.sdrSource.isAvailable(): |         if self.sdrSource.isAvailable(): | ||||||
|             self.dsp.start() |             self.dsp.start() | ||||||
|  |  | ||||||
|     def add_output(self, type, read_fn): |     def supports_type(self, t): | ||||||
|         if type != "audio": |         return t == "audio" | ||||||
|             logger.error("unsupported output type received by FFT: %s", type) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|  |     def receive_output(self, type, read_fn): | ||||||
|         if self.props["csdr_dynamic_bufsize"]: |         if self.props["csdr_dynamic_bufsize"]: | ||||||
|             read_fn(8) #dummy read to skip bufsize & preamble |             read_fn(8)  # dummy read to skip bufsize & preamble | ||||||
|             logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") |             logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1") | ||||||
|  |  | ||||||
|         def pipe(): |         threading.Thread(target=self.pump(read_fn, self.sdrSource.writeSpectrumData)).start() | ||||||
|             run = True |  | ||||||
|             while run: |  | ||||||
|                 data = read_fn() |  | ||||||
|                 if len(data) == 0: |  | ||||||
|                     run = False |  | ||||||
|                 else: |  | ||||||
|                     self.sdrSource.writeSpectrumData(data) |  | ||||||
|  |  | ||||||
|         threading.Thread(target = pipe).start() |  | ||||||
|  |  | ||||||
|     def stop(self): |     def stop(self): | ||||||
|         self.dsp.stop() |         self.dsp.stop() | ||||||
| @@ -338,20 +395,36 @@ class SpectrumThread(csdr.output): | |||||||
|  |  | ||||||
|     def onSdrAvailable(self): |     def onSdrAvailable(self): | ||||||
|         self.dsp.start() |         self.dsp.start() | ||||||
|  |  | ||||||
|     def onSdrUnavailable(self): |     def onSdrUnavailable(self): | ||||||
|         self.dsp.stop() |         self.dsp.stop() | ||||||
|  |  | ||||||
|  |  | ||||||
| class DspManager(csdr.output): | class DspManager(csdr.output): | ||||||
|     def __init__(self, handler, sdrSource): |     def __init__(self, handler, sdrSource): | ||||||
|         self.handler = handler |         self.handler = handler | ||||||
|         self.sdrSource = sdrSource |         self.sdrSource = sdrSource | ||||||
|         self.metaParser = MetaParser(self.handler) |         self.metaParser = MetaParser(self.handler) | ||||||
|  |         self.wsjtParser = WsjtParser(self.handler) | ||||||
|  |  | ||||||
|         self.localProps = self.sdrSource.getProps().collect( |         self.localProps = ( | ||||||
|             "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", |             self.sdrSource.getProps() | ||||||
|             "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality", |             .collect( | ||||||
|             "dmr_filter" |                 "audio_compression", | ||||||
|         ).defaults(PropertyManager.getSharedInstance()) |                 "fft_compression", | ||||||
|  |                 "digimodes_fft_size", | ||||||
|  |                 "csdr_dynamic_bufsize", | ||||||
|  |                 "csdr_print_bufsizes", | ||||||
|  |                 "csdr_through", | ||||||
|  |                 "digimodes_enable", | ||||||
|  |                 "samp_rate", | ||||||
|  |                 "digital_voice_unvoiced_quality", | ||||||
|  |                 "dmr_filter", | ||||||
|  |                 "temporary_directory", | ||||||
|  |                 "center_freq", | ||||||
|  |             ) | ||||||
|  |             .defaults(PropertyManager.getSharedInstance()) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         self.dsp = csdr.dsp(self) |         self.dsp = csdr.dsp(self) | ||||||
|         self.dsp.nc_port = self.sdrSource.getPort() |         self.dsp.nc_port = self.sdrSource.getPort() | ||||||
| @@ -366,6 +439,9 @@ class DspManager(csdr.output): | |||||||
|             bpf[1] = cut |             bpf[1] = cut | ||||||
|             self.dsp.set_bpf(*bpf) |             self.dsp.set_bpf(*bpf) | ||||||
|  |  | ||||||
|  |         def set_dial_freq(key, value): | ||||||
|  |             self.wsjtParser.setDialFrequency(self.localProps["center_freq"] + self.localProps["offset_freq"]) | ||||||
|  |  | ||||||
|         self.subscriptions = [ |         self.subscriptions = [ | ||||||
|             self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression), |             self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression), | ||||||
|             self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression), |             self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression), | ||||||
| @@ -378,28 +454,35 @@ class DspManager(csdr.output): | |||||||
|             self.localProps.getProperty("high_cut").wire(set_high_cut), |             self.localProps.getProperty("high_cut").wire(set_high_cut), | ||||||
|             self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), |             self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), | ||||||
|             self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality), |             self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality), | ||||||
|             self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter) |             self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter), | ||||||
|  |             self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory), | ||||||
|  |             self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq), | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|         self.dsp.set_offset_freq(0) |         self.dsp.set_offset_freq(0) | ||||||
|         self.dsp.set_bpf(-4000,4000) |         self.dsp.set_bpf(-4000, 4000) | ||||||
|         self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"] |         self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"] | ||||||
|         self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"] |         self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"] | ||||||
|         self.dsp.csdr_through = self.localProps["csdr_through"] |         self.dsp.csdr_through = self.localProps["csdr_through"] | ||||||
|  |  | ||||||
|         if (self.localProps["digimodes_enable"]): |         if self.localProps["digimodes_enable"]: | ||||||
|  |  | ||||||
|             def set_secondary_mod(mod): |             def set_secondary_mod(mod): | ||||||
|                 if mod == False: mod = None |                 if mod == False: | ||||||
|  |                     mod = None | ||||||
|                 self.dsp.set_secondary_demodulator(mod) |                 self.dsp.set_secondary_demodulator(mod) | ||||||
|                 if mod is not None: |                 if mod is not None: | ||||||
|                     self.handler.write_secondary_dsp_config({ |                     self.handler.write_secondary_dsp_config( | ||||||
|                         "secondary_fft_size":self.localProps["digimodes_fft_size"], |                         { | ||||||
|                         "if_samp_rate":self.dsp.if_samp_rate(), |                             "secondary_fft_size": self.localProps["digimodes_fft_size"], | ||||||
|                         "secondary_bw":self.dsp.secondary_bw() |                             "if_samp_rate": self.dsp.if_samp_rate(), | ||||||
|                     }) |                             "secondary_bw": self.dsp.secondary_bw(), | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|             self.subscriptions += [ |             self.subscriptions += [ | ||||||
|                 self.localProps.getProperty("secondary_mod").wire(set_secondary_mod), |                 self.localProps.getProperty("secondary_mod").wire(set_secondary_mod), | ||||||
|                 self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq) |                 self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq), | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|         self.sdrSource.addClient(self) |         self.sdrSource.addClient(self) | ||||||
| @@ -410,30 +493,19 @@ class DspManager(csdr.output): | |||||||
|         if self.sdrSource.isAvailable(): |         if self.sdrSource.isAvailable(): | ||||||
|             self.dsp.start() |             self.dsp.start() | ||||||
|  |  | ||||||
|     def add_output(self, t, read_fn): |     def receive_output(self, t, read_fn): | ||||||
|         logger.debug("adding new output of type %s", t) |         logger.debug("adding new output of type %s", t) | ||||||
|         writers = { |         writers = { | ||||||
|             "audio": self.handler.write_dsp_data, |             "audio": self.handler.write_dsp_data, | ||||||
|             "smeter": self.handler.write_s_meter_level, |             "smeter": self.handler.write_s_meter_level, | ||||||
|             "secondary_fft": self.handler.write_secondary_fft, |             "secondary_fft": self.handler.write_secondary_fft, | ||||||
|             "secondary_demod": self.handler.write_secondary_demod, |             "secondary_demod": self.handler.write_secondary_demod, | ||||||
|             "meta": self.metaParser.parse |             "meta": self.metaParser.parse, | ||||||
|  |             "wsjt_demod": self.wsjtParser.parse, | ||||||
|         } |         } | ||||||
|         write = writers[t] |         write = writers[t] | ||||||
|  |  | ||||||
|         def pump(read, write): |         threading.Thread(target=self.pump(read_fn, write)).start() | ||||||
|             def copy(): |  | ||||||
|                 run = True |  | ||||||
|                 while run: |  | ||||||
|                     data = read() |  | ||||||
|                     if data is None or (isinstance(data, bytes) and len(data) == 0): |  | ||||||
|                         logger.warning("zero read on {0}".format(t)) |  | ||||||
|                         run = False |  | ||||||
|                     else: |  | ||||||
|                         write(data) |  | ||||||
|             return copy |  | ||||||
|  |  | ||||||
|         threading.Thread(target=pump(read_fn, write)).start() |  | ||||||
|  |  | ||||||
|     def stop(self): |     def stop(self): | ||||||
|         self.dsp.stop() |         self.dsp.stop() | ||||||
| @@ -453,8 +525,10 @@ class DspManager(csdr.output): | |||||||
|         logger.debug("received onSdrUnavailable, shutting down DspSource") |         logger.debug("received onSdrUnavailable, shutting down DspSource") | ||||||
|         self.dsp.stop() |         self.dsp.stop() | ||||||
|  |  | ||||||
|  |  | ||||||
| class CpuUsageThread(threading.Thread): | class CpuUsageThread(threading.Thread): | ||||||
|     sharedInstance = None |     sharedInstance = None | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def getSharedInstance(): |     def getSharedInstance(): | ||||||
|         if CpuUsageThread.sharedInstance is None: |         if CpuUsageThread.sharedInstance is None: | ||||||
| @@ -482,21 +556,23 @@ class CpuUsageThread(threading.Thread): | |||||||
|  |  | ||||||
|     def get_cpu_usage(self): |     def get_cpu_usage(self): | ||||||
|         try: |         try: | ||||||
|             f = open("/proc/stat","r") |             f = open("/proc/stat", "r") | ||||||
|         except: |         except: | ||||||
|             return 0 #Workaround, possibly we're on a Mac |             return 0  # Workaround, possibly we're on a Mac | ||||||
|         line = "" |         line = "" | ||||||
|         while not "cpu " in line: line=f.readline() |         while not "cpu " in line: | ||||||
|  |             line = f.readline() | ||||||
|         f.close() |         f.close() | ||||||
|         spl = line.split(" ") |         spl = line.split(" ") | ||||||
|         worktime = int(spl[2]) + int(spl[3]) + int(spl[4]) |         worktime = int(spl[2]) + int(spl[3]) + int(spl[4]) | ||||||
|         idletime = int(spl[5]) |         idletime = int(spl[5]) | ||||||
|         dworktime = (worktime - self.last_worktime) |         dworktime = worktime - self.last_worktime | ||||||
|         didletime = (idletime - self.last_idletime) |         didletime = idletime - self.last_idletime | ||||||
|         rate = float(dworktime) / (didletime+dworktime) |         rate = float(dworktime) / (didletime + dworktime) | ||||||
|         self.last_worktime = worktime |         self.last_worktime = worktime | ||||||
|         self.last_idletime = idletime |         self.last_idletime = idletime | ||||||
|         if (self.last_worktime==0): return 0 |         if self.last_worktime == 0: | ||||||
|  |             return 0 | ||||||
|         return rate |         return rate | ||||||
|  |  | ||||||
|     def add_client(self, c): |     def add_client(self, c): | ||||||
| @@ -514,11 +590,14 @@ class CpuUsageThread(threading.Thread): | |||||||
|         CpuUsageThread.sharedInstance = None |         CpuUsageThread.sharedInstance = None | ||||||
|         self.doRun = False |         self.doRun = False | ||||||
|  |  | ||||||
|  |  | ||||||
| class TooManyClientsException(Exception): | class TooManyClientsException(Exception): | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class ClientRegistry(object): | class ClientRegistry(object): | ||||||
|     sharedInstance = None |     sharedInstance = None | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def getSharedInstance(): |     def getSharedInstance(): | ||||||
|         if ClientRegistry.sharedInstance is None: |         if ClientRegistry.sharedInstance is None: | ||||||
|   | |||||||
| @@ -3,48 +3,76 @@ import hashlib | |||||||
| import json | import json | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class WebSocketConnection(object): | class WebSocketConnection(object): | ||||||
|     def __init__(self, handler, messageHandler): |     def __init__(self, handler, messageHandler): | ||||||
|         self.handler = handler |         self.handler = handler | ||||||
|         self.messageHandler = messageHandler |         self.messageHandler = messageHandler | ||||||
|         my_headers = self.handler.headers.items() |         my_headers = self.handler.headers.items() | ||||||
|         my_header_keys = list(map(lambda x:x[0],my_headers)) |         my_header_keys = list(map(lambda x: x[0], my_headers)) | ||||||
|         h_key_exists = lambda x:my_header_keys.count(x) |         h_key_exists = lambda x: my_header_keys.count(x) | ||||||
|         h_value = lambda x:my_headers[my_header_keys.index(x)][1] |         h_value = lambda x: my_headers[my_header_keys.index(x)][1] | ||||||
|         if (not h_key_exists("Upgrade")) or not (h_value("Upgrade")=="websocket") or (not h_key_exists("Sec-WebSocket-Key")): |         if ( | ||||||
|  |             (not h_key_exists("Upgrade")) | ||||||
|  |             or not (h_value("Upgrade") == "websocket") | ||||||
|  |             or (not h_key_exists("Sec-WebSocket-Key")) | ||||||
|  |         ): | ||||||
|             raise WebSocketException |             raise WebSocketException | ||||||
|         ws_key = h_value("Sec-WebSocket-Key") |         ws_key = h_value("Sec-WebSocket-Key") | ||||||
|         shakey = hashlib.sha1() |         shakey = hashlib.sha1() | ||||||
|         shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key = ws_key).encode()) |         shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode()) | ||||||
|         ws_key_toreturn = base64.b64encode(shakey.digest()) |         ws_key_toreturn = base64.b64encode(shakey.digest()) | ||||||
|         self.handler.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(ws_key_toreturn.decode()).encode()) |         self.handler.wfile.write( | ||||||
|  |             "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format( | ||||||
|  |                 ws_key_toreturn.decode() | ||||||
|  |             ).encode() | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def get_header(self, size, opcode): |     def get_header(self, size, opcode): | ||||||
|         ws_first_byte = 0b10000000 | (opcode & 0x0F) |         ws_first_byte = 0b10000000 | (opcode & 0x0F) | ||||||
|         if (size > 125): |         if size > 2 ** 16 - 1: | ||||||
|             return bytes([ws_first_byte, 126, (size>>8) & 0xff, size & 0xff]) |             # frame size can be increased up to 2^64 by setting the size to 127 | ||||||
|  |             # anything beyond that would need to be segmented into frames. i don't really think we'll need more. | ||||||
|  |             return bytes( | ||||||
|  |                 [ | ||||||
|  |                     ws_first_byte, | ||||||
|  |                     127, | ||||||
|  |                     (size >> 56) & 0xFF, | ||||||
|  |                     (size >> 48) & 0xFF, | ||||||
|  |                     (size >> 40) & 0xFF, | ||||||
|  |                     (size >> 32) & 0xFF, | ||||||
|  |                     (size >> 24) & 0xFF, | ||||||
|  |                     (size >> 16) & 0xFF, | ||||||
|  |                     (size >> 8) & 0xFF, | ||||||
|  |                     size & 0xFF, | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |         elif size > 125: | ||||||
|  |             # up to 2^16 can be sent using the extended payload size field by putting the size to 126 | ||||||
|  |             return bytes([ws_first_byte, 126, (size >> 8) & 0xFF, size & 0xFF]) | ||||||
|         else: |         else: | ||||||
|             # 256 bytes binary message in a single unmasked frame |             # 125 bytes binary message in a single unmasked frame | ||||||
|             return bytes([ws_first_byte, size]) |             return bytes([ws_first_byte, size]) | ||||||
|  |  | ||||||
|     def send(self, data): |     def send(self, data): | ||||||
|         # convenience |         # convenience | ||||||
|         if (type(data) == dict): |         if type(data) == dict: | ||||||
|             # allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway. |             # allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway. | ||||||
|             data = json.dumps(data, allow_nan = False) |             data = json.dumps(data, allow_nan=False) | ||||||
|  |  | ||||||
|         # string-type messages are sent as text frames |         # string-type messages are sent as text frames | ||||||
|         if (type(data) == str): |         if type(data) == str: | ||||||
|             header = self.get_header(len(data), 1) |             header = self.get_header(len(data), 1) | ||||||
|             data_to_send = header + data.encode('utf-8') |             data_to_send = header + data.encode("utf-8") | ||||||
|         # anything else as binary |         # anything else as binary | ||||||
|         else: |         else: | ||||||
|             header = self.get_header(len(data), 2) |             header = self.get_header(len(data), 2) | ||||||
|             data_to_send = header + data |             data_to_send = header + data | ||||||
|         written = self.handler.wfile.write(data_to_send) |         written = self.handler.wfile.write(data_to_send) | ||||||
|         if (written != len(data_to_send)): |         if written != len(data_to_send): | ||||||
|             logger.error("incomplete write! closing socket!") |             logger.error("incomplete write! closing socket!") | ||||||
|             self.close() |             self.close() | ||||||
|         else: |         else: | ||||||
| @@ -52,25 +80,25 @@ class WebSocketConnection(object): | |||||||
|  |  | ||||||
|     def read_loop(self): |     def read_loop(self): | ||||||
|         open = True |         open = True | ||||||
|         while (open): |         while open: | ||||||
|             header = self.handler.rfile.read(2) |             header = self.handler.rfile.read(2) | ||||||
|             opcode = header[0] & 0x0F |             opcode = header[0] & 0x0F | ||||||
|             length = header[1] & 0x7F |             length = header[1] & 0x7F | ||||||
|             mask = (header[1] & 0x80) >> 7 |             mask = (header[1] & 0x80) >> 7 | ||||||
|             if (length == 126): |             if length == 126: | ||||||
|                 header = self.handler.rfile.read(2) |                 header = self.handler.rfile.read(2) | ||||||
|                 length = (header[0] << 8) + header[1] |                 length = (header[0] << 8) + header[1] | ||||||
|             if (mask): |             if mask: | ||||||
|                 masking_key = self.handler.rfile.read(4) |                 masking_key = self.handler.rfile.read(4) | ||||||
|             data = self.handler.rfile.read(length) |             data = self.handler.rfile.read(length) | ||||||
|             if (mask): |             if mask: | ||||||
|                 data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) |                 data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) | ||||||
|             if (opcode == 1): |             if opcode == 1: | ||||||
|                 message = data.decode('utf-8') |                 message = data.decode("utf-8") | ||||||
|                 self.messageHandler.handleTextMessage(self, message) |                 self.messageHandler.handleTextMessage(self, message) | ||||||
|             elif (opcode == 2): |             elif opcode == 2: | ||||||
|                 self.messageHandler.handleBinaryMessage(self, data) |                 self.messageHandler.handleBinaryMessage(self, data) | ||||||
|             elif (opcode == 8): |             elif opcode == 8: | ||||||
|                 open = False |                 open = False | ||||||
|                 self.messageHandler.handleClose(self) |                 self.messageHandler.handleClose(self) | ||||||
|             else: |             else: | ||||||
|   | |||||||
							
								
								
									
										277
									
								
								owrx/wsjt.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								owrx/wsjt.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,277 @@ | |||||||
|  | import threading | ||||||
|  | import wave | ||||||
|  | from datetime import datetime, timedelta, date, timezone | ||||||
|  | import time | ||||||
|  | import sched | ||||||
|  | import subprocess | ||||||
|  | import os | ||||||
|  | from multiprocessing.connection import Pipe | ||||||
|  | from owrx.map import Map, LocatorLocation | ||||||
|  | import re | ||||||
|  | from owrx.config import PropertyManager | ||||||
|  | from owrx.bands import Bandplan | ||||||
|  | from owrx.metrics import Metrics | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class WsjtChopper(threading.Thread): | ||||||
|  |     def __init__(self, source): | ||||||
|  |         self.source = source | ||||||
|  |         self.tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"] | ||||||
|  |         (self.wavefilename, self.wavefile) = self.getWaveFile() | ||||||
|  |         self.switchingLock = threading.Lock() | ||||||
|  |         self.scheduler = sched.scheduler(time.time, time.sleep) | ||||||
|  |         self.fileQueue = [] | ||||||
|  |         (self.outputReader, self.outputWriter) = Pipe() | ||||||
|  |         self.doRun = True | ||||||
|  |         super().__init__() | ||||||
|  |  | ||||||
|  |     def getWaveFile(self): | ||||||
|  |         filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format( | ||||||
|  |             tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.fileTimestampFormat) | ||||||
|  |         ) | ||||||
|  |         wavefile = wave.open(filename, "wb") | ||||||
|  |         wavefile.setnchannels(1) | ||||||
|  |         wavefile.setsampwidth(2) | ||||||
|  |         wavefile.setframerate(12000) | ||||||
|  |         return (filename, wavefile) | ||||||
|  |  | ||||||
|  |     def getNextDecodingTime(self): | ||||||
|  |         t = datetime.now() | ||||||
|  |         zeroed = t.replace(minute=0, second=0, microsecond=0) | ||||||
|  |         delta = t - zeroed | ||||||
|  |         seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval | ||||||
|  |         t = zeroed + timedelta(seconds=seconds) | ||||||
|  |         logger.debug("scheduling: {0}".format(t)) | ||||||
|  |         return t.timestamp() | ||||||
|  |  | ||||||
|  |     def startScheduler(self): | ||||||
|  |         self._scheduleNextSwitch() | ||||||
|  |         threading.Thread(target=self.scheduler.run).start() | ||||||
|  |  | ||||||
|  |     def emptyScheduler(self): | ||||||
|  |         for event in self.scheduler.queue: | ||||||
|  |             self.scheduler.cancel(event) | ||||||
|  |  | ||||||
|  |     def _scheduleNextSwitch(self): | ||||||
|  |         self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles) | ||||||
|  |  | ||||||
|  |     def switchFiles(self): | ||||||
|  |         self.switchingLock.acquire() | ||||||
|  |         file = self.wavefile | ||||||
|  |         filename = self.wavefilename | ||||||
|  |         (self.wavefilename, self.wavefile) = self.getWaveFile() | ||||||
|  |         self.switchingLock.release() | ||||||
|  |  | ||||||
|  |         file.close() | ||||||
|  |         self.fileQueue.append(filename) | ||||||
|  |         self._scheduleNextSwitch() | ||||||
|  |  | ||||||
|  |     def decoder_commandline(self, file): | ||||||
|  |         """ | ||||||
|  |         must be overridden in child classes | ||||||
|  |         """ | ||||||
|  |         return [] | ||||||
|  |  | ||||||
|  |     def decode(self): | ||||||
|  |         def decode_and_unlink(file): | ||||||
|  |             decoder = subprocess.Popen(self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir) | ||||||
|  |             while True: | ||||||
|  |                 line = decoder.stdout.readline() | ||||||
|  |                 if line is None or (isinstance(line, bytes) and len(line) == 0): | ||||||
|  |                     break | ||||||
|  |                 self.outputWriter.send(line) | ||||||
|  |             rc = decoder.wait() | ||||||
|  |             if rc != 0: | ||||||
|  |                 logger.warning("decoder return code: %i", rc) | ||||||
|  |             os.unlink(file) | ||||||
|  |  | ||||||
|  |         if self.fileQueue: | ||||||
|  |             file = self.fileQueue.pop() | ||||||
|  |             logger.debug("processing file {0}".format(file)) | ||||||
|  |             threading.Thread(target=decode_and_unlink, args=[file]).start() | ||||||
|  |  | ||||||
|  |     def run(self) -> None: | ||||||
|  |         logger.debug("WSJT chopper starting up") | ||||||
|  |         self.startScheduler() | ||||||
|  |         while self.doRun: | ||||||
|  |             data = self.source.read(256) | ||||||
|  |             if data is None or (isinstance(data, bytes) and len(data) == 0): | ||||||
|  |                 self.doRun = False | ||||||
|  |             else: | ||||||
|  |                 self.switchingLock.acquire() | ||||||
|  |                 self.wavefile.writeframes(data) | ||||||
|  |                 self.switchingLock.release() | ||||||
|  |  | ||||||
|  |             self.decode() | ||||||
|  |         logger.debug("WSJT chopper shutting down") | ||||||
|  |         self.outputReader.close() | ||||||
|  |         self.outputWriter.close() | ||||||
|  |         self.emptyScheduler() | ||||||
|  |         try: | ||||||
|  |             os.unlink(self.wavefilename) | ||||||
|  |         except Exception: | ||||||
|  |             logger.exception("error removing undecoded file") | ||||||
|  |  | ||||||
|  |     def read(self): | ||||||
|  |         try: | ||||||
|  |             return self.outputReader.recv() | ||||||
|  |         except EOFError: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Ft8Chopper(WsjtChopper): | ||||||
|  |     def __init__(self, source): | ||||||
|  |         self.interval = 15 | ||||||
|  |         self.fileTimestampFormat = "%y%m%d_%H%M%S" | ||||||
|  |         super().__init__(source) | ||||||
|  |  | ||||||
|  |     def decoder_commandline(self, file): | ||||||
|  |         # TODO expose decoding quality parameters through config | ||||||
|  |         return ["jt9", "--ft8", "-d", "3", file] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class WsprChopper(WsjtChopper): | ||||||
|  |     def __init__(self, source): | ||||||
|  |         self.interval = 120 | ||||||
|  |         self.fileTimestampFormat = "%y%m%d_%H%M" | ||||||
|  |         super().__init__(source) | ||||||
|  |  | ||||||
|  |     def decoder_commandline(self, file): | ||||||
|  |         # TODO expose decoding quality parameters through config | ||||||
|  |         return ["wsprd", "-d", file] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Jt65Chopper(WsjtChopper): | ||||||
|  |     def __init__(self, source): | ||||||
|  |         self.interval = 60 | ||||||
|  |         self.fileTimestampFormat = "%y%m%d_%H%M" | ||||||
|  |         super().__init__(source) | ||||||
|  |  | ||||||
|  |     def decoder_commandline(self, file): | ||||||
|  |         # TODO expose decoding quality parameters through config | ||||||
|  |         return ["jt9", "--jt65", "-d", "3", file] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Jt9Chopper(WsjtChopper): | ||||||
|  |     def __init__(self, source): | ||||||
|  |         self.interval = 60 | ||||||
|  |         self.fileTimestampFormat = "%y%m%d_%H%M" | ||||||
|  |         super().__init__(source) | ||||||
|  |  | ||||||
|  |     def decoder_commandline(self, file): | ||||||
|  |         # TODO expose decoding quality parameters through config | ||||||
|  |         return ["jt9", "--jt9", "-d", "3", file] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Ft4Chopper(WsjtChopper): | ||||||
|  |     def __init__(self, source): | ||||||
|  |         self.interval = 7.5 | ||||||
|  |         self.fileTimestampFormat = "%y%m%d_%H%M%S" | ||||||
|  |         super().__init__(source) | ||||||
|  |  | ||||||
|  |     def decoder_commandline(self, file): | ||||||
|  |         # TODO expose decoding quality parameters through config | ||||||
|  |         return ["jt9", "--ft4", "-d", "3", file] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class WsjtParser(object): | ||||||
|  |     locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") | ||||||
|  |     wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") | ||||||
|  |  | ||||||
|  |     def __init__(self, handler): | ||||||
|  |         self.handler = handler | ||||||
|  |         self.dial_freq = None | ||||||
|  |         self.band = None | ||||||
|  |  | ||||||
|  |     modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"} | ||||||
|  |  | ||||||
|  |     def parse(self, data): | ||||||
|  |         try: | ||||||
|  |             msg = data.decode().rstrip() | ||||||
|  |             # known debug messages we know to skip | ||||||
|  |             if msg.startswith("<DecodeFinished>"): | ||||||
|  |                 return | ||||||
|  |             if msg.startswith(" EOF on input file"): | ||||||
|  |                 return | ||||||
|  |  | ||||||
|  |             modes = list(WsjtParser.modes.keys()) | ||||||
|  |             if msg[21] in modes or msg[19] in modes: | ||||||
|  |                 out = self.parse_from_jt9(msg) | ||||||
|  |             else: | ||||||
|  |                 out = self.parse_from_wsprd(msg) | ||||||
|  |  | ||||||
|  |             self.handler.write_wsjt_message(out) | ||||||
|  |         except ValueError: | ||||||
|  |             logger.exception("error while parsing wsjt message") | ||||||
|  |  | ||||||
|  |     def parse_timestamp(self, instring, dateformat): | ||||||
|  |         ts = datetime.strptime(instring, dateformat) | ||||||
|  |         return int(datetime.combine(date.today(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000) | ||||||
|  |  | ||||||
|  |     def parse_from_jt9(self, msg): | ||||||
|  |         # ft8 sample | ||||||
|  |         # '222100 -15 -0.0  508 ~  CQ EA7MJ IM66' | ||||||
|  |         # jt65 sample | ||||||
|  |         # '2352  -7  0.4 1801 #  R0WAS R2ABM KO85' | ||||||
|  |         # '0003  -4  0.4 1762 #  CQ R2ABM KO85' | ||||||
|  |         modes = list(WsjtParser.modes.keys()) | ||||||
|  |         if msg[19] in modes: | ||||||
|  |             dateformat = "%H%M" | ||||||
|  |         else: | ||||||
|  |             dateformat = "%H%M%S" | ||||||
|  |         timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat) | ||||||
|  |         msg = msg[len(dateformat) + 1 :] | ||||||
|  |         modeChar = msg[14:15] | ||||||
|  |         mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" | ||||||
|  |         wsjt_msg = msg[17:53].strip() | ||||||
|  |         self.parseLocator(wsjt_msg, mode) | ||||||
|  |         Metrics.getSharedInstance().pushDecodes(self.band, mode) | ||||||
|  |         return { | ||||||
|  |             "timestamp": timestamp, | ||||||
|  |             "db": float(msg[0:3]), | ||||||
|  |             "dt": float(msg[4:8]), | ||||||
|  |             "freq": int(msg[9:13]), | ||||||
|  |             "mode": mode, | ||||||
|  |             "msg": wsjt_msg, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     def parseLocator(self, msg, mode): | ||||||
|  |         m = WsjtParser.locator_pattern.match(msg) | ||||||
|  |         if m is None: | ||||||
|  |             return | ||||||
|  |         # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very | ||||||
|  |         # likely this just means roger roger goodbye. | ||||||
|  |         if m.group(2) == "RR73": | ||||||
|  |             return | ||||||
|  |         Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), mode, self.band) | ||||||
|  |  | ||||||
|  |     def parse_from_wsprd(self, msg): | ||||||
|  |         # wspr sample | ||||||
|  |         # '2600 -24  0.4   0.001492 -1  G8AXA JO01 33' | ||||||
|  |         # '0052 -29  2.6   0.001486  0  G02CWT IO92 23' | ||||||
|  |         wsjt_msg = msg[29:].strip() | ||||||
|  |         self.parseWsprMessage(wsjt_msg) | ||||||
|  |         Metrics.getSharedInstance().pushDecodes(self.band, "WSPR") | ||||||
|  |         return { | ||||||
|  |             "timestamp": self.parse_timestamp(msg[0:4], "%H%M"), | ||||||
|  |             "db": float(msg[5:8]), | ||||||
|  |             "dt": float(msg[9:13]), | ||||||
|  |             "freq": float(msg[14:24]), | ||||||
|  |             "drift": int(msg[25:28]), | ||||||
|  |             "mode": "WSPR", | ||||||
|  |             "msg": wsjt_msg, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     def parseWsprMessage(self, msg): | ||||||
|  |         m = WsjtParser.wspr_splitter_pattern.match(msg) | ||||||
|  |         if m is None: | ||||||
|  |             return | ||||||
|  |         Map.getSharedInstance().updateLocation(m.group(1), LocatorLocation(m.group(2)), "WSPR", self.band) | ||||||
|  |  | ||||||
|  |     def setDialFrequency(self, freq): | ||||||
|  |         self.dial_freq = freq | ||||||
|  |         self.band = Bandplan.getSharedInstance().findBand(freq) | ||||||
							
								
								
									
										3
									
								
								sdrhu.py
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								sdrhu.py
									
									
									
									
									
								
							| @@ -23,10 +23,9 @@ | |||||||
| from owrx.sdrhu import SdrHuUpdater | from owrx.sdrhu import SdrHuUpdater | ||||||
| from owrx.config import PropertyManager | from owrx.config import PropertyManager | ||||||
|  |  | ||||||
| if __name__=="__main__": | if __name__ == "__main__": | ||||||
|     pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") |     pm = PropertyManager.getSharedInstance().loadConfig("config_webrx") | ||||||
|  |  | ||||||
|     if not "sdrhu_key" in pm: |     if not "sdrhu_key" in pm: | ||||||
|         exit(1) |         exit(1) | ||||||
|     SdrHuUpdater().update() |     SdrHuUpdater().update() | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Jakob Ketterl
					Jakob Ketterl