diff --git a/README.md b/README.md index 1794031..a799180 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,44 @@ -openwebrx +OpenWebRX ========= -Open source web-based SDR receiver software +OpenWebRX is a multi-user SDR receiver software with a web interface. + +![OpenWebRX](/screenshot.jpg?raw=true) + +It has the following features: + +- libcsdr based demodulators (AM/FM/SSB), +- filter bandwith, BFO, PBS 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 works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28), +- currently only supports RTL-SDR, but other SDR hardware may be easily added. + +## Setup + +OpenWebRX currently requires a Linux machine to run. + +First you will need to install the dependencies: +- libcsdr +- rtl-sdr + +After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: + + python openwebrx.py + +You can now open the GUI at http://localhost:8073. + +Please note that it is also listening on the following ports (on localhost only): +- port 8888 for the I/Q source, +- port 4951 for the multi-user I/Q server. + +Now the next step is to customize the parameters of your server in `config_webrx.py`. + +Actually, if you do something cool with OpenWebRX (or just have a problem), please drop me a mail: Andras Retzler, HA7ILM >randras@sdr.hu<. +I would like to maintain a list of online amateur radio receivers on sdr.hu. + +## Usage tips + +The filter envelope can be dragged at its ends and moved. + +However, if you hold the shift key, you can drag the center line (BFO) or the passband (PBS). diff --git a/config_rtl.py b/config_rtl.py new file mode 100755 index 0000000..c759978 --- /dev/null +++ b/config_rtl.py @@ -0,0 +1,87 @@ +''' +This file is part of RTL Multi-User Server, + that makes multi-user access to your DVB-T dongle used as an SDR. +Copyright (c) 2013-2014 by Andras Retzler + +RTL Multi-User Server is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +RTL Multi-User Server 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with RTL Multi-User Server. If not, see . +''' + +my_ip='127.0.0.1' # leave blank for listening on all interfaces +my_listening_port = 4951 + +rtl_tcp_host,rtl_tcp_port='localhost',8888 + +send_first="" +#send_first=chr(9)+chr(0)+chr(0)+chr(0)+chr(1) # set direct sampling + +setuid_on_start = 0 # we normally start with root privileges and setuid() to another user +uid = 999 # determine by issuing: $ id -u username +ignore_clients_without_commands = 1 # we won't serve data to telnet sessions and things like that + # we'll start to serve data after getting the first valid command + +freq_allowed_ranges = [[0,2200000000]] + +client_cant_set_until=0 +first_client_can_set=True # openwebrx - spectrum thread will set things on start # no good, clients set parameters and things +buffer_size=25000000 # per client +log_file_path = "/dev/null" # Might be set to /dev/null to turn off logging + +''' +Allow any host to connect: + use_ip_access_control=0 + +Allow from specific ranges: + use_ip_access_control=1 + order_allow_deny=0 # deny and then allow + denied_ip_ranges=() # deny from all + allowed_ip_ranges=('192.168.','44.','127.0.0.1') # allow only from ... + +Deny from specific ranges: + use_ip_access_control=1 + order_allow_deny=0 # allow and then deny + allowed_ip_ranges=() # allow from all + denied_ip_ranges=('192.168.') # deny any hosts from ... +''' +use_ip_access_control=1 #You may want to open up the I/Q server to the public, then set this to zero. +order_allow_deny=0 +denied_ip_ranges=() # deny from all +allowed_ip_ranges=('127.0.0.1') # allow only local connections (from openwebrx). +allow_gain_set=1 + +use_dsp_command=False # you can process raw I/Q data with a custom command that starts a process that we can pipe the data into, and also pipe out of. +debug_dsp_command=False # show sample rate before and after the dsp command +dsp_command="" + +''' +Example DSP commands: + * Compress I/Q data with FLAC: + flac --force-raw-format --channels 2 --sample-rate=250000 --sign=unsigned --bps=8 --endian=little -o - - + * Decompress FLAC-coded I/Q data: + flac --force-raw-format --decode --endian=little --sign=unsigned - - +''' +watchdog_interval=1.5 +reconnect_interval=10 +''' +If there's no input I/Q data after N seconds, input will be filled with zero samples, +so that GNU Radio won't fail in openwebrx. It may reconnect rtl_tcp_tread. +If watchdog_interval is 0, then watchdog thread is not started. + +''' +cache_full_behaviour=2 +''' + 0 = drop samples + 1 = close client + 2 = openwebrx: don't care about that client until it wants samples again (gr-osmosdr bug workaround) +''' + diff --git a/config_webrx.py b/config_webrx.py new file mode 100755 index 0000000..2565f54 --- /dev/null +++ b/config_webrx.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +""" +config_webrx: configuration options for OpenWebRX + +OpenWebRX (c) Copyright 2013-2014 Andras Retzler + +This file is part of OpenWebRX. + + OpenWebRX is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenWebRX 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenWebRX. If not, see . + +""" +#Server settings +web_port=8073 +server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket) + +#Web GUI configuration +receiver_name="[Callsign]" +receiver_location="Budapest, Hungary" +receiver_qra="JN97ML" +receiver_asl=182 +receiver_ant="Longwire" +receiver_device="RTL-SDR" +receiver_admin="localhost@localhost" +receiver_gps=(47.000000,19.000000) +photo_height=350 +photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory" +photo_desc=""" +You can add your own background photo and receiver information.
+Receiver is operated by: %[RX_ADMIN]
+Device: %[RX_DEVICE]
+Antenna: %[RX_ANT]
+Website: http://localhost +""" + +#DSP/RX settings +dsp_plugin="csdr" +fft_fps=9 +fft_size=4096 +samp_rate = 250000 +center_freq = 145525000 +rf_gain = 5 + +start_rtl_thread=True #rtl_sdr is more stable than rtl_tcp... +start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} - | nc -vvl 127.0.0.1 8888".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) +#start_rtl_tcp_command="rtl_tcp -s 250000 -f 145525000 -g 0 -p 8888" +#You can use other SDR hardware as well, but if the command above outputs samples in a format other than [unsigned char], then the dsp plugin has to be slightly modified (at the csdr convert_u8_f part). + diff --git a/htdocs/favicon.ico b/htdocs/favicon.ico new file mode 100644 index 0000000..f8c9a2a Binary files /dev/null and b/htdocs/favicon.ico differ diff --git a/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf b/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf new file mode 100644 index 0000000..dfc87f8 Binary files /dev/null and b/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf differ diff --git a/htdocs/gfx/font-expletus-sans/OFL.txt b/htdocs/gfx/font-expletus-sans/OFL.txt new file mode 100644 index 0000000..5979654 --- /dev/null +++ b/htdocs/gfx/font-expletus-sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2011, Jasper de Waard (jasper@designtown.nl), +with Reserved Font Name "Expletus Sans". +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/htdocs/gfx/openwebrx-avatar-background.png b/htdocs/gfx/openwebrx-avatar-background.png new file mode 100755 index 0000000..e52cb0b Binary files /dev/null and b/htdocs/gfx/openwebrx-avatar-background.png differ diff --git a/htdocs/gfx/openwebrx-avatar.png b/htdocs/gfx/openwebrx-avatar.png new file mode 100755 index 0000000..91486a6 Binary files /dev/null and b/htdocs/gfx/openwebrx-avatar.png differ diff --git a/htdocs/gfx/openwebrx-background-cool-blue.png b/htdocs/gfx/openwebrx-background-cool-blue.png new file mode 100755 index 0000000..7430bd8 Binary files /dev/null and b/htdocs/gfx/openwebrx-background-cool-blue.png differ diff --git a/htdocs/gfx/openwebrx-background-lingrad.png b/htdocs/gfx/openwebrx-background-lingrad.png new file mode 100755 index 0000000..48537f7 Binary files /dev/null and b/htdocs/gfx/openwebrx-background-lingrad.png differ diff --git a/htdocs/gfx/openwebrx-logo-big.png b/htdocs/gfx/openwebrx-logo-big.png new file mode 100755 index 0000000..dcafb2e Binary files /dev/null and b/htdocs/gfx/openwebrx-logo-big.png differ diff --git a/htdocs/gfx/openwebrx-rx-details-arrow-up.png b/htdocs/gfx/openwebrx-rx-details-arrow-up.png new file mode 100755 index 0000000..0baccd0 Binary files /dev/null and b/htdocs/gfx/openwebrx-rx-details-arrow-up.png differ diff --git a/htdocs/gfx/openwebrx-rx-details-arrow.png b/htdocs/gfx/openwebrx-rx-details-arrow.png new file mode 100755 index 0000000..9995118 Binary files /dev/null and b/htdocs/gfx/openwebrx-rx-details-arrow.png differ diff --git a/htdocs/gfx/openwebrx-scale-background.png b/htdocs/gfx/openwebrx-scale-background.png new file mode 100755 index 0000000..7fbb4d2 Binary files /dev/null and b/htdocs/gfx/openwebrx-scale-background.png differ diff --git a/htdocs/gfx/openwebrx-top-logo.png b/htdocs/gfx/openwebrx-top-logo.png new file mode 100755 index 0000000..4772425 Binary files /dev/null and b/htdocs/gfx/openwebrx-top-logo.png differ diff --git a/htdocs/gfx/openwebrx-top-photo.jpg b/htdocs/gfx/openwebrx-top-photo.jpg new file mode 100755 index 0000000..cf521c7 Binary files /dev/null and b/htdocs/gfx/openwebrx-top-photo.jpg differ diff --git a/htdocs/gfx/webrx-ha5kfu-top-logo.png b/htdocs/gfx/webrx-ha5kfu-top-logo.png new file mode 100755 index 0000000..2686eef Binary files /dev/null and b/htdocs/gfx/webrx-ha5kfu-top-logo.png differ diff --git a/htdocs/index.wrx b/htdocs/index.wrx new file mode 100755 index 0000000..7cf73ba --- /dev/null +++ b/htdocs/index.wrx @@ -0,0 +1,93 @@ + + + + + OpenWebRX | Open Source Web-based SDR for everyone! + + + + + + +
+
+
+ +
%[RX_PHOTO_TITLE]
+
%[RX_PHOTO_DESC]
+
+
+
+ + + + +
%[RX_TITLE]
+
%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, [maps]
+
+ + +
+
+
+
+
+ +
+
+
+ +
+
+
+
---.--- MHz
+
---.--- MHz
+ +
FM
+
AM
+
LSB
+
USB
+
CW
+
+
+
+
openwebrx.js (beta) client log
+ Author: HA7ILM. Please send me bug reports and suggestions.
+ Client status: +
+ + Your client ID is: %[CLIENT_ID]
+
+
+
+
+ Under construction +
We're working on the code right now, so the application might fail. +
+
+
+
+ + diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css new file mode 100755 index 0000000..15993fc --- /dev/null +++ b/htdocs/openwebrx.css @@ -0,0 +1,433 @@ +/* +OpenWebRX (c) Copyright 2013-2014 Andras Retzler + +This file is part of OpenWebRX. + + OpenWebRX is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenWebRX 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenWebRX. If not, see . +*/ + +html, body +{ + margin: 0; + padding: 0; + height: 100%; + font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; + overflow: hidden; +} + +#webrx-top-container +{ + position: relative; + z-index:1000; +} + +.webrx-top-bar-parts +{ + position: absolute; + top: 0px; + left: 0px; + width:100%; + height:67px; +} + +#webrx-top-bar-background +{ + background-color: #808080; + opacity: 0.15; + filter:alpha(opacity=15); +} + +#webrx-top-bar +{ + margin:0; + padding:0; +} + + +#webrx-top-logo +{ + position: absolute; + top: 12px; + left: 15px; +} + +#webrx-ha5kfu-top-logo +{ + position: absolute; + top: 19px; + right: 15px; +} + +#webrx-top-photo +{ + width: 100%; + display: block; +} + +#webrx-rx-avatar-background +{ + cursor:pointer; + position: absolute; + left: 285px; + top: 6px; +} + +#webrx-rx-avatar +{ + cursor:pointer; + position: absolute; + left: 289px; + top: 10px; + width: 46px; + height: 46px; +} + +#webrx-top-photo-clip +{ + max-height: 350px; + overflow: hidden; + position: relative; +} + +/*#webrx-bottom-bar +{ + position: absolute; + bottom: 0px; + width: 100%; + height: 117px; + background-image:url(gfx/webrx-bottom-bar.png); +}*/ + +#webrx-page-container +{ + min-height:100%; + position:relative; +} + +/*#webrx-photo-gradient-left +{ + position: absolute; + bottom: 0px; + left: 0px; + background-image:url(gfx/webrx-photo-gradient-corner.png); + width: 59px; + height: 92px; + +} + +#webrx-photo-gradient-middle +{ + position: absolute; + bottom: 0px; + left: 59px; + right: 59px; + height: 92px; + background-image:url(gfx/webrx-photo-gradient-middle.png); +} + +#webrx-photo-gradient-right +{ + position: absolute; + bottom: 0px; + right: 0px; + background-image:url(gfx/webrx-photo-gradient-corner.png); + width: 59px; + height: 92px; + -webkit-transform:scaleX(-1); + -moz-transform:scaleX(-1); + -ms-transform:scaleX(-1); + -o-transform:scaleX(-1); + transform:scaleX(-1); +}*/ + +#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-title +{ + white-space:nowrap; + overflow: hidden; + cursor:pointer; + position: absolute; + left: 350px; + top: 13px; + 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; + position: absolute; + left: 350px; + top: 34px; +} + +#webrx-rx-desc a +{ + color: #909090; + /*text-decoration: none;*/ +} + +#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 +{ + height: 47px; + background-image: url("gfx/openwebrx-scale-background.png"); + background-repeat: repeat-x; + overflow: hidden; + z-index:1000; + position: relative; +} + +#webrx-canvas-container +{ + /*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/ + position: relative; + height: 2000px; + overflow-y: scroll; + overflow-x: hidden; + /*background-color: #646464;*/ + /*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-repeat: no-repeat; + background-color: #1e5f7f; + cursor: crosshair; +} + +#webrx-canvas-container canvas +{ + position: absolute; + border-style: none; +} + +#openwebrx-phantom-canvas +{ + position: absolute; + width: 0px; + height: 0px; +} + +/*#openwebrx-canvas-gradient-background +{ + overflow: hidden; + width: 100%; + height: 396px; +}*/ + +/*#webrx-debugdiv +{ + font-size: 10pt; + /*overflow-y:scroll;*/ +/*}*/ + +#webrx-main-container +{ + position: relative; + width: 100%; + margin: 0; + padding: 0; +} + +.webrx-error +{ + font-weight: bold; + color: #ff6262; +} + +#openwebrx-problems span +{ + background: #ff6262; + padding: 3px; + font-size: 8pt; + color: white; + font-weight: bold; + border-radius: 4px; + -moz-border-radius: 4px; + margin: 0px 2px 0px 2px; +} + +/*#webrx-freq-show +{ + visibility: hidden; + position: absolute; + top: 0px; + left: 0px; + padding: 5px; + font-weight: bold; + border-radius: 10px; + -moz-border-radius: 10px; + background-color: #999999; + color: White; + z-index:9999; /*should be higher? + +}*/ + +/* removed non-free fonts like that: */ +/*@font-face { + font-family: 'unibody_8_pro_regregular'; + src: url('gfx/unibody8pro-regular-webfont.eot'); + src: url('gfx/unibody8pro-regular-webfont.ttf'); + font-weight: normal; + font-style: normal; +}*/ + +@font-face { + font-family: 'expletus-sans-medium'; + src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf'); + font-weight: normal; + font-style: normal; +} + +#webrx-actual-freq +{ + width: 100%; + text-align: left; + font-size: 16pt; + font-family: 'expletus-sans-medium'; + padding: 0; + margin: 0; + line-height:22px; + +} + +#webrx-mouse-freq +{ + width: 100%; + text-align: left; + font-size: 10pt; + color: #AAA; + font-family: 'expletus-sans-medium'; + margin-bottom: 5px; +} + +.openwebrx-panel +{ + visibility: hidden; + background-color: #575757; + padding: 10px; + color: white; + position: fixed; + font-size: 10pt; + border-radius: 15px; + -moz-border-radius: 15px; +} + +.openwebrx-panel a +{ + color: #5ca8ff; + text-shadow: none; +} + +.openwebrx-panel-inner +{ + overflow-y: auto; + overflow-x: hidden; + height: 100%; +} + +.openwebrx-button +{ + background-color: #373737; + padding: 5px; + border-radius: 5px; + -moz-border-radius: 5px; + color: White; + font-weight: bold; + width: auto; + float: left; + margin-right: 5px; + cursor: pointer; + background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) ); + background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% ); +} + +.openwebrx-button:hover +{ + /*background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #3F3F3F), color-stop(1, #777777) ); + background:-moz-linear-gradient( center top, #373737 5%, #4F4F4F 100% );*/ + background: #474747; + color: #FFFF50; +} + +.openwebrx-button:active +{ + background: #777777; + color: #FFFF50; +} + +#openwebrx-client-log-title +{ + margin-bottom: 5px; + font-weight: bold; +} diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js new file mode 100755 index 0000000..381da83 --- /dev/null +++ b/htdocs/openwebrx.js @@ -0,0 +1,1634 @@ +/* + +OpenWebRX (c) Copyright 2013-2014 Andras Retzler + +This file is part of OpenWebRX. + + OpenWebRX is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenWebRX 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenWebRX. If not, see . + +*/ + +is_firefox=navigator.userAgent.indexOf("Firefox")!=-1; + +function arrayBufferToString(buf) { + //http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers + return String.fromCharCode.apply(null, new Uint8Array(buf)); +} + +function getFirstChars(buf, num) +{ + var u8buf=new Uint8Array(buf); + var output=String(); + num=Math.min(num,u8buf.length); + for(i=0;i9||unit!="px") new_val=(to+accel*remain); + else {if(Math.abs(remain)<2) new_val=to; + else new_val=to+remain-(remain/Math.abs(remain));} + object.style[style_name]=new_val.toString()+unit; + } + } + else + {object.style[style_name]=to.toString()+unit; window.clearInterval(object.anim_timer); delete object.anim_timer; } + if(to_exec!=0) to_exec(); + },1000/fps); +} + +function animate_to(object,style_name,unit,to,accel,time_ms,fps,to_exec) +{ + from=parseFloat(style_value(object,style_name)); + animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec); +} + + +// ======================================================== +// ================ DEMODULATOR ROUTINES ================ +// ======================================================== + +demodulators=[] + +demodulator_color_index=0; +demodulator_colors=["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"] +function demodulators_get_next_color() +{ + if(demodulator_color_index>=demodulator_colors.length) demodulator_color_index=0; + return(demodulator_colors[demodulator_color_index++]); +} + +function demod_envelope_draw(range, from, to, color, line) +{ // ____ + // Draws a standard filter envelope like this: _/ \_ + // Parameters are given in offset frequency (Hz). + // Envelope is drawn on the scale canvas. + // A "drag range" object is returned, containing information about the draggable areas of the envelope + // (beginning, ending and the line showing the offset frequency). + if(typeof color == "undefined") color="#ffff00"; //yellow + env_bounding_line_w=5; // + env_att_w=5; // _______ ___env_h2 in px ___|_____ + env_h1=17; // _/| \_ ___env_h1 in px _/ |_ \_ + env_h2=5; // |||env_att_line_w |_env_lineplus + env_lineplus=1; // ||env_bounding_line_w + env_line_click_area=6; + //range=get_visible_freq_range(); + from_px=scale_px_from_freq(from,range); + to_px=scale_px_from_freq(to,range); + if(to_pxwindow.innerWidth)) // out of screen? + { + drag_ranges.beginning={x1:from_px, x2: from_px+env_bounding_line_w+env_att_w}; + drag_ranges.ending={x1:to_px-env_bounding_line_w-env_att_w, x2: to_px}; + drag_ranges.whole_envelope={x1:from_px, x2: to_px}; + drag_ranges.envelope_on_screen=true; + scale_ctx.beginPath(); + scale_ctx.moveTo(from_px,env_h1); + scale_ctx.lineTo(from_px+env_bounding_line_w, env_h1); + scale_ctx.lineTo(from_px+env_bounding_line_w+env_att_w, env_h2); + scale_ctx.lineTo(to_px-env_bounding_line_w-env_att_w, env_h2); + scale_ctx.lineTo(to_px-env_bounding_line_w, env_h1); + scale_ctx.lineTo(to_px, env_h1); + scale_ctx.globalAlpha = 0.3; + scale_ctx.fill(); + scale_ctx.globalAlpha = 1; + scale_ctx.stroke(); + } + if(typeof line != "undefined") // out of screen? + { + line_px=scale_px_from_freq(line,range); + if(!(line_px<0||line_px>window.innerWidth)) + { + drag_ranges.line={x1:line_px-env_line_click_area/2, x2: line_px+env_line_click_area/2}; + drag_ranges.line_on_screen=true; + scale_ctx.moveTo(line_px,env_h1+env_lineplus); + scale_ctx.lineTo(line_px,env_h2-env_lineplus); + scale_ctx.stroke(); + } + } + return drag_ranges; +} + +function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) +{ // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). + in_range=function(x,range) { return range.x1<=x&&range.x2>=x; } + dr=demodulator.draggable_ranges; + + if(key_modifiers.shiftKey) + { + //Check first: shift + center drag emulates BFO knob + if(drag_ranges.line_on_screen&&in_range(x,drag_ranges.line)) return dr.bfo; + //Check second: shift + envelope drag emulates PBF knob + if(drag_ranges.envelope_on_screen&&in_range(x,drag_ranges.whole_envelope)) return dr.pbs; + } + if(drag_ranges.envelope_on_screen) + { + // For low and high cut: + if(in_range(x,drag_ranges.beginning)) return dr.beginning; + if(in_range(x,drag_ranges.ending)) return dr.ending; + // Last priority: having clicked anything else on the envelope, without holding the shift key + if(in_range(x,drag_ranges.whole_envelope)) return dr.anything_else; + } + return dr.none; //User doesn't drag the envelope for this demodulator +} + +//******* class demodulator ******* +// this can be used as a base class for ANY demodulator +demodulator=function(offset_frequency) +{ + //console.log("this too"); + this.offset_frequency=offset_frequency; + this.has_audio_output=true; + this.has_text_output=false; + this.envelope={}; + this.color=demodulators_get_next_color(); + this.stop=function(){}; +} +//ranges on filter envelope that can be dragged: +demodulator.draggable_ranges={none: 0, beginning:1 /*from*/, ending: 2 /*to*/, anything_else: 3, bfo: 4 /*line (while holding shift)*/, pbs: 5 } //to which parameter these correspond in demod_envelope_draw() + +//******* class demodulator_default_analog ******* +// This can be used as a base for basic audio demodulators. +// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB + +demodulator_response_time=100; +//in ms; if we don't limit the number of SETs sent to the server, audio will underrun (possibly output buffer is cleared on SETs in GNU Radio + +function demodulator_default_analog(offset_frequency,subtype) +{ + //console.log("hopefully this happens"); + //http://stackoverflow.com/questions/4152931/javascript-inheritance-call-super-constructor-or-use-prototype-chain + demodulator.call(this,offset_frequency); + this.subtype=subtype; + this.filter={ + min_passband: 100, + high_cut_limit: audio_context.sampleRate/2, + low_cut_limit: -audio_context.sampleRate/2 + }; + //Subtypes only define some filter parameters and the mod string sent to server, + //so you may set these parameters in your custom child class. + //Why? As of demodulation is done on the server, difference is mainly on the server side. + this.server_mod=subtype; + if(subtype=="lsb") + { + this.low_cut=-3000; + this.high_cut=-300; + this.server_mod="ssb"; + } + else if(subtype=="usb") + { + this.low_cut=300; + this.high_cut=3000; + this.server_mod="ssb"; + } + else if(subtype=="cw") + { + this.low_cut=700; + this.high_cut=900; + this.server_mod="ssb"; + } + else if(subtype=="nfm") + { + this.low_cut=-4000; + this.high_cut=4000; + } + else if(subtype=="am") + { + this.low_cut=-4000; + this.high_cut=4000; + } + + this.wait_for_timer=false; + this.set_after=false; + this.set=function() + { //set() is a wrapper to call doset(), but it ensures that doset won't execute more frequently than demodulator_response_time. + if(!this.wait_for_timer) + { + this.doset(false); + this.set_after=false; + this.wait_for_timer=true; + timeout_this=this; //http://stackoverflow.com/a/2130411 + window.setTimeout(function() { + timeout_this.wait_for_timer=false; + if(timeout_this.set_after) timeout_this.set(); + },demodulator_response_time); + } + else + { + this.set_after=true; + } + } + + this.doset=function(first_time) + { //this function sends demodulator parameters to the server + ws.send("SET"+((first_time)?" mod="+this.server_mod:"")+ + " low_cut="+this.low_cut.toString()+" high_cut="+this.high_cut.toString()+ + " offset_freq="+this.offset_frequency.toString()); + } + this.doset(true); //we set parameters on object creation + + //******* envelope object ******* + // for drawing the filter envelope above scale + this.envelope.parent=this; + + this.envelope.draw=function(visible_range) + { + this.visible_range=visible_range; + this.drag_ranges=demod_envelope_draw(range, + center_freq+this.parent.offset_frequency+this.parent.low_cut, + center_freq+this.parent.offset_frequency+this.parent.high_cut, + this.color,center_freq+this.parent.offset_frequency); + }; + + // event handlers + this.envelope.drag_start=function(x, key_modifiers) + { + this.key_modifiers=key_modifiers; + this.dragged_range=demod_envelope_where_clicked(x,this.drag_ranges, key_modifiers); + //console.log("dragged_range: "+this.dragged_range.toString()); + this.drag_origin={ + x: x, + low_cut: this.parent.low_cut, + high_cut: this.parent.high_cut, + offset_frequency: this.parent.offset_frequency + }; + return this.dragged_range!=demodulator.draggable_ranges.none; + }; + + this.envelope.drag_move=function(x) + { + dr=demodulator.draggable_ranges; + if(this.dragged_range==dr.none) return false; // we return if user is not dragging (us) at all + freq_change=Math.round(this.visible_range.hps*(x-this.drag_origin.x)); + /*if(this.dragged_range==dr.beginning||this.dragged_range==dr.ending) + { + //we don't let the passband be too small + if(this.parent.low_cut+new_freq_change<=this.parent.high_cut-this.parent.filter.min_passband) this.freq_change=new_freq_change; + else return; + } + var new_value;*/ + + //dragging the line in the middle of the filter envelope while holding Shift does emulate + //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged + //Filter passband moves in the opposite direction than dragged, hence the minus below. + minus=(this.dragged_range==dr.bfo)?-1:1; + //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob + //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset + //frequency. + if(this.dragged_range==dr.beginning||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) + { + //we don't let low_cut go beyond its limits + if((new_value=this.drag_origin.low_cut+minus*freq_change)=this.parent.high_cut) return true; + this.parent.low_cut=new_value; + } + if(this.dragged_range==dr.ending||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) + { + //we don't let high_cut go beyond its limits + if((new_value=this.drag_origin.high_cut+minus*freq_change)>this.parent.filter.high_cut_limit) return true; + //nor the filter passband be too small + if(new_value-this.parent.low_cutbandwidth/2||new_value<-bandwidth/2) return true; //we don't allow tuning above Nyquist frequency :-) + this.parent.offset_frequency=new_value; + } + //now do the actual modifications: + mkenvelopes(this.visible_range); + this.parent.set(); + //will have to change this when changing to multi-demodulator mode: + e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+this.parent.offset_frequency,1e6,4); + return true; + }; + + this.envelope.drag_end=function(x) + { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. + to_return=this.dragged_range!=demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset + this.dragged_range=demodulator.draggable_ranges.none; + return to_return; + }; + +} + +demodulator_default_analog.prototype=new demodulator(); + +function mkenvelopes(visible_range) //called from mkscale +{ + scale_ctx.clearRect(0,0,scale_ctx.canvas.width,22); //clear the upper part of the canvas (where filter envelopes reside) + for (var i=0;ibandwidth/2||to_what<-bandwidth/2) return; + demodulators[0].offset_frequency=Math.round(to_what); + demodulators[0].set(); + mkenvelopes(get_visible_freq_range()); +} + + +// ======================================================== +// =================== SCALE ROUTINES =================== +// ======================================================== + +var scale_ctx; +var scale_canvas; + +function scale_setup() +{ + e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(window.innerWidth/2),1e6,4); + scale_canvas=e("openwebrx-scale-canvas"); + scale_ctx=scale_canvas.getContext("2d"); + scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false); + scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); + scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); + resize_scale(); +} + +var scale_canvas_drag_params={ + mouse_down: false, + drag: false, + start_x: 0, + key_modifiers: {shiftKey:false, altKey: false, ctrlKey: false} +}; + +function scale_canvas_mousedown(evt) +{ + with(scale_canvas_drag_params) + { + mouse_down=true; + drag=false; + start_x=evt.pageX; + key_modifiers.shiftKey=evt.shiftKey; + key_modifiers.altKey=evt.altKey; + key_modifiers.ctrlKey=evt.ctrlKey; + } + evt.preventDefault(); +} + +function scale_offset_freq_from_px(x, visible_range) +{ + if(typeof visible_range === "undefined") visible_range=get_visible_freq_range(); + return (visible_range.start+visible_range.bw*(x/canvas_container.clientWidth))-center_freq; +} + +function scale_canvas_mousemove(evt) +{ + var event_handled; + if(scale_canvas_drag_params.mouse_down&&!scale_canvas_drag_params.drag&&Math.abs(evt.pageX-scale_canvas_drag_params.start_x)>canvas_drag_min_delta) + //we can use the main drag_min_delta thing of the main canvas + { + scale_canvas_drag_params.drag=true; + //call the drag_start for all demodulators (and they will decide if they're dragged, based on X coordinate) + for (var i=0;i=scale_min_space_bw_small_markers&&freq.toString()[0]!="5") {out.small/=2; out.ratio*=2; } + out.smallbw=freq/out.ratio; + return true; + } + for(i=scale_markers_levels.length-1;i>=0;i--) + { + mp=scale_markers_levels[i]; + if (!fcalc(mp.large_marker_per_hz)) continue; + //console.log(mp.large_marker_per_hz); + //console.log(out); + if (out.large-mp.estimated_text_width>scale_min_space_bw_texts) break; + } + out.params=mp; + return out; +} + +function mkscale() +{ + //clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes): + range=get_visible_freq_range(); + mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too + scale_ctx.clearRect(0,22,scale_ctx.canvas.width,scale_ctx.canvas.height-22); + scale_ctx.strokeStyle = "#fff"; + scale_ctx.font = "bold 11px sans-serif"; + scale_ctx.textBaseline = "top"; + scale_ctx.fillStyle = "#fff"; + spacing=get_scale_mark_spacing(range); + //console.log(spacing); + marker_hz=Math.ceil(range.start/spacing.smallbw)*spacing.smallbw; + text_h_pos=22+10+((is_firefox)?3:0); + var text_to_draw; + var ftext=function(f) {text_to_draw=format_frequency(spacing.params.format,f,spacing.params.pre_divide,spacing.params.decimals);} + var last_large; + for(;;) + { + var x=scale_px_from_freq(marker_hz,range); + if(x>window.innerWidth) break; + scale_ctx.beginPath(); + scale_ctx.moveTo(x, 22); + if(marker_hz%spacing.params.large_marker_per_hz==0) + { //large marker + if(typeof first_large == "undefined") var first_large=marker_hz; + last_large=marker_hz; + scale_ctx.lineWidth=3.5; + scale_ctx.lineTo(x,22+11); + ftext(marker_hz); + var text_measured=scale_ctx.measureText(text_to_draw); + scale_ctx.textAlign = "center"; + //advanced text drawing begins + if(zoom_level==0&&range.start+spacing.smallbw*spacing.ratio>marker_hz) + { //if this is the first overall marker when zoomed out + if(x=scale_min_space_bw_texts) + { //and if we have enough space to draw it correctly without clipping + scale_ctx.textAlign = "left"; + scale_ctx.fillText(text_to_draw, 0, text_h_pos); + } + } + } + else if(zoom_level==0&&range.end-spacing.smallbw*spacing.ratiowindow.innerWidth-text_measured.width/2) + { //and if it would be clipped off the screen + if(window.innerWidth-text_measured.width-scale_px_from_freq(marker_hz-spacing.smallbw*spacing.ratio,range)>=scale_min_space_bw_texts) + { //and if we have enough space to draw it correctly without clipping + scale_ctx.textAlign = "right"; + scale_ctx.fillText(text_to_draw, window.innerWidth, text_h_pos); + } + } + } + else scale_ctx.fillText(text_to_draw, x, text_h_pos); //draw text normally + } + else + { //small marker + scale_ctx.lineWidth=2; + scale_ctx.lineTo(x,22+8); + } + marker_hz+=spacing.smallbw; + scale_ctx.stroke(); + } + if(zoom_level!=0) + { // if zoomed, we don't want the texts to disappear because their markers can't be seen + // on the left side + scale_ctx.textAlign = "center"; + var f=first_large-spacing.smallbw*spacing.ratio; + var x=scale_px_from_freq(f,range); + ftext(f); + var w=scale_ctx.measureText(text_to_draw).width; + if(x+w/2>0) scale_ctx.fillText(text_to_draw, x, 22+10); + // on the right side + f=last_large+spacing.smallbw*spacing.ratio; + x=scale_px_from_freq(f,range); + ftext(f); + w=scale_ctx.measureText(text_to_draw).width; + if(x-w/23) + { + out=out.substr(0,at)+","+out.substr(at); + at+=4; + decimals-=3; + } + return out; +} + +canvas_drag=false; +canvas_drag_min_delta=1; +canvas_mouse_down=false; + +function canvas_mousedown(evt) +{ + canvas_mouse_down=true; + canvas_drag=false; + canvas_drag_last_x=canvas_drag_start_x=evt.pageX; + canvas_drag_last_y=canvas_drag_start_y=evt.pageY; + evt.preventDefault(); //don't show text selection mouse pointer +} + +function canvas_mousemove(evt) +{ + if(!waterfall_setup_done) return; + //element=e("webrx-freq-show"); + relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; + /*realX=(relativeX-element.clientWidth/2); + maxX=(canvases[0].clientWidth-element.clientWidth); + if(realX>maxX) realX=maxX; + if(realX<0) realX=0; + element.style.left=realX.toString()+"px";*/ + if(canvas_mouse_down) + { + if(!canvas_drag&&Math.abs(evt.pageX-canvas_drag_start_x)>canvas_drag_min_delta) + { + canvas_drag=true; + canvas_container.style.cursor="move"; + } + if(canvas_drag) + { + var deltaX=canvas_drag_last_x-evt.pageX; + var deltaY=canvas_drag_last_y-evt.pageY; + //zoom_center_where=zoom_center_where_calc(evt.pageX); + var dpx=range.hps*deltaX; + if( + !(zoom_center_rel+dpx>(bandwidth/2-canvas_container.clientWidth*(1-zoom_center_where)*range.hps)) && + !(zoom_center_rel+dpx<-bandwidth/2+canvas_container.clientWidth*zoom_center_where*range.hps) + ) { zoom_center_rel+=dpx; } +// -((canvases_new_width*(0.5+zoom_center_rel/bandwidth))-(winsize*zoom_center_where)); + resize_canvases(false); + canvas_drag_last_x=evt.pageX; + canvas_drag_last_y=evt.pageY; + mkscale(); + } + } + else e("webrx-mouse-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); +} + +function canvas_container_mouseout(evt) +{ + canvas_end_drag(); +} + +//function body_mouseup() { canvas_end_drag(); console.log("body_mouseup"); } +//function window_mouseout() { canvas_end_drag(); console.log("document_mouseout"); } + +function canvas_mouseup(evt) +{ + if(!waterfall_setup_done) return; + relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; + + if(!canvas_drag) + { + //ws.send("SET offset_freq="+canvas_get_freq_offset(relativeX).toString()); + //e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); + demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX)); + } + else + { + canvas_end_drag(); + } + canvas_mouse_down=false; +} + +function canvas_end_drag() +{ + canvas_container.style.cursor="crosshair"; + canvas_mouse_down=false; +} + +function zoom_center_where_calc(screenposX) +{ + //return (screenposX-(window.innerWidth-canvas_container.clientWidth))/canvas_container.clientWidth; + return screenposX/canvas_container.clientWidth; +} + +function canvas_mousewheel(evt) +{ + if(!waterfall_setup_done) return; + //var i=Math.abs(evt.wheelDelta); + //var dir=(i/evt.wheelDelta)<0; + //console.log(evt); + var relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; + var dir=(evt.deltaY/Math.abs(evt.deltaY))>0; + console.log(dir); + //i/=120; + /*while (i--)*/ zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); + evt.preventDefault(); + //evt.returnValue = false; //disable scrollbar move +} + + +zoom_max_level_hps=33; //Hz/pixel +zoom_levels_count=5; + +function get_zoom_coeff_from_hps(hps) +{ + var shown_bw=(window.innerWidth*hps); + return bandwidth/shown_bw; +} + +zoom_levels=[1]; +zoom_level=0; +zoom_freq=0; +zoom_offset_px=0; +zoom_center_rel=0; +zoom_center_where=0; + +function mkzoomlevels() +{ + zoom_levels=[1]; + maxc=get_zoom_coeff_from_hps(zoom_max_level_hps); + if(maxc<1) return; + for(i=1;i=zoom_levels_count-1)) return; + + if(out) --zoom_level; + else ++zoom_level; + zoom_center_rel=canvas_get_freq_offset(where); + //console.log("zoom_step || zlevel: "+zoom_level.toString()+" zlevel_val: "+zoom_levels[zoom_level].toString()+" zoom_center_rel: "+zoom_center_rel.toString()); + zoom_center_where=onscreen; + resize_canvases(true); + mkscale(); +} + +function zoom_calc() +{ + winsize=canvas_container.clientWidth; + var canvases_new_width=winsize*zoom_levels[zoom_level]; + zoom_offset_px=-((canvases_new_width*(0.5+zoom_center_rel/bandwidth))-(winsize*zoom_center_where)); + if(zoom_offset_px>0) zoom_offset_px=0; + if(zoom_offset_pxaudio_buffering_fill_to) audio_init() + } + else if(firstChars=="FFT") + { + //alert("Yupee! Doing FFT"); + var floatArray = new Float32Array(evt.data,4); + waterfall_add_queue(floatArray); + } else if(firstChars=="MSG") + { + /*try + {*/ + var stringData=arrayBufferToString(evt.data); + params=stringData.substring(4).split(" "); + for(i=0;ifft_fps/2) //in case of emergency + { + add_problem("fft overflow"); + while(waterfall_queue.length) waterfall_add(waterfall_queue.shift()); + } +} + +function on_ws_opened() +{ + ws.send("SERVER DE CLIENT openwebrx.js"); + divlog("WebSocket opened to "+ws_url); +} + +function divlog(what, is_error) +{ + if(typeof is_error !== undefined && is_error == 1) what=""+what+""; + e("openwebrx-debugdiv").innerHTML+=what+"
"; +} + +var audio_context; +var audio_initialized=0; + +var audio_received = Array(); +var audio_buffer_index = 0; +var audio_resampler; +var audio_node; +//var audio_received_sample_rate = 48000; +var audio_input_buffer_size; + +// Optimalise these if audio lags or is choppy: +var audio_buffer_size = 8192;//2048 was choppy +var audio_buffer_maximal_length_sec=1.7; //actual number of samples are calculated from sample rate +var audio_flush_interval_ms=250; //the interval in which audio_flush() is called + +var audio_prepared_buffers = Array(); +var audio_last_output_buffer = new Float32Array(audio_buffer_size); +var audio_last_output_offset = 0; +var audio_buffering = false; +var audio_buffering_fill_to=10; //on audio underrun we wait until this n*audio_buffer_size samples are present + +function audio_prepare(data) +{ + //console.log("audio_prepare :: "+data.length.toString()); + //console.log("data.len = "+data.length.toString()); + var dopush=function() + { + audio_prepared_buffers.push(audio_last_output_buffer); + audio_last_output_offset=0; + audio_last_output_buffer=new Float32Array(audio_buffer_size); + audio_buffer_current_count_debug++; + }; + + if(data.length==0) return; + if(audio_last_output_offset+data.length<=audio_buffer_size) + { //array fits into output buffer + for(var i=0;iaudio_buffering_fill_to) audio_buffering=false; +} + +if (!AudioBuffer.prototype.copyToChannel) +{ //Chrome 36 does not have it, Firefox does + AudioBuffer.prototype.copyToChannel=function(input,channel) //input is Float32Array + { + var cd=this.getChannelData(channel); + for(var i=0;iread_remain) + { + for (i=audio_buffer_index; i"+read_remain.toString()+" obi="+obi.toString()+"\n"; + audio_buffer_index+=read_remain; + break; + } + else + { + for (i=audio_buffer_index; iaudio_buffer_maximal_length) + { + add_problem("audio overrun"); + audio_received.splice(0,audio_received.length-audio_buffer_maximal_length); + } + else*/ + audio_received.splice(0,1); + //debug_str+="added remain, remain="+read_remain.toString()+" abi="+audio_buffer_index.toString()+" alen="+int_buffer.length.toString()+" i="+i.toString()+" arecva="+audio_received.length.toString()+" obi="+obi.toString()+"\n"; + audio_buffer_index = 0; + if(audio_received.length == 0 || read_remain == 0) return; + int_buffer = audio_received[0]; + } + } + //debug_str+="obi="+obi.toString(); + //alert(debug_str); +} + +function audio_flush_notused() +{ + if (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate) + { + add_problem("audio overrun"); + console.log("audio_flush() :: size: "+audio_buffer_current_size.toString()+" allowed: "+(audio_buffer_maximal_length_sec*audio_context.sampleRate).toString()); + while (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate*0.5) + { + audio_buffer_current_size-=audio_received[0].length; + audio_received.splice(0,1); + } + } +} + +function webrx_set_param(what, value) +{ + ws.send("SET "+what+"="+value.toString()); +} + +function audio_init() +{ + //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js + audio_initialized=1; // only tell on_ws_recv() not to call it again + try + { + window.AudioContext = window.AudioContext||window.webkitAudioContext; + audio_context = new AudioContext(); + } + catch(e) + { + divlog('Your browser does not support Web Audio API, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.', 1); + } + + //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor + createjsnode_function = (audio_context.createJavaScriptNode == undefined)?audio_context.createScriptProcessor.bind(audio_context):audio_context.createJavaScriptNode.bind(audio_context); + audio_node = createjsnode_function(audio_buffer_size, 0, 1); + audio_node.onaudioprocess = audio_onprocess; + audio_node.connect(audio_context.destination); + // --- Resampling --- + //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js + //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); + //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); + webrx_set_param("audio_rate",audio_context.sampleRate); //Don't try to resample + window.setInterval(audio_flush,audio_flush_interval_ms); + divlog('Web Audio API succesfully initialized, sample rate: '+audio_context.sampleRate.toString()+ " sps"); + /*audio_source=audio_context.createBufferSource(); + audio_buffer = audio_context.createBuffer(xhr.response, false); + audio_source.buffer = buffer; + audio_source.noteOn(0);*/ + demodulator_analog_replace('nfm'); //needs audio_context.sampleRate to exist +} + +function on_ws_closed() +{ + try + { + audio_node.disconnect(); + } + catch (dont_care) {} + divlog("WebSocket has closed unexpectedly. Please reload the page.", 1); +} + +function on_ws_error(event) +{ + divlog("WebSocket error.",1); +} + +function open_websocket() +{ + if (!("WebSocket" in window)) + divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); + ws = new WebSocket(ws_url+client_id); + ws.onopen = on_ws_opened; + ws.onmessage = on_ws_recv; + ws.onclose = on_ws_closed; + ws.binaryType = "arraybuffer"; + window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript + ws.onclose = function () {}; + ws.close(); + }; + ws.onerror = on_ws_error; +} + +//var color_scale=[0xFFFFFFFF, 0x000000FF]; +//var color_scale=[0x000000FF, 0x000000FF, 0x3a0090ff, 0x10c400ff, 0xffef00ff, 0xff5656ff]; +//var color_scale=[0x000000FF, 0x000000FF, 0x534b37ff, 0xcedffaff, 0x8899a9ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]; + +//var color_scale=[ 0x000000FF, 0xff5656ff, 0xffffffff]; + +//2014-04-22 +var color_scale=[0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]; + +function waterfall_mkcolor(db_value) +{ + min_value=-100; //in dB + max_value=10 + if(db_valuemax_value) db_value=max_value + full_scale=max_value-min_value; + relative_value=db_value-min_value; + value_percent=relative_value/full_scale; + percent_for_one_color=1/(color_scale.length-1); + index=Math.floor(value_percent/percent_for_one_color); + remain=(value_percent-percent_for_one_color*index)/percent_for_one_color; + return color_between(color_scale[index+1],color_scale[index],remain); +} + +function color_between(first, second, percent) +{ + output=0; + for(i=0;i<4;i++) + { + add = ((((first&(0xff<<(i*8)))>>>0)*percent) + (((second&(0xff<<(i*8)))>>>0)*(1-percent))) & (0xff<<(i*8)); + output |= add>>>0; + } + return output>>>0; +} + + +var canvas_context; +var canvases = []; +var canvas_default_height = 200; +var canvas_container; +var canvas_phantom; + +function add_canvas() +{ + new_canvas = document.createElement("canvas"); + new_canvas.width=fft_size; + new_canvas.height=canvas_default_height; + canvas_actual_line=canvas_default_height-1; + new_canvas.style.width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; + new_canvas.style.left=zoom_offset_px.toString()+"px"; + new_canvas.style.height=canvas_default_height.toString()+"px"; + new_canvas.openwebrx_top=(-canvas_default_height+1); + new_canvas.style.top=new_canvas.openwebrx_top.toString()+"px"; + canvas_context = new_canvas.getContext("2d"); + canvas_container.appendChild(new_canvas); + new_canvas.addEventListener("mouseover", canvas_mouseover, false); + new_canvas.addEventListener("mouseout", canvas_mouseout, false); + new_canvas.addEventListener("mousemove", canvas_mousemove, false); + new_canvas.addEventListener("mouseup", canvas_mouseup, false); + new_canvas.addEventListener("mousedown", canvas_mousedown, false); + new_canvas.addEventListener("wheel",canvas_mousewheel, false); + canvases.push(new_canvas); +} + +function init_canvas_container() +{ + canvas_container=e("webrx-canvas-container"); + canvas_container.addEventListener("mouseout",canvas_container_mouseout, false); + //window.addEventListener("mouseout",window_mouseout,false); + //document.body.addEventListener("mouseup",body_mouseup,false); + canvas_phantom=e("openwebrx-phantom-canvas"); + canvas_phantom.addEventListener("mouseover", canvas_mouseover, false); + canvas_phantom.addEventListener("mouseout", canvas_mouseout, false); + canvas_phantom.addEventListener("mousemove", canvas_mousemove, false); + canvas_phantom.addEventListener("mouseup", canvas_mouseup, false); + canvas_phantom.addEventListener("mousedown", canvas_mousedown, false); + canvas_phantom.addEventListener("wheel",canvas_mousewheel, false); + canvas_phantom.style.width=canvas_container.clientWidth+"px"; + add_canvas(); +} + +canvas_maxshift=0; + +function shift_canvases() +{ + canvases.forEach(function(p) + { + p.style.top=(p.openwebrx_top++).toString()+"px"; + }); + canvas_maxshift++; + if(canvas_container.clientHeight>canvas_maxshift) + { + canvas_phantom.style.top=canvas_maxshift.toString()+"px"; + canvas_phantom.style.height=(canvas_container.clientHeight-canvas_maxshift).toString()+"px"; + canvas_phantom.style.display="block"; + } + else + canvas_phantom.style.display="none"; + + + //canvas_container.style.height=(((canvases.length-1)*canvas_default_height)+(canvas_default_height-canvas_actual_line)).toString()+"px"; + //canvas_container.style.height="100%"; +} + +function resize_canvases(zoom) +{ + if(typeof zoom == "undefined") zoom=false; + if(!zoom) mkzoomlevels(); + zoom_calc(); + new_width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; + var zoom_value=zoom_offset_px.toString()+"px"; + canvases.forEach(function(p) + { + p.style.width=new_width; + p.style.left=zoom_value; + }); + canvas_phantom.style.width=new_width; + canvas_phantom.style.left=zoom_value; +} + +function waterfall_init() +{ + init_canvas_container(); + waterfall_timer = window.setInterval(waterfall_dequeue,900/fft_fps); + resize_waterfall_container(false); /* then */ resize_canvases(); + scale_setup(); + mkzoomlevels(); + waterfall_setup_done=1; +} + +var waterfall_dont_scale=0; + +function waterfall_add(data) +{ + if(!waterfall_setup_done) return; + var w=fft_size; + + //waterfall_shift(); + // ==== do scaling if required ==== + /*if(waterfall_dont_scale) + { + scaled=data; + for(i=scaled.length;i1) + { + scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); + remain--; + } + else + { + j++; + scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); + remain=pixel_per_point-(1-remain); + } + } + + } + else + { //make line smaller (linear decimation, moving average) + point_per_pixel=(to-from)/w; + scaled=Array(); + j=0; + remain=point_per_pixel; + last_pixel=0; + for(i=from; i1) + { + last_pixel+=data[i]; + remain--; + } + else + { + last_pixel+=data[i]*remain; + scaled[j++]=last_pixel/point_per_pixel; + last_pixel=data[i]*(1-remain); + remain=point_per_pixel-(1-remain); //? + } + } + } + } + + //Add line to waterfall image + base=(h-1)*w*4; + for(x=0;x>>0)>>((3-i)*8))&0xff; + }*/ + + //Add line to waterfall image + oneline_image = canvas_context.createImageData(w,1); + for(x=0;x>>0)>>((3-i)*8))&0xff; + } + + + //Draw image + canvas_context.putImageData(oneline_image, 0, canvas_actual_line--); + shift_canvases(); + if(canvas_actual_line<0) add_canvas(); + //divlog("Drawn FFT"); +} + +/* +function waterfall_shift() +{ + w=canvas.width; + h=canvas.height; + for(y=0; ytl.offsetLeft-20) tl.style.display="none"; + else tl.style.display="block"; +} + +function openwebrx_resize() +{ + resize_canvases(); + resize_waterfall_container(true); + resize_scale(); + check_top_bar_congestion(); +} + +function openwebrx_init() +{ + init_rx_photo(); + open_websocket(); + place_panels(); + window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); + window.addEventListener("resize",openwebrx_resize); +} + +/* +window.setInterval(function(){ + sum=0; + for(i=0;i=(c=c.charCodeAt(0)+13)?c:c-26);}); + window.location.href="mailto:"+what; +}*/ + +var rt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c<="Z"?90:122)>=(c=c.charCodeAt(0)+n)?c:c-26);});} +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); } + +var audio_debug_time_taken=0; + +function debug_audio() +{ + audio_debug_time_taken+=1; + e("openwebrx-audio-sps").innerHTML= + "audio recv. at "+audio_buffer_current_size_debug.toString()+" sps ("+ + (audio_buffer_all_size_debug/audio_debug_time_taken).toFixed(1)+" sps avg.), feed at "+ + ((audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken).toFixed(1)+" sps output"; + audio_buffer_current_size_debug=0; +} + +// ======================================================== +// ======================= PANELS ======================= +// ======================================================== + +panel_margin=10; + +function pop_bottommost_panel(from) +{ + min_order=parseInt(from[0].dataset.panelOrder); + min_index=0; + for(i=0;i0) + { + p=pop_bottommost_panel(left_col); + p.style.left="0px"; + p.style.bottom=y.toString()+"px"; + p.style.visibility="visible"; + y+=p.openwebrxPanelHeight+3*panel_margin; + } + y=0; + while(right_col.length>0) + { + p=pop_bottommost_panel(right_col); + p.style.right="10px"; + p.style.bottom=y.toString()+"px"; + p.style.visibility="visible"; + y+=p.openwebrxPanelHeight+3*panel_margin; + } +} + diff --git a/htdocs/openwebrx.js~ b/htdocs/openwebrx.js~ new file mode 100755 index 0000000..bb92e15 --- /dev/null +++ b/htdocs/openwebrx.js~ @@ -0,0 +1,1538 @@ +/* + +OpenWebRX (c) Copyright 2013 Andras Retzler + +This file is part of OpenWebRX. + + OpenWebRX is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenWebRX 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenWebRX. If not, see . + +*/ + +function arrayBufferToString(buf) { + //http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers + return String.fromCharCode.apply(null, new Uint8Array(buf)); +} + +//Chrome console: +//ws.send("SET center_freq=100000000") + +var bandwidth; +var center_freq; +var audio_buffer_current_size_debug=0; +var audio_buffer_current_size=0; +var fft_size; +var fft_fps; +var waterfall_setup_done=0; +var waterfall_queue = []; +var waterfall_timer; + +/*function fade(something,from,to,time_ms,fps) +{ + something.style.opacity=from; + something.fade_i=0; + n_of_iters=time_ms/(1000/fps); + change=(to-from)/(n_of_iters-1); + + something.fade_timer=window.setInterval( + function(){ + if(something.fade_i++9||unit!="px") new_val=(to+accel*remain); + else {if(Math.abs(remain)<2) new_val=to; + else new_val=to+remain-(remain/Math.abs(remain));} + object.style[style_name]=new_val.toString()+unit; + } + } + else + {object.style[style_name]=to.toString()+unit; window.clearInterval(object.anim_timer); delete object.anim_timer; } + if(to_exec!=0) to_exec(); + },1000/fps); +} + +function animate_to(object,style_name,unit,to,accel,time_ms,fps,to_exec) +{ + from=parseFloat(style_value(object,style_name)); + animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec); +} + + +// ======================================================== +// ================ DEMODULATOR ROUTINES ================ +// ======================================================== + +demodulators=[] + +demodulator_color_index=0; +demodulator_colors=["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"] +function demodulators_get_next_color() +{ + if(demodulator_color_index>=demodulator_colors.length) demodulator_color_index=0; + return(demodulator_colors[demodulator_color_index++]); +} + +function demod_envelope_draw(range, from, to, color, line) +{ // ____ + // Draws a standard filter envelope like this: _/ \_ + // Parameters are given in offset frequency (Hz). + // Envelope is drawn on the scale canvas. + // A "drag range" object is returned, containing information about the draggable areas of the envelope + // (beginning, ending and the line showing the offset frequency). + if(typeof color == "undefined") color="#ffff00"; //yellow + env_bounding_line_w=5; // + env_att_w=5; // _______ ___env_h2 in px ___|_____ + env_h1=17; // _/| \_ ___env_h1 in px _/ |_ \_ + env_h2=5; // |||env_att_line_w |_env_lineplus + env_lineplus=1; // ||env_bounding_line_w + env_line_click_area=6; + //range=get_visible_freq_range(); + from_px=scale_px_from_freq(from,range); + to_px=scale_px_from_freq(to,range); + if(to_pxwindow.innerWidth)) // out of screen? + { + drag_ranges.beginning={x1:from_px, x2: from_px+env_bounding_line_w+env_att_w}; + drag_ranges.ending={x1:to_px-env_bounding_line_w-env_att_w, x2: to_px}; + drag_ranges.whole_envelope={x1:from_px, x2: to_px}; + drag_ranges.envelope_on_screen=true; + scale_ctx.beginPath(); + scale_ctx.moveTo(from_px,env_h1); + scale_ctx.lineTo(from_px+env_bounding_line_w, env_h1); + scale_ctx.lineTo(from_px+env_bounding_line_w+env_att_w, env_h2); + scale_ctx.lineTo(to_px-env_bounding_line_w-env_att_w, env_h2); + scale_ctx.lineTo(to_px-env_bounding_line_w, env_h1); + scale_ctx.lineTo(to_px, env_h1); + scale_ctx.globalAlpha = 0.3; + scale_ctx.fill(); + scale_ctx.globalAlpha = 1; + scale_ctx.stroke(); + } + if(typeof line != "undefined") // out of screen? + { + line_px=scale_px_from_freq(line,range); + if(!(line_px<0||line_px>window.innerWidth)) + { + drag_ranges.line={x1:line_px-env_line_click_area/2, x2: line_px+env_line_click_area/2}; + drag_ranges.line_on_screen=true; + scale_ctx.moveTo(line_px,env_h1+env_lineplus); + scale_ctx.lineTo(line_px,env_h2-env_lineplus); + scale_ctx.stroke(); + } + } + return drag_ranges; +} + +function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) +{ // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). + in_range=function(x,range) { return range.x1<=x&&range.x2>=x; } + dr=demodulator.draggable_ranges; + + if(key_modifiers.shiftKey) + { + //Check first: shift + center drag emulates BFO knob + if(drag_ranges.line_on_screen&&in_range(x,drag_ranges.line)) return dr.bfo; + //Check second: shift + envelope drag emulates PBF knob + if(drag_ranges.envelope_on_screen&&in_range(x,drag_ranges.whole_envelope)) return dr.pbs; + } + if(drag_ranges.envelope_on_screen) + { + // For low and high cut: + if(in_range(x,drag_ranges.beginning)) return dr.beginning; + if(in_range(x,drag_ranges.ending)) return dr.ending; + // Last priority: having clicked anything else on the envelope, without holding the shift key + if(in_range(x,drag_ranges.whole_envelope)) return dr.anything_else; + } + return dr.none; //User doesn't drag the envelope for this demodulator +} + +//******* class demodulator ******* +// this can be used as a base class for ANY demodulator +demodulator=function(offset_frequency) +{ + //console.log("this too"); + this.offset_frequency=offset_frequency; + this.has_audio_output=true; + this.has_text_output=false; + this.envelope={}; + this.color=demodulators_get_next_color(); + this.stop=function(){}; +} +//ranges on filter envelope that can be dragged: +demodulator.draggable_ranges={none: 0, beginning:1 /*from*/, ending: 2 /*to*/, anything_else: 3, bfo: 4 /*line (while holding shift)*/, pbs: 5 } //to which parameter these correspond in demod_envelope_draw() + +//******* class demodulator_default_analog ******* +// This can be used as a base for basic audio demodulators. +// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB + +demodulator_response_time=100; +//in ms; if we don't limit the number of SETs sent to the server, audio will underrun (possibly output buffer is cleared on SETs in GNU Radio + +function demodulator_default_analog(offset_frequency,subtype) +{ + //console.log("hopefully this happens"); + //http://stackoverflow.com/questions/4152931/javascript-inheritance-call-super-constructor-or-use-prototype-chain + demodulator.call(this,offset_frequency); + this.subtype=subtype; + this.filter={ + min_passband: 100, + high_cut_limit: audio_context.sampleRate/2, + low_cut_limit: -audio_context.sampleRate/2 + }; + //Subtypes only define some filter parameters and the mod string sent to server, + //so you may set these parameters in your custom child class. + //Why? As of demodulation is done on the server, difference is mainly on the server side. + this.server_mod=subtype; + if(subtype=="lsb") + { + this.low_cut=-3000; + this.high_cut=-300; + this.server_mod="ssb"; + } + else if(subtype=="usb") + { + this.low_cut=300; + this.high_cut=3000; + this.server_mod="ssb"; + } + else if(subtype=="cw") + { + this.low_cut=700; + this.high_cut=900; + this.server_mod="ssb"; + } + else if(subtype=="nfm") + { + this.low_cut=-4000; + this.high_cut=4000; + } + else if(subtype=="am") + { + this.low_cut=-4000; + this.high_cut=4000; + } + + this.wait_for_timer=false; + this.set_after=false; + this.set=function() + { //set() is a wrapper to call doset(), but it ensures that doset won't execute more frequently than demodulator_response_time. + if(!this.wait_for_timer) + { + this.doset(false); + this.set_after=false; + this.wait_for_timer=true; + timeout_this=this; //http://stackoverflow.com/a/2130411 + window.setTimeout(function() { + timeout_this.wait_for_timer=false; + if(timeout_this.set_after) timeout_this.set(); + },demodulator_response_time); + } + else + { + this.set_after=true; + } + } + + this.doset=function(first_time) + { //this function sends demodulator parameters to the server + ws.send("SET"+((first_time)?" mod="+this.server_mod:"")+ + " low_cut="+this.low_cut.toString()+" high_cut="+this.high_cut.toString()+ + " offset_freq="+this.offset_frequency.toString()); + } + this.doset(true); //we set parameters on object creation + + //******* envelope object ******* + // for drawing the filter envelope above scale + this.envelope.parent=this; + + this.envelope.draw=function(visible_range) + { + this.visible_range=visible_range; + this.drag_ranges=demod_envelope_draw(range, + center_freq+this.parent.offset_frequency+this.parent.low_cut, + center_freq+this.parent.offset_frequency+this.parent.high_cut, + this.color,center_freq+this.parent.offset_frequency); + }; + + // event handlers + this.envelope.drag_start=function(x, key_modifiers) + { + this.key_modifiers=key_modifiers; + this.dragged_range=demod_envelope_where_clicked(x,this.drag_ranges, key_modifiers); + //console.log("dragged_range: "+this.dragged_range.toString()); + this.drag_origin={ + x: x, + low_cut: this.parent.low_cut, + high_cut: this.parent.high_cut, + offset_frequency: this.parent.offset_frequency + }; + return this.dragged_range!=demodulator.draggable_ranges.none; + }; + + this.envelope.drag_move=function(x) + { + dr=demodulator.draggable_ranges; + if(this.dragged_range==dr.none) return false; // we return if user is not dragging (us) at all + freq_change=Math.round(this.visible_range.hps*(x-this.drag_origin.x)); + /*if(this.dragged_range==dr.beginning||this.dragged_range==dr.ending) + { + //we don't let the passband be too small + if(this.parent.low_cut+new_freq_change<=this.parent.high_cut-this.parent.filter.min_passband) this.freq_change=new_freq_change; + else return; + } + var new_value;*/ + + //dragging the line in the middle of the filter envelope while holding Shift does emulate + //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged + //Filter passband moves in the opposite direction than dragged, hence the minus below. + minus=(this.dragged_range==dr.bfo)?-1:1; + //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob + //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset + //frequency. + if(this.dragged_range==dr.beginning||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) + { + //we don't let low_cut go beyond its limits + if((new_value=this.drag_origin.low_cut+minus*freq_change)=this.parent.high_cut) return true; + this.parent.low_cut=new_value; + } + if(this.dragged_range==dr.ending||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) + { + //we don't let high_cut go beyond its limits + if((new_value=this.drag_origin.high_cut+minus*freq_change)>this.parent.filter.high_cut_limit) return true; + //nor the filter passband be too small + if(new_value-this.parent.low_cutbandwidth/2||new_value<-bandwidth/2) return true; //we don't allow tuning above Nyquist frequency :-) + this.parent.offset_frequency=new_value; + } + //now do the actual modifications: + mkenvelopes(this.visible_range); + this.parent.set(); + //will have to change this when changing to multi-demodulator mode: + e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+this.parent.offset_frequency,1e6,4); + return true; + }; + + this.envelope.drag_end=function(x) + { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. + to_return=this.dragged_range!=demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset + this.dragged_range=demodulator.draggable_ranges.none; + return to_return; + }; + +} + +demodulator_default_analog.prototype=new demodulator(); + +function mkenvelopes(visible_range) //called from mkscale +{ + scale_ctx.clearRect(0,0,scale_ctx.canvas.width,22); //clear the upper part of the canvas (where filter envelopes reside) + for (var i=0;ibandwidth/2||to_what<-bandwidth/2) return; + demodulators[0].offset_frequency=Math.round(to_what); + demodulators[0].set(); + mkenvelopes(get_visible_freq_range()); +} + + +// ======================================================== +// =================== SCALE ROUTINES =================== +// ======================================================== + +var scale_ctx; +var scale_canvas; + +function scale_setup() +{ + e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(window.innerWidth/2),1e6,4); + scale_canvas=e("openwebrx-scale-canvas"); + scale_ctx=scale_canvas.getContext("2d"); + scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false); + scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); + scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); + resize_scale(); +} + +var scale_canvas_drag_params={ + mouse_down: false, + drag: false, + start_x: 0, + key_modifiers: {shiftKey:false, altKey: false, ctrlKey: false} +}; + +function scale_canvas_mousedown(evt) +{ + with(scale_canvas_drag_params) + { + mouse_down=true; + drag=false; + start_x=evt.pageX; + key_modifiers.shiftKey=evt.shiftKey; + key_modifiers.altKey=evt.altKey; + key_modifiers.ctrlKey=evt.ctrlKey; + } + evt.preventDefault(); +} + +function scale_offset_freq_from_px(x, visible_range) +{ + if(typeof visible_range === "undefined") visible_range=get_visible_freq_range(); + return (visible_range.start+visible_range.bw*(x/canvas_container.clientWidth))-center_freq; +} + +function scale_canvas_mousemove(evt) +{ + var event_handled; + if(scale_canvas_drag_params.mouse_down&&!scale_canvas_drag_params.drag&&Math.abs(evt.pageX-scale_canvas_drag_params.start_x)>canvas_drag_min_delta) + //we can use the main drag_min_delta thing of the main canvas + { + scale_canvas_drag_params.drag=true; + //call the drag_start for all demodulators (and they will decide if they're dragged, based on X coordinate) + for (var i=0;i=scale_min_space_bw_small_markers&&freq.toString()[0]!="5") {out.small/=2; out.ratio*=2; } + out.smallbw=freq/out.ratio; + return true; + } + for(i=scale_markers_levels.length-1;i>=0;i--) + { + mp=scale_markers_levels[i]; + if (!fcalc(mp.large_marker_per_hz)) continue; + //console.log(mp.large_marker_per_hz); + //console.log(out); + if (out.large-mp.estimated_text_width>scale_min_space_bw_texts) break; + } + out.params=mp; + return out; +} + +function mkscale() +{ + //clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes): + range=get_visible_freq_range(); + mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too + scale_ctx.clearRect(0,22,scale_ctx.canvas.width,scale_ctx.canvas.height-22); + scale_ctx.strokeStyle = "#fff"; + scale_ctx.font = "bold 11px sans-serif"; + scale_ctx.textBaseline = "top"; + scale_ctx.fillStyle = "#fff"; + spacing=get_scale_mark_spacing(range); + //console.log(spacing); + marker_hz=Math.ceil(range.start/spacing.smallbw)*spacing.smallbw; + var text_to_draw; + var ftext=function(f) {text_to_draw=format_frequency(spacing.params.format,f,spacing.params.pre_divide,spacing.params.decimals);} + var last_large; + for(;;) + { + var x=scale_px_from_freq(marker_hz,range); + if(x>window.innerWidth) break; + scale_ctx.beginPath(); + scale_ctx.moveTo(x, 22); + if(marker_hz%spacing.params.large_marker_per_hz==0) + { //large marker + if(typeof first_large == "undefined") var first_large=marker_hz; + last_large=marker_hz; + scale_ctx.lineWidth=3.5; + scale_ctx.lineTo(x,22+11); + ftext(marker_hz); + var text_measured=scale_ctx.measureText(text_to_draw); + scale_ctx.textAlign = "center"; + //advanced text drawing begins + if(zoom_level==0&&range.start+spacing.smallbw*spacing.ratio>marker_hz) + { //if this is the first overall marker when zoomed out + if(x=scale_min_space_bw_texts) + { //and if we have enough space to draw it correctly without clipping + scale_ctx.textAlign = "left"; + scale_ctx.fillText(text_to_draw, 0, 22+10); + } + } + } + else if(zoom_level==0&&range.end-spacing.smallbw*spacing.ratiowindow.innerWidth-text_measured.width/2) + { //and if it would be clipped off the screen + if(window.innerWidth-text_measured.width-scale_px_from_freq(marker_hz-spacing.smallbw*spacing.ratio,range)>=scale_min_space_bw_texts) + { //and if we have enough space to draw it correctly without clipping + scale_ctx.textAlign = "right"; + scale_ctx.fillText(text_to_draw, window.innerWidth, 22+10); + } + } + } + else scale_ctx.fillText(text_to_draw, x, 22+10); //draw text normally + } + else + { //small marker + scale_ctx.lineWidth=2; + scale_ctx.lineTo(x,22+8); + } + marker_hz+=spacing.smallbw; + scale_ctx.stroke(); + } + if(zoom_level!=0) + { // if zoomed, we don't want the texts to disappear because their markers can't be seen + // on the left side + scale_ctx.textAlign = "center"; + var f=first_large-spacing.smallbw*spacing.ratio; + var x=scale_px_from_freq(f,range); + ftext(f); + var w=scale_ctx.measureText(text_to_draw).width; + if(x+w/2>0) scale_ctx.fillText(text_to_draw, x, 22+10); + // on the right side + f=last_large+spacing.smallbw*spacing.ratio; + x=scale_px_from_freq(f,range); + ftext(f); + w=scale_ctx.measureText(text_to_draw).width; + if(x-w/23) + { + out=out.substr(0,at)+","+out.substr(at); + at+=4; + decimals-=3; + } + return out; +} + +canvas_drag=false; +canvas_drag_min_delta=1; +canvas_mouse_down=false; + +function canvas_mousedown(evt) +{ + canvas_mouse_down=true; + canvas_drag=false; + canvas_drag_last_x=canvas_drag_start_x=evt.pageX; + canvas_drag_last_y=canvas_drag_start_y=evt.pageY; + evt.preventDefault(); //don't show text selection mouse pointer +} + +function canvas_mousemove(evt) +{ + if(!waterfall_setup_done) return; + //element=e("webrx-freq-show"); + relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; + /*realX=(relativeX-element.clientWidth/2); + maxX=(canvases[0].clientWidth-element.clientWidth); + if(realX>maxX) realX=maxX; + if(realX<0) realX=0; + element.style.left=realX.toString()+"px";*/ + if(canvas_mouse_down) + { + if(!canvas_drag&&Math.abs(evt.pageX-canvas_drag_start_x)>canvas_drag_min_delta) + { + canvas_drag=true; + canvas_container.style.cursor="move"; + } + if(canvas_drag) + { + var deltaX=canvas_drag_last_x-evt.pageX; + var deltaY=canvas_drag_last_y-evt.pageY; + //zoom_center_where=zoom_center_where_calc(evt.pageX); + var dpx=range.hps*deltaX; + if( + !(zoom_center_rel+dpx>(bandwidth/2-canvas_container.clientWidth*(1-zoom_center_where)*range.hps)) && + !(zoom_center_rel+dpx<-bandwidth/2+canvas_container.clientWidth*zoom_center_where*range.hps) + ) { zoom_center_rel+=dpx; } +// -((canvases_new_width*(0.5+zoom_center_rel/bandwidth))-(winsize*zoom_center_where)); + resize_canvases(false); + canvas_drag_last_x=evt.pageX; + canvas_drag_last_y=evt.pageY; + mkscale(); + } + } + else e("webrx-mouse-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); +} + +function canvas_container_mouseout(evt) +{ + canvas_end_drag(); +} + +//function body_mouseup() { canvas_end_drag(); console.log("body_mouseup"); } +//function window_mouseout() { canvas_end_drag(); console.log("document_mouseout"); } + +function canvas_mouseup(evt) +{ + if(!waterfall_setup_done) return; + relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; + + if(!canvas_drag) + { + //ws.send("SET offset_freq="+canvas_get_freq_offset(relativeX).toString()); + //e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); + demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX)); + } + else + { + canvas_end_drag(); + } + canvas_mouse_down=false; +} + +function canvas_end_drag() +{ + canvas_container.style.cursor="crosshair"; + canvas_mouse_down=false; +} + +function zoom_center_where_calc(screenposX) +{ + //return (screenposX-(window.innerWidth-canvas_container.clientWidth))/canvas_container.clientWidth; + return screenposX/canvas_container.clientWidth; +} + +function canvas_mousewheel(evt) +{ + if(!waterfall_setup_done) return; + var i=Math.abs(evt.wheelDelta); + var dir=(i/evt.wheelDelta)<0; + var relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; + i/=120; + while (i--) zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); + evt.preventDefault(); + //evt.returnValue = false; //disable scrollbar move +} + + +zoom_max_level_hps=33; //Hz/pixel +zoom_levels_count=5; + +function get_zoom_coeff_from_hps(hps) +{ + var shown_bw=(window.innerWidth*hps); + return bandwidth/shown_bw; +} + +zoom_levels=[1]; +zoom_level=0; +zoom_freq=0; +zoom_offset_px=0; +zoom_center_rel=0; +zoom_center_where=0; + +function mkzoomlevels() +{ + zoom_levels=[1]; + maxc=get_zoom_coeff_from_hps(zoom_max_level_hps); + if(maxc<1) return; + for(i=1;i=zoom_levels_count-1)) return; + + if(out) --zoom_level; + else ++zoom_level; + zoom_center_rel=canvas_get_freq_offset(where); + //console.log("zoom_step || zlevel: "+zoom_level.toString()+" zlevel_val: "+zoom_levels[zoom_level].toString()+" zoom_center_rel: "+zoom_center_rel.toString()); + zoom_center_where=onscreen; + resize_canvases(true); + mkscale(); +} + +function zoom_calc() +{ + winsize=canvas_container.clientWidth; + var canvases_new_width=winsize*zoom_levels[zoom_level]; + zoom_offset_px=-((canvases_new_width*(0.5+zoom_center_rel/bandwidth))-(winsize*zoom_center_where)); + if(zoom_offset_px>0) zoom_offset_px=0; + if(zoom_offset_px10) audio_init() + } + else if(firstChars=="FFT") + { + //alert("Yupee! Doing FFT"); + var floatArray = new Float32Array(evt.data,4); + waterfall_add_queue(floatArray); + } else if(firstChars=="MSG") + { + /*try + {*/ + params=stringData.substring(4).split(" "); + for(i=0;ifft_fps/2) //in case of emergency + { + add_problem("fft overflow"); + while(waterfall_queue.length) waterfall_add(waterfall_queue.shift()); + } +} + +function on_ws_opened() +{ + ws.send("SERVER DE CLIENT openwebrx.js"); + divlog("WebSocket opened to "+ws_url); +} + +function divlog(what, is_error) +{ + if(typeof is_error !== undefined && is_error == 1) what=""+what+""; + e("openwebrx-debugdiv").innerHTML+=what+"
"; +} + +var audio_context; +var audio_initialized=0; + +var audio_received = Array(); +var audio_buffer_index = 0; +var audio_resampler; +var audio_node; +//var audio_received_sample_rate = 48000; +var audio_input_buffer_size; + +// Optimalise these if audio lags or is choppy: +var audio_buffer_size = 8192;//2048 was choppy +var audio_buffer_maximal_length_sec=2; //actual number of samples are calculated from sample rate +var audio_flush_interval_ms=250; //the interval in which audio_flush() is called + +function audio_onprocess(e) +{ + //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js + if(audio_received.length==0) + { add_problem("audio underrun"); return; } + output = e.outputBuffer.getChannelData(0); + int_buffer = audio_received[0]; + read_remain = audio_buffer_size; + //audio_buffer_maximal_length=120; + + obi=0; //output buffer index + debug_str="" + while(1) + { + if(int_buffer.length-audio_buffer_index>read_remain) + { + for (i=audio_buffer_index; i"+read_remain.toString()+" obi="+obi.toString()+"\n"; + audio_buffer_index+=read_remain; + break; + } + else + { + for (i=audio_buffer_index; iaudio_buffer_maximal_length) + { + add_problem("audio overrun"); + audio_received.splice(0,audio_received.length-audio_buffer_maximal_length); + } + else*/ + audio_received.splice(0,1); + //debug_str+="added remain, remain="+read_remain.toString()+" abi="+audio_buffer_index.toString()+" alen="+int_buffer.length.toString()+" i="+i.toString()+" arecva="+audio_received.length.toString()+" obi="+obi.toString()+"\n"; + audio_buffer_index = 0; + if(audio_received.length == 0 || read_remain == 0) return; + int_buffer = audio_received[0]; + } + } + //debug_str+="obi="+obi.toString(); + //alert(debug_str); +} + +function audio_flush() +{ + if (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate) + { + add_problem("audio overrun"); + console.log("audio_flush() :: size: "+audio_buffer_current_size.toString()+" allowed: "+(audio_buffer_maximal_length_sec*audio_context.sampleRate).toString()); + while (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate*0.5) + { + audio_buffer_current_size-=audio_received[0].length; + audio_received.splice(0,1); + } + } + +} + +function webrx_set_param(what, value) +{ + ws.send("SET "+what+"="+value.toString()); +} + +function audio_init() +{ + //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js + audio_initialized=1; // only tell on_ws_recv() not to call it again + try + { + window.AudioContext = window.AudioContext||window.webkitAudioContext; + audio_context = new AudioContext(); + } + catch(e) + { + divlog('Your browser does not support Web Audio API, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.', 1); + } + audio_node = audio_context.createJavaScriptNode(audio_buffer_size, 0, 1); + audio_node.onaudioprocess = audio_onprocess; + audio_node.connect(audio_context.destination); + // --- Resampling --- + //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js + //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); + //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); + webrx_set_param("audio_rate",audio_context.sampleRate); //Don't try to resample + window.setInterval(audio_flush,audio_flush_interval_ms); + divlog('Web Audio API succesfully initialized, sample rate: '+audio_context.sampleRate.toString()); + /*audio_source=audio_context.createBufferSource(); + audio_buffer = audio_context.createBuffer(xhr.response, false); + audio_source.buffer = buffer; + audio_source.noteOn(0);*/ + demodulator_analog_replace('nfm'); //needs audio_context.sampleRate to exist +} + +function on_ws_closed() +{ + try + { + audio_node.disconnect(); + } + catch (dont_care) {} + divlog("WebSocket has closed unexpectedly. Please reload the page.", 1); +} + +function on_ws_error(event) +{ + divlog(event.toString(),1); +} + +function open_websocket() +{ + if (!("WebSocket" in window)) + divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); + ws = new WebSocket(ws_url+client_id); + ws.onopen = on_ws_opened; + ws.onmessage = on_ws_recv; + ws.onclose = on_ws_closed; + ws.binaryType = "arraybuffer"; + window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript + ws.onclose = function () {}; + ws.close(); + }; + //ws.onerror = on_ws_error; +} + +//var color_scale=[0xFFFFFFFF, 0x000000FF]; +//var color_scale=[0x000000FF, 0x000000FF, 0x3a0090ff, 0x10c400ff, 0xffef00ff, 0xff5656ff]; +//var color_scale=[0x000000FF, 0x000000FF, 0x534b37ff, 0xcedffaff, 0x8899a9ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]; + +//var color_scale=[ 0x000000FF, 0xff5656ff, 0xffffffff]; + +//2014-04-22 +//var color_scale=[0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]; + +//Nona: +var color_scale=[0x000000ff,0x000000ff, 0xffffffff, 0x0000ffff, 0x00ffffff]; + +function waterfall_mkcolor(db_value) +{ + min_value=-100; //in dB + max_value=10 + if(db_valuemax_value) db_value=max_value + full_scale=max_value-min_value; + relative_value=db_value-min_value; + value_percent=relative_value/full_scale; + percent_for_one_color=1/(color_scale.length-1); + index=Math.floor(value_percent/percent_for_one_color); + remain=(value_percent-percent_for_one_color*index)/percent_for_one_color; + return color_between(color_scale[index+1],color_scale[index],remain); +} + +function color_between(first, second, percent) +{ + output=0; + for(i=0;i<4;i++) + { + add = ((((first&(0xff<<(i*8)))>>>0)*percent) + (((second&(0xff<<(i*8)))>>>0)*(1-percent))) & (0xff<<(i*8)); + output |= add>>>0; + } + return output>>>0; +} + + +var canvas_context; +var canvases = []; +var canvas_default_height = 200; +var canvas_container; +var canvas_phantom; + +function add_canvas() +{ + new_canvas = document.createElement("canvas"); + new_canvas.width=fft_size; + new_canvas.height=canvas_default_height; + canvas_actual_line=canvas_default_height-1; + new_canvas.style.width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; + new_canvas.style.left=zoom_offset_px.toString()+"px"; + new_canvas.style.height=canvas_default_height.toString()+"px"; + new_canvas.openwebrx_top=(-canvas_default_height+1); + new_canvas.style.top=new_canvas.openwebrx_top.toString()+"px"; + canvas_context = new_canvas.getContext("2d"); + canvas_container.appendChild(new_canvas); + new_canvas.addEventListener("mouseover", canvas_mouseover, false); + new_canvas.addEventListener("mouseout", canvas_mouseout, false); + new_canvas.addEventListener("mousemove", canvas_mousemove, false); + new_canvas.addEventListener("mouseup", canvas_mouseup, false); + new_canvas.addEventListener("mousedown", canvas_mousedown, false); + new_canvas.addEventListener("mousewheel",canvas_mousewheel, false); + canvases.push(new_canvas); +} + +function init_canvas_container() +{ + canvas_container=e("webrx-canvas-container"); + canvas_container.addEventListener("mouseout",canvas_container_mouseout, false); + //window.addEventListener("mouseout",window_mouseout,false); + //document.body.addEventListener("mouseup",body_mouseup,false); + canvas_phantom=e("openwebrx-phantom-canvas"); + canvas_phantom.addEventListener("mouseover", canvas_mouseover, false); + canvas_phantom.addEventListener("mouseout", canvas_mouseout, false); + canvas_phantom.addEventListener("mousemove", canvas_mousemove, false); + canvas_phantom.addEventListener("mouseup", canvas_mouseup, false); + canvas_phantom.addEventListener("mousedown", canvas_mousedown, false); + canvas_phantom.addEventListener("mousewheel",canvas_mousewheel, false); + canvas_phantom.style.width=canvas_container.clientWidth+"px"; + add_canvas(); +} + +canvas_maxshift=0; + +function shift_canvases() +{ + canvases.forEach(function(p) + { + p.style.top=(p.openwebrx_top++).toString()+"px"; + }); + canvas_maxshift++; + if(canvas_container.clientHeight>canvas_maxshift) + { + canvas_phantom.style.top=canvas_maxshift.toString()+"px"; + canvas_phantom.style.height=(canvas_container.clientHeight-canvas_maxshift).toString()+"px"; + canvas_phantom.style.display="block"; + } + else + canvas_phantom.style.display="none"; + + + //canvas_container.style.height=(((canvases.length-1)*canvas_default_height)+(canvas_default_height-canvas_actual_line)).toString()+"px"; + //canvas_container.style.height="100%"; +} + +function resize_canvases(zoom) +{ + if(typeof zoom == "undefined") zoom=false; + if(!zoom) mkzoomlevels(); + zoom_calc(); + new_width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; + var zoom_value=zoom_offset_px.toString()+"px"; + canvases.forEach(function(p) + { + p.style.width=new_width; + p.style.left=zoom_value; + }); + canvas_phantom.style.width=new_width; + canvas_phantom.style.left=zoom_value; +} + +function waterfall_init() +{ + init_canvas_container(); + waterfall_timer = window.setInterval(waterfall_dequeue,900/fft_fps); + resize_waterfall_container(false); /* then */ resize_canvases(); + scale_setup(); + mkzoomlevels(); + waterfall_setup_done=1; +} + +var waterfall_dont_scale=0; + +function waterfall_add(data) +{ + if(!waterfall_setup_done) return; + var w=fft_size; + + //waterfall_shift(); + // ==== do scaling if required ==== + /*if(waterfall_dont_scale) + { + scaled=data; + for(i=scaled.length;i1) + { + scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); + remain--; + } + else + { + j++; + scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); + remain=pixel_per_point-(1-remain); + } + } + + } + else + { //make line smaller (linear decimation, moving average) + point_per_pixel=(to-from)/w; + scaled=Array(); + j=0; + remain=point_per_pixel; + last_pixel=0; + for(i=from; i1) + { + last_pixel+=data[i]; + remain--; + } + else + { + last_pixel+=data[i]*remain; + scaled[j++]=last_pixel/point_per_pixel; + last_pixel=data[i]*(1-remain); + remain=point_per_pixel-(1-remain); //? + } + } + } + } + + //Add line to waterfall image + base=(h-1)*w*4; + for(x=0;x>>0)>>((3-i)*8))&0xff; + }*/ + + //Add line to waterfall image + oneline_image = canvas_context.createImageData(w,1); + for(x=0;x>>0)>>((3-i)*8))&0xff; + } + + + //Draw image + canvas_context.putImageData(oneline_image, 0, canvas_actual_line--); + shift_canvases(); + if(canvas_actual_line<0) add_canvas(); + //divlog("Drawn FFT"); +} + +/* +function waterfall_shift() +{ + w=canvas.width; + h=canvas.height; + for(y=0; ytl.offsetLeft-20) tl.style.display="none"; + else tl.style.display="block"; +} + +function webrx_resize() +{ + resize_canvases(); + resize_waterfall_container(true); + resize_scale(); + check_top_bar_congestion(); +} + +function webrx_init() +{ + init_rx_photo(); + open_websocket(); + place_panels(); + window.setInterval(debug_audio,1000); +} + +/* +window.setInterval(function(){ + sum=0; + for(i=0;i=(c=c.charCodeAt(0)+13)?c:c-26);}); + window.location.href="mailto:"+what; +}*/ + +var rt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c<="Z"?90:122)>=(c=c.charCodeAt(0)+n)?c:c-26);});} +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); } + +function debug_audio() +{ + + e("openwebrx-audio-sps").innerHTML=audio_buffer_current_size_debug.toString(); + audio_buffer_current_size_debug=0; +} + +// ======================================================== +// ======================= PANELS ======================= +// ======================================================== + +panel_margin=10; + +function pop_bottommost_panel(from) +{ + min_order=parseInt(from[0].dataset.panelOrder); + min_index=0; + for(i=0;i0) + { + p=pop_bottommost_panel(left_col); + p.style.left="0px"; + p.style.bottom=y.toString()+"px"; + p.style.visibility="visible"; + y+=p.openwebrxPanelHeight+3*panel_margin; + } + y=0; + while(right_col.length>0) + { + p=pop_bottommost_panel(right_col); + p.style.right="10px"; + p.style.bottom=y.toString()+"px"; + p.style.visibility="visible"; + y+=p.openwebrxPanelHeight+3*panel_margin; + } +} + diff --git a/htdocs/upgrade.html b/htdocs/upgrade.html new file mode 100644 index 0000000..b9a498d --- /dev/null +++ b/htdocs/upgrade.html @@ -0,0 +1,93 @@ + + +OpenWebRX + + + + + + +
+ +
+ Only the latest Google Chrome browser is supported at the moment.
+ Please download and install Google Chrome.
+
+ Alternatively, you may proceed to OpenWebRX, but it's not supposed to work as expected.
+ Click here if you still want to try OpenWebRX. +
+
+
+ + + diff --git a/openwebrx.py b/openwebrx.py new file mode 100755 index 0000000..c26cc46 --- /dev/null +++ b/openwebrx.py @@ -0,0 +1,378 @@ + +""" +OpenWebRX: open-source web based SDR for everyone! + +This file is part of OpenWebRX. + + OpenWebRX is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenWebRX 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenWebRX. If not, see . + +Authors: + Andras Retzler, HA7ILM + +""" + +# http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python +# some ideas are used from the artice above + + +import os +import code +import importlib +import plugins +import plugins.dsp +import thread +import time +import subprocess +import os +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +from SocketServer import ThreadingMixIn +import fcntl +import time +import md5 +import random +import threading +import dl +import sys +import traceback +from collections import namedtuple +import Queue +import ctypes + +#import rtl_mus +import rxws +import uuid +import config_webrx as cfg + +def import_all_plugins(directory): + for subdir in os.listdir(directory): + if os.path.isdir(directory+subdir) and not subdir[0]=="_": + exact_path=directory+subdir+"/plugin.py" + if os.path.isfile(exact_path): + importname=(directory+subdir+"/plugin").replace("/",".") + print "[openwebrx-import] Found plugin:",importname + importlib.import_module(importname) + +class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer): + pass + +def main(): + global clients + global clients_mutex + + print + print "OpenWebRX - Open Source Web Based SDR for Everyone | for license see LICENSE file in the package" + print "_________________________________________________________________________________________________" + print + print "Author contact info: Andras Retzler, HA7ILM " + print + + #Load plugins + import_all_plugins("plugins/dsp/") + + #Change process name to "openwebrx" (to be seen in ps) + try: + for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]: + if os.path.exists(libcpath): + libc = dl.open(libcpath) + libc.call("prctl", 15, "openwebrx", 0, 0, 0) + break + except: + pass + + #Start rtl thread + if cfg.start_rtl_thread: + rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=()) + rtl_thread.start() + print "[openwebrx-main] Started rtl thread: "+cfg.start_rtl_command + + #Run rtl_mus.py in a different OS thread + rtl_mus_thread=threading.Thread(target = lambda:subprocess.Popen("python rtl_mus.py config_rtl", shell=True), args=()) + rtl_mus_thread.start() # The new feature in GNU Radio 3.7: top_block() locks up ALL python threads until it gets the TCP connection. + print "[openwebrx-main] Started rtl_mus" + time.sleep(1) #wait until it really starts + + #Initialize clients + clients=[] + clients_mutex=threading.Lock() + + #Start spectrum thread + print "[openwebrx-main] Starting spectrum thread." + spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ()) + spectrum_thread.start() + + #threading.Thread(target = measure_thread_function, args = ()).start() + + #Start HTTP thread + httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler) + print('[openwebrx-main] Starting HTTP server.') + httpd.serve_forever() + + +# This is a debug function below: +measure_value=0 +def measure_thread_function(): + global measure_value + while True: + print "[openwebrx-measure] value is",measure_value + measure_value=0 + time.sleep(1) + + +def spectrum_thread_function(): + global clients_mutex + global clients + dsp=getattr(plugins.dsp,cfg.dsp_plugin).plugin.dsp_plugin() + dsp.set_demodulator("fft") + dsp.set_samp_rate(cfg.samp_rate) + dsp.set_fft_size(cfg.fft_size) + dsp.set_fft_fps(cfg.fft_fps) + sleep_sec=0.87/cfg.fft_fps + print "[openwebrx-spectrum] Spectrum thread initialized successfully. Thread id:", ctypes.CDLL('/lib/i386-linux-gnu/libc.so.6').syscall(224) + dsp.start() + print "[openwebrx-spectrum] Spectrum thread started." + while True: + data=dsp.read(cfg.fft_size*4) + #print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()" + clients_mutex.acquire() + for i in range(0,len(clients)): + if (clients[i].ws_started): + if clients[i].spectrum_queue.full(): + close_client(i, False) + else: + clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients + clients_mutex.release() + +def get_client_by_id(client_id, use_mutex=True): + global clients_mutex + global clients + output=-1 + if use_mutex: clients_mutex.acquire() + for i in range(0,len(clients)): + if(clients[i].id==client_id): + output=i + break + if use_mutex: clients_mutex.release() + if output==-1: + raise ClientNotFoundException + else: + return output + +def log_client(client, what): + print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what) + +def cleanup_clients(): + # if client doesn't open websocket for too long time, we drop it + global clients_mutex + global clients + clients_mutex.acquire() + correction=0 + for i in range(0,len(clients)): + i-=correction + #print "cleanup_clients:: len(clients)=", len(clients), "i=", i + if (not clients[i].ws_started) and (time.time()-clients[i].gen_time)>180: + close_client(i, False) + correction+=1 + clients_mutex.release() + +def generate_client_id(ip): + #add a client + global clients + global clients_mutex + new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip") + new_client.id=md5.md5(str(random.random())).hexdigest() + new_client.gen_time=time.time() + new_client.ws_started=False # to check whether client has ever tried to open the websocket + new_client.spectrum_queue=Queue.Queue(1000) + new_client.ip=ip + clients_mutex.acquire() + clients.append(new_client) + log_client(new_client,"client added. Clients now: {0}".format(len(clients))) + clients_mutex.release() + cleanup_clients() + return new_client.id + +def close_client(i, use_mutex=True): + global clients_mutex + global clients + log_client(clients[i],"client being closed.") + if use_mutex: clients_mutex.acquire() + del clients[i] + if use_mutex: clients_mutex.release() + +class WebRXHandler(BaseHTTPRequestHandler): + def proc_read_thread(): + pass + + def do_GET(self): + global dsp_plugin + rootdir = 'htdocs' + self.path=self.path.replace("..","") + path_temp_parts=self.path.split("?") + self.path=path_temp_parts[0] + request_param=path_temp_parts[1] if(len(path_temp_parts)>1) else "" + try: + if self.path=="/": + self.path="/index.wrx" + # there's even another cool tip at http://stackoverflow.com/questions/4419650/how-to-implement-timeout-in-basehttpserver-basehttprequesthandler-python + if self.path[:4]=="/ws/": + try: + # ========= WebSocket handshake ========= + try: + rxws.handshake(self) + clients_mutex.acquire() + client_i=get_client_by_id(self.path[4:], False) + myclient=clients[client_i] + clients_mutex.release() + except rxws.WebSocketException: + self.send_error(400, 'Bad request.') + return + except ClientNotFoundException: + self.send_error(400, 'Bad request.') + return + + # ========= Client handshake ========= + rxws.send(self, "CLIENT DE SERVER openwebrx.py") + client_ans=rxws.recv(self, True) + if client_ans[:16]!="SERVER DE CLIENT": + rxws.send("ERR Bad answer.") + return + myclient.ws_started=True + #send default parameters + rxws.send(self, "MSG center_freq={0} bandwidth={1} fft_size={2} fft_fps={3} setup".format(str(cfg.center_freq),str(cfg.samp_rate),cfg.fft_size,cfg.fft_fps)) + + # ========= Initialize DSP ========= + dsp=getattr(plugins.dsp,cfg.dsp_plugin).plugin.dsp_plugin() + dsp.set_samp_rate(cfg.samp_rate) + dsp.set_demodulator("nfm") + dsp.set_offset_freq(0) + dsp.set_bpf(-4000,4000) + dsp.start() + + while True: + # ========= send audio ========= + temp_audio_data=dsp.read(1024*8) + rxws.send(self, temp_audio_data, "AUD ") + + # ========= send spectrum ========= + while not myclient.spectrum_queue.empty(): + spectrum_data=myclient.spectrum_queue.get() + spectrum_data_mid=len(spectrum_data[0])/2 + rxws.send(self, spectrum_data[0][spectrum_data_mid:]+spectrum_data[0][:spectrum_data_mid], "FFT ") + # (it seems GNU Radio exchanges the first and second part of the FFT output, we correct it) + + # ========= process commands ========= + while True: + rdata=rxws.recv(self, False) + if not rdata: break + #try: + elif rdata[:3]=="SET": + print "[openwebrx-httpd:ws,%d] command: %s"%(client_i,rdata) + pairs=rdata[4:].split(" ") + bpf_set=False + new_bpf=dsp.get_bpf() + filter_limit=dsp.get_output_rate()/2 + for pair in pairs: + param_name, param_value = pair.split("=") + if param_name == "low_cut" and -filter_limit <= float(param_value) <= filter_limit: + bpf_set=True + new_bpf[0]=param_value + elif param_name == "high_cut" and -filter_limit <= float(param_value) <= filter_limit: + bpf_set=True + new_bpf[1]=param_value + elif param_name == "offset_freq" and -cfg.samp_rate/2 <= float(param_value) <= cfg.samp_rate/2: + dsp.set_offset_freq(param_value) + elif param_name=="mod": + dsp.stop() + dsp.set_demodulator(param_value) + dsp.start() + else: + print "[openwebrx-httpd:ws] invalid parameter" + if bpf_set: + dsp.set_bpf(*new_bpf) + #code.interact(local=locals()) + except: + print "[openwebrx-httpd] exception happened at all" + exc_type, exc_value, exc_traceback = sys.exc_info() + if exc_value[0]==32: #"broken pipe", client disconnected + pass + elif exc_value[0]==11: #"resource unavailable" on recv, client disconnected + pass + else: + print "[openwebrx-httpd] error: ",exc_type,exc_value + traceback.print_tb(exc_traceback) + #delete disconnected client + try: + dsp.stop() + del dsp + except: + pass + clients_mutex.acquire() + id_to_close=get_client_by_id(myclient.id,False) + close_client(id_to_close,False) + clients_mutex.release() + return + else: + f=open(rootdir+self.path) + data=f.read() + extension=self.path[(len(self.path)-4):len(self.path)] + extension=extension[2:] if extension[1]=='.' else extension[1:] + if extension == "wrx" and ((self.headers['user-agent'].count("Chrome")==0 and self.headers['user-agent'].count("Firefox")==0) if 'user-agent' in self.headers.keys() else True) and (not request_param.count("unsupported")): + self.send_response(302) + self.send_header('Content-type','text/html') + self.send_header("Location", "http://{0}:{1}/upgrade.html".format(cfg.server_hostname,cfg.web_port)) + self.end_headers() + self.wfile.write("

Object moved

Please click here to continue.") + return + self.send_response(200) + if(("wrx","html","htm").count(extension)): + self.send_header('Content-type','text/html') + elif(extension=="js"): + self.send_header('Content-type','text/javascript') + elif(extension=="css"): + self.send_header('Content-type','text/css') + self.end_headers() + if extension == "wrx": + replace_dictionary=( + ("%[RX_PHOTO_DESC]",cfg.photo_desc), + ("%[CLIENT_ID]",generate_client_id(self.client_address[0])), + ("%[WS_URL]","ws://"+cfg.server_hostname+":"+str(cfg.web_port)+"/ws/"), + ("%[RX_TITLE]",cfg.receiver_name), + ("%[RX_LOC]",cfg.receiver_location), + ("%[RX_QRA]",cfg.receiver_qra), + ("%[RX_ASL]",str(cfg.receiver_asl)), + ("%[RX_GPS]",str(cfg.receiver_gps[0])+","+str(cfg.receiver_gps[1])), + ("%[RX_PHOTO_HEIGHT]",str(cfg.photo_height)),("%[RX_PHOTO_TITLE]",cfg.photo_title), + ("%[RX_ADMIN]",cfg.receiver_admin), + ("%[RX_ANT]",cfg.receiver_ant), + ("%[RX_DEVICE]",cfg.receiver_device) + ) + for rule in replace_dictionary: + while data.find(rule[0])!=-1: + data=data.replace(rule[0],rule[1]) + self.wfile.write(data) + f.close() + return + except IOError: + self.send_error(404, 'Invalid path.') + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + print "[openwebrx-httpd] exception happened (outside):", exc_type, exc_value + traceback.print_tb(exc_traceback) + +class ClientNotFoundException(Exception): + pass + +if __name__=="__main__": + main() + diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/plugins/__init__.pyc b/plugins/__init__.pyc new file mode 100644 index 0000000..d84020a Binary files /dev/null and b/plugins/__init__.pyc differ diff --git a/plugins/dsp/__init__.py b/plugins/dsp/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/plugins/dsp/__init__.pyc b/plugins/dsp/__init__.pyc new file mode 100644 index 0000000..7dc2093 Binary files /dev/null and b/plugins/dsp/__init__.pyc differ diff --git a/plugins/dsp/csdr/__init__.py b/plugins/dsp/csdr/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/plugins/dsp/csdr/__init__.pyc b/plugins/dsp/csdr/__init__.pyc new file mode 100644 index 0000000..7f0bd13 Binary files /dev/null and b/plugins/dsp/csdr/__init__.pyc differ diff --git a/plugins/dsp/csdr/plugin.py b/plugins/dsp/csdr/plugin.py new file mode 100644 index 0000000..511c962 --- /dev/null +++ b/plugins/dsp/csdr/plugin.py @@ -0,0 +1,135 @@ +import subprocess +import time +import os +import code + +class dsp_plugin: + + def __init__(self): + self.samp_rate = 250000 + self.output_rate = 44100 #this is default, and cannot be set at the moment + self.fft_size = 1024 + self.fft_fps = 5 + self.offset_freq = 0 + self.low_cut = -4000 + self.high_cut = 4000 + self.bpf_transition_bw = 300 #Hz, and this is a constant + self.running = False + chain_begin="nc localhost 4951 | csdr convert_u8_f | csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} 0.005 HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | " + self.chains = { + "nfm" : chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 48000 | csdr fastagc_ff | csdr convert_f_i16", + "am" : chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_i16", + "ssb" : chain_begin + "csdr realpart_cf | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_i16", + "fft" : "nc -vv localhost 4951 | csdr convert_u8_f | csdr fft_cc {fft_size} {fft_block_size} | csdr logpower_cf -70" + } + self.demodulator = "nfm" + self.name = "csdr" + try: + subprocess.Popen("nc",stdout=subprocess.PIPE,stderr=subprocess.PIPE) + except: + print "[openwebrx-plugin:csdr] error: netcat not found, please install netcat!" + + def set_samp_rate(self,samp_rate): + #to change this, restart is required + self.samp_rate=samp_rate + self.decimation=1 + while self.samp_rate/(self.decimation+1)>self.output_rate: + self.decimation+=1 + self.last_decimation=float(self.if_samp_rate())/self.output_rate + + def if_samp_rate(self): + return self.samp_rate/self.decimation + + def get_name(self): + return self.name + + def get_output_rate(self): + return self.output_rate + + + def set_demodulator(self,demodulator): + #to change this, restart is required + self.demodulator=demodulator + + def set_fft_size(self,fft_size): + #to change this, restart is required + self.fft_size=fft_size + + def set_fft_fps(self,fft_fps): + #to change this, restart is required + self.fft_fps=fft_fps + + def fft_block_size(self): + return self.samp_rate/self.fft_fps + + def set_offset_freq(self,offset_freq): + self.offset_freq=offset_freq + if self.running: + self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate)) + self.shift_pipe_file.flush() + + def set_bpf(self,low_cut,high_cut): + self.low_cut=low_cut + self.high_cut=high_cut + if self.running: + 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() + + def get_bpf(self): + return [self.low_cut, self.high_cut] + + def mkfifo(self,path): + try: + os.unlink(path) + except: + pass + os.mkfifo(path) + + def start(self): + command_base=self.chains[self.demodulator] + + #create control pipes for csdr + pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) + self.bpf_pipe = self.shift_pipe = None + if "{bpf_pipe}" in command_base: + self.bpf_pipe=pipe_base_path+"bpf" + self.mkfifo(self.bpf_pipe) + if "{shift_pipe}" in command_base: + self.shift_pipe=pipe_base_path+"shift" + self.mkfifo(self.shift_pipe) + + #run the command + command=command_base.format(bpf_pipe=self.bpf_pipe,shift_pipe=self.shift_pipe,decimation=self.decimation,last_decimation=self.last_decimation,fft_size=self.fft_size,fft_block_size=self.fft_block_size(),bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate()) + print "[openwebrx-dsp-plugin:csdr] Command =",command + #code.interact(local=locals()) + self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) + self.running = True + + #open control pipes for csdr and send initialization data + if self.bpf_pipe != None: + self.bpf_pipe_file=open(self.bpf_pipe,"w") + self.set_bpf(self.low_cut,self.high_cut) + if self.shift_pipe != None: + self.shift_pipe_file=open(self.shift_pipe,"w") + self.set_offset_freq(self.offset_freq) + + def read(self,size): + return self.process.stdout.read(size) + + def stop(self): + if(self.process!=None):return # returns None while subprocess is running + while(self.process.poll()==None): + self.process.kill() + time.sleep(0.1) + os.unlink(self.bpf_pipe) + os.unlink(self.shift_pipe) + self.running = False + + def restart(self): + self.stop() + self.start() + + def __del__(self): + self.stop() + del(self.process) + diff --git a/plugins/dsp/csdr/plugin.pyc b/plugins/dsp/csdr/plugin.pyc new file mode 100644 index 0000000..4e0345f Binary files /dev/null and b/plugins/dsp/csdr/plugin.pyc differ diff --git a/rtl_mus.py b/rtl_mus.py new file mode 100644 index 0000000..b348961 --- /dev/null +++ b/rtl_mus.py @@ -0,0 +1,514 @@ +''' +This file is part of RTL Multi-User Server, + that makes multi-user access to your DVB-T dongle used as an SDR. +Copyright (c) 2013-2014 by Andras Retzler, HA7ILM + +RTL Multi-User Server is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +RTL Multi-User Server 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with RTL Multi-User Server. If not, see . + +----- + +2013-11? Asyncore version +2014-03 Fill with null on no data + +''' + +import socket +import sys +import array +import time +import logging +import os +import time +import subprocess +import fcntl +import thread +import pdb +import asyncore +import multiprocessing +import dl + +import code +import traceback + +def ip_match(this,ip_ranges,for_allow): + if not len(ip_ranges): + return 1 #empty list matches all ip addresses + for ip_range in ip_ranges: + #print this[0:len(ip_range)], ip_range + if this[0:len(ip_range)]==ip_range: + return 1 + return 0 + +def ip_access_control(ip): + if(not cfg.use_ip_access_control): return 1 + allowed=0 + if(cfg.order_allow_deny): + if ip_match(ip,cfg.allowed_ip_ranges,1): allowed=1 + if ip_match(ip,cfg.denied_ip_ranges,0): allowed=0 + else: + if ip_match(ip,cfg.denied_ip_ranges,0): + allowed=0 + if ip_match(ip,cfg.allowed_ip_ranges,1): + allowed=1 + return allowed + +def add_data_to_clients(new_data): + # might be called from: + # -> dsp_read + # -> rtl_tcp_asyncore.handle_read + global clients + global clients_mutex + clients_mutex.acquire() + for client in clients: + #print "client %d size: %d"%(client[0].ident,client[0].waiting_data.qsize()) + if(client[0].waiting_data.full()): + if cfg.cache_full_behaviour == 0: + log.error("client cache full, dropping samples: "+str(client[0].ident)+"@"+client[0].socket[1][0]) + while not client[0].waiting_data.empty(): # clear queue + client[0].waiting_data.get(False, None) + elif cfg.cache_full_behaviour == 1: + #rather closing client: + log.error("client cache full, dropping client: "+str(client[0].ident)+"@"+client[0].socket[1][0]) + client[0].close() + elif cfg.cache_full_behaviour == 2: + pass #client cache full, just not taking care + else: log.error("invalid value for cfg.cache_full_behaviour") + else: + client[0].waiting_data.put(new_data) + clients_mutex.release() + +def dsp_read_thread(): + global proc + global dsp_data_count + while True: + try: + my_buffer=proc.stdout.read(1024) + except IOError: + log.error("DSP subprocess is not ready for reading.") + time.sleep(1) + continue + add_data_to_clients(my_buffer) + if cfg.debug_dsp_command: + dsp_data_count+=len(my_buffer) + +def dsp_write_thread(): + global proc + global dsp_input_queue + global original_data_count + while True: + try: + my_buffer=dsp_input_queue.get(timeout=0.3) + except: + continue + proc.stdin.write(my_buffer) + proc.stdin.flush() + if cfg.debug_dsp_command: + original_data_count+=len(my_buffer) + +class client_handler(asyncore.dispatcher): + + def __init__(self,client_param): + self.client=client_param + self.client[0].asyncore=self + self.sent_dongle_id=False + self.last_waiting_buffer="" + asyncore.dispatcher.__init__(self, self.client[0].socket[0]) + + def handle_read(self): + global commands + new_command = self.recv(5) + if len(new_command)>=5: + if handle_command(new_command, self.client): + commands.put(new_command) + + def handle_error(self): + exc_type, exc_value, exc_traceback = sys.exc_info() + log.info("client error: "+str(self.client[0].ident)+"@"+self.client[0].socket[1][0]) + traceback.print_tb(exc_traceback) + self.close() + + def handle_close(self): + self.client[0].close() + log.info("client disconnected: "+str(self.client[0].ident)+"@"+self.client[0].socket[1][0]) + + def writable(self): + #print "queryWritable",not self.client[0].waiting_data.empty() + return not self.client[0].waiting_data.empty() + + def handle_write(self): + global last_waiting + global rtl_dongle_identifier + global sample_rate + if not self.sent_dongle_id: + self.send(rtl_dongle_identifier) + self.sent_dongle_id=True + return + #print "write2client",self.client[0].waiting_data.qsize() + next=self.last_waiting_buffer+self.client[0].waiting_data.get() + sent=asyncore.dispatcher.send(self, next) + self.last_waiting_buffer=next[sent:] + +class server_asyncore(asyncore.dispatcher): + + def __init__(self): + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((cfg.my_ip, cfg.my_listening_port)) + self.listen(5) + log.info("Server listening on port: "+str(cfg.my_listening_port)) + + def handle_accept(self): + global max_client_id + global clients_mutex + global clients + my_client=[client()] + my_client[0].socket=self.accept() + if (my_client[0].socket is None): # not sure if required + return + if (ip_access_control(my_client[0].socket[1][0])): + my_client[0].ident=max_client_id + max_client_id+=1 + my_client[0].start_time=time.time() + my_client[0].waiting_data=multiprocessing.Queue(250) + clients_mutex.acquire() + clients.append(my_client) + clients_mutex.release() + handler = client_handler(my_client) + log.info("client accepted: "+str(len(clients)-1)+"@"+my_client[0].socket[1][0]+":"+str(my_client[0].socket[1][1])+" users now: "+str(len(clients))) + else: + log.info("client denied: "+str(len(clients)-1)+"@"+my_client[0].socket[1][0]+":"+str(my_client[0].socket[1][1])+" blocked by ip") + my_client.socket.close() + +rtl_tcp_resetting=False #put me away + +def rtl_tcp_asyncore_reset(timeout): + global rtl_tcp_core + global rtl_tcp_resetting + if rtl_tcp_resetting: return + #print "rtl_tcp_asyncore_reset" + rtl_tcp_resetting=True + time.sleep(timeout) + try: + rtl_tcp_core.close() + except: + pass + try: + del rtl_tcp_core + except: + pass + rtl_tcp_core=rtl_tcp_asyncore() + #print asyncore.socket_map + rtl_tcp_resetting=False + +class rtl_tcp_asyncore(asyncore.dispatcher): + def __init__(self): + global server_missing_logged + asyncore.dispatcher.__init__(self) + self.ok=True + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.connect((cfg.rtl_tcp_host, cfg.rtl_tcp_port)) + self.socket.settimeout(0.1) + except: + log.error("rtl_tcp connection refused. Retrying.") + thread.start_new_thread(rtl_tcp_asyncore_reset, (1,)) + self.close() + return + + def handle_error(self): + global server_missing_logged + global rtl_tcp_connected + rtl_tcp_connected=False + exc_type, exc_value, exc_traceback = sys.exc_info() + self.ok=False + server_is_missing=hasattr(exc_value,"errno") and exc_value.errno==111 + if (not server_is_missing) or (not server_missing_logged): + log.error("with rtl_tcp host connection: "+str(exc_value)) + #traceback.print_tb(exc_traceback) + server_missing_logged|=server_is_missing + try: + self.close() + except: + pass + thread.start_new_thread(rtl_tcp_asyncore_reset, (2,)) + + def handle_connect(self): + global server_missing_logged + global rtl_tcp_connected + self.socket.settimeout(0.1) + rtl_tcp_connected=True + if self.ok: + log.info("rtl_tcp host connection estabilished") + server_missing_logged=False + + def handle_close(self): + global rtl_tcp_connected + global rtl_tcp_core + rtl_tcp_connected=False + log.error("rtl_tcp host connection has closed, now trying to reopen") + try: + self.close() + except: + pass + thread.start_new_thread(rtl_tcp_asyncore_reset, (2,)) + + def handle_read(self): + global rtl_dongle_identifier + global dsp_input_queue + global watchdog_data_count + if(len(rtl_dongle_identifier)==0): + rtl_dongle_identifier=self.recv(12) + return + new_data_buffer=self.recv(16348) + if cfg.watchdog_interval: + watchdog_data_count+=16348 + if cfg.use_dsp_command: + dsp_input_queue.put(new_data_buffer) + #print "did put anyway" + else: + add_data_to_clients(new_data_buffer) + + def writable(self): + #check if any new commands to write + global commands + return not commands.empty() + + def handle_write(self): + global commands + while not commands.empty(): + mcmd=commands.get() + self.send(mcmd) + +def xxd(data): + #diagnostic purposes only + output="" + for d in data: + output+=hex(ord(d))[2:].zfill(2)+" " + return output + +def handle_command(command, client_param): + global sample_rate + client=client_param[0] + param=array.array("I", command[1:5])[0] + param=socket.ntohl(param) + command_id=ord(command[0]) + client_info=str(client.ident)+"@"+client.socket[1][0]+":"+str(client.socket[1][1]) + if(time.time()-client.start_time client can't set anything until "+str(cfg.client_cant_set_until)+" seconds") + return 0 + if command_id == 1: + if max(map((lambda r: param>=r[0] and param<=r[1]),cfg.freq_allowed_ranges)): + log.debug("allow: "+client_info+" -> set freq "+str(param)) + return 1 + else: + log.debug("deny: "+client_info+" -> set freq - out of range: "+str(param)) + elif command_id == 2: + log.debug("deny: "+client_info+" -> set sample rate: "+str(param)) + sample_rate=param + return 0 # ordinary clients are not allowed to do this + elif command_id == 3: + log.debug("deny/allow: "+client_info+" -> set gain mode: "+str(param)) + return cfg.allow_gain_set + elif command_id == 4: + log.debug("deny/allow: "+client_info+" -> set gain: "+str(param)) + return cfg.allow_gain_set + elif command_id == 5: + log.debug("deny: "+client_info+" -> set freq correction: "+str(param)) + return 0 + elif command_id == 6: + log.debug("deny/allow: set if stage gain") + return cfg.allow_gain_set + elif command_id == 7: + log.debug("deny: set test mode") + return 0 + elif command_id == 8: + log.debug("deny/allow: set agc mode") + return cfg.allow_gain_set + elif command_id == 9: + log.debug("deny: set direct sampling") + return 0 + elif command_id == 10: + log.debug("deny: set offset tuning") + return 0 + elif command_id == 11: + log.debug("deny: set rtl xtal") + return 0 + elif command_id == 12: + log.debug("deny: set tuner xtal") + return 0 + elif command_id == 13: + log.debug("deny/allow: set tuner gain by index") + return cfg.allow_gain_set + else: + log.debug("deny: "+client_info+" sent an ivalid command: "+str(param)) + return 0 + +def watchdog_thread(): + global rtl_tcp_connected + global rtl_tcp_core + global watchdog_data_count + global sample_rate + zero_buffer_size=16348 + second_frac=10 + zero_buffer='\x7f'*zero_buffer_size + watchdog_data_count=0 + rtl_tcp_connected=False + null_fill=False + time.sleep(4) # wait before activating this thread + log.info("watchdog started") + first_start=True + n=0 + while True: + wait_altogether=cfg.watchdog_interval if rtl_tcp_connected or first_start else cfg.reconnect_interval + first_start=False + if null_fill: + log.error("watchdog: filling buffer with zeros.") + while wait_altogether>0: + wait_altogether-=1.0/second_frac + for i in range(0,((2*sample_rate)/second_frac)/zero_buffer_size): + add_data_to_clients(zero_buffer) + n+=len(zero_buffer) + time.sleep(0) #yield + if watchdog_data_count: break + if watchdog_data_count: break + time.sleep(1.0/second_frac) + #print "sent altogether",n + else: + time.sleep(wait_altogether) + null_fill=not watchdog_data_count + if not watchdog_data_count: + log.error("watchdog: restarting rtl_tcp_asyncore() now.") + rtl_tcp_asyncore_reset(0) + watchdog_data_count=0 + + + +def dsp_debug_thread(): + global dsp_data_count + global original_data_count + while 1: + time.sleep(1) + print "[rtl-mus] DSP | Original data: "+str(int(original_data_count/1000))+"kB/sec | Processed data: "+str(int(dsp_data_count/1000))+"kB/sec" + dsp_data_count = original_data_count=0 + +class client: + ident=None #id + to_close=False + waiting_data=None + start_time=None + socket=None + asyncore=None + + def close(self): + global clients_mutex + global clients + clients_mutex.acquire() + for i in range(0,len(clients)): + if clients[i][0].ident==self.ident: + try: + self.socket.close() + except: + pass + try: + self.asyncore.close() + del self.asyncore + except: + pass + break + clients_mutex.release() + + +def main(): + global server_missing_logged + global rtl_dongle_identifier + global log + global clients + global clients_mutex + global original_data_count + global dsp_input_queue + global dsp_data_count + global proc + global commands + global max_client_id + global rtl_tcp_core + global sample_rate + + # set up logging + log = logging.getLogger("rtl_mus") + log.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + stream_handler = logging.StreamHandler() + stream_handler.setLevel(logging.DEBUG) + stream_handler.setFormatter(formatter) + log.addHandler(stream_handler) + file_handler = logging.FileHandler(cfg.log_file_path) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(formatter) + log.addHandler(file_handler) + log.info("Server is UP") + + server_missing_logged=0 # Not to flood the screen with messages related to rtl_tcp disconnect + rtl_dongle_identifier='' # rtl_tcp sends some identifier on dongle type and gain values in the first few bytes right after connection + clients=[] + dsp_data_count=original_data_count=0 + commands=multiprocessing.Queue() + dsp_input_queue=multiprocessing.Queue() + clients_mutex=multiprocessing.Lock() + max_client_id=0 + sample_rate=250000 # so far only watchdog thread uses it to fill buffer up with zeros on missing input + + # start dsp threads + if cfg.use_dsp_command: + print "[rtl_mus] Opening DSP process..." + proc = subprocess.Popen (cfg.dsp_command.split(" "), stdin = subprocess.PIPE, stdout = subprocess.PIPE) #!! should fix the split :-S + dsp_read_thread_v=thread.start_new_thread(dsp_read_thread, ()) + dsp_write_thread_v=thread.start_new_thread(dsp_write_thread, ()) + if cfg.debug_dsp_command: + dsp_debug_thread_v=thread.start_new_thread(dsp_debug_thread,()) + + # start watchdog thread + if cfg.watchdog_interval != 0: + watchdog_thread_v=thread.start_new_thread(watchdog_thread,()) + + # start asyncores + rtl_tcp_core = rtl_tcp_asyncore() + server_core = server_asyncore() + + asyncore.loop(0.1) + + +if __name__=="__main__": + print + print "rtl_mus: Multi-User I/Q Data Server for RTL-SDR v0.22, made at HA5KFU Amateur Radio Club (http://ha5kfu.hu)" + print " code by Andras Retzler, HA7ILM " + print " distributed under GNU GPL v3" + print + + for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]: + if os.path.exists(libcpath): + libc = dl.open(libcpath) + libc.call("prctl", 15, "rtl_mus", 0, 0, 0) + break + + # === Load configuration script === + if len(sys.argv)==1: + print "[rtl_mus] Warning! Configuration script not specified. I will use: \"config_rtl.py\"" + config_script="config_rtl" + else: + config_script=sys.argv[1] + cfg=__import__(config_script) + if cfg.setuid_on_start: + os.setuid(cfg.uid) + main() diff --git a/rxws.py b/rxws.py new file mode 100644 index 0000000..0b42d6c --- /dev/null +++ b/rxws.py @@ -0,0 +1,148 @@ +""" +rxws: WebSocket methods implemented for OpenWebRX + +This file is part of OpenWebRX. + + OpenWebRX is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenWebRX 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenWebRX. If not, see . + +Authors: + Andras Retzler, HA7ILM + +""" + +import base64 +import sha +import select + +class WebSocketException(Exception): + pass + +def handshake(myself): + my_client_id=myself.path[4:] + my_headers=myself.headers.items() + my_header_keys=map(lambda x:x[0],my_headers) + h_key_exists=lambda x:my_header_keys.count(x) + h_value=lambda x:my_headers[my_header_keys.index(x)][1] + #print "The Lambdas(tm)" + #print h_key_exists("upgrade") + #print h_value("upgrade") + #print 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 + ws_key=h_value("sec-websocket-key") + ws_key_toreturn=base64.b64encode(sha.new(ws_key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest()) + #A sample list of keys we get: [('origin', 'http://localhost:8073'), ('upgrade', 'websocket'), ('sec-websocket-extensions', 'x-webkit-deflate-frame'), ('sec-websocket-version', '13'), ('host', 'localhost:8073'), ('sec-websocket-key', 't9J1rgy4fc9fg2Hshhnkmg=='), ('connection', 'Upgrade'), ('pragma', 'no-cache'), ('cache-control', 'no-cache')] + myself.connection.send("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "+ws_key_toreturn+"\r\nCQ-CQ-de: HA5KFU\r\n\r\n") + +def get_header(size): + #this does something similar: https://github.com/lemmingzshadow/php-websocket/blob/master/server/lib/WebSocket/Connection.php + ws_first_byte=0b10000010 # FIN=1, OP=2 + if(size>125): + ws_second_byte=126 # The following two bytes will indicate frame size + extended_size=chr((size>>8)&0xff)+chr(size&0xff) #Okay, it uses reverse byte order (little-endian) compared to anything else sent on TCP + else: + ws_second_byte=size + #256 bytes binary message in a single unmasked frame | 0x82 0x7E 0x0100 [256 bytes of binary data] + extended_size="" + return chr(ws_first_byte)+chr(ws_second_byte)+extended_size + +def code_payload(data, masking_key=""): + # both encode or decode + if masking_key=="": + key = (61, 84, 35, 6) + else: + key = [ord(i) for i in masking_key] + encoded="" + for i in range(0,len(data)): + encoded+=chr(ord(data[i])^key[i%4]) + return encoded + +def xxdg(data): + output="" + for i in range(0,len(data)/8): + output+=xxd(data[i:i+8]) + if i%2: output+="\n" + else: output+=" " + return output + + +def xxd(data): + #diagnostic purposes only + output="" + for d in data: + output+=hex(ord(d))[2:].zfill(2)+" " + return output + +def recv(myself, blocking=False, debug=False): + bufsize=70000 + myself.connection.setblocking(blocking) + if debug: print "ws_recv begin" + try: + data=myself.connection.recv(6) + #print "rxws.recv bytes:",xxd(data) + except: + if debug: print "ws_recv error" + return "" + if debug: print "ws_recv recved" + if(len(data)==0): return "" + fin=ord(data[0])&128!=0 + is_text_frame=ord(data[0])&15==1 + length=ord(data[1])&0x7f + data+=myself.connection.recv(length) + #print "rxws.recv length is ",length," (multiple packets together?) len(data) =",len(data) + has_one_byte_length=length<125 + masked=ord(data[1])&0x80!=0 + #print "len=", length, len(data)-2 + #print "fin, is_text_frame, has_one_byte_length, masked = ", (fin, is_text_frame, has_one_byte_length, masked) + #print xxd(data) + if fin and is_text_frame and has_one_byte_length: + if masked: + return code_payload(data[6:], data[2:6]) + else: + return data[2:] + +#Useful links for ideas on WebSockets: +# http://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side +# https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server +# http://tools.ietf.org/html/rfc6455#section-5.2 + + +def flush(myself): + lR,lW,lX = select.select([],[myself.connection,],[],60) + + +def send(myself, data, begin_id="", debug=0): + base_frame_size=35000 #could guess by MTU? + debug=0 + #try: + while True: + counter=0 + from_end=len(data)-counter + if from_end+len(begin_id)>base_frame_size: + data_to_send=begin_id+data[counter:counter+base_frame_size-len(begin_id)] + header=get_header(len(data_to_send)) + flush(myself) + myself.connection.send(header+data_to_send) + if debug: print "rxws.send ==================== #1 if branch :: from={0} to={1} dlen={2} hlen={3}".format(counter,counter+base_frame_size-len(begin_id),len(data_to_send),len(header)) + else: + data_to_send=begin_id+data[counter:] + header=get_header(len(data_to_send)) + flush(myself) + myself.connection.send(header+data_to_send) + if debug: print "rxws.send :: #2 else branch :: dlen={0} hlen={1}".format(len(data_to_send),len(header)) + #if debug: print "header:\n"+xxdg(header)+"\n\nws data:\n"+xxdg(data_to_send) + break + counter+=base_frame_size-len(begin_id) + #except: + # pass diff --git a/screenshot b/screenshot new file mode 100644 index 0000000..6344776 Binary files /dev/null and b/screenshot differ