initial commit
44
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:
|
||||
|
||||
- <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 >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:
|
||||
- <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 >randras@sdr.hu<.
|
||||
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
@ -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
@ -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
After Width: | Height: | Size: 318 B |
BIN
htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf
Normal file
93
htdocs/gfx/font-expletus-sans/OFL.txt
Normal 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.
|
BIN
htdocs/gfx/openwebrx-avatar-background.png
Executable file
After Width: | Height: | Size: 459 B |
BIN
htdocs/gfx/openwebrx-avatar.png
Executable file
After Width: | Height: | Size: 742 B |
BIN
htdocs/gfx/openwebrx-background-cool-blue.png
Executable file
After Width: | Height: | Size: 90 KiB |
BIN
htdocs/gfx/openwebrx-background-lingrad.png
Executable file
After Width: | Height: | Size: 679 B |
BIN
htdocs/gfx/openwebrx-logo-big.png
Executable file
After Width: | Height: | Size: 44 KiB |
BIN
htdocs/gfx/openwebrx-rx-details-arrow-up.png
Executable file
After Width: | Height: | Size: 518 B |
BIN
htdocs/gfx/openwebrx-rx-details-arrow.png
Executable file
After Width: | Height: | Size: 505 B |
BIN
htdocs/gfx/openwebrx-scale-background.png
Executable file
After Width: | Height: | Size: 64 KiB |
BIN
htdocs/gfx/openwebrx-top-logo.png
Executable file
After Width: | Height: | Size: 16 KiB |
BIN
htdocs/gfx/openwebrx-top-photo.jpg
Executable file
After Width: | Height: | Size: 125 KiB |
BIN
htdocs/gfx/webrx-ha5kfu-top-logo.png
Executable file
After Width: | Height: | Size: 2.0 KiB |
93
htdocs/index.wrx
Executable 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
@ -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
1538
htdocs/openwebrx.js~
Executable file
93
htdocs/upgrade.html
Normal 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
@ -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
BIN
plugins/__init__.pyc
Normal file
0
plugins/dsp/__init__.py
Executable file
BIN
plugins/dsp/__init__.pyc
Normal file
0
plugins/dsp/csdr/__init__.py
Executable file
BIN
plugins/dsp/csdr/__init__.pyc
Normal file
135
plugins/dsp/csdr/plugin.py
Normal 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
514
rtl_mus.py
Normal 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
@ -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
After Width: | Height: | Size: 1.2 MiB |