├── .gitignore ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── ICLA.txt ├── LICENSE.txt ├── README.md ├── config_webrx.py ├── csdr.py ├── htdocs ├── favicon.ico ├── gfx │ ├── font-expletus-sans │ │ ├── ExpletusSans-Medium.ttf │ │ └── OFL.txt │ ├── openwebrx-3d-spectrum.png │ ├── openwebrx-avatar-background.png │ ├── openwebrx-avatar.png │ ├── openwebrx-background-cool-blue.png │ ├── openwebrx-background-lingrad.png │ ├── openwebrx-ha5kfu-top-logo.png │ ├── openwebrx-logo-big.png │ ├── openwebrx-panel-log.png │ ├── openwebrx-panel-receiver.png │ ├── openwebrx-panel-status.png │ ├── openwebrx-play-button.png │ ├── openwebrx-rx-details-arrow-up.png │ ├── openwebrx-rx-details-arrow.png │ ├── openwebrx-scale-background.png │ ├── openwebrx-speaker-muted.png │ ├── openwebrx-speaker.png │ ├── openwebrx-squelch-button.png │ ├── openwebrx-top-logo.png │ ├── openwebrx-top-photo.jpg │ ├── openwebrx-waterfall-auto.png │ ├── openwebrx-waterfall-default.png │ ├── openwebrx-zoom-in-total.png │ ├── openwebrx-zoom-in.png │ ├── openwebrx-zoom-out-total.png │ └── openwebrx-zoom-out.png ├── inactive.html ├── index.wrx ├── jquery-3.2.1.min.js ├── jquery.nanoscroller.js ├── mathbox-bundle.min.js ├── mathbox.css ├── nanoscroller.css ├── openwebrx.css ├── openwebrx.js ├── retry.html ├── sdr.js └── upgrade.html ├── openwebrx.py ├── rxws.py └── sdrhu.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | tags 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | First of all, thank you for taking the time to contribute to this project! 2 | 3 | Before I can accept your contributions, I need a signed copy of the Individual Contributor License Agreement (ICLA) from you, which is available here. 4 | 5 | The ICLA is needed because it will allow me to dual license the OpenWebRX project under AGPL and a commercial license. 6 | I will also apply dual licensing to csdr, but only those parts that are original work (e.g. without the parts enabled by `-DUSE_IMA_ADPCM`; code taken from other projects is clearly separable). 7 | 8 | However, even if there is commercial interest in the projects, I promise to keep them as open as possible, keeping my original intention to provide an open-source web-based SDR receiver software to the amateur radio operators and SDR enthusiasts. 9 | 10 | This contributor agreement is based on the one of Apache Software Foundation, with some modifications. (You can review differences here). 11 | When you contribute for the first time, I will send you the ICLA. Replying with only the information requested and the text "I Agree" is sufficient. 12 | 13 | Thanks, 14 | 15 | Andras, HA7ILM 16 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | This is a list of the great people who contributed code to the OpenWebRX repository. (Names are sorted alphabetically.) 2 | 3 | Gnoxter 4 | John Seamons, ZL/KF6VO 5 | 6 | -------------------------------------------------------------------------------- /ICLA.txt: -------------------------------------------------------------------------------- 1 | Individual Contributor License Agreement ("Agreement") 2 | 3 | In order to clarify the intellectual property license granted 4 | with Contributions from any person or entity, Retzler András 5 | (hereinafter referred to as "Project Owner") must have a 6 | Contributor License Agreement ("CLA") on file that has 7 | been signed by each Contributor, indicating agreement to the license 8 | terms below. This license is for your protection as a Contributor as 9 | well as the protection of the Project Owner; it does not change your 10 | rights to use your own Contributions for any other purpose. 11 | Please read this document carefully before signing and keep a copy 12 | for your records. 13 | 14 | Full name: ______________________________________________________ 15 | 16 | (optional) Public name: _________________________________________ 17 | 18 | Mailing Address: ________________________________________________ 19 | 20 | ________________________________________________ 21 | 22 | Country: ______________________________________________________ 23 | 24 | (optional) Telephone: ___________________________________________ 25 | 26 | E-Mail: ______________________________________________________ 27 | 28 | You accept and agree to the following terms and conditions for Your 29 | present and future Contributions submitted to the Project Owner. 30 | 31 | Except for the license granted herein to the Project Owner and recipients 32 | of software distributed by the Project Owner, You reserve all right, title, 33 | and interest in and to Your Contributions. 34 | 35 | 1. Definitions. 36 | 37 | "You" (or "Your") shall mean the copyright owner or legal entity 38 | authorized by the copyright owner that is making this Agreement 39 | with the Project Owner. For legal entities, the entity making a 40 | Contribution and all other entities that control, are controlled 41 | by, or are under common control with that entity are considered to 42 | be a single Contributor. For the purposes of this definition, 43 | "control" means (i) the power, direct or indirect, to cause the 44 | direction or management of such entity, whether by contract or 45 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 46 | outstanding shares, or (iii) beneficial ownership of such entity. 47 | 48 | "Contribution" shall mean any original work of authorship, 49 | including any modifications or additions to an existing work, that 50 | is intentionally submitted by You to the Project Owner for inclusion 51 | in, or documentation of, any of the products owned or managed by 52 | the Project Owner (the "Work"). For the purposes of this definition, 53 | "submitted" means any form of electronic, verbal, or written 54 | communication sent to the Project Owner or its representatives, 55 | including but not limited to communication on electronic mailing 56 | lists, source code control systems, and issue tracking systems that 57 | are managed by, or on behalf of, the Project Owner for the purpose of 58 | discussing and improving the Work, but excluding communication that 59 | is conspicuously marked or otherwise designated in writing by You 60 | as "Not a Contribution." 61 | 62 | 2. Grant of Copyright License. Subject to the terms and conditions of 63 | this Agreement, You hereby grant to the Project Owner and to 64 | recipients of software distributed by the Project Owner a perpetual, 65 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 66 | copyright license to reproduce, prepare derivative works of, 67 | publicly display, publicly perform, sublicense, and distribute Your 68 | Contributions and such derivative works. 69 | 70 | 3. Grant of Patent License. Subject to the terms and conditions of 71 | this Agreement, You hereby grant to the Project Owner and to 72 | recipients of software distributed by the Project Owner a perpetual, 73 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 74 | (except as stated in this section) patent license to make, have 75 | made, use, offer to sell, sell, import, and otherwise transfer the 76 | Work, where such license applies only to those patent claims 77 | licensable by You that are necessarily infringed by Your 78 | Contribution(s) alone or by combination of Your Contribution(s) 79 | with the Work to which such Contribution(s) was submitted. If any 80 | entity institutes patent litigation against You or any other entity 81 | (including a cross-claim or counterclaim in a lawsuit) alleging 82 | that your Contribution, or the Work to which you have contributed, 83 | constitutes direct or contributory patent infringement, then any 84 | patent licenses granted to that entity under this Agreement for 85 | that Contribution or Work shall terminate as of the date such 86 | litigation is filed. 87 | 88 | 4. You represent that you are legally entitled to grant the above 89 | license. If your employer(s) has rights to intellectual property 90 | that you create that includes your Contributions, you represent 91 | that you have received permission to make Contributions on behalf 92 | of that employer, that your employer has waived such rights for 93 | your Contributions to the Project Owner, or that your employer has 94 | executed a separate Corporate CLA with the Project Owner. 95 | 96 | 5. You represent that each of Your Contributions is Your original 97 | creation (see section 7 for submissions on behalf of others). You 98 | represent that Your Contribution submissions include complete 99 | details of any third-party license or other restriction (including, 100 | but not limited to, related patents and trademarks) of which you 101 | are personally aware and which are associated with any part of Your 102 | Contributions. 103 | 104 | 6. You are not expected to provide support for Your Contributions, 105 | except to the extent You desire to provide support. You may provide 106 | support for free, for a fee, or not at all. Unless required by 107 | applicable law or agreed to in writing, You provide Your 108 | Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 109 | OF ANY KIND, either express or implied, including, without 110 | limitation, any warranties or conditions of TITLE, NON- 111 | INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 112 | 113 | 7. Should You wish to submit work that is not Your original creation, 114 | You may submit it to the Project Owner separately from any 115 | Contribution, identifying the complete details of its source and of 116 | any license or other restriction (including, but not limited to, 117 | related patents, trademarks, and license agreements) of which you 118 | are personally aware, and conspicuously marking the work as 119 | "Submitted on behalf of a third-party: [named here]". 120 | 121 | 8. You agree to notify the Project Owner of any facts or circumstances of 122 | which you become aware that would make these representations 123 | inaccurate in any respect. 124 | 125 | Please sign: __________________________________ Date: ________________ 126 | 127 | Text derived from the Apache Individual Contributor License Agreement 128 | ("Agreement") V2.0, available at http://apache.org/licenses/icla.txt 129 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenWebRX 2 | 3 | OpenWebRX is a multi-user SDR receiver software with a web interface. 4 | 5 | ---- 6 | 7 | ### ⚠️ From 2019-12-29 OpenWebRX development is discontinued. ⚠️ 8 | 9 | I'm would like to say a big thanks to everyone who supported me during this project, including those who contributed either code or donations. It has been a very fruitful 6 years, but now it's time to move on to other projects. See also my [blog](https://blog.sdr.hu) about that. 10 | 11 | (@simonyiszk, please keep this GitHub repo for historic purposes.) 12 | 13 | Know limitations of the last version: 14 | 15 | - Python 2.7, a main dependency of the project, will be not be officially maintained from 1 January 2020. By time, probably it will not be secure to use this version on public servers, unless someone still provides security patches for Python 2. 16 | - Some specific parts of the DSP code could be improved for better SNR. 17 | 18 | Even though these limitations are probably acceptable in an amateur radio project, I would not build critical infrastructure on it. 19 | 20 | For commercial inquiries (e.g. if someone wants me to develop an improved version without these limitations), I'm still open, [drop me an e-mail](mailto:randras@sdr.hu). 21 | 22 | ---- 23 | 24 | [:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/) 25 | 26 | ![OpenWebRX](http://blog.sdr.hu/images/openwebrx/screenshot.png) 27 | 28 | It has the following features: 29 | 30 | - csdr based demodulators (AM/FM/SSB/CW/BPSK31), 31 | - filter passband can be set from GUI, 32 | - waterfall display can be shifted back in time, 33 | - it extensively uses HTML5 features like WebSocket, Web Audio API, and <canvas>, 34 | - it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28), 35 | - currently supports RTL-SDR, HackRF, SDRplay, AirSpy and many other devices, see the OpenWebRX Wiki, 36 | - it has a 3D waterfall display: 37 | 38 | ![OpenWebRX 3D waterfall](http://blog.sdr.hu/images/openwebrx/screenshot-3d.gif) 39 | 40 | **News (2015-08-18)** 41 | - My BSc. thesis written on OpenWebRX is available here. 42 | - Several bugs were fixed to improve reliability and stability. 43 | - OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.) 44 | - OpenWebRX now uses sdr.js (*libcsdr* compiled to JavaScript) for some client-side DSP tasks. 45 | - Receivers can now be listed on SDR.hu. 46 | - License for OpenWebRX is now Affero GPL v3. 47 | 48 | **News (2016-02-14)** 49 | - The DDC in *csdr* has been manually optimized for ARM NEON, so it runs around 3 times faster on the Raspberry Pi 2 than before. 50 | - Also we use *ncat* instead of *rtl_mus*, and it is 3 times faster in some cases. 51 | - OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb` 52 | - UI improvements were made, thanks to John Seamons and Gnoxter. 53 | 54 | **News (2017-04-04)** 55 | - *ncat* has been replaced with a custom implementation called *nmux* due to a bug that caused regular crashes on some machines. The *nmux* tool is part of the *csdr* package. 56 | - Most consumer SDR devices are supported via rx_tools, see the OpenWebRX Wiki on that. 57 | 58 | **News (2017-07-12)** 59 | - OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display. 60 | 61 | > When upgrading OpenWebRX, please make sure that you also upgrade *csdr*! 62 | 63 | ## OpenWebRX servers on SDR.hu 64 | 65 | [SDR.hu](http://sdr.hu) is a site which lists the active, public OpenWebRX servers. Your receiver [can also be part of it](http://sdr.hu/openwebrx), if you want. 66 | 67 | ![sdr.hu](http://blog.sdr.hu/images/openwebrx/screenshot-sdrhu.png) 68 | 69 | ## Setup 70 | 71 | OpenWebRX currently requires Linux and python 2.7 to run. 72 | 73 | First you will need to install the dependencies: 74 | 75 | - libcsdr 76 | - rtl-sdr 77 | 78 | After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: 79 | 80 | python openwebrx.py 81 | 82 | You can now open the GUI at http://localhost:8073. 83 | 84 | Please note that the server is also listening on the following ports (on localhost only): 85 | 86 | - port 4951 for the multi-user I/Q server. 87 | 88 | Now the next step is to customize the parameters of your server in `config_webrx.py`. 89 | 90 | Actually, if you do something cool with OpenWebRX, please drop me a mail: 91 | *Andras Retzler, HA7ILM <randras@sdr.hu>* 92 | 93 | ## Usage tips 94 | 95 | You can zoom the waterfall display by the mouse wheel. You can also drag the waterfall to pan across it. 96 | 97 | The filter envelope can be dragged at its ends and moved around to set the passband. 98 | 99 | However, if you hold down the shift key, you can drag the center line (BFO) or the whole passband (PBS). 100 | 101 | ## Setup tips 102 | 103 | If you have any problems installing OpenWebRX, you should check out the Wiki about it, which has a page on the common problems and their solutions. 104 | 105 | Sometimes the actual error message is not at the end of the terminal output, you may have to look at the whole output to find it. 106 | 107 | If you want to run OpenWebRX on a remote server instead of *localhost*, do not forget to set *server_hostname* in `config_webrx.py`. 108 | 109 | ## Licensing 110 | 111 | OpenWebRX is available under Affero GPL v3 license (summary). 112 | 113 | OpenWebRX is also available under a commercial license on request. Please contact me at the address *<randras@sdr.hu>* for licensing options. 114 | -------------------------------------------------------------------------------- /config_webrx.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | config_webrx: configuration options for OpenWebRX 5 | 6 | This file is part of OpenWebRX, 7 | an open-source SDR receiver software with a web UI. 8 | Copyright (c) 2013-2015 by Andras Retzler 9 | 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU Affero General Public License as 12 | published by the Free Software Foundation, either version 3 of the 13 | License, or (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU Affero General Public License for more details. 19 | 20 | You should have received a copy of the GNU Affero General Public License 21 | along with this program. If not, see . 22 | 23 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 24 | 25 | In addition, as a special exception, the copyright holders 26 | state that config_rtl.py and config_webrx.py are not part of the 27 | Corresponding Source defined in GNU AGPL version 3 section 1. 28 | 29 | (It means that you do not have to redistribute config_rtl.py and 30 | config_webrx.py if you make any changes to these two configuration files, 31 | and use them for running your web service with OpenWebRX.) 32 | """ 33 | 34 | # NOTE: you can find additional information about configuring OpenWebRX in the Wiki: 35 | # https://github.com/simonyiszk/openwebrx/wiki 36 | 37 | # ==== Server settings ==== 38 | web_port=8073 39 | server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket) 40 | max_clients=20 41 | 42 | # ==== Web GUI configuration ==== 43 | receiver_name="[Callsign]" 44 | receiver_location="Budapest, Hungary" 45 | receiver_qra="JN97ML" 46 | receiver_asl=200 47 | receiver_ant="Longwire" 48 | receiver_device="RTL-SDR" 49 | receiver_admin="example@example.com" 50 | receiver_gps=(47.000000,19.000000) 51 | photo_height=350 52 | photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory" 53 | photo_desc=""" 54 | You can add your own background photo and receiver information.
55 | Receiver is operated by: %[RX_ADMIN]
56 | Device: %[RX_DEVICE]
57 | Antenna: %[RX_ANT]
58 | Website: http://localhost 59 | """ 60 | 61 | # ==== sdr.hu listing ==== 62 | # If you want your ham receiver to be listed publicly on sdr.hu, then take the following steps: 63 | # 1. Register at: http://sdr.hu/register 64 | # 2. You will get an unique key by email. Copy it and paste here: 65 | sdrhu_key = "" 66 | # 3. Set this setting to True to enable listing: 67 | sdrhu_public_listing = False 68 | 69 | # ==== DSP/RX settings ==== 70 | fft_fps=9 71 | fft_size=4096 #Should be power of 2 72 | fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. 73 | 74 | # samp_rate = 250000 75 | samp_rate = 2400000 76 | center_freq = 144250000 77 | rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode. 78 | ppm = 0 79 | 80 | audio_compression="adpcm" #valid values: "adpcm", "none" 81 | fft_compression="adpcm" #valid values: "adpcm", "none" 82 | 83 | digimodes_enable=True #Decoding digimodes come with higher CPU usage. 84 | digimodes_fft_size=1024 85 | 86 | start_rtl_thread=True 87 | 88 | """ 89 | Note: if you experience audio underruns while CPU usage is 100%, you can: 90 | - decrease `samp_rate`, 91 | - set `fft_voverlap_factor` to 0, 92 | - decrease `fft_fps` and `fft_size`, 93 | - limit the number of users by decreasing `max_clients`. 94 | """ 95 | 96 | # ==== I/Q sources ==== 97 | # (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.) 98 | 99 | ################################################################################################# 100 | # Is my SDR hardware supported? # 101 | # Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support # 102 | ################################################################################################# 103 | 104 | # You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR): 105 | 106 | # >> RTL-SDR via rtl_sdr 107 | start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) 108 | format_conversion="csdr convert_u8_f" 109 | 110 | #lna_gain=8 111 | #rf_amp=1 112 | #start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain) 113 | #format_conversion="csdr convert_s8_f" 114 | """ 115 | To use a HackRF, compile the HackRF host tools from its "stdout" branch: 116 | git clone https://github.com/mossmann/hackrf/ 117 | cd hackrf 118 | git fetch 119 | git checkout origin/stdout 120 | cd host 121 | mkdir build 122 | cd build 123 | cmake .. -DINSTALL_UDEV_RULES=ON 124 | make 125 | sudo make install 126 | """ 127 | 128 | # >> Sound card SDR (needs ALSA) 129 | # I did not have the chance to properly test it. 130 | #samp_rate = 96000 131 | #start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate) 132 | #format_conversion="csdr convert_s16_f | csdr gain_ff 30" 133 | 134 | # >> /dev/urandom test signal source 135 | # samp_rate = 2400000 136 | # start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) 137 | # format_conversion="csdr convert_u8_f" 138 | 139 | # >> Pre-recorded raw I/Q file as signal source 140 | # You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file. 141 | #start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05) 142 | #format_conversion="csdr convert_u8_f" 143 | 144 | #>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc. 145 | # It will auto-detect your SDR hardware if the following tools are installed: 146 | # * the vendor provided driver and library, 147 | # * the vendor-specific SoapySDR wrapper library, 148 | # * and SoapySDR itself. 149 | # Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/ 150 | #start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) 151 | #format_conversion="" 152 | 153 | # >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source) 154 | #start_rtl_command="cat /tmp/osmocom_fifo" 155 | #format_conversion="" 156 | 157 | # ==== Misc settings ==== 158 | 159 | shown_center_freq = center_freq #you can change this if you use an upconverter 160 | 161 | client_audio_buffer_size = 5 162 | #increasing client_audio_buffer_size will: 163 | # - also increase the latency 164 | # - decrease the chance of audio underruns 165 | 166 | start_freq = center_freq 167 | start_mod = "nfm" #nfm, am, lsb, usb, cw 168 | 169 | iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. 170 | 171 | #access_log = "~/openwebrx_access.log" 172 | 173 | # ==== Color themes ==== 174 | 175 | #A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels 176 | 177 | ### default theme by teejez: 178 | waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]" 179 | waterfall_min_level = -88 #in dB 180 | waterfall_max_level = -20 181 | waterfall_auto_level_margin = (5, 40) 182 | ### old theme by HA7ILM: 183 | #waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" 184 | #waterfall_min_level = -115 #in dB 185 | #waterfall_max_level = 0 186 | #waterfall_auto_level_margin = (20, 30) 187 | ##For the old colors, you might also want to set [fft_voverlap_factor] to 0. 188 | 189 | #Note: When the auto waterfall level button is clicked, the following happens: 190 | # [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]] 191 | # [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] 192 | # 193 | # ___|____________________________________|____________________________________|____________________________________|___> signal power 194 | # \_waterfall_auto_level_margin[0]_/ |__ current_min_power_level | \_waterfall_auto_level_margin[1]_/ 195 | # current_max_power_level __| 196 | 197 | # 3D view settings 198 | mathbox_waterfall_frequency_resolution = 128 #bins 199 | mathbox_waterfall_history_length = 10 #seconds 200 | mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" 201 | 202 | # === Experimental settings === 203 | #Warning! The settings below are very experimental. 204 | csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr. 205 | csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes. 206 | csdr_through = False # Setting this True will print out how much data is going into the DSP chains. 207 | 208 | nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux. 209 | 210 | #Look up external IP address automatically from icanhazip.com, and use it as [server_hostname] 211 | """ 212 | print "[openwebrx-config] Detecting external IP address..." 213 | import urllib2 214 | server_hostname=urllib2.urlopen("http://icanhazip.com").read()[:-1] 215 | print "[openwebrx-config] External IP address detected:", server_hostname 216 | """ 217 | -------------------------------------------------------------------------------- /csdr.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenWebRX csdr plugin: do the signal processing with csdr 3 | 4 | This file is part of OpenWebRX, 5 | an open-source SDR receiver software with a web UI. 6 | Copyright (c) 2013-2015 by Andras Retzler 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU Affero General Public License as 10 | published by the Free Software Foundation, either version 3 of the 11 | License, or (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU Affero General Public License for more details. 17 | 18 | You should have received a copy of the GNU Affero General Public License 19 | along with this program. If not, see . 20 | 21 | """ 22 | 23 | import subprocess 24 | import time 25 | import os 26 | import code 27 | import signal 28 | import fcntl 29 | 30 | class dsp: 31 | 32 | def __init__(self): 33 | self.samp_rate = 250000 34 | self.output_rate = 11025 #this is default, and cannot be set at the moment 35 | self.fft_size = 1024 36 | self.fft_fps = 5 37 | self.offset_freq = 0 38 | self.low_cut = -4000 39 | self.high_cut = 4000 40 | self.bpf_transition_bw = 320 #Hz, and this is a constant 41 | self.ddc_transition_bw_rate = 0.15 # of the IF sample rate 42 | self.running = False 43 | self.secondary_processes_running = False 44 | self.audio_compression = "none" 45 | self.fft_compression = "none" 46 | self.demodulator = "nfm" 47 | self.name = "csdr" 48 | self.format_conversion = "csdr convert_u8_f" 49 | self.base_bufsize = 512 50 | self.nc_port = 4951 51 | self.csdr_dynamic_bufsize = False 52 | self.csdr_print_bufsizes = False 53 | self.csdr_through = False 54 | self.squelch_level = 0 55 | self.fft_averages = 50 56 | self.iqtee = False 57 | self.iqtee2 = False 58 | self.secondary_demodulator = None 59 | self.secondary_fft_size = 1024 60 | self.secondary_process_fft = None 61 | self.secondary_process_demod = None 62 | self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "iqtee_pipe", "iqtee2_pipe"] 63 | self.secondary_pipe_names=["secondary_shift_pipe"] 64 | self.secondary_offset_freq = 1000 65 | 66 | def chain(self,which): 67 | any_chain_base="nc -v 127.0.0.1 {nc_port} | " 68 | if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | " 69 | if self.csdr_through: any_chain_base+="csdr through | " 70 | any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | " 71 | if which == "fft": 72 | fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \ 73 | ("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \ 74 | "csdr fft_exchange_sides_ff {fft_size}" 75 | if self.fft_compression=="adpcm": 76 | return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}" 77 | else: 78 | return fft_chain_base 79 | chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | " 80 | if self.secondary_demodulator: 81 | chain_begin+="csdr tee {iqtee_pipe} | " 82 | chain_begin+="csdr tee {iqtee2_pipe} | " 83 | chain_end = "" 84 | if self.audio_compression=="adpcm": 85 | chain_end = " | csdr encode_ima_adpcm_i16_u8" 86 | if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr fastagc_ff 1024 | csdr convert_f_s16"+chain_end 87 | elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end 88 | elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end 89 | 90 | def secondary_chain(self, which): 91 | secondary_chain_base="cat {input_pipe} | " 92 | if which == "fft": 93 | return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "") 94 | elif which == "bpsk31": 95 | return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ 96 | "csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \ 97 | "csdr simple_agc_cc 0.001 0.5 | " + \ 98 | "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ 99 | "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ 100 | "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" 101 | 102 | def set_secondary_demodulator(self, what): 103 | self.secondary_demodulator = what 104 | 105 | def secondary_fft_block_size(self): 106 | return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here 107 | 108 | def secondary_decimation(self): 109 | return 1 #currently unused 110 | 111 | def secondary_bpf_cutoff(self): 112 | if self.secondary_demodulator == "bpsk31": 113 | return (31.25/2) / self.if_samp_rate() 114 | return 0 115 | 116 | def secondary_bpf_transition_bw(self): 117 | if self.secondary_demodulator == "bpsk31": 118 | return (31.25/2) / self.if_samp_rate() 119 | return 0 120 | 121 | def secondary_samples_per_bits(self): 122 | if self.secondary_demodulator == "bpsk31": 123 | return int(round(self.if_samp_rate()/31.25))&~3 124 | return 0 125 | 126 | def secondary_bw(self): 127 | if self.secondary_demodulator == "bpsk31": 128 | return 31.25 129 | 130 | def start_secondary_demodulator(self): 131 | if(not self.secondary_demodulator): return 132 | print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate() 133 | secondary_command_fft=self.secondary_chain("fft") 134 | secondary_command_demod=self.secondary_chain(self.secondary_demodulator) 135 | self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) 136 | 137 | secondary_command_fft=secondary_command_fft.format( \ 138 | input_pipe=self.iqtee_pipe, \ 139 | secondary_fft_input_size=self.secondary_fft_size, \ 140 | secondary_fft_size=self.secondary_fft_size, \ 141 | secondary_fft_block_size=self.secondary_fft_block_size(), \ 142 | ) 143 | secondary_command_demod=secondary_command_demod.format( \ 144 | input_pipe=self.iqtee2_pipe, \ 145 | secondary_shift_pipe=self.secondary_shift_pipe, \ 146 | secondary_decimation=self.secondary_decimation(), \ 147 | secondary_samples_per_bits=self.secondary_samples_per_bits(), \ 148 | secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \ 149 | secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \ 150 | if_samp_rate=self.if_samp_rate() 151 | ) 152 | 153 | print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft 154 | print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod 155 | #code.interact(local=locals()) 156 | my_env=os.environ.copy() 157 | #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; 158 | if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; 159 | self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) 160 | print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)" 161 | self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes 162 | print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes 163 | self.secondary_processes_running = True 164 | 165 | #open control pipes for csdr and send initialization data 166 | # print "==========> 1" 167 | if self.secondary_shift_pipe != None: #TODO digimodes 168 | # print "==========> 2", self.secondary_shift_pipe 169 | self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes 170 | # print "==========> 3" 171 | self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes 172 | # print "==========> 4" 173 | 174 | self.set_pipe_nonblocking(self.secondary_process_demod.stdout) 175 | self.set_pipe_nonblocking(self.secondary_process_fft.stdout) 176 | 177 | def set_secondary_offset_freq(self, value): 178 | self.secondary_offset_freq=value 179 | if self.secondary_processes_running: 180 | self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate())) 181 | self.secondary_shift_pipe_file.flush() 182 | 183 | def stop_secondary_demodulator(self): 184 | if self.secondary_processes_running == False: return 185 | self.try_delete_pipes(self.secondary_pipe_names) 186 | if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM) 187 | if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM) 188 | self.secondary_processes_running = False 189 | 190 | def read_secondary_demod(self, size): 191 | return self.secondary_process_demod.stdout.read(size) 192 | 193 | def read_secondary_fft(self, size): 194 | return self.secondary_process_fft.stdout.read(size) 195 | 196 | def get_secondary_demodulator(self): 197 | return self.secondary_demodulator 198 | 199 | def set_secondary_fft_size(self,secondary_fft_size): 200 | #to change this, restart is required 201 | self.secondary_fft_size=secondary_fft_size 202 | 203 | def set_audio_compression(self,what): 204 | self.audio_compression = what 205 | 206 | def set_fft_compression(self,what): 207 | self.fft_compression = what 208 | 209 | def get_fft_bytes_to_read(self): 210 | if self.fft_compression=="none": return self.fft_size*4 211 | if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2) 212 | 213 | def get_secondary_fft_bytes_to_read(self): 214 | if self.fft_compression=="none": return self.secondary_fft_size*4 215 | if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) 216 | 217 | def set_samp_rate(self,samp_rate): 218 | #to change this, restart is required 219 | self.samp_rate=samp_rate 220 | self.decimation=1 221 | while self.samp_rate/(self.decimation+1)>self.output_rate: 222 | self.decimation+=1 223 | self.last_decimation=float(self.if_samp_rate())/self.output_rate 224 | 225 | def if_samp_rate(self): 226 | return self.samp_rate/self.decimation 227 | 228 | def get_name(self): 229 | return self.name 230 | 231 | def get_output_rate(self): 232 | return self.output_rate 233 | 234 | def set_output_rate(self,output_rate): 235 | self.output_rate=output_rate 236 | self.set_samp_rate(self.samp_rate) #as it depends on output_rate 237 | 238 | def set_demodulator(self,demodulator): 239 | #to change this, restart is required 240 | self.demodulator=demodulator 241 | 242 | def get_demodulator(self): 243 | return self.demodulator 244 | 245 | def set_fft_size(self,fft_size): 246 | #to change this, restart is required 247 | self.fft_size=fft_size 248 | 249 | def set_fft_fps(self,fft_fps): 250 | #to change this, restart is required 251 | self.fft_fps=fft_fps 252 | 253 | def set_fft_averages(self,fft_averages): 254 | #to change this, restart is required 255 | self.fft_averages=fft_averages 256 | 257 | def fft_block_size(self): 258 | if self.fft_averages == 0: return self.samp_rate/self.fft_fps 259 | else: return self.samp_rate/self.fft_fps/self.fft_averages 260 | 261 | def set_format_conversion(self,format_conversion): 262 | self.format_conversion=format_conversion 263 | 264 | def set_offset_freq(self,offset_freq): 265 | self.offset_freq=offset_freq 266 | if self.running: 267 | self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate)) 268 | self.shift_pipe_file.flush() 269 | 270 | def set_bpf(self,low_cut,high_cut): 271 | self.low_cut=low_cut 272 | self.high_cut=high_cut 273 | if self.running: 274 | self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) ) 275 | self.bpf_pipe_file.flush() 276 | 277 | def get_bpf(self): 278 | return [self.low_cut, self.high_cut] 279 | 280 | def set_squelch_level(self, squelch_level): 281 | self.squelch_level=squelch_level 282 | if self.running: 283 | self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) ) 284 | self.squelch_pipe_file.flush() 285 | 286 | def get_smeter_level(self): 287 | if self.running: 288 | line=self.smeter_pipe_file.readline() 289 | return float(line[:-1]) 290 | 291 | def mkfifo(self,path): 292 | try: 293 | os.unlink(path) 294 | except: 295 | pass 296 | os.mkfifo(path) 297 | 298 | def ddc_transition_bw(self): 299 | return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate)) 300 | 301 | def try_create_pipes(self, pipe_names, command_base): 302 | # print "try_create_pipes" 303 | for pipe_name in pipe_names: 304 | # print "\t"+pipe_name 305 | if "{"+pipe_name+"}" in command_base: 306 | setattr(self, pipe_name, self.pipe_base_path+pipe_name) 307 | self.mkfifo(getattr(self, pipe_name)) 308 | else: 309 | setattr(self, pipe_name, None) 310 | 311 | def try_delete_pipes(self, pipe_names): 312 | for pipe_name in pipe_names: 313 | pipe_path = getattr(self,pipe_name,None) 314 | if pipe_path: 315 | try: os.unlink(pipe_path) 316 | except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e 317 | 318 | def set_pipe_nonblocking(self, pipe): 319 | flags = fcntl.fcntl(pipe, fcntl.F_GETFL) 320 | fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK) 321 | 322 | def start(self): 323 | command_base=self.chain(self.demodulator) 324 | 325 | #create control pipes for csdr 326 | self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) 327 | # self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None 328 | 329 | self.try_create_pipes(self.pipe_names, command_base) 330 | 331 | # if "{bpf_pipe}" in command_base: 332 | # self.bpf_pipe=pipe_base_path+"bpf" 333 | # self.mkfifo(self.bpf_pipe) 334 | # if "{shift_pipe}" in command_base: 335 | # self.shift_pipe=pipe_base_path+"shift" 336 | # self.mkfifo(self.shift_pipe) 337 | # if "{squelch_pipe}" in command_base: 338 | # self.squelch_pipe=pipe_base_path+"squelch" 339 | # self.mkfifo(self.squelch_pipe) 340 | # if "{smeter_pipe}" in command_base: 341 | # self.smeter_pipe=pipe_base_path+"smeter" 342 | # self.mkfifo(self.smeter_pipe) 343 | # if "{iqtee_pipe}" in command_base: 344 | # self.iqtee_pipe=pipe_base_path+"iqtee" 345 | # self.mkfifo(self.iqtee_pipe) 346 | # if "{iqtee2_pipe}" in command_base: 347 | # self.iqtee2_pipe=pipe_base_path+"iqtee2" 348 | # self.mkfifo(self.iqtee2_pipe) 349 | 350 | #run the command 351 | command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \ 352 | last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, \ 353 | bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \ 354 | flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \ 355 | squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe ) 356 | 357 | print "[openwebrx-dsp-plugin:csdr] Command =",command 358 | #code.interact(local=locals()) 359 | my_env=os.environ.copy() 360 | if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; 361 | if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; 362 | self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) 363 | self.running = True 364 | 365 | #open control pipes for csdr and send initialization data 366 | if self.bpf_pipe != None: 367 | self.bpf_pipe_file=open(self.bpf_pipe,"w") 368 | self.set_bpf(self.low_cut,self.high_cut) 369 | if self.shift_pipe != None: 370 | self.shift_pipe_file=open(self.shift_pipe,"w") 371 | self.set_offset_freq(self.offset_freq) 372 | if self.squelch_pipe != None: 373 | self.squelch_pipe_file=open(self.squelch_pipe,"w") 374 | self.set_squelch_level(self.squelch_level) 375 | if self.smeter_pipe != None: 376 | self.smeter_pipe_file=open(self.smeter_pipe,"r") 377 | self.set_pipe_nonblocking(self.smeter_pipe_file) 378 | 379 | self.start_secondary_demodulator() 380 | 381 | def read(self,size): 382 | return self.process.stdout.read(size) 383 | 384 | def stop(self): 385 | os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) 386 | self.stop_secondary_demodulator() 387 | #if(self.process.poll()!=None):return # returns None while subprocess is running 388 | #while(self.process.poll()==None): 389 | # #self.process.kill() 390 | # print "killproc",os.getpgid(self.process.pid),self.process.pid 391 | # os.killpg(self.process.pid, signal.SIGTERM) 392 | # 393 | # time.sleep(0.1) 394 | 395 | self.try_delete_pipes(self.pipe_names) 396 | 397 | # if self.bpf_pipe: 398 | # try: os.unlink(self.bpf_pipe) 399 | # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe 400 | # if self.shift_pipe: 401 | # try: os.unlink(self.shift_pipe) 402 | # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe 403 | # if self.squelch_pipe: 404 | # try: os.unlink(self.squelch_pipe) 405 | # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe 406 | # if self.smeter_pipe: 407 | # try: os.unlink(self.smeter_pipe) 408 | # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe 409 | # if self.iqtee_pipe: 410 | # try: os.unlink(self.iqtee_pipe) 411 | # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe 412 | # if self.iqtee2_pipe: 413 | # try: os.unlink(self.iqtee2_pipe) 414 | # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe 415 | 416 | self.running = False 417 | 418 | def restart(self): 419 | self.stop() 420 | self.start() 421 | 422 | def __del__(self): 423 | self.stop() 424 | del(self.process) 425 | -------------------------------------------------------------------------------- /htdocs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/favicon.ico -------------------------------------------------------------------------------- /htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf -------------------------------------------------------------------------------- /htdocs/gfx/font-expletus-sans/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Jasper de Waard (jasper@designtown.nl), 2 | with Reserved Font Name "Expletus Sans". 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-3d-spectrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-3d-spectrum.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-avatar-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-avatar-background.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-avatar.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-background-cool-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-background-cool-blue.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-background-lingrad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-background-lingrad.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-ha5kfu-top-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-ha5kfu-top-logo.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-logo-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-logo-big.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-panel-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-panel-log.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-panel-receiver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-panel-receiver.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-panel-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-panel-status.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-play-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-play-button.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-rx-details-arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-rx-details-arrow-up.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-rx-details-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-rx-details-arrow.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-scale-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-scale-background.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-speaker-muted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-speaker-muted.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-speaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-speaker.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-squelch-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-squelch-button.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-top-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-top-logo.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-top-photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-top-photo.jpg -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-waterfall-auto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-waterfall-auto.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-waterfall-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-waterfall-default.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-zoom-in-total.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-zoom-in-total.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-zoom-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-zoom-in.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-zoom-out-total.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-zoom-out-total.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-zoom-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ha7ilm/openwebrx/d84e672e6ed90a8aa6f4c37c3e4eddb42a39dee6/htdocs/gfx/openwebrx-zoom-out.png -------------------------------------------------------------------------------- /htdocs/inactive.html: -------------------------------------------------------------------------------- 1 | 2 | 22 | OpenWebRX 23 | 74 | 75 | 76 | 77 |
78 | 79 |
80 | Sorry, the receiver is inactive due to internal error. 81 |
82 |
83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /htdocs/index.wrx: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | OpenWebRX | Open Source SDR Web App for Everyone! 25 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 |
54 |
55 | 56 |
%[RX_PHOTO_TITLE]
57 |
%[RX_PHOTO_DESC]
58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 |
%[RX_TITLE]
66 |
%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, [maps]
67 |
68 | 69 | 70 |
71 |
72 |
    73 |

  • Status
  • 74 |

  • Log
  • 75 |

  • Receiver
  • 76 |
77 |
78 |
79 |
80 |
81 |
82 | 83 |
84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 |
---.--- MHz
92 |
---.--- MHz
93 |
94 |
FM
96 |
AM
98 |
LSB
100 |
USB
102 |
CW
104 |
105 |
106 |
DIG
107 | 111 |
112 |
113 |
114 | 115 |
116 | 117 |
118 |
119 |
120 | 121 |
122 | 123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
0 dB
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
OpenWebRX client log
142 | Author: András Retzler, HA7ILM
You can donate to say thanks for former development (this is the final version).
143 |
144 |
145 |
146 |
147 |
148 |
Audio buffer [0 ms]
149 |
Audio output [0 sps]
150 |
Audio stream [0 kbps]
151 |
Network usage [0 kbps]
152 |
Server CPU [0%]
153 |
Clients [1]
154 |
155 |
156 | Under construction 157 |
We're working on the code right now, so the application might fail. 158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | 167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 | 176 |

Start OpenWebRX 177 |
178 |
179 | 180 | 181 | -------------------------------------------------------------------------------- /htdocs/jquery.nanoscroller.js: -------------------------------------------------------------------------------- 1 | /*! nanoScrollerJS - v0.8.7 - 2015 2 | * http://jamesflorentino.github.com/nanoScrollerJS/ 3 | * Copyright (c) 2015 James Florentino; Licensed MIT */ 4 | (function(factory) { 5 | if (typeof define === 'function' && define.amd) { 6 | return define(['jquery'], function($) { 7 | return factory($, window, document); 8 | }); 9 | } else if (typeof exports === 'object') { 10 | return module.exports = factory(require('jquery'), window, document); 11 | } else { 12 | return factory(jQuery, window, document); 13 | } 14 | })(function($, window, document) { 15 | "use strict"; 16 | var BROWSER_IS_IE7, BROWSER_SCROLLBAR_WIDTH, DOMSCROLL, DOWN, DRAG, ENTER, KEYDOWN, KEYUP, MOUSEDOWN, MOUSEENTER, MOUSEMOVE, MOUSEUP, MOUSEWHEEL, NanoScroll, PANEDOWN, RESIZE, SCROLL, SCROLLBAR, TOUCHMOVE, UP, WHEEL, cAF, defaults, getBrowserScrollbarWidth, hasTransform, isFFWithBuggyScrollbar, rAF, transform, _elementStyle, _prefixStyle, _vendor; 17 | defaults = { 18 | 19 | /** 20 | a classname for the pane element. 21 | @property paneClass 22 | @type String 23 | @default 'nano-pane' 24 | */ 25 | paneClass: 'nano-pane', 26 | 27 | /** 28 | a classname for the slider element. 29 | @property sliderClass 30 | @type String 31 | @default 'nano-slider' 32 | */ 33 | sliderClass: 'nano-slider', 34 | 35 | /** 36 | a classname for the content element. 37 | @property contentClass 38 | @type String 39 | @default 'nano-content' 40 | */ 41 | contentClass: 'nano-content', 42 | 43 | /** 44 | a classname for enabled mode 45 | @property enabledClass 46 | @type String 47 | @default 'has-scrollbar' 48 | */ 49 | enabledClass: 'has-scrollbar', 50 | 51 | /** 52 | a classname for flashed mode 53 | @property flashedClass 54 | @type String 55 | @default 'flashed' 56 | */ 57 | flashedClass: 'flashed', 58 | 59 | /** 60 | a classname for active mode 61 | @property activeClass 62 | @type String 63 | @default 'active' 64 | */ 65 | activeClass: 'active', 66 | 67 | /** 68 | a setting to enable native scrolling in iOS devices. 69 | @property iOSNativeScrolling 70 | @type Boolean 71 | @default false 72 | */ 73 | iOSNativeScrolling: false, 74 | 75 | /** 76 | a setting to prevent the rest of the page being 77 | scrolled when user scrolls the `.content` element. 78 | @property preventPageScrolling 79 | @type Boolean 80 | @default false 81 | */ 82 | preventPageScrolling: false, 83 | 84 | /** 85 | a setting to disable binding to the resize event. 86 | @property disableResize 87 | @type Boolean 88 | @default false 89 | */ 90 | disableResize: false, 91 | 92 | /** 93 | a setting to make the scrollbar always visible. 94 | @property alwaysVisible 95 | @type Boolean 96 | @default false 97 | */ 98 | alwaysVisible: false, 99 | 100 | /** 101 | a default timeout for the `flash()` method. 102 | @property flashDelay 103 | @type Number 104 | @default 1500 105 | */ 106 | flashDelay: 1500, 107 | 108 | /** 109 | a minimum height for the `.slider` element. 110 | @property sliderMinHeight 111 | @type Number 112 | @default 20 113 | */ 114 | sliderMinHeight: 20, 115 | 116 | /** 117 | a maximum height for the `.slider` element. 118 | @property sliderMaxHeight 119 | @type Number 120 | @default null 121 | */ 122 | sliderMaxHeight: null, 123 | 124 | /** 125 | an alternate document context. 126 | @property documentContext 127 | @type Document 128 | @default null 129 | */ 130 | documentContext: null, 131 | 132 | /** 133 | an alternate window context. 134 | @property windowContext 135 | @type Window 136 | @default null 137 | */ 138 | windowContext: null 139 | }; 140 | 141 | /** 142 | @property SCROLLBAR 143 | @type String 144 | @static 145 | @final 146 | @private 147 | */ 148 | SCROLLBAR = 'scrollbar'; 149 | 150 | /** 151 | @property SCROLL 152 | @type String 153 | @static 154 | @final 155 | @private 156 | */ 157 | SCROLL = 'scroll'; 158 | 159 | /** 160 | @property MOUSEDOWN 161 | @type String 162 | @final 163 | @private 164 | */ 165 | MOUSEDOWN = 'mousedown'; 166 | 167 | /** 168 | @property MOUSEENTER 169 | @type String 170 | @final 171 | @private 172 | */ 173 | MOUSEENTER = 'mouseenter'; 174 | 175 | /** 176 | @property MOUSEMOVE 177 | @type String 178 | @static 179 | @final 180 | @private 181 | */ 182 | MOUSEMOVE = 'mousemove'; 183 | 184 | /** 185 | @property MOUSEWHEEL 186 | @type String 187 | @final 188 | @private 189 | */ 190 | MOUSEWHEEL = 'mousewheel'; 191 | 192 | /** 193 | @property MOUSEUP 194 | @type String 195 | @static 196 | @final 197 | @private 198 | */ 199 | MOUSEUP = 'mouseup'; 200 | 201 | /** 202 | @property RESIZE 203 | @type String 204 | @final 205 | @private 206 | */ 207 | RESIZE = 'resize'; 208 | 209 | /** 210 | @property DRAG 211 | @type String 212 | @static 213 | @final 214 | @private 215 | */ 216 | DRAG = 'drag'; 217 | 218 | /** 219 | @property ENTER 220 | @type String 221 | @static 222 | @final 223 | @private 224 | */ 225 | ENTER = 'enter'; 226 | 227 | /** 228 | @property UP 229 | @type String 230 | @static 231 | @final 232 | @private 233 | */ 234 | UP = 'up'; 235 | 236 | /** 237 | @property PANEDOWN 238 | @type String 239 | @static 240 | @final 241 | @private 242 | */ 243 | PANEDOWN = 'panedown'; 244 | 245 | /** 246 | @property DOMSCROLL 247 | @type String 248 | @static 249 | @final 250 | @private 251 | */ 252 | DOMSCROLL = 'DOMMouseScroll'; 253 | 254 | /** 255 | @property DOWN 256 | @type String 257 | @static 258 | @final 259 | @private 260 | */ 261 | DOWN = 'down'; 262 | 263 | /** 264 | @property WHEEL 265 | @type String 266 | @static 267 | @final 268 | @private 269 | */ 270 | WHEEL = 'wheel'; 271 | 272 | /** 273 | @property KEYDOWN 274 | @type String 275 | @static 276 | @final 277 | @private 278 | */ 279 | KEYDOWN = 'keydown'; 280 | 281 | /** 282 | @property KEYUP 283 | @type String 284 | @static 285 | @final 286 | @private 287 | */ 288 | KEYUP = 'keyup'; 289 | 290 | /** 291 | @property TOUCHMOVE 292 | @type String 293 | @static 294 | @final 295 | @private 296 | */ 297 | TOUCHMOVE = 'touchmove'; 298 | 299 | /** 300 | @property BROWSER_IS_IE7 301 | @type Boolean 302 | @static 303 | @final 304 | @private 305 | */ 306 | BROWSER_IS_IE7 = window.navigator.appName === 'Microsoft Internet Explorer' && /msie 7./i.test(window.navigator.appVersion) && window.ActiveXObject; 307 | 308 | /** 309 | @property BROWSER_SCROLLBAR_WIDTH 310 | @type Number 311 | @static 312 | @default null 313 | @private 314 | */ 315 | BROWSER_SCROLLBAR_WIDTH = null; 316 | rAF = window.requestAnimationFrame; 317 | cAF = window.cancelAnimationFrame; 318 | _elementStyle = document.createElement('div').style; 319 | _vendor = (function() { 320 | var i, transform, vendor, vendors, _i, _len; 321 | vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT']; 322 | for (i = _i = 0, _len = vendors.length; _i < _len; i = ++_i) { 323 | vendor = vendors[i]; 324 | transform = vendors[i] + 'ransform'; 325 | if (transform in _elementStyle) { 326 | return vendors[i].substr(0, vendors[i].length - 1); 327 | } 328 | } 329 | return false; 330 | })(); 331 | _prefixStyle = function(style) { 332 | if (_vendor === false) { 333 | return false; 334 | } 335 | if (_vendor === '') { 336 | return style; 337 | } 338 | return _vendor + style.charAt(0).toUpperCase() + style.substr(1); 339 | }; 340 | transform = _prefixStyle('transform'); 341 | hasTransform = transform !== false; 342 | 343 | /** 344 | Returns browser's native scrollbar width 345 | @method getBrowserScrollbarWidth 346 | @return {Number} the scrollbar width in pixels 347 | @static 348 | @private 349 | */ 350 | getBrowserScrollbarWidth = function() { 351 | var outer, outerStyle, scrollbarWidth; 352 | outer = document.createElement('div'); 353 | outerStyle = outer.style; 354 | outerStyle.position = 'absolute'; 355 | outerStyle.width = '100px'; 356 | outerStyle.height = '100px'; 357 | outerStyle.overflow = SCROLL; 358 | outerStyle.top = '-9999px'; 359 | document.body.appendChild(outer); 360 | scrollbarWidth = outer.offsetWidth - outer.clientWidth; 361 | document.body.removeChild(outer); 362 | return scrollbarWidth; 363 | }; 364 | isFFWithBuggyScrollbar = function() { 365 | var isOSXFF, ua, version; 366 | ua = window.navigator.userAgent; 367 | isOSXFF = /(?=.+Mac OS X)(?=.+Firefox)/.test(ua); 368 | if (!isOSXFF) { 369 | return false; 370 | } 371 | version = /Firefox\/\d{2}\./.exec(ua); 372 | if (version) { 373 | version = version[0].replace(/\D+/g, ''); 374 | } 375 | return isOSXFF && +version > 23; 376 | }; 377 | 378 | /** 379 | @class NanoScroll 380 | @param element {HTMLElement|Node} the main element 381 | @param options {Object} nanoScroller's options 382 | @constructor 383 | */ 384 | NanoScroll = (function() { 385 | function NanoScroll(el, options) { 386 | this.el = el; 387 | this.options = options; 388 | BROWSER_SCROLLBAR_WIDTH || (BROWSER_SCROLLBAR_WIDTH = getBrowserScrollbarWidth()); 389 | this.$el = $(this.el); 390 | this.doc = $(this.options.documentContext || document); 391 | this.win = $(this.options.windowContext || window); 392 | this.body = this.doc.find('body'); 393 | this.$content = this.$el.children("." + this.options.contentClass); 394 | this.$content.attr('tabindex', this.options.tabIndex || 0); 395 | this.content = this.$content[0]; 396 | this.previousPosition = 0; 397 | if (this.options.iOSNativeScrolling && (this.el.style.WebkitOverflowScrolling != null)) { 398 | this.nativeScrolling(); 399 | } else { 400 | this.generate(); 401 | } 402 | this.createEvents(); 403 | this.addEvents(); 404 | this.reset(); 405 | } 406 | 407 | 408 | /** 409 | Prevents the rest of the page being scrolled 410 | when user scrolls the `.nano-content` element. 411 | @method preventScrolling 412 | @param event {Event} 413 | @param direction {String} Scroll direction (up or down) 414 | @private 415 | */ 416 | 417 | NanoScroll.prototype.preventScrolling = function(e, direction) { 418 | if (!this.isActive) { 419 | return; 420 | } 421 | if (e.type === DOMSCROLL) { 422 | if (direction === DOWN && e.originalEvent.detail > 0 || direction === UP && e.originalEvent.detail < 0) { 423 | e.preventDefault(); 424 | } 425 | } else if (e.type === MOUSEWHEEL) { 426 | if (!e.originalEvent || !e.originalEvent.wheelDelta) { 427 | return; 428 | } 429 | if (direction === DOWN && e.originalEvent.wheelDelta < 0 || direction === UP && e.originalEvent.wheelDelta > 0) { 430 | e.preventDefault(); 431 | } 432 | } 433 | }; 434 | 435 | 436 | /** 437 | Enable iOS native scrolling 438 | @method nativeScrolling 439 | @private 440 | */ 441 | 442 | NanoScroll.prototype.nativeScrolling = function() { 443 | this.$content.css({ 444 | WebkitOverflowScrolling: 'touch' 445 | }); 446 | this.iOSNativeScrolling = true; 447 | this.isActive = true; 448 | }; 449 | 450 | 451 | /** 452 | Updates those nanoScroller properties that 453 | are related to current scrollbar position. 454 | @method updateScrollValues 455 | @private 456 | */ 457 | 458 | NanoScroll.prototype.updateScrollValues = function() { 459 | var content, direction; 460 | content = this.content; 461 | this.maxScrollTop = content.scrollHeight - content.clientHeight; 462 | this.prevScrollTop = this.contentScrollTop || 0; 463 | this.contentScrollTop = content.scrollTop; 464 | direction = this.contentScrollTop > this.previousPosition ? "down" : this.contentScrollTop < this.previousPosition ? "up" : "same"; 465 | this.previousPosition = this.contentScrollTop; 466 | if (direction !== "same") { 467 | this.$el.trigger('update', { 468 | position: this.contentScrollTop, 469 | maximum: this.maxScrollTop, 470 | direction: direction 471 | }); 472 | } 473 | if (!this.iOSNativeScrolling) { 474 | this.maxSliderTop = this.paneHeight - this.sliderHeight; 475 | this.sliderTop = this.maxScrollTop === 0 ? 0 : this.contentScrollTop * this.maxSliderTop / this.maxScrollTop; 476 | } 477 | }; 478 | 479 | 480 | /** 481 | Updates CSS styles for current scroll position. 482 | Uses CSS 2d transfroms and `window.requestAnimationFrame` if available. 483 | @method setOnScrollStyles 484 | @private 485 | */ 486 | 487 | NanoScroll.prototype.setOnScrollStyles = function() { 488 | var cssValue; 489 | if (hasTransform) { 490 | cssValue = {}; 491 | cssValue[transform] = "translate(0, " + this.sliderTop + "px)"; 492 | } else { 493 | cssValue = { 494 | top: this.sliderTop 495 | }; 496 | } 497 | if (rAF) { 498 | if (cAF && this.scrollRAF) { 499 | cAF(this.scrollRAF); 500 | } 501 | this.scrollRAF = rAF((function(_this) { 502 | return function() { 503 | _this.scrollRAF = null; 504 | return _this.slider.css(cssValue); 505 | }; 506 | })(this)); 507 | } else { 508 | this.slider.css(cssValue); 509 | } 510 | }; 511 | 512 | 513 | /** 514 | Creates event related methods 515 | @method createEvents 516 | @private 517 | */ 518 | 519 | NanoScroll.prototype.createEvents = function() { 520 | this.events = { 521 | down: (function(_this) { 522 | return function(e) { 523 | _this.isBeingDragged = true; 524 | _this.offsetY = e.pageY - _this.slider.offset().top; 525 | if (!_this.slider.is(e.target)) { 526 | _this.offsetY = 0; 527 | } 528 | _this.pane.addClass(_this.options.activeClass); 529 | _this.doc.bind(MOUSEMOVE, _this.events[DRAG]).bind(MOUSEUP, _this.events[UP]); 530 | _this.body.bind(MOUSEENTER, _this.events[ENTER]); 531 | return false; 532 | }; 533 | })(this), 534 | drag: (function(_this) { 535 | return function(e) { 536 | _this.sliderY = e.pageY - _this.$el.offset().top - _this.paneTop - (_this.offsetY || _this.sliderHeight * 0.5); 537 | _this.scroll(); 538 | if (_this.contentScrollTop >= _this.maxScrollTop && _this.prevScrollTop !== _this.maxScrollTop) { 539 | _this.$el.trigger('scrollend'); 540 | } else if (_this.contentScrollTop === 0 && _this.prevScrollTop !== 0) { 541 | _this.$el.trigger('scrolltop'); 542 | } 543 | return false; 544 | }; 545 | })(this), 546 | up: (function(_this) { 547 | return function(e) { 548 | _this.isBeingDragged = false; 549 | _this.pane.removeClass(_this.options.activeClass); 550 | _this.doc.unbind(MOUSEMOVE, _this.events[DRAG]).unbind(MOUSEUP, _this.events[UP]); 551 | _this.body.unbind(MOUSEENTER, _this.events[ENTER]); 552 | return false; 553 | }; 554 | })(this), 555 | resize: (function(_this) { 556 | return function(e) { 557 | _this.reset(); 558 | }; 559 | })(this), 560 | panedown: (function(_this) { 561 | return function(e) { 562 | _this.sliderY = (e.offsetY || e.originalEvent.layerY) - (_this.sliderHeight * 0.5); 563 | _this.scroll(); 564 | _this.events.down(e); 565 | return false; 566 | }; 567 | })(this), 568 | scroll: (function(_this) { 569 | return function(e) { 570 | _this.updateScrollValues(); 571 | if (_this.isBeingDragged) { 572 | return; 573 | } 574 | if (!_this.iOSNativeScrolling) { 575 | _this.sliderY = _this.sliderTop; 576 | _this.setOnScrollStyles(); 577 | } 578 | if (e == null) { 579 | return; 580 | } 581 | if (_this.contentScrollTop >= _this.maxScrollTop) { 582 | if (_this.options.preventPageScrolling) { 583 | _this.preventScrolling(e, DOWN); 584 | } 585 | if (_this.prevScrollTop !== _this.maxScrollTop) { 586 | _this.$el.trigger('scrollend'); 587 | } 588 | } else if (_this.contentScrollTop === 0) { 589 | if (_this.options.preventPageScrolling) { 590 | _this.preventScrolling(e, UP); 591 | } 592 | if (_this.prevScrollTop !== 0) { 593 | _this.$el.trigger('scrolltop'); 594 | } 595 | } 596 | }; 597 | })(this), 598 | wheel: (function(_this) { 599 | return function(e) { 600 | var delta; 601 | if (e == null) { 602 | return; 603 | } 604 | delta = e.delta || e.wheelDelta || (e.originalEvent && e.originalEvent.wheelDelta) || -e.detail || (e.originalEvent && -e.originalEvent.detail); 605 | if (delta) { 606 | _this.sliderY += -delta / 3; 607 | } 608 | _this.scroll(); 609 | return false; 610 | }; 611 | })(this), 612 | enter: (function(_this) { 613 | return function(e) { 614 | var _ref; 615 | if (!_this.isBeingDragged) { 616 | return; 617 | } 618 | if ((e.buttons || e.which) !== 1) { 619 | return (_ref = _this.events)[UP].apply(_ref, arguments); 620 | } 621 | }; 622 | })(this) 623 | }; 624 | }; 625 | 626 | 627 | /** 628 | Adds event listeners with jQuery. 629 | @method addEvents 630 | @private 631 | */ 632 | 633 | NanoScroll.prototype.addEvents = function() { 634 | var events; 635 | this.removeEvents(); 636 | events = this.events; 637 | if (!this.options.disableResize) { 638 | this.win.bind(RESIZE, events[RESIZE]); 639 | } 640 | if (!this.iOSNativeScrolling) { 641 | this.slider.bind(MOUSEDOWN, events[DOWN]); 642 | this.pane.bind(MOUSEDOWN, events[PANEDOWN]).bind("" + MOUSEWHEEL + " " + DOMSCROLL, events[WHEEL]); 643 | } 644 | this.$content.bind("" + SCROLL + " " + MOUSEWHEEL + " " + DOMSCROLL + " " + TOUCHMOVE, events[SCROLL]); 645 | }; 646 | 647 | 648 | /** 649 | Removes event listeners with jQuery. 650 | @method removeEvents 651 | @private 652 | */ 653 | 654 | NanoScroll.prototype.removeEvents = function() { 655 | var events; 656 | events = this.events; 657 | this.win.unbind(RESIZE, events[RESIZE]); 658 | if (!this.iOSNativeScrolling) { 659 | this.slider.unbind(); 660 | this.pane.unbind(); 661 | } 662 | this.$content.unbind("" + SCROLL + " " + MOUSEWHEEL + " " + DOMSCROLL + " " + TOUCHMOVE, events[SCROLL]); 663 | }; 664 | 665 | 666 | /** 667 | Generates nanoScroller's scrollbar and elements for it. 668 | @method generate 669 | @chainable 670 | @private 671 | */ 672 | 673 | NanoScroll.prototype.generate = function() { 674 | var contentClass, cssRule, currentPadding, options, pane, paneClass, sliderClass; 675 | options = this.options; 676 | paneClass = options.paneClass, sliderClass = options.sliderClass, contentClass = options.contentClass; 677 | if (!(pane = this.$el.children("." + paneClass)).length && !pane.children("." + sliderClass).length) { 678 | this.$el.append("
"); 679 | } 680 | this.pane = this.$el.children("." + paneClass); 681 | this.slider = this.pane.find("." + sliderClass); 682 | if (BROWSER_SCROLLBAR_WIDTH === 0 && isFFWithBuggyScrollbar()) { 683 | currentPadding = window.getComputedStyle(this.content, null).getPropertyValue('padding-right').replace(/[^0-9.]+/g, ''); 684 | cssRule = { 685 | right: -14, 686 | paddingRight: +currentPadding + 14 687 | }; 688 | } else if (BROWSER_SCROLLBAR_WIDTH) { 689 | cssRule = { 690 | right: -BROWSER_SCROLLBAR_WIDTH 691 | }; 692 | this.$el.addClass(options.enabledClass); 693 | } 694 | if (cssRule != null) { 695 | this.$content.css(cssRule); 696 | } 697 | return this; 698 | }; 699 | 700 | 701 | /** 702 | @method restore 703 | @private 704 | */ 705 | 706 | NanoScroll.prototype.restore = function() { 707 | this.stopped = false; 708 | if (!this.iOSNativeScrolling) { 709 | this.pane.show(); 710 | } 711 | this.addEvents(); 712 | }; 713 | 714 | 715 | /** 716 | Resets nanoScroller's scrollbar. 717 | @method reset 718 | @chainable 719 | @example 720 | $(".nano").nanoScroller(); 721 | */ 722 | 723 | NanoScroll.prototype.reset = function() { 724 | var content, contentHeight, contentPosition, contentStyle, contentStyleOverflowY, paneBottom, paneHeight, paneOuterHeight, paneTop, parentMaxHeight, right, sliderHeight; 725 | if (this.iOSNativeScrolling) { 726 | this.contentHeight = this.content.scrollHeight; 727 | return; 728 | } 729 | if (!this.$el.find("." + this.options.paneClass).length) { 730 | this.generate().stop(); 731 | } 732 | if (this.stopped) { 733 | this.restore(); 734 | } 735 | content = this.content; 736 | contentStyle = content.style; 737 | contentStyleOverflowY = contentStyle.overflowY; 738 | if (BROWSER_IS_IE7) { 739 | this.$content.css({ 740 | height: this.$content.height() 741 | }); 742 | } 743 | contentHeight = content.scrollHeight + BROWSER_SCROLLBAR_WIDTH; 744 | parentMaxHeight = parseInt(this.$el.css("max-height"), 10); 745 | if (parentMaxHeight > 0) { 746 | this.$el.height(""); 747 | this.$el.height(content.scrollHeight > parentMaxHeight ? parentMaxHeight : content.scrollHeight); 748 | } 749 | paneHeight = this.pane.outerHeight(false); 750 | paneTop = parseInt(this.pane.css('top'), 10); 751 | paneBottom = parseInt(this.pane.css('bottom'), 10); 752 | paneOuterHeight = paneHeight + paneTop + paneBottom; 753 | sliderHeight = Math.round(paneOuterHeight / contentHeight * paneHeight); 754 | if (sliderHeight < this.options.sliderMinHeight) { 755 | sliderHeight = this.options.sliderMinHeight; 756 | } else if ((this.options.sliderMaxHeight != null) && sliderHeight > this.options.sliderMaxHeight) { 757 | sliderHeight = this.options.sliderMaxHeight; 758 | } 759 | if (contentStyleOverflowY === SCROLL && contentStyle.overflowX !== SCROLL) { 760 | sliderHeight += BROWSER_SCROLLBAR_WIDTH; 761 | } 762 | this.maxSliderTop = paneOuterHeight - sliderHeight; 763 | this.contentHeight = contentHeight; 764 | this.paneHeight = paneHeight; 765 | this.paneOuterHeight = paneOuterHeight; 766 | this.sliderHeight = sliderHeight; 767 | this.paneTop = paneTop; 768 | this.slider.height(sliderHeight); 769 | this.events.scroll(); 770 | this.pane.show(); 771 | this.isActive = true; 772 | if ((content.scrollHeight === content.clientHeight) || (this.pane.outerHeight(true) >= content.scrollHeight && contentStyleOverflowY !== SCROLL)) { 773 | this.pane.hide(); 774 | this.isActive = false; 775 | } else if (this.el.clientHeight === content.scrollHeight && contentStyleOverflowY === SCROLL) { 776 | this.slider.hide(); 777 | } else { 778 | this.slider.show(); 779 | } 780 | this.pane.css({ 781 | opacity: (this.options.alwaysVisible ? 1 : ''), 782 | visibility: (this.options.alwaysVisible ? 'visible' : '') 783 | }); 784 | contentPosition = this.$content.css('position'); 785 | if (contentPosition === 'static' || contentPosition === 'relative') { 786 | right = parseInt(this.$content.css('right'), 10); 787 | if (right) { 788 | this.$content.css({ 789 | right: '', 790 | marginRight: right 791 | }); 792 | } 793 | } 794 | return this; 795 | }; 796 | 797 | 798 | /** 799 | @method scroll 800 | @private 801 | @example 802 | $(".nano").nanoScroller({ scroll: 'top' }); 803 | */ 804 | 805 | NanoScroll.prototype.scroll = function() { 806 | if (!this.isActive) { 807 | return; 808 | } 809 | this.sliderY = Math.max(0, this.sliderY); 810 | this.sliderY = Math.min(this.maxSliderTop, this.sliderY); 811 | this.$content.scrollTop(this.maxScrollTop * this.sliderY / this.maxSliderTop); 812 | if (!this.iOSNativeScrolling) { 813 | this.updateScrollValues(); 814 | this.setOnScrollStyles(); 815 | } 816 | return this; 817 | }; 818 | 819 | 820 | /** 821 | Scroll at the bottom with an offset value 822 | @method scrollBottom 823 | @param offsetY {Number} 824 | @chainable 825 | @example 826 | $(".nano").nanoScroller({ scrollBottom: value }); 827 | */ 828 | 829 | NanoScroll.prototype.scrollBottom = function(offsetY) { 830 | if (!this.isActive) { 831 | return; 832 | } 833 | this.$content.scrollTop(this.contentHeight - this.$content.height() - offsetY).trigger(MOUSEWHEEL); 834 | this.stop().restore(); 835 | return this; 836 | }; 837 | 838 | 839 | /** 840 | Scroll at the top with an offset value 841 | @method scrollTop 842 | @param offsetY {Number} 843 | @chainable 844 | @example 845 | $(".nano").nanoScroller({ scrollTop: value }); 846 | */ 847 | 848 | NanoScroll.prototype.scrollTop = function(offsetY) { 849 | if (!this.isActive) { 850 | return; 851 | } 852 | this.$content.scrollTop(+offsetY).trigger(MOUSEWHEEL); 853 | this.stop().restore(); 854 | return this; 855 | }; 856 | 857 | 858 | /** 859 | Scroll to an element 860 | @method scrollTo 861 | @param node {Node} A node to scroll to. 862 | @chainable 863 | @example 864 | $(".nano").nanoScroller({ scrollTo: $('#a_node') }); 865 | */ 866 | 867 | NanoScroll.prototype.scrollTo = function(node) { 868 | if (!this.isActive) { 869 | return; 870 | } 871 | this.scrollTop(this.$el.find(node).get(0).offsetTop); 872 | return this; 873 | }; 874 | 875 | 876 | /** 877 | To stop the operation. 878 | This option will tell the plugin to disable all event bindings and hide the gadget scrollbar from the UI. 879 | @method stop 880 | @chainable 881 | @example 882 | $(".nano").nanoScroller({ stop: true }); 883 | */ 884 | 885 | NanoScroll.prototype.stop = function() { 886 | if (cAF && this.scrollRAF) { 887 | cAF(this.scrollRAF); 888 | this.scrollRAF = null; 889 | } 890 | this.stopped = true; 891 | this.removeEvents(); 892 | if (!this.iOSNativeScrolling) { 893 | this.pane.hide(); 894 | } 895 | return this; 896 | }; 897 | 898 | 899 | /** 900 | Destroys nanoScroller and restores browser's native scrollbar. 901 | @method destroy 902 | @chainable 903 | @example 904 | $(".nano").nanoScroller({ destroy: true }); 905 | */ 906 | 907 | NanoScroll.prototype.destroy = function() { 908 | if (!this.stopped) { 909 | this.stop(); 910 | } 911 | if (!this.iOSNativeScrolling && this.pane.length) { 912 | this.pane.remove(); 913 | } 914 | if (BROWSER_IS_IE7) { 915 | this.$content.height(''); 916 | } 917 | this.$content.removeAttr('tabindex'); 918 | if (this.$el.hasClass(this.options.enabledClass)) { 919 | this.$el.removeClass(this.options.enabledClass); 920 | this.$content.css({ 921 | right: '' 922 | }); 923 | } 924 | return this; 925 | }; 926 | 927 | 928 | /** 929 | To flash the scrollbar gadget for an amount of time defined in plugin settings (defaults to 1,5s). 930 | Useful if you want to show the user (e.g. on pageload) that there is more content waiting for him. 931 | @method flash 932 | @chainable 933 | @example 934 | $(".nano").nanoScroller({ flash: true }); 935 | */ 936 | 937 | NanoScroll.prototype.flash = function() { 938 | if (this.iOSNativeScrolling) { 939 | return; 940 | } 941 | if (!this.isActive) { 942 | return; 943 | } 944 | this.reset(); 945 | this.pane.addClass(this.options.flashedClass); 946 | setTimeout((function(_this) { 947 | return function() { 948 | _this.pane.removeClass(_this.options.flashedClass); 949 | }; 950 | })(this), this.options.flashDelay); 951 | return this; 952 | }; 953 | 954 | return NanoScroll; 955 | 956 | })(); 957 | $.fn.nanoScroller = function(settings) { 958 | return this.each(function() { 959 | var options, scrollbar; 960 | if (!(scrollbar = this.nanoscroller)) { 961 | options = $.extend({}, defaults, settings); 962 | this.nanoscroller = scrollbar = new NanoScroll(this, options); 963 | } 964 | if (settings && typeof settings === "object") { 965 | $.extend(scrollbar.options, settings); 966 | if (settings.scrollBottom != null) { 967 | return scrollbar.scrollBottom(settings.scrollBottom); 968 | } 969 | if (settings.scrollTop != null) { 970 | return scrollbar.scrollTop(settings.scrollTop); 971 | } 972 | if (settings.scrollTo) { 973 | return scrollbar.scrollTo(settings.scrollTo); 974 | } 975 | if (settings.scroll === 'bottom') { 976 | return scrollbar.scrollBottom(0); 977 | } 978 | if (settings.scroll === 'top') { 979 | return scrollbar.scrollTop(0); 980 | } 981 | if (settings.scroll && settings.scroll instanceof $) { 982 | return scrollbar.scrollTo(settings.scroll); 983 | } 984 | if (settings.stop) { 985 | return scrollbar.stop(); 986 | } 987 | if (settings.destroy) { 988 | return scrollbar.destroy(); 989 | } 990 | if (settings.flash) { 991 | return scrollbar.flash(); 992 | } 993 | } 994 | return scrollbar.reset(); 995 | }); 996 | }; 997 | $.fn.nanoScroller.Constructor = NanoScroll; 998 | }); 999 | 1000 | //# sourceMappingURL=jquery.nanoscroller.js.map 1001 | -------------------------------------------------------------------------------- /htdocs/mathbox.css: -------------------------------------------------------------------------------- 1 | .shadergraph-graph { 2 | font: 12px sans-serif; 3 | line-height: 25px; 4 | position: relative; 5 | } 6 | .shadergraph-graph:after { 7 | content: ' '; 8 | display: block; 9 | height: 0; 10 | font-size: 0; 11 | clear: both; 12 | } 13 | .shadergraph-graph svg { 14 | pointer-events: none; 15 | } 16 | .shadergraph-clear { 17 | clear: both; 18 | } 19 | .shadergraph-graph svg { 20 | position: absolute; 21 | left: 0; 22 | right: 0; 23 | top: 0; 24 | bottom: 0; 25 | width: auto; 26 | height: auto; 27 | } 28 | .shadergraph-column { 29 | float: left; 30 | } 31 | .shadergraph-node .shadergraph-graph { 32 | float: left; 33 | clear: both; 34 | overflow: visible; 35 | } 36 | .shadergraph-node .shadergraph-graph .shadergraph-node { 37 | margin: 5px 15px 15px; 38 | } 39 | .shadergraph-node { 40 | margin: 5px 15px 25px; 41 | background: rgba(0, 0, 0, .1); 42 | border-radius: 5px; 43 | box-shadow: 0 1px 2px rgba(0, 0, 0, .2), 44 | 0 1px 10px rgba(0, 0, 0, .2); 45 | min-height: 35px; 46 | float: left; 47 | clear: left; 48 | position: relative; 49 | } 50 | .shadergraph-type { 51 | font-weight: bold; 52 | } 53 | .shadergraph-header { 54 | font-weight: bold; 55 | text-align: center; 56 | height: 25px; 57 | background: rgba(0, 0, 0, .3); 58 | text-shadow: 0 1px 2px rgba(0, 0, 0, .25); 59 | color: #fff; 60 | border-top-left-radius: 5px; 61 | border-top-right-radius: 5px; 62 | margin-bottom: 5px; 63 | padding: 0 10px; 64 | } 65 | .shadergraph-outlet div { 66 | } 67 | .shadergraph-outlet-in .shadergraph-name { 68 | margin-right: 7px; 69 | } 70 | .shadergraph-outlet-out .shadergraph-name { 71 | margin-left: 7px; 72 | } 73 | 74 | .shadergraph-name { 75 | margin: 0 4px; 76 | } 77 | .shadergraph-point { 78 | margin: 6px; 79 | width: 11px; 80 | height: 11px; 81 | border-radius: 7.5px; 82 | background: rgba(255, 255, 255, 1); 83 | } 84 | .shadergraph-outlet-in { 85 | float: left; 86 | clear: left; 87 | } 88 | .shadergraph-outlet-in div { 89 | float: left; 90 | } 91 | .shadergraph-outlet-out { 92 | float: right; 93 | clear: right; 94 | } 95 | .shadergraph-outlet-out div { 96 | float: right; 97 | } 98 | 99 | .shadergraph-node-callback { 100 | background: rgba(205, 209, 221, .5); 101 | box-shadow: 0 1px 2px rgba(0, 10, 40, .2), 102 | 0 1px 10px rgba(0, 10, 40, .2); 103 | } 104 | .shadergraph-node-callback > .shadergraph-header { 105 | background: rgba(0, 20, 80, .3); 106 | } 107 | .shadergraph-graph .shadergraph-graph .shadergraph-node-callback { 108 | background: rgba(0, 20, 80, .1); 109 | } 110 | 111 | .shadergraph-node-call { 112 | background: rgba(209, 221, 205, .5); 113 | box-shadow: 0 1px 2px rgba(10, 40, 0, .2), 114 | 0 1px 10px rgba(10, 40, 0, .2); 115 | } 116 | .shadergraph-node-call > .shadergraph-header { 117 | background: rgba(20, 80, 0, .3); 118 | } 119 | .shadergraph-graph .shadergraph-graph .shadergraph-node-call { 120 | background: rgba(20, 80, 0, .1); 121 | } 122 | 123 | .shadergraph-node-isolate { 124 | background: rgba(221, 205, 209, .5); 125 | box-shadow: 0 1px 2px rgba(40, 0, 10, .2), 126 | 0 1px 10px rgba(40, 0, 10, .2); 127 | } 128 | .shadergraph-node-isolate > .shadergraph-header { 129 | background: rgba(80, 0, 20, .3); 130 | } 131 | .shadergraph-graph .shadergraph-graph .shadergraph-node-isolate { 132 | background: rgba(80, 0, 20, .1); 133 | } 134 | 135 | .shadergraph-node.shadergraph-has-code { 136 | cursor: pointer; 137 | } 138 | .shadergraph-node.shadergraph-has-code::before { 139 | position: absolute; 140 | content: ' '; 141 | top: 0; 142 | left: 0; 143 | right: 0; 144 | bottom: 0; 145 | display: none; 146 | border: 2px solid rgba(0, 0, 0, .25); 147 | border-radius: 5px; 148 | } 149 | .shadergraph-node.shadergraph-has-code:hover::before { 150 | display: block; 151 | } 152 | .shadergraph-code { 153 | z-index: 10000; 154 | display: none; 155 | position: absolute; 156 | background: #fff; 157 | color: #000; 158 | white-space: pre; 159 | padding: 10px; 160 | border-radius: 5px; 161 | box-shadow: 0 1px 2px rgba(0, 0, 0, .2), 162 | 0 1px 10px rgba(0, 0, 0, .2); 163 | font-family: monospace; 164 | font-size: 10px; 165 | line-height: 12px; 166 | } 167 | 168 | .shadergraph-overlay { 169 | position: fixed; 170 | top: 50%; 171 | left: 0; 172 | right: 0; 173 | bottom: 0; 174 | background: #fff; 175 | border-top: 1px solid #CCC; 176 | } 177 | .shadergraph-overlay .shadergraph-view { 178 | position: absolute; 179 | left: 0; 180 | top: 0; 181 | right: 0; 182 | bottom: 0; 183 | overflow: auto; 184 | } 185 | .shadergraph-overlay .shadergraph-inside { 186 | width: 4000px; 187 | min-height: 100%; 188 | box-sizing: border-box; 189 | } 190 | .shadergraph-overlay .shadergraph-close { 191 | position: absolute; 192 | top: 5px; 193 | right: 5px; 194 | padding: 4px; 195 | border-radius: 16px; 196 | background: rgba(255,255,255,.3); 197 | color: rgba(0, 0, 0, .3); 198 | cursor: pointer; 199 | font-size: 24px; 200 | line-height: 24px; 201 | width: 24px; 202 | text-align: center; 203 | vertical-align: middle; 204 | } 205 | .shadergraph-overlay .shadergraph-close:hover { 206 | background: rgba(255,255,255,1); 207 | color: rgba(0, 0, 0, 1); 208 | } 209 | .shadergraph-overlay .shadergraph-graph { 210 | padding-top: 10px; 211 | overflow: visible; 212 | min-height: 100%; 213 | } 214 | .shadergraph-overlay span { 215 | display: block; 216 | padding: 5px 15px; 217 | margin: 0; 218 | background: rgba(0, 0, 0, .1); 219 | font-weight: bold; 220 | font-family: sans-serif; 221 | } 222 | .mathbox-loader { 223 | position: absolute; 224 | top: 50%; 225 | left: 50%; 226 | -webkit-transform: translate(-50%, -50%); 227 | transform: translate(-50%, -50%); 228 | padding: 10px; 229 | border-radius: 50%; 230 | background: #fff; 231 | } 232 | 233 | .mathbox-loader.mathbox-exit { 234 | opacity: 0; 235 | -webkit-transition: 236 | opacity .15s ease-in-out; 237 | transition: 238 | opacity .15s ease-in-out; 239 | } 240 | 241 | .mathbox-progress { 242 | height: 10px; 243 | border-radius: 5px; 244 | width: 80px; 245 | margin: 0 auto 20px; 246 | box-shadow: 247 | 1px 1px 1px rgba(255, 255, 255, .2), 248 | 1px -1px 1px rgba(255, 255, 255, .2), 249 | -1px 1px 1px rgba(255, 255, 255, .2), 250 | -1px -1px 1px rgba(255, 255, 255, .2); 251 | background: #ccc; 252 | overflow: hidden; 253 | } 254 | 255 | .mathbox-progress > div { 256 | display: block; 257 | width: 0px; 258 | height: 10px; 259 | background: #888; 260 | } 261 | 262 | .mathbox-logo { 263 | position: relative; 264 | width: 140px; 265 | height: 100px; 266 | margin: 0 auto 10px; 267 | -webkit-perspective: 200px; 268 | perspective: 200px; 269 | } 270 | 271 | .mathbox-logo > div { 272 | position: absolute; 273 | left: 0; 274 | top: 0; 275 | bottom: 0; 276 | right: 0; 277 | -webkit-transform-style: preserve-3d; 278 | transform-style: preserve-3d; 279 | } 280 | 281 | .mathbox-logo > :nth-child(1) { 282 | -webkit-transform: rotateZ(22deg) rotateX(24deg) rotateY(30deg); 283 | transform: rotateZ(22deg) rotateX(24deg) rotateY(30deg); 284 | } 285 | 286 | .mathbox-logo > :nth-child(2) { 287 | -webkit-transform: rotateZ(11deg) rotateX(12deg) rotateY(15deg) scale3d(.6, .6, .6); 288 | transform: rotateZ(11deg) rotateX(12deg) rotateY(15deg) scale3d(.6, .6, .6); 289 | } 290 | 291 | .mathbox-logo > div > div { 292 | position: absolute; 293 | top: 50%; 294 | left: 50%; 295 | margin-left: -100px; 296 | margin-top: -100px; 297 | width: 200px; 298 | height: 200px; 299 | box-sizing: border-box; 300 | border-radius: 50%; 301 | } 302 | 303 | .mathbox-logo > div > :nth-child(1) { 304 | -webkit-transform: scale(0.5, 0.5); 305 | transform: rotateX(30deg) scale(0.5, 0.5); 306 | } 307 | 308 | .mathbox-logo > div > :nth-child(2) { 309 | -webkit-transform: rotateX(90deg) scale(0.42, 0.42); 310 | transform: rotateX(90deg) scale(0.42, 0.42); 311 | } 312 | 313 | .mathbox-logo > div > :nth-child(3) { 314 | -webkit-transform: rotateY(90deg) scale(0.35, 0.35); 315 | transform: rotateY(90deg) scale(0.35, 0.35); 316 | } 317 | 318 | .mathbox-logo > :nth-child(1) > :nth-child(1) { 319 | border: 16px solid #808080; 320 | } 321 | .mathbox-logo > :nth-child(1) > :nth-child(2) { 322 | border: 19px solid #A0A0A0; 323 | } 324 | .mathbox-logo > :nth-child(1) > :nth-child(3) { 325 | border: 23px solid #C0C0C0; 326 | } 327 | .mathbox-logo > :nth-child(2) > :nth-child(1) { 328 | border: 27px solid #808080; 329 | } 330 | .mathbox-logo > :nth-child(2) > :nth-child(2) { 331 | border: 32px solid #A0A0A0; 332 | } 333 | .mathbox-logo > :nth-child(2) > :nth-child(3) { 334 | border: 38px solid #C0C0C0; 335 | } 336 | 337 | .mathbox-splash-blue .mathbox-progress { 338 | background: #def; 339 | } 340 | .mathbox-splash-blue .mathbox-progress > div { 341 | background: #1979e7; 342 | } 343 | .mathbox-splash-blue .mathbox-logo > :nth-child(1) > :nth-child(1) { 344 | border-color: #1979e7; 345 | } 346 | .mathbox-splash-blue .mathbox-logo > :nth-child(1) > :nth-child(2) { 347 | border-color: #33b0ff; 348 | } 349 | .mathbox-splash-blue .mathbox-logo > :nth-child(1) > :nth-child(3) { 350 | border-color: #75eaff; 351 | } 352 | .mathbox-splash-blue .mathbox-logo > :nth-child(2) > :nth-child(1) { 353 | border-color: #18487F; 354 | } 355 | .mathbox-splash-blue .mathbox-logo > :nth-child(2) > :nth-child(2) { 356 | border-color: #33b0ff; 357 | } 358 | .mathbox-splash-blue .mathbox-logo > :nth-child(2) > :nth-child(3) { 359 | border-color: #75eaff; 360 | } 361 | 362 | 363 | 364 | 365 | .mathbox-overlays { 366 | position: absolute; 367 | left: 0; 368 | top: 0; 369 | right: 0; 370 | bottom: 0; 371 | pointer-events: none; 372 | transform-style: preserve-3d; 373 | overflow: hidden; 374 | } 375 | .mathbox-overlays > div { 376 | transform-style: preserve-3d; 377 | } 378 | .mathbox-overlay > div { 379 | position: absolute; 380 | will-change: transform, opacity; 381 | } 382 | .mathbox-label { 383 | font-family: sans-serif; 384 | } 385 | .mathbox-outline-1 { 386 | text-shadow: 387 | -1px -1px 0px rgb(255, 255, 255), 388 | 1px 1px 0px rgb(255, 255, 255), 389 | -1px 1px 0px rgb(255, 255, 255), 390 | 1px -1px 0px rgb(255, 255, 255), 391 | 1px 0px 1px rgb(255, 255, 255), 392 | -1px 0px 1px rgb(255, 255, 255), 393 | 0px -1px 1px rgb(255, 255, 255), 394 | 0px 1px 1px rgb(255, 255, 255); 395 | } 396 | .mathbox-outline-2 { 397 | text-shadow: 398 | 0px -2px 0px rgb(255, 255, 255), 399 | 0px 2px 0px rgb(255, 255, 255), 400 | -2px 0px 0px rgb(255, 255, 255), 401 | 2px 0px 0px rgb(255, 255, 255), 402 | -1px -2px 0px rgb(255, 255, 255), 403 | -2px -1px 0px rgb(255, 255, 255), 404 | -1px 2px 0px rgb(255, 255, 255), 405 | -2px 1px 0px rgb(255, 255, 255), 406 | 1px 2px 0px rgb(255, 255, 255), 407 | 2px 1px 0px rgb(255, 255, 255), 408 | 1px -2px 0px rgb(255, 255, 255), 409 | 2px -1px 0px rgb(255, 255, 255); 410 | } 411 | .mathbox-outline-3 { 412 | text-shadow: 413 | 3px 0px 0px rgb(255, 255, 255), 414 | -3px 0px 0px rgb(255, 255, 255), 415 | 0px 3px 0px rgb(255, 255, 255), 416 | 0px -3px 0px rgb(255, 255, 255), 417 | 418 | -2px -2px 0px rgb(255, 255, 255), 419 | -2px 2px 0px rgb(255, 255, 255), 420 | 2px 2px 0px rgb(255, 255, 255), 421 | 2px -2px 0px rgb(255, 255, 255), 422 | 423 | -1px -2px 1px rgb(255, 255, 255), 424 | -2px -1px 1px rgb(255, 255, 255), 425 | -1px 2px 1px rgb(255, 255, 255), 426 | -2px 1px 1px rgb(255, 255, 255), 427 | 1px 2px 1px rgb(255, 255, 255), 428 | 2px 1px 1px rgb(255, 255, 255), 429 | 1px -2px 1px rgb(255, 255, 255), 430 | 2px -1px 1px rgb(255, 255, 255); 431 | } 432 | .mathbox-outline-4 { 433 | text-shadow: 434 | 4px 0px 0px rgb(255, 255, 255), 435 | -4px 0px 0px rgb(255, 255, 255), 436 | 0px 4px 0px rgb(255, 255, 255), 437 | 0px -4px 0px rgb(255, 255, 255), 438 | 439 | -3px -2px 0px rgb(255, 255, 255), 440 | -3px 2px 0px rgb(255, 255, 255), 441 | 3px 2px 0px rgb(255, 255, 255), 442 | 3px -2px 0px rgb(255, 255, 255), 443 | 444 | -2px -3px 0px rgb(255, 255, 255), 445 | -2px 3px 0px rgb(255, 255, 255), 446 | 2px 3px 0px rgb(255, 255, 255), 447 | 2px -3px 0px rgb(255, 255, 255), 448 | 449 | -1px -2px 1px rgb(255, 255, 255), 450 | -2px -1px 1px rgb(255, 255, 255), 451 | -1px 2px 1px rgb(255, 255, 255), 452 | -2px 1px 1px rgb(255, 255, 255), 453 | 1px 2px 1px rgb(255, 255, 255), 454 | 2px 1px 1px rgb(255, 255, 255), 455 | 1px -2px 1px rgb(255, 255, 255), 456 | 2px -1px 1px rgb(255, 255, 255); 457 | 458 | } 459 | .mathbox-outline-fill, .mathbox-outline-fill * { 460 | color: #fff !important; 461 | } 462 | -------------------------------------------------------------------------------- /htdocs/nanoscroller.css: -------------------------------------------------------------------------------- 1 | /** initial setup **/ 2 | .nano { 3 | position : relative; 4 | width : 100%; 5 | height : 100%; 6 | overflow : hidden; 7 | } 8 | .nano > .nano-content { 9 | position : absolute; 10 | overflow : scroll; 11 | overflow-x : hidden; 12 | top : 0; 13 | right : 0; 14 | bottom : 0; 15 | left : 0; 16 | } 17 | .nano > .nano-content:focus { 18 | outline: thin dotted; 19 | } 20 | .nano > .nano-content::-webkit-scrollbar { 21 | display: none; 22 | } 23 | .has-scrollbar > .nano-content::-webkit-scrollbar { 24 | display: block; 25 | } 26 | .nano > .nano-pane { 27 | background : rgba(0,0,0,.25); 28 | position : absolute; 29 | width : 8px; 30 | right : 0; 31 | top : 0; 32 | bottom : 0; 33 | visibility : hidden\9; /* Target only IE7 and IE8 with this hack */ 34 | opacity : .01; 35 | -webkit-transition : .2s; 36 | -moz-transition : .2s; 37 | -o-transition : .2s; 38 | transition : .2s; 39 | -moz-border-radius : 3px; 40 | -webkit-border-radius : 3px; 41 | border-radius : 3px; 42 | } 43 | .nano > .nano-pane > .nano-slider { 44 | background: #444; 45 | background: rgba(0,0,0,.5); 46 | position : relative; 47 | margin : 0 0px; 48 | -moz-border-radius : 4px; 49 | -webkit-border-radius : 4px; 50 | border-radius : 4px; 51 | } 52 | .nano:hover > .nano-pane, .nano-pane.active, .nano-pane.flashed { 53 | visibility : visible\9; /* Target only IE7 and IE8 with this hack */ 54 | opacity : 0.99; 55 | } 56 | -------------------------------------------------------------------------------- /htdocs/openwebrx.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This file is part of OpenWebRX, 4 | an open-source SDR receiver software with a web UI. 5 | Copyright (c) 2013-2015 by Andras Retzler 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU Affero General Public License as 9 | published by the Free Software Foundation, either version 3 of the 10 | License, or (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU Affero General Public License for more details. 16 | 17 | You should have received a copy of the GNU Affero General Public License 18 | along with this program. If not, see . 19 | 20 | */ 21 | 22 | html, body 23 | { 24 | margin: 0; 25 | padding: 0; 26 | height: 100%; 27 | font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; 28 | overflow: hidden; 29 | } 30 | 31 | select 32 | { 33 | font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; 34 | } 35 | 36 | input 37 | { 38 | vertical-align:middle; 39 | } 40 | 41 | input[type=range] 42 | { 43 | -webkit-appearance: none; 44 | margin: 0 0; 45 | } 46 | input[type=range]:focus 47 | { 48 | outline: none; 49 | } 50 | 51 | input[type=range]::-webkit-slider-runnable-track 52 | { 53 | height: 5px; 54 | cursor: pointer; 55 | animate: 0.2s; 56 | box-shadow: 0px 0px 0px #000000; 57 | background: #B6B6B6; 58 | /*border-radius: 11px;*/ 59 | border: 1px solid #8A8A8A; 60 | } 61 | 62 | input[type=range]::-webkit-slider-thumb 63 | { 64 | box-shadow: 1px 1px 1px #828282; 65 | border: 1px solid #8A8A8A; 66 | height: 15px; 67 | width: 15px; 68 | border-radius: 10px; 69 | background: #FFFFFF; 70 | cursor: pointer; 71 | -webkit-appearance: none; 72 | margin-top: -7px; 73 | } 74 | 75 | input[type=range]:focus::-webkit-slider-runnable-track 76 | { 77 | background: #B6B6B6; 78 | } 79 | 80 | input[type=range]::-moz-range-track 81 | { 82 | height: 3px; 83 | cursor: pointer; 84 | animate: 0.2s; 85 | box-shadow: 0px 0px 0px #000000; 86 | background: #B6B6B6; 87 | border-radius: 11px; 88 | border: 1px solid #8A8A8A; 89 | } 90 | 91 | input[type=range]::-moz-range-thumb 92 | { 93 | box-shadow: 1px 1px 1px #828282; 94 | border: 1px solid #8A8A8A; 95 | height: 12px; 96 | width: 12px; 97 | border-radius: 10px; 98 | background: #FFFFFF; 99 | cursor: pointer; 100 | } 101 | 102 | input[type=range]::-ms-track 103 | { 104 | width: 100%; 105 | height: 7px; 106 | cursor: pointer; 107 | animate: 0.2s; 108 | background: transparent; 109 | border-color: transparent; 110 | color: transparent; 111 | } 112 | 113 | input[type=range]::-ms-fill-lower 114 | { 115 | background: #B6B6B6; 116 | border: 1px solid #8A8A8A; 117 | border-radius: 22px; 118 | box-shadow: 0px 0px 0px #000000; 119 | } 120 | 121 | input[type=range]::-ms-fill-upper 122 | { 123 | background: #B6B6B6; 124 | border: 1px solid #8A8A8A; 125 | border-radius: 22px; 126 | box-shadow: 0px 0px 0px #000000; 127 | } 128 | 129 | input[type=range]::-ms-thumb 130 | { 131 | box-shadow: 1px 1px 1px #828282; 132 | border: 1px solid #8A8A8A; 133 | height: 24px; 134 | width: 7px; 135 | border-radius: 0px; 136 | background: #FFFFFF; 137 | cursor: pointer; 138 | } 139 | 140 | input[type=range]:focus::-ms-fill-lower 141 | { 142 | background: #B6B6B6; 143 | } 144 | 145 | input[type=range]:focus::-ms-fill-upper 146 | { 147 | background: #B6B6B6; 148 | } 149 | 150 | #webrx-top-container 151 | { 152 | position: relative; 153 | z-index:1000; 154 | } 155 | 156 | .webrx-top-bar-parts 157 | { 158 | position: absolute; 159 | top: 0px; 160 | left: 0px; 161 | width:100%; 162 | height:67px; 163 | } 164 | 165 | #webrx-top-bar-background 166 | { 167 | background-color: #808080; 168 | opacity: 0.15; 169 | filter:alpha(opacity=15); 170 | } 171 | 172 | #webrx-top-bar 173 | { 174 | margin:0; 175 | padding:0; 176 | user-select: none; 177 | -webkit-touch-callout: none; 178 | -webkit-user-select: none; 179 | -khtml-user-select: none; 180 | -moz-user-select: none; 181 | -ms-user-select: none; 182 | } 183 | 184 | #webrx-top-logo 185 | { 186 | position: absolute; 187 | top: 12px; 188 | left: 15px; 189 | } 190 | 191 | #webrx-ha5kfu-top-logo 192 | { 193 | position: absolute; 194 | top: 15px; 195 | right: 15px; 196 | } 197 | 198 | #webrx-top-photo 199 | { 200 | width: 100%; 201 | display: block; 202 | } 203 | 204 | #webrx-rx-avatar-background 205 | { 206 | cursor:pointer; 207 | position: absolute; 208 | left: 285px; 209 | top: 6px; 210 | } 211 | 212 | #webrx-rx-avatar 213 | { 214 | cursor:pointer; 215 | position: absolute; 216 | left: 289px; 217 | top: 10px; 218 | width: 46px; 219 | height: 46px; 220 | } 221 | 222 | #webrx-top-photo-clip 223 | { 224 | max-height: 350px; 225 | overflow: hidden; 226 | position: relative; 227 | } 228 | 229 | /*#webrx-bottom-bar 230 | { 231 | position: absolute; 232 | bottom: 0px; 233 | width: 100%; 234 | height: 117px; 235 | background-image:url(gfx/webrx-bottom-bar.png); 236 | }*/ 237 | 238 | #webrx-page-container 239 | { 240 | min-height:100%; 241 | position:relative; 242 | } 243 | 244 | /*#webrx-photo-gradient-left 245 | { 246 | position: absolute; 247 | bottom: 0px; 248 | left: 0px; 249 | background-image:url(gfx/webrx-photo-gradient-corner.png); 250 | width: 59px; 251 | height: 92px; 252 | 253 | } 254 | 255 | #webrx-photo-gradient-middle 256 | { 257 | position: absolute; 258 | bottom: 0px; 259 | left: 59px; 260 | right: 59px; 261 | height: 92px; 262 | background-image:url(gfx/webrx-photo-gradient-middle.png); 263 | } 264 | 265 | #webrx-photo-gradient-right 266 | { 267 | position: absolute; 268 | bottom: 0px; 269 | right: 0px; 270 | background-image:url(gfx/webrx-photo-gradient-corner.png); 271 | width: 59px; 272 | height: 92px; 273 | -webkit-transform:scaleX(-1); 274 | -moz-transform:scaleX(-1); 275 | -ms-transform:scaleX(-1); 276 | -o-transform:scaleX(-1); 277 | transform:scaleX(-1); 278 | }*/ 279 | 280 | #webrx-rx-photo-title 281 | { 282 | position: absolute; 283 | left: 15px; 284 | top: 78px; 285 | color: White; 286 | font-size: 16pt; 287 | text-shadow: 1px 1px 4px #444; 288 | opacity: 1; 289 | } 290 | 291 | #webrx-rx-photo-desc 292 | { 293 | position: absolute; 294 | left: 15px; 295 | top: 109px; 296 | color: White; 297 | font-size: 10pt; 298 | font-weight: bold; 299 | text-shadow: 0px 0px 6px #444; 300 | opacity: 1; 301 | line-height: 1.5em; 302 | } 303 | 304 | #webrx-rx-photo-desc a 305 | { 306 | /*color: #007df1;*/ 307 | color: #5ca8ff; 308 | text-shadow: none; 309 | /*text-shadow: 0px 0px 7px #fff;*/ 310 | } 311 | 312 | #webrx-rx-title 313 | { 314 | white-space:nowrap; 315 | overflow: hidden; 316 | cursor:pointer; 317 | position: absolute; 318 | left: 350px; 319 | top: 13px; 320 | font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; 321 | color: #909090; 322 | font-size: 11pt; 323 | font-weight: bold; 324 | } 325 | 326 | #webrx-rx-desc 327 | { 328 | white-space:nowrap; 329 | overflow: hidden; 330 | cursor:pointer; 331 | font-size: 10pt; 332 | color: #909090; 333 | position: absolute; 334 | left: 350px; 335 | top: 34px; 336 | } 337 | 338 | #webrx-rx-desc a 339 | { 340 | color: #909090; 341 | /*text-decoration: none;*/ 342 | } 343 | 344 | #openwebrx-rx-details-arrow 345 | { 346 | cursor:pointer; 347 | position: absolute; 348 | left: 470px; 349 | top: 51px; 350 | } 351 | 352 | #openwebrx-rx-details-arrow a 353 | { 354 | margin: 0; 355 | padding: 0; 356 | } 357 | 358 | #openwebrx-rx-details-arrow-down 359 | { 360 | display:none; 361 | } 362 | 363 | /*canvas#waterfall-canvas 364 | { 365 | border-style: none; 366 | border-width: 1px; 367 | height: 150px; 368 | width: 100%; 369 | }*/ 370 | 371 | #openwebrx-scale-container 372 | { 373 | height: 47px; 374 | background-image: url("gfx/openwebrx-scale-background.png"); 375 | background-repeat: repeat-x; 376 | overflow: hidden; 377 | z-index:1000; 378 | position: relative; 379 | } 380 | 381 | #webrx-canvas-container 382 | { 383 | /*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/ 384 | position: relative; 385 | height: 2000px; 386 | overflow-y: scroll; 387 | overflow-x: hidden; 388 | /*background-color: #646464;*/ 389 | /*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/ 390 | background-image: url('gfx/openwebrx-background-cool-blue.png'); 391 | background-repeat: no-repeat; 392 | background-color: #1e5f7f; 393 | cursor: crosshair; 394 | } 395 | 396 | #webrx-canvas-container canvas 397 | { 398 | position: absolute; 399 | border-style: none; 400 | image-rendering: crisp-edges; 401 | image-rendering: -webkit-optimize-contrast; 402 | /*transition: left 200ms, width 200ms;*/ 403 | } 404 | 405 | #openwebrx-mathbox-container 406 | { 407 | overflow: none; 408 | display: none; 409 | } 410 | 411 | #openwebrx-phantom-canvas 412 | { 413 | position: absolute; 414 | width: 0px; 415 | height: 0px; 416 | } 417 | 418 | /*#openwebrx-canvas-gradient-background 419 | { 420 | overflow: hidden; 421 | width: 100%; 422 | height: 396px; 423 | }*/ 424 | 425 | #openwebrx-log-scroll 426 | { 427 | /*overflow-y:auto;*/ 428 | height: 125px; 429 | width: 619px 430 | } 431 | 432 | .nano .nano-pane { background: #444; } 433 | .nano .nano-slider { background: #eee !important; } 434 | 435 | #webrx-main-container 436 | { 437 | position: relative; 438 | width: 100%; 439 | margin: 0; 440 | padding: 0; 441 | } 442 | 443 | .webrx-error 444 | { 445 | font-weight: bold; 446 | color: #ff6262; 447 | } 448 | 449 | #openwebrx-problems span 450 | { 451 | background: #ff6262; 452 | padding: 3px; 453 | font-size: 8pt; 454 | color: white; 455 | font-weight: bold; 456 | border-radius: 4px; 457 | -moz-border-radius: 4px; 458 | margin: 0px 2px 0px 2px; 459 | } 460 | 461 | /*#webrx-freq-show 462 | { 463 | visibility: hidden; 464 | position: absolute; 465 | top: 0px; 466 | left: 0px; 467 | padding: 5px; 468 | font-weight: bold; 469 | border-radius: 10px; 470 | -moz-border-radius: 10px; 471 | background-color: #999999; 472 | color: White; 473 | z-index:9999; /*should be higher? 474 | 475 | }*/ 476 | 477 | /* removed non-free fonts like that: */ 478 | /*@font-face { 479 | font-family: 'unibody_8_pro_regregular'; 480 | src: url('gfx/unibody8pro-regular-webfont.eot'); 481 | src: url('gfx/unibody8pro-regular-webfont.ttf'); 482 | font-weight: normal; 483 | font-style: normal; 484 | }*/ 485 | 486 | @font-face { 487 | font-family: 'expletus-sans-medium'; 488 | src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf'); 489 | font-weight: normal; 490 | font-style: normal; 491 | } 492 | 493 | #webrx-actual-freq 494 | { 495 | width: 100%; 496 | text-align: left; 497 | font-size: 16pt; 498 | font-family: 'expletus-sans-medium'; 499 | padding: 0; 500 | margin: 0; 501 | line-height:22px; 502 | 503 | } 504 | 505 | #webrx-mouse-freq 506 | { 507 | width: 100%; 508 | text-align: left; 509 | font-size: 10pt; 510 | color: #AAA; 511 | font-family: 'expletus-sans-medium'; 512 | margin-bottom: 5px; 513 | } 514 | 515 | 516 | .openwebrx-panel 517 | { 518 | transform: perspective( 600px ) rotateX( 90deg ); 519 | visibility: hidden; 520 | background-color: #575757; 521 | padding: 10px; 522 | color: white; 523 | position: fixed; 524 | font-size: 10pt; 525 | border-radius: 15px; 526 | -moz-border-radius: 15px; 527 | } 528 | 529 | .openwebrx-panel a 530 | { 531 | color: #5ca8ff; 532 | text-shadow: none; 533 | } 534 | 535 | .openwebrx-panel-inner 536 | { 537 | overflow-y: auto; 538 | overflow-x: hidden; 539 | height: 100%; 540 | } 541 | 542 | .openwebrx-button 543 | { 544 | background-color: #373737; 545 | padding: 4.2px; 546 | border-radius: 5px; 547 | -moz-border-radius: 5px; 548 | color: White; 549 | font-weight: bold; 550 | margin-right: 1px; 551 | cursor: pointer; 552 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) ); 553 | background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% ); 554 | user-select: none; 555 | -webkit-touch-callout: none; 556 | -webkit-user-select: none; 557 | -khtml-user-select: none; 558 | -moz-user-select: none; 559 | -ms-user-select: none; 560 | display: inline-block; 561 | } 562 | 563 | .openwebrx-button:hover, .openwebrx-demodulator-button.highlighted 564 | { 565 | /*background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #3F3F3F), color-stop(1, #777777) ); 566 | background:-moz-linear-gradient( center top, #373737 5%, #4F4F4F 100% );*/ 567 | background: #474747; 568 | color: #FFFF50; 569 | } 570 | 571 | .openwebrx-button:active 572 | { 573 | background: #777777; 574 | color: #FFFF50; 575 | } 576 | 577 | .openwebrx-demodulator-button 578 | { 579 | width: 38px; 580 | height: 19px; 581 | font-size: 12pt; 582 | text-align: center; 583 | } 584 | 585 | .openwebrx-square-button img 586 | { 587 | height: 27px; 588 | } 589 | 590 | .openwebrx-round-button 591 | { 592 | margin-right: -2px; 593 | width: 35px; 594 | height: 35px; 595 | border-radius: 25px; 596 | } 597 | 598 | .openwebrx-round-button img 599 | { 600 | height: 30px; 601 | } 602 | 603 | .openwebrx-round-button-small 604 | { 605 | margin-right: -3px; 606 | width: 20px; 607 | height: 20px; 608 | border-radius: 25px; 609 | } 610 | 611 | .openwebrx-round-button-small img 612 | { 613 | height: 20px; 614 | } 615 | 616 | img.openwebrx-mirror-img 617 | { 618 | transform: scale(-1, 1); 619 | } 620 | 621 | 622 | .openwebrx-round-rightarrow img 623 | { 624 | position: relative; 625 | left: 12px; 626 | top: 3px; 627 | } 628 | 629 | .openwebrx-round-leftarrow img 630 | { 631 | position: relative; 632 | left: 7px; 633 | top: 3px; 634 | } 635 | 636 | #openwebrx-client-log-title 637 | { 638 | margin-bottom: 5px; 639 | font-weight: bold; 640 | } 641 | 642 | .openwebrx-progressbar 643 | { 644 | position: relative; 645 | border-radius: 5px; 646 | background-color: #003850; /*#006235;*/ 647 | display: inline-block; 648 | text-align: center; 649 | font-size: 8pt; 650 | font-weight: bold; 651 | text-shadow: 0px 0px 4px #000000; 652 | cursor: default; 653 | user-select: none; 654 | -webkit-touch-callout: none; 655 | -webkit-user-select: none; 656 | -khtml-user-select: none; 657 | -moz-user-select: none; 658 | -ms-user-select: none; 659 | } 660 | 661 | .openwebrx-progressbar-bar 662 | { 663 | border-radius: 5px; 664 | height: 100%; 665 | width: 100%; 666 | } 667 | 668 | .openwebrx-progressbar-text 669 | { 670 | position: absolute; 671 | left:0px; 672 | top:4px; 673 | width: inherit; 674 | } 675 | 676 | #openwebrx-panel-status 677 | { 678 | margin: 0px; 679 | padding: 0px; 680 | background-color:rgba(0, 0, 0, 0); 681 | } 682 | 683 | #openwebrx-panel-status div.openwebrx-progressbar 684 | { 685 | width: 200px; 686 | height: 20px; 687 | } 688 | 689 | #openwebrx-main-buttons img 690 | { 691 | } 692 | 693 | #openwebrx-main-buttons ul 694 | { 695 | display: table; 696 | margin:0; 697 | } 698 | 699 | 700 | #openwebrx-main-buttons ul li 701 | { 702 | display: table-cell; 703 | padding-left: 5px; 704 | padding-right: 5px; 705 | cursor:pointer; 706 | } 707 | 708 | #openwebrx-main-buttons li:hover 709 | { 710 | background-color: rgba(255, 255, 255, 0.3); 711 | } 712 | 713 | #openwebrx-main-buttons li:active 714 | { 715 | background-color: rgba(255, 255, 255, 0.55); 716 | } 717 | 718 | 719 | #openwebrx-main-buttons 720 | { 721 | position: absolute; 722 | right: 133px; 723 | top: 3px; 724 | margin:0; 725 | color: white; 726 | text-shadow: 0px 0px 4px #000000; 727 | text-align: center; 728 | font-size: 9pt; 729 | font-weight: bold; 730 | } 731 | 732 | #openwebrx-panel-receiver 733 | { 734 | width:110px; 735 | } 736 | 737 | #openwebrx-mute-on 738 | { 739 | color: lime; 740 | } 741 | 742 | #openwebrx-mute-off 743 | { 744 | color: white; 745 | } 746 | 747 | .openwebrx-panel-slider 748 | { 749 | position: relative; 750 | top: -2px; 751 | width: 95px; 752 | } 753 | 754 | .openwebrx-sliderbtn-img 755 | { 756 | width: 14px; 757 | position:relative; 758 | top: 1px; 759 | } 760 | 761 | .openwebrx-panel-line 762 | { 763 | padding-top: 5px; 764 | } 765 | 766 | #openwebrx-smeter-outer 767 | { 768 | border-color: #888; 769 | border-style: solid; 770 | border-width: 0px; 771 | width: 255px; 772 | height: 7px; 773 | background-color: #373737; 774 | border-radius: 3px; 775 | position: relative; 776 | } 777 | #openwebrx-smeter-bar 778 | { 779 | transition: all 0.2s linear; 780 | width: 0px; 781 | height: 7px; 782 | background: linear-gradient(to top, #ff5939 , #961700); 783 | position: absolute; 784 | margin: 0; padding: 0; left: 0; 785 | border-radius: 3px; 786 | } 787 | 788 | #openwebrx-smeter-db 789 | { 790 | color: #aaa; 791 | display: inline-block; 792 | font-size: 10pt; 793 | float: right; 794 | margin-right: 5px; 795 | margin-top: 24px; 796 | font-family: 'expletus-sans-medium'; 797 | } 798 | 799 | #openwebrx-big-grey 800 | { 801 | position: fixed; 802 | width: 100%; 803 | height: 100%; 804 | margin: 0; 805 | padding: 0; 806 | opacity: 0.8; 807 | background-color: #777; 808 | left: 0; 809 | top: 0; 810 | z-index: 1001; 811 | display: none; 812 | vertical-align: middle; 813 | text-align: center; 814 | color: white; 815 | font-weight: bold; 816 | font-size: 20pt; 817 | cursor: pointer; 818 | transition: opacity 0.3s linear; 819 | } 820 | 821 | #openwebrx-big-grey img 822 | { 823 | width: 150px; 824 | } 825 | 826 | #openwebrx-digimode-canvas-container 827 | { 828 | /*margin: -10px -10px 10px -10px;*/ 829 | margin: -10px -10px 0px -10px; 830 | border-radius: 15px; 831 | height: 150px; 832 | background-color: #333; 833 | position: relative; 834 | overflow: hidden; 835 | } 836 | 837 | #openwebrx-digimode-canvas-container canvas 838 | { 839 | position: absolute; 840 | pointer-events: none; 841 | transition: width 500ms, left 500ms; 842 | } 843 | 844 | #openwebrx-secondary-demod-listbox 845 | { 846 | width: 201px; 847 | height: 27px; 848 | border-radius: 5px; 849 | background-color: #373737; 850 | color: White; 851 | font-weight: normal; 852 | font-size: 13pt; 853 | margin-right: 1px; 854 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) ); 855 | background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% ); 856 | border-color: transparent; 857 | border-width: 0px; 858 | -moz-appearance: none; 859 | padding-left:3px; 860 | } 861 | 862 | #openwebrx-secondary-demod-listbox option 863 | { 864 | border-width: 0px; 865 | background-color: #373737; 866 | color: White; 867 | } 868 | 869 | #openwebrx-cursor-blink 870 | { 871 | animation: cursor-blink 1s infinite; 872 | /*animation: cursor-3d 2s infinite;*/ 873 | animation-timing-function: linear; 874 | animation-direction: alternate; 875 | height: 1em; 876 | width: 8px; 877 | background-color: White; 878 | display: inline-block; 879 | position: relative; 880 | top: 1px; 881 | /*perspective: 60px;*/ 882 | 883 | } 884 | 885 | @keyframes cursor-blink 886 | { 887 | 0%{ opacity: 0; } 888 | 50% { opacity: 1; } 889 | 100%{ opacity: 0; } 890 | } 891 | 892 | @keyframes cursor-3d 893 | { 894 | 0%{ transform: rotateX(0deg) rotateX(Ydeg); } 895 | 50% { transform: rotateX(180deg) rotateY(360deg); opacity: 0.1; } 896 | 100%{ transform: rotateX(360deg) rotateY(720deg); } 897 | } 898 | 899 | #openwebrx-digimode-content 900 | { 901 | word-wrap: break-word; 902 | position: absolute; 903 | bottom: 0; 904 | width: 100%; 905 | } 906 | 907 | #openwebrx-digimode-content-container 908 | { 909 | overflow-y: hidden; 910 | display: block; 911 | height: 50px; 912 | position: relative; 913 | } 914 | 915 | #openwebrx-digimode-content-container .gradient 916 | { 917 | width: 100%; 918 | height: 20px; 919 | background: linear-gradient(to top, rgba(87,87,87,0) 0%,rgba(87,87,87,1) 100%); 920 | position: absolute; 921 | top: 0; 922 | z-index: 10; 923 | } 924 | 925 | 926 | #openwebrx-digimode-content .part 927 | { 928 | perspective: 700px; 929 | } 930 | 931 | #openwebrx-digimode-content .part 932 | { 933 | animation: new-digimode-data-3d 100ms; 934 | animation-timing-function: linear; 935 | display: inline-block; 936 | perspective-origin: 50% 50%; 937 | transform-origin: 0% 50%; 938 | } 939 | 940 | #openwebrx-digimode-content .part .subpart 941 | { 942 | } 943 | 944 | 945 | @keyframes new-digimode-data 946 | { 947 | 0%{ opacity: 0; } 948 | 100%{ opacity: 1; } 949 | } 950 | 951 | @keyframes new-digimode-data-3d 952 | { 953 | 0%{ transform: rotateX(0deg) rotateY(-90deg) translateX(-5px) scale(1.3); } 954 | 100%{ transform: rotateX(0deg) rotateY(0deg) translateX(0) scale(1); } 955 | } 956 | 957 | #openwebrx-digimode-select-channel 958 | { 959 | transition: all 500ms; 960 | background-color: Yellow; 961 | display: block; 962 | position: absolute; 963 | pointer-events: none; 964 | height: 100%; 965 | width: 0px; 966 | top: 0px; 967 | left: 0px; 968 | opacity: 0.7; 969 | border-style: solid; 970 | border-width: 0px; 971 | border-color: Red; 972 | } 973 | 974 | -------------------------------------------------------------------------------- /htdocs/retry.html: -------------------------------------------------------------------------------- 1 | 2 | 22 | OpenWebRX 23 | 74 | 79 | 80 | 81 | 82 | 83 |
84 | 85 |
86 | There are no client slots left on this server. 87 |
88 | Please wait until a client disconnects.
We will try to reconnect in 30 seconds... 89 |
90 |
91 |
92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /htdocs/upgrade.html: -------------------------------------------------------------------------------- 1 | 2 | 22 | OpenWebRX 23 | 74 | 78 | 79 | 80 | 81 | 82 |
83 | 84 |
85 | Only the latest Google Chrome browser is supported at the moment.
86 | Please download and install Google Chrome.
87 |
88 | Alternatively, you may proceed to OpenWebRX, but it's not supposed to work as expected.
89 | Click here if you still want to try OpenWebRX. 90 |
91 |
92 |
93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /openwebrx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | print "" # python2.7 is required to run OpenWebRX instead of python3. Please run me by: python2 openwebrx.py 3 | """ 4 | 5 | This file is part of OpenWebRX, 6 | an open-source SDR receiver software with a web UI. 7 | Copyright (c) 2013-2015 by Andras Retzler 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU Affero General Public License as 11 | published by the Free Software Foundation, either version 3 of the 12 | License, or (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU Affero General Public License for more details. 18 | 19 | You should have received a copy of the GNU Affero General Public License 20 | along with this program. If not, see . 21 | 22 | """ 23 | sw_version="v0.17" 24 | #0.15 (added nmux) 25 | 26 | import os 27 | import code 28 | import importlib 29 | import csdr 30 | import thread 31 | import time 32 | import datetime 33 | import subprocess 34 | import os 35 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 36 | from SocketServer import ThreadingMixIn 37 | import fcntl 38 | import time 39 | import md5 40 | import random 41 | import threading 42 | import sys 43 | import traceback 44 | from collections import namedtuple 45 | import Queue 46 | import ctypes 47 | 48 | #import rtl_mus 49 | import rxws 50 | import uuid 51 | import signal 52 | import socket 53 | 54 | try: import sdrhu 55 | except: sdrhu=False 56 | avatar_ctime="" 57 | 58 | #pypy compatibility 59 | try: import dl 60 | except: pass 61 | try: import __pypy__ 62 | except: pass 63 | pypy="__pypy__" in globals() 64 | 65 | """ 66 | def import_all_plugins(directory): 67 | for subdir in os.listdir(directory): 68 | if os.path.isdir(directory+subdir) and not subdir[0]=="_": 69 | exact_path=directory+subdir+"/plugin.py" 70 | if os.path.isfile(exact_path): 71 | importname=(directory+subdir+"/plugin").replace("/",".") 72 | print "[openwebrx-import] Found plugin:",importname 73 | importlib.import_module(importname) 74 | """ 75 | 76 | class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer): 77 | pass 78 | 79 | def handle_signal(sig, frame): 80 | global spectrum_dsp 81 | if sig == signal.SIGUSR1: 82 | print "[openwebrx] Verbose status information on USR1 signal" 83 | print 84 | print "time.time() =", time.time() 85 | print "clients_mutex.locked() =", clients_mutex.locked() 86 | print "clients_mutex_locker =", clients_mutex_locker 87 | if server_fail: print "server_fail = ", server_fail 88 | print "spectrum_thread_watchdog_last_tick =", spectrum_thread_watchdog_last_tick 89 | print 90 | print "clients:",len(clients) 91 | for client in clients: 92 | print 93 | for key in client._fields: 94 | print "\t%s = %s"%(key,str(getattr(client,key))) 95 | elif sig == signal.SIGUSR2: 96 | code.interact(local=globals()) 97 | else: 98 | print "[openwebrx] Ctrl+C: aborting." 99 | cleanup_clients(True) 100 | spectrum_dsp.stop() 101 | os._exit(1) #not too graceful exit 102 | 103 | def access_log(data): 104 | global logs 105 | logs.access_log.write("["+datetime.datetime.now().isoformat()+"] "+data+"\n") 106 | logs.access_log.flush() 107 | 108 | receiver_failed=spectrum_thread_watchdog_last_tick=rtl_thread=spectrum_dsp=server_fail=None 109 | 110 | def main(): 111 | global clients, clients_mutex, pypy, lock_try_time, avatar_ctime, cfg, logs 112 | global serverfail, rtl_thread 113 | print 114 | print "OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package" 115 | print "_________________________________________________________________________________________________" 116 | print 117 | print "Author contact info: Andras Retzler, HA7ILM " 118 | print 119 | 120 | no_arguments=len(sys.argv)==1 121 | if no_arguments: print "[openwebrx-main] Configuration script not specified. I will use: \"config_webrx.py\"" 122 | cfg=__import__("config_webrx" if no_arguments else sys.argv[1]) 123 | for option in ("access_log","csdr_dynamic_bufsize","csdr_print_bufsizes","csdr_through"): 124 | if not option in dir(cfg): setattr(cfg, option, False) #initialize optional config parameters 125 | 126 | #Open log files 127 | logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})() 128 | 129 | #Set signal handler 130 | signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python 131 | signal.signal(signal.SIGUSR1, handle_signal) 132 | signal.signal(signal.SIGUSR2, handle_signal) 133 | 134 | #Pypy 135 | if pypy: print "pypy detected (and now something completely different: c code is expected to run at a speed of 3*10^8 m/s?)" 136 | 137 | #Change process name to "openwebrx" (to be seen in ps) 138 | try: 139 | for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]: 140 | if os.path.exists(libcpath): 141 | libc = dl.open(libcpath) 142 | libc.call("prctl", 15, "openwebrx", 0, 0, 0) 143 | break 144 | except: 145 | pass 146 | 147 | #Start rtl thread 148 | if os.system("csdr 2> /dev/null") == 32512: #check for csdr 149 | print "[openwebrx-main] You need to install \"csdr\" to run OpenWebRX!\n" 150 | return 151 | if os.system("nmux --help 2> /dev/null") == 32512: #check for nmux 152 | print "[openwebrx-main] You need to install an up-to-date version of \"csdr\" that contains the \"nmux\" tool to run OpenWebRX! Please upgrade \"csdr\"!\n" 153 | return 154 | if cfg.start_rtl_thread: 155 | nmux_bufcnt = nmux_bufsize = 0 156 | while nmux_bufsize < cfg.samp_rate/4: nmux_bufsize += 4096 157 | while nmux_bufsize * nmux_bufcnt < cfg.nmux_memory * 1e6: nmux_bufcnt += 1 158 | if nmux_bufcnt == 0 or nmux_bufsize == 0: 159 | print "[openwebrx-main] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py" 160 | return 161 | print "[openwebrx-main] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt) 162 | cfg.start_rtl_command += "| nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, cfg.iq_server_port) 163 | rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=()) 164 | rtl_thread.start() 165 | print "[openwebrx-main] Started rtl_thread: "+cfg.start_rtl_command 166 | print "[openwebrx-main] Waiting for I/Q server to start..." 167 | while True: 168 | testsock=socket.socket() 169 | try: testsock.connect(("127.0.0.1", cfg.iq_server_port)) 170 | except: 171 | time.sleep(0.1) 172 | continue 173 | testsock.close() 174 | break 175 | print "[openwebrx-main] I/Q server started." 176 | 177 | #Initialize clients 178 | clients=[] 179 | clients_mutex=threading.Lock() 180 | lock_try_time=0 181 | 182 | #Start watchdog thread 183 | print "[openwebrx-main] Starting watchdog threads." 184 | mutex_test_thread=threading.Thread(target = mutex_test_thread_function, args = ()) 185 | mutex_test_thread.start() 186 | mutex_watchdog_thread=threading.Thread(target = mutex_watchdog_thread_function, args = ()) 187 | mutex_watchdog_thread.start() 188 | 189 | 190 | #Start spectrum thread 191 | print "[openwebrx-main] Starting spectrum thread." 192 | spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ()) 193 | spectrum_thread.start() 194 | #spectrum_watchdog_thread=threading.Thread(target = spectrum_watchdog_thread_function, args = ()) 195 | #spectrum_watchdog_thread.start() 196 | 197 | get_cpu_usage() 198 | bcastmsg_thread=threading.Thread(target = bcastmsg_thread_function, args = ()) 199 | bcastmsg_thread.start() 200 | 201 | #threading.Thread(target = measure_thread_function, args = ()).start() 202 | 203 | #Start sdr.hu update thread 204 | if sdrhu and cfg.sdrhu_key and cfg.sdrhu_public_listing: 205 | print "[openwebrx-main] Starting sdr.hu update thread..." 206 | avatar_ctime=str(os.path.getctime("htdocs/gfx/openwebrx-avatar.png")) 207 | sdrhu_thread=threading.Thread(target = sdrhu.run, args = ()) 208 | sdrhu_thread.start() 209 | 210 | #Start HTTP thread 211 | httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler) 212 | print('[openwebrx-main] Starting HTTP server.') 213 | access_log("Starting OpenWebRX...") 214 | httpd.serve_forever() 215 | 216 | 217 | # This is a debug function below: 218 | measure_value=0 219 | def measure_thread_function(): 220 | global measure_value 221 | while True: 222 | print "[openwebrx-measure] value is",measure_value 223 | measure_value=0 224 | time.sleep(1) 225 | 226 | def bcastmsg_thread_function(): 227 | global clients 228 | while True: 229 | time.sleep(3) 230 | try: cpu_usage=get_cpu_usage() 231 | except: cpu_usage=0 232 | cma("bcastmsg_thread") 233 | for i in range(0,len(clients)): 234 | clients[i].bcastmsg="MSG cpu_usage={0} clients={1}".format(int(cpu_usage*100),len(clients)) 235 | cmr() 236 | 237 | def mutex_test_thread_function(): 238 | global clients_mutex, lock_try_time 239 | while True: 240 | time.sleep(0.5) 241 | lock_try_time=time.time() 242 | clients_mutex.acquire() 243 | clients_mutex.release() 244 | lock_try_time=0 245 | 246 | def cma(what): #clients_mutex acquire 247 | global clients_mutex 248 | global clients_mutex_locker 249 | if not clients_mutex.locked(): clients_mutex_locker = what 250 | clients_mutex.acquire() 251 | 252 | def cmr(): 253 | global clients_mutex 254 | global clients_mutex_locker 255 | clients_mutex_locker = None 256 | clients_mutex.release() 257 | 258 | def mutex_watchdog_thread_function(): 259 | global lock_try_time 260 | global clients_mutex_locker 261 | global clients_mutex 262 | while True: 263 | if lock_try_time != 0 and time.time()-lock_try_time > 3.0: 264 | #if 3 seconds pass without unlock 265 | print "[openwebrx-mutex-watchdog] Mutex unlock timeout. Locker: \""+str(clients_mutex_locker)+"\" Now unlocking..." 266 | clients_mutex.release() 267 | time.sleep(0.5) 268 | 269 | def spectrum_watchdog_thread_function(): 270 | global spectrum_thread_watchdog_last_tick, receiver_failed 271 | while True: 272 | time.sleep(60) 273 | if spectrum_thread_watchdog_last_tick and time.time()-spectrum_thread_watchdog_last_tick > 60.0: 274 | print "[openwebrx-spectrum-watchdog] Spectrum timeout. Seems like no I/Q data is coming from the receiver.\nIf you're using RTL-SDR, the receiver hardware may randomly fail under some circumstances:\n1) high temperature,\n2) insufficient current available from the USB port." 275 | print "[openwebrx-spectrum-watchdog] Deactivating receiver." 276 | receiver_failed="spectrum" 277 | return 278 | 279 | def check_server(): 280 | global spectrum_dsp, server_fail, rtl_thread 281 | if server_fail: return server_fail 282 | #print spectrum_dsp.process.poll() 283 | if spectrum_dsp and spectrum_dsp.process.poll()!=None: server_fail = "spectrum_thread dsp subprocess failed" 284 | #if rtl_thread and not rtl_thread.is_alive(): server_fail = "rtl_thread failed" 285 | if server_fail: print "[openwebrx-check_server] >>>>>>> ERROR:", server_fail 286 | return server_fail 287 | 288 | def apply_csdr_cfg_to_dsp(dsp): 289 | dsp.csdr_dynamic_bufsize = cfg.csdr_dynamic_bufsize 290 | dsp.csdr_print_bufsizes = cfg.csdr_print_bufsizes 291 | dsp.csdr_through = cfg.csdr_through 292 | 293 | def spectrum_thread_function(): 294 | global clients, spectrum_dsp, spectrum_thread_watchdog_last_tick 295 | spectrum_dsp=dsp=csdr.dsp() 296 | dsp.nc_port=cfg.iq_server_port 297 | dsp.set_demodulator("fft") 298 | dsp.set_samp_rate(cfg.samp_rate) 299 | dsp.set_fft_size(cfg.fft_size) 300 | dsp.set_fft_fps(cfg.fft_fps) 301 | dsp.set_fft_averages(int(round(1.0 * cfg.samp_rate / cfg.fft_size / cfg.fft_fps / (1.0 - cfg.fft_voverlap_factor))) if cfg.fft_voverlap_factor>0 else 0) 302 | dsp.set_fft_compression(cfg.fft_compression) 303 | dsp.set_format_conversion(cfg.format_conversion) 304 | apply_csdr_cfg_to_dsp(dsp) 305 | sleep_sec=0.87/cfg.fft_fps 306 | print "[openwebrx-spectrum] Spectrum thread initialized successfully." 307 | dsp.start() 308 | if cfg.csdr_dynamic_bufsize: 309 | dsp.read(8) #dummy read to skip bufsize & preamble 310 | print "[openwebrx-spectrum] Note: CSDR_DYNAMIC_BUFSIZE_ON = 1" 311 | print "[openwebrx-spectrum] Spectrum thread started." 312 | bytes_to_read=int(dsp.get_fft_bytes_to_read()) 313 | spectrum_thread_counter=0 314 | while True: 315 | data=dsp.read(bytes_to_read) 316 | #print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()" 317 | if spectrum_thread_counter >= cfg.fft_fps: 318 | spectrum_thread_counter=0 319 | spectrum_thread_watchdog_last_tick = time.time() #once every second 320 | else: spectrum_thread_counter+=1 321 | cma("spectrum_thread") 322 | correction=0 323 | for i in range(0,len(clients)): 324 | i-=correction 325 | if (clients[i].ws_started): 326 | if clients[i].spectrum_queue.full(): 327 | print "[openwebrx-spectrum] client spectrum queue full, closing it." 328 | close_client(i, False) 329 | correction+=1 330 | else: 331 | clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients 332 | cmr() 333 | 334 | def get_client_by_id(client_id, use_mutex=True): 335 | global clients 336 | output=-1 337 | if use_mutex: cma("get_client_by_id") 338 | for i in range(0,len(clients)): 339 | if(clients[i].id==client_id): 340 | output=i 341 | break 342 | if use_mutex: cmr() 343 | if output==-1: 344 | raise ClientNotFoundException 345 | else: 346 | return output 347 | 348 | def log_client(client, what): 349 | print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what) 350 | 351 | def cleanup_clients(end_all=False): 352 | # - if a client doesn't open websocket for too long time, we drop it 353 | # - or if end_all is true, we drop all clients 354 | global clients 355 | cma("cleanup_clients") 356 | correction=0 357 | for i in range(0,len(clients)): 358 | i-=correction 359 | #print "cleanup_clients:: len(clients)=", len(clients), "i=", i 360 | if end_all or ((not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45): 361 | if not end_all: print "[openwebrx] cleanup_clients :: client timeout to open WebSocket" 362 | close_client(i, False) 363 | correction+=1 364 | cmr() 365 | 366 | def generate_client_id(ip): 367 | #add a client 368 | global clients 369 | new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip closed bcastmsg dsp loopstat") 370 | new_client.id=md5.md5(str(random.random())).hexdigest() 371 | new_client.gen_time=time.time() 372 | new_client.ws_started=False # to check whether client has ever tried to open the websocket 373 | new_client.spectrum_queue=Queue.Queue(1000) 374 | new_client.ip=ip 375 | new_client.bcastmsg="" 376 | new_client.closed=[False] #byref, not exactly sure if required 377 | new_client.dsp=None 378 | cma("generate_client_id") 379 | clients.append(new_client) 380 | log_client(new_client,"client added. Clients now: {0}".format(len(clients))) 381 | cmr() 382 | cleanup_clients() 383 | return new_client.id 384 | 385 | def close_client(i, use_mutex=True): 386 | global clients 387 | log_client(clients[i],"client being closed.") 388 | if use_mutex: cma("close_client") 389 | try: 390 | clients[i].dsp.stop() 391 | except: 392 | exc_type, exc_value, exc_traceback = sys.exc_info() 393 | print "[openwebrx] close_client dsp.stop() :: error -",exc_type,exc_value 394 | traceback.print_tb(exc_traceback) 395 | clients[i].closed[0]=True 396 | access_log("Stopped streaming to client: "+clients[i].ip+"#"+str(clients[i].id)+" (users now: "+str(len(clients)-1)+")") 397 | del clients[i] 398 | if use_mutex: cmr() 399 | 400 | # http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python 401 | # some ideas are used from the artice above 402 | 403 | class WebRXHandler(BaseHTTPRequestHandler): 404 | def proc_read_thread(): 405 | pass 406 | 407 | def send_302(self,what): 408 | self.send_response(302) 409 | self.send_header('Content-type','text/html') 410 | self.send_header("Location", "http://{0}:{1}/{2}".format(cfg.server_hostname,cfg.web_port,what)) 411 | self.end_headers() 412 | self.wfile.write("

Object moved

Please click here to continue.".format(what)) 413 | 414 | 415 | def do_GET(self): 416 | self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 417 | global dsp_plugin, clients_mutex, clients, avatar_ctime, sw_version, receiver_failed 418 | rootdir = 'htdocs' 419 | self.path=self.path.replace("..","") 420 | path_temp_parts=self.path.split("?") 421 | self.path=path_temp_parts[0] 422 | request_param=path_temp_parts[1] if(len(path_temp_parts)>1) else "" 423 | access_log("GET "+self.path+" from "+self.client_address[0]) 424 | try: 425 | if self.path=="/": 426 | self.path="/index.wrx" 427 | # there's even another cool tip at http://stackoverflow.com/questions/4419650/how-to-implement-timeout-in-basehttpserver-basehttprequesthandler-python 428 | #if self.path[:5]=="/lock": cma("do_GET /lock/") # to test mutex_watchdog_thread. Do not uncomment in production environment! 429 | if self.path[:4]=="/ws/": 430 | print "[openwebrx-ws] Client requested WebSocket connection" 431 | if receiver_failed: self.send_error(500,"Internal server error") 432 | try: 433 | # ========= WebSocket handshake ========= 434 | ws_success=True 435 | try: 436 | rxws.handshake(self) 437 | cma("do_GET /ws/") 438 | client_i=get_client_by_id(self.path[4:], False) 439 | myclient=clients[client_i] 440 | except rxws.WebSocketException: ws_success=False 441 | except ClientNotFoundException: ws_success=False 442 | finally: 443 | if clients_mutex.locked(): cmr() 444 | if not ws_success: 445 | self.send_error(400, 'Bad request.') 446 | return 447 | 448 | # ========= Client handshake ========= 449 | if myclient.ws_started: 450 | print "[openwebrx-httpd] error: second WS connection with the same client id, throwing it." 451 | self.send_error(400, 'Bad request.') #client already started 452 | return 453 | rxws.send(self, "CLIENT DE SERVER openwebrx.py") 454 | client_ans=rxws.recv(self, True) 455 | if client_ans[:16]!="SERVER DE CLIENT": 456 | rxws.send("ERR Bad answer.") 457 | return 458 | myclient.ws_started=True 459 | #send default parameters 460 | rxws.send(self, "MSG center_freq={0} bandwidth={1} fft_size={2} fft_fps={3} audio_compression={4} fft_compression={5} max_clients={6} setup".format(str(cfg.shown_center_freq),str(cfg.samp_rate),cfg.fft_size,cfg.fft_fps,cfg.audio_compression,cfg.fft_compression,cfg.max_clients)) 461 | 462 | # ========= Initialize DSP ========= 463 | dsp=csdr.dsp() 464 | dsp_initialized=False 465 | dsp.set_audio_compression(cfg.audio_compression) 466 | dsp.set_fft_compression(cfg.fft_compression) #used by secondary chains 467 | dsp.set_format_conversion(cfg.format_conversion) 468 | dsp.set_offset_freq(0) 469 | dsp.set_bpf(-4000,4000) 470 | dsp.set_secondary_fft_size(cfg.digimodes_fft_size) 471 | dsp.nc_port=cfg.iq_server_port 472 | apply_csdr_cfg_to_dsp(dsp) 473 | myclient.dsp=dsp 474 | do_secondary_demod=False 475 | access_log("Started streaming to client: "+self.client_address[0]+"#"+myclient.id+" (users now: "+str(len(clients))+")") 476 | 477 | while True: 478 | myclient.loopstat=0 479 | if myclient.closed[0]: 480 | print "[openwebrx-httpd:ws] client closed by other thread" 481 | break 482 | 483 | # ========= send audio ========= 484 | if dsp_initialized: 485 | myclient.loopstat=10 486 | temp_audio_data=dsp.read(256) 487 | myclient.loopstat=11 488 | rxws.send(self, temp_audio_data, "AUD ") 489 | 490 | # ========= send spectrum ========= 491 | while not myclient.spectrum_queue.empty(): 492 | myclient.loopstat=20 493 | spectrum_data=myclient.spectrum_queue.get() 494 | #spectrum_data_mid=len(spectrum_data[0])/2 495 | #rxws.send(self, spectrum_data[0][spectrum_data_mid:]+spectrum_data[0][:spectrum_data_mid], "FFT ") 496 | # (it seems GNU Radio exchanges the first and second part of the FFT output, we correct it) 497 | myclient.loopstat=21 498 | rxws.send(self, spectrum_data[0],"FFT ") 499 | 500 | # ========= send smeter_level ========= 501 | smeter_level=None 502 | while True: 503 | try: 504 | myclient.loopstat=30 505 | smeter_level=dsp.get_smeter_level() 506 | if smeter_level == None: break 507 | except: 508 | break 509 | if smeter_level!=None: 510 | myclient.loopstat=31 511 | rxws.send(self, "MSG s={0}".format(smeter_level)) 512 | 513 | # ========= send bcastmsg ========= 514 | if myclient.bcastmsg!="": 515 | myclient.loopstat=40 516 | rxws.send(self,myclient.bcastmsg) 517 | myclient.bcastmsg="" 518 | 519 | # ========= send secondary ========= 520 | if do_secondary_demod: 521 | myclient.loopstat=41 522 | while True: 523 | try: 524 | secondary_spectrum_data=dsp.read_secondary_fft(dsp.get_secondary_fft_bytes_to_read()) 525 | if len(secondary_spectrum_data) == 0: break 526 | # print "len(secondary_spectrum_data)", len(secondary_spectrum_data) #TODO digimodes 527 | rxws.send(self, secondary_spectrum_data, "FFTS") 528 | except: break 529 | myclient.loopstat=42 530 | while True: 531 | try: 532 | myclient.loopstat=422 533 | secondary_demod_data=dsp.read_secondary_demod(1) 534 | myclient.loopstat=423 535 | if len(secondary_demod_data) == 0: break 536 | # print "len(secondary_demod_data)", len(secondary_demod_data), secondary_demod_data #TODO digimodes 537 | rxws.send(self, secondary_demod_data, "DAT ") 538 | except: break 539 | 540 | # ========= process commands ========= 541 | while True: 542 | myclient.loopstat=50 543 | rdata=rxws.recv(self, False) 544 | myclient.loopstat=51 545 | #try: 546 | if not rdata: break 547 | elif rdata[:3]=="SET": 548 | print "[openwebrx-httpd:ws,%d] command: %s"%(client_i,rdata) 549 | pairs=rdata[4:].split(" ") 550 | bpf_set=False 551 | new_bpf=dsp.get_bpf() 552 | filter_limit=dsp.get_output_rate()/2 553 | for pair in pairs: 554 | param_name, param_value = pair.split("=") 555 | if param_name == "low_cut" and -filter_limit <= int(param_value) <= filter_limit: 556 | bpf_set=True 557 | new_bpf[0]=int(param_value) 558 | elif param_name == "high_cut" and -filter_limit <= int(param_value) <= filter_limit: 559 | bpf_set=True 560 | new_bpf[1]=int(param_value) 561 | elif param_name == "offset_freq" and -cfg.samp_rate/2 <= int(param_value) <= cfg.samp_rate/2: 562 | myclient.loopstat=510 563 | dsp.set_offset_freq(int(param_value)) 564 | elif param_name == "squelch_level" and float(param_value) >= 0: 565 | myclient.loopstat=520 566 | dsp.set_squelch_level(float(param_value)) 567 | elif param_name=="mod": 568 | if (dsp.get_demodulator()!=param_value): 569 | myclient.loopstat=530 570 | if dsp_initialized: dsp.stop() 571 | dsp.set_demodulator(param_value) 572 | if dsp_initialized: dsp.start() 573 | elif param_name == "output_rate": 574 | if not dsp_initialized: 575 | myclient.loopstat=540 576 | dsp.set_output_rate(int(param_value)) 577 | myclient.loopstat=541 578 | dsp.set_samp_rate(cfg.samp_rate) 579 | elif param_name=="action" and param_value=="start": 580 | if not dsp_initialized: 581 | myclient.loopstat=550 582 | dsp.start() 583 | dsp_initialized=True 584 | elif param_name=="secondary_mod" and cfg.digimodes_enable: 585 | if (dsp.get_secondary_demodulator() != param_value): 586 | if dsp_initialized: dsp.stop() 587 | if param_value == "off": 588 | dsp.set_secondary_demodulator(None) 589 | do_secondary_demod = False 590 | else: 591 | dsp.set_secondary_demodulator(param_value) 592 | do_secondary_demod = True 593 | rxws.send(self, "MSG secondary_fft_size={0} if_samp_rate={1} secondary_bw={2} secondary_setup".format(cfg.digimodes_fft_size, dsp.if_samp_rate(), dsp.secondary_bw())) 594 | if dsp_initialized: dsp.start() 595 | elif param_name=="secondary_offset_freq" and 0 <= int(param_value) <= dsp.if_samp_rate()/2 and cfg.digimodes_enable: 596 | dsp.set_secondary_offset_freq(int(param_value)) 597 | else: 598 | print "[openwebrx-httpd:ws] invalid parameter" 599 | if bpf_set: 600 | myclient.loopstat=560 601 | dsp.set_bpf(*new_bpf) 602 | #code.interact(local=locals()) 603 | except: 604 | myclient.loopstat=990 605 | exc_type, exc_value, exc_traceback = sys.exc_info() 606 | print "[openwebrx-httpd:ws] exception: ",exc_type,exc_value 607 | traceback.print_tb(exc_traceback) #TODO digimodes 608 | #if exc_value[0]==32: #"broken pipe", client disconnected 609 | # pass 610 | #elif exc_value[0]==11: #"resource unavailable" on recv, client disconnected 611 | # pass 612 | #else: 613 | # print "[openwebrx-httpd] error in /ws/ handler: ",exc_type,exc_value 614 | # traceback.print_tb(exc_traceback) 615 | 616 | #stop dsp for the disconnected client 617 | myclient.loopstat=991 618 | try: 619 | dsp.stop() 620 | del dsp 621 | except: 622 | print "[openwebrx-httpd] error in dsp.stop()" 623 | 624 | #delete disconnected client 625 | myclient.loopstat=992 626 | try: 627 | cma("do_GET /ws/ delete disconnected") 628 | id_to_close=get_client_by_id(myclient.id,False) 629 | close_client(id_to_close,False) 630 | except: 631 | exc_type, exc_value, exc_traceback = sys.exc_info() 632 | print "[openwebrx-httpd] client cannot be closed: ",exc_type,exc_value 633 | traceback.print_tb(exc_traceback) 634 | finally: 635 | cmr() 636 | myclient.loopstat=1000 637 | return 638 | elif self.path in ("/status", "/status/"): 639 | #self.send_header('Content-type','text/plain') 640 | getbands=lambda: str(int(cfg.shown_center_freq-cfg.samp_rate/2))+"-"+str(int(cfg.shown_center_freq+cfg.samp_rate/2)) 641 | self.wfile.write("status="+("inactive" if receiver_failed else "active")+"\nname="+cfg.receiver_name+"\nsdr_hw="+cfg.receiver_device+"\nop_email="+cfg.receiver_admin+"\nbands="+getbands()+"\nusers="+str(len(clients))+"\nusers_max="+str(cfg.max_clients)+"\navatar_ctime="+avatar_ctime+"\ngps="+str(cfg.receiver_gps)+"\nasl="+str(cfg.receiver_asl)+"\nloc="+cfg.receiver_location+"\nsw_version="+sw_version+"\nantenna="+cfg.receiver_ant+"\n") 642 | print "[openwebrx-httpd] GET /status/ from",self.client_address[0] 643 | else: 644 | f=open(rootdir+self.path) 645 | data=f.read() 646 | extension=self.path[(len(self.path)-4):len(self.path)] 647 | extension=extension[2:] if extension[1]=='.' else extension[1:] 648 | checkresult=check_server() 649 | if extension == "wrx" and (checkresult or receiver_failed): 650 | self.send_302("inactive.html") 651 | return 652 | anyStringsPresentInUserAgent=lambda a: reduce(lambda x,y:x or y, map(lambda b:self.headers['user-agent'].count(b), a), False) 653 | if extension == "wrx" and ( (not anyStringsPresentInUserAgent(("Chrome","Firefox","Googlebot","iPhone","iPad","iPod"))) if 'user-agent' in self.headers.keys() else True ) and (not request_param.count("unsupported")): 654 | self.send_302("upgrade.html") 655 | return 656 | if extension == "wrx": 657 | cleanup_clients(False) 658 | if cfg.max_clients<=len(clients): 659 | self.send_302("retry.html") 660 | return 661 | self.send_response(200) 662 | if(("wrx","html","htm").count(extension)): 663 | self.send_header('Content-type','text/html') 664 | elif(extension=="js"): 665 | self.send_header('Content-type','text/javascript') 666 | elif(extension=="css"): 667 | self.send_header('Content-type','text/css') 668 | self.end_headers() 669 | if extension == "wrx": 670 | replace_dictionary=( 671 | ("%[RX_PHOTO_DESC]",cfg.photo_desc), 672 | ("%[CLIENT_ID]", generate_client_id(self.client_address[0])) if "%[CLIENT_ID]" in data else "", 673 | ("%[WS_URL]","ws://"+cfg.server_hostname+":"+str(cfg.web_port)+"/ws/"), 674 | ("%[RX_TITLE]",cfg.receiver_name), 675 | ("%[RX_LOC]",cfg.receiver_location), 676 | ("%[RX_QRA]",cfg.receiver_qra), 677 | ("%[RX_ASL]",str(cfg.receiver_asl)), 678 | ("%[RX_GPS]",str(cfg.receiver_gps[0])+","+str(cfg.receiver_gps[1])), 679 | ("%[RX_PHOTO_HEIGHT]",str(cfg.photo_height)),("%[RX_PHOTO_TITLE]",cfg.photo_title), 680 | ("%[RX_ADMIN]",cfg.receiver_admin), 681 | ("%[RX_ANT]",cfg.receiver_ant), 682 | ("%[RX_DEVICE]",cfg.receiver_device), 683 | ("%[AUDIO_BUFSIZE]",str(cfg.client_audio_buffer_size)), 684 | ("%[START_OFFSET_FREQ]",str(cfg.start_freq-cfg.center_freq)), 685 | ("%[START_MOD]",cfg.start_mod), 686 | ("%[WATERFALL_COLORS]",cfg.waterfall_colors), 687 | ("%[WATERFALL_MIN_LEVEL]",str(cfg.waterfall_min_level)), 688 | ("%[WATERFALL_MAX_LEVEL]",str(cfg.waterfall_max_level)), 689 | ("%[WATERFALL_AUTO_LEVEL_MARGIN]","[%d,%d]"%cfg.waterfall_auto_level_margin), 690 | ("%[DIGIMODES_ENABLE]",("true" if cfg.digimodes_enable else "false")), 691 | ("%[MATHBOX_WATERFALL_FRES]",str(cfg.mathbox_waterfall_frequency_resolution)), 692 | ("%[MATHBOX_WATERFALL_THIST]",str(cfg.mathbox_waterfall_history_length)), 693 | ("%[MATHBOX_WATERFALL_COLORS]",cfg.mathbox_waterfall_colors) 694 | ) 695 | for rule in replace_dictionary: 696 | while data.find(rule[0])!=-1: 697 | data=data.replace(rule[0],rule[1]) 698 | self.wfile.write(data) 699 | f.close() 700 | return 701 | except IOError: 702 | self.send_error(404, 'Invalid path.') 703 | except: 704 | exc_type, exc_value, exc_traceback = sys.exc_info() 705 | print "[openwebrx-httpd] error (@outside):", exc_type, exc_value 706 | traceback.print_tb(exc_traceback) 707 | 708 | 709 | class ClientNotFoundException(Exception): 710 | pass 711 | 712 | last_worktime=0 713 | last_idletime=0 714 | 715 | def get_cpu_usage(): 716 | global last_worktime, last_idletime 717 | try: 718 | f=open("/proc/stat","r") 719 | except: 720 | return 0 #Workaround, possibly we're on a Mac 721 | line="" 722 | while not "cpu " in line: line=f.readline() 723 | f.close() 724 | spl=line.split(" ") 725 | worktime=int(spl[2])+int(spl[3])+int(spl[4]) 726 | idletime=int(spl[5]) 727 | dworktime=(worktime-last_worktime) 728 | didletime=(idletime-last_idletime) 729 | rate=float(dworktime)/(didletime+dworktime) 730 | last_worktime=worktime 731 | last_idletime=idletime 732 | if(last_worktime==0): return 0 733 | return rate 734 | 735 | 736 | if __name__=="__main__": 737 | main() 738 | -------------------------------------------------------------------------------- /rxws.py: -------------------------------------------------------------------------------- 1 | """ 2 | rxws: WebSocket methods implemented for OpenWebRX 3 | 4 | This file is part of OpenWebRX, 5 | an open-source SDR receiver software with a web UI. 6 | Copyright (c) 2013-2015 by Andras Retzler 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU Affero General Public License as 10 | published by the Free Software Foundation, either version 3 of the 11 | License, or (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU Affero General Public License for more details. 17 | 18 | You should have received a copy of the GNU Affero General Public License 19 | along with this program. If not, see . 20 | 21 | """ 22 | 23 | import base64 24 | import sha 25 | import select 26 | import code 27 | 28 | class WebSocketException(Exception): 29 | pass 30 | 31 | def handshake(myself): 32 | my_client_id=myself.path[4:] 33 | my_headers=myself.headers.items() 34 | my_header_keys=map(lambda x:x[0],my_headers) 35 | h_key_exists=lambda x:my_header_keys.count(x) 36 | h_value=lambda x:my_headers[my_header_keys.index(x)][1] 37 | #print "The Lambdas(tm)" 38 | #print h_key_exists("upgrade") 39 | #print h_value("upgrade") 40 | #print h_key_exists("sec-websocket-key") 41 | if (not h_key_exists("upgrade")) or not (h_value("upgrade")=="websocket") or (not h_key_exists("sec-websocket-key")): 42 | raise WebSocketException 43 | ws_key=h_value("sec-websocket-key") 44 | ws_key_toreturn=base64.b64encode(sha.new(ws_key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest()) 45 | #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')] 46 | myself.wfile.write("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") 47 | 48 | def get_header(size): 49 | #this does something similar: https://github.com/lemmingzshadow/php-websocket/blob/master/server/lib/WebSocket/Connection.php 50 | ws_first_byte=0b10000010 # FIN=1, OP=2 51 | if(size>125): 52 | ws_second_byte=126 # The following two bytes will indicate frame size 53 | extended_size=chr((size>>8)&0xff)+chr(size&0xff) #Okay, it uses reverse byte order (little-endian) compared to anything else sent on TCP 54 | else: 55 | ws_second_byte=size 56 | #256 bytes binary message in a single unmasked frame | 0x82 0x7E 0x0100 [256 bytes of binary data] 57 | extended_size="" 58 | return chr(ws_first_byte)+chr(ws_second_byte)+extended_size 59 | 60 | def code_payload(data, masking_key=""): 61 | # both encode or decode 62 | if masking_key=="": 63 | key = (61, 84, 35, 6) 64 | else: 65 | key = [ord(i) for i in masking_key] 66 | encoded="" 67 | for i in range(0,len(data)): 68 | encoded+=chr(ord(data[i])^key[i%4]) 69 | return encoded 70 | 71 | def xxdg(data): 72 | output="" 73 | for i in range(0,len(data)/8): 74 | output+=xxd(data[i:i+8]) 75 | if i%2: output+="\n" 76 | else: output+=" " 77 | return output 78 | 79 | 80 | def xxd(data): 81 | #diagnostic purposes only 82 | output="" 83 | for d in data: 84 | output+=hex(ord(d))[2:].zfill(2)+" " 85 | return output 86 | 87 | #for R/W the WebSocket, use recv/send 88 | #for reading the TCP socket, use readsock 89 | #for writing the TCP socket, use myself.wfile.write and flush 90 | 91 | def readsock(myself,size,blocking): 92 | #http://thenestofheliopolis.blogspot.hu/2011/01/how-to-implement-non-blocking-two-way.html 93 | if blocking: 94 | return myself.rfile.read(size) 95 | else: 96 | poll = select.poll() 97 | poll.register(myself.rfile.fileno(), select.POLLIN or select.POLLPRI) 98 | fd = poll.poll(0) #timeout is 0 99 | if len(fd): 100 | f = fd[0] 101 | if f[1] > 0: 102 | return myself.rfile.read(size) 103 | return "" 104 | 105 | 106 | def recv(myself, blocking=False, debug=False): 107 | bufsize=70000 108 | #myself.connection.setblocking(blocking) #umm... we cannot do that with rfile 109 | if debug: print "ws_recv begin" 110 | try: 111 | data=readsock(myself,6,blocking) 112 | #print "rxws.recv bytes:",xxd(data) 113 | except: 114 | if debug: print "ws_recv error" 115 | return "" 116 | if debug: print "ws_recv recved" 117 | if(len(data)==0): return "" 118 | fin=ord(data[0])&128!=0 119 | is_text_frame=ord(data[0])&15==1 120 | length=ord(data[1])&0x7f 121 | data+=readsock(myself,length,blocking) 122 | #print "rxws.recv length is ",length," (multiple packets together?) len(data) =",len(data) 123 | has_one_byte_length=length<125 124 | masked=ord(data[1])&0x80!=0 125 | #print "len=", length, len(data)-2 126 | #print "fin, is_text_frame, has_one_byte_length, masked = ", (fin, is_text_frame, has_one_byte_length, masked) 127 | #print xxd(data) 128 | if fin and is_text_frame and has_one_byte_length: 129 | if masked: 130 | return code_payload(data[6:], data[2:6]) 131 | else: 132 | return data[2:] 133 | 134 | #Useful links for ideas on WebSockets: 135 | # http://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side 136 | # https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server 137 | # http://tools.ietf.org/html/rfc6455#section-5.2 138 | 139 | 140 | def flush(myself): 141 | myself.wfile.flush() 142 | #or the socket, not the rfile: 143 | #lR,lW,lX = select.select([],[myself.connection,],[],60) 144 | 145 | 146 | def send(myself, data, begin_id="", debug=0): 147 | base_frame_size=35000 #could guess by MTU? 148 | debug=0 149 | #try: 150 | while True: 151 | counter=0 152 | from_end=len(data)-counter 153 | if from_end+len(begin_id)>base_frame_size: 154 | data_to_send=begin_id+data[counter:counter+base_frame_size-len(begin_id)] 155 | header=get_header(len(data_to_send)) 156 | flush(myself) 157 | myself.wfile.write(header+data_to_send) 158 | flush(myself) 159 | 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)) 160 | else: 161 | data_to_send=begin_id+data[counter:] 162 | header=get_header(len(data_to_send)) 163 | flush(myself) 164 | myself.wfile.write(header+data_to_send) 165 | flush(myself) 166 | if debug: print "rxws.send :: #2 else branch :: dlen={0} hlen={1}".format(len(data_to_send),len(header)) 167 | #if debug: print "header:\n"+xxdg(header)+"\n\nws data:\n"+xxdg(data_to_send) 168 | break 169 | counter+=base_frame_size-len(begin_id) 170 | #except: 171 | # pass 172 | -------------------------------------------------------------------------------- /sdrhu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | """ 3 | 4 | This file is part of OpenWebRX, 5 | an open-source SDR receiver software with a web UI. 6 | Copyright (c) 2013-2015 by Andras Retzler 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU Affero General Public License as 10 | published by the Free Software Foundation, either version 3 of the 11 | License, or (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU Affero General Public License for more details. 17 | 18 | You should have received a copy of the GNU Affero General Public License 19 | along with this program. If not, see . 20 | 21 | """ 22 | 23 | import config_webrx as cfg, time, subprocess 24 | 25 | def run(continuously=True): 26 | if not cfg.sdrhu_key: return 27 | firsttime="(Your receiver is soon getting listed on sdr.hu!)" 28 | while True: 29 | cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://"+cfg.server_hostname+":"+str(cfg.web_port)+"&apikey="+cfg.sdrhu_key+"\" 2>&1" 30 | print "[openwebrx-sdrhu]", cmd 31 | returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() 32 | returned=returned[0] 33 | #print returned 34 | if "UPDATE:" in returned: 35 | retrytime_mins = 20 36 | value=returned.split("UPDATE:")[1].split("\n",1)[0] 37 | if value.startswith("SUCCESS"): 38 | print "[openwebrx-sdrhu] Update succeeded! "+firsttime 39 | firsttime="" 40 | else: 41 | print "[openwebrx-sdrhu] Update failed, your receiver cannot be listed on sdr.hu! Reason:", value 42 | else: 43 | retrytime_mins = 2 44 | print "[openwebrx-sdrhu] wget failed while updating, your receiver cannot be listed on sdr.hu!" 45 | if not continuously: break 46 | time.sleep(60*retrytime_mins) 47 | 48 | if __name__=="__main__": 49 | run(False) 50 | 51 | --------------------------------------------------------------------------------