initial commit

This commit is contained in:
ha7ilm 2014-11-29 01:07:10 +01:00
parent 6a5dbec351
commit 59188c9b5a
34 changed files with 5247 additions and 2 deletions

View File

@ -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:
- <a href="https://github.com/simonyiszk/csdr">libcsdr</a> 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 &gt;canvas&lt;.
- 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:
- <a href="https://github.com/simonyiszk/csdr">libcsdr</a>
- <a href="http://sdr.osmocom.org/trac/wiki/rtl-sdr">rtl-sdr</a>
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 <a href="http://localhost:8073">http://localhost:8073</a>.
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 &gt;randras@sdr.hu&lt;.
I would like to maintain a list of online amateur radio receivers on <a href="http://sdr.hu/">sdr.hu</a>.
## 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).

87
config_rtl.py Executable file
View File

@ -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 <randras@sdr.hu>
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 <http://www.gnu.org/licenses/>.
'''
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)
'''

59
config_webrx.py Executable file
View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
"""
config_webrx: configuration options for OpenWebRX
OpenWebRX (c) Copyright 2013-2014 Andras Retzler <randras@sdr.hu>
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 <http://www.gnu.org/licenses/>.
"""
#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.<br />
Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/>
Device: %[RX_DEVICE]<br />
Antenna: %[RX_ANT]<br />
Website: <a href="http://localhost" target="_blank">http://localhost</a>
"""
#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).

BIN
htdocs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

View File

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

BIN
htdocs/gfx/openwebrx-avatar.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

BIN
htdocs/gfx/openwebrx-logo-big.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
htdocs/gfx/openwebrx-top-logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

93
htdocs/index.wrx Executable file
View File

@ -0,0 +1,93 @@
<!DOCTYPE HTML>
<!--
OpenWebRX (c) Copyright 2013-2014 Andras Retzler <randras@sdr.hu>
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 <http://www.gnu.org/licenses/>.
-->
<html>
<head>
<title>OpenWebRX | Open Source Web-based SDR for everyone!</title>
<script type="text/javascript">
//Local variables
client_id="%[CLIENT_ID]";
ws_url="%[WS_URL]";
rx_photo_height=%[RX_PHOTO_HEIGHT];
</script>
<script src="openwebrx.js"></script>
<link rel="stylesheet" type="text/css" href="openwebrx.css" />
<meta charset="utf-8">
</head>
<body onload="openwebrx_init();">
<div id="webrx-page-container">
<div id="webrx-top-container">
<div id="webrx-top-photo-clip">
<img src="gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
<div id="webrx-rx-photo-title">%[RX_PHOTO_TITLE]</div>
<div id="webrx-rx-photo-desc">%[RX_PHOTO_DESC]</div>
</div>
<div id="webrx-top-bar-background" class="webrx-top-bar-parts"></div>
<div id="webrx-top-bar" class="webrx-top-bar-parts">
<a href="http://openwebrx.org/" target="_blank"><img src="gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
<img id="webrx-rx-avatar-background" src="gfx/openwebrx-avatar-background.png" onclick="toggle_rx_photo();"/>
<img id="webrx-rx-avatar" src="gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/>
<div id="webrx-rx-title" onclick="toggle_rx_photo();">%[RX_TITLE]</div>
<div id="webrx-rx-desc" onclick="toggle_rx_photo();">%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, <a href="https://www.google.hu/maps/place/%[RX_GPS]" target="_blank" onclick="dont_toggle_rx_photo();">[maps]</a></div>
<div id="openwebrx-rx-details-arrow">
<a id="openwebrx-rx-details-arrow-up" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow-up.png" /></a>
<a id="openwebrx-rx-details-arrow-down" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow.png" /></a>
</div>
</div>
</div>
<div id="webrx-main-container">
<div id="openwebrx-scale-container">
<canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas>
</div>
<div id="webrx-canvas-container">
<div id="openwebrx-phantom-canvas"></div>
<!-- add canvas here by javascript -->
</div>
<div id="openwebrx-panels-container">
<div class="openwebrx-panel" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="215,70">
<div id="webrx-actual-freq">---.--- MHz</div>
<div id="webrx-mouse-freq">---.--- MHz</div>
<!--<div class="openwebrx-button" onclick="ws.send('SET mod=wfm');" >WFM</div>-->
<div class="openwebrx-button" onclick="demodulator_analog_replace('nfm');">FM</div>
<div class="openwebrx-button" onclick="demodulator_analog_replace('am');">AM</div>
<div class="openwebrx-button" onclick="demodulator_analog_replace('lsb');">LSB</div>
<div class="openwebrx-button" onclick="demodulator_analog_replace('usb');">USB</div>
<div class="openwebrx-button" onclick="demodulator_analog_replace('cw');">CW</div>
</div>
<div class="openwebrx-panel" id="webrx-config" data-panel-name="debug" data-panel-pos="left" data-panel-order="0" data-panel-size="585,130">
<div class="openwebrx-panel-inner">
<div id="openwebrx-client-log-title">openwebrx.js (beta) client log </strong><span id="openwebrx-problems"></span></div>
Author: <a href="javascript:sendmail2('pi7qtu=alz$pc');">HA7ILM</a>. Please send me bug reports and suggestions.<br/>
Client status: <span id="openwebrx-client-status">
<span id="openwebrx-audio-sps"></span><br/>
<!--Server status: <span id="openwebrx-server-status">no information</span><br/>-->
Your client ID is: <em>%[CLIENT_ID]</em><br />
<div id="openwebrx-debugdiv"></div>
</div>
</div>
<div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="none" data-panel-order="0" data-panel-size="245,55" style="background-color: Red;">
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
<br />We're working on the code right now, so the application might fail.
</div>
</div>
</div>
</div>
</body>
</html>

433
htdocs/openwebrx.css Executable file
View File

@ -0,0 +1,433 @@
/*
OpenWebRX (c) Copyright 2013-2014 Andras Retzler <randras@sdr.hu>
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 <http://www.gnu.org/licenses/>.
*/
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;
}

1634
htdocs/openwebrx.js Executable file

File diff suppressed because it is too large Load Diff

1538
htdocs/openwebrx.js~ Executable file

File diff suppressed because it is too large Load Diff

93
htdocs/upgrade.html Normal file
View File

@ -0,0 +1,93 @@
<html>
<!--
OpenWebRX (c) Copyright 2013 Andras Retzler <ha7ilm@sdr.hu>
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 <http://www.gnu.org/licenses/>.
-->
<head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
html, body
{
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
width: 100%;
text-align: center;
margin: 0;
padding: 0;
}
img.logo
{
margin-top: 120px;
}
div.frame
{
text-align: left;
margin:0px auto;
width: 800px;
}
div.panel
{
text-align: center;
background-color:#777777;
border-radius: 15px;
padding: 12px;
font-weight: bold;
color: White;
font-size: 13pt;
/*text-shadow: 1px 1px 4px #444;*/
font-family: sans;
}
div.alt
{
font-size: 10pt;
padding-top: 10px;
}
body div a
{
color: #5ca8ff;
text-shadow: none;
}
span.browser
{
}
</style>
<script>
var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});}
var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); }
</script>
</head>
<body>
<div class="frame">
<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/>
<div class="panel">
Only the latest <span class="browser">Google Chrome</span> browser is supported at the moment.<br/>
Please <a href="http://chrome.google.com/">download and install Google Chrome.</a><br />
<div class="alt">
Alternatively, you may proceed to OpenWebRX, but it's not supposed to work as expected. <br />
<a href="/?unsupported">Click here</a> if you still want to try OpenWebRX.</a>
</div>
</div>
</div>
</body>
</html>

378
openwebrx.py Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
Authors:
Andras Retzler, HA7ILM <randras@sdr.hu>
"""
# 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 <randras@sdr.hu>"
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("<html><body><h1>Object moved</h1>Please <a href=\"/upgrade.html\">click here</a> to continue.</body></html>")
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()

0
plugins/__init__.py Executable file
View File

BIN
plugins/__init__.pyc Normal file

Binary file not shown.

0
plugins/dsp/__init__.py Executable file
View File

BIN
plugins/dsp/__init__.pyc Normal file

Binary file not shown.

0
plugins/dsp/csdr/__init__.py Executable file
View File

Binary file not shown.

135
plugins/dsp/csdr/plugin.py Normal file
View File

@ -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)

BIN
plugins/dsp/csdr/plugin.pyc Normal file

Binary file not shown.

514
rtl_mus.py Normal file
View File

@ -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 <randras@sdr.hu>
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 <http://www.gnu.org/licenses/>.
-----
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<cfg.client_cant_set_until and not (cfg.first_client_can_set and client.ident==0) ):
log.info("deny: "+client_info+" -> 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 <randras@sdr.hu>"
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()

148
rxws.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
Authors:
Andras Retzler, HA7ILM <retzlerandras@gmail.com>
"""
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

BIN
screenshot Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB