├── .gitignore ├── LICENSE ├── README.md ├── config.model.py ├── fft_mask.py ├── interface.py ├── primitives.py ├── scanner.py ├── spectrum_viewer.py ├── spur_search.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cyberspectrum 2 | ============= 3 | 4 | Spectrum scanning/analysis tools 5 | -------------------------------------------------------------------------------- /config.model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # config.model.py 5 | # 6 | # Part of: https://github.com/balint256/cyberspectrum 7 | # 8 | # Copyright 2014 Balint Seeber 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (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 General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | # MA 02110-1301, USA. 24 | # 25 | # 26 | 27 | from primitives import * # See in here for more info 28 | 29 | _rate = 25e6 30 | _sample_count = 0.5 # float: seconds, int: # samples 31 | 32 | _b200_frequency_config = [FrequencyConfig(default_step=0.85, frequency_ranges=[FrequencyRange(5e9, 6e9)])] 33 | 34 | _test_channel_config_b210 = ChannelConfig(default_gains=[25],relative_gain=False,default_antennas='TX/RX',frequencies=_b200_frequency_config,lo_offset=0) 35 | _test_channel_config_b210_lo_offset = ChannelConfig(default_gains=[25],relative_gain=False,default_antennas='TX/RX',frequencies=_b200_frequency_config,lo_offset=15e6) 36 | _b2x0_channel_config = [_test_channel_config_b210] # Single channel 37 | _b210_channel_config = [_test_channel_config_b210, _test_channel_config_b210] # Dual channel (remember to set 'master_clock_rate' lower than 30.72e6 wherever this is used!) 38 | 39 | _x300_frequency_config = [FrequencyConfig(default_step=0.85)] 40 | 41 | # These are created separately as they'll be filled with different ranges automatically for a dual-channel motherboard populated with a WBX & SBX 42 | _x300_channel_config_wbx = ChannelConfig(default_gains=[15],relative_gain=False,default_antennas='TX/RX',frequencies=_x300_frequency_config,lo_offset=15e6) 43 | _x300_channel_config_sbx = ChannelConfig(default_gains=[15],relative_gain=False,default_antennas='TX/RX',frequencies=_x300_frequency_config,lo_offset=15e6) 44 | 45 | _x300_tune_policy = TunePolicy(400e-6, 5) 46 | 47 | # 'linked' indicates the radio has a shared LO for multiple channels 48 | 49 | _config = [ 50 | Config("x300", _sample_count, "type=x300", _rate/2, channel_config=[_x300_channel_config_wbx,_x300_channel_config_sbx], tune_policy=_x300_tune_policy, skip_samples=256), 51 | 52 | Config("b200", _sample_count, "type=b200,master_clock_rate=25e6,num_recv_frames=512", _rate/2, channel_config=_b2x0_channel_config, linked=True, skip_samples=256), 53 | Config("b200-1", _sample_count, "type=b200,num_recv_frames=512", 16e6, channel_config=_b2x0_channel_config, linked=True, skip_samples=256), 54 | Config("b210", _sample_count, "type=b200,num_recv_frames=512,master_clock_rate=16e6", 16e6, channel_config=_b2x0_channel_config*2, linked=True, skip_samples=256), # Cheating with "*2" to create two channels 55 | 56 | Config("b200-spur", 2500000, "type=b200,master_clock_rate=25e6,num_recv_frames=512", 25e6, 57 | channel_config=[ChannelConfig(default_gains=[25],relative_gain=False,default_antennas='TX/RX', 58 | frequencies=[FrequencyConfig(default_step=0.85, frequency_ranges=[FrequencyRange(50e6, 6e9)])] # In-line frequencies 59 | )], 60 | linked=True, skip_samples=256), 61 | 62 | Config("e300", _sample_count, "", 4e6, channel_config=_b2x0_channel_config, linked=True, skip_samples=256), 63 | ] 64 | 65 | def main(): 66 | for c in _config: 67 | print c.name 68 | 69 | if __name__ == '__main__': 70 | main() 71 | -------------------------------------------------------------------------------- /fft_mask.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # fft_mask.py 5 | # 6 | # Part of: https://github.com/balint256/cyberspectrum 7 | # 8 | # Copyright 2014 Balint Seeber 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (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 General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | # MA 02110-1301, USA. 24 | # 25 | # 26 | 27 | import interface 28 | import numpy 29 | 30 | from utils import * 31 | 32 | import tcp_server 33 | 34 | LISTEN_RETRY_INTERVAL = 5 35 | 36 | class Mask(): 37 | def __init__(self, name, options, spacing): 38 | self.name = name 39 | # options.mask_snr 40 | self.options = options 41 | self.spacing = (spacing + (spacing % 2)) / 2 # Compute either side 42 | self.points = None 43 | 44 | def build(self, fft_points): 45 | sorted_points = numpy.sort(fft_points) 46 | sorted_idices = numpy.argsort(fft_points) 47 | flags = [0] * len(fft_points) 48 | self.points = numpy.zeros(len(fft_points)) 49 | for n in range(len(fft_points)): 50 | v = sorted_points[(len(sorted_points)-1) - n] 51 | i = sorted_idices[(len(sorted_points)-1) - n] 52 | if flags[i] != 0: 53 | continue 54 | self.points[i] = v 55 | if self.options.fixed_threshold is None: 56 | self.points[i] += self.options.mask_snr 57 | flags[i] = 1 58 | for j in range(self.spacing): 59 | lhs = i - j 60 | rhs = i + j 61 | if lhs >= 0: 62 | if flags[lhs] == 0: 63 | self.points[lhs] = self.points[i] 64 | flags[lhs] = 1 65 | if rhs < len(fft_points): 66 | if flags[rhs] == 0: 67 | self.points[rhs] = self.points[i] 68 | flags[rhs] = 1 69 | 70 | def check(self, fft_points, stitching=0): 71 | result = fft_points - self.points 72 | 73 | hits = [] 74 | 75 | thru = False 76 | first = None 77 | for i in range(len(result)): 78 | if result[i] > 0.0: 79 | if not thru: 80 | if len(hits) > 0 and ((i - hits[-1][1]) <= stitching): 81 | #print "Stitching %d to %d" % (hits[-1][1], i) 82 | first = hits[-1][0] 83 | del hits[-1] 84 | else: 85 | first = i 86 | thru = True 87 | elif thru: 88 | if result[i] <= 0.0: 89 | hits += [(first, i)] 90 | thru = False 91 | 92 | if thru: 93 | hits += [(first, len(result)-1)] 94 | 95 | return hits 96 | 97 | class EnergyDetector(interface.Module): 98 | def __init__(self, config, options, *args, **kwds): 99 | interface.Module.__init__(self, config, options, *args, **kwds) 100 | 101 | self.masks = [] 102 | self.server = None 103 | 104 | #print "Initialised Energy Detector" 105 | 106 | def populate_options(self, parser): 107 | parser.add_option("--mask-snr", type="float", default=15.0, help="Mask threshold above noise floor (dB) [default: %default]") 108 | parser.add_option("--mask-space", type="float", default=150e3, help="Mask spacing (Hz) [default=%default]") 109 | parser.add_option("--mask-stitch", type="float", default=200e3, help="Mask spacing (Hz) [default=%default]") 110 | parser.add_option("--listen", type="int", default=10000, help="TCP server listen port [default=%default]") 111 | parser.add_option("--min-width", type="float", default=50e3, help="Minimum detection width (Hz) [default=%default]") 112 | parser.add_option("--fixed-threshold", type="float", default=None, help="Fixed detection threshold (dB) [default=%default]") 113 | parser.add_option("--only-save-hits", action="store_true", default=False, help="Only save data from acquisitions that trigger the detector [default=%default]") 114 | 115 | def init(self, usrp, info, states, state_machines, fft_graph, scope_graph): 116 | interface.Module.init(self, usrp, info, states, state_machines, fft_graph, scope_graph) 117 | 118 | if self.options.fixed_threshold is not None: 119 | print "Fixed mask threshold:", self.options.fixed_threshold 120 | 121 | for i in range(len(state_machines)): 122 | self.masks += [{}] 123 | 124 | print "Prepared %d mask lists" % (len(self.masks)) 125 | 126 | self.server = tcp_server.ThreadedTCPServer(("", self.options.listen), silent=False) # buffer_size=options.buffer_size, blocking_mode=options.blocking_send, send_limit=options.limit, 127 | 128 | def _log_listen_retry(e, msg): 129 | print " Socket error:", msg 130 | if (e == 98): 131 | print " Waiting, then trying again..." 132 | 133 | self.server.start(retry=True, wait=LISTEN_RETRY_INTERVAL, log=_log_listen_retry) 134 | print "==> TCP server running in thread:", self.server.server_thread.getName() 135 | 136 | def shutdown(self): 137 | def _log_shutdown(client): 138 | print "==> Disconnecting client:", client.client_address 139 | 140 | self.server.shutdown(True, log=_log_shutdown) 141 | 142 | def start(self, count, current_hw_states): 143 | interface.Module.start(self, count, current_hw_states) 144 | 145 | self.triggered = [] 146 | 147 | def query_fft(self, sample_idx, hw_state): 148 | return True 149 | 150 | def process(self, sample_idx, hw_state, s, fft_data, partial_name, fft_channel_graph, scope_channel_graph): 151 | self.server.send("[%04d] Processing chan #%d: %s\n" % (self.last_count, sample_idx, str(hw_state))) 152 | 153 | masks = self.masks[sample_idx] 154 | key = str(hw_state) 155 | first = False 156 | if key not in masks.keys(): 157 | if self.options.fixed_threshold is not None: 158 | fft_points = [self.options.fixed_threshold]*len(fft_data['ave']) 159 | else: 160 | first = True 161 | # Use 'ave' and ignore spurs with options.min_width 162 | fft_points = fft_data['ave'] # 'ave', 'max' 163 | 164 | if False: 165 | window_length = 3 # MAGIC 166 | weights = numpy.repeat(1.0, window_length)/window_length 167 | ma_fft_points = numpy.convolve(fft_points, weights, 'valid') 168 | diff = len(fft_points) - len(ma_fft_points) 169 | ma_fft_points = numpy.append(numpy.array([float(ma_fft_points[0])]*(diff/2)), [ma_fft_points]) 170 | ma_fft_points = numpy.append(ma_fft_points, [numpy.array([float(ma_fft_points[len(ma_fft_points)-1])]*(len(fft_points) - len(ma_fft_points)))]) 171 | fft_points = ma_fft_points 172 | 173 | spacing = int((self.options.mask_space / self.config.rate) * len(fft_points)) 174 | mask = Mask(key, self.options, spacing) 175 | mask.build(fft_points) 176 | masks[key] = mask 177 | 178 | built_msg = "Built mask for channel %d with key: %s" % (sample_idx, key) 179 | print built_msg 180 | self.server.send("[%04d] %s\n" % (self.last_count, built_msg)) 181 | 182 | if not first: 183 | fft_points = fft_data['max'] 184 | mask = masks[key] 185 | 186 | if fft_channel_graph: 187 | mask_points = [] 188 | for i in range(len(fft_points)): 189 | f = hw_state.freq + ((self.config.rate / len(fft_points)) * (i - len(fft_points)/2 + 1)) 190 | mask_points += [(f, mask.points[i])] 191 | 192 | fft_channel_graph.add_points(mask_points, 'bo') 193 | 194 | if first: return 195 | 196 | ######################################################################## 197 | # Process as normal 198 | 199 | stitching = int((self.options.mask_stitch / self.config.rate) * len(fft_points)) 200 | 201 | hits = mask.check(fft_points, stitching) 202 | 203 | #print "Hits:", hits 204 | 205 | self.triggered += [(len(hits) > 0)] 206 | 207 | for hit in hits: 208 | f1 = hw_state.freq - self.config.rate/2 209 | f2 = self.config.rate / len(fft_points) 210 | hit_freq = [ 211 | (f1 + f2*hit[0]), 212 | (f1 + f2*hit[1]) 213 | ] 214 | freq_range = hit_freq[1] - hit_freq[0] 215 | 216 | if freq_range < self.options.min_width: 217 | continue 218 | 219 | mid_point = hit_freq[0] + (freq_range / 2) 220 | points = fft_points[hit[0]:hit[1]] 221 | ave_power = numpy.average(points) 222 | 223 | hit_str = "Hit %04d-%04d: %s (%s wide) @ %f dBFs" % (hit[0], hit[1], format_freq(mid_point), format_freq(freq_range), ave_power) 224 | print hit_str 225 | 226 | def _log_send_error(client, e, msg): 227 | if e != 32: # Broken pipe 228 | print "==> While sending to", client.client_address, "-", e, msg 229 | 230 | self.server.send(hit_str + "\n", log=_log_send_error) 231 | 232 | if fft_channel_graph: 233 | hit_points = [] 234 | for i in range(hit[0], hit[1]): 235 | f = hw_state.freq + ((self.config.rate / len(fft_points)) * (i - len(fft_points)/2 + 1)) 236 | hit_points += [(f, fft_points[i])] 237 | fft_channel_graph.add_points(hit_points, 'ro') 238 | 239 | def query_save(self, which): 240 | if self.options.only_save_hits: 241 | if which == 'data': 242 | return self.triggered[-1] 243 | elif which == 'fft_graph': 244 | return reduce(lambda x,y: x or y, self.triggered) 245 | return None 246 | 247 | def get_modules(): 248 | return [{'class':EnergyDetector, 'name':"Energy Detector"}] 249 | 250 | def main(): 251 | return 0 252 | 253 | if __name__ == '__main__': 254 | main() 255 | -------------------------------------------------------------------------------- /interface.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # interface.py 5 | # 6 | # Part of: https://github.com/balint256/cyberspectrum 7 | # 8 | # Copyright 2014 Balint Seeber 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (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 General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | # MA 02110-1301, USA. 24 | # 25 | # 26 | 27 | class Module(): 28 | def __init__(self, config, options, *args, **kwds): 29 | self.config = config 30 | self.options = options 31 | def populate_options(self, parser): 32 | pass 33 | def init(self, usrp, info, states, state_machines, fft_graph, scope_graph): 34 | self.usrp = usrp 35 | self.info = info 36 | self.states = states 37 | self.state_machines = state_machines 38 | self.fft_graph = fft_graph 39 | self.scope_graph = scope_graph 40 | def start(self, count, current_hw_states): 41 | self.last_count = count 42 | self.last_hw_states = current_hw_states 43 | def query_stop(self, channel_idx, state_machine, hw_state): 44 | return False 45 | def query_fft(self, sample_idx, hw_state): 46 | return False 47 | def process(self, sample_idx, hw_state, s, fft_data, partial_name, fft_channel_graph, scope_channel_graph): 48 | return 49 | def stop(self, successful): 50 | return 51 | def query_save(self, which): # data, fft_graph 52 | return None 53 | def shutdown(self): 54 | return 55 | 56 | def main(): 57 | return 0 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /primitives.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # primitives.py 5 | # 6 | # Part of: https://github.com/balint256/cyberspectrum 7 | # 8 | # Copyright 2014 Balint Seeber 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (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 General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | # MA 02110-1301, USA. 24 | # 25 | # 26 | 27 | import math 28 | 29 | from utils import * 30 | 31 | class StateMachine(): 32 | def __init__(self, states, idx=-1): 33 | self.states = states 34 | self.idx = idx 35 | self.loops = 0 36 | def next(self): 37 | self.idx += 1 38 | if self.idx == len(self.states): 39 | self.idx = 0 40 | self.loops += 1 41 | return self.current() 42 | def count(self): 43 | return len(self.states) 44 | def index(self): 45 | return self.idx 46 | def current(self): 47 | try: 48 | return self.states[self.idx] 49 | except Exception, e: 50 | print "Tried to access state #%d but there are only %d" % (self.idx+1, len(self.states)) 51 | raise 52 | def loop_count(self): 53 | return self.loops 54 | 55 | class Statistics(): 56 | def __init__(self): 57 | self.reset() 58 | def reset(self): 59 | self._sum = 0.0 60 | self._min = None 61 | self._max = None 62 | self._count = 0 63 | def add(self, v): 64 | self._sum += float(v) 65 | if self._min is None: 66 | self._min = v 67 | else: 68 | self._min = min(self._min, v) 69 | if self._max is None: 70 | self._max = v 71 | else: 72 | self._max = max(self._max, v) 73 | self._count += 1 74 | def ave(self): 75 | return self._sum / self._count 76 | def count(self): 77 | return self._count 78 | def min(self): 79 | return self._min 80 | def max(self): 81 | return self._max 82 | 83 | class ChannelCapabilites(): 84 | def __init__(self, freq_range, antennas, gain_range): 85 | self.freq_range = freq_range 86 | self.antennas = antennas 87 | self.gain_range = gain_range 88 | def __str__(self): 89 | return "Freq: %s, Gains: %s, Antennas: %s" % ( 90 | self.freq_range, 91 | self.gain_range, 92 | self.antennas 93 | ) 94 | 95 | class HardwareState(): 96 | def __init__(self, state, gain, antenna, lo_offset, bandwidth, freq=None): 97 | self.state = state 98 | self.gain = gain 99 | self.antenna = antenna 100 | self.lo_offset = lo_offset 101 | self.bandwidth = bandwidth 102 | self.freq = freq 103 | def get_antenna(self): 104 | if len(self.antenna) == 0: 105 | return "(default)" 106 | return self.antenna 107 | def __str__(self): 108 | res = "Gain: %.1f, Antenna: %s, LO offset: %s, Bandwidth: %s" % (self.gain, self.get_antenna(), format_freq(self.lo_offset), format_freq(self.bandwidth)) 109 | if self.freq is not None: 110 | res += ", Freq: %s" % (format_freq(self.freq)) 111 | return res 112 | 113 | class ScannerState(): 114 | def __init__(self, freq_range, freq_config, channel, config, chan_caps): 115 | self.freq_range, self.freq_config, self.channel, self.config, self.chan_caps = freq_range, freq_config, channel, config, chan_caps 116 | # Other parameters filled in by 'setup' in FrequencyRange 117 | # FrequencyRange 118 | self.start, self.stop, self.step, self.edge, self.bandwidth = None, None, None, None, None 119 | # FrequencyConfig 120 | self.gains = None 121 | self.antennas = None 122 | self.lo_offset = None 123 | def __str__(self): 124 | return "Freq: %s-%s (%s steps, edge: %s), Gains: %s, Antennas: %s, LO offset: %s, Bandwidth: %s" % ( 125 | format_freq(self.start), format_freq(self.stop), format_freq(self.step), self.edge, 126 | self.gains, 127 | self.antennas, 128 | format_freq(self.lo_offset), 129 | format_freq(self.bandwidth) 130 | ) 131 | def get_hw_states(self, calc_freqs): 132 | states = [] 133 | 134 | if calc_freqs: 135 | if self.edge: 136 | start = self.start + (self.config.rate / 2.0) 137 | stop = self.stop - (self.config.rate / 2.0) 138 | 139 | if stop < start: # If rate > freq range, pick center 140 | start = stop = self.start + ((self.stop - self.start) / 2.0) 141 | else: 142 | start = self.start 143 | stop = self.stop 144 | 145 | if stop < start: 146 | raise Exception("Invalid frequency range") 147 | 148 | freq_range = stop - start 149 | steps = int(math.ceil(freq_range / self.step)) + 1 150 | #steps = max(1, int(math.ceil(freq_range / self.step))) # Always have one step (when start & stop are the same) 151 | #if (freq_range > 0.0) and ((freq_range / self.step) == (freq_range // self.step)): # If on boundary, include the last 152 | # steps += 1 153 | 154 | if (freq_range > 0.0) and (freq_range < (self.config.rate / 2.0)): 155 | steps -= 1 156 | 157 | for i in range(steps): 158 | freq = start + (self.step * i) 159 | if freq > stop: 160 | freq = stop # Should only happen once at the end if last is not equally spaced 161 | for gain in self.gains: 162 | for antenna in self.antennas: 163 | states += [HardwareState(self, gain, antenna, self.lo_offset, self.bandwidth, freq)] 164 | else: 165 | for gain in self.gains: 166 | for antenna in self.antennas: 167 | states += [HardwareState(self, gain, antenna, self.lo_offset, self.bandwidth)] 168 | 169 | return states 170 | 171 | def _choose(val, default): 172 | if val is None: 173 | return default 174 | return val 175 | 176 | # Sample rate will determine total available step 177 | class FrequencyRange(): 178 | def __init__(self, start=None, stop=None, step=None, edge=False, bandwidth=None): 179 | self.start = start # None: lowest supported 180 | self.stop = stop # None: highest supported 181 | self.step = step # Relative to sample rate (None: use default) 182 | self.edge = edge # Start the LO at the range edge 183 | self.bandwidth = bandwidth 184 | def setup(self, freq_config, channel, config, chan_caps): 185 | state = ScannerState(self, freq_config, channel, config, chan_caps) 186 | # FrequencyRange 187 | state.start= _choose(self.start, chan_caps.freq_range.start()) - freq_config.padding 188 | state.stop = _choose(self.stop, chan_caps.freq_range.stop()) + freq_config.padding 189 | state.step = _choose(self.step, freq_config.default_step) * config.rate 190 | state.edge = self.edge 191 | state.bandwidth = _choose(self.bandwidth, freq_config.default_bandwidth) 192 | # FrequencyConfig 193 | if freq_config.gains is None: 194 | state.gains = channel.default_gains 195 | relative_gain = channel.relative_gain 196 | else: 197 | state.gains = freq_config.gains 198 | relative_gain = freq_config.relative_gain 199 | if not isinstance(state.gains, list): 200 | state.gains = [state.gains] 201 | # FIXME: Check gains in range 202 | if relative_gain: 203 | state.gains = [chan_caps.gain_range.start() + (chan_caps.gain_range.stop() - chan_caps.gain_range.start()) * g for g in state.gains] 204 | state.antennas = _choose(freq_config.antennas, channel.default_antennas) 205 | if isinstance(state.antennas, str): 206 | state.antennas = [state.antennas] 207 | elif state.antennas is None or state.antennas == False: 208 | state.antennas = [''] 209 | elif state.antennas == True: 210 | state.antennas = chan_caps.antennas 211 | # FIXME: Check antennas against caps 212 | state.lo_offset = channel.lo_offset 213 | return state 214 | 215 | _default_frequency_ranges = [FrequencyRange()] 216 | 217 | # Step is fraction of sample rate (bandwidth) 218 | class FrequencyConfig(): 219 | def __init__(self, frequency_ranges=_default_frequency_ranges, default_step=1.0, gains=None, relative_gain=None, antennas=None, default_bandwidth=None, padding=0.0): 220 | self.frequency_ranges = frequency_ranges 221 | self.default_step = default_step 222 | self.gains = gains # None: use default 223 | self.relative_gain = relative_gain # None: use default 224 | self.antennas = antennas # None: use default 225 | self.default_bandwidth = default_bandwidth 226 | self.padding = padding 227 | def setup(self, channel, config, chan_caps): 228 | freqs = [] 229 | for fr in self.frequency_ranges: 230 | freqs += [fr.setup(self, channel, config, chan_caps)] 231 | return freqs 232 | 233 | _default_frequency_config = [FrequencyConfig()] 234 | 235 | # Maps to a side (daughterboard) 236 | # Set defaults: 237 | # Specify possible antennas to use (or all) 238 | # gain, frequency, 239 | class ChannelConfig(): 240 | def __init__(self, frequencies=_default_frequency_config, default_gains=[0.25], relative_gain=True, default_antennas=False, subdev="", lo_offset=0.0): 241 | self.frequencies = frequencies 242 | self.default_gains = default_gains 243 | self.relative_gain = relative_gain 244 | self.default_antennas = default_antennas # False: default, True: all, or strings 245 | self.subdev = subdev 246 | self.lo_offset = lo_offset 247 | def setup(self, config, chan_caps): 248 | freqs = [] 249 | for f in self.frequencies: 250 | freqs += f.setup(self, config, chan_caps) 251 | return freqs 252 | 253 | class TunePolicy(): 254 | def __init__(self, settling_time=0.0, consecutive_locks=1, wait_time=0.001, timeout=1, sensor_name="lo_locked"): 255 | self.settling_time = settling_time 256 | self.consecutive_locks = consecutive_locks 257 | self.wait_time = wait_time 258 | self.timeout = timeout 259 | self.sensor_name = sensor_name 260 | 261 | _noise_floor = -130.0 # dB (lowest reasonable) 262 | _default_sample_count = 1.0 # float: seconds, int: samples 263 | _default_channel_config = [ChannelConfig()] 264 | 265 | # Maps to a radio 266 | # Specify sample rate 267 | class Config(): 268 | def __init__(self, name, length=_default_sample_count, args="", rate=1e6, channel_config=_default_channel_config, noise_floor=_noise_floor, linked=False, tune_policy=TunePolicy(), skip_samples=0, master_clock_rate=None): 269 | self.name = name 270 | self.length = length 271 | self.args = args 272 | self.rate = rate 273 | self.channel_config = channel_config 274 | self.linked = linked # When channels are linked (single frequency, e.g. B210) 275 | self.noise_floor = noise_floor 276 | self.tune_policy = tune_policy 277 | self.skip_samples = skip_samples 278 | self.master_clock_rate = master_clock_rate 279 | 280 | if isinstance(self.length, float): 281 | self.sample_count = int(self.rate * self.length) 282 | else: 283 | self.sample_count = self.length 284 | 285 | _max_sample_count = (1 << 31) - 1 286 | if (self.sample_count + self.skip_samples) > _max_sample_count: 287 | print "Clamping sample count from %d to %d (skipping %d)" % (self.sample_count, (_max_sample_count - skip_samples), skip_samples) 288 | self.sample_count = _max_sample_count - self.skip_samples 289 | def setup(self, chan_caps): 290 | if len(chan_caps) != len(self.channel_config): 291 | raise Exception("Number of channels in capabilities must match current hardware configuration") 292 | # FIXME: Check self.rate in supported rates 293 | channels = [] 294 | idx = 0 295 | for cc in self.channel_config: 296 | channels += [cc.setup(self, chan_caps[idx])] 297 | idx += 1 298 | return channels 299 | 300 | def main(): 301 | return 0 302 | 303 | if __name__ == '__main__': 304 | main() 305 | -------------------------------------------------------------------------------- /scanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # scanner.py 5 | # 6 | # Part of: https://github.com/balint256/cyberspectrum 7 | # 8 | # Copyright 2014 Balint Seeber 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (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 General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | # MA 02110-1301, USA. 24 | # 25 | # 26 | 27 | # TODO: 28 | # - GPS logging module 29 | # - exception handling around GUI close (and when it doesn't raise, figure out why it remains blank) 30 | 31 | import sys, socket, traceback, time, datetime, os, signal 32 | from optparse import OptionParser 33 | 34 | import numpy 35 | 36 | try: 37 | from realtime_graph import * 38 | except Exception, e: 39 | print "Failed to import realtime_graph:", e 40 | print "Clone https://github.com/balint256/baz-utils and add lib/python to your PYTHONPATH" 41 | realtime_graph = None 42 | matplotlib = None 43 | 44 | try: 45 | from fft_tools import * 46 | except Exception, e: 47 | print "Failed to import fft_tools:", e 48 | print "Clone https://github.com/balint256/baz-utils and add lib/python to your PYTHONPATH" 49 | raise e 50 | 51 | try: 52 | import wx # To detect when MPL window is closed 53 | except Exception, e: 54 | print "Failed to import wx:", e 55 | wx = Exception() 56 | wx._core = Exception() 57 | wx._core.PyDeadObjectError = Exception() 58 | 59 | from gnuradio import gr, uhd 60 | try: 61 | from baz import usrp_acquire 62 | except Exception, e: 63 | print "baz.usrp_acquire will not be available (failed to import):", e 64 | usrp_acquire = None 65 | 66 | from utils import * 67 | from primitives import * 68 | 69 | _configs = None 70 | 71 | try: 72 | import config as _config 73 | _configs = _config._config 74 | except Exception, e: 75 | print "Could not import configuration:", e 76 | print "Make sure you have a config.py (e.g. make a copy of config.model.py)" 77 | #traceback.print_exc() 78 | raise e 79 | 80 | running = True 81 | 82 | def signal_term_handler(signal, frame): 83 | global running 84 | print 85 | print "Received signal" 86 | running = False 87 | 88 | # RuntimeError: EnvironmentError: IOError: Radio ctrl (A) packet parse error - AssertionError: packet_info.packet_count == (seq_to_ack & 0xfff) 89 | 90 | ######################################################################## 91 | # Magic to turn pointers into numpy arrays 92 | # http://docs.scipy.org/doc/numpy/reference/arrays.interface.html 93 | ######################################################################## 94 | def pointer_to_ndarray(addr, dtype, nitems, read_only=False): 95 | class array_like: 96 | __array_interface__ = { 97 | 'data' : (int(addr), read_only), 98 | 'typestr' : dtype.base.str, 99 | 'descr' : dtype.base.descr, 100 | 'shape' : (nitems,) + dtype.shape, 101 | 'strides' : None, 102 | 'version' : 3 103 | } 104 | return numpy.asarray(array_like()).view(dtype.base) 105 | 106 | class RestartException(Exception): 107 | pass 108 | 109 | def main(): 110 | parser = OptionParser(usage="%prog: [options] [config name] [-- module-specific options]") 111 | 112 | parser.add_option("-l", "--length", type="string", default=None, help="Override capture length [default=%default]") 113 | parser.add_option("-a", "--args", type="string", default=None, help="Override UHD device args [default=%default]") 114 | #parser.add_option("-f", "--fifo", type="string", default="", help="GPS FIFO path [default=%default]") 115 | #parser.add_option("-p", "--port", type="int", default=12345, help="GPS server port [default=%default]") 116 | parser.add_option("-G", "--graph", action="store_true", default=False, help="Graph samples [default=%default]") 117 | parser.add_option("-L", "--location", type="string", default=None, help="Capture location [default=%default]") 118 | parser.add_option("-s", "--skip", type="int", default=0, help="Iterations to skip [default=%default]") 119 | parser.add_option("--no-gps", action="store_true", default=False, help="Don't query GPS [default=%default]") 120 | parser.add_option("--wait", action="store_true", default=False, help="Wait after each iteration [default: %default]") 121 | parser.add_option("", "--fft-length", type="int", default=2048, help="FFT length [default=%default]") 122 | parser.add_option("", "--scope-length", type="int", default=2048, help="Scope length [default=%default]") 123 | parser.add_option("-m", "--modules", type="string", default="", help="Load additional modules [default=%default]") 124 | parser.add_option("-v", "--verbose", action="store_true", default=False, help="Verbose output [default: %default]") 125 | parser.add_option("-P", "--pad-fft", action="store_true", default=False, help="Pad FFTs [default: %default]") 126 | parser.add_option("-S", "--fft-step", type="int", default=1, help="FFT step size [default=%default]") 127 | parser.add_option("-w", "--window", type="string", default="hamming", help="FFT window function [default=%default]") 128 | parser.add_option("", "--abort", action="store_true", default=False, help="Abort on error that is otherwise retried [default: %default]") 129 | parser.add_option("", "--restart", action="store_true", default=False, help="Restart on error that is otherwise retried [default: %default]") 130 | parser.add_option("--once", action="store_true", default=False, help="Exit instead of looping [default=%default]") 131 | parser.add_option("", "--retry-sleep", type="float", default=0.5, help="Sleep time before retrying acquisition (s) [default=%default]") 132 | parser.add_option("-o", "--otw", type="string", default=None, help="Over The Wire format [default=%default]") 133 | parser.add_option("-t", "--type", type="string", default="fc32", help="CPU data/file format [default=%default]") 134 | parser.add_option("-W", "--width", type="float", default=8, help="Graph width [default=%default]") 135 | parser.add_option("-H", "--height", type="float", default=10, help="Graph height [default=%default]") 136 | parser.add_option("", "--flush", type="int", default=16, help="Samples to receive for flush during retry [default=%default]") 137 | parser.add_option("", "--delay", type="float", default=0.01, help="Stream command time delay (s) [default=%default]") 138 | parser.add_option("", "--force-delay", action="store_true", default=False, help="Delay on single channel capture [default: %default]") 139 | 140 | (options, args) = parser.parse_args() 141 | 142 | numpy_dtypes = {'fc32': (numpy.complex64, False), 'sc16': (numpy.int16, True), 'sc8': (numpy.int8, True)} 143 | 144 | if usrp_acquire is None: 145 | if options.type != "fc32": 146 | print "Type '%s' not supported when using generic GNU Radio interface (install gr-baz)" % (options.type) 147 | return 148 | else: 149 | if options.type not in numpy_dtypes.keys(): 150 | print "Type '%s' not supported by 'usrp_acquire' interface" % (options.type) 151 | return 152 | 153 | config = None 154 | if len(args) >= 1 and args[0] != "-": 155 | for c in _configs: 156 | if c.name.lower() == args[0].lower(): 157 | config = c 158 | if config == None and len(args[0]) > 0: 159 | print "Config '%s' not found" % (args[0]) 160 | return 161 | 162 | if config is None: 163 | config = _config.Config("(default)") 164 | 165 | print "Using config:", config.name 166 | 167 | if options.graph and realtime_graph is None: 168 | print "Cannot graph when realtime_graph unavailable" 169 | return 170 | 171 | if options.args is not None: 172 | print "Overriding config args \"%s\" with \"%s\"" % (config.args, options.args) 173 | config.args = options.args 174 | 175 | if options.length is not None and len(options.length) > 0: 176 | print "Overriding length '%s' with '%s'" % (str(config.length), options.length) 177 | config.length = float(options.length) 178 | if not options.length.find('.'): 179 | config.length = int(config.length) 180 | 181 | window_fn = None 182 | if len(options.window) > 0 and options.window != "-": 183 | try: 184 | window_fn = getattr(numpy, options.window) 185 | print "Using window function:", options.window 186 | except: 187 | print "Window function not found:", options.window 188 | raise 189 | else: 190 | print "Not using a window function" 191 | 192 | module_list = [] 193 | 194 | if len(options.modules) > 0: 195 | module_names = options.modules.split(',') 196 | for module_name in module_names: 197 | exec("import " + module_name) 198 | print "Loaded:", module_name 199 | module = sys.modules[module_name] 200 | module_list += module.get_modules() 201 | 202 | if gr.enable_realtime_scheduling() != gr.RT_OK: 203 | print "Failed to enable realtime scheduling - do you have sufficient permissions?" 204 | 205 | nmea_sensors = ["gps_gpgga", "gps_gprmc"] 206 | 207 | channel_count = len(config.channel_config) 208 | print "Sampling from %d channels" % (channel_count) 209 | channels = range(channel_count) 210 | 211 | signal.signal(signal.SIGTERM, signal_term_handler) 212 | signal.signal(signal.SIGINT, signal_term_handler) 213 | print "Installed signal handlers" 214 | 215 | fft_graph = None 216 | fft_channel_graphs = {} 217 | scope_graph = None 218 | scope_channel_graphs = {} 219 | 220 | font = { 221 | #'family' : 'normal', 222 | #'weight' : 'bold', 223 | 'size' : 10 224 | } 225 | 226 | if options.graph and matplotlib is not None: 227 | matplotlib.rc('font', **font) 228 | 229 | global running 230 | while running: 231 | modules = [] 232 | module_options = OptionParser() 233 | 234 | for m in module_list: 235 | module_instance = m['class'](config, options) 236 | module_instance.populate_options(module_options) 237 | modules += [module_instance] 238 | print "Added:", m['class'] 239 | 240 | (module_opts, extra_args) = module_options.parse_args(args=[]) # Init defaults 241 | 242 | if len(args) > 1: # First will be config name 243 | (module_opts, extra_args) = module_options.parse_args(args=args[1:], values=module_opts) 244 | 245 | for opt in module_options.option_list: 246 | if opt.dest is None: 247 | continue 248 | o = getattr(module_opts, opt.dest) 249 | #print opt.dest, "=", o 250 | setattr(options, opt.dest, o) 251 | 252 | stream_kwds = {} 253 | if options.otw is not None and len(options.otw) > 0: 254 | stream_kwds['otw_format'] = options.otw 255 | 256 | stream_args = uhd.stream_args( 257 | cpu_format=options.type, 258 | channels=channels, 259 | **stream_kwds 260 | ) 261 | 262 | # FIXME: This can throw on X310 263 | try: 264 | usrp = uhd.usrp_source( 265 | device_addr=config.args, 266 | stream_args=stream_args, 267 | ) 268 | except RuntimeError, e: 269 | print "Likely UHD exception:", e 270 | print "Waiting..." 271 | time.sleep(5) 272 | print "Trying again..." 273 | continue 274 | except Exception, e: 275 | print "Unknown exception:", e 276 | break 277 | 278 | usrp_acquire_src = None 279 | if usrp_acquire: 280 | #usrp_acquire_src = usrp_acquire(usrp.get_device(), stream_args) 281 | usrp_acquire_src = usrp_acquire.make_from_source(usrp.to_basic_block(), stream_args) 282 | print "Using usrp_acquire" 283 | assert(stream_args.cpu_format in numpy_dtypes.keys()) 284 | numpy_dtype = numpy_dtypes[stream_args.cpu_format][0] 285 | make_complex = numpy_dtypes[stream_args.cpu_format][1] 286 | else: 287 | print "Using uhd" 288 | assert(stream_args.cpu_format == "fc32") 289 | numpy_dtype = numpy.complex64 290 | make_complex = False 291 | 292 | info = {} 293 | uhd_info = usrp.get_usrp_info() 294 | for k in uhd_info.keys(): 295 | #print k, "=", info.get(k) 296 | info[k] = uhd_info.get(k) 297 | print "Device info:", info 298 | 299 | mboard_sensor_names = usrp.get_mboard_sensor_names() 300 | _available_nmea_sensors = [] 301 | for sensor_name in mboard_sensor_names: 302 | if sensor_name in nmea_sensors: 303 | _available_nmea_sensors += [sensor_name] 304 | 305 | if len(_available_nmea_sensors) == 0: 306 | print "GPS not available" 307 | else: 308 | print "GPS sensors available:", _available_nmea_sensors 309 | 310 | if config.master_clock_rate is not None: 311 | usrp.set_clock_rate(config.master_clock_rate) 312 | 313 | usrp.set_samp_rate(config.rate) 314 | 315 | print "Master clock rate:", usrp.get_clock_rate() 316 | print "Sample rate:", usrp.get_samp_rate() 317 | #print "Sample rates:", len(usrp.get_samp_rates()) 318 | #print "Center freq:", usrp.get_center_freq() 319 | #print "Freq range:", usrp.get_freq_range() 320 | #print "Gain:", usrp.get_gain() 321 | #print "Gain names:", usrp.get_gain_names() 322 | #print "Gain range:", usrp.get_gain_range() 323 | #print "Antenna:", usrp.get_antenna() 324 | #print "Antennas:", usrp.get_antennas() 325 | 326 | chan_caps = [] 327 | chan_sensors = [] 328 | for chan_idx in channels: 329 | chan_caps += [_config.ChannelCapabilites(usrp.get_freq_range(chan_idx), usrp.get_antennas(chan_idx), usrp.get_gain_range(chan_idx))] 330 | chan_sensors += [usrp.get_sensor_names(chan_idx)] 331 | print "Capabilites:" 332 | print "\n".join(map(str, chan_caps)) 333 | print "Sensors:" 334 | print "\n".join(map(str, chan_sensors)) 335 | 336 | mboard = 0 337 | 338 | # FIXME: Args 339 | #usrp.set_clock_source(ref, mboard) 340 | #usrp.set_time_source(pps, mboard) 341 | 342 | print "Clock source:", usrp.get_clock_source(mboard) 343 | print "Time source: ", usrp.get_time_source(mboard) 344 | print "Clock rate: ", usrp.get_clock_rate() 345 | print "Time now: ", usrp.get_time_now().get_real_secs() 346 | print "Time PPS: ", usrp.get_time_last_pps().get_real_secs() 347 | 348 | subdev_spec = " ".join([cc.subdev for cc in config.channel_config]).strip() 349 | # FIXME: Validation 350 | 351 | if len(subdev_spec) > 0: 352 | usrp.set_subdev_spec(subdev_spec) 353 | 354 | print "Subdev spec:", usrp.get_subdev_spec() 355 | 356 | # HW channels must map directly to order in ChannelConfig 357 | states = config.setup(chan_caps) 358 | 359 | print "States:" 360 | for i in range(len(states)): 361 | print "%d:" % (i) 362 | print "\n".join(map(str, states[i])) 363 | 364 | # Init state machine 365 | state_machines = [] 366 | for state in states: 367 | hw_state = [] 368 | for s in state: 369 | hw_state += s.get_hw_states(True) 370 | state_machines += [StateMachine(hw_state)] 371 | 372 | ################################ 373 | 374 | gui_fft_length = options.fft_length 375 | gui_scope_length = options.scope_length 376 | 377 | # FIXME: CTRL+C handling doesn't work with this 378 | if options.graph: 379 | if fft_graph: 380 | fft_graph.close() 381 | if scope_graph: 382 | scope_graph.close() 383 | 384 | padding = 0.05 385 | spacing = 0.1 386 | figure_width = options.width 387 | figure_height = options.height 388 | 389 | if channel_count > 2: 390 | channel_pos = 220 391 | figure_width = figure_width * 2 392 | elif channel_count == 2: 393 | channel_pos = 210 394 | else: 395 | channel_pos = 110 396 | 397 | figsize = (figure_width, figure_height) 398 | padding = {'wspace':spacing,'hspace':spacing,'top':1.-padding,'left':padding,'bottom':padding,'right':1.-padding} 399 | scope_graph = realtime_graph(title="Scope", show=True, manual=True, redraw=False, figsize=figsize, padding=padding) 400 | fft_graph = realtime_graph(title="FFT", show=True, manual=True, redraw=False, figsize=figsize, padding=padding) 401 | 402 | pos_count = 0 403 | y_limits = (config.noise_floor - 10.0, -30*0) # For FFT # FIXME: Arg 404 | for channel_idx in channels: 405 | #if channel_count > 2: 406 | # pos_offset = ((pos_count % 2) * 2) + (pos_count / 2) + 1 # Re-order column-major 407 | #else: 408 | pos_offset = pos_count + 1 409 | subplot_pos = (channel_pos + pos_offset) 410 | 411 | scope_channel_graphs[channel_idx] = sub_graph = realtime_graph(parent=scope_graph, show=True, redraw=False, sub_title="Channel %i" % (channel_idx), pos=subplot_pos) #, x_range=NUM_BINS_SPUR, y_limits=y_limits 412 | 413 | fft_channel_graphs[channel_idx] = sub_graph = realtime_graph(parent=fft_graph, show=True, redraw=False, sub_title="Channel %i" % (channel_idx), pos=subplot_pos, y_limits=y_limits, x_range=gui_fft_length) 414 | sub_graph.add_horz_line(config.noise_floor) 415 | 416 | pos_count = pos_count + 1 417 | 418 | # So the GUIs are updated 419 | fft_graph.redraw() 420 | scope_graph.redraw() 421 | 422 | for m in modules: m.init(usrp, info, states, state_machines, fft_graph, scope_graph) 423 | 424 | ################################ 425 | 426 | count = 0 427 | #tune_times = [] 428 | tune_stats = Statistics() 429 | iteration_stats = Statistics() 430 | acquisition_stats = Statistics() 431 | computation_stats = Statistics() 432 | skip = options.skip 433 | 434 | while running: 435 | count += 1 436 | idx = count - 1 437 | 438 | iteration_start = time.time() 439 | 440 | current_hw_states = [] 441 | 442 | for channel_idx in channels: 443 | state_machine = state_machines[channel_idx] 444 | hw_state = state_machine.next() 445 | if options.once and state_machine.loops > 0: # This will catch the *first* state machine loop 446 | running = False 447 | break 448 | 449 | for m in modules: 450 | if m.query_stop(channel_idx, state_machine, hw_state): 451 | running = False 452 | break 453 | if not running: 454 | break 455 | 456 | current_hw_states += [hw_state] 457 | 458 | print 459 | #print "Current HW states:" 460 | #print "\n".join(map(str, current_hw_states)) 461 | 462 | if not running: 463 | break 464 | 465 | if skip > 0: 466 | skip -= 1 467 | continue 468 | 469 | try: 470 | if count > 0 and options.wait: 471 | print "Waiting: ", 472 | raw_input() 473 | 474 | #print 475 | print "Iteration:", count 476 | 477 | for channel_idx in channels: 478 | state_machine = state_machines[channel_idx] 479 | print "Channel #%d state machine index: %03d/%03d" % (channel_idx, (state_machine.index()+1), state_machine.count()) 480 | 481 | #ts = time.time() 482 | #time_str = time.strftime("%a, %d %b %Y %H:%M:%S", ts) 483 | time_now = datetime.datetime.now() 484 | time_now_str = time_now.strftime("%Y/%m/%d %H:%M:%S.%f") 485 | print "Host time:", time_now_str 486 | print "USRP time:", usrp.get_time_now().get_real_secs() 487 | 488 | if not options.no_gps: 489 | for sensor_name in _available_nmea_sensors: 490 | sensor_value = usrp.get_mboard_sensor(sensor_name) 491 | value = sensor_value.value.strip() 492 | if value == "": 493 | continue 494 | print value 495 | 496 | for m in modules: m.start(count, current_hw_states) # FIXME: GPS info? 497 | 498 | for channel_idx in channels: # FIXME: Any callback into modules here? E.g. modify state/skip? 499 | hw_state = current_hw_states[channel_idx] 500 | 501 | print "Chan %d: %s" % (channel_idx, str(hw_state)) 502 | 503 | if not (hw_state.antenna is None or len(hw_state.antenna) == 0): 504 | usrp.set_antenna(hw_state.antenna, channel_idx) 505 | 506 | # This here prevents LO offset from being used 507 | #if config.linked and channel_idx > 0: 508 | # continue 509 | 510 | # [Anything after this should be first channel, or in unlinked front-end] 511 | 512 | usrp.set_gain(hw_state.gain, channel_idx) # Still set the gain first, in case moving to a band of powerful signals 513 | 514 | tune_start = time.time() 515 | usrp.set_center_freq(uhd.tune_request(hw_state.freq, hw_state.lo_offset), channel_idx) 516 | tune_duration = time.time() - tune_start 517 | #tune_times += [tune_duration] 518 | tune_stats.add(tune_duration) 519 | print "Tune time: %f ms (average: %f ms, min: %f ms, max: %f ms)" % ( 520 | tune_duration*1e3, 521 | tune_stats.ave()*1e3, #numpy.average(tune_times)*1e3, 522 | tune_stats.min()*1e3, #min(tune_times)*1e3, 523 | tune_stats.max()*1e3) #max(tune_times)*1e3) 524 | 525 | if hw_state.bandwidth is not None: 526 | usrp.set_bandwidth(hw_state.bandwidth, channel_idx) 527 | 528 | if config.linked and channel_idx > 0: 529 | continue 530 | 531 | if config.tune_policy.settling_time > 0: 532 | time.sleep(config.tune_policy.settling_time) 533 | 534 | channel_sensor_names = chan_sensors[channel_idx] 535 | if config.tune_policy.sensor_name in channel_sensor_names: 536 | consecutive_locks = 0 537 | time_start = time.time() 538 | sys.stdout.write("Waiting for LO lock: ") 539 | sys.stdout.flush() 540 | while (time.time() - time_start) < config.tune_policy.timeout: 541 | lo_locked_sensor = usrp.get_sensor(config.tune_policy.sensor_name) 542 | lo_locked = lo_locked_sensor.to_bool() 543 | if lo_locked: 544 | sys.stdout.write("*") 545 | sys.stdout.flush() 546 | 547 | consecutive_locks += 1 548 | if consecutive_locks == config.tune_policy.consecutive_locks: 549 | break 550 | else: 551 | sys.stdout.write("_") 552 | sys.stdout.flush() 553 | 554 | if consecutive_locks > 0: 555 | print "Re-trying tune..." 556 | usrp.set_center_freq(uhd.tune_request(hw_state.freq, hw_state.lo_offset), channel_idx) # Try again 557 | 558 | consecutive_locks = 0 559 | time.sleep(config.tune_policy.wait_time) 560 | 561 | print 562 | 563 | if consecutive_locks != config.tune_policy.consecutive_locks: 564 | print "Failed to lock!" 565 | continue 566 | 567 | retry = True 568 | flush = False 569 | while retry and running: 570 | retry = False # Default path is to break from the loop 571 | 572 | if flush: 573 | total_sample_count = options.flush # This always seems to cause 0 samples to be acquired 574 | skip_samples = 0 575 | else: 576 | total_sample_count = config.sample_count + config.skip_samples 577 | skip_samples = config.skip_samples 578 | 579 | acquisition_start = time.time() 580 | 581 | orig_samples = [] 582 | 583 | if usrp_acquire_src: 584 | if options.force_delay: 585 | stream_now = False 586 | else: 587 | stream_now = (len(channels) == 1) 588 | delay = options.delay 589 | timeout = 1.0 590 | sample_ptrs = usrp_acquire_src.finite_acquisition_v(total_sample_count, stream_now=stream_now, delay=delay, skip=skip_samples, timeout=timeout) 591 | samples = [] 592 | acquired_sample_count = sample_ptrs[-1] 593 | for sample_ptr_idx in range(len(sample_ptrs)-1): 594 | if make_complex: 595 | channel_samples = pointer_to_ndarray(sample_ptrs[sample_ptr_idx], numpy.dtype(numpy_dtype), acquired_sample_count * 2, True) 596 | orig_samples.append(channel_samples) 597 | complex_channel_samples = (channel_samples[::2] + (channel_samples[1::2] * 1j)) / (2.0**15) 598 | samples.append(complex_channel_samples) 599 | else: 600 | samples.append(pointer_to_ndarray(sample_ptrs[sample_ptr_idx], numpy.dtype(numpy_dtype), acquired_sample_count, True)) 601 | if not make_complex: 602 | orig_samples = samples 603 | else: 604 | samples = usrp.finite_acquisition_v(total_sample_count) 605 | orig_samples = samples 606 | acquired_sample_count = None 607 | for s in samples: 608 | if acquired_sample_count is None: 609 | acquired_sample_count = len(s) 610 | else: 611 | acquired_sample_count = min(acquired_sample_count, len(s)) 612 | 613 | if flush: 614 | flush = False 615 | retry = True 616 | print "Retrying after flush (%d samples received)..." % (acquired_sample_count) 617 | continue 618 | 619 | expected_sample_count = config.sample_count 620 | if usrp_acquire_src is None: 621 | expected_sample_count += config.skip_samples 622 | 623 | acquisition_duration = time.time() - acquisition_start 624 | if expected_sample_count == acquired_sample_count: 625 | acquisition_stats.add(acquisition_duration) 626 | else: 627 | pass # FIXME: Per-channel length processing logic below should be moved here 628 | if acquisition_stats.count() > 0: 629 | print "Acquisition time: %f ms (average: %f ms, min: %f ms, max: %f ms)" % ( 630 | acquisition_duration*1e3, 631 | acquisition_stats.ave()*1e3, 632 | acquisition_stats.min()*1e3, 633 | acquisition_stats.max()*1e3) 634 | 635 | computation_state = time.time() 636 | 637 | partial_name = "%s-%s-%05d-%d-%d-%.1f-%s" % (config.name, time_now_str, count, channel_count, int(hw_state.freq), hw_state.gain, hw_state.get_antenna()) 638 | partial_name = partial_name.replace("/", "_").replace(":", "_").replace(" ", "_") 639 | 640 | sample_idx = 0 641 | for s in samples: 642 | if len(s) != expected_sample_count: 643 | if options.abort: 644 | if len(s) == 0: 645 | print "Channel %d: didn't receive any samples - aborting." % (sample_idx) 646 | else: 647 | print "Channel %d: only received %d samples (%d short) - aborting." % (sample_idx, len(s), (expected_sample_count-len(s))) 648 | running = False 649 | break 650 | elif options.restart: 651 | if len(s) == 0: 652 | print "Channel %d: didn't receive any samples - restarting." % (sample_idx) 653 | else: 654 | print "Channel %d: only received %d samples (%d short) - restarting." % (sample_idx, len(s), (expected_sample_count-len(s))) 655 | raise RestartException() 656 | if len(s) == 0: 657 | print "Channel %d: didn't receive any samples - retrying..." % (sample_idx) 658 | else: 659 | print "Channel %d: only received %d samples (%d short) - retrying..." % (sample_idx, len(s), (expected_sample_count-len(s))) 660 | time.sleep(options.retry_sleep) 661 | retry = True 662 | flush = True 663 | break 664 | print "Channel %d: received %d samples" % (sample_idx, len(s)) 665 | 666 | if config.skip_samples > 0 and usrp_acquire_src is None: 667 | print "Removing skipped samples..." 668 | s = numpy.array(s[config.skip_samples:]) # This is slow 669 | print "Removed skipped samples." 670 | 671 | hw_state = current_hw_states[sample_idx] 672 | 673 | title = "Chan %d: %s" % (sample_idx, hw_state) 674 | 675 | fft_length = gui_fft_length 676 | 677 | force_fft = False 678 | for m in modules: 679 | if m.query_fft(sample_idx, hw_state): 680 | force_fft = True 681 | 682 | num_ffts = 0 683 | fft_avg, fft_min, fft_max = None, None, None 684 | if fft_graph is not None or force_fft: 685 | num_ffts, fft_avg, fft_min, fft_max = calc_fft( 686 | s, 687 | gui_fft_length, 688 | verbose=options.verbose, 689 | pad=options.pad_fft, 690 | step=options.fft_step, 691 | window=window_fn 692 | ) 693 | 694 | freq_min = hw_state.freq-config.rate/2 695 | freq_max = hw_state.freq+config.rate/2 696 | 697 | fft_channel_graph = None 698 | if fft_graph is not None: 699 | fft_channel_graph = fft_channel_graphs[sample_idx] 700 | #freqs = [fft_result.bin_to_freq(bin) for bin in range(len(fft_dbm))] 701 | freqs = numpy.linspace(freq_min, freq_max, len(fft_avg)) 702 | fft_channel_graph.update(data=[fft_avg, fft_min, fft_max], sub_title=title, redraw=False, x=freqs, points=[]) #, points=spurs_detected 703 | 704 | scope_channel_graph = None 705 | if scope_graph is not None: 706 | scope_channel_graph = scope_channel_graphs[sample_idx] 707 | mag_samples = numpy.absolute(numpy.array(s[:gui_scope_length])) 708 | scope_channel_graph.update(data=mag_samples, sub_title=title, redraw=False) 709 | 710 | for m in modules: m.process(sample_idx, hw_state, s, {'num':num_ffts, 'ave':fft_avg, 'min':fft_min, 'max':fft_max}, partial_name, fft_channel_graph, scope_channel_graph) 711 | 712 | if options.location is not None: 713 | force_save = None 714 | for m in modules: 715 | query_result = m.query_save('data') 716 | if query_result == True: 717 | force_save = True 718 | break # Prioritise True 719 | elif query_result == False: 720 | force_save = False 721 | 722 | if force_save is None or force_save == True: # Default to save 723 | capture_file_name = "%s-%d.%s.cfile" % (partial_name, sample_idx, stream_args.cpu_format) 724 | capture_file_path = os.path.join(options.location, capture_file_name) 725 | print "Saving to:", capture_file_path 726 | try: 727 | #f = open(capture_file_path, "w") 728 | #s.astype('c8').tofile(capture_file_path) 729 | if usrp_acquire is not None: 730 | orig_samples[sample_idx].tofile(capture_file_path) # Should already be in correct format 731 | else: 732 | orig_samples[sample_idx].astype(numpy_dtype, copy=False).tofile(capture_file_path) 733 | #f.close() 734 | except Exception, e: 735 | print "Failed to save samples to file:", capture_file_path 736 | print e 737 | 738 | sample_idx += 1 739 | 740 | successful = (sample_idx == channel_count) # If it didn't break out of the inner-loop prematurely... 741 | for m in modules: m.stop(successful) 742 | 743 | computation_duration = time.time() - computation_state 744 | if successful: 745 | computation_stats.add(computation_duration) 746 | print "Computation time: %f ms (average: %f ms, min: %f ms, max: %f ms)" % ( 747 | computation_duration*1e3, 748 | computation_stats.ave()*1e3, 749 | computation_stats.min()*1e3, 750 | computation_stats.max()*1e3) 751 | 752 | if fft_graph is not None: 753 | fft_graph.redraw() 754 | 755 | if options.location is not None: 756 | force_save = None 757 | for m in modules: 758 | query_result = m.query_save('fft_graph') 759 | if query_result == True: 760 | force_save = True 761 | break # Prioritise True 762 | elif query_result == False: 763 | force_save = False 764 | 765 | if force_save is None or force_save == True: # Default to save 766 | fig_name = "fft-%s.png" % (partial_name) 767 | fig_name = os.path.join(options.location, fig_name) 768 | print "Saving FFT graph to: \"%s\"" % (fig_name) 769 | fft_graph.save(fig_name) 770 | if scope_graph is not None: 771 | scope_graph.redraw() 772 | 773 | # Done 774 | 775 | iteration_end = time.time() 776 | iteration_duration = iteration_end - iteration_start 777 | iteration_stats.add(iteration_duration) # FIXME: Decide whether to add to stats if exiting a stuck retry loop 778 | print "Iteration time: %f ms (average: %f ms, min: %f ms, max: %f ms)" % ( 779 | iteration_duration*1e3, 780 | iteration_stats.ave()*1e3, 781 | iteration_stats.min()*1e3, 782 | iteration_stats.max()*1e3) 783 | except RestartException: 784 | print "Deleting USRP for restart..." 785 | del usrp_acquire_src 786 | del usrp 787 | break 788 | except wx._core.PyDeadObjectError: 789 | print "GUI window closed" 790 | running = False 791 | break 792 | #except KeyboardInterrupt: # Using signal handler instead 793 | # print "Stopping..." 794 | # running = False 795 | # break 796 | except RuntimeError, e: 797 | print "Likely UHD runtime exception:", e 798 | #print "Args:", e.args 799 | #print "Message:", e.message 800 | 801 | # Abort if too many errors in short space of time 802 | # How to handle such errors as bad MCR? Try dummy iteration? 803 | 804 | print "Deleting USRP..." 805 | del usrp_acquire_src 806 | del usrp 807 | 808 | if options.abort: 809 | running = False 810 | else: 811 | print 812 | 813 | break 814 | except IOError, e: 815 | print "Caught an I/O error: %s" % (e) 816 | traceback.print_exc() 817 | running = False 818 | break 819 | except Exception, e: 820 | print "Caught unhandled exception (%s): %s" % (type(e), e) 821 | traceback.print_exc() 822 | running = False 823 | break 824 | 825 | for m in modules: m.shutdown() 826 | 827 | if __name__ == '__main__': 828 | main() 829 | -------------------------------------------------------------------------------- /spectrum_viewer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # spectrum_viewer.py 5 | # 6 | # Part of: https://github.com/balint256/cyberspectrum 7 | # 8 | # Copyright 2014 Balint Seeber 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (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 General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | # MA 02110-1301, USA. 24 | # 25 | # 26 | 27 | import sys, os, datetime 28 | from optparse import OptionParser 29 | 30 | import numpy 31 | 32 | from fft_tools import * 33 | from realtime_graph import * 34 | from utils import * 35 | 36 | class CaptureFile(): 37 | def __init__(self, path, rate, config_name, capture_time, idx, chan, freq, gain, antenna): 38 | self.path = path 39 | self.rate = rate 40 | self.config_name = config_name 41 | self.capture_time = capture_time 42 | self.idx = idx 43 | self.chan = chan 44 | self.freq = freq 45 | self.gain = gain 46 | self.antenna = antenna 47 | def __str__(self): 48 | # Not showing: path, config_name, chan 49 | return "%s %05d: %d sps, %d Hz, %d, %s" % (self.capture_time, self.idx, self.rate, self.freq, self.gain, self.antenna) 50 | 51 | def _stitch_array(orig, new, overlap): 52 | if overlap == 0: 53 | return numpy.concatenate((orig, new)) 54 | 55 | new_part = new[:overlap] 56 | orig_part = orig[-overlap:] 57 | ramp = numpy.linspace(0.0, 1.0, overlap) 58 | part = numpy.multiply(new_part, ramp) + numpy.multiply(orig_part, numpy.subtract(numpy.ones(overlap), ramp)) 59 | return numpy.concatenate((orig[:-overlap], part, new[overlap:])) 60 | 61 | def main(): 62 | parser = OptionParser(usage="%prog: [options] files...") 63 | 64 | parser.add_option("-s", "--samp-rate", type="float", default=1e6, help="Sample rate (Hz) [default=%default]") 65 | parser.add_option("-c", "--channel", type="string", default=None, help="Plot single channel [default=%default]") 66 | parser.add_option("-l", "--fft-length", type="int", default=16384, help="FFT length [default=%default]") 67 | parser.add_option("-L", "--lower-limit", type="float", default=-130, help="Lower amplitude limit [default=%default]") 68 | parser.add_option("-U", "--upper-limit", type="float", default=0, help="Upper amplitude limit [default=%default]") 69 | parser.add_option("-t", "--type", type="string", default="c8", help="Numpy file type [default=%default]") 70 | parser.add_option("-S", "--start-idx", type="int", default=0, help="Capture list index start [default=%default]") 71 | parser.add_option("-E", "--end-idx", type="int", default=-1, help="Capture list index end [default=%default]") 72 | parser.add_option("-o", "--overlap", type="float", default=None, help="Overlap amount [default=%default]") 73 | parser.add_option("-f", "--start-freq", type="float", default=None, help="Lower frequency limit [default=%default]") 74 | parser.add_option("-e", "--stop-freq", type="float", default=None, help="Upper frequency limit [default=%default]") 75 | parser.add_option("-v", "--verbose", action="store_true", default=False, help="Verbose output [default: %default]") 76 | 77 | (options, args) = parser.parse_args() 78 | 79 | print "Files:", len(args) 80 | 81 | channels = {} 82 | for arg in args: 83 | name = os.path.basename(arg) 84 | name = os.path.splitext(name)[0] 85 | parts = name.split("-") 86 | time_parts = parts[1].split("_") 87 | sec = float(time_parts[5]) 88 | whole_sec = int(sec) 89 | us = int((sec - whole_sec) * 1e6) 90 | capture_time = datetime.datetime(int(time_parts[0]), int(time_parts[1]), int(time_parts[2]), int(time_parts[3]), int(time_parts[4]), whole_sec, us) 91 | channel_count = int(parts[3]) 92 | freq = float(parts[4]) 93 | antenna = parts[6].replace("_", "/") 94 | idx = int(parts[7].split('.')[0]) 95 | cap = CaptureFile(arg, options.samp_rate, parts[0], capture_time, int(parts[2]), idx, freq, float(parts[5]), antenna) 96 | if idx in channels.keys(): 97 | channels[idx] += [cap] 98 | else: 99 | channels[idx] = [cap] 100 | 101 | if False: 102 | for channel in channels.keys(): 103 | print channel 104 | channel_caps = channels[channel] 105 | for cap in channel_caps: 106 | print cap 107 | 108 | if options.channel is None or len(options.channel) == 0: 109 | channels_to_show = channels.keys() 110 | else: 111 | channels_to_show = map(int, options.channel.split(',')) 112 | 113 | channel_count = len(channels_to_show) 114 | 115 | ############################### 116 | 117 | font = { 118 | #'family' : 'normal', 119 | #'weight' : 'bold', 120 | 'size' : 10 121 | } 122 | 123 | matplotlib.rc('font', **font) 124 | 125 | padding = 0.05 126 | spacing = 0.1 127 | figure_width = 8 128 | figure_height = 10 129 | 130 | if channel_count > 2: 131 | channel_pos = 220 132 | figure_width = figure_width * 2 133 | elif channel_count == 2: 134 | channel_pos = 210 135 | else: 136 | channel_pos = 110 137 | 138 | figsize = (figure_width, figure_height) 139 | padding = {'wspace':spacing,'hspace':spacing,'top':1.-padding,'left':padding,'bottom':padding,'right':1.-padding} 140 | fft_graph = realtime_graph(title="FFT", show=True, manual=True, redraw=False, figsize=figsize, padding=padding, verbose=options.verbose) 141 | fft_channel_graphs = {} 142 | 143 | pos_count = 0 144 | y_limits = (options.lower_limit, options.upper_limit) 145 | for channel_idx in channels_to_show: 146 | #if channel_count > 2: 147 | # pos_offset = ((pos_count % 2) * 2) + (pos_count / 2) + 1 # Re-order column-major 148 | #else: 149 | pos_offset = pos_count + 1 150 | subplot_pos = (channel_pos + pos_offset) 151 | 152 | channel = channels[channel_idx] 153 | fft_avg, fft_min, fft_max = numpy.array([]), numpy.array([]), numpy.array([]) 154 | cnt = 0 155 | if options.end_idx == -1: 156 | options.end_idx = len(channel) - 1 157 | #for cap in channel: 158 | freq_min = None 159 | freq_max = None 160 | freq_last = None 161 | freq_line = [] 162 | freq_center_line = [] 163 | for cap_idx in range(options.start_idx, options.end_idx+1): 164 | cnt += 1 165 | cap = channel[cap_idx] 166 | freq_bottom = cap.freq - options.samp_rate/2 167 | freq_top = cap.freq + options.samp_rate/2 168 | 169 | if options.start_freq is not None and freq_top < options.start_freq: 170 | continue 171 | elif options.stop_freq is not None and freq_bottom > options.stop_freq: 172 | break 173 | 174 | if freq_min is None: freq_min = cap.freq 175 | else: 176 | if cap.freq <= freq_min: 177 | raise Exception("Out-of-order captures") 178 | if freq_max is None: freq_max = cap.freq 179 | else: 180 | if cap.freq <= freq_max: 181 | raise Exception("Out-of-order captures") 182 | else: 183 | freq_max = cap.freq 184 | overlap_bins = 0 185 | if freq_last is not None: 186 | if options.overlap is None: 187 | freq_diff = cap.freq - freq_last 188 | if freq_diff < 0: 189 | raise Exception("Negative frequency step") 190 | overlap = max(0.0, 1.0 - (freq_diff / options.samp_rate)) 191 | # print "Overlap:", overlap 192 | else: 193 | overlap = options.overlap 194 | overlap_bins = int(options.fft_length * overlap) 195 | print "[%05d] Freq: %s (%s - %s) overlap bins: %d" % (cnt, format_freq(cap.freq), format_freq(freq_bottom), format_freq(freq_top), overlap_bins) 196 | freq_line += [freq_bottom, freq_top] 197 | freq_center_line += [cap.freq] 198 | freq_last = cap.freq 199 | 200 | data = numpy.fromfile(cap.path, numpy.dtype(options.type)) 201 | print "[%05d] Read %d items from %s" % (cnt, len(data), cap.path) 202 | 203 | _num_ffts, _fft_avg, _fft_min, _fft_max = calc_fft(data, options.fft_length, verbose=options.verbose) 204 | 205 | fft_avg = _stitch_array(fft_avg, _fft_avg, overlap_bins) 206 | fft_min = _stitch_array(fft_min, _fft_min, overlap_bins) 207 | fft_max = _stitch_array(fft_max, _fft_max, overlap_bins) 208 | 209 | print "Avg/min/max lengths: %d/%d/%d" % (len(fft_avg), len(fft_min), len(fft_max)) 210 | freq_min -= (options.samp_rate / 2) 211 | freq_max += (options.samp_rate / 2) 212 | print "Freq range: %s - %s" % (format_freq(freq_min), format_freq(freq_max)) 213 | 214 | x = numpy.linspace(freq_min, freq_max, len(fft_avg)) 215 | 216 | fft_channel_graphs[channel_idx] = sub_graph = realtime_graph( 217 | parent=fft_graph, 218 | show=True, 219 | redraw=False, 220 | sub_title="Channel %i" % (channel_idx), 221 | pos=subplot_pos, 222 | y_limits=y_limits, 223 | # x_range=, # Leave as auto 224 | data=[fft_avg, fft_min, fft_max], 225 | x=x, 226 | verbose=options.verbose) 227 | 228 | for freq in freq_line: 229 | sub_graph.add_vert_line(freq, 'gray') 230 | for freq in freq_center_line: 231 | sub_graph.add_vert_line(freq) 232 | 233 | pos_count = pos_count + 1 234 | 235 | # fft_graph.redraw() # Not necessary 236 | fft_graph.go_modal() 237 | 238 | return 0 239 | 240 | if __name__ == '__main__': 241 | main() 242 | -------------------------------------------------------------------------------- /spur_search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # spur_search.py 5 | # 6 | # Copyright 2014 Balint Seeber 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (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 General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | # 24 | 25 | import math 26 | 27 | import numpy 28 | 29 | import interface 30 | 31 | def get_spurs(bins, freq_min, freq_max, snr=6.0, percent_noise_bins=80.0): 32 | """ 33 | Get a list of bins sticking out of the noise floor 34 | NOTE: This routine assumes flat noise floor with most bins as noise 35 | @param snr the number of db a bin needs to stick out of the noise floor 36 | @param percent_noise_bins is the minimum percentage of fft bins expected to be noise 37 | """ 38 | h = numpy.histogram(bins, numpy.arange(min(bins), max(bins), float(snr)/2.0)) 39 | #print len(h[0]), h[0] 40 | #print len(h[1]), h[1] 41 | percent = 0.0 42 | for i in range(len(h[0])): 43 | percent += 100.0 * float(h[0][i])/float(len(h[0])) 44 | if percent > percent_noise_bins: break 45 | 46 | threshold = h[1][min(len(h[1])-1,i+2)] 47 | 48 | def _bin_to_freq(idx): 49 | freq_range = float(freq_max - freq_min) 50 | return idx * freq_range / (len(bins) - 1) + freq_min 51 | 52 | spurs = list() 53 | for i in range(len(bins)): 54 | if bins[i] > threshold: spurs.append((_bin_to_freq(i), bins[i])) 55 | 56 | return spurs 57 | 58 | class SpurSearch(interface.Module): 59 | def __init__(self, config, options, *args, **kwds): 60 | interface.Module.__init__(self, config, options, *args, **kwds) 61 | 62 | self.spur_log_file = None 63 | self.noise_log_file = None 64 | self.total_spur_count = 0 65 | 66 | def __del__(self): 67 | if self.spur_log_file: self.spur_log_file.close() 68 | if self.noise_log_file: self.noise_log_file.close() 69 | 70 | def populate_options(self, parser): 71 | parser.add_option("--spur-log", type="string", default=None, help="Spur log file [default=%default]") 72 | parser.add_option("--ignore-lo", action="store_true", help="Ignore LO spur", default=False) 73 | parser.add_option("--lo-tolerance", type="float", default=7.5e3, help="Ignore LO spur +/- from DC (Hz) [default: %default]") 74 | parser.add_option("--spur-snr", type="float", default=1.0, help="Spur threshold above noise floor (dB) [default: %default]") 75 | parser.add_option("--only-save-spurs", action="store_true", default=False, help="Only save image when spurs are detected [default: %default]") 76 | parser.add_option("--noise-log", type="string", default=None, help="Noise floor log file [default=%default]") 77 | 78 | def init(self, usrp, info, states, state_machines, fft_graph, scope_graph): 79 | interface.Module.init(self, usrp, info, states, state_machines, fft_graph, scope_graph) 80 | 81 | if not self.spur_log_file and self.options.spur_log is not None and len(self.options.spur_log) > 0: 82 | self.spur_log_file = open(self.options.spur_log, "w") 83 | if not self.noise_log_file and self.options.noise_log is not None and len(self.options.noise_log) > 0: 84 | self.noise_log_file = open(self.options.noise_log, "w") 85 | 86 | def start(self, count, current_hw_states): 87 | interface.Module.start(self, count, current_hw_states) 88 | 89 | self.total_spur_count = 0 90 | 91 | def query_stop(self, channel_idx, state_machine, hw_state): 92 | return (state_machine.loops > 0) 93 | 94 | def query_fft(self, sample_idx, hw_state): 95 | return True 96 | 97 | def process(self, sample_idx, hw_state, s, fft_data, partial_name, fft_channel_graph, scope_channel_graph): 98 | spurs_detected = [] 99 | lo_spurs = [] 100 | noise = None 101 | freq_min = hw_state.freq - self.config.rate/2 102 | freq_max = hw_state.freq + self.config.rate/2 103 | 104 | fft_avg = fft_data['ave'] 105 | 106 | hz_per_bin = math.ceil(self.config.rate / len(fft_avg)) 107 | lo_bins = int(math.ceil(self.options.lo_tolerance / hz_per_bin)) 108 | #print "Skipping %i LO bins" % (lo_bins) 109 | lhs = fft_avg[0:((len(fft_avg) + 1)/2) - ((lo_bins-1)/2)] 110 | rhs = fft_avg[len(fft_avg)/2 + ((lo_bins-1)/2):] 111 | #print len(fft_avg), len(lhs), len(rhs) 112 | fft_minus_lo = numpy.concatenate((lhs, rhs)) 113 | #noise = numpy.average(numpy.array(fft_minus_lo)) 114 | noise = 10.0 * math.log10(numpy.average(10.0 ** (fft_minus_lo / 10.0))) # dB 115 | print ("\t[%i] Noise (skipped %i LO FFT bins)" % (sample_idx, lo_bins)), noise, "dB" 116 | 117 | lo_freq = hw_state.freq + hw_state.lo_offset 118 | fig_name = "fft-%s.png" % (partial_name) # Same as scanner.py 119 | 120 | if self.noise_log_file: 121 | self.noise_log_file.write("%d,%d,%f,%f,%f,%s,%f,%s\n" % ( 122 | self.last_count, 123 | sample_idx, 124 | hw_state.freq, 125 | lo_freq, 126 | hw_state.gain, 127 | hw_state.get_antenna(), 128 | noise, 129 | fig_name, 130 | )) 131 | 132 | spurs = get_spurs(fft_avg, freq_min, freq_max) # snr=6.0, percent_noise_bins=80.0 133 | 134 | spur_threshold = noise + self.options.spur_snr 135 | 136 | for spur_freq, spur_level in spurs: 137 | if spur_level > spur_threshold: 138 | if self.options.ignore_lo and abs(lo_freq - spur_freq) < self.options.lo_tolerance: 139 | #print "\t[%i]\tLO @ %f MHz (%03f dBm) for LO %f MHz (offset %f Hz)" % (channel, spur_freq, spur_level, lo_freq, (spur_freq-lo_freq)) 140 | lo_spurs += [(spur_freq, spur_level)] 141 | else: 142 | spurs_detected += [(spur_freq, spur_level)] 143 | #d = { 144 | # 'id': id, 145 | # 'spur_level': spur_level, 146 | # 'spur_freq': spur_freq, 147 | # 'lo_freq': lo_freq, 148 | # 'channel': channel, 149 | # 'noise_floor': noise, 150 | #} 151 | #print '\t\tSpur:', d 152 | print "\t[%i]\tSpur @ %f Hz (%03f dBFS) for LO %f MHz (offset %f Hz)" % ( 153 | sample_idx, 154 | spur_freq, 155 | spur_level, 156 | lo_freq, 157 | (spur_freq-lo_freq) 158 | ) 159 | if self.spur_log_file: 160 | self.spur_log_file.write("%d,%d,%f,%f,%f,%s\n" % ( 161 | self.last_count, 162 | sample_idx, 163 | spur_freq, 164 | spur_level, 165 | lo_freq, 166 | fig_name, 167 | )) 168 | self.total_spur_count += 1 169 | 170 | if fft_channel_graph is not None: 171 | fft_channel_graph.add_points(spurs_detected) 172 | fft_channel_graph.add_horz_line(noise, 'gray', '--', id='noise') 173 | fft_channel_graph.add_horz_line(spur_threshold, 'gray', '-', id='spur_threshold') 174 | fft_channel_graph.add_points(lo_spurs, 'go') 175 | 176 | def query_save(self, which): 177 | if which == 'fft_graph': 178 | if self.options.only_save_spurs: 179 | return (self.total_spur_count > 0) 180 | return None 181 | 182 | def shutdown(self): 183 | return 184 | 185 | def get_modules(): 186 | return [{'class':SpurSearch, 'name':"Spur Search"}] 187 | 188 | def main(): 189 | return 0 190 | 191 | if __name__ == '__main__': 192 | main() 193 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # utils.py 5 | # 6 | # Part of: https://github.com/balint256/cyberspectrum 7 | # 8 | # Copyright 2014 Balint Seeber 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (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 General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | # MA 02110-1301, USA. 24 | # 25 | # 26 | 27 | def format_freq(f, decimals=None, units=True): 28 | if f is None: 29 | return "(none)" 30 | unit = '' 31 | if f >= 1e9: 32 | f /= 1e9 33 | unit = 'G' 34 | elif f >= 1e6: 35 | f /= 1e6 36 | unit = 'M' 37 | elif f >= 1e3: 38 | f /= 1e3 39 | unit = 'k' 40 | if decimals is None: 41 | fmt = "%f" 42 | else: 43 | fmt = "%%f.%d" % (decimals) 44 | freq_str = fmt % f 45 | if units: 46 | freq_str += " %sHz" % (unit) 47 | return freq_str 48 | 49 | def main(): 50 | return 0 51 | 52 | if __name__ == '__main__': 53 | main() 54 | --------------------------------------------------------------------------------