├── .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 |
--------------------------------------------------------------------------------