def __demod(self): chunks = self._samples.size / DEMOD_BINS if chunks == 0: Utils.error('Sample time too long') signals = numpy.empty((chunks, len(self._frequencies)), dtype=numpy.float16) # Split samples into chunks freqBins = fftpack.fftfreq(DEMOD_BINS, 1. / self._fs) freqInds = freqBins.argsort() for chunkNum in range(chunks): if self._timing is not None: self._timing.start('Demod') chunkStart = chunkNum * DEMOD_BINS chunk = self._samples[chunkStart:chunkStart + DEMOD_BINS] # Analyse chunk fft = fftpack.fft(chunk, overwrite_x=True) fft /= DEMOD_BINS mags = numpy.absolute(fft) bins = numpy.searchsorted(freqBins[freqInds], self._frequencies) levels = mags[freqInds][bins] signals[chunkNum] = levels if self._timing is not None: self._timing.stop() signals = signals.T self.__smooth(signals, 4) return signals
def start(self, _timing=None): sampleSize = self.fs * SAMPLE_TIME sampleBlocks = self.iq.size / sampleSize if sampleBlocks == 0: Utils.error('Capture too short') for blockNum in range(sampleBlocks): sampleStart = blockNum * sampleSize samples = self.iq[sampleStart:sampleStart + sampleSize] self._callback(samples)
def start(self, _timing=None): length = os.path.getsize(self._filename) / 2 sampleSize = int(self.fs * SAMPLE_TIME) sampleBlocks = int(length / sampleSize) if sampleBlocks == 0: Utils.error('Capture too short') f = open(self._filename, 'rb') for _i in range(sampleBlocks): data = bytearray(f.read(sampleSize * 2)) iq = numpy.array(data).astype(numpy.float32).view(numpy.complex64) iq /= 255. self._callback(iq) f.close()
def __find_pulses(self, signal, negIndices, posIndices, pulseWidths): pulse = None length = signal.size # Find pulses of pulseWidths widths = negIndices - posIndices for wMax, wMin in pulseWidths: posValid = numpy.where((widths > wMin) & (widths < wMax))[0] # Must have between 3 and 6 pulses if posValid.size > 2 and posValid.size < 7 and posValid.size == widths.size: pulseValid = posIndices[posValid] pulseRate = numpy.diff(pulseValid) pulseAvg = numpy.average(pulseRate) # Constant rate? maxDeviation = pulseAvg * PULSE_RATE_DEVIATION / 100. if numpy.std(pulseRate) < maxDeviation: # Calculate frequency freq = length / (pulseAvg * float(SAMPLE_TIME)) rate = freq * 60 # Limit to PULSE_RATES closest = min(PULSE_RATES, key=lambda x: abs(x - rate)) if (abs(closest - rate)) <= closest * PULSE_RATE_TOL / 100.0: # Test for missing pulses if pulseValid[0] - min(pulseRate) < 0 and pulseValid[ -1] + min(pulseRate) > length: # Get pulse levels level = 0 for posValid in range(len(pulseValid)): pos = pulseValid[posValid] width = widths[posValid] pulseSignal = signal[pos:pos + width - 1] level += numpy.average(pulseSignal) level /= len(pulseValid) # Store valid pulse pulse = collar.Collar( widths.size, freq * 60., level, width * SAMPLE_TIME * 1000. / length) break elif self._debug is not None and self._debug.verbose: Utils.error('Missing pulses', False) elif self._debug is not None and self._debug.verbose: Utils.error('Invalid rate {:.1f}PPM'.format(rate), False) elif self._debug is not None and self._debug.verbose: msg = 'Collar rate deviation {:.1f} >= {:.1f}ms' msg = msg.format( 1000 * numpy.std(pulseRate) * SAMPLE_TIME / length, 1000 * maxDeviation * SAMPLE_TIME / length) Utils.error(msg, False) elif self._debug is not None and self._debug.verbose: Utils.error( 'Invalid number of pulses ({}) or invalid pulse widths'. format(posValid.size), False) return pulse
def __detect(self, signals, baseband): collars = [] # Calculate valid pulse widths with PULSE_WIDTH_TOL tolerance sampleRate = signals.shape[1] / float(SAMPLE_TIME) pulseWidths = [width * sampleRate for width in sorted(PULSE_WIDTHS)] pulseWidths = Utils.calc_tolerances(pulseWidths, PULSE_WIDTH_TOL) signalNum = 0 for signal in signals: if self._timing is not None: self._timing.start('Detect') self._signals.append(signal) (threshPos, threshNeg, posIndices, negIndices) = self.__find_edges(signal, pulseWidths) # Find CW collars pulse = self.__find_pulses(signal, negIndices, posIndices, pulseWidths) # Find AM collars if pulse is None: if self._debug is not None and not self._debug.disableAm: am, posIndicesAm, negIndicesAm = self.__find_am( signal, posIndices, negIndices) if self._debug is not None: if not self._debug.disableAm and am is not None: pulse = self.__find_pulses(am, negIndicesAm, posIndicesAm, pulseWidths) if pulse is not None: pulse.mod = collar.AM posIndices = posIndicesAm negIndices = negIndicesAm else: pulse.mod = collar.CW if pulse is not None: pulse.signalNum = signalNum freq = self._frequencies[signalNum] + baseband freq = int(round(freq / CHANNEL_SPACE) * CHANNEL_SPACE) pulse.freq = freq pulse.rate = min(PULSE_RATES, key=lambda x: abs(x - pulse.rate)) collars.append(pulse) if self._timing is not None: self._timing.stop() if self._debug is not None: self._debug.callback_edge(baseband, signal, signalNum, pulse, self._frequencies, threshPos, threshNeg, posIndices, negIndices) signalNum += 1 return collars
def __find_tone(self, signal, indices, freqs): if not len(indices): return None, None sampleRate = signal.size / float(SAMPLE_TIME) periods = [sampleRate / freq for freq in freqs] periods = Utils.calc_tolerances(periods, TONE_TOL) # Edge widths if indices[0] != 0: indices = numpy.insert(indices, 0, 0) widths = numpy.diff(indices) # Count valid widths for each period counts = [] for maxPeriod, minPeriod in periods: valid = (widths > minPeriod) & (widths < maxPeriod) counts.append(numpy.sum(valid)) # Find maximum maxCounts = max(counts) if maxCounts == 0: if self._debug is not None and self._debug.verbose: Utils.error('No tone found', False) return None, None maxPos = counts.index(maxCounts) maxPeriod, minPeriod = periods[maxPos] # Matching widths periodsValid = (widths > minPeriod) & (widths < maxPeriod) periodAvg = numpy.average(widths[periodsValid]) freq = sampleRate / periodAvg # Create pulses from signal pulse = numpy.zeros((signal.size), dtype=numpy.float16) pos = 0 for i in range(widths.size): width = widths[i] valid = periodsValid[i] signalPos = indices[i] level = numpy.average(signal[signalPos:signalPos + width]) level = abs(level) pulse[pos:pos + width].fill(valid * level) pos += width return freq, pulse
def search(self): if self._samples.size < SCAN_BINS: Utils.error('Sample too short') if self._timing is not None: self._timing.start('Scan') f, l = psd(self._samples, SCAN_BINS, self._fs) decibels = 10 * numpy.log10(l) freqIndices = self.__peak_detect(decibels) self._freqs = f self._levels = decibels self._peaks = decibels[freqIndices] freqs = f[freqIndices] if self._timing is not None: self._timing.stop() return freqs
def __init__(self, filename, noiseLevel, callback): self._callback = callback name = os.path.split(filename)[1] print 'Wav file:' print '\tLoading capture file: {}'.format(name) self.fs, data = wavfile.read(filename) if data.shape[1] != 2: Utils.error('Not an IQ file') if data.dtype not in ['int16', 'uint16', 'int8', 'uint8']: Utils.error('Unexpected format') # Get baseband from filename regex = re.compile(r'_(\d+)kHz_IQ') matches = regex.search(name) if matches is not None: self.baseband = int(matches.group(1)) * 1000 else: self.baseband = 0 print '\tSample rate: {:.2f}MSPS'.format(self.fs / 1e6) print '\tLength: {:.2f}s'.format(float(len(data)) / self.fs) # Scale data to +/-1 data = data.astype(numpy.float32, copy=False) data /= 256. # Convert right/left to complex numbers self.iq = 1j * data[..., 0] self.iq += data[..., 1] # Add noise if noiseLevel > 0: noiseI = numpy.random.uniform(-1, 1, self.iq.size) noiseQ = numpy.random.uniform(-1, 1, self.iq.size) self.iq += (noiseI + 1j * noiseQ) * 10. ** (noiseLevel / 10.)
def __parse_arguments(self, argList=None): parser = argparse.ArgumentParser(description='Receiver testmode') parser.add_argument('-i', '--info', help='Display summary info', action='store_true') parser.add_argument('-s', '--spectrum', help='Display capture spectrum', action='store_true') parser.add_argument('-c', '--scan', help='Display signal search', action='store_true') parser.add_argument('-e', '--edges', help='Display pulse edges', type=float, nargs='?', const=0, default=None) parser.add_argument('-a', '--am', help='Display AM detection', action='store_true') parser.add_argument('-b', '--block', help='Block to process', type=int, default=0) parser.add_argument('-da', '--disableAm', help='Disable AM detection', action='store_true') parser.add_argument('--collars', help='Save capture if number of COLLARS not found', type=int, default=None) parser.add_argument('-v', '--verbose', help='Be more verbose', action='store_true') subparser = parser.add_subparsers(help='Source') parserWav = subparser.add_parser('wav') parserWav.add_argument('-n', '--noise', help='Add noise (dB)', type=float, default=0) parserWav.add_argument('wav', help='IQ wav file') parserBin = subparser.add_parser('bin') parserBin.add_argument('bin', help='IQ bin file') parserRtl = subparser.add_parser('rtlsdr') parserRtl.add_argument('-f', '--frequency', help='RTLSDR frequency (MHz)', type=float, required=True) parserRtl.add_argument('-g', '--gain', help='RTLSDR gain (dB)', type=float, default=None) args = parser.parse_args(argList) if 'wav' in args and args.wav is not None and not os.path.isfile(args.wav): Utils.error('Cannot find wav file') if 'bin' in args and args.bin is not None and not os.path.isfile(args.bin): Utils.error('Cannot find bin file') if args.am and args.disableAm: Utils.error('AM detection disabled - will not display graphs', False) return args