// Copyright 2012 (C) Matthew Brejza // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. package rtty; import java.util.ArrayList; import edu.emory.mathcs.jtransforms.fft.DoubleFFT_1D; public class fsk_receiver implements StringRxEvent { public int FFT_half_len = 512; private DoubleFFT_1D ft_obj = new DoubleFFT_1D(FFT_half_len*2); //int FFT_follow_half_len = 512; //private DoubleFFT_1D ft_follow = new DoubleFFT_1D(FFT_follow_half_len*2); private fsk_demodulator decoder = new fsk_demodulator(1200,1800); ConfidenceCalculator cc = new ConfidenceCalculator(8000); private Telemetry_handler telem_hand_7 = new Telemetry_handler(); private Telemetry_handler telem_hand_8 = new Telemetry_handler(); private Telemetry_handler telem_hand_f = new Telemetry_handler(); private Binary_frame_handler telem_hand_bin = new Binary_frame_handler(new boolean[] //{true, false,true, false,true, false,true, false,true, false,true, false}); {true, false, false, true, true, false, true, false, false, true, false, true, false, false, true, false, true, true, true, false, false, true, true, true, false, false, false, true, false, true, true, false}); //{true,true,false,false,false,true,false,true,true,false,false});//{true,false,false,false,true,false,false,true,false,false,true,true}); private String last_sha = ""; public boolean enableFFT = true; private moving_average av_shift = new moving_average(10); public int search_range_rtty = 14; public enum Mode { BINARY, RTTY }; public enum Modulation { FSK, AFSK }; public enum State { INACTIVE, IDLE, FOUND_SIG, BAUD_KNOWN }; public State current_state = State.INACTIVE; public Mode current_mode = Mode.RTTY; public Modulation current_modulation = Modulation.FSK; public int current_data_bits = 7; public int current_stop_bits = 1; public int current_baud = 300; public boolean auto_rtty_finding = true; public boolean enable_afc = true; public int afc_update_freq = 8000; private int samples_since_afc = 0; public int search_freq = 8000; private int samples_since_search = 0; public int fft_update_freq = 2000; private int samples_since_fft = 0; public int samples_since_last_valid = 0; private double[] _samples; private double[] _fft; private boolean _fft_updated = false; private int[] _peaklocs; private Bits_to_chars bit2char_7 = new Bits_to_chars(7,Bits_to_chars.Method.WAIT_FOR_START_BIT); private Bits_to_chars bit2char_8 = new Bits_to_chars(8,Bits_to_chars.Method.WAIT_FOR_START_BIT); private Bits_to_chars bit2char_fixed = new Bits_to_chars(7,2,Bits_to_chars.Method.FIXED_POSITION); //used for 'string received' event protected ArrayList<StringRxEvent> _listeners = new ArrayList<StringRxEvent>(); public fsk_receiver() { // TODO Auto-generated constructor stub telem_hand_7.addStringRecievedListener(this); telem_hand_8.addStringRecievedListener(this); telem_hand_f.addStringRecievedListener(this); telem_hand_bin.addStringRecievedListener(this); } public void addStringRecievedListener(StringRxEvent listener) { _listeners.add(listener); } protected void fireStringReceived(String str, boolean checksum) { for (int i = 0; i < _listeners.size(); i++) { _listeners.get(i).StringRx(str,checksum); } } protected void fireStringReceived(byte[] str, boolean checksum, int length, int flags, int fixed) { for (int i = 0; i < _listeners.size(); i++) { _listeners.get(i).StringRx(str,checksum, length, flags, fixed); } } public double[] find_fsk(double[] samples) { _samples = samples; _fft_updated = false; return find_fsk(false); } //note: gives the square of the FFT private boolean calcuate_FFT() { if (_fft_updated) return true; if (_samples.length < FFT_half_len*2) return false; //get 256 (useful) FFT bins double[] fftar = new double[_samples.length]; System.arraycopy(_samples,0,fftar,0,_samples.length); ft_obj.realForward(fftar); _fft = (new double[FFT_half_len]); //calculate abs(.) for (int i = 0; i < FFT_half_len; i++) { _fft[i] = Math.pow(fftar[i*2], 2) + Math.pow(fftar[i*2 +1], 2); } _fft_updated = true; return true; } private double[] find_fsk(boolean update) { //returns array where [0] is f1 and [1] is f2 double[] out = new double[] {0,0}; if (!calcuate_FFT()) return out; int windows = 15; int win_size = (int)FFT_half_len/windows; double[][] peak = new double[windows][2]; //0: loc; 1: val //int[] peak_loc = new int[windows]; //double[] peak_val = new double[windows]; int peak_count = 0; // TODO each peak should be y dB higher than the min in the window (kinda done) //peak search double win_min_c=1e20; //current minimum double win_min_p=1e20; //previous minimum int min_win_cnt = 0; for (int i = 1; i < FFT_half_len-1; i++) { //used to get minimum values for 2 windows in advance if (_fft[i] < win_min_c) win_min_c = _fft[i]; min_win_cnt++; if (min_win_cnt >= win_size) { win_min_p = win_min_c; win_min_c = 1e20; min_win_cnt=0; } if ((_fft[i-1] < _fft[i]) && (_fft[i] > _fft[i+1]) && (_fft[i] > win_min_c*10 || _fft[i] > win_min_p*10)) //if found peak { if (i < peak[peak_count][0]+win_size) //if another peak in the same window { if (peak[peak_count][1] < _fft[i] ) { peak[peak_count][0] = i; peak[peak_count][1] = _fft[i]; } } else { peak_count++; peak[peak_count][0] = i; peak[peak_count][1] = _fft[i]; } } } _peaklocs = (new int[peak_count]); for (int i = 0; i < peak_count; i++) { peak[peak_count][1] = Math.sqrt(peak[peak_count][1]); //sqrt the peaks _peaklocs[i] = (int)peak[i][0]; //copy to variable to allow for debug access } //now have a list of peaks in the fft //sort peaks by amplitude of peak java.util.Arrays.sort(peak, new java.util.Comparator<double[]>() { public int compare(double[]a, double[]b) { return (int)(b[1] - a[1]); } }); int bb_len = Math.min(2000, _samples.length); double bb[][] = new double [peak_count][bb_len]; double maxs[] = new double [peak_count]; double mins[] = new double [peak_count]; double means[] = new double [peak_count]; double upthre[] = new double [peak_count]; double lothre[] = new double [peak_count]; // TODO demod and check in order of highest amplitude to reduce the need to demod every signal //demodulate at each peak for (int i = 0; i < peak_count; i++) { //fir_filter filts = new fir_filter(new double[] {-0.00903981521632955, -0.0176057278961508, -0.0214888217308206, -0.00894080820836387, 0.0281449413985572, 0.0880962336584224, 0.156738932301488, 0.211778824066603, 0.232865400131940, 0.211778824066603, 0.156738932301488, 0.0880962336584224, 0.0281449413985572, -0.00894080820836387, -0.0214888217308206, -0.0176057278961508, -0.00903981521632955}); //fir_filter filtc = new fir_filter(new double[] {-0.00903981521632955, -0.0176057278961508, -0.0214888217308206, -0.00894080820836387, 0.0281449413985572, 0.0880962336584224, 0.156738932301488, 0.211778824066603, 0.232865400131940, 0.211778824066603, 0.156738932301488, 0.0880962336584224, 0.0281449413985572, -0.00894080820836387, -0.0214888217308206, -0.0176057278961508, -0.00903981521632955}); fir_filter filts = new fir_filter(new double[] {0.0856108043700266, 0.0887368103506232, 0.0912267090885950, 0.0930370874042555, 0.0941362126682943, 0.0945047522364111, 0.0941362126682943, 0.0930370874042555, 0.0912267090885950, 0.0887368103506232, 0.0856108043700266}); fir_filter filtc = new fir_filter(new double[] {0.0856108043700266, 0.0887368103506232, 0.0912267090885950, 0.0930370874042555, 0.0941362126682943, 0.0945047522364111, 0.0941362126682943, 0.0930370874042555, 0.0912267090885950, 0.0887368103506232, 0.0856108043700266}); double lo_phase = 0; double freq = ((double)peak[i][0])/((double)(FFT_half_len*2)); for (int j = 0; j< bb_len;j++) { //multiply, filter, square and add bb[i][j] = Math.pow(filts.step(_samples[j] * Math.sin(2*Math.PI*lo_phase)),2) + Math.pow(filtc.step(_samples[j] * Math.cos(2*Math.PI*lo_phase)),2); lo_phase = lo_phase + freq; if (j >= 30) { //maxs[i] = Math.max(maxs[i], bb[i][j]); if (bb[i][j]> maxs[i] && j > 60) { maxs[i] = bb[i][j]; } mins[i] = Math.min(mins[i], bb[i][j]); means[i] = means[i] + bb[i][j]; } } means[i] = means[i] / (bb_len-30); upthre[i] = means[i]*1.2; //(maxs[i]-means[i])*0.3 + means[i]; lothre[i] = means[i]*0.8; //-(maxs[i]-means[i])*0.3 + means[i]; } // grtty.clearMarkers(); // System.out.println(); // System.out.println(); peak_count=Math.min(4, peak_count); //iterate through all combinations of peaks to find a signal for (int i = 0; i < peak_count-1; i++) { for (int j=i+1; j< peak_count; j++) { if ((peak[i][1] > peak[j][1]*.16 ) && (peak[i][1] < peak[j][1]*7 )) { if ((means[i] > mins[j]) && (means[j] > mins[i])) { //count the number of transitions between the two potential signals int transitionsl = 0; int transitionsh = 0; int highs = 0; boolean last_state = bb[i][40] > bb[j][40]; boolean last_state1 = bb[i][40] > bb[j][40]; for (int k = 50; k < bb_len-10; k=k+10) { //Transition checker boolean current_state = (bb[i][k] > bb[j][k]); if (last_state1 != current_state) { if ( (current_state == (bb[i][k-5] > bb[j][k-5])) || (current_state == (bb[i][k+5] > bb[j][k+5])) ) { if (current_state) { if ( (bb[i][k] > upthre[i]) && (bb[j][k] < lothre[j]) ) { transitionsh++; } } else { if ( (bb[j][k] > upthre[j]) && (bb[i][k] < lothre[i]) ) { transitionsl++; } } } } last_state1 = last_state; //test c last_state = current_state; //both not high at same time check if (!(bb[j][k]>maxs[j]/4) && !(bb[i][k]>maxs[i]/4)) { if (!(bb[j][k+5]>maxs[j]/4) && !(bb[i][k+5]>maxs[i]/4)) { if (!(bb[j][k-5]>maxs[j]/4) && !(bb[i][k-5]>maxs[i]/4)) highs++; } } } // System.out.println(transitionsh + " " + transitionsl + " " + highs); //TODO: look at reinstating this if ( /*(transitionsh+transitionsl > 2) && (transitionsh >0) && (transitionsl > 0) && */(highs < 12)) { //TODO check there are some transitions in the data if (peak[i][0] > peak[j][0]) { out[1] = peak[i][0]*8000/(FFT_half_len*2); out[0] = peak[j][0]*8000/(FFT_half_len*2); } else { out[0] = peak[i][0]*8000/(FFT_half_len*2); out[1] = peak[j][0]*8000/(FFT_half_len*2); } // grtty.drawfft(_fft); // grtty.addMarkers(peak[i][0],peak[j][0]); // //grtty.addMarkers(peak[][0]); // for (int z = 0; z < peak_count; z++) // grtty.addMarkers(peak[z][0]); if (update) { decoder._f1=out[0]/8000; decoder._f2=out[1]/8000; } return out; } } } } } return out; } private void follow_fsk(boolean initial_solution) { //calls follow RTTY, then updates the demod by first making sure the shift is averaged. //if initial solution, the update is applied without any averaging double[] new_pos = follow_fsk_getpos( search_range_rtty); if (av_shift.getMA() == 0 || initial_solution) { av_shift.init(decoder._f2 - decoder._f1); } if (new_pos[0] < 0) { //update position based only on upper freq decoder._f2 = new_pos[1]; decoder._f1 = new_pos[1] - av_shift.getMA(); } else if (new_pos[1] < 0) { //update position based only on lower freq decoder._f1 = new_pos[0]; decoder._f2 = new_pos[0] + av_shift.getMA(); } else { if (new_pos[1]-new_pos[0] < 50/8000) av_shift.update(50); else av_shift.update(new_pos[1]-new_pos[0]); double centre = (new_pos[1]-new_pos[0])/2 + new_pos[0]; decoder._f1 = centre - av_shift.getMA()/2; decoder._f2 = centre + av_shift.getMA()/2; } } private double[] follow_fsk_getpos(int search_range) { //search range is number of fft bins each side of old_Fx double[] out = new double[] {0,0}; double old_f1=decoder._f1; double old_f2=decoder._f2; if (!calcuate_FFT()) return out; double[] int_f1 = new double[2*search_range -1]; double[] int_f2 = new double[2*search_range -1]; int bin_f1 = (int)(old_f1 * 1024); int bin_f2 = (int)(old_f2 * 1024); int j=0; //now integrate the FFT plot around each old freq for (int i = bin_f1-search_range+1; i < bin_f1+search_range; i++) { if ((i >= 0) && (i < FFT_half_len)) { if (j==0) int_f1[0]=Math.sqrt(_fft[i]); else int_f1[j]=int_f1[j-1]+Math.sqrt(_fft[i]); } else { if (j==0) int_f1[0]=0; else int_f1[j]=int_f1[j-1]; } j++; } j=0; for (int i = bin_f2-search_range+1; i < bin_f2+search_range; i++) { if ((i >= 0) && (i < FFT_half_len)) { if (j==0) int_f2[0]=Math.sqrt(_fft[i]); else int_f2[j]=int_f2[j-1]+Math.sqrt(_fft[i]); } else { if (j==0) int_f2[0]=0; else int_f2[j]=int_f2[j-1]; } j++; } int midbin_1=0; int midbin_2=0; //look for midpoint of integration curve - where the middle of the peak is for (int i=0; i < 2*search_range -2; i++) { if ((midbin_1 ==0) && (int_f1[i] > int_f1[2*search_range -2]/2)) { midbin_1=i+bin_f1-search_range+1; } if ((midbin_2 ==0) && (int_f2[i] > int_f2[2*search_range -2]/2)) { midbin_2=i+bin_f2-search_range+1; } } //return -1 if one peak is somewhat less then the other //this indicates that carrier wasnt present in the fft range double peak = Math.max(int_f1[2*search_range -2], int_f2[2*search_range -2]); if ( int_f1[2*search_range -2] < peak/3 ) out[0] = (double)-1; else out[0] = (double)(midbin_1)/(2*FFT_half_len); if ( int_f2[2*search_range -2] < peak/3 ) out[1] = (double)-1; else out[1] = (double)(midbin_2)/(2*FFT_half_len); return out; } public String processBlock (double[] samples, int baud) { //TODO: consider writing samples as a class wide variable rather than passing to each method //TODO: search and afc both fft but dont share results _samples = samples; _fft_updated = false; samples_since_afc += _samples.length; samples_since_search += _samples.length; samples_since_fft += _samples.length; cc.samplesElapsed(samples.length); ConfidenceCalculator.State initialState = cc.state; if (auto_rtty_finding && cc.fullSearchDue()) { samples_since_fft = 0; double[] loc = find_fsk(false); boolean up = cc.putFrequencies(loc[0]/8000, loc[1]/8000); if (up){ decoder._f1 = cc.getFrequencies(0); decoder._f2 = cc.getFrequencies(1); } } if (enable_afc && samples_since_afc >= afc_update_freq) //step 2 : follow signal if afc is set { samples_since_fft = 0; samples_since_afc = 0; follow_fsk(initialState != ConfidenceCalculator.State.SIG_DROPPED); cc.AFCUpdate(decoder._f1,decoder._f2); } if (samples_since_fft >= fft_update_freq && enableFFT) { calcuate_FFT(); samples_since_fft = 0; } /* //step 1 : find rtty signal if needed if (auto_rtty_finding && current_state == State.INACTIVE && samples_since_search >= search_freq) { samples_since_fft = 0; samples_since_search = 0; double[] loc = findRTTY(true); if (loc[0] > 0) { current_state = State.FOUND_SIG; followRTTY(true); } } else if (enable_afc && samples_since_afc >= afc_update_freq) //step 2 : follow signal if afc is set { samples_since_fft = 0; samples_since_afc = 0; followRTTY(false); } if (samples_since_fft >= fft_update_freq) { calcuate_FFT(); samples_since_fft = 0; } */ //step 3 : demodulate the signal String str = ""; if (current_mode == Mode.RTTY) { boolean[] bits = decoder.processBlock_2bits(samples,baud); cc.putPowerLevels(decoder.getLastMaxPower(),decoder.getLastAveragePower()); //step 4 : convert a bitstream to telemetry boolean valid7 = false,valid8 = false; if (current_state == State.IDLE) //if data /stops are known then used fixed stops decoder too { bit2char_fixed.DataBits(current_data_bits); bit2char_fixed.StopBits(current_stop_bits); str = bit2char_fixed.bits2chars(bits); telem_hand_f.ExtractPacket(str); if (current_data_bits ==7) { str = bit2char_8.bits2chars(bits); valid8 = telem_hand_8.ExtractPacket(str); str = bit2char_7.bits2chars(bits); valid7 = telem_hand_7.ExtractPacket(str); } else { str = bit2char_7.bits2chars(bits); valid7 = telem_hand_7.ExtractPacket(str); str = bit2char_8.bits2chars(bits); valid8 = telem_hand_8.ExtractPacket(str); } if (cc.getState() == ConfidenceCalculator.State.SIG_DROPPED) current_state = State.INACTIVE; } else if (current_data_bits ==7) //if data / stops not known try 7nX and 8nX, returning the results of whatever setting was used last { str = bit2char_8.bits2chars(bits); valid8 = telem_hand_8.ExtractPacket(str); str = bit2char_7.bits2chars(bits); valid7 = telem_hand_7.ExtractPacket(str); } else { str = bit2char_7.bits2chars(bits); valid7 = telem_hand_7.ExtractPacket(str); str = bit2char_8.bits2chars(bits); valid8 = telem_hand_8.ExtractPacket(str); } //System.out.println(bit2char_7.Average_bit_period()); //at this stage, if valid7/8 is high, then the databits info is known, and the fixed extractor can be used if (valid7) { current_state = State.IDLE; cc.gotDecode(); current_data_bits = 7; current_stop_bits = (int) Math.round(bit2char_7.average_stop_bits()); } else if (valid8) { current_state = State.IDLE; cc.gotDecode(); current_data_bits = 8; current_stop_bits = (int) Math.round(bit2char_8.average_stop_bits()); } } else { double[] bits = decoder.processBlock_2bits_llr(samples,baud); boolean valid_bin = false; str = telem_hand_bin.bits2chars(bits); valid_bin = telem_hand_bin.get_last_valid(); //if (current_modulation == Modulation.AFSK) //{ if (cc.getState() == ConfidenceCalculator.State.SIG_DROPPED) current_state = State.INACTIVE; if (valid_bin) { cc.gotDecode(); current_state = State.IDLE; } //} } return str; } public void StringRx(byte[] str, boolean checksum, int length, int flags, int fixed) { if (_listeners.size() > 0) { fireStringReceived(str, checksum, length, flags, fixed); } } public void StringRx(String str, boolean checksum) { if (!last_sha.equals(str)) { last_sha = str; if (_listeners.size() > 0) { fireStringReceived(str, checksum); } } } public double[] get_fft() { return _fft; } public double get_fft(int i) { if (_fft == null) return 0; if (i < _fft.length) return _fft[i]; else return 0; } public int[] get_peaklocs() { return _peaklocs; } public double get_f1() { return decoder._f1; } public double get_f2() { return decoder._f2; } public void setFreq(double f1, double f2) { decoder.setFreq(f1, f2); } public boolean get_fft_updated() { return _fft_updated; } //IG_LOST,SIG_JUST_FOUND,SIG_TRACKING,SIG_DROPPED}; public String statusToString() { if (current_modulation == Modulation.AFSK) return "Tracking Signal"; switch (cc.state) { case SIG_LOST : return "Searching"; case SIG_JUST_FOUND : return "Found Signal"; case SIG_TRACKING : return "Tracking Signal"; case SIG_DROPPED : return "Dropped Signal"; default : return "Inactive"; } } public void setModulation(Modulation set) { current_modulation = set; if (current_modulation == Modulation.AFSK) { auto_rtty_finding = false; enable_afc = false; } else { auto_rtty_finding = true; enable_afc = true; } } public void setMode(Mode set) { current_mode = set; } public boolean paramsValid() { return (current_state == State.IDLE); } public void provide_binary_sync_helper(byte[] data, byte[] mask, String id, int len) { telem_hand_bin.provide_binary_sync_helper(data, mask, id, len); } }